@tekmidian/pai 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-route-tE6ymgYb.mjs +86 -0
- package/dist/auto-route-tE6ymgYb.mjs.map +1 -0
- package/dist/checkpoint-block-CkvwYA5y.mjs +1062 -0
- package/dist/checkpoint-block-CkvwYA5y.mjs.map +1 -0
- package/dist/cli/index.mjs +6 -5
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +6 -5
- package/dist/cli/program.mjs.map +1 -1
- package/dist/clusters-CRXU2T6Z.mjs +201 -0
- package/dist/clusters-CRXU2T6Z.mjs.map +1 -0
- package/dist/config-CcdkNSWa.mjs +204 -0
- package/dist/config-CcdkNSWa.mjs.map +1 -0
- package/dist/daemon/index.mjs +9 -9
- package/dist/daemon-DPHtKQB2.mjs +1576 -0
- package/dist/daemon-DPHtKQB2.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +2 -2
- package/dist/detector-BGw8SNWe.mjs +74 -0
- package/dist/detector-BGw8SNWe.mjs.map +1 -0
- package/dist/factory-1UI19STL.mjs +72 -0
- package/dist/factory-1UI19STL.mjs.map +1 -0
- package/dist/indexer-backend-B93q9ICi.mjs +299 -0
- package/dist/indexer-backend-B93q9ICi.mjs.map +1 -0
- package/dist/ipc-client-aVKVERjJ.mjs +152 -0
- package/dist/ipc-client-aVKVERjJ.mjs.map +1 -0
- package/dist/kg-entity-r8duqhi9.mjs +176 -0
- package/dist/kg-entity-r8duqhi9.mjs.map +1 -0
- package/dist/latent-ideas-CHrgdkSW.mjs +191 -0
- package/dist/latent-ideas-CHrgdkSW.mjs.map +1 -0
- package/dist/main-resolver-BozXCqpl.mjs +1340 -0
- package/dist/main-resolver-BozXCqpl.mjs.map +1 -0
- package/dist/neighborhood-jPQBkbB-.mjs +135 -0
- package/dist/neighborhood-jPQBkbB-.mjs.map +1 -0
- package/dist/note-context-D2S2sR38.mjs +126 -0
- package/dist/note-context-D2S2sR38.mjs.map +1 -0
- package/dist/pick-BRdJ4gpT.mjs +12666 -0
- package/dist/pick-BRdJ4gpT.mjs.map +1 -0
- package/dist/postgres-D96XWfwI.mjs +891 -0
- package/dist/postgres-D96XWfwI.mjs.map +1 -0
- package/dist/query-feedback-CdBJ2icj.mjs +76 -0
- package/dist/query-feedback-CdBJ2icj.mjs.map +1 -0
- package/dist/runtime-paths-B0P1TvUr.mjs +50 -0
- package/dist/runtime-paths-B0P1TvUr.mjs.map +1 -0
- package/dist/sqlite-CR9aMImG.mjs +271 -0
- package/dist/sqlite-CR9aMImG.mjs.map +1 -0
- package/dist/state-DTvy-jRB.mjs +102 -0
- package/dist/state-DTvy-jRB.mjs.map +1 -0
- package/dist/themes-HzMKPTas.mjs +148 -0
- package/dist/themes-HzMKPTas.mjs.map +1 -0
- package/dist/tools-DuorP_SS.mjs +1939 -0
- package/dist/tools-DuorP_SS.mjs.map +1 -0
- package/dist/trace-Bf7OprC5.mjs +137 -0
- package/dist/trace-Bf7OprC5.mjs.map +1 -0
- package/dist/vault-indexer-qAfX09RY.mjs +536 -0
- package/dist/vault-indexer-qAfX09RY.mjs.map +1 -0
- package/dist/work-queue-worker-Bov9eWMC.mjs +1856 -0
- package/dist/work-queue-worker-Bov9eWMC.mjs.map +1 -0
- package/dist/zettelkasten-DcZk-XSm.mjs +1063 -0
- package/dist/zettelkasten-DcZk-XSm.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint-block-CkvwYA5y.mjs","names":[],"sources":["../src/cli/commands/registry/utils.ts","../src/cli/commands/registry/scan.ts","../src/daemon/templates/triple-extraction-prompt.ts","../src/memory/kg-extraction.ts","../src/session/checkpoint-block.ts"],"sourcesContent":["/** Shared database helpers for registry command operations. */\n\nimport type { Database } from \"better-sqlite3\";\nimport { now } from \"../../utils.js\";\nimport { basename } from \"node:path\";\n\n/**\n * Upsert a project row. Returns { id, isNew }.\n *\n * Matching priority:\n * 1. root_path — most reliable; handles slug collisions\n * 2. encoded_dir — Claude project dirs are canonical\n * 3. Insert with suffix-deduplication on slug collision\n *\n * display_name is set to basename(rootPath) on INSERT so that the unified\n * listing always shows a human-readable name rather than the kebab-case slug.\n */\nexport function upsertProject(\n db: Database,\n slug: string,\n rootPath: string,\n encodedDir: string\n): { id: number; isNew: boolean } {\n const ts = now();\n\n const byPath = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(rootPath) as { id: number } | undefined;\n\n if (byPath) {\n const encodedOwner = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(encodedDir) as { id: number } | undefined;\n\n if (!encodedOwner || encodedOwner.id === byPath.id) {\n db.prepare(\n \"UPDATE projects SET encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(encodedDir, ts, byPath.id);\n }\n return { id: byPath.id, isNew: false };\n }\n\n const byEncoded = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(encodedDir) as { id: number } | undefined;\n\n if (byEncoded) {\n const pathOwner = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(rootPath) as { id: number } | undefined;\n\n if (!pathOwner || pathOwner.id === byEncoded.id) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(rootPath, ts, byEncoded.id);\n }\n return { id: byEncoded.id, isNew: false };\n }\n\n // Insert — deduplicate slug with numeric suffix if needed.\n let finalSlug = slug;\n let attempt = 0;\n while (true) {\n const conflict = db\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(finalSlug) as { id: number } | undefined;\n if (!conflict) break;\n attempt++;\n finalSlug = `${slug}-${attempt}`;\n }\n\n // Use basename(rootPath) as the human display name. If rootPath is empty or\n // just \"/\" fall back to the slug so we always have something non-empty.\n const displayName = basename(rootPath) || finalSlug;\n\n const result = db\n .prepare(\n `INSERT OR IGNORE INTO projects\n (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n )\n .run(finalSlug, displayName, rootPath, encodedDir, ts, ts);\n\n if (result.changes === 0) {\n const fallback =\n (db.prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\").get(encodedDir) as { id: number } | undefined) ??\n (db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(rootPath) as { id: number } | undefined);\n\n if (fallback) {\n return { id: fallback.id, isNew: false };\n }\n\n throw new Error(\n `upsertProject: INSERT OR IGNORE was suppressed but no matching row found ` +\n `for root_path=${rootPath} encoded_dir=${encodedDir}`\n );\n }\n\n return { id: result.lastInsertRowid as number, isNew: true };\n}\n\n/** Upsert a session note. Returns true if newly inserted. */\nexport function upsertSession(\n db: Database,\n projectId: number,\n number: number,\n date: string,\n slug: string,\n title: string,\n filename: string\n): boolean {\n const existing = db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n .get(projectId, number);\n\n if (existing) return false;\n\n const ts = now();\n db.prepare(\n `INSERT INTO sessions\n (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, 'completed', ?)`\n ).run(projectId, number, date, slug, title, filename, ts);\n\n return true;\n}\n","/** Registry scan command: walk ~/.claude/projects/ and populate the registry. */\n\nimport { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport { join, basename, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { ok, warn, err, dim, bold } from \"../../utils.js\";\nimport { encodeDir } from \"../../utils.js\";\nimport { decodeEncodedDir, slugify, parseSessionFilename, buildEncodedDirMap } from \"../../../registry/migrate.js\";\nimport { ensurePaiMarker, discoverPaiMarkers } from \"../../../registry/pai-marker.js\";\nimport { upsertProject, upsertSession } from \"./utils.js\";\nimport type { Database } from \"better-sqlite3\";\n\n// ---------------------------------------------------------------------------\n// clc session.json fallback map\n// ---------------------------------------------------------------------------\n\n/**\n * Build a reverse map from encoded-dir → real path using the clc session\n * registry (~/.claude/session.json).\n *\n * The clc registry stores { sessions: [{ directory, ... }, ...] }. For each\n * entry with a resolvable directory, we encode the realpath and add it to\n * the map. This lets the scanner recover project paths that are not in\n * Claude's session-registry.json but were registered by clc.\n */\nfunction buildClcDirMap(): Map<string, string> {\n const map = new Map<string, string>();\n const sessionFile = join(homedir(), \".claude\", \"session.json\");\n if (!existsSync(sessionFile)) return map;\n\n try {\n const raw = readFileSync(sessionFile, \"utf8\");\n const parsed = JSON.parse(raw) as { sessions?: Array<{ directory?: string }> };\n for (const entry of parsed.sessions ?? []) {\n const dir = entry.directory;\n if (!dir) continue;\n try {\n const real = realpathSync(dir);\n if (!existsSync(real)) continue;\n const encoded = encodeDir(real);\n map.set(encoded, real);\n } catch {\n // Unresolvable path — skip\n }\n }\n } catch {\n // Unparseable — return empty map\n }\n return map;\n}\n\n// ---------------------------------------------------------------------------\n// Config helpers\n// ---------------------------------------------------------------------------\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\nconst PAI_CONFIG_DIR = join(homedir(), \".pai\");\nconst PAI_CONFIG_FILE = join(PAI_CONFIG_DIR, \"config.json\");\n\ninterface PaiConfig {\n scan_dirs: string[];\n}\n\nexport function loadScanConfig(): PaiConfig {\n if (!existsSync(PAI_CONFIG_FILE)) return { scan_dirs: [] };\n try {\n return JSON.parse(readFileSync(PAI_CONFIG_FILE, \"utf8\")) as PaiConfig;\n } catch {\n return { scan_dirs: [] };\n }\n}\n\nexport function saveScanConfig(config: PaiConfig): void {\n mkdirSync(PAI_CONFIG_DIR, { recursive: true });\n writeFileSync(PAI_CONFIG_FILE, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\");\n}\n\n/**\n * Resolve a path to its canonical form, falling back to the input.\n *\n * Every path that becomes a `projects.root_path` must go through this. That\n * column is UNIQUE, so two spellings of one directory create two projects and\n * split session history between them. The spellings are not hypothetical: a\n * configured scan_dir of `~/dev/ai` (where `~/dev` is a symlink) makes every\n * project under it register under the symlinked path, while a session started\n * from the resolved path registers a rival row.\n */\nexport function canonicalPath(p: string): string {\n try {\n return realpathSync(p);\n } catch {\n return p;\n }\n}\n\nexport function resolveHome(p: string): string {\n if (p.startsWith(\"~/\")) return join(homedir(), p.slice(2));\n return resolve(p);\n}\n\n// ---------------------------------------------------------------------------\n// File discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively find all .md files in a directory, including YYYY/MM subdirectories.\n * Returns filenames (basename only).\n */\nexport function findNoteFiles(dir: string): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(entry.name);\n } else if (entry.isDirectory() && /^\\d{4}$/.test(entry.name)) {\n const yearDir = join(dir, entry.name);\n for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) {\n if (monthEntry.isDirectory() && /^\\d{2}$/.test(monthEntry.name)) {\n const monthDir = join(yearDir, monthEntry.name);\n for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) {\n if (noteEntry.isFile() && noteEntry.name.endsWith(\".md\")) {\n results.push(noteEntry.name);\n }\n }\n }\n }\n }\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Scan result type\n// ---------------------------------------------------------------------------\n\nexport interface ScanResult {\n projectsScanned: number;\n projectsNew: number;\n projectsUpdated: number;\n sessionsScanned: number;\n sessionsNew: number;\n skipped: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Core scan logic\n// ---------------------------------------------------------------------------\n\nexport function performScan(db: Database): ScanResult {\n const result: ScanResult = {\n projectsScanned: 0,\n projectsNew: 0,\n projectsUpdated: 0,\n sessionsScanned: 0,\n sessionsNew: 0,\n skipped: [],\n };\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n throw new Error(`Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`);\n }\n\n const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {\n const full = join(CLAUDE_PROJECTS_DIR, name);\n return statSync(full).isDirectory();\n });\n\n const lookupMap = buildEncodedDirMap();\n const clcMap = buildClcDirMap();\n\n for (const encodedDir of entries) {\n let rootPath = decodeEncodedDir(encodedDir, lookupMap);\n\n // If the decoded path doesn't exist, try the clc session.json fallback map.\n // The clc map is keyed by encodeDir(realpathSync(directory)), so paths that\n // were registered by clc but not in session-registry.json can be recovered.\n if (!existsSync(rootPath) && clcMap.has(encodedDir)) {\n const clcPath = clcMap.get(encodedDir)!;\n if (existsSync(clcPath)) {\n rootPath = clcPath;\n }\n }\n\n if (!existsSync(rootPath)) {\n result.skipped.push(`${encodedDir} (decoded: ${rootPath} — path not found on disk)`);\n result.projectsScanned++;\n continue;\n }\n\n // Canonicalize before upserting.\n //\n // ~/.claude/projects/ holds one encoded entry per path *spelling*, so a\n // directory reachable through a symlinked prefix (~/dev -> Cloud/Development)\n // appears twice. Decoding each to its literal path registers two projects\n // for one directory, and session history then splits between them —\n // whichever spelling the shell used gets the session.\n //\n // upsertProject looks up by root_path first, so resolving here makes the\n // second spelling land on the row the first one created instead of\n // creating a rival. Both encoded_dir values stay valid for locating\n // central notes; only the project identity is unified.\n rootPath = canonicalPath(rootPath);\n\n const slug = slugify(basename(rootPath) || encodedDir);\n const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);\n\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try {\n ensurePaiMarker(rootPath, slug);\n } catch {\n // Non-fatal\n }\n\n const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, \"Notes\");\n\n if (existsSync(claudeNotesDir)) {\n const rootNotesDir = join(rootPath, \"Notes\");\n if (claudeNotesDir !== rootNotesDir) {\n db.prepare(\n \"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(claudeNotesDir, Date.now(), id);\n }\n }\n\n if (!existsSync(claudeNotesDir)) continue;\n\n const noteFiles = findNoteFiles(claudeNotesDir);\n\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n\n // Phase 2: Scan project-root Notes/ for all registered active projects\n {\n const activeProjects = db\n .prepare(\"SELECT id, slug, root_path FROM projects WHERE status = 'active'\")\n .all() as { id: number; slug: string; root_path: string }[];\n\n for (const project of activeProjects) {\n const notesDir = join(project.root_path, \"Notes\");\n if (!existsSync(notesDir)) continue;\n\n let files: string[];\n try {\n files = findNoteFiles(notesDir);\n } catch {\n continue;\n }\n\n for (const filename of files) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = upsertSession(db, project.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n }\n\n // Phase 3: Scan extra directories from config\n const config = loadScanConfig();\n if (config.scan_dirs.length) {\n for (const rawDir of config.scan_dirs) {\n const scanDir = resolveHome(rawDir);\n if (!existsSync(scanDir)) {\n result.skipped.push(`${rawDir} (configured scan_dir not found)`);\n continue;\n }\n\n const children = readdirSync(scanDir).filter((name) => {\n if (name.startsWith(\".\")) return false;\n const full = join(scanDir, name);\n try { return statSync(full).isDirectory(); } catch { return false; }\n });\n\n for (const child of children) {\n // Canonicalize: a scan_dir of \"~/dev/ai\" (symlinked prefix) would\n // otherwise register every project under the symlinked spelling and\n // duplicate anything already registered under the resolved one.\n const childPath = canonicalPath(join(scanDir, child));\n const childSlug = slugify(child);\n const childEncoded = encodeDir(childPath);\n\n const existing = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(childPath) as { id: number } | undefined;\n\n if (existing) {\n result.projectsScanned++;\n result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (upsertSession(db, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n continue;\n }\n\n const { id, isNew } = upsertProject(db, childSlug, childPath, childEncoded);\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n }\n }\n }\n\n // Phase 4: Discover PAI.md markers in scan_dirs\n if (config.scan_dirs.length) {\n const resolvedScanDirs = config.scan_dirs.map(resolveHome).filter(existsSync);\n const markers = discoverPaiMarkers(resolvedScanDirs);\n\n for (const marker of markers) {\n const registeredRow = db\n .prepare(\"SELECT id, root_path, slug FROM projects WHERE slug = ?\")\n .get(marker.slug) as { id: number; root_path: string; slug: string } | undefined;\n\n if (!registeredRow) continue;\n\n // The marker was found by walking a scan_dir, so its projectRoot carries\n // whatever spelling that dir used. Canonicalize before comparing —\n // otherwise this \"repair\" step rewrites a correct root_path back to the\n // symlinked form on every scan, undoing any deduplication.\n const markerRoot = canonicalPath(marker.projectRoot);\n\n if (registeredRow.root_path !== markerRoot) {\n const newEncoded = encodeDir(markerRoot);\n const now4 = Date.now();\n\n const encodedOwner = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(newEncoded) as { id: number } | undefined;\n const pathOwner = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(markerRoot) as { id: number } | undefined;\n\n const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;\n const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;\n\n if (encodedSafe && pathSafe) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(markerRoot, newEncoded, now4, registeredRow.id);\n } else if (pathSafe) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(markerRoot, now4, registeredRow.id);\n }\n }\n }\n }\n\n // Phase 5: Backfill display_name for rows where it still equals the slug.\n // This converts legacy entries created before the basename-based naming was\n // introduced: \"jobs-grazyna\" → \"Jobs Grazyna\" (from basename of root_path).\n {\n const stale = db\n .prepare(\"SELECT id, slug, root_path FROM projects WHERE display_name = slug AND root_path IS NOT NULL AND root_path != ''\")\n .all() as { id: number; slug: string; root_path: string }[];\n\n for (const row of stale) {\n const name = basename(row.root_path);\n if (name && name !== row.slug) {\n db.prepare(\"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\")\n .run(name, Date.now(), row.id);\n }\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// cmdScan\n// ---------------------------------------------------------------------------\n\n/**\n * Run the registry scan CLI command.\n *\n * @param opts.quick When true, skips verbose output (same scan, less noise).\n * The underlying scan is always incremental via upsert —\n * this flag exists for hook/daemon-triggered invocations that\n * want minimal log output.\n */\nexport function cmdScan(db: Database, opts: { quick?: boolean } = {}): void {\n const config = loadScanConfig();\n if (!opts.quick) {\n console.log(dim(\"Scanning ~/.claude/projects/ ...\"));\n if (config.scan_dirs.length) {\n console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(\", \")}`));\n }\n console.log(dim(\"Scanning project-root Notes/ directories ...\"));\n }\n\n let result: ScanResult;\n try {\n result = performScan(db);\n } catch (e) {\n console.error(err(String(e)));\n process.exit(1);\n }\n\n if (!opts.quick) {\n console.log(\n ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`)\n );\n console.log(dim(` Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`));\n console.log(dim(` Sessions: ${result.sessionsNew} new`));\n\n if (result.skipped.length) {\n console.log();\n console.log(warn(` ${result.skipped.length} project(s) skipped (path not found on disk):`));\n for (const s of result.skipped.slice(0, 10)) {\n console.log(dim(` ${s}`));\n }\n if (result.skipped.length > 10) {\n console.log(dim(` ... and ${result.skipped.length - 10} more`));\n }\n }\n } else {\n // Quick mode: single compact line (stderr so it doesn't pollute pipe output)\n process.stderr.write(\n `[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions ` +\n `(${result.projectsNew}+${result.sessionsNew} new).\\n`\n );\n }\n}\n","/**\n * triple-extraction-prompt.ts — Prompt template for KG triple extraction.\n *\n * Used by the session-summary-worker to extract structured facts from\n * a completed session summary and store them in the temporal knowledge graph.\n */\n\nexport function buildTripleExtractionPrompt(params: {\n sessionContent: string;\n projectSlug: string;\n gitLog: string;\n}): string {\n return `Extract structured entities and relations from this coding session.\n\nOutput a single JSON object with two arrays: \"entities\" and \"relations\".\n\nEntity types: project | person | concept | tool | file | version | decision | technology | organization\n\nRules:\n- Be SPECIFIC: entity names must be concrete (e.g., \"FSRS\", \"Glidr\", \"Matthias\")\n- Use snake_case relation verb phrases (e.g., \"uses_algorithm\", \"decided_to\", \"shipped_version\")\n- Skip opinions, speculation, and \"we should\" statements\n- Skip entities obvious from project metadata unless they have a meaningful relation\n- Maximum 15 relations per session — pick the most important\n- Each entity should have a brief description (1 sentence, what it is in this context)\n- Each relation must reference entity names that appear in the entities array\n\nExample output:\n{\n \"entities\": [\n {\"name\": \"Glidr\", \"type\": \"project\", \"description\": \"Flashcard app using FSRS spaced repetition\"},\n {\"name\": \"FSRS\", \"type\": \"concept\", \"description\": \"Free Spaced Repetition Scheduler algorithm\"},\n {\"name\": \"Matthias\", \"type\": \"person\", \"description\": \"Developer of Glidr and Quassl\"},\n {\"name\": \"Quassl\", \"type\": \"project\", \"description\": \"iOS app being rewritten in Flutter\"},\n {\"name\": \"Flutter\", \"type\": \"technology\", \"description\": \"Cross-platform mobile framework\"}\n ],\n \"relations\": [\n {\"source\": \"Glidr\", \"relation\": \"uses_algorithm\", \"target\": \"FSRS\"},\n {\"source\": \"Glidr\", \"relation\": \"shipped_version\", \"target\": \"1.0.5\"},\n {\"source\": \"Matthias\", \"relation\": \"decided_to_rewrite\", \"target\": \"Quassl\"},\n {\"source\": \"Quassl\", \"relation\": \"migrating_to\", \"target\": \"Flutter\"}\n ]\n}\n\nPROJECT: ${params.projectSlug}\n\nSESSION CONTENT:\n${params.sessionContent}\n\nGIT COMMITS:\n${params.gitLog}\n\nJSON object (entities + relations):`;\n}\n","/**\n * kg-extraction.ts — Shared KG triple extraction logic.\n *\n * Extracted from session-summary-worker.ts so both the worker and the\n * CLI backfill (`pai kg backfill`) can use the same code path.\n *\n * Provides:\n * - findClaudeBinary() — locate the claude CLI\n * - spawnClaude() — generic prompt -> response runner (strips ANTHROPIC_API_KEY)\n * - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Pool } from \"pg\";\nimport type { Database } from \"better-sqlite3\";\n\nimport { buildTripleExtractionPrompt } from \"../daemon/templates/triple-extraction-prompt.js\";\nimport { kgAdd, kgQuery, kgInvalidate } from \"./kg.js\";\nimport { upsertKgEntity } from \"./kg-entity.js\";\n\n// ---------------------------------------------------------------------------\n// Claude CLI binary discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary. Checks common installation locations first\n * (launchd PATH is minimal so bare \"claude\" often won't resolve).\n */\nexport function findClaudeBinary(): string | null {\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n return \"claude\";\n}\n\nconst CLAUDE_TIMEOUT_MS: Record<string, number> = {\n haiku: 60_000,\n sonnet: 120_000,\n opus: 300_000,\n};\n\n/**\n * Spawn the claude CLI with a prompt on stdin and return stdout.\n *\n * IMPORTANT: ANTHROPIC_API_KEY is stripped from the spawned environment so\n * the CLI uses the user's Max plan (free) instead of billing the API key.\n */\nexport async function spawnClaude(\n prompt: string,\n model: \"haiku\" | \"sonnet\" | \"opus\" = \"sonnet\"\n): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\"[kg-extraction] claude CLI not found.\\n\");\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const { ANTHROPIC_API_KEY: _drop, ...envWithoutApiKey } = process.env;\n const child = spawn(\n claudeBin,\n [\"--model\", model, \"-p\", \"--no-session-persistence\"],\n { env: envWithoutApiKey, stdio: [\"pipe\", \"pipe\", \"pipe\"] }\n );\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n child.stderr.on(\"data\", (chunk: Buffer) => { stderr += chunk.toString(); });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[kg-extraction] ${model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[kg-extraction] ${model} exited ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n timer = setTimeout(() => {\n process.stderr.write(`[kg-extraction] ${model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, CLAUDE_TIMEOUT_MS[model] ?? 120_000);\n\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Triple extraction\n// ---------------------------------------------------------------------------\n\nexport interface ExtractTriplesParams {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog?: string;\n model?: \"haiku\" | \"sonnet\" | \"opus\";\n /** Optional federation SQLite db — when provided, entities are upserted into kg_entities (QW1) */\n federationDb?: Database;\n /** Tenant ID for multi-tenant entity scoping (default: \"default\") */\n tenantId?: string;\n}\n\nexport interface ExtractTriplesResult {\n extracted: number;\n added: number;\n superseded: number;\n}\n\n/**\n * Extract structured KG triples from a session summary and store them in\n * Postgres. Idempotent: if a (subject, predicate) pair already has the same\n * object, no new row is added; if the object differs, the old triple is\n * invalidated (valid_to = NOW()) and a new one is inserted.\n *\n * Best-effort: per-triple errors are caught and logged but never thrown.\n * Returns a small stats object so callers can report progress.\n */\nexport async function extractAndStoreTriples(\n pool: Pool,\n params: ExtractTriplesParams\n): Promise<ExtractTriplesResult> {\n const stats: ExtractTriplesResult = { extracted: 0, added: 0, superseded: 0 };\n\n const prompt = buildTripleExtractionPrompt({\n sessionContent: params.summaryText,\n projectSlug: params.projectSlug,\n gitLog: params.gitLog ?? \"\",\n });\n\n const jsonOutput = await spawnClaude(prompt, params.model ?? \"sonnet\");\n if (!jsonOutput) return stats;\n\n // Strip markdown code fences if Claude wrapped the JSON\n const cleaned = jsonOutput\n .replace(/^```json\\s*/m, \"\")\n .replace(/^```\\s*/m, \"\")\n .replace(/\\s*```$/m, \"\")\n .trim();\n\n // Support both legacy array format and new structured format\n type LegacyTriple = { subject: string; predicate: string; object: string };\n type NewRelation = { source: string; relation: string; target: string };\n type NewEntity = { name: string; type: string; description: string };\n type NewFormat = { entities: NewEntity[]; relations: NewRelation[] };\n\n let triples: Array<LegacyTriple>;\n try {\n const parsed = JSON.parse(cleaned);\n\n if (Array.isArray(parsed)) {\n // Legacy format: [{subject, predicate, object}]\n triples = parsed;\n } else if (parsed && typeof parsed === \"object\" && Array.isArray(parsed.relations)) {\n // New structured format: {entities: [...], relations: [...]}\n const newFmt = parsed as NewFormat;\n\n // QW1: Upsert entities into federation SQLite kg_entities table when db is available\n if (params.federationDb && Array.isArray(newFmt.entities)) {\n const tenantId = params.tenantId ?? \"default\";\n for (const entity of newFmt.entities) {\n if (!entity.name) continue;\n try {\n upsertKgEntity(params.federationDb, {\n name: entity.name,\n type: entity.type ?? \"unknown\",\n description: entity.description,\n tenantId,\n });\n } catch (entityErr) {\n process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\\n`);\n }\n }\n }\n\n triples = newFmt.relations.map((r: NewRelation) => ({\n subject: r.source,\n predicate: r.relation,\n object: r.target,\n }));\n } else {\n process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\\n`);\n return stats;\n }\n } catch (e) {\n process.stderr.write(`[kg-extraction] JSON parse failed: ${e}\\n`);\n return stats;\n }\n\n if (!Array.isArray(triples)) return stats;\n stats.extracted = triples.length;\n\n for (const t of triples) {\n if (!t.subject || !t.predicate || !t.object) continue;\n\n try {\n const existing = await kgQuery(pool, {\n subject: t.subject,\n predicate: t.predicate,\n project_id: params.projectId ?? undefined,\n });\n\n // If an identical (subject, predicate, object) is already valid, skip — idempotent\n const alreadyValid = existing.find((e) => e.object === t.object && !e.valid_to);\n if (alreadyValid) continue;\n\n // Invalidate any superseded triple (same subject+predicate, different object)\n const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);\n if (supersedes) {\n await kgInvalidate(pool, supersedes.id);\n stats.superseded++;\n }\n\n await kgAdd(pool, {\n subject: t.subject,\n predicate: t.predicate,\n object: t.object,\n project_id: params.projectId ?? undefined,\n source_session: params.sessionId,\n confidence: \"EXTRACTED\",\n });\n stats.added++;\n } catch (tripleErr) {\n process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\\n`);\n }\n }\n\n return stats;\n}\n","/**\n * Shared \"## Continue\" checkpoint logic for a project's TODO.md.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * Before this, `pause.ts` and `handover.ts` each carried their own copy of\n * findProjectTodo / stripContinueSection / block-builder. Both wrote the same\n * fixed four-line block, and both stripped any existing ## Continue section\n * unconditionally. The consequence was that a rich, model-authored checkpoint\n * could never survive:\n *\n * 1. `pai pause` had no way to accept a body, so the model printed its\n * checkpoint to the terminal and it was lost.\n * 2. Even if a body had been written by hand, the session-stop hook runs\n * `pai session handover` on every clean exit, which regenerated the\n * generic block and erased it.\n *\n * So there are two jobs here:\n *\n * - AUTHORED writes (`pai pause --body-file`) carry the model's markdown\n * verbatim, wrapped in explicit start/end markers.\n * - AUTO writes (hooks) must never destroy an authored checkpoint belonging\n * to the *same* session. They may replace a stale one left by an earlier\n * session, otherwise TODO.md would show a checkpoint that no longer\n * describes where the work stands.\n *\n * PARSING\n * -------\n * A rich body can legitimately contain `---` rules and `##` headings, which the\n * old heuristic scanner treated as section terminators. Authored blocks are\n * therefore delimited by an explicit HTML-comment pair; the legacy heuristic is\n * kept only as a fallback for blocks written before this change.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n readdirSync,\n renameSync,\n mkdirSync,\n realpathSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Locations searched for a project TODO.md, in priority order. */\nexport const TODO_LOCATIONS = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n];\n\nexport const MARKER_OPEN = \"<!-- pai:checkpoint\";\nexport const MARKER_CLOSE = \"<!-- /pai:checkpoint -->\";\n\nexport const CONTINUE_HEADING = \"## Continue\";\n\n/** Who wrote the checkpoint currently in TODO.md. */\nexport type Authorship = \"model\" | \"auto\";\n\n/** Result of applying a checkpoint. */\nexport type ApplyAction = \"written\" | \"preserved\" | \"failed\";\n\n// ---------------------------------------------------------------------------\n// TODO.md discovery\n// ---------------------------------------------------------------------------\n\nexport function findProjectTodo(\n rootPath: string\n): { path: string; content: string } | null {\n for (const rel of TODO_LOCATIONS) {\n const full = join(rootPath, rel);\n if (existsSync(full)) {\n try {\n return { path: full, content: readFileSync(full, \"utf8\") };\n } catch {\n // unreadable — try next\n }\n }\n }\n return null;\n}\n\n/**\n * Resolve the TODO.md to write to, creating Notes/ if nothing exists yet.\n * Returns null only when the directory could not be created.\n */\nexport function resolveTodoTarget(\n rootPath: string,\n opts: { create?: boolean } = {}\n): { path: string; content: string } | null {\n const found = findProjectTodo(rootPath);\n if (found) return found;\n\n const notesDir = join(rootPath, \"Notes\");\n if (opts.create !== false) {\n try {\n if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });\n } catch {\n return null;\n }\n }\n return { path: join(notesDir, \"TODO.md\"), content: \"\" };\n}\n\n// ---------------------------------------------------------------------------\n// Marker parsing\n// ---------------------------------------------------------------------------\n\nexport interface CheckpointMeta {\n authored: Authorship;\n /** Session line the checkpoint describes, e.g. \"0003 - 2026-08-01 - Title\". */\n session?: string;\n /** Claude Code session UUID, when known. */\n sessionId?: string;\n ts?: string;\n}\n\n/**\n * Parse a `<!-- pai:checkpoint key=\"value\" ... -->` marker line.\n * Returns null when the line is not a marker.\n */\nexport function parseMarker(line: string): CheckpointMeta | null {\n const trimmed = line.trim();\n if (!trimmed.startsWith(MARKER_OPEN)) return null;\n\n const attrs: Record<string, string> = {};\n for (const m of trimmed.matchAll(/([a-zA-Z][\\w-]*)=\"([^\"]*)\"/g)) {\n attrs[m[1]] = m[2];\n }\n\n return {\n authored: attrs.authored === \"model\" ? \"model\" : \"auto\",\n session: attrs.session || undefined,\n sessionId: attrs[\"session-id\"] || undefined,\n ts: attrs.ts || undefined,\n };\n}\n\nexport interface LocatedContinue {\n /** Index of the \"## Continue\" heading line. */\n startIdx: number;\n /** Exclusive end index, past any trailing `---` separator. */\n endIdx: number;\n meta: CheckpointMeta | null;\n /** Raw lines of the section, heading included. */\n lines: string[];\n}\n\n/**\n * Lines an auto-generated block is made of. Anything else in a section is\n * content somebody put there deliberately.\n */\nconst BOILERPLATE_PATTERNS = [\n /^##\\s+Continue$/,\n /^<!--\\s*\\/?pai:checkpoint/,\n /^>\\s*\\*\\*Last session:\\*\\*/,\n /^>\\s*\\*\\*Paused at:\\*\\*/,\n /^>\\s*Working directory:/,\n /^>\\s*Resume with:/,\n /^>\\s*_No checkpoint body was recorded/,\n /^-{3,}$/,\n];\n\n/** A blank line, with or without a blockquote marker. */\nconst BLANK_LINE = /^>?\\s*$/;\n\n/**\n * True when a section contains nothing but generated header lines.\n *\n * This is the guard that stops an auto write from destroying content it did\n * not author. A session that hit the old clobbering bug may have worked around\n * it by hand — writing its state into a subsection underneath the generated\n * header lines, in an unmarked block. Those blocks predate the marker, so\n * authorship cannot be read off them; the only safe signal is whether anything\n * beyond boilerplate is present.\n */\nexport function isBoilerplateOnly(lines: string[]): boolean {\n return lines.every((line) => {\n const t = line.trim();\n return BLANK_LINE.test(t) || BOILERPLATE_PATTERNS.some((re) => re.test(t));\n });\n}\n\n/**\n * Extract the non-boilerplate content of a section, or \"\" if there is none.\n *\n * Interior blank lines are kept. They are not decoration — in Markdown they\n * are what separates a paragraph from the table or list that follows, so\n * dropping them (as this did while it treated every blank line as boilerplate)\n * silently welds a checkpoint body into one unreadable run.\n */\nexport function extractSectionContent(lines: string[]): string {\n const kept: string[] = [];\n for (const line of lines) {\n const t = line.trim();\n if (BOILERPLATE_PATTERNS.some((re) => re.test(t))) continue;\n kept.push(line);\n }\n return trimBlankEdges(collapseBlankRuns(kept)).join(\"\\n\");\n}\n\n/** Drop leading and trailing blank lines, leaving interior spacing alone. */\nfunction trimBlankEdges(lines: string[]): string[] {\n let start = 0;\n let end = lines.length;\n while (start < end && BLANK_LINE.test(lines[start].trim())) start += 1;\n while (end > start && BLANK_LINE.test(lines[end - 1].trim())) end -= 1;\n return lines.slice(start, end);\n}\n\n/**\n * Collapse runs of blank lines to a single blank.\n *\n * Removing a boilerplate line leaves the blank that surrounded it behind, so\n * stripping the header can open a three-line gap in the middle of the body.\n * One blank line is all Markdown needs.\n */\nfunction collapseBlankRuns(lines: string[]): string[] {\n const out: string[] = [];\n let lastWasBlank = false;\n for (const line of lines) {\n const isBlank = BLANK_LINE.test(line.trim());\n if (isBlank && lastWasBlank) continue;\n out.push(line);\n lastWasBlank = isBlank;\n }\n return out;\n}\n\n/**\n * Locate the existing ## Continue section.\n *\n * When the section carries an explicit marker pair, the close marker defines\n * the end — this is what lets a rich body contain `---` and `##` safely.\n * Otherwise the legacy heuristic applies: stop at the first `---` or the next\n * `##` heading.\n */\nexport function locateContinue(content: string): LocatedContinue | null {\n const lines = content.split(\"\\n\");\n const startIdx = lines.findIndex((l) => l.trim() === CONTINUE_HEADING);\n if (startIdx === -1) return null;\n\n // Look for an open marker in the first few lines of the section.\n let meta: CheckpointMeta | null = null;\n let markerIdx = -1;\n for (let i = startIdx + 1; i < Math.min(startIdx + 6, lines.length); i++) {\n const parsed = parseMarker(lines[i]);\n if (parsed) {\n meta = parsed;\n markerIdx = i;\n break;\n }\n // A non-blank, non-marker line means there is no marker for this section.\n if (lines[i].trim() !== \"\") break;\n }\n\n let endIdx = lines.length;\n\n if (markerIdx !== -1) {\n const closeIdx = lines.findIndex(\n (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n );\n if (closeIdx !== -1) {\n endIdx = closeIdx + 1;\n } else {\n // Malformed (open without close) — fall back to the heuristic so we do\n // not swallow the rest of the file.\n endIdx = heuristicEnd(lines, startIdx);\n }\n } else {\n endIdx = heuristicEnd(lines, startIdx);\n }\n\n // Consume one trailing `---` separator and the blank lines around it.\n let trailingEnd = endIdx;\n while (trailingEnd < lines.length && lines[trailingEnd].trim() === \"\") {\n trailingEnd += 1;\n }\n if (trailingEnd < lines.length && lines[trailingEnd].trim() === \"---\") {\n trailingEnd += 1;\n } else {\n trailingEnd = endIdx;\n }\n\n return {\n startIdx,\n endIdx: trailingEnd,\n meta,\n lines: lines.slice(startIdx, trailingEnd),\n };\n}\n\n/**\n * Legacy scanner for blocks written before checkpoint markers existed.\n *\n * Terminates on a horizontal rule or the next level-2 heading. Note the\n * `(?!#)` — `###` is a *subsection* of `## Continue`, not a terminator. The\n * original scanner stopped at any run of `#`, which meant a `### Restored\n * state` subsection fell outside the section entirely and could not be seen,\n * let alone carried forward.\n */\nfunction heuristicEnd(lines: string[], startIdx: number): number {\n for (let i = startIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (\n trimmed === \"---\" ||\n (/^##(?!#)/.test(trimmed) && trimmed !== CONTINUE_HEADING)\n ) {\n return i;\n }\n }\n return lines.length;\n}\n\n/** Remove the ## Continue section, returning the remainder of the document. */\nexport function stripContinue(content: string): string {\n const found = locateContinue(content);\n if (!found) return content;\n\n const lines = content.split(\"\\n\");\n const before = lines.slice(0, found.startIdx);\n const after = lines.slice(found.endIdx);\n while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n return [...before, ...after].join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Reading a checkpoint back\n// ---------------------------------------------------------------------------\n\nexport interface ContinueCheckpoint {\n meta: CheckpointMeta | null;\n /** The authored body — the section with generated header lines removed. */\n body: string;\n /** The full section, heading included. */\n raw: string;\n}\n\n/**\n * Read the `## Continue` checkpoint out of a TODO.md.\n *\n * Writing a checkpoint is only half of a handover; something has to deliver it\n * to the next session. This is the read side, used by the SessionStart hook.\n *\n * Returns null when there is no section, or when the section holds nothing but\n * generated header lines — a bodyless block carries no information a new\n * session does not already have, and injecting it would only add noise.\n */\nexport function readContinueCheckpoint(\n content: string\n): ContinueCheckpoint | null {\n const found = locateContinue(content);\n if (!found) return null;\n\n const body = found.meta\n ? extractMarkedBody(found.lines)\n : extractSectionContent(found.lines);\n if (!body) return null;\n\n return { meta: found.meta, body, raw: found.lines.join(\"\\n\") };\n}\n\n/**\n * Extract the body of a marker-delimited block by position rather than by\n * pattern.\n *\n * The layout is fixed: open marker, a contiguous run of `>` header lines, the\n * body, then the close marker. Because the boundaries are known exactly, the\n * body comes back byte-for-byte — including any `---` rules or `##` headings\n * of its own, which a pattern-based filter would mistake for boilerplate and\n * delete. Falls back to the pattern filter if the shape is not as expected.\n */\nfunction extractMarkedBody(lines: string[]): string {\n const markerIdx = lines.findIndex((l) => parseMarker(l) !== null);\n if (markerIdx === -1) return extractSectionContent(lines);\n\n const closeIdx = lines.findIndex(\n (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n );\n if (closeIdx === -1) return extractSectionContent(lines);\n\n // Skip the blank lines and the `>` header run that follow the open marker.\n let bodyStart = markerIdx + 1;\n while (bodyStart < closeIdx) {\n const t = lines[bodyStart].trim();\n if (BLANK_LINE.test(t) || t.startsWith(\">\")) {\n bodyStart += 1;\n continue;\n }\n break;\n }\n\n return trimBlankEdges(lines.slice(bodyStart, closeIdx)).join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Block construction\n// ---------------------------------------------------------------------------\n\nexport interface BuildOptions {\n authored: Authorship;\n /** Human-readable session line, or \"Unknown session\". */\n sessionLine: string;\n /** Claude Code session UUID — the `claude --resume` handle. */\n sessionId?: string;\n cwd: string;\n /** Model-authored markdown. Omitted for auto blocks. */\n body?: string;\n /** Overridable for deterministic tests. */\n timestamp?: string;\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"'\");\n}\n\nexport function buildContinueBlock(opts: BuildOptions): string {\n const ts = opts.timestamp ?? new Date().toISOString();\n\n const attrs = [\n `authored=\"${opts.authored}\"`,\n `session=\"${escapeAttr(opts.sessionLine)}\"`,\n opts.sessionId ? `session-id=\"${escapeAttr(opts.sessionId)}\"` : null,\n `ts=\"${ts}\"`,\n ]\n .filter(Boolean)\n .join(\" \");\n\n const header = [\n `> **Last session:** ${opts.sessionLine}`,\n `> **Paused at:** ${ts}`,\n \">\",\n `> Working directory: ${opts.cwd}`,\n ];\n\n if (opts.sessionId) {\n header.push(\">\", `> Resume with: \\`claude --resume ${opts.sessionId}\\``);\n }\n\n const body = (opts.body ?? \"\").trim();\n\n const parts = [\n CONTINUE_HEADING,\n \"\",\n `${MARKER_OPEN} ${attrs} -->`,\n \"\",\n ...header,\n ];\n\n if (body) {\n parts.push(\"\", body);\n } else {\n parts.push(\n \">\",\n \"> _No checkpoint body was recorded — see the latest session note._\"\n );\n }\n\n parts.push(\"\", MARKER_CLOSE, \"\", \"---\", \"\");\n\n return parts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Apply\n// ---------------------------------------------------------------------------\n\nexport interface ApplyOptions extends BuildOptions {\n /** Project root; TODO.md is resolved beneath it. */\n rootPath: string;\n /** Preview only — nothing is written. */\n dryRun?: boolean;\n}\n\nexport interface ApplyResult {\n action: ApplyAction;\n path: string | null;\n block: string;\n /** Set when action === \"preserved\". */\n preservedMeta?: CheckpointMeta;\n /** Set when unattributed content was carried forward into the new block. */\n carriedForward?: boolean;\n error?: string;\n}\n\n/**\n * Write the ## Continue block, honouring the preservation rules.\n *\n * An AUTO write is unattended — it fires from the session-stop and pre-compact\n * hooks — so it operates under one governing rule: **never destroy content it\n * did not author.** Three cases follow from that:\n *\n * 1. An authored checkpoint for the SAME session is left untouched. The hooks\n * fire after the model has already recorded the real state; overwriting it\n * with metadata is the bug this module exists to fix.\n *\n * \"Same session\" is decided by the Claude session UUID whenever both sides\n * know it, and only falls back to the human-readable session line when one\n * of them does not. The line is derived from the session note filename,\n * and `session-stop.sh` *renames and renumbers that file* — via `session\n * slug --apply` and `session cleanup --execute` — before it reaches the\n * handover step. So the key the hook computes at exit is not the key the\n * model wrote seconds earlier, and a filename-keyed comparison mismatches\n * by construction. Observed live on 2026-08-01: notes renumbered twice\n * within a single session. The UUID is the only identifier that holds\n * still.\n * 2. An authored checkpoint from an EARLIER session is stale — TODO.md would\n * otherwise keep pointing at the wrong session — so it is replaced. Its\n * content is not lost: `pai pause` mirrors every authored body into the\n * session note.\n * 3. An UNMARKED block predates the marker, so authorship cannot be read off\n * it. If it is nothing but generated header lines it is replaced. If it\n * carries anything else, that content was put there deliberately — quite\n * possibly as a hand-rolled workaround for the very clobbering this fixes —\n * and is carried forward into the new block rather than dropped.\n *\n * A MODEL write always replaces: the model is authoring the checkpoint, and a\n * newer one supersedes an older one.\n */\n/**\n * Do an existing checkpoint and an incoming write describe the same session?\n *\n * The UUID is authoritative when both sides carry one: it is assigned by Claude\n * Code and never changes for the life of the session. The session line is a\n * derived, mutable label and is only consulted when there is no UUID to compare\n * — a checkpoint written before `--session-id` was threaded through, or an auto\n * write from a caller that was not given one.\n */\nfunction isSameSession(\n meta: CheckpointMeta,\n opts: Pick<ApplyOptions, \"sessionId\" | \"sessionLine\">\n): boolean {\n if (meta.sessionId && opts.sessionId) {\n return meta.sessionId === opts.sessionId;\n }\n return meta.session === opts.sessionLine;\n}\n\nexport function applyContinue(opts: ApplyOptions): ApplyResult {\n const target = resolveTodoTarget(opts.rootPath, { create: !opts.dryRun });\n if (!target) {\n return {\n action: \"failed\",\n path: null,\n block: buildContinueBlock(opts),\n error: \"Could not resolve or create a TODO.md target\",\n };\n }\n\n const existing = locateContinue(target.content);\n let carriedForward = false;\n let effectiveBody = opts.body;\n\n if (opts.authored === \"auto\" && existing) {\n // Case 1 — authored, same session: hands off.\n if (existing.meta?.authored === \"model\" && isSameSession(existing.meta, opts)) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta,\n };\n }\n\n // Case 1b — the incoming write carries no body at all, and what is already\n // there does. A metadata-only block says \"see the latest session note\", so\n // replacing a real handover with one trades content for a pointer — and the\n // pointer is not always good: observed live on 2026-08-01 in the AIBroker\n // project, where the stop hook's bodyless handover overwrote the autosave's\n // body and named a session note that had never been created, leaving the\n // next session nothing to resume from.\n //\n // Deferring to the session note is only safe when the note demonstrably has\n // the content. This code cannot see that, so it does the one thing that is\n // never wrong: a write with nothing to say does not get to destroy\n // something that does. A stale-but-real handover beats a fresh dead link.\n // Scoped to blocks we marked. An unmarked block falls through to case 3,\n // which salvages its content into the new block rather than freezing the\n // old one — better, because an unmarked block has no session metadata worth\n // preserving and may predate this scheme entirely.\n if (\n existing.meta &&\n !(opts.body ?? \"\").trim() &&\n !isBoilerplateOnly(existing.lines)\n ) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta ?? undefined,\n };\n }\n\n // Case 3 — unmarked block holding content nobody can attribute to us.\n if (!existing.meta && !isBoilerplateOnly(existing.lines)) {\n const salvaged = extractSectionContent(existing.lines);\n if (salvaged) {\n effectiveBody = [\n \"_Carried forward from the previous checkpoint (author unknown — this\",\n \"block predates checkpoint authorship markers):_\",\n \"\",\n salvaged,\n ].join(\"\\n\");\n carriedForward = true;\n }\n }\n }\n\n const block = buildContinueBlock({ ...opts, body: effectiveBody });\n\n if (opts.dryRun) {\n return { action: \"written\", path: target.path, block, carriedForward };\n }\n\n const newContent = block + stripContinue(target.content).trimStart();\n const tmpPath = `${target.path}.continue.tmp`;\n\n try {\n writeFileSync(tmpPath, newContent, \"utf8\");\n renameSync(tmpPath, target.path);\n } catch (err) {\n try {\n if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n } catch {\n /* ignore */\n }\n return {\n action: \"failed\",\n path: target.path,\n block,\n error: String(err),\n };\n }\n\n return { action: \"written\", path: target.path, block, carriedForward };\n}\n\n// ---------------------------------------------------------------------------\n// Session-note discovery\n//\n// Lifted verbatim from end.ts so pause, end and any future caller share one\n// implementation. end.ts now imports these rather than carrying its own copy.\n// ---------------------------------------------------------------------------\n\n/** PAI_DIR — mirrors pai-paths.ts resolution. */\nfunction getPaiDir(): string {\n const envDir = process.env.PAI_DIR;\n if (envDir) {\n try {\n return realpathSync(envDir);\n } catch {\n return envDir;\n }\n }\n return join(homedir(), \".claude\");\n}\n\n/**\n * Find the notes directory for a project — local first, then the central\n * ~/.claude/projects/<encoded>/Notes fallback. Never creates.\n */\nexport function findNotesDir(\n rootPath: string,\n encodedDir: string\n): string | null {\n for (const rel of [\"Notes\", \"notes\", \".claude/Notes\"]) {\n const p = join(rootPath, rel);\n if (existsSync(p)) return p;\n }\n const central = join(getPaiDir(), \"projects\", encodedDir, \"Notes\");\n if (existsSync(central)) return central;\n return null;\n}\n\n/**\n * Find the current (highest-numbered) session note: current month, then the\n * previous month, then a flat notesDir as legacy fallback.\n */\nexport function findLatestNote(notesDir: string): string | null {\n const findIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n let files: string[];\n try {\n files = readdirSync(dir);\n } catch {\n return null;\n }\n const notes = files\n .filter((f) => /^\\d{3,4}[\\s_-].*\\.md$/.test(f))\n .sort((a, b) => {\n const na = parseInt(a.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n const nb = parseInt(b.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n return na - nb;\n });\n return notes.length > 0 ? join(dir, notes[notes.length - 1]) : null;\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, \"0\");\n\n const current = findIn(join(notesDir, year, month));\n if (current) return current;\n\n const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const py = String(prev.getFullYear());\n const pm = String(prev.getMonth() + 1).padStart(2, \"0\");\n const prevFound = findIn(join(notesDir, py, pm));\n if (prevFound) return prevFound;\n\n return findIn(notesDir);\n}\n\n// ---------------------------------------------------------------------------\n// Session-note append\n// ---------------------------------------------------------------------------\n\n/**\n * Append the checkpoint body to a session note.\n *\n * TODO.md's ## Continue is a single slot that every later checkpoint\n * overwrites. The session note is the durable record, so the body goes to both.\n * Idempotent per timestamp: re-running with the same stamp will not duplicate.\n */\nexport function appendCheckpointToNote(\n notePath: string,\n body: string,\n timestamp?: string\n): { appended: boolean; error?: string } {\n const ts = timestamp ?? new Date().toISOString();\n const heading = `## Pause Checkpoint — ${ts}`;\n\n let existing: string;\n try {\n existing = readFileSync(notePath, \"utf8\");\n } catch (err) {\n return { appended: false, error: String(err) };\n }\n\n if (existing.includes(heading)) return { appended: false };\n\n const block = `\\n\\n---\\n\\n${heading}\\n\\n${body.trim()}\\n`;\n const tmpPath = `${notePath}.checkpoint.tmp`;\n\n try {\n writeFileSync(tmpPath, existing.trimEnd() + block, \"utf8\");\n renameSync(tmpPath, notePath);\n } catch (err) {\n try {\n if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n } catch {\n /* ignore */\n }\n return { appended: false, error: String(err) };\n }\n\n return { appended: true };\n}\n\n// ---------------------------------------------------------------------------\n// Body loading\n// ---------------------------------------------------------------------------\n\n/** Read a checkpoint body from a file, or from stdin when path is \"-\". */\nexport function readBodyFile(path: string): string {\n if (path === \"-\") {\n return readFileSync(0, \"utf8\");\n }\n return readFileSync(path, \"utf8\");\n}\n"],"mappings":"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"}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -7,12 +7,13 @@ import "../helpers-crDEr6S2.mjs";
|
|
|
7
7
|
import "../sync--BoxBBok.mjs";
|
|
8
8
|
import "../embeddings-Bn86ssxR.mjs";
|
|
9
9
|
import "../search-CpTv1I24.mjs";
|
|
10
|
-
import "../pick-
|
|
11
|
-
import "../checkpoint-block-
|
|
10
|
+
import "../pick-BRdJ4gpT.mjs";
|
|
11
|
+
import "../checkpoint-block-CkvwYA5y.mjs";
|
|
12
12
|
import "../indexer-AEcT8wHf.mjs";
|
|
13
|
-
import "../
|
|
14
|
-
import "../
|
|
15
|
-
import "../
|
|
13
|
+
import "../ipc-client-aVKVERjJ.mjs";
|
|
14
|
+
import "../config-CcdkNSWa.mjs";
|
|
15
|
+
import "../factory-1UI19STL.mjs";
|
|
16
|
+
import "../main-resolver-BozXCqpl.mjs";
|
|
16
17
|
import { buildProgram } from "./program.mjs";
|
|
17
18
|
|
|
18
19
|
//#region src/cli/index.ts
|
package/dist/cli/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Knowledge OS — CLI entry point.\n *\n * Thin entry: builds the Commander program (see ./program.ts) and parses argv.\n * All command construction lives in buildProgram() so the docs generator can\n * introspect the same tree that powers `--help`.\n *\n * Daily surface:\n * pai → deduped session listing (one row per name)\n * pai <name> → universal: switch live tab / resume / fresh\n * pai <uuid-prefix> → direct session resume via filesystem scan\n * pai pause [all] → save state (or mass-pause every live session)\n * pai end → finalize session\n * pai help [area] → rich man page for a command area\n */\n\nimport { buildProgram } from \"./program.js\";\n\nbuildProgram().parse(process.argv);\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Knowledge OS — CLI entry point.\n *\n * Thin entry: builds the Commander program (see ./program.ts) and parses argv.\n * All command construction lives in buildProgram() so the docs generator can\n * introspect the same tree that powers `--help`.\n *\n * Daily surface:\n * pai → deduped session listing (one row per name)\n * pai <name> → universal: switch live tab / resume / fresh\n * pai <uuid-prefix> → direct session resume via filesystem scan\n * pai pause [all] → save state (or mass-pause every live session)\n * pai end → finalize session\n * pai help [area] → rich man page for a command area\n */\n\nimport { buildProgram } from \"./program.js\";\n\nbuildProgram().parse(process.argv);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA,cAAc,CAAC,MAAM,QAAQ,KAAK"}
|
package/dist/cli/program.mjs
CHANGED
|
@@ -6,12 +6,13 @@ import "../helpers-crDEr6S2.mjs";
|
|
|
6
6
|
import "../sync--BoxBBok.mjs";
|
|
7
7
|
import "../embeddings-Bn86ssxR.mjs";
|
|
8
8
|
import "../search-CpTv1I24.mjs";
|
|
9
|
-
import { A as registerProjectsCommands, C as registerRestoreCommands, D as registerIdentityCommands, E as registerMcpCommands, M as resolveIdentifier, O as registerMemoryCommands, S as registerSetupCommand, T as registerDaemonCommands, _ as registerUpdateCommand, a as cmdEnd, b as registerZettelCommands, c as cmdGoto, d as registerHelpCommand, f as registerDbCommands, g as registerNotifyCommands, h as registerTaskCommands, i as cmdPauseAll, j as findMovedPath, k as registerRegistryCommands, l as cmdPause, m as registerTopicCommands, n as cmdFind, o as registerSessionCleanupCommand, p as registerKgCommands, r as cmdClearNames, s as registerSessionCommands, t as cmdPick, u as cmdList, v as registerSkillCommands, w as registerBackupCommands, x as registerObsidianCommands, y as registerObservationCommands } from "../pick-
|
|
10
|
-
import "../checkpoint-block-
|
|
9
|
+
import { A as registerProjectsCommands, C as registerRestoreCommands, D as registerIdentityCommands, E as registerMcpCommands, M as resolveIdentifier, O as registerMemoryCommands, S as registerSetupCommand, T as registerDaemonCommands, _ as registerUpdateCommand, a as cmdEnd, b as registerZettelCommands, c as cmdGoto, d as registerHelpCommand, f as registerDbCommands, g as registerNotifyCommands, h as registerTaskCommands, i as cmdPauseAll, j as findMovedPath, k as registerRegistryCommands, l as cmdPause, m as registerTopicCommands, n as cmdFind, o as registerSessionCleanupCommand, p as registerKgCommands, r as cmdClearNames, s as registerSessionCommands, t as cmdPick, u as cmdList, v as registerSkillCommands, w as registerBackupCommands, x as registerObsidianCommands, y as registerObservationCommands } from "../pick-BRdJ4gpT.mjs";
|
|
10
|
+
import "../checkpoint-block-CkvwYA5y.mjs";
|
|
11
11
|
import "../indexer-AEcT8wHf.mjs";
|
|
12
|
-
import "../
|
|
13
|
-
import "../
|
|
14
|
-
import
|
|
12
|
+
import "../ipc-client-aVKVERjJ.mjs";
|
|
13
|
+
import "../config-CcdkNSWa.mjs";
|
|
14
|
+
import "../factory-1UI19STL.mjs";
|
|
15
|
+
import { t as cmdMain } from "../main-resolver-BozXCqpl.mjs";
|
|
15
16
|
import { existsSync, readFileSync } from "node:fs";
|
|
16
17
|
import { basename, dirname, join } from "node:path";
|
|
17
18
|
import { Command } from "commander";
|
package/dist/cli/program.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.mjs","names":[],"sources":["../../src/cli/program.ts"],"sourcesContent":["/**\n * PAI CLI — Commander program builder.\n *\n * Constructs the full `pai` command tree WITHOUT parsing argv. This separation\n * lets the documentation generator (scripts/build-docs.mjs) import and introspect\n * the live program object — the single source of truth for `--help`, the\n * generated man pages, and `pai help <area>`.\n *\n * The thin entry point (cli/index.ts) imports buildProgram() and calls parse().\n */\n\nimport { Command } from \"commander\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { openRegistry } from \"../registry/db.js\";\nimport type { Database } from \"better-sqlite3\";\nimport { registerProjectsCommands } from \"./commands/project/projects-index.js\";\nimport { findMovedPath } from \"./commands/project/commands.js\";\nimport { existsSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { registerRegistryCommands } from \"./commands/registry.js\";\nimport { registerMemoryCommands } from \"./commands/memory.js\";\nimport { registerIdentityCommands } from \"./commands/identity.js\";\nimport { registerMcpCommands } from \"./commands/mcp.js\";\nimport { registerDaemonCommands } from \"./commands/daemon.js\";\nimport { registerBackupCommands } from \"./commands/backup.js\";\nimport { registerRestoreCommands } from \"./commands/restore.js\";\nimport { registerSetupCommand } from \"./commands/setup.js\";\nimport { registerObsidianCommands } from \"./commands/obsidian.js\";\nimport { registerZettelCommands } from \"./commands/zettel.js\";\nimport { registerObservationCommands } from \"./commands/observation.js\";\nimport { registerSkillCommands } from \"./commands/skill.js\";\nimport { registerUpdateCommand } from \"./commands/update.js\";\nimport { registerNotifyCommands } from \"./commands/notify.js\";\nimport { registerTaskCommands } from \"./commands/task.js\";\nimport { registerTopicCommands } from \"./commands/topic.js\";\nimport { registerKgCommands } from \"./commands/kg.js\";\nimport { registerDbCommands } from \"./commands/db.js\";\nimport { registerHelpCommand } from \"./commands/help.js\";\nimport { registerSessionCommands } from \"./commands/session/index.js\";\nimport { registerSessionCleanupCommand } from \"./commands/session-cleanup/index.js\";\nimport { err, warn, ok, dim, shortenPath, encodeDir, now } from \"./utils.js\";\nimport { cmdPause } from \"./commands/session/pause.js\";\nimport { cmdEnd } from \"./commands/session/end.js\";\nimport { cmdGoto } from \"./commands/session/goto.js\";\nimport { cmdPauseAll } from \"./commands/session/pause-all.js\";\nimport { cmdClearNames } from \"./commands/session/clear-names.js\";\nimport { cmdList as cmdNotesList } from \"./commands/session/commands.js\";\nimport { resolveIdentifier } from \"./commands/project/helpers.js\";\nimport { cmdFind } from \"./commands/find.js\";\nimport { cmdMain } from \"./commands/main-resolver.js\";\nimport { cmdPick } from \"./commands/pick.js\";\n\n// ---------------------------------------------------------------------------\n// Version resolution\n// ---------------------------------------------------------------------------\n\nfunction getVersion(): string {\n try {\n const __dirname = dirname(fileURLToPath(import.meta.url));\n const pkgPath = join(__dirname, \"../../package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Lazy database singleton\n// ---------------------------------------------------------------------------\n\nlet _db: Database | null = null;\n\nfunction getDb(): Database {\n if (!_db) {\n try {\n _db = openRegistry();\n } catch (e) {\n console.error(err(`Failed to open PAI registry: ${e}`));\n process.exit(1);\n }\n }\n return _db;\n}\n\n// ---------------------------------------------------------------------------\n// Program builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build and return the fully configured `pai` Commander program.\n * Does NOT call parse — the caller (cli/index.ts) does that.\n */\nexport function buildProgram(): Command {\n const program = new Command();\n\n program\n .name(\"pai\")\n .description(\"PAI Knowledge OS — Personal AI Infrastructure CLI\")\n .version(getVersion(), \"-V, --version\", \"Print version and exit\")\n .addHelpText(\n \"after\",\n `\nDaily commands:\n pai Show your work: live tabs + recent sessions + recent projects\n pai <name> Switch / resume / fresh — universal\n pai pause [all] Pause current (or all live) sessions\n pai end Finalize current session\n\nSubcommands (rarely needed):\n pai projects ... Project registry management (cd, rebind, info, ...)\n pai registry ... Registry maintenance (scan, ...)\n pai memory ... Memory engine (index, search, ...)\n pai shell-init Shell integration (eval \"$(pai shell-init)\")\n\nRecovery:\n pai clear-names Wipe corrupt iTerm tab-name store\n\nExamples:\n pai aibroker Switch to the live AIBroker tab in iTerm\n pai mdf Find sessions where you worked on MDF\n pai 81c5c3dc Resume session by UUID prefix\n pai --all Show everything including cold and zero-session projects`\n );\n\n // -------------------------------------------------------------------------\n // pai projects (canonical plural namespace)\n // -------------------------------------------------------------------------\n\n const projectsCmd = program\n .command(\"projects\")\n .description(\"Manage registered projects (list, cd, add, info, ...)\");\n\n registerProjectsCommands(projectsCmd, getDb);\n\n // Singular alias: `pai project` → `pai projects`\n const projectCmd = program\n .command(\"project\")\n .description(\"Alias for `pai projects`\");\n\n registerProjectsCommands(projectCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai sessions (alias for the unified `pai` listing)\n // -------------------------------------------------------------------------\n\n // -------------------------------------------------------------------------\n // pai session <subcommand>\n //\n // These were written, exported and never wired: neither\n // registerSessionCommands nor registerSessionCleanupCommand was called from\n // anywhere, so every `pai session <x>` fell through to the default `query`\n // resolver and was interpreted as a prompt-history search.\n //\n // That is why session-stop.sh has been a near no-op — all four of its calls\n // (`session cleanup --execute`, `session checkpoint`, `session slug --apply`,\n // `session handover`) failed silently behind `2>/dev/null || true`.\n // -------------------------------------------------------------------------\n\n const sessionCmd = program\n .command(\"session\")\n .description(\n \"Session management: list, info, checkpoint, handover, cleanup, slug, tag, route.\\n\" +\n \"Short forms: `pai pause`, `pai end`, `pai resume <name>`.\"\n );\n\n registerSessionCommands(sessionCmd, getDb);\n registerSessionCleanupCommand(sessionCmd, getDb);\n\n program\n .command(\"sessions\")\n .description(\"Alias for `pai` — show the unified deduped listing.\")\n .option(\"--all\", \"Show all entries including cold / zero-session / archived projects\")\n .option(\"-n, --n <count>\", \"Max candidates for history search\", \"20\")\n .action(async (opts: { all?: boolean; n?: string }) => {\n await cmdMain(getDb(), undefined, undefined, opts);\n });\n\n // -------------------------------------------------------------------------\n // pai registry\n // -------------------------------------------------------------------------\n\n const registryCmd = program\n .command(\"registry\")\n .description(\"Registry maintenance: scan, migrate, stats, rebuild\");\n\n registerRegistryCommands(registryCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai memory\n // -------------------------------------------------------------------------\n\n const memoryCmd = program\n .command(\"memory\")\n .description(\"Memory engine: index, search, and status\");\n\n registerMemoryCommands(memoryCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai identity\n // -------------------------------------------------------------------------\n\n const identityCmd = program\n .command(\"identity\")\n .description(\"Declare who you are: self addresses and where mail is delivered\");\n\n registerIdentityCommands(identityCmd);\n\n // -------------------------------------------------------------------------\n // pai mcp\n // -------------------------------------------------------------------------\n\n const mcpCmd = program\n .command(\"mcp\")\n .description(\"MCP server management: install and status\");\n\n registerMcpCommands(mcpCmd);\n\n // -------------------------------------------------------------------------\n // pai daemon\n // -------------------------------------------------------------------------\n\n const daemonCmd = program\n .command(\"daemon\")\n .description(\"PAI daemon management: serve, status, restart, install, uninstall, logs\");\n\n registerDaemonCommands(daemonCmd);\n\n // -------------------------------------------------------------------------\n // pai backup / pai restore\n // -------------------------------------------------------------------------\n\n registerBackupCommands(program);\n registerRestoreCommands(program);\n\n // -------------------------------------------------------------------------\n // pai setup / pai update\n // -------------------------------------------------------------------------\n\n registerSetupCommand(program);\n registerUpdateCommand(program);\n\n // -------------------------------------------------------------------------\n // pai notify\n // -------------------------------------------------------------------------\n\n const notifyCmd = program\n .command(\"notify\")\n .description(\"Notification config: status, get, set, test, send\");\n\n registerNotifyCommands(notifyCmd);\n\n // -------------------------------------------------------------------------\n // pai task\n // -------------------------------------------------------------------------\n\n const taskCmd = program\n .command(\"task\")\n .alias(\"tasks\")\n .description(\"Task bus: list, add, dispatch, and complete cross-session work\");\n\n registerTaskCommands(taskCmd);\n\n // -------------------------------------------------------------------------\n // pai topic\n // -------------------------------------------------------------------------\n\n const topicCmd = program\n .command(\"topic\")\n .description(\"Topic shift detection: check whether context has drifted to a different project\");\n\n registerTopicCommands(topicCmd);\n\n // -------------------------------------------------------------------------\n // pai kg\n // -------------------------------------------------------------------------\n\n const kgCmd = program\n .command(\"kg\")\n .description(\"Temporal knowledge graph: backfill, query, list, stats\");\n\n registerKgCommands(kgCmd);\n\n // -------------------------------------------------------------------------\n // pai db\n // -------------------------------------------------------------------------\n\n const dbCmd = program\n .command(\"db\")\n .description(\"Database inspection: query, tables, schema (sqlite or postgres)\");\n\n registerDbCommands(dbCmd);\n\n // -------------------------------------------------------------------------\n // pai obsidian\n // -------------------------------------------------------------------------\n\n const obsidianCmd = program\n .command(\"obsidian\")\n .description(\"Obsidian vault: sync project notes, view status, open in Obsidian\");\n\n registerObsidianCommands(obsidianCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai zettel\n // -------------------------------------------------------------------------\n\n const zettelCmd = program\n .command(\"zettel\")\n .description(\"Zettelkasten intelligence: explore, surprise, converse, themes, health, suggest\");\n\n registerZettelCommands(zettelCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai observation\n // -------------------------------------------------------------------------\n\n const observationCmd = program\n .command(\"observation\")\n .description(\"Observation capture: list, search, and stats\");\n\n registerObservationCommands(observationCmd);\n\n // -------------------------------------------------------------------------\n // pai skill\n // -------------------------------------------------------------------------\n\n const skillCmd = program\n .command(\"skill\")\n .description(\"Skill telemetry and (future) discovery for the self-educating skill system\");\n\n registerSkillCommands(skillCmd);\n\n // -------------------------------------------------------------------------\n // pai help [area] — rich man-page viewer for the generated docs\n // -------------------------------------------------------------------------\n\n registerHelpCommand(program);\n\n // -------------------------------------------------------------------------\n // DAILY VERBS — top-level\n // -------------------------------------------------------------------------\n\n // pai pause [all] [--dry-run] [--exit] [--wait <ms>]\n program\n .command(\"pause [target]\")\n .description(\n \"Save state and display safe-exit instructions for the current session.\\n\" +\n \"Writes a ## Continue checkpoint to the project's TODO.md.\\n\" +\n \"Use `pai pause all` to pause every live session via AIBroker.\"\n )\n .option(\"--dry-run\", \"Preview changes without writing them\")\n .option(\n \"--body-file <path>\",\n \"File holding the checkpoint markdown to persist ('-' reads stdin). Required unless --no-body.\"\n )\n .option(\n \"--session-id <uuid>\",\n \"Claude Code session UUID — recorded as the `claude --resume` handle\"\n )\n .option(\n \"--no-body\",\n \"Deliberately write a metadata-only checkpoint (no content)\"\n )\n .option(\"--exit\", \"(pause all only) Also send /exit to each session after pausing\")\n .option(\n \"--wait <ms>\",\n \"(pause all only) Max milliseconds to wait for a session to finish its\\n\" +\n \"checkpoint before /exit. Not a fixed delay — sessions are exited as soon\\n\" +\n \"as they return to the prompt, and any still busy at the deadline are left\\n\" +\n \"open rather than killed mid-write. (default: 180000)\",\n \"180000\"\n )\n .action(async (target: string | undefined, opts: { dryRun?: boolean; exit?: boolean; wait?: string; bodyFile?: string; sessionId?: string; body?: boolean }) => {\n if (target === \"all\") {\n await cmdPauseAll({\n exit: opts.exit,\n dryRun: opts.dryRun,\n wait: opts.wait !== undefined ? parseInt(opts.wait, 10) : undefined,\n });\n } else {\n if (target !== undefined) {\n console.error(err(`Unknown target: ${target}. Did you mean 'pai pause all'?`));\n process.exitCode = 1;\n return;\n }\n // Commander maps `--no-body` to opts.body === false.\n cmdPause(getDb(), {\n dryRun: opts.dryRun,\n bodyFile: opts.bodyFile,\n sessionId: opts.sessionId,\n noBody: opts.body === false,\n });\n }\n });\n\n // pai end [--body-file <path>] [--session-id <uuid>] [--no-body] [--dry-run]\n program\n .command(\"end\")\n .description(\n \"Finalize a session: save state, mark note Completed, display safe-exit instructions.\"\n )\n .option(\"--dry-run\", \"Preview all changes without writing them\")\n .option(\n \"--body-file <path>\",\n \"File holding the checkpoint markdown to persist ('-' reads stdin). Required unless --no-body.\"\n )\n .option(\n \"--session-id <uuid>\",\n \"Claude Code session UUID — recorded as the `claude --resume` handle\"\n )\n .option(\n \"--no-body\",\n \"Deliberately write a metadata-only checkpoint (no content)\"\n )\n .action((opts: { dryRun?: boolean; bodyFile?: string; sessionId?: string; body?: boolean }) => {\n cmdEnd(getDb(), {\n dryRun: opts.dryRun,\n bodyFile: opts.bodyFile,\n sessionId: opts.sessionId,\n noBody: opts.body === false,\n });\n });\n\n // pai clear-names [--dry-run] — recovery: wipe corrupt iTerm/JSON name store\n program\n .command(\"clear-names\")\n .description(\n \"Recovery: wipe corrupted iTerm2 session name state.\\n\" +\n \"Clears ~/.aibroker/session-names.json AND user.paiName from all live iTerm2 sessions.\\n\" +\n \"After running this, use /Name to re-label each tab.\"\n )\n .option(\"--dry-run\", \"Preview what would be cleared without making changes\")\n .action(async (opts: { dryRun?: boolean }) => {\n await cmdClearNames(opts);\n });\n\n // -------------------------------------------------------------------------\n // HIDDEN COMPAT ALIASES\n // -------------------------------------------------------------------------\n\n // pai resume <name> [--dry-run] — hidden; prefer `pai <name>`\n program\n .command(\"resume <name>\", { hidden: true })\n .description(\"Go to a session by name or UUID (prefer: pai <name>)\")\n .option(\"--dry-run\", \"Print the exact argv and cwd, then exit without launching\")\n .action((name: string, opts: { dryRun?: boolean }) => {\n cmdGoto(getDb(), name, { dryRun: opts.dryRun });\n });\n\n // pai cd <identifier> — hidden; shell wrapper uses this\n program\n .command(\"cd <identifier>\", { hidden: true })\n .description(\"cd to a project directory (shell wrapper handles the actual cd)\")\n .action((identifier: string) => {\n const db = getDb();\n const project = resolveIdentifier(db, identifier);\n if (!project) {\n console.error(`Project not found: ${identifier}`);\n process.exit(1);\n return;\n }\n\n if (existsSync(project.root_path)) {\n process.stdout.write(project.root_path + \"\\n\");\n return;\n }\n\n process.stderr.write(\n warn(`Path not found: ${project.root_path}\\n`) +\n dim(\" Searching for moved location...\\n\")\n );\n\n const result = findMovedPath(project.root_path);\n\n if (result.found) {\n const newPath = result.found;\n const newEncoded = encodeDir(newPath);\n const ts = now();\n db.prepare(\n \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(newPath, newEncoded, ts, project.id);\n process.stderr.write(\n ok(`Project moved: ${shortenPath(project.root_path, 50)}\\n`) +\n dim(` → ${newPath}\\n`) +\n ok(\"Registry updated.\\n\")\n );\n process.stdout.write(newPath + \"\\n\");\n return;\n }\n\n if (result.ambiguous) {\n process.stderr.write(\n warn(`Multiple directories named \"${basename(project.root_path)}\" found:\\n`)\n );\n for (const candidate of result.ambiguous!) {\n process.stderr.write(dim(` ${candidate}\\n`));\n }\n process.stderr.write(\n dim(`\\n Disambiguate with: pai projects rebind ${project.slug} <path>\\n`)\n );\n process.exitCode = 1;\n return;\n }\n\n process.stderr.write(\n err(\n `Project \"${project.slug}\" root_path \"${project.root_path}\" does not exist on disk\\n` +\n ` and no folder named \"${basename(project.root_path)}\" was found in scan dirs.\\n`\n ) +\n dim(` Fix with: pai projects rebind ${project.slug} <new-path>\\n`)\n );\n process.exitCode = 1;\n });\n\n // -------------------------------------------------------------------------\n // pai notes — hidden power-user alias; accessible but not in main help\n // -------------------------------------------------------------------------\n\n const notesCmd = program\n .command(\"notes [project-slug]\", { hidden: true })\n .description(\"Markdown session notes — list notes, optionally filtered to a project.\")\n .option(\"--limit <n>\", \"Maximum number of notes to show\", \"20\")\n .option(\"--status <status>\", \"Filter by status: open | completed | compacted\")\n .action(\n (projectSlug: string | undefined, opts: { limit?: string; status?: string }) => {\n cmdNotesList(getDb(), projectSlug, opts);\n }\n );\n\n notesCmd\n .command(\"list [project-slug]\")\n .description(\"List markdown session notes (same as: pai notes)\")\n .option(\"--limit <n>\", \"Maximum number of notes to show\", \"20\")\n .option(\"--status <status>\", \"Filter by status: open | completed | compacted\")\n .action(\n (projectSlug: string | undefined, opts: { limit?: string; status?: string }) => {\n cmdNotesList(getDb(), projectSlug, opts);\n }\n );\n\n // -------------------------------------------------------------------------\n // pai find <query> — hidden power-user alias for history search\n // -------------------------------------------------------------------------\n\n program\n .command(\"find <query>\", { hidden: true })\n .description(\"Search prompt history for matching sessions.\")\n .option(\"-n, --n <count>\", \"Maximum number of sessions to show\", \"20\")\n .option(\"--json\", \"Output as JSON array\")\n .action(async (query: string, opts: { n?: string; json?: boolean }) => {\n await cmdFind(query, opts);\n });\n\n // -------------------------------------------------------------------------\n // pai shell-init — emit shell function for eval \"$(pai shell-init)\"\n // -------------------------------------------------------------------------\n\n program\n .command(\"shell-init\")\n .description('Emit shell integration code. Add to ~/.zshrc: eval \"$(pai shell-init)\"')\n .action(() => {\n process.stdout.write(\n `# PAI shell integration — generated by: pai shell-init\npai() {\n if [[ \"$1\" == \"cd\" ]]; then\n local dir=$(command pai cd \"$2\" 2>/dev/null)\n if [[ -n \"$dir\" && -d \"$dir\" ]]; then\n builtin cd \"$dir\"\n echo \"-> $dir\"\n else\n echo \"Project not found: $2\" >&2\n return 1\n fi\n elif [[ \"$1\" == \"projects\" && \"$2\" == \"cd\" ]]; then\n local dir=$(command pai projects cd \"$3\" 2>/dev/null)\n if [[ -n \"$dir\" && -d \"$dir\" ]]; then\n builtin cd \"$dir\"\n echo \"-> $dir\"\n else\n echo \"Project not found: $3\" >&2\n return 1\n fi\n elif [[ $# -eq 0 ]]; then\n # Bare \\`pai\\` → interactive picker. fzf draws on /dev/tty; the picker writes\n # a chosen directory to PAI_PICK_OUT when you press Ctrl-G (cd only), which we\n # then cd into here (a child process can't change this shell's cwd itself).\n local __pai_f\n __pai_f=$(mktemp -t pai-pick 2>/dev/null) || { command pai; return $?; }\n PAI_PICK_OUT=\"$__pai_f\" command pai\n local __pai_rc=$?\n local __pai_dir=\"\"\n [[ -s \"$__pai_f\" ]] && __pai_dir=$(cat \"$__pai_f\")\n command rm -f \"$__pai_f\"\n if [[ -n \"$__pai_dir\" && -d \"$__pai_dir\" ]]; then\n builtin cd \"$__pai_dir\"\n echo \"-> $__pai_dir\"\n fi\n return $__pai_rc\n else\n command pai \"$@\"\n fi\n}\n\n# Print the working directory after an interactive Claude Code session exits.\n# A SessionEnd hook can't do this reliably: Claude Code does not fire SessionEnd\n# on /exit (anthropics/claude-code#17885) and may reap the hook mid-run (#41577).\n# Running it from the shell, after \\`claude\\` returns, sidesteps both.\nclaude() {\n local __pai_skip=0 __pai_arg\n for __pai_arg in \"$@\"; do\n case \"$__pai_arg\" in\n -p|--print) __pai_skip=1 ;;\n esac\n done\n command claude \"$@\"\n local __pai_rc=$?\n if [[ -t 1 && $__pai_skip -eq 0 ]]; then\n printf '\\\\n\\\\033[2m📂 Working directory:\\\\033[0m %s\\\\n\\\\033[2m cd \"%s\"\\\\033[0m\\\\n' \"$PWD\" \"$PWD\"\n fi\n return $__pai_rc\n}\n`\n );\n });\n\n // -------------------------------------------------------------------------\n // pai [query] [pick] — DEFAULT COMMAND (must be registered LAST)\n // -------------------------------------------------------------------------\n\n program\n .command(\"query [query] [pick]\", { isDefault: true, hidden: true })\n .description(\n \"Find and launch a session by topic, name, or UUID.\\n\" +\n \"No arg → deduped session listing\\n\" +\n \"Name → switch live tab, or resume, or start fresh\\n\" +\n \"UUID → direct resume via filesystem scan\\n\" +\n \"Topic → history search → candidate list → pick #\"\n )\n .option(\"-y, --auto\", \"Auto-pick #1 without prompting\")\n .option(\"--dry-run\", \"Print what would happen without launching\")\n .option(\"-n, --n <count>\", \"Max candidates for history search\", \"20\")\n .option(\"--all\", \"Show all entries including cold / zero-session / archived projects\")\n .option(\"--list\", \"Skip the interactive picker; print the static session listing\")\n .action(async (query: string | undefined, pick: string | undefined, opts: { auto?: boolean; dryRun?: boolean; n?: string; all?: boolean; list?: boolean }) => {\n // No query → interactive fzf picker (projects + sessions, go where you pick).\n // `--list` (or a non-TTY pipe, handled inside cmdPick) falls back to the\n // static listing. Any query still goes through the name/UUID/topic resolver.\n if (query === undefined && !opts.list) {\n await cmdPick(getDb(), { all: opts.all, dryRun: opts.dryRun });\n return;\n }\n const pickN = pick !== undefined ? parseInt(pick, 10) : undefined;\n await cmdMain(getDb(), query, pickN, opts);\n });\n\n // -------------------------------------------------------------------------\n // Error handling\n // -------------------------------------------------------------------------\n\n program.configureOutput({\n writeErr: (str) => process.stderr.write(err(str)),\n });\n\n program.exitOverride((error) => {\n if (error.code === \"commander.helpDisplayed\" || error.code === \"commander.version\") {\n process.exit(0);\n }\n if (\n error.code === \"commander.missingArgument\" ||\n error.code === \"commander.unknownOption\" ||\n error.code === \"commander.unknownCommand\"\n ) {\n process.exit(1);\n }\n console.error(err(error.message));\n process.exit(1);\n });\n\n return program;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"program.mjs","names":[],"sources":["../../src/cli/program.ts"],"sourcesContent":["/**\n * PAI CLI — Commander program builder.\n *\n * Constructs the full `pai` command tree WITHOUT parsing argv. This separation\n * lets the documentation generator (scripts/build-docs.mjs) import and introspect\n * the live program object — the single source of truth for `--help`, the\n * generated man pages, and `pai help <area>`.\n *\n * The thin entry point (cli/index.ts) imports buildProgram() and calls parse().\n */\n\nimport { Command } from \"commander\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { openRegistry } from \"../registry/db.js\";\nimport type { Database } from \"better-sqlite3\";\nimport { registerProjectsCommands } from \"./commands/project/projects-index.js\";\nimport { findMovedPath } from \"./commands/project/commands.js\";\nimport { existsSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { registerRegistryCommands } from \"./commands/registry.js\";\nimport { registerMemoryCommands } from \"./commands/memory.js\";\nimport { registerIdentityCommands } from \"./commands/identity.js\";\nimport { registerMcpCommands } from \"./commands/mcp.js\";\nimport { registerDaemonCommands } from \"./commands/daemon.js\";\nimport { registerBackupCommands } from \"./commands/backup.js\";\nimport { registerRestoreCommands } from \"./commands/restore.js\";\nimport { registerSetupCommand } from \"./commands/setup.js\";\nimport { registerObsidianCommands } from \"./commands/obsidian.js\";\nimport { registerZettelCommands } from \"./commands/zettel.js\";\nimport { registerObservationCommands } from \"./commands/observation.js\";\nimport { registerSkillCommands } from \"./commands/skill.js\";\nimport { registerUpdateCommand } from \"./commands/update.js\";\nimport { registerNotifyCommands } from \"./commands/notify.js\";\nimport { registerTaskCommands } from \"./commands/task.js\";\nimport { registerTopicCommands } from \"./commands/topic.js\";\nimport { registerKgCommands } from \"./commands/kg.js\";\nimport { registerDbCommands } from \"./commands/db.js\";\nimport { registerHelpCommand } from \"./commands/help.js\";\nimport { registerSessionCommands } from \"./commands/session/index.js\";\nimport { registerSessionCleanupCommand } from \"./commands/session-cleanup/index.js\";\nimport { err, warn, ok, dim, shortenPath, encodeDir, now } from \"./utils.js\";\nimport { cmdPause } from \"./commands/session/pause.js\";\nimport { cmdEnd } from \"./commands/session/end.js\";\nimport { cmdGoto } from \"./commands/session/goto.js\";\nimport { cmdPauseAll } from \"./commands/session/pause-all.js\";\nimport { cmdClearNames } from \"./commands/session/clear-names.js\";\nimport { cmdList as cmdNotesList } from \"./commands/session/commands.js\";\nimport { resolveIdentifier } from \"./commands/project/helpers.js\";\nimport { cmdFind } from \"./commands/find.js\";\nimport { cmdMain } from \"./commands/main-resolver.js\";\nimport { cmdPick } from \"./commands/pick.js\";\n\n// ---------------------------------------------------------------------------\n// Version resolution\n// ---------------------------------------------------------------------------\n\nfunction getVersion(): string {\n try {\n const __dirname = dirname(fileURLToPath(import.meta.url));\n const pkgPath = join(__dirname, \"../../package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Lazy database singleton\n// ---------------------------------------------------------------------------\n\nlet _db: Database | null = null;\n\nfunction getDb(): Database {\n if (!_db) {\n try {\n _db = openRegistry();\n } catch (e) {\n console.error(err(`Failed to open PAI registry: ${e}`));\n process.exit(1);\n }\n }\n return _db;\n}\n\n// ---------------------------------------------------------------------------\n// Program builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build and return the fully configured `pai` Commander program.\n * Does NOT call parse — the caller (cli/index.ts) does that.\n */\nexport function buildProgram(): Command {\n const program = new Command();\n\n program\n .name(\"pai\")\n .description(\"PAI Knowledge OS — Personal AI Infrastructure CLI\")\n .version(getVersion(), \"-V, --version\", \"Print version and exit\")\n .addHelpText(\n \"after\",\n `\nDaily commands:\n pai Show your work: live tabs + recent sessions + recent projects\n pai <name> Switch / resume / fresh — universal\n pai pause [all] Pause current (or all live) sessions\n pai end Finalize current session\n\nSubcommands (rarely needed):\n pai projects ... Project registry management (cd, rebind, info, ...)\n pai registry ... Registry maintenance (scan, ...)\n pai memory ... Memory engine (index, search, ...)\n pai shell-init Shell integration (eval \"$(pai shell-init)\")\n\nRecovery:\n pai clear-names Wipe corrupt iTerm tab-name store\n\nExamples:\n pai aibroker Switch to the live AIBroker tab in iTerm\n pai mdf Find sessions where you worked on MDF\n pai 81c5c3dc Resume session by UUID prefix\n pai --all Show everything including cold and zero-session projects`\n );\n\n // -------------------------------------------------------------------------\n // pai projects (canonical plural namespace)\n // -------------------------------------------------------------------------\n\n const projectsCmd = program\n .command(\"projects\")\n .description(\"Manage registered projects (list, cd, add, info, ...)\");\n\n registerProjectsCommands(projectsCmd, getDb);\n\n // Singular alias: `pai project` → `pai projects`\n const projectCmd = program\n .command(\"project\")\n .description(\"Alias for `pai projects`\");\n\n registerProjectsCommands(projectCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai sessions (alias for the unified `pai` listing)\n // -------------------------------------------------------------------------\n\n // -------------------------------------------------------------------------\n // pai session <subcommand>\n //\n // These were written, exported and never wired: neither\n // registerSessionCommands nor registerSessionCleanupCommand was called from\n // anywhere, so every `pai session <x>` fell through to the default `query`\n // resolver and was interpreted as a prompt-history search.\n //\n // That is why session-stop.sh has been a near no-op — all four of its calls\n // (`session cleanup --execute`, `session checkpoint`, `session slug --apply`,\n // `session handover`) failed silently behind `2>/dev/null || true`.\n // -------------------------------------------------------------------------\n\n const sessionCmd = program\n .command(\"session\")\n .description(\n \"Session management: list, info, checkpoint, handover, cleanup, slug, tag, route.\\n\" +\n \"Short forms: `pai pause`, `pai end`, `pai resume <name>`.\"\n );\n\n registerSessionCommands(sessionCmd, getDb);\n registerSessionCleanupCommand(sessionCmd, getDb);\n\n program\n .command(\"sessions\")\n .description(\"Alias for `pai` — show the unified deduped listing.\")\n .option(\"--all\", \"Show all entries including cold / zero-session / archived projects\")\n .option(\"-n, --n <count>\", \"Max candidates for history search\", \"20\")\n .action(async (opts: { all?: boolean; n?: string }) => {\n await cmdMain(getDb(), undefined, undefined, opts);\n });\n\n // -------------------------------------------------------------------------\n // pai registry\n // -------------------------------------------------------------------------\n\n const registryCmd = program\n .command(\"registry\")\n .description(\"Registry maintenance: scan, migrate, stats, rebuild\");\n\n registerRegistryCommands(registryCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai memory\n // -------------------------------------------------------------------------\n\n const memoryCmd = program\n .command(\"memory\")\n .description(\"Memory engine: index, search, and status\");\n\n registerMemoryCommands(memoryCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai identity\n // -------------------------------------------------------------------------\n\n const identityCmd = program\n .command(\"identity\")\n .description(\"Declare who you are: self addresses and where mail is delivered\");\n\n registerIdentityCommands(identityCmd);\n\n // -------------------------------------------------------------------------\n // pai mcp\n // -------------------------------------------------------------------------\n\n const mcpCmd = program\n .command(\"mcp\")\n .description(\"MCP server management: install and status\");\n\n registerMcpCommands(mcpCmd);\n\n // -------------------------------------------------------------------------\n // pai daemon\n // -------------------------------------------------------------------------\n\n const daemonCmd = program\n .command(\"daemon\")\n .description(\"PAI daemon management: serve, status, restart, install, uninstall, logs\");\n\n registerDaemonCommands(daemonCmd);\n\n // -------------------------------------------------------------------------\n // pai backup / pai restore\n // -------------------------------------------------------------------------\n\n registerBackupCommands(program);\n registerRestoreCommands(program);\n\n // -------------------------------------------------------------------------\n // pai setup / pai update\n // -------------------------------------------------------------------------\n\n registerSetupCommand(program);\n registerUpdateCommand(program);\n\n // -------------------------------------------------------------------------\n // pai notify\n // -------------------------------------------------------------------------\n\n const notifyCmd = program\n .command(\"notify\")\n .description(\"Notification config: status, get, set, test, send\");\n\n registerNotifyCommands(notifyCmd);\n\n // -------------------------------------------------------------------------\n // pai task\n // -------------------------------------------------------------------------\n\n const taskCmd = program\n .command(\"task\")\n .alias(\"tasks\")\n .description(\"Task bus: list, add, dispatch, and complete cross-session work\");\n\n registerTaskCommands(taskCmd);\n\n // -------------------------------------------------------------------------\n // pai topic\n // -------------------------------------------------------------------------\n\n const topicCmd = program\n .command(\"topic\")\n .description(\"Topic shift detection: check whether context has drifted to a different project\");\n\n registerTopicCommands(topicCmd);\n\n // -------------------------------------------------------------------------\n // pai kg\n // -------------------------------------------------------------------------\n\n const kgCmd = program\n .command(\"kg\")\n .description(\"Temporal knowledge graph: backfill, query, list, stats\");\n\n registerKgCommands(kgCmd);\n\n // -------------------------------------------------------------------------\n // pai db\n // -------------------------------------------------------------------------\n\n const dbCmd = program\n .command(\"db\")\n .description(\"Database inspection: query, tables, schema (sqlite or postgres)\");\n\n registerDbCommands(dbCmd);\n\n // -------------------------------------------------------------------------\n // pai obsidian\n // -------------------------------------------------------------------------\n\n const obsidianCmd = program\n .command(\"obsidian\")\n .description(\"Obsidian vault: sync project notes, view status, open in Obsidian\");\n\n registerObsidianCommands(obsidianCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai zettel\n // -------------------------------------------------------------------------\n\n const zettelCmd = program\n .command(\"zettel\")\n .description(\"Zettelkasten intelligence: explore, surprise, converse, themes, health, suggest\");\n\n registerZettelCommands(zettelCmd, getDb);\n\n // -------------------------------------------------------------------------\n // pai observation\n // -------------------------------------------------------------------------\n\n const observationCmd = program\n .command(\"observation\")\n .description(\"Observation capture: list, search, and stats\");\n\n registerObservationCommands(observationCmd);\n\n // -------------------------------------------------------------------------\n // pai skill\n // -------------------------------------------------------------------------\n\n const skillCmd = program\n .command(\"skill\")\n .description(\"Skill telemetry and (future) discovery for the self-educating skill system\");\n\n registerSkillCommands(skillCmd);\n\n // -------------------------------------------------------------------------\n // pai help [area] — rich man-page viewer for the generated docs\n // -------------------------------------------------------------------------\n\n registerHelpCommand(program);\n\n // -------------------------------------------------------------------------\n // DAILY VERBS — top-level\n // -------------------------------------------------------------------------\n\n // pai pause [all] [--dry-run] [--exit] [--wait <ms>]\n program\n .command(\"pause [target]\")\n .description(\n \"Save state and display safe-exit instructions for the current session.\\n\" +\n \"Writes a ## Continue checkpoint to the project's TODO.md.\\n\" +\n \"Use `pai pause all` to pause every live session via AIBroker.\"\n )\n .option(\"--dry-run\", \"Preview changes without writing them\")\n .option(\n \"--body-file <path>\",\n \"File holding the checkpoint markdown to persist ('-' reads stdin). Required unless --no-body.\"\n )\n .option(\n \"--session-id <uuid>\",\n \"Claude Code session UUID — recorded as the `claude --resume` handle\"\n )\n .option(\n \"--no-body\",\n \"Deliberately write a metadata-only checkpoint (no content)\"\n )\n .option(\"--exit\", \"(pause all only) Also send /exit to each session after pausing\")\n .option(\n \"--wait <ms>\",\n \"(pause all only) Max milliseconds to wait for a session to finish its\\n\" +\n \"checkpoint before /exit. Not a fixed delay — sessions are exited as soon\\n\" +\n \"as they return to the prompt, and any still busy at the deadline are left\\n\" +\n \"open rather than killed mid-write. (default: 180000)\",\n \"180000\"\n )\n .action(async (target: string | undefined, opts: { dryRun?: boolean; exit?: boolean; wait?: string; bodyFile?: string; sessionId?: string; body?: boolean }) => {\n if (target === \"all\") {\n await cmdPauseAll({\n exit: opts.exit,\n dryRun: opts.dryRun,\n wait: opts.wait !== undefined ? parseInt(opts.wait, 10) : undefined,\n });\n } else {\n if (target !== undefined) {\n console.error(err(`Unknown target: ${target}. Did you mean 'pai pause all'?`));\n process.exitCode = 1;\n return;\n }\n // Commander maps `--no-body` to opts.body === false.\n cmdPause(getDb(), {\n dryRun: opts.dryRun,\n bodyFile: opts.bodyFile,\n sessionId: opts.sessionId,\n noBody: opts.body === false,\n });\n }\n });\n\n // pai end [--body-file <path>] [--session-id <uuid>] [--no-body] [--dry-run]\n program\n .command(\"end\")\n .description(\n \"Finalize a session: save state, mark note Completed, display safe-exit instructions.\"\n )\n .option(\"--dry-run\", \"Preview all changes without writing them\")\n .option(\n \"--body-file <path>\",\n \"File holding the checkpoint markdown to persist ('-' reads stdin). Required unless --no-body.\"\n )\n .option(\n \"--session-id <uuid>\",\n \"Claude Code session UUID — recorded as the `claude --resume` handle\"\n )\n .option(\n \"--no-body\",\n \"Deliberately write a metadata-only checkpoint (no content)\"\n )\n .action((opts: { dryRun?: boolean; bodyFile?: string; sessionId?: string; body?: boolean }) => {\n cmdEnd(getDb(), {\n dryRun: opts.dryRun,\n bodyFile: opts.bodyFile,\n sessionId: opts.sessionId,\n noBody: opts.body === false,\n });\n });\n\n // pai clear-names [--dry-run] — recovery: wipe corrupt iTerm/JSON name store\n program\n .command(\"clear-names\")\n .description(\n \"Recovery: wipe corrupted iTerm2 session name state.\\n\" +\n \"Clears ~/.aibroker/session-names.json AND user.paiName from all live iTerm2 sessions.\\n\" +\n \"After running this, use /Name to re-label each tab.\"\n )\n .option(\"--dry-run\", \"Preview what would be cleared without making changes\")\n .action(async (opts: { dryRun?: boolean }) => {\n await cmdClearNames(opts);\n });\n\n // -------------------------------------------------------------------------\n // HIDDEN COMPAT ALIASES\n // -------------------------------------------------------------------------\n\n // pai resume <name> [--dry-run] — hidden; prefer `pai <name>`\n program\n .command(\"resume <name>\", { hidden: true })\n .description(\"Go to a session by name or UUID (prefer: pai <name>)\")\n .option(\"--dry-run\", \"Print the exact argv and cwd, then exit without launching\")\n .action((name: string, opts: { dryRun?: boolean }) => {\n cmdGoto(getDb(), name, { dryRun: opts.dryRun });\n });\n\n // pai cd <identifier> — hidden; shell wrapper uses this\n program\n .command(\"cd <identifier>\", { hidden: true })\n .description(\"cd to a project directory (shell wrapper handles the actual cd)\")\n .action((identifier: string) => {\n const db = getDb();\n const project = resolveIdentifier(db, identifier);\n if (!project) {\n console.error(`Project not found: ${identifier}`);\n process.exit(1);\n return;\n }\n\n if (existsSync(project.root_path)) {\n process.stdout.write(project.root_path + \"\\n\");\n return;\n }\n\n process.stderr.write(\n warn(`Path not found: ${project.root_path}\\n`) +\n dim(\" Searching for moved location...\\n\")\n );\n\n const result = findMovedPath(project.root_path);\n\n if (result.found) {\n const newPath = result.found;\n const newEncoded = encodeDir(newPath);\n const ts = now();\n db.prepare(\n \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(newPath, newEncoded, ts, project.id);\n process.stderr.write(\n ok(`Project moved: ${shortenPath(project.root_path, 50)}\\n`) +\n dim(` → ${newPath}\\n`) +\n ok(\"Registry updated.\\n\")\n );\n process.stdout.write(newPath + \"\\n\");\n return;\n }\n\n if (result.ambiguous) {\n process.stderr.write(\n warn(`Multiple directories named \"${basename(project.root_path)}\" found:\\n`)\n );\n for (const candidate of result.ambiguous!) {\n process.stderr.write(dim(` ${candidate}\\n`));\n }\n process.stderr.write(\n dim(`\\n Disambiguate with: pai projects rebind ${project.slug} <path>\\n`)\n );\n process.exitCode = 1;\n return;\n }\n\n process.stderr.write(\n err(\n `Project \"${project.slug}\" root_path \"${project.root_path}\" does not exist on disk\\n` +\n ` and no folder named \"${basename(project.root_path)}\" was found in scan dirs.\\n`\n ) +\n dim(` Fix with: pai projects rebind ${project.slug} <new-path>\\n`)\n );\n process.exitCode = 1;\n });\n\n // -------------------------------------------------------------------------\n // pai notes — hidden power-user alias; accessible but not in main help\n // -------------------------------------------------------------------------\n\n const notesCmd = program\n .command(\"notes [project-slug]\", { hidden: true })\n .description(\"Markdown session notes — list notes, optionally filtered to a project.\")\n .option(\"--limit <n>\", \"Maximum number of notes to show\", \"20\")\n .option(\"--status <status>\", \"Filter by status: open | completed | compacted\")\n .action(\n (projectSlug: string | undefined, opts: { limit?: string; status?: string }) => {\n cmdNotesList(getDb(), projectSlug, opts);\n }\n );\n\n notesCmd\n .command(\"list [project-slug]\")\n .description(\"List markdown session notes (same as: pai notes)\")\n .option(\"--limit <n>\", \"Maximum number of notes to show\", \"20\")\n .option(\"--status <status>\", \"Filter by status: open | completed | compacted\")\n .action(\n (projectSlug: string | undefined, opts: { limit?: string; status?: string }) => {\n cmdNotesList(getDb(), projectSlug, opts);\n }\n );\n\n // -------------------------------------------------------------------------\n // pai find <query> — hidden power-user alias for history search\n // -------------------------------------------------------------------------\n\n program\n .command(\"find <query>\", { hidden: true })\n .description(\"Search prompt history for matching sessions.\")\n .option(\"-n, --n <count>\", \"Maximum number of sessions to show\", \"20\")\n .option(\"--json\", \"Output as JSON array\")\n .action(async (query: string, opts: { n?: string; json?: boolean }) => {\n await cmdFind(query, opts);\n });\n\n // -------------------------------------------------------------------------\n // pai shell-init — emit shell function for eval \"$(pai shell-init)\"\n // -------------------------------------------------------------------------\n\n program\n .command(\"shell-init\")\n .description('Emit shell integration code. Add to ~/.zshrc: eval \"$(pai shell-init)\"')\n .action(() => {\n process.stdout.write(\n `# PAI shell integration — generated by: pai shell-init\npai() {\n if [[ \"$1\" == \"cd\" ]]; then\n local dir=$(command pai cd \"$2\" 2>/dev/null)\n if [[ -n \"$dir\" && -d \"$dir\" ]]; then\n builtin cd \"$dir\"\n echo \"-> $dir\"\n else\n echo \"Project not found: $2\" >&2\n return 1\n fi\n elif [[ \"$1\" == \"projects\" && \"$2\" == \"cd\" ]]; then\n local dir=$(command pai projects cd \"$3\" 2>/dev/null)\n if [[ -n \"$dir\" && -d \"$dir\" ]]; then\n builtin cd \"$dir\"\n echo \"-> $dir\"\n else\n echo \"Project not found: $3\" >&2\n return 1\n fi\n elif [[ $# -eq 0 ]]; then\n # Bare \\`pai\\` → interactive picker. fzf draws on /dev/tty; the picker writes\n # a chosen directory to PAI_PICK_OUT when you press Ctrl-G (cd only), which we\n # then cd into here (a child process can't change this shell's cwd itself).\n local __pai_f\n __pai_f=$(mktemp -t pai-pick 2>/dev/null) || { command pai; return $?; }\n PAI_PICK_OUT=\"$__pai_f\" command pai\n local __pai_rc=$?\n local __pai_dir=\"\"\n [[ -s \"$__pai_f\" ]] && __pai_dir=$(cat \"$__pai_f\")\n command rm -f \"$__pai_f\"\n if [[ -n \"$__pai_dir\" && -d \"$__pai_dir\" ]]; then\n builtin cd \"$__pai_dir\"\n echo \"-> $__pai_dir\"\n fi\n return $__pai_rc\n else\n command pai \"$@\"\n fi\n}\n\n# Print the working directory after an interactive Claude Code session exits.\n# A SessionEnd hook can't do this reliably: Claude Code does not fire SessionEnd\n# on /exit (anthropics/claude-code#17885) and may reap the hook mid-run (#41577).\n# Running it from the shell, after \\`claude\\` returns, sidesteps both.\nclaude() {\n local __pai_skip=0 __pai_arg\n for __pai_arg in \"$@\"; do\n case \"$__pai_arg\" in\n -p|--print) __pai_skip=1 ;;\n esac\n done\n command claude \"$@\"\n local __pai_rc=$?\n if [[ -t 1 && $__pai_skip -eq 0 ]]; then\n printf '\\\\n\\\\033[2m📂 Working directory:\\\\033[0m %s\\\\n\\\\033[2m cd \"%s\"\\\\033[0m\\\\n' \"$PWD\" \"$PWD\"\n fi\n return $__pai_rc\n}\n`\n );\n });\n\n // -------------------------------------------------------------------------\n // pai [query] [pick] — DEFAULT COMMAND (must be registered LAST)\n // -------------------------------------------------------------------------\n\n program\n .command(\"query [query] [pick]\", { isDefault: true, hidden: true })\n .description(\n \"Find and launch a session by topic, name, or UUID.\\n\" +\n \"No arg → deduped session listing\\n\" +\n \"Name → switch live tab, or resume, or start fresh\\n\" +\n \"UUID → direct resume via filesystem scan\\n\" +\n \"Topic → history search → candidate list → pick #\"\n )\n .option(\"-y, --auto\", \"Auto-pick #1 without prompting\")\n .option(\"--dry-run\", \"Print what would happen without launching\")\n .option(\"-n, --n <count>\", \"Max candidates for history search\", \"20\")\n .option(\"--all\", \"Show all entries including cold / zero-session / archived projects\")\n .option(\"--list\", \"Skip the interactive picker; print the static session listing\")\n .action(async (query: string | undefined, pick: string | undefined, opts: { auto?: boolean; dryRun?: boolean; n?: string; all?: boolean; list?: boolean }) => {\n // No query → interactive fzf picker (projects + sessions, go where you pick).\n // `--list` (or a non-TTY pipe, handled inside cmdPick) falls back to the\n // static listing. Any query still goes through the name/UUID/topic resolver.\n if (query === undefined && !opts.list) {\n await cmdPick(getDb(), { all: opts.all, dryRun: opts.dryRun });\n return;\n }\n const pickN = pick !== undefined ? parseInt(pick, 10) : undefined;\n await cmdMain(getDb(), query, pickN, opts);\n });\n\n // -------------------------------------------------------------------------\n // Error handling\n // -------------------------------------------------------------------------\n\n program.configureOutput({\n writeErr: (str) => process.stderr.write(err(str)),\n });\n\n program.exitOverride((error) => {\n if (error.code === \"commander.helpDisplayed\" || error.code === \"commander.version\") {\n process.exit(0);\n }\n if (\n error.code === \"commander.missingArgument\" ||\n error.code === \"commander.unknownOption\" ||\n error.code === \"commander.unknownCommand\"\n ) {\n process.exit(1);\n }\n console.error(err(error.message));\n process.exit(1);\n });\n\n return program;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { t as STOP_WORDS } from "./stop-words-BaMEGVeY.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/graph/clusters.ts
|
|
4
|
+
/**
|
|
5
|
+
* Query pai_observations (Postgres) for observation types associated with
|
|
6
|
+
* the given file paths. Returns a map from vault_path → type counts.
|
|
7
|
+
*
|
|
8
|
+
* Falls back to an empty map when the pool is not available or the query fails.
|
|
9
|
+
*/
|
|
10
|
+
async function fetchObservationTypes(pool, filePaths, projectId) {
|
|
11
|
+
if (filePaths.length === 0) return /* @__PURE__ */ new Map();
|
|
12
|
+
try {
|
|
13
|
+
const params = [...filePaths];
|
|
14
|
+
let projectFilter = "";
|
|
15
|
+
if (projectId !== void 0) {
|
|
16
|
+
params.push(projectId);
|
|
17
|
+
projectFilter = `AND project_id = $${params.length}`;
|
|
18
|
+
}
|
|
19
|
+
const result = await pool.query(`SELECT unnested_path AS path, type, COUNT(*) AS cnt
|
|
20
|
+
FROM pai_observations,
|
|
21
|
+
LATERAL unnest(files_modified || files_read) AS unnested_path
|
|
22
|
+
WHERE unnested_path = ANY($1::text[])
|
|
23
|
+
${projectFilter}
|
|
24
|
+
GROUP BY unnested_path, type`, [filePaths, ...params.slice(filePaths.length)]);
|
|
25
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const row of result.rows) {
|
|
27
|
+
const existing = byPath.get(row.path) ?? {};
|
|
28
|
+
existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);
|
|
29
|
+
byPath.set(row.path, existing);
|
|
30
|
+
}
|
|
31
|
+
return byPath;
|
|
32
|
+
} catch {
|
|
33
|
+
return /* @__PURE__ */ new Map();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Aggregate per-path observation type counts into cluster-level counts,
|
|
38
|
+
* then pick the dominant type.
|
|
39
|
+
*/
|
|
40
|
+
function aggregateObservationTypes(paths, byPath) {
|
|
41
|
+
const counts = {};
|
|
42
|
+
for (const path of paths) {
|
|
43
|
+
const pathCounts = byPath.get(path);
|
|
44
|
+
if (!pathCounts) continue;
|
|
45
|
+
for (const [type, n] of Object.entries(pathCounts)) counts[type] = (counts[type] ?? 0) + n;
|
|
46
|
+
}
|
|
47
|
+
let dominant = "unknown";
|
|
48
|
+
let maxCount = 0;
|
|
49
|
+
for (const [type, n] of Object.entries(counts)) if (n > maxCount) {
|
|
50
|
+
maxCount = n;
|
|
51
|
+
dominant = type;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
dominant,
|
|
55
|
+
counts
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const SKIP_PREFIXES = [
|
|
59
|
+
"Attachments/",
|
|
60
|
+
"🗓️ Daily Notes/",
|
|
61
|
+
"Copilot/copilot-conversations/",
|
|
62
|
+
"Z - Zettelkasten/Tweets/"
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Cluster vault notes by wikilink connectivity when embeddings aren't available.
|
|
66
|
+
* Uses BFS to find connected components in the link graph, then picks the
|
|
67
|
+
* largest components as clusters. Labels are derived from the most common
|
|
68
|
+
* title words in each component.
|
|
69
|
+
*/
|
|
70
|
+
async function clusterByLinks(backend, lookbackDays, minSize, maxClusters) {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const from = now - lookbackDays * 864e5;
|
|
73
|
+
const recentNotes = (await backend.getRecentVaultFiles(from)).filter((f) => f.vaultPath.endsWith(".md"));
|
|
74
|
+
const noteMap = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const n of recentNotes) noteMap.set(n.vaultPath, {
|
|
76
|
+
title: n.title,
|
|
77
|
+
indexed_at: n.indexedAt
|
|
78
|
+
});
|
|
79
|
+
const adj = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const path of noteMap.keys()) if (!adj.has(path)) adj.set(path, /* @__PURE__ */ new Set());
|
|
81
|
+
const linkGraph = await backend.getVaultLinkGraph();
|
|
82
|
+
for (const { source_path, target_path } of linkGraph) if (noteMap.has(source_path) && noteMap.has(target_path)) {
|
|
83
|
+
adj.get(source_path).add(target_path);
|
|
84
|
+
adj.get(target_path).add(source_path);
|
|
85
|
+
}
|
|
86
|
+
const degrees = [...adj.entries()].map(([p, s]) => ({
|
|
87
|
+
path: p,
|
|
88
|
+
degree: s.size
|
|
89
|
+
}));
|
|
90
|
+
degrees.sort((a, b) => b.degree - a.degree);
|
|
91
|
+
const hubThreshold = Math.max(10, degrees[Math.floor(degrees.length * .05)]?.degree ?? 10);
|
|
92
|
+
const hubNodes = /* @__PURE__ */ new Set();
|
|
93
|
+
for (const { path, degree } of degrees) if (degree >= hubThreshold) hubNodes.add(path);
|
|
94
|
+
else break;
|
|
95
|
+
for (const hub of hubNodes) adj.delete(hub);
|
|
96
|
+
for (const [, neighbors] of adj) for (const hub of hubNodes) neighbors.delete(hub);
|
|
97
|
+
const visited = /* @__PURE__ */ new Set();
|
|
98
|
+
const components = [];
|
|
99
|
+
for (const path of noteMap.keys()) {
|
|
100
|
+
if (visited.has(path) || hubNodes.has(path)) continue;
|
|
101
|
+
if (SKIP_PREFIXES.some((p) => path.startsWith(p))) {
|
|
102
|
+
visited.add(path);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const component = [];
|
|
106
|
+
const queue = [path];
|
|
107
|
+
visited.add(path);
|
|
108
|
+
while (queue.length > 0) {
|
|
109
|
+
const current = queue.shift();
|
|
110
|
+
component.push(current);
|
|
111
|
+
const neighbors = adj.get(current);
|
|
112
|
+
if (!neighbors) continue;
|
|
113
|
+
for (const neighbor of neighbors) if (!visited.has(neighbor) && !SKIP_PREFIXES.some((p) => neighbor.startsWith(p))) {
|
|
114
|
+
visited.add(neighbor);
|
|
115
|
+
queue.push(neighbor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (component.length >= minSize) components.push(component);
|
|
119
|
+
}
|
|
120
|
+
components.sort((a, b) => b.length - a.length);
|
|
121
|
+
const topComponents = components.slice(0, maxClusters);
|
|
122
|
+
function generateLinkLabel(paths) {
|
|
123
|
+
const wordCounts = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const p of paths) {
|
|
125
|
+
const title = noteMap.get(p)?.title;
|
|
126
|
+
if (!title) continue;
|
|
127
|
+
const words = title.toLowerCase().replace(/[^a-z0-9äöüàéèêëçñß\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
128
|
+
for (const word of words) wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
return [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([w]) => w).join(" / ") || "Linked Notes";
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
themes: topComponents.map((component, idx) => {
|
|
134
|
+
const notes = component.map((p) => ({
|
|
135
|
+
path: p,
|
|
136
|
+
title: noteMap.get(p)?.title ?? null
|
|
137
|
+
}));
|
|
138
|
+
const avgRecency = component.reduce((sum, p) => sum + (noteMap.get(p)?.indexed_at ?? 0), 0) / component.length;
|
|
139
|
+
const uniqueFolders = new Set(component.map((p) => p.split("/")[0]));
|
|
140
|
+
return {
|
|
141
|
+
id: idx,
|
|
142
|
+
label: generateLinkLabel(component),
|
|
143
|
+
notes,
|
|
144
|
+
size: component.length,
|
|
145
|
+
folderDiversity: uniqueFolders.size / component.length,
|
|
146
|
+
avgRecency,
|
|
147
|
+
linkedRatio: 1,
|
|
148
|
+
suggestIndexNote: component.length >= 10
|
|
149
|
+
};
|
|
150
|
+
}),
|
|
151
|
+
totalNotesAnalyzed: recentNotes.length,
|
|
152
|
+
timeWindow: {
|
|
153
|
+
from,
|
|
154
|
+
to: now
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function handleGraphClusters(pool, backend, params) {
|
|
159
|
+
const minSize = params.min_size ?? 3;
|
|
160
|
+
const maxClusters = params.max_clusters ?? 20;
|
|
161
|
+
const lookbackDays = params.lookback_days ?? 90;
|
|
162
|
+
if (!(params.project_id ?? 0)) throw new Error("graph_clusters: project_id is required (pass the vault project's numeric ID)");
|
|
163
|
+
const themeResult = await clusterByLinks(backend, lookbackDays, minSize, maxClusters);
|
|
164
|
+
const allPaths = themeResult.themes.flatMap((t) => t.notes.map((n) => n.path));
|
|
165
|
+
const observationsByPath = pool !== null ? await fetchObservationTypes(pool, allPaths, params.project_id) : /* @__PURE__ */ new Map();
|
|
166
|
+
const fileRows = await backend.getVaultFilesByPaths(allPaths);
|
|
167
|
+
const indexedAtMap = new Map(fileRows.map((f) => [f.vaultPath, f.indexedAt]));
|
|
168
|
+
const clusters = themeResult.themes.map((theme) => {
|
|
169
|
+
const notePaths = theme.notes.map((n) => n.path);
|
|
170
|
+
const notesWithTimestamps = theme.notes.map((n) => ({
|
|
171
|
+
vault_path: n.path,
|
|
172
|
+
title: n.title ?? n.path.split("/").pop() ?? n.path,
|
|
173
|
+
indexed_at: indexedAtMap.get(n.path) ?? 0
|
|
174
|
+
}));
|
|
175
|
+
const avgRecency = theme.avgRecency;
|
|
176
|
+
const { dominant, counts } = aggregateObservationTypes(notePaths, observationsByPath);
|
|
177
|
+
return {
|
|
178
|
+
id: theme.id,
|
|
179
|
+
label: theme.label,
|
|
180
|
+
size: theme.size,
|
|
181
|
+
folder_diversity: theme.folderDiversity,
|
|
182
|
+
avg_recency: avgRecency,
|
|
183
|
+
linked_ratio: theme.linkedRatio,
|
|
184
|
+
dominant_observation_type: dominant,
|
|
185
|
+
observation_type_counts: counts,
|
|
186
|
+
suggest_index_note: theme.suggestIndexNote,
|
|
187
|
+
has_idea_note: false,
|
|
188
|
+
notes: notesWithTimestamps
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
clusters.sort((a, b) => b.size - a.size);
|
|
192
|
+
return {
|
|
193
|
+
clusters: clusters.slice(0, maxClusters),
|
|
194
|
+
total_notes_analyzed: themeResult.totalNotesAnalyzed,
|
|
195
|
+
time_window: themeResult.timeWindow
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
export { handleGraphClusters };
|
|
201
|
+
//# sourceMappingURL=clusters-CRXU2T6Z.mjs.map
|