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:
co-authored by
Claude Opus 4.8
parent
60d47a852e
commit
01a390fa10
@@ -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 });
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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'),
|
||||
});
|
||||
@@ -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'),
|
||||
});
|
||||
Reference in New Issue
Block a user