@tekmidian/pai 0.9.12 → 0.9.14

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.
Files changed (69) hide show
  1. package/dist/{auto-route-DL4ViDKC.mjs → auto-route-D54Kv7yI.mjs} +2 -2
  2. package/dist/{auto-route-DL4ViDKC.mjs.map → auto-route-D54Kv7yI.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +808 -345
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/{clusters-CRlPBpq8.mjs → clusters-BwmdKB-V.mjs} +1 -1
  6. package/dist/{clusters-CRlPBpq8.mjs.map → clusters-BwmdKB-V.mjs.map} +1 -1
  7. package/dist/{config-BuhHWyOK.mjs → config-DqBY3aT0.mjs} +1 -1
  8. package/dist/{config-BuhHWyOK.mjs.map → config-DqBY3aT0.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +12 -8
  10. package/dist/daemon/index.mjs.map +1 -1
  11. package/dist/daemon-BryEC3as.mjs +1426 -0
  12. package/dist/daemon-BryEC3as.mjs.map +1 -0
  13. package/dist/daemon-mcp/index.mjs +3 -3
  14. package/dist/daemon-mcp/index.mjs.map +1 -1
  15. package/dist/{detector-AzVtGLtL.mjs → detector-Q_lVXtj-.mjs} +1 -1
  16. package/dist/{detector-AzVtGLtL.mjs.map → detector-Q_lVXtj-.mjs.map} +1 -1
  17. package/dist/{factory-BDBIfTDC.mjs → factory-BufouUQ1.mjs} +3 -3
  18. package/dist/{factory-BDBIfTDC.mjs.map → factory-BufouUQ1.mjs.map} +1 -1
  19. package/dist/hooks/context-compression-hook.mjs +39 -0
  20. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  21. package/dist/hooks/stop-hook.mjs +36 -0
  22. package/dist/hooks/stop-hook.mjs.map +2 -2
  23. package/dist/index.mjs +2 -2
  24. package/dist/indexer-D7MvSQPY.mjs +1 -0
  25. package/dist/{indexer-backend-DXkDh-kc.mjs → indexer-backend-BrrCnasS.mjs} +1 -1
  26. package/dist/{indexer-backend-DXkDh-kc.mjs.map → indexer-backend-BrrCnasS.mjs.map} +1 -1
  27. package/dist/{ipc-client-CoyUHPod.mjs → ipc-client-C3pjwy2m.mjs} +1 -1
  28. package/dist/{ipc-client-CoyUHPod.mjs.map → ipc-client-C3pjwy2m.mjs.map} +1 -1
  29. package/dist/{kg-entity-D5v7RCDi.mjs → kg-entity-DVzTsy6G.mjs} +1 -1
  30. package/dist/{kg-entity-D5v7RCDi.mjs.map → kg-entity-DVzTsy6G.mjs.map} +1 -1
  31. package/dist/kg-extraction-r5KkTmWY.mjs +524 -0
  32. package/dist/kg-extraction-r5KkTmWY.mjs.map +1 -0
  33. package/dist/{latent-ideas-DvWBRHsy.mjs → latent-ideas-dWVuI_dg.mjs} +2 -2
  34. package/dist/{latent-ideas-DvWBRHsy.mjs.map → latent-ideas-dWVuI_dg.mjs.map} +1 -1
  35. package/dist/{migrate-jokLenje.mjs → migrate-fLD6rAdO.mjs} +2 -2
  36. package/dist/{migrate-jokLenje.mjs.map → migrate-fLD6rAdO.mjs.map} +1 -1
  37. package/dist/{neighborhood-u8ytjmWq.mjs → neighborhood-DixmpfPT.mjs} +1 -1
  38. package/dist/{neighborhood-u8ytjmWq.mjs.map → neighborhood-DixmpfPT.mjs.map} +1 -1
  39. package/dist/{note-context-CG2_e-0W.mjs → note-context-BY7rfGdl.mjs} +1 -1
  40. package/dist/{note-context-CG2_e-0W.mjs.map → note-context-BY7rfGdl.mjs.map} +1 -1
  41. package/dist/{postgres-BGERehmX.mjs → postgres-Cp25xZes.mjs} +1 -1
  42. package/dist/{postgres-BGERehmX.mjs.map → postgres-Cp25xZes.mjs.map} +1 -1
  43. package/dist/{query-feedback-CQSumXDy.mjs → query-feedback-C0hKUHVz.mjs} +1 -1
  44. package/dist/{query-feedback-CQSumXDy.mjs.map → query-feedback-C0hKUHVz.mjs.map} +1 -1
  45. package/dist/{sqlite-xUe94oUZ.mjs → sqlite-B3JHfJc2.mjs} +1 -1
  46. package/dist/{sqlite-xUe94oUZ.mjs.map → sqlite-B3JHfJc2.mjs.map} +1 -1
  47. package/dist/{state-BIlxNRUn.mjs → state-BHZAhLou.mjs} +1 -1
  48. package/dist/{state-BIlxNRUn.mjs.map → state-BHZAhLou.mjs.map} +1 -1
  49. package/dist/{themes-9jxFn3Rf.mjs → themes-DO8mUVwD.mjs} +1 -1
  50. package/dist/{themes-9jxFn3Rf.mjs.map → themes-DO8mUVwD.mjs.map} +1 -1
  51. package/dist/{tools-BNBvJNph.mjs → tools-C1JNgya7.mjs} +17 -16
  52. package/dist/tools-C1JNgya7.mjs.map +1 -0
  53. package/dist/{trace-C2XrzssW.mjs → trace-TYCLMCkY.mjs} +1 -1
  54. package/dist/{trace-C2XrzssW.mjs.map → trace-TYCLMCkY.mjs.map} +1 -1
  55. package/dist/{utils-QSfKagcj.mjs → utils-BAxjW3j8.mjs} +2 -2
  56. package/dist/{utils-QSfKagcj.mjs.map → utils-BAxjW3j8.mjs.map} +1 -1
  57. package/dist/{vault-indexer-iHRjcy6e.mjs → vault-indexer-DsPC6wQf.mjs} +1 -1
  58. package/dist/{vault-indexer-iHRjcy6e.mjs.map → vault-indexer-DsPC6wQf.mjs.map} +1 -1
  59. package/dist/{daemon-DaTQ-VtJ.mjs → work-queue-worker-94INtUiz.mjs} +54 -1398
  60. package/dist/work-queue-worker-94INtUiz.mjs.map +1 -0
  61. package/dist/{zettelkasten-BdaMzTGQ.mjs → zettelkasten-B-94vghH.mjs} +3 -3
  62. package/dist/{zettelkasten-BdaMzTGQ.mjs.map → zettelkasten-B-94vghH.mjs.map} +1 -1
  63. package/package.json +1 -1
  64. package/src/hooks/ts/pre-compact/context-compression-hook.ts +48 -0
  65. package/src/hooks/ts/stop/stop-hook.ts +40 -0
  66. package/dist/daemon-DaTQ-VtJ.mjs.map +0 -1
  67. package/dist/kg-extraction-Uvr9fTlz.mjs +0 -246
  68. package/dist/kg-extraction-Uvr9fTlz.mjs.map +0 -1
  69. package/dist/tools-BNBvJNph.mjs.map +0 -1
@@ -0,0 +1,524 @@
1
+ import { _ as warn, c as ok, i as err, n as dim, r as encodeDir, s as now, t as bold } from "./utils-BAxjW3j8.mjs";
2
+ import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-fLD6rAdO.mjs";
3
+ import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-CXQPX2P6.mjs";
4
+ import { i as kgAdd, o as kgInvalidate, r as upsertKgEntity, s as kgQuery } from "./kg-entity-DVzTsy6G.mjs";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { basename, join, resolve } from "node:path";
8
+
9
+ //#region src/cli/commands/registry/utils.ts
10
+ /**
11
+ * Upsert a project row. Returns { id, isNew }.
12
+ *
13
+ * Matching priority:
14
+ * 1. root_path — most reliable; handles slug collisions
15
+ * 2. encoded_dir — Claude project dirs are canonical
16
+ * 3. Insert with suffix-deduplication on slug collision
17
+ */
18
+ function upsertProject(db, slug, rootPath, encodedDir) {
19
+ const ts = now();
20
+ const byPath = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(rootPath);
21
+ if (byPath) {
22
+ const encodedOwner = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(encodedDir);
23
+ if (!encodedOwner || encodedOwner.id === byPath.id) db.prepare("UPDATE projects SET encoded_dir = ?, updated_at = ? WHERE id = ?").run(encodedDir, ts, byPath.id);
24
+ return {
25
+ id: byPath.id,
26
+ isNew: false
27
+ };
28
+ }
29
+ const byEncoded = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(encodedDir);
30
+ if (byEncoded) {
31
+ const pathOwner = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(rootPath);
32
+ if (!pathOwner || pathOwner.id === byEncoded.id) db.prepare("UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?").run(rootPath, ts, byEncoded.id);
33
+ return {
34
+ id: byEncoded.id,
35
+ isNew: false
36
+ };
37
+ }
38
+ let finalSlug = slug;
39
+ let attempt = 0;
40
+ while (true) {
41
+ if (!db.prepare("SELECT id FROM projects WHERE slug = ?").get(finalSlug)) break;
42
+ attempt++;
43
+ finalSlug = `${slug}-${attempt}`;
44
+ }
45
+ const result = db.prepare(`INSERT OR IGNORE INTO projects
46
+ (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)
47
+ VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`).run(finalSlug, finalSlug, rootPath, encodedDir, ts, ts);
48
+ if (result.changes === 0) {
49
+ const fallback = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(encodedDir) ?? db.prepare("SELECT id FROM projects WHERE root_path = ?").get(rootPath);
50
+ if (fallback) return {
51
+ id: fallback.id,
52
+ isNew: false
53
+ };
54
+ throw new Error(`upsertProject: INSERT OR IGNORE was suppressed but no matching row found for root_path=${rootPath} encoded_dir=${encodedDir}`);
55
+ }
56
+ return {
57
+ id: result.lastInsertRowid,
58
+ isNew: true
59
+ };
60
+ }
61
+ /** Upsert a session note. Returns true if newly inserted. */
62
+ function upsertSession(db, projectId, number, date, slug, title, filename) {
63
+ if (db.prepare("SELECT id FROM sessions WHERE project_id = ? AND number = ?").get(projectId, number)) return false;
64
+ const ts = now();
65
+ db.prepare(`INSERT INTO sessions
66
+ (project_id, number, date, slug, title, filename, status, created_at)
67
+ VALUES (?, ?, ?, ?, ?, ?, 'completed', ?)`).run(projectId, number, date, slug, title, filename, ts);
68
+ return true;
69
+ }
70
+
71
+ //#endregion
72
+ //#region src/cli/commands/registry/scan.ts
73
+ /** Registry scan command: walk ~/.claude/projects/ and populate the registry. */
74
+ const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
75
+ const PAI_CONFIG_DIR = join(homedir(), ".pai");
76
+ const PAI_CONFIG_FILE = join(PAI_CONFIG_DIR, "config.json");
77
+ function loadScanConfig() {
78
+ if (!existsSync(PAI_CONFIG_FILE)) return { scan_dirs: [] };
79
+ try {
80
+ return JSON.parse(readFileSync(PAI_CONFIG_FILE, "utf8"));
81
+ } catch {
82
+ return { scan_dirs: [] };
83
+ }
84
+ }
85
+ function saveScanConfig(config) {
86
+ mkdirSync(PAI_CONFIG_DIR, { recursive: true });
87
+ writeFileSync(PAI_CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf8");
88
+ }
89
+ function resolveHome(p) {
90
+ if (p.startsWith("~/")) return join(homedir(), p.slice(2));
91
+ return resolve(p);
92
+ }
93
+ /**
94
+ * Recursively find all .md files in a directory, including YYYY/MM subdirectories.
95
+ * Returns filenames (basename only).
96
+ */
97
+ function findNoteFiles(dir) {
98
+ const results = [];
99
+ if (!existsSync(dir)) return results;
100
+ for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isFile() && entry.name.endsWith(".md")) results.push(entry.name);
101
+ else if (entry.isDirectory() && /^\d{4}$/.test(entry.name)) {
102
+ const yearDir = join(dir, entry.name);
103
+ for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) if (monthEntry.isDirectory() && /^\d{2}$/.test(monthEntry.name)) {
104
+ const monthDir = join(yearDir, monthEntry.name);
105
+ for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) if (noteEntry.isFile() && noteEntry.name.endsWith(".md")) results.push(noteEntry.name);
106
+ }
107
+ }
108
+ return results;
109
+ }
110
+ function performScan(db) {
111
+ const result = {
112
+ projectsScanned: 0,
113
+ projectsNew: 0,
114
+ projectsUpdated: 0,
115
+ sessionsScanned: 0,
116
+ sessionsNew: 0,
117
+ skipped: []
118
+ };
119
+ if (!existsSync(CLAUDE_PROJECTS_DIR)) throw new Error(`Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`);
120
+ const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {
121
+ return statSync(join(CLAUDE_PROJECTS_DIR, name)).isDirectory();
122
+ });
123
+ const lookupMap = buildEncodedDirMap();
124
+ for (const encodedDir of entries) {
125
+ const rootPath = decodeEncodedDir(encodedDir, lookupMap);
126
+ if (!existsSync(rootPath)) {
127
+ result.skipped.push(`${encodedDir} (decoded: ${rootPath} — path not found on disk)`);
128
+ result.projectsScanned++;
129
+ continue;
130
+ }
131
+ const slug = slugify(basename(rootPath) || encodedDir);
132
+ const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);
133
+ result.projectsScanned++;
134
+ if (isNew) result.projectsNew++;
135
+ else result.projectsUpdated++;
136
+ try {
137
+ ensurePaiMarker(rootPath, slug);
138
+ } catch {}
139
+ const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, "Notes");
140
+ if (existsSync(claudeNotesDir)) {
141
+ if (claudeNotesDir !== join(rootPath, "Notes")) db.prepare("UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?").run(claudeNotesDir, Date.now(), id);
142
+ }
143
+ if (!existsSync(claudeNotesDir)) continue;
144
+ const noteFiles = findNoteFiles(claudeNotesDir);
145
+ for (const filename of noteFiles) {
146
+ const parsed = parseSessionFilename(filename);
147
+ if (!parsed) continue;
148
+ result.sessionsScanned++;
149
+ if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) result.sessionsNew++;
150
+ }
151
+ }
152
+ {
153
+ const activeProjects = db.prepare("SELECT id, slug, root_path FROM projects WHERE status = 'active'").all();
154
+ for (const project of activeProjects) {
155
+ const notesDir = join(project.root_path, "Notes");
156
+ if (!existsSync(notesDir)) continue;
157
+ let files;
158
+ try {
159
+ files = findNoteFiles(notesDir);
160
+ } catch {
161
+ continue;
162
+ }
163
+ for (const filename of files) {
164
+ const parsed = parseSessionFilename(filename);
165
+ if (!parsed) continue;
166
+ result.sessionsScanned++;
167
+ if (upsertSession(db, project.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) result.sessionsNew++;
168
+ }
169
+ }
170
+ }
171
+ const config = loadScanConfig();
172
+ if (config.scan_dirs.length) for (const rawDir of config.scan_dirs) {
173
+ const scanDir = resolveHome(rawDir);
174
+ if (!existsSync(scanDir)) {
175
+ result.skipped.push(`${rawDir} (configured scan_dir not found)`);
176
+ continue;
177
+ }
178
+ const children = readdirSync(scanDir).filter((name) => {
179
+ if (name.startsWith(".")) return false;
180
+ const full = join(scanDir, name);
181
+ try {
182
+ return statSync(full).isDirectory();
183
+ } catch {
184
+ return false;
185
+ }
186
+ });
187
+ for (const child of children) {
188
+ const childPath = join(scanDir, child);
189
+ const childSlug = slugify(child);
190
+ const childEncoded = encodeDir(childPath);
191
+ const existing = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(childPath);
192
+ if (existing) {
193
+ result.projectsScanned++;
194
+ result.projectsUpdated++;
195
+ try {
196
+ ensurePaiMarker(childPath, childSlug);
197
+ } catch {}
198
+ const notesDir = join(childPath, "Notes");
199
+ if (existsSync(notesDir)) {
200
+ const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(".md"));
201
+ for (const filename of noteFiles) {
202
+ const parsed = parseSessionFilename(filename);
203
+ if (!parsed) continue;
204
+ result.sessionsScanned++;
205
+ if (upsertSession(db, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) result.sessionsNew++;
206
+ }
207
+ }
208
+ continue;
209
+ }
210
+ const { id, isNew } = upsertProject(db, childSlug, childPath, childEncoded);
211
+ result.projectsScanned++;
212
+ if (isNew) result.projectsNew++;
213
+ else result.projectsUpdated++;
214
+ try {
215
+ ensurePaiMarker(childPath, childSlug);
216
+ } catch {}
217
+ const notesDir = join(childPath, "Notes");
218
+ if (existsSync(notesDir)) {
219
+ const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(".md"));
220
+ for (const filename of noteFiles) {
221
+ const parsed = parseSessionFilename(filename);
222
+ if (!parsed) continue;
223
+ result.sessionsScanned++;
224
+ if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) result.sessionsNew++;
225
+ }
226
+ }
227
+ }
228
+ }
229
+ if (config.scan_dirs.length) {
230
+ const markers = discoverPaiMarkers(config.scan_dirs.map(resolveHome).filter(existsSync));
231
+ for (const marker of markers) {
232
+ const registeredRow = db.prepare("SELECT id, root_path, slug FROM projects WHERE slug = ?").get(marker.slug);
233
+ if (!registeredRow) continue;
234
+ if (registeredRow.root_path !== marker.projectRoot) {
235
+ const newEncoded = encodeDir(marker.projectRoot);
236
+ const now4 = Date.now();
237
+ const encodedOwner = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(newEncoded);
238
+ const pathOwner = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(marker.projectRoot);
239
+ const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;
240
+ const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;
241
+ if (encodedSafe && pathSafe) db.prepare("UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?").run(marker.projectRoot, newEncoded, now4, registeredRow.id);
242
+ else if (pathSafe) db.prepare("UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?").run(marker.projectRoot, now4, registeredRow.id);
243
+ }
244
+ }
245
+ }
246
+ return result;
247
+ }
248
+ /**
249
+ * Run the registry scan CLI command.
250
+ *
251
+ * @param opts.quick When true, skips verbose output (same scan, less noise).
252
+ * The underlying scan is always incremental via upsert —
253
+ * this flag exists for hook/daemon-triggered invocations that
254
+ * want minimal log output.
255
+ */
256
+ function cmdScan(db, opts = {}) {
257
+ const config = loadScanConfig();
258
+ if (!opts.quick) {
259
+ console.log(dim("Scanning ~/.claude/projects/ ..."));
260
+ if (config.scan_dirs.length) console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(", ")}`));
261
+ console.log(dim("Scanning project-root Notes/ directories ..."));
262
+ }
263
+ let result;
264
+ try {
265
+ result = performScan(db);
266
+ } catch (e) {
267
+ console.error(err(String(e)));
268
+ process.exit(1);
269
+ }
270
+ if (!opts.quick) {
271
+ console.log(ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`));
272
+ console.log(dim(` Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`));
273
+ console.log(dim(` Sessions: ${result.sessionsNew} new`));
274
+ if (result.skipped.length) {
275
+ console.log();
276
+ console.log(warn(` ${result.skipped.length} project(s) skipped (path not found on disk):`));
277
+ for (const s of result.skipped.slice(0, 10)) console.log(dim(` ${s}`));
278
+ if (result.skipped.length > 10) console.log(dim(` ... and ${result.skipped.length - 10} more`));
279
+ }
280
+ } else process.stderr.write(`[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions (${result.projectsNew}+${result.sessionsNew} new).\n`);
281
+ }
282
+
283
+ //#endregion
284
+ //#region src/daemon/templates/triple-extraction-prompt.ts
285
+ /**
286
+ * triple-extraction-prompt.ts — Prompt template for KG triple extraction.
287
+ *
288
+ * Used by the session-summary-worker to extract structured facts from
289
+ * a completed session summary and store them in the temporal knowledge graph.
290
+ */
291
+ function buildTripleExtractionPrompt(params) {
292
+ return `Extract structured entities and relations from this coding session.
293
+
294
+ Output a single JSON object with two arrays: "entities" and "relations".
295
+
296
+ Entity types: project | person | concept | tool | file | version | decision | technology | organization
297
+
298
+ Rules:
299
+ - Be SPECIFIC: entity names must be concrete (e.g., "FSRS", "Glidr", "Matthias")
300
+ - Use snake_case relation verb phrases (e.g., "uses_algorithm", "decided_to", "shipped_version")
301
+ - Skip opinions, speculation, and "we should" statements
302
+ - Skip entities obvious from project metadata unless they have a meaningful relation
303
+ - Maximum 15 relations per session — pick the most important
304
+ - Each entity should have a brief description (1 sentence, what it is in this context)
305
+ - Each relation must reference entity names that appear in the entities array
306
+
307
+ Example output:
308
+ {
309
+ "entities": [
310
+ {"name": "Glidr", "type": "project", "description": "Flashcard app using FSRS spaced repetition"},
311
+ {"name": "FSRS", "type": "concept", "description": "Free Spaced Repetition Scheduler algorithm"},
312
+ {"name": "Matthias", "type": "person", "description": "Developer of Glidr and Quassl"},
313
+ {"name": "Quassl", "type": "project", "description": "iOS app being rewritten in Flutter"},
314
+ {"name": "Flutter", "type": "technology", "description": "Cross-platform mobile framework"}
315
+ ],
316
+ "relations": [
317
+ {"source": "Glidr", "relation": "uses_algorithm", "target": "FSRS"},
318
+ {"source": "Glidr", "relation": "shipped_version", "target": "1.0.5"},
319
+ {"source": "Matthias", "relation": "decided_to_rewrite", "target": "Quassl"},
320
+ {"source": "Quassl", "relation": "migrating_to", "target": "Flutter"}
321
+ ]
322
+ }
323
+
324
+ PROJECT: ${params.projectSlug}
325
+
326
+ SESSION CONTENT:
327
+ ${params.sessionContent}
328
+
329
+ GIT COMMITS:
330
+ ${params.gitLog}
331
+
332
+ JSON object (entities + relations):`;
333
+ }
334
+
335
+ //#endregion
336
+ //#region src/memory/kg-extraction.ts
337
+ /**
338
+ * kg-extraction.ts — Shared KG triple extraction logic.
339
+ *
340
+ * Extracted from session-summary-worker.ts so both the worker and the
341
+ * CLI backfill (`pai kg backfill`) can use the same code path.
342
+ *
343
+ * Provides:
344
+ * - findClaudeBinary() — locate the claude CLI
345
+ * - spawnClaude() — generic prompt -> response runner (strips ANTHROPIC_API_KEY)
346
+ * - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres
347
+ */
348
+ /**
349
+ * Find the `claude` CLI binary. Checks common installation locations first
350
+ * (launchd PATH is minimal so bare "claude" often won't resolve).
351
+ */
352
+ function findClaudeBinary() {
353
+ const candidates = [
354
+ join(homedir(), ".local", "bin", "claude"),
355
+ join(homedir(), ".claude", "local", "claude"),
356
+ "/usr/local/bin/claude",
357
+ "/opt/homebrew/bin/claude"
358
+ ];
359
+ for (const candidate of candidates) try {
360
+ if (existsSync(candidate)) return candidate;
361
+ } catch {}
362
+ return "claude";
363
+ }
364
+ const CLAUDE_TIMEOUT_MS = {
365
+ haiku: 6e4,
366
+ sonnet: 12e4,
367
+ opus: 3e5
368
+ };
369
+ /**
370
+ * Spawn the claude CLI with a prompt on stdin and return stdout.
371
+ *
372
+ * IMPORTANT: ANTHROPIC_API_KEY is stripped from the spawned environment so
373
+ * the CLI uses the user's Max plan (free) instead of billing the API key.
374
+ */
375
+ async function spawnClaude(prompt, model = "sonnet") {
376
+ const claudeBin = findClaudeBinary();
377
+ if (!claudeBin) {
378
+ process.stderr.write("[kg-extraction] claude CLI not found.\n");
379
+ return null;
380
+ }
381
+ const { spawn } = await import("node:child_process");
382
+ return new Promise((resolve) => {
383
+ let timer = null;
384
+ const { ANTHROPIC_API_KEY: _drop, ...envWithoutApiKey } = process.env;
385
+ const child = spawn(claudeBin, [
386
+ "--model",
387
+ model,
388
+ "-p",
389
+ "--no-session-persistence"
390
+ ], {
391
+ env: envWithoutApiKey,
392
+ stdio: [
393
+ "pipe",
394
+ "pipe",
395
+ "pipe"
396
+ ]
397
+ });
398
+ let stdout = "";
399
+ let stderr = "";
400
+ child.stdout.on("data", (chunk) => {
401
+ stdout += chunk.toString();
402
+ });
403
+ child.stderr.on("data", (chunk) => {
404
+ stderr += chunk.toString();
405
+ });
406
+ child.on("error", (err) => {
407
+ if (timer) {
408
+ clearTimeout(timer);
409
+ timer = null;
410
+ }
411
+ process.stderr.write(`[kg-extraction] ${model} spawn error: ${err.message}\n`);
412
+ resolve(null);
413
+ });
414
+ child.on("close", (code) => {
415
+ if (timer) {
416
+ clearTimeout(timer);
417
+ timer = null;
418
+ }
419
+ if (code !== 0) {
420
+ process.stderr.write(`[kg-extraction] ${model} exited ${code}: ${stderr.slice(0, 300)}\n`);
421
+ resolve(null);
422
+ } else resolve(stdout.trim() || null);
423
+ });
424
+ timer = setTimeout(() => {
425
+ process.stderr.write(`[kg-extraction] ${model} timed out — killing process.\n`);
426
+ child.kill("SIGTERM");
427
+ resolve(null);
428
+ }, CLAUDE_TIMEOUT_MS[model] ?? 12e4);
429
+ child.stdin.write(prompt);
430
+ child.stdin.end();
431
+ });
432
+ }
433
+ /**
434
+ * Extract structured KG triples from a session summary and store them in
435
+ * Postgres. Idempotent: if a (subject, predicate) pair already has the same
436
+ * object, no new row is added; if the object differs, the old triple is
437
+ * invalidated (valid_to = NOW()) and a new one is inserted.
438
+ *
439
+ * Best-effort: per-triple errors are caught and logged but never thrown.
440
+ * Returns a small stats object so callers can report progress.
441
+ */
442
+ async function extractAndStoreTriples(pool, params) {
443
+ const stats = {
444
+ extracted: 0,
445
+ added: 0,
446
+ superseded: 0
447
+ };
448
+ const jsonOutput = await spawnClaude(buildTripleExtractionPrompt({
449
+ sessionContent: params.summaryText,
450
+ projectSlug: params.projectSlug,
451
+ gitLog: params.gitLog ?? ""
452
+ }), params.model ?? "sonnet");
453
+ if (!jsonOutput) return stats;
454
+ const cleaned = jsonOutput.replace(/^```json\s*/m, "").replace(/^```\s*/m, "").replace(/\s*```$/m, "").trim();
455
+ let triples;
456
+ try {
457
+ const parsed = JSON.parse(cleaned);
458
+ if (Array.isArray(parsed)) triples = parsed;
459
+ else if (parsed && typeof parsed === "object" && Array.isArray(parsed.relations)) {
460
+ const newFmt = parsed;
461
+ if (params.federationDb && Array.isArray(newFmt.entities)) {
462
+ const tenantId = params.tenantId ?? "default";
463
+ for (const entity of newFmt.entities) {
464
+ if (!entity.name) continue;
465
+ try {
466
+ upsertKgEntity(params.federationDb, {
467
+ name: entity.name,
468
+ type: entity.type ?? "unknown",
469
+ description: entity.description,
470
+ tenantId
471
+ });
472
+ } catch (entityErr) {
473
+ process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\n`);
474
+ }
475
+ }
476
+ }
477
+ triples = newFmt.relations.map((r) => ({
478
+ subject: r.source,
479
+ predicate: r.relation,
480
+ object: r.target
481
+ }));
482
+ } else {
483
+ process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\n`);
484
+ return stats;
485
+ }
486
+ } catch (e) {
487
+ process.stderr.write(`[kg-extraction] JSON parse failed: ${e}\n`);
488
+ return stats;
489
+ }
490
+ if (!Array.isArray(triples)) return stats;
491
+ stats.extracted = triples.length;
492
+ for (const t of triples) {
493
+ if (!t.subject || !t.predicate || !t.object) continue;
494
+ try {
495
+ const existing = await kgQuery(pool, {
496
+ subject: t.subject,
497
+ predicate: t.predicate,
498
+ project_id: params.projectId ?? void 0
499
+ });
500
+ if (existing.find((e) => e.object === t.object && !e.valid_to)) continue;
501
+ const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);
502
+ if (supersedes) {
503
+ await kgInvalidate(pool, supersedes.id);
504
+ stats.superseded++;
505
+ }
506
+ await kgAdd(pool, {
507
+ subject: t.subject,
508
+ predicate: t.predicate,
509
+ object: t.object,
510
+ project_id: params.projectId ?? void 0,
511
+ source_session: params.sessionId,
512
+ confidence: "EXTRACTED"
513
+ });
514
+ stats.added++;
515
+ } catch (tripleErr) {
516
+ process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\n`);
517
+ }
518
+ }
519
+ return stats;
520
+ }
521
+
522
+ //#endregion
523
+ export { resolveHome as a, upsertSession as c, performScan as i, cmdScan as n, saveScanConfig as o, loadScanConfig as r, upsertProject as s, extractAndStoreTriples as t };
524
+ //# sourceMappingURL=kg-extraction-r5KkTmWY.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kg-extraction-r5KkTmWY.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"],"sourcesContent":["/** Shared database helpers for registry command operations. */\n\nimport type { Database } from \"better-sqlite3\";\nimport { now } from \"../../utils.js\";\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 */\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 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, finalSlug, 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 { 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// 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\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\n for (const encodedDir of entries) {\n const rootPath = decodeEncodedDir(encodedDir, lookupMap);\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 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 const childPath = 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 if (registeredRow.root_path !== marker.projectRoot) {\n const newEncoded = encodeDir(marker.projectRoot);\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(marker.projectRoot) 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(marker.projectRoot, newEncoded, now4, registeredRow.id);\n } else if (pathSafe) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(marker.projectRoot, now4, registeredRow.id);\n }\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"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import "./embeddings-DGRAPAYb.mjs";
2
2
  import { n as TITLE_STOP_WORDS } from "./stop-words-BaMEGVeY.mjs";
3
- import { t as zettelThemes } from "./themes-9jxFn3Rf.mjs";
3
+ import { t as zettelThemes } from "./themes-DO8mUVwD.mjs";
4
4
  import { mkdirSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
 
@@ -188,4 +188,4 @@ function handleIdeaMaterialize(params, vaultPath) {
188
188
 
189
189
  //#endregion
190
190
  export { handleGraphLatentIdeas, handleIdeaMaterialize };
191
- //# sourceMappingURL=latent-ideas-DvWBRHsy.mjs.map
191
+ //# sourceMappingURL=latent-ideas-dWVuI_dg.mjs.map