refactor: improve type safety by removing 'as any' casts

Created database.ts with proper database result types and replaced 38+ 'as any' casts throughout the codebase with proper type annotations.

Changes:
- Created src/types/database.ts with TableColumnInfo, IndexInfo, and database record types
- Fixed all type casts in SessionStore.ts (migrations, query results)
- Fixed type casts in SessionSearch.ts, SettingsManager.ts, SettingsRoutes.ts
- Improved MCP server JSON schema typing

All builds pass and worker service runs successfully.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-12-07 21:41:40 -05:00
parent b1fb135d9c
commit 922f04e66a
9 changed files with 260 additions and 108 deletions
+1 -1
View File
@@ -383,7 +383,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: zodToJsonSchema(tool.inputSchema) as any
inputSchema: zodToJsonSchema(tool.inputSchema) as Record<string, unknown>
}))
};
});
+3 -2
View File
@@ -1,4 +1,5 @@
import Database from 'better-sqlite3';
import { TableNameRow } from '../../types/database.js';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
import {
ObservationSearchResult,
@@ -48,8 +49,8 @@ export class SessionSearch {
private ensureFTSTables(): void {
try {
// Check if FTS tables already exist
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts'").all() as any[];
const hasFTS = tables.some((t: any) => t.name === 'observations_fts' || t.name === 'session_summaries_fts');
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts'").all() as TableNameRow[];
const hasFTS = tables.some(t => t.name === 'observations_fts' || t.name === 'session_summaries_fts');
if (hasFTS) {
// Already migrated
+67 -56
View File
@@ -1,6 +1,17 @@
import Database from 'better-sqlite3';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
import { logger } from '../../utils/logger.js';
import {
TableColumnInfo,
IndexInfo,
TableNameRow,
SchemaVersion,
SdkSessionRecord,
ObservationRecord,
SessionSummaryRecord,
UserPromptRecord,
LatestPromptResult
} from '../../types/database.js';
/**
* Session data store for SDK sessions, observations, and summaries
@@ -47,7 +58,7 @@ export class SessionStore {
`);
// Get applied migrations
const appliedVersions = this.db.prepare('SELECT version FROM schema_versions ORDER BY version').all() as Array<{version: number}>;
const appliedVersions = this.db.prepare('SELECT version FROM schema_versions ORDER BY version').all() as SchemaVersion[];
const maxApplied = appliedVersions.length > 0 ? Math.max(...appliedVersions.map(v => v.version)) : 0;
// Only run migration004 if no migrations have been applied
@@ -131,12 +142,12 @@ export class SessionStore {
private ensureWorkerPortColumn(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(5) as {version: number} | undefined;
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(5) as SchemaVersion | undefined;
if (applied) return;
// Check if column exists
const tableInfo = this.db.pragma('table_info(sdk_sessions)');
const hasWorkerPort = (tableInfo as any[]).some((col: any) => col.name === 'worker_port');
const tableInfo = this.db.pragma('table_info(sdk_sessions)') as TableColumnInfo[];
const hasWorkerPort = tableInfo.some(col => col.name === 'worker_port');
if (!hasWorkerPort) {
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER');
@@ -156,12 +167,12 @@ export class SessionStore {
private ensurePromptTrackingColumns(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(6) as {version: number} | undefined;
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(6) as SchemaVersion | undefined;
if (applied) return;
// Check sdk_sessions for prompt_counter
const sessionsInfo = this.db.pragma('table_info(sdk_sessions)');
const hasPromptCounter = (sessionsInfo as any[]).some((col: any) => col.name === 'prompt_counter');
const sessionsInfo = this.db.pragma('table_info(sdk_sessions)') as TableColumnInfo[];
const hasPromptCounter = sessionsInfo.some(col => col.name === 'prompt_counter');
if (!hasPromptCounter) {
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0');
@@ -169,8 +180,8 @@ export class SessionStore {
}
// Check observations for prompt_number
const observationsInfo = this.db.pragma('table_info(observations)');
const obsHasPromptNumber = (observationsInfo as any[]).some((col: any) => col.name === 'prompt_number');
const observationsInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
const obsHasPromptNumber = observationsInfo.some(col => col.name === 'prompt_number');
if (!obsHasPromptNumber) {
this.db.exec('ALTER TABLE observations ADD COLUMN prompt_number INTEGER');
@@ -178,8 +189,8 @@ export class SessionStore {
}
// Check session_summaries for prompt_number
const summariesInfo = this.db.pragma('table_info(session_summaries)');
const sumHasPromptNumber = (summariesInfo as any[]).some((col: any) => col.name === 'prompt_number');
const summariesInfo = this.db.pragma('table_info(session_summaries)') as TableColumnInfo[];
const sumHasPromptNumber = summariesInfo.some(col => col.name === 'prompt_number');
if (!sumHasPromptNumber) {
this.db.exec('ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER');
@@ -199,12 +210,12 @@ export class SessionStore {
private removeSessionSummariesUniqueConstraint(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(7) as {version: number} | undefined;
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(7) as SchemaVersion | undefined;
if (applied) return;
// Check if UNIQUE constraint exists
const summariesIndexes = this.db.pragma('index_list(session_summaries)');
const hasUniqueConstraint = (summariesIndexes as any[]).some((idx: any) => idx.unique === 1);
const summariesIndexes = this.db.pragma('index_list(session_summaries)') as IndexInfo[];
const hasUniqueConstraint = summariesIndexes.some(idx => idx.unique === 1);
if (!hasUniqueConstraint) {
// Already migrated (no constraint exists)
@@ -284,12 +295,12 @@ export class SessionStore {
private addObservationHierarchicalFields(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(8) as {version: number} | undefined;
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(8) as SchemaVersion | undefined;
if (applied) return;
// Check if new fields already exist
const tableInfo = this.db.pragma('table_info(observations)');
const hasTitle = (tableInfo as any[]).some((col: any) => col.name === 'title');
const tableInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
const hasTitle = tableInfo.some(col => col.name === 'title');
if (hasTitle) {
// Already migrated
@@ -326,12 +337,12 @@ export class SessionStore {
private makeObservationsTextNullable(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(9) as {version: number} | undefined;
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(9) as SchemaVersion | undefined;
if (applied) return;
// Check if text column is already nullable
const tableInfo = this.db.pragma('table_info(observations)');
const textColumn = (tableInfo as any[]).find((col: any) => col.name === 'text');
const tableInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
const textColumn = tableInfo.find(col => col.name === 'text');
if (!textColumn || textColumn.notnull === 0) {
// Already migrated or text column doesn't exist
@@ -413,12 +424,12 @@ export class SessionStore {
private createUserPromptsTable(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(10) as {version: number} | undefined;
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(10) as SchemaVersion | undefined;
if (applied) return;
// Check if table already exists
const tableInfo = this.db.pragma('table_info(user_prompts)');
if ((tableInfo as any[]).length > 0) {
const tableInfo = this.db.pragma('table_info(user_prompts)') as TableColumnInfo[];
if (tableInfo.length > 0) {
// Already migrated
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(10, new Date().toISOString());
return;
@@ -502,12 +513,12 @@ export class SessionStore {
private ensureDiscoveryTokensColumn(): void {
try {
// Check if migration already applied to avoid unnecessary re-runs
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(11) as {version: number} | undefined;
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(11) as SchemaVersion | undefined;
if (applied) return;
// Check if discovery_tokens column exists in observations table
const observationsInfo = this.db.pragma('table_info(observations)');
const obsHasDiscoveryTokens = (observationsInfo as any[]).some((col: any) => col.name === 'discovery_tokens');
const observationsInfo = this.db.pragma('table_info(observations)') as TableColumnInfo[];
const obsHasDiscoveryTokens = observationsInfo.some(col => col.name === 'discovery_tokens');
if (!obsHasDiscoveryTokens) {
this.db.exec('ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
@@ -515,8 +526,8 @@ export class SessionStore {
}
// Check if discovery_tokens column exists in session_summaries table
const summariesInfo = this.db.pragma('table_info(session_summaries)');
const sumHasDiscoveryTokens = (summariesInfo as any[]).some((col: any) => col.name === 'discovery_tokens');
const summariesInfo = this.db.pragma('table_info(session_summaries)') as TableColumnInfo[];
const sumHasDiscoveryTokens = summariesInfo.some(col => col.name === 'discovery_tokens');
if (!sumHasDiscoveryTokens) {
this.db.exec('ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
@@ -556,7 +567,7 @@ export class SessionStore {
LIMIT ?
`);
return stmt.all(project, limit) as any[];
return stmt.all(project, limit);
}
/**
@@ -581,7 +592,7 @@ export class SessionStore {
LIMIT ?
`);
return stmt.all(project, limit) as any[];
return stmt.all(project, limit);
}
/**
@@ -601,7 +612,7 @@ export class SessionStore {
LIMIT ?
`);
return stmt.all(project, limit) as any[];
return stmt.all(project, limit);
}
/**
@@ -625,7 +636,7 @@ export class SessionStore {
LIMIT ?
`);
return stmt.all(limit) as any[];
return stmt.all(limit);
}
/**
@@ -655,7 +666,7 @@ export class SessionStore {
LIMIT ?
`);
return stmt.all(limit) as any[];
return stmt.all(limit);
}
/**
@@ -685,7 +696,7 @@ export class SessionStore {
LIMIT ?
`);
return stmt.all(limit) as any[];
return stmt.all(limit);
}
/**
@@ -728,7 +739,7 @@ export class SessionStore {
LIMIT 1
`);
return stmt.get(claudeSessionId) as any;
return stmt.get(claudeSessionId) as LatestPromptResult | undefined;
}
/**
@@ -760,7 +771,7 @@ export class SessionStore {
ORDER BY started_at_epoch ASC
`);
return stmt.all(project, limit) as any[];
return stmt.all(project, limit);
}
/**
@@ -779,20 +790,20 @@ export class SessionStore {
ORDER BY created_at_epoch ASC
`);
return stmt.all(sdkSessionId) as any[];
return stmt.all(sdkSessionId);
}
/**
* Get a single observation by ID
*/
getObservationById(id: number): any | null {
getObservationById(id: number): ObservationRecord | null {
const stmt = this.db.prepare(`
SELECT *
FROM observations
WHERE id = ?
`);
return stmt.get(id) as any || null;
return stmt.get(id) as ObservationRecord | undefined || null;
}
/**
@@ -801,7 +812,7 @@ export class SessionStore {
getObservationsByIds(
ids: number[],
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
): any[] {
): ObservationRecord[] {
if (ids.length === 0) return [];
const { orderBy = 'date_desc', limit } = options;
@@ -819,7 +830,7 @@ export class SessionStore {
${limitClause}
`);
return stmt.all(...ids) as any[];
return stmt.all(...ids) as ObservationRecord[];
}
/**
@@ -847,7 +858,7 @@ export class SessionStore {
LIMIT 1
`);
return stmt.get(sdkSessionId) as any || null;
return stmt.get(sdkSessionId) || null;
}
/**
@@ -920,7 +931,7 @@ export class SessionStore {
LIMIT 1
`);
return stmt.get(id) as any || null;
return stmt.get(id) || null;
}
/**
@@ -939,7 +950,7 @@ export class SessionStore {
LIMIT 1
`);
return stmt.get(claudeSessionId) as any || null;
return stmt.get(claudeSessionId) || null;
}
/**
@@ -953,7 +964,7 @@ export class SessionStore {
LIMIT 1
`);
return stmt.get(claudeSessionId) as any || null;
return stmt.get(claudeSessionId) || null;
}
/**
@@ -1343,7 +1354,7 @@ export class SessionStore {
getSessionSummariesByIds(
ids: number[],
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
): any[] {
): SessionSummaryRecord[] {
if (ids.length === 0) return [];
const { orderBy = 'date_desc', limit } = options;
@@ -1358,7 +1369,7 @@ export class SessionStore {
${limitClause}
`);
return stmt.all(...ids) as any[];
return stmt.all(...ids) as SessionSummaryRecord[];
}
/**
@@ -1368,7 +1379,7 @@ export class SessionStore {
getUserPromptsByIds(
ids: number[],
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
): any[] {
): UserPromptRecord[] {
if (ids.length === 0) return [];
const { orderBy = 'date_desc', limit } = options;
@@ -1388,7 +1399,7 @@ export class SessionStore {
${limitClause}
`);
return stmt.all(...ids) as any[];
return stmt.all(...ids) as UserPromptRecord[];
}
/**
@@ -1451,8 +1462,8 @@ export class SessionStore {
`;
try {
const beforeRecords = this.db.prepare(beforeQuery).all(anchorObservationId, ...projectParams, depthBefore + 1) as any[];
const afterRecords = this.db.prepare(afterQuery).all(anchorObservationId, ...projectParams, depthAfter + 1) as any[];
const beforeRecords = this.db.prepare(beforeQuery).all(anchorObservationId, ...projectParams, depthBefore + 1) as Array<{id: number; created_at_epoch: number}>;
const afterRecords = this.db.prepare(afterQuery).all(anchorObservationId, ...projectParams, depthAfter + 1) as Array<{id: number; created_at_epoch: number}>;
// Get the earliest and latest timestamps from boundary observations
if (beforeRecords.length === 0 && afterRecords.length === 0) {
@@ -1484,8 +1495,8 @@ export class SessionStore {
`;
try {
const beforeRecords = this.db.prepare(beforeQuery).all(anchorEpoch, ...projectParams, depthBefore) as any[];
const afterRecords = this.db.prepare(afterQuery).all(anchorEpoch, ...projectParams, depthAfter + 1) as any[];
const beforeRecords = this.db.prepare(beforeQuery).all(anchorEpoch, ...projectParams, depthBefore) as Array<{created_at_epoch: number}>;
const afterRecords = this.db.prepare(afterQuery).all(anchorEpoch, ...projectParams, depthAfter + 1) as Array<{created_at_epoch: number}>;
if (beforeRecords.length === 0 && afterRecords.length === 0) {
return { observations: [], sessions: [], prompts: [] };
@@ -1523,9 +1534,9 @@ export class SessionStore {
`;
try {
const observations = this.db.prepare(obsQuery).all(startEpoch, endEpoch, ...projectParams) as any[];
const sessions = this.db.prepare(sessQuery).all(startEpoch, endEpoch, ...projectParams) as any[];
const prompts = this.db.prepare(promptQuery).all(startEpoch, endEpoch, ...projectParams) as any[];
const observations = this.db.prepare(obsQuery).all(startEpoch, endEpoch, ...projectParams) as ObservationRecord[];
const sessions = this.db.prepare(sessQuery).all(startEpoch, endEpoch, ...projectParams) as SessionSummaryRecord[];
const prompts = this.db.prepare(promptQuery).all(startEpoch, endEpoch, ...projectParams) as UserPromptRecord[];
return {
observations,
+1 -1
View File
@@ -37,7 +37,7 @@ export class SettingsManager {
for (const row of rows) {
const key = row.key as keyof ViewerSettings;
if (key in settings) {
(settings as any)[key] = JSON.parse(row.value);
settings[key] = JSON.parse(row.value) as ViewerSettings[typeof key];
}
}
@@ -17,7 +17,9 @@ import {
OBSERVATION_TYPES,
OBSERVATION_CONCEPTS,
DEFAULT_OBSERVATION_TYPES_STRING,
DEFAULT_OBSERVATION_CONCEPTS_STRING
DEFAULT_OBSERVATION_CONCEPTS_STRING,
ObservationType,
ObservationConcept
} from '../../../../constants/observation-metadata.js';
export class SettingsRoutes {
@@ -348,7 +350,7 @@ export class SettingsRoutes {
if (settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES) {
const types = settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES.split(',').map((t: string) => t.trim());
for (const type of types) {
if (type && !OBSERVATION_TYPES.includes(type as any)) {
if (type && !OBSERVATION_TYPES.includes(type as ObservationType)) {
return { valid: false, error: `Invalid observation type: ${type}. Valid types: ${OBSERVATION_TYPES.join(', ')}` };
}
}
@@ -358,7 +360,7 @@ export class SettingsRoutes {
if (settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS) {
const concepts = settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS.split(',').map((c: string) => c.trim());
for (const concept of concepts) {
if (concept && !OBSERVATION_CONCEPTS.includes(concept as any)) {
if (concept && !OBSERVATION_CONCEPTS.includes(concept as ObservationConcept)) {
return { valid: false, error: `Invalid observation concept: ${concept}. Valid concepts: ${OBSERVATION_CONCEPTS.join(', ')}` };
}
}
+138
View File
@@ -0,0 +1,138 @@
/**
* TypeScript types for database query results
* Provides type safety for better-sqlite3 query results
*/
/**
* Schema information from sqlite3 PRAGMA table_info
*/
export interface TableColumnInfo {
cid: number;
name: string;
type: string;
notnull: number;
dflt_value: string | null;
pk: number;
}
/**
* Index information from sqlite3 PRAGMA index_list
*/
export interface IndexInfo {
seq: number;
name: string;
unique: number;
origin: string;
partial: number;
}
/**
* Table name from sqlite_master
*/
export interface TableNameRow {
name: string;
}
/**
* Schema version record
*/
export interface SchemaVersion {
version: number;
}
/**
* SDK Session database record
*/
export interface SdkSessionRecord {
id: number;
claude_session_id: string;
sdk_session_id: string | null;
project: string;
user_prompt: string | null;
started_at: string;
started_at_epoch: number;
completed_at: string | null;
completed_at_epoch: number | null;
status: 'active' | 'completed' | 'failed';
worker_port?: number;
prompt_counter?: number;
}
/**
* Observation database record
*/
export interface ObservationRecord {
id: number;
sdk_session_id: string;
project: string;
text: string | null;
type: 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change';
created_at: string;
created_at_epoch: number;
title?: string;
concept?: string;
source_files?: string;
prompt_number?: number;
discovery_tokens?: number;
}
/**
* Session Summary database record
*/
export interface SessionSummaryRecord {
id: number;
sdk_session_id: string;
project: string;
request: string | null;
investigated: string | null;
learned: string | null;
completed: string | null;
next_steps: string | null;
created_at: string;
created_at_epoch: number;
prompt_number?: number;
discovery_tokens?: number;
}
/**
* User Prompt database record
*/
export interface UserPromptRecord {
id: number;
claude_session_id: string;
prompt_number: number;
prompt_text: string;
created_at: string;
created_at_epoch: number;
}
/**
* Latest user prompt with session join
*/
export interface LatestPromptResult {
id: number;
claude_session_id: string;
sdk_session_id: string;
project: string;
prompt_number: number;
prompt_text: string;
created_at_epoch: number;
}
/**
* Observation with context (for time-based queries)
*/
export interface ObservationWithContext {
id: number;
sdk_session_id: string;
project: string;
text: string | null;
type: string;
created_at: string;
created_at_epoch: number;
title?: string;
concept?: string;
source_files?: string;
prompt_number?: number;
discovery_tokens?: number;
}