refactor(app): migrate main + preload to TypeScript with typed seams (Phase 5d-3c-ii)

Convert the app's main and preload source from .js to .ts (git mv preserves
history), adding types where they carry value: the core-consumption seam
(BuildIndexOptions/BuildIndexResult, FileInfo), the indexer service/worker
factories, and the preload IPC bridge. Module-to-module specifiers use the
real .ts extension (mirroring scripts/, since Node type-stripping does not
rewrite .js->.ts); the worker's runtime path stays indexer-worker.js because
that is the built output.

Toolchain:
- Add app/tsconfig.json: strict but noImplicitAny:false (the app orchestrates
  the already-strict core; annotating every SQLite-handle helper is low-value
  churn) + allowImportingTsExtensions (safe under noEmit).
- Add @types/better-sqlite3 for the injected binding.
- electron.vite.config.ts inputs -> .ts; refresh the stale CommonJS comment.
- typecheck script runs root + app projects. Root tsconfig excludes the
  app-importing tests (app-*.test.mjs, recap-capture-query.test.mjs) so the
  lenient app files are not dragged into the strict root program; the app
  source is covered by app/tsconfig.json instead. See docs/adr/0005.

Verified: npm run typecheck (root + app) clean; suite 121/121; electron-vite
build emits all 6 main entries + preload with no .ts/node:sqlite residue in
the bundles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tommy0103
2026-07-09 16:25:59 +08:00
co-authored by Claude Opus 4.8
parent 60d47a852e
commit 01a390fa10
21 changed files with 299 additions and 151 deletions
+16 -15
View File
@@ -5,10 +5,10 @@ import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import Database from 'better-sqlite3';
import chokidar from 'chokidar';
import { writeHeartbeat } from './indexer.js';
import { createIndexerService } from './indexer-service.js';
import { createWorkerBuildIndex } from './indexer-worker-client.js';
import { buildRecapExportQuery } from './recap-capture-query.js';
import { writeHeartbeat } from './indexer.ts';
import { createIndexerService } from './indexer-service.ts';
import { createWorkerBuildIndex } from './indexer-worker-client.ts';
import { buildRecapExportQuery } from './recap-capture-query.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -68,7 +68,7 @@ function migrateLegacyDbIfNeeded(paths = getPathsForClaudeDir()) {
fs.mkdirSync(path.dirname(paths.dbPath), { recursive: true });
fs.copyFileSync(legacyDbPath, paths.dbPath);
} catch (error) {
console.warn?.(`Obelisk legacy DB migration skipped: ${error.message}`);
console.warn?.(`Obelisk legacy DB migration skipped: ${(error as Error).message}`);
}
}
@@ -111,7 +111,7 @@ function resolveSchemaPath() {
path.join(__dirname, 'schema.sql'),
path.join(__dirname, '..', 'scripts', 'schema.sql'),
process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null,
].filter(Boolean);
].filter((c): c is string => Boolean(c));
return candidates.find(p => fs.existsSync(p));
}
@@ -160,7 +160,7 @@ function openDb(dbPath = getPathsForClaudeDir().dbPath) {
return db;
}
function notifyIndexUpdated(result = {}) {
function notifyIndexUpdated(result: { affectedSessionIds?: unknown } = {}) {
const affectedSessionIds = Array.isArray(result.affectedSessionIds)
? [...new Set(result.affectedSessionIds.filter(Boolean))]
: [];
@@ -173,7 +173,7 @@ function notifyIndexUpdated(result = {}) {
}
}
function sourceWhereClause(opts = {}, column = 'source') {
function sourceWhereClause(opts: { includeCodex?: boolean; source?: string } = {}, column = "source"): { sql: string; params: unknown[] } {
if (opts.includeCodex || opts.source === 'all') return { sql: '', params: [] };
if (opts.source) return { sql: `COALESCE(${column}, 'claude') = ?`, params: [opts.source] };
return { sql: `COALESCE(${column}, 'claude') = 'claude'`, params: [] };
@@ -283,7 +283,7 @@ function createWindow() {
const OBELISK_DIR = path.join(os.homedir(), '.obelisk');
const RECAP_DIR = path.join(OBELISK_DIR, 'recap');
let obeliskWatcher = null;
let obeliskWatcher: import("chokidar").FSWatcher | null = null;
function startObeliskWatcher() {
if (obeliskWatcher) return obeliskWatcher;
@@ -340,7 +340,7 @@ ipcMain.handle('db:getSessions', (_, opts = {}) => {
if (!db) return [];
const { project, limit = 200 } = opts;
let sql = `SELECT id, title, project, project_path, started_at, ended_at, git_branch, version, message_count, jsonl_path, source FROM sessions`;
const params = [];
const params: unknown[] = [];
const sourceFilter = sourceWhereClause(opts);
if (sourceFilter.sql) {
sql = appendWhere(sql, params, sourceFilter.sql);
@@ -437,7 +437,7 @@ ipcMain.handle('db:getMessageFullText', (_, uuid) => {
if (!match) return null;
const rawThreadId = match[1];
const targetLine = Number(match[2]);
let jsonlPath = null;
let jsonlPath: string | null = null;
if (!msg.agent_id) {
jsonlPath = db.prepare('SELECT jsonl_path FROM sessions WHERE id=?').get(msg.session_id)?.jsonl_path || null;
}
@@ -469,7 +469,7 @@ ipcMain.handle('db:getMessageFullText', (_, uuid) => {
}
// Resolve JSONL path
let jsonlPath = null;
let jsonlPath: string | null = null;
if (msg.agent_id) {
const wa = db.prepare('SELECT agent_id, run_id, session_id FROM workflow_agents WHERE agent_id=?').get(msg.agent_id);
if (wa) {
@@ -501,7 +501,7 @@ ipcMain.handle('db:getMessageFullText', (_, uuid) => {
const content = obj.message?.content;
if (typeof content === 'string') return content;
if (!Array.isArray(content)) return null;
const parts = [];
const parts: string[] = [];
for (const b of content) {
if (b.type === 'text' && b.text) parts.push(b.text);
else if (b.type === 'thinking' && b.thinking) parts.push(b.thinking);
@@ -614,7 +614,7 @@ async function createExportCapture(parentWin, query) {
nodeIntegration: false,
offscreen: true,
deviceScaleFactor: 2,
},
} as Electron.WebPreferences,
});
const isDev = process.argv.includes('--dev') || !!process.env.ELECTRON_RENDERER_URL;
@@ -802,6 +802,7 @@ ipcMain.handle('settings:set', async (_, key, value) => {
ipcMain.handle('settings:browseFolder', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return null;
const { filePaths } = await dialog.showOpenDialog(win, {
properties: ['openDirectory'],
title: 'Select Claude Code data folder',
@@ -851,7 +852,7 @@ ipcMain.handle('settings:rebuildIndex', async () => {
try {
openDb(paths.dbPath);
} catch (error) {
console.warn?.(`Obelisk DB reopen after rebuild failed: ${error.message}`);
console.warn?.(`Obelisk DB reopen after rebuild failed: ${(error as Error).message}`);
}
}
if (shouldRestartWatcher) startIndexerService({ buildOnStart: false });
@@ -9,6 +9,34 @@ const DEFAULT_STABILITY_MS = 500;
const DEFAULT_HEARTBEAT_MS = 30000;
const DEFAULT_WATCH_RETRY_MS = 5000;
type TimerHandle = ReturnType<typeof setTimeout>;
interface Timers {
setTimeout: (fn: () => void, ms?: number) => TimerHandle;
clearTimeout: (handle: TimerHandle) => void;
setInterval?: (fn: () => void, ms?: number) => TimerHandle;
clearInterval?: (handle: TimerHandle) => void;
}
interface Watcher {
close(): unknown;
}
interface IndexerServiceOptions {
projectsDir?: string;
watchDirs?: string | string[];
debounceMs?: number;
stabilityMs?: number;
heartbeatMs?: number;
watchRetryMs?: number;
buildIndex?: (args: { reason?: string; changedPaths?: string[] }) => unknown;
writeHeartbeat?: () => void;
watchProjects?: (onChange: (changedPath: string) => void) => Watcher | null;
chokidar?: any;
timers?: Timers;
logger?: { warn?: (msg: string) => void };
}
function createIndexerService({
projectsDir = DEFAULT_PROJECTS_DIR,
watchDirs = [projectsDir],
@@ -27,13 +55,13 @@ function createIndexerService({
clearInterval,
},
logger = console,
} = {}) {
}: IndexerServiceOptions = {}) {
if (typeof buildIndex !== 'function') throw new Error('createIndexerService() requires buildIndex');
const watch = watchProjects || ((onChange) => {
const roots = [...new Set((Array.isArray(watchDirs) ? watchDirs : [watchDirs]).filter(Boolean))];
const existingRoots = roots.filter(root => fs.existsSync(root));
if (!existingRoots.length) return null;
const watchers = [];
const watchers: any[] = [];
const onFileChange = (filename) => {
const name = filename ? String(filename) : '';
if (!name || name.endsWith('.jsonl') || name.endsWith('.json')) onChange(name);
@@ -57,7 +85,7 @@ function createIndexerService({
.on('change', onFileChange)
.on('unlink', onFileChange)
.on('error', (error) => {
logger.warn?.(`Obelisk watcher failed: ${error.message}`);
logger.warn?.(`Obelisk watcher failed: ${(error as Error).message}`);
});
watchers.push(watcher);
}
@@ -68,19 +96,19 @@ function createIndexerService({
};
});
let buildTimer = null;
let stabilityTimer = null;
let heartbeatTimer = null;
let watchRetryTimer = null;
let watcher = null;
let buildTimer: TimerHandle | null = null;
let stabilityTimer: TimerHandle | null = null;
let heartbeatTimer: TimerHandle | null = null;
let watchRetryTimer: TimerHandle | null = null;
let watcher: Watcher | null = null;
let stopped = false;
let running = false;
let pending = false;
let lastReason = null;
let changedPaths = new Set();
let lastReason: string | null = null;
let changedPaths = new Set<string>();
let idlePromise = Promise.resolve();
const addChangedPath = (changedPath) => {
const addChangedPath = (changedPath?: string | string[]) => {
if (Array.isArray(changedPath)) {
for (const p of changedPath) addChangedPath(p);
return;
@@ -96,7 +124,7 @@ function createIndexerService({
return paths;
};
const runBuildNow = (reason = 'manual', paths = undefined) => {
const runBuildNow = (reason = "manual", paths: string[] | undefined = undefined) => {
addChangedPath(paths);
if (stopped) return idlePromise;
if (running) {
@@ -113,7 +141,7 @@ function createIndexerService({
.catch((error) => {
// A build in flight when the service is stopped (e.g. a manual rebuild
// tears down the worker) is a deliberate cancellation, not a failure.
if (!stopped) logger.warn?.(`Obelisk index build failed: ${error.message}`);
if (!stopped) logger.warn?.(`Obelisk index build failed: ${(error as Error).message}`);
})
.finally(() => {
running = false;
@@ -125,7 +153,7 @@ function createIndexerService({
return idlePromise;
};
const scheduleBuild = (reason = 'change', changedPath = undefined) => {
const scheduleBuild = (reason = "change", changedPath: string | undefined = undefined) => {
if (stopped) return;
addChangedPath(changedPath);
lastReason = reason;
@@ -165,7 +193,7 @@ function createIndexerService({
try {
writeHeartbeat();
} catch (error) {
logger.warn?.(`Obelisk heartbeat failed: ${error.message}`);
logger.warn?.(`Obelisk heartbeat failed: ${(error as Error).message}`);
}
}, heartbeatMs);
}
@@ -4,23 +4,41 @@ import { Worker } from 'node:worker_threads';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
interface WorkerMessage {
id: number;
result?: unknown;
error?: { message: string; stack?: string };
}
interface PendingBuild {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
}
interface WorkerBuildIndexOptions {
workerPath?: string;
WorkerImpl?: typeof Worker;
}
function createWorkerBuildIndex({
// indexer-worker.js is the built worker output emitted next to this module.
workerPath = path.join(__dirname, 'indexer-worker.js'),
WorkerImpl = Worker,
} = {}) {
let worker = null;
}: WorkerBuildIndexOptions = {}) {
let worker: Worker | null = null;
let nextId = 1;
const pending = new Map();
const pending = new Map<number, PendingBuild>();
const rejectPending = (error) => {
const rejectPending = (error: Error) => {
for (const { reject } of pending.values()) reject(error);
pending.clear();
};
const ensureWorker = () => {
const ensureWorker = (): Worker => {
if (worker) return worker;
worker = new WorkerImpl(workerPath, { type: 'module' });
worker.on('message', (message) => {
const active = new WorkerImpl(workerPath, { type: 'module' } as ConstructorParameters<typeof Worker>[1]);
worker = active;
active.on('message', (message: WorkerMessage) => {
const current = pending.get(message.id);
if (!current) return;
pending.delete(message.id);
@@ -32,18 +50,18 @@ function createWorkerBuildIndex({
current.resolve(message.result);
}
});
worker.on('error', (error) => {
active.on('error', (error: Error) => {
rejectPending(error);
worker = null;
});
worker.on('exit', (code) => {
active.on('exit', (code: number) => {
if (pending.size) rejectPending(new Error(`Indexer worker exited with code ${code}`));
worker = null;
});
return worker;
return active;
};
const buildIndex = (args = {}) => new Promise((resolve, reject) => {
const buildIndex = (args: Record<string, unknown> = {}) => new Promise((resolve, reject) => {
const id = nextId++;
pending.set(id, { resolve, reject });
ensureWorker().postMessage({ id, args });
-17
View File
@@ -1,17 +0,0 @@
import { parentPort } from 'node:worker_threads';
import { buildIndex } from './indexer.js';
parentPort.on('message', ({ id, args }) => {
try {
const result = buildIndex(args || {});
parentPort.postMessage({ id, result });
} catch (error) {
parentPort.postMessage({
id,
error: {
message: error.message,
stack: error.stack,
},
});
}
});
+20
View File
@@ -0,0 +1,20 @@
import { parentPort } from 'node:worker_threads';
import { buildIndex } from './indexer.ts';
if (!parentPort) throw new Error('indexer-worker must run as a worker thread');
const port = parentPort;
port.on('message', ({ id, args }: { id: number; args?: Record<string, unknown> }) => {
try {
const result = buildIndex(args || {});
port.postMessage({ id, result });
} catch (error) {
port.postMessage({
id,
error: {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
},
});
}
});
@@ -25,12 +25,22 @@ const DEFAULT_DB_PATH = path.join(DEFAULT_OBELISK_DIR, 'obelisk.sqlite');
const DEFAULT_PROJECTS_DIR = path.join(DEFAULT_CLAUDE_DIR, 'projects');
const DEFAULT_HISTORY_PATH = path.join(DEFAULT_CLAUDE_DIR, 'history.jsonl');
interface FileInfo {
path: string;
sessionId?: string;
project?: string;
isSubagent?: boolean;
agentId?: string;
workflowRunId?: string;
source?: string;
}
function resolveSchemaPath() {
const candidates = [
path.join(__dirname, 'schema.sql'),
path.join(__dirname, '..', '..', '..', 'scripts', 'schema.sql'),
process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null,
].filter(Boolean);
].filter((c): c is string => Boolean(c));
const found = candidates.find(p => fs.existsSync(p));
if (!found) throw new Error('Obelisk schema.sql not found');
return found;
@@ -41,7 +51,7 @@ function installSchema(db, schemaPath = resolveSchemaPath()) {
migrateDb(db);
}
function openIndexDb({ dbPath = DEFAULT_DB_PATH, schemaPath = resolveSchemaPath(), DatabaseImpl = Database } = {}) {
function openIndexDb({ dbPath = DEFAULT_DB_PATH, schemaPath = resolveSchemaPath(), DatabaseImpl = Database }: { dbPath?: string; schemaPath?: string; DatabaseImpl?: new (dbPath: string) => any } = {}) {
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const db = new DatabaseImpl(dbPath);
db.pragma('journal_mode = WAL');
@@ -104,7 +114,7 @@ function copyMemoriesFromDb(db, sourceDbPath) {
}
}
function discoverJsonlFiles({ projectsDir = DEFAULT_PROJECTS_DIR, changedPaths = undefined } = {}) {
function discoverJsonlFiles({ projectsDir = DEFAULT_PROJECTS_DIR, changedPaths = undefined }: { projectsDir?: string; changedPaths?: string[] } = {}) {
if (Array.isArray(changedPaths) && changedPaths.length) {
const changedFiles = discoverJsonlFilesForChanges({ projectsDir, changedPaths });
if (changedFiles.length) return changedFiles;
@@ -168,8 +178,8 @@ function dedupeFileInfos(files) {
return [...byPath.values()];
}
function discoverJsonlFilesForChanges({ projectsDir = DEFAULT_PROJECTS_DIR, changedPaths = [] } = {}) {
const files = [];
function discoverJsonlFilesForChanges({ projectsDir = DEFAULT_PROJECTS_DIR, changedPaths = [] }: { projectsDir?: string; changedPaths?: string[] } = {}) {
const files: FileInfo[] = [];
for (const changedPath of changedPaths) {
const info = jsonlFileInfoFromPath(projectsDir, changedPath);
if (info) files.push(info);
@@ -178,7 +188,7 @@ function discoverJsonlFilesForChanges({ projectsDir = DEFAULT_PROJECTS_DIR, chan
}
function discoverJsonlFilesFull({ projectsDir = DEFAULT_PROJECTS_DIR } = {}) {
const files = [];
const files: FileInfo[] = [];
if (!fs.existsSync(projectsDir)) return files;
let projects;
try { projects = fs.readdirSync(projectsDir); } catch { return files; }
@@ -219,7 +229,7 @@ function discoverJsonlFilesFull({ projectsDir = DEFAULT_PROJECTS_DIR } = {}) {
return files;
}
function discoverCodexJsonlFiles({ codexDir = DEFAULT_CODEX_DIR, changedPaths = undefined } = {}) {
function discoverCodexJsonlFiles({ codexDir = DEFAULT_CODEX_DIR, changedPaths = undefined }: { codexDir?: string; changedPaths?: string[] } = {}) {
if (Array.isArray(changedPaths) && changedPaths.length) {
const changedFiles = discoverCodexJsonlFilesForChanges({ codexDir, changedPaths });
if (changedFiles.length) return changedFiles;
@@ -244,8 +254,8 @@ function isPathInside(rootDir, candidate) {
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
}
function discoverCodexJsonlFilesForChanges({ codexDir = DEFAULT_CODEX_DIR, changedPaths = [] } = {}) {
const files = [];
function discoverCodexJsonlFilesForChanges({ codexDir = DEFAULT_CODEX_DIR, changedPaths = [] }: { codexDir?: string; changedPaths?: string[] } = {}) {
const files: FileInfo[] = [];
const sessionsDir = codexSessionsDir(codexDir);
for (const changedPath of changedPaths) {
const rootRelativePath = normalizeChangedPathForRoot(codexDir, changedPath);
@@ -255,7 +265,7 @@ function discoverCodexJsonlFilesForChanges({ codexDir = DEFAULT_CODEX_DIR, chang
}
const sessionRelativePath = normalizeChangedPathForRoot(sessionsDir, changedPath);
const fp = isPathInside(sessionsDir, rootRelativePath) ? rootRelativePath : sessionRelativePath;
if (!fp.endsWith('.jsonl') || !isPathInside(sessionsDir, fp)) continue;
if (!fp || !fp.endsWith(".jsonl") || !isPathInside(sessionsDir, fp)) continue;
if (!fs.existsSync(fp)) continue;
files.push({ path: fp, source: 'codex' });
}
@@ -264,11 +274,11 @@ function discoverCodexJsonlFilesForChanges({ codexDir = DEFAULT_CODEX_DIR, chang
function discoverCodexJsonlFilesFull({ codexDir = DEFAULT_CODEX_DIR } = {}) {
const root = codexSessionsDir(codexDir);
const files = [];
const files: FileInfo[] = [];
if (!fs.existsSync(root)) return files;
const stack = [root];
while (stack.length) {
const current = stack.pop();
const current = stack.pop()!;
let entries;
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
for (const entry of entries) {
@@ -308,7 +318,7 @@ function indexClaudeFile(db, file) {
}
function codexSessionMeta(filePath) {
let meta = null;
let meta: any = null;
readLines(filePath, (line) => {
let obj;
try { obj = JSON.parse(line); } catch { return; }
@@ -328,7 +338,7 @@ function indexCodexFile(db, file) {
if (!needed) {
if (guardian) {
persist(db, { key: file.path, sessionId: '' }, (function* () {
yield { kind: 'delete-session', sessionId: codexDbId(guardian.threadRawId) };
yield { kind: "delete-session", sessionId: codexDbId(guardian.threadRawId) as string };
return null;
})());
}
@@ -352,7 +362,7 @@ function indexCodexSessionIndex(db, { codexDir = DEFAULT_CODEX_DIR } = {}) {
db.prepare('UPDATE sessions SET title=COALESCE(title, ?), ended_at=COALESCE(ended_at, ?) WHERE id=? AND source=?')
.run(item.thread_name, item.updated_at || null, codexDbId(item.id), 'codex');
} catch (error) {
console.warn(`Warning: malformed Codex session index line: ${error.message}`);
console.warn(`Warning: malformed Codex session index line: ${(error as Error).message}`);
}
});
}
@@ -387,7 +397,7 @@ function indexSubagentMeta(db, fi) {
db.prepare('INSERT OR REPLACE INTO subagents VALUES(?,?,?,?,?,?,?)').run(fi.agentId, fi.sessionId, meta.toolUseId||null, meta.agentType||null, meta.description||null, dur, tok?.t||0);
}
} catch (error) {
console.warn(`Warning: failed to read subagent meta ${mp}: ${error.message}`);
console.warn(`Warning: failed to read subagent meta ${mp}: ${(error as Error).message}`);
}
}
@@ -423,7 +433,7 @@ function indexWorkflows(db, { projectsDir = DEFAULT_PROJECTS_DIR } = {}) {
item.durationMs||null, item.tokens||null, item.toolCalls||null, 'agent-' + item.agentId);
}
} catch (error) {
console.warn(`Warning: failed to index workflow ${f}: ${error.message}`);
console.warn(`Warning: failed to index workflow ${f}: ${(error as Error).message}`);
}
}
}
@@ -437,7 +447,7 @@ function indexHistory(db, { historyPath = DEFAULT_HISTORY_PATH } = {}) {
const o = JSON.parse(line);
if (o.sessionId && o.title) db.prepare('UPDATE sessions SET title=? WHERE id=? AND title IS NULL').run(o.title, o.sessionId);
} catch (error) {
console.warn(`Warning: malformed history line: ${error.message}`);
console.warn(`Warning: malformed history line: ${(error as Error).message}`);
}
});
}
@@ -488,6 +498,26 @@ function writeHeartbeat({ dbPath = DEFAULT_DB_PATH, DatabaseImpl = Database } =
}
}
interface BuildIndexOptions {
claudeDir?: string;
codexDir?: string;
projectsDir?: string;
historyPath?: string;
dbPath?: string;
schemaPath?: string;
DatabaseImpl?: new (dbPath: string) => any;
force?: boolean;
changedPaths?: string[];
preserveDbPath?: string | null;
}
interface BuildIndexResult {
files: number;
latestSourceMtime: number;
affectedSessionIds: string[];
ftsRebuilt: boolean;
}
function buildIndex({
claudeDir = DEFAULT_CLAUDE_DIR,
codexDir = path.join(path.dirname(claudeDir), '.codex'),
@@ -499,7 +529,7 @@ function buildIndex({
force = false,
changedPaths = undefined,
preserveDbPath = null,
} = {}) {
}: BuildIndexOptions = {}): BuildIndexResult {
const db = openIndexDb({ dbPath, schemaPath, DatabaseImpl });
let messageFtsTriggersDropped = false;
if (preserveDbPath && path.resolve(preserveDbPath) !== path.resolve(dbPath)) {
@@ -531,7 +561,7 @@ function buildIndex({
db.prepare("DELETE FROM workflows").run();
db.prepare("DELETE FROM workflow_agents").run();
}
const affectedSessionIds = new Set();
const affectedSessionIds = new Set<string>();
if (Array.isArray(changedPaths)) {
for (const changedPath of changedPaths) {
const sessionId = sessionIdFromChangedPath(projectsDir, changedPath);
@@ -547,7 +577,7 @@ function buildIndex({
db.exec('COMMIT');
} catch (error) {
db.exec('ROLLBACK');
console.warn(`Warning: failed to index ${file.path}: ${error.message}`);
console.warn(`Warning: failed to index ${file.path}: ${(error as Error).message}`);
}
}
db.exec('BEGIN');
@@ -575,7 +605,7 @@ function buildIndex({
try {
installSchema(db, schemaPath);
} catch (error) {
console.warn(`Warning: failed to restore message FTS triggers: ${error.message}`);
console.warn(`Warning: failed to restore message FTS triggers: ${(error as Error).message}`);
}
}
checkpointDb(db);
@@ -1,11 +1,17 @@
import path from 'node:path';
function cleanRecapFilename(filename) {
function cleanRecapFilename(filename?: string | null): string {
if (!filename) return '';
return path.basename(String(filename));
}
function buildRecapExportQuery({ cardIdx = 0, archetype = '', filename = '' } = {}) {
interface RecapExportQueryOptions {
cardIdx?: number | string;
archetype?: string;
filename?: string;
}
function buildRecapExportQuery({ cardIdx = 0, archetype = '', filename = '' }: RecapExportQueryOptions = {}): string {
const params = new URLSearchParams();
const cardNumber = Number(cardIdx);
params.set('card', Number.isFinite(cardNumber) ? String(cardNumber) : '0');
-46
View File
@@ -1,46 +0,0 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('obelisk', {
getSessions: (opts) => ipcRenderer.invoke('db:getSessions', opts),
getSessionMessages: (id) => ipcRenderer.invoke('db:getSessionMessages', id),
getSessionToolCalls: (id) => ipcRenderer.invoke('db:getSessionToolCalls', id),
getSessionToolResults: (id) => ipcRenderer.invoke('db:getSessionToolResults', id),
getSessionSubagents: (id) => ipcRenderer.invoke('db:getSessionSubagents', id),
getSessionWorkflows: (id) => ipcRenderer.invoke('db:getSessionWorkflows', id),
getSubagentMessages: (agentId) => ipcRenderer.invoke('db:getSubagentMessages', agentId),
getSubagentToolCalls: (agentId) => ipcRenderer.invoke('db:getSubagentToolCalls', agentId),
getSubagentToolResults: (agentId) => ipcRenderer.invoke('db:getSubagentToolResults', agentId),
getSessionSummaries: (id) => ipcRenderer.invoke('db:getSessionSummaries', id),
getMessageFullText: (uuid) => ipcRenderer.invoke('db:getMessageFullText', uuid),
getMemories: () => ipcRenderer.invoke('db:getMemories'),
readMemoryFile: (path) => ipcRenderer.invoke('db:readMemoryFile', path),
archiveMemory: (id, reason) => ipcRenderer.invoke('db:archiveMemory', id, reason),
restoreMemory: (id) => ipcRenderer.invoke('db:restoreMemory', id),
getProjects: () => ipcRenderer.invoke('db:getProjects'),
getStats: () => ipcRenderer.invoke('db:getStats'),
getUsageStats: () => ipcRenderer.invoke('db:getUsageStats'),
onIndexUpdated: (callback) => {
const listener = (_, payload) => callback(payload);
ipcRenderer.on('obelisk:index-updated', listener);
return () => ipcRenderer.removeListener('obelisk:index-updated', listener);
},
onSessionUpdated: (callback) => {
const listener = (_, payload) => callback(payload);
ipcRenderer.on('obelisk:session-updated', listener);
return () => ipcRenderer.removeListener('obelisk:session-updated', listener);
},
captureExport: (opts) => ipcRenderer.invoke('capture:export', opts),
copyImage: (opts) => ipcRenderer.invoke('capture:copy', opts),
recapList: () => ipcRenderer.invoke('recap:list'),
recapRead: (filename) => ipcRenderer.invoke('recap:read', filename),
onRecapUpdated: (callback) => {
const listener = (_, filePath) => callback(filePath);
ipcRenderer.on('obelisk:recap-updated', listener);
return () => ipcRenderer.removeListener('obelisk:recap-updated', listener);
},
getSettings: () => ipcRenderer.invoke('settings:get'),
browseFolder: () => ipcRenderer.invoke('settings:browseFolder'),
setSetting: (key, value) => ipcRenderer.invoke('settings:set', key, value),
revealPath: (p) => ipcRenderer.invoke('settings:revealPath', p),
rebuildIndex: () => ipcRenderer.invoke('settings:rebuildIndex'),
});
+46
View File
@@ -0,0 +1,46 @@
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron';
contextBridge.exposeInMainWorld('obelisk', {
getSessions: (opts?: unknown) => ipcRenderer.invoke('db:getSessions', opts),
getSessionMessages: (id: string) => ipcRenderer.invoke('db:getSessionMessages', id),
getSessionToolCalls: (id: string) => ipcRenderer.invoke('db:getSessionToolCalls', id),
getSessionToolResults: (id: string) => ipcRenderer.invoke('db:getSessionToolResults', id),
getSessionSubagents: (id: string) => ipcRenderer.invoke('db:getSessionSubagents', id),
getSessionWorkflows: (id: string) => ipcRenderer.invoke('db:getSessionWorkflows', id),
getSubagentMessages: (agentId: string) => ipcRenderer.invoke('db:getSubagentMessages', agentId),
getSubagentToolCalls: (agentId: string) => ipcRenderer.invoke('db:getSubagentToolCalls', agentId),
getSubagentToolResults: (agentId: string) => ipcRenderer.invoke('db:getSubagentToolResults', agentId),
getSessionSummaries: (id: string) => ipcRenderer.invoke('db:getSessionSummaries', id),
getMessageFullText: (uuid: string) => ipcRenderer.invoke('db:getMessageFullText', uuid),
getMemories: () => ipcRenderer.invoke('db:getMemories'),
readMemoryFile: (path: string) => ipcRenderer.invoke('db:readMemoryFile', path),
archiveMemory: (id: string, reason?: string) => ipcRenderer.invoke('db:archiveMemory', id, reason),
restoreMemory: (id: string) => ipcRenderer.invoke('db:restoreMemory', id),
getProjects: () => ipcRenderer.invoke('db:getProjects'),
getStats: () => ipcRenderer.invoke('db:getStats'),
getUsageStats: () => ipcRenderer.invoke('db:getUsageStats'),
onIndexUpdated: (callback: (payload: unknown) => void) => {
const listener = (_: IpcRendererEvent, payload: unknown) => callback(payload);
ipcRenderer.on('obelisk:index-updated', listener);
return () => ipcRenderer.removeListener('obelisk:index-updated', listener);
},
onSessionUpdated: (callback: (payload: unknown) => void) => {
const listener = (_: IpcRendererEvent, payload: unknown) => callback(payload);
ipcRenderer.on('obelisk:session-updated', listener);
return () => ipcRenderer.removeListener('obelisk:session-updated', listener);
},
captureExport: (opts?: unknown) => ipcRenderer.invoke('capture:export', opts),
copyImage: (opts?: unknown) => ipcRenderer.invoke('capture:copy', opts),
recapList: () => ipcRenderer.invoke('recap:list'),
recapRead: (filename: string) => ipcRenderer.invoke('recap:read', filename),
onRecapUpdated: (callback: (filePath: unknown) => void) => {
const listener = (_: IpcRendererEvent, filePath: unknown) => callback(filePath);
ipcRenderer.on('obelisk:recap-updated', listener);
return () => ipcRenderer.removeListener('obelisk:recap-updated', listener);
},
getSettings: () => ipcRenderer.invoke('settings:get'),
browseFolder: () => ipcRenderer.invoke('settings:browseFolder'),
setSetting: (key: string, value: unknown) => ipcRenderer.invoke('settings:set', key, value),
revealPath: (p: string) => ipcRenderer.invoke('settings:revealPath', p),
rebuildIndex: () => ipcRenderer.invoke('settings:rebuildIndex'),
});