@tekmidian/pai 0.9.0 → 0.9.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.
Files changed (48) hide show
  1. package/dist/{auto-route-C-DrW6BL.mjs → auto-route-CruBrTf-.mjs} +2 -2
  2. package/dist/{auto-route-C-DrW6BL.mjs.map → auto-route-CruBrTf-.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +345 -23
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/{clusters-JIDQW65f.mjs → clusters-CRlPBpq8.mjs} +1 -1
  6. package/dist/{clusters-JIDQW65f.mjs.map → clusters-CRlPBpq8.mjs.map} +1 -1
  7. package/dist/daemon/index.mjs +6 -6
  8. package/dist/{daemon-VIFoKc_z.mjs → daemon-kp49BE7u.mjs} +74 -21
  9. package/dist/daemon-kp49BE7u.mjs.map +1 -0
  10. package/dist/{detector-jGBuYQJM.mjs → detector-CNU3zCwP.mjs} +1 -1
  11. package/dist/{detector-jGBuYQJM.mjs.map → detector-CNU3zCwP.mjs.map} +1 -1
  12. package/dist/{factory-e0k1HWuc.mjs → factory-DKDPRhAN.mjs} +3 -3
  13. package/dist/{factory-e0k1HWuc.mjs.map → factory-DKDPRhAN.mjs.map} +1 -1
  14. package/dist/{indexer-backend-jcJFsmB4.mjs → indexer-backend-CIIlrYh6.mjs} +1 -1
  15. package/dist/{indexer-backend-jcJFsmB4.mjs.map → indexer-backend-CIIlrYh6.mjs.map} +1 -1
  16. package/dist/kg-B5ysyRLC.mjs +94 -0
  17. package/dist/kg-B5ysyRLC.mjs.map +1 -0
  18. package/dist/kg-extraction-BlGM40q7.mjs +211 -0
  19. package/dist/kg-extraction-BlGM40q7.mjs.map +1 -0
  20. package/dist/{latent-ideas-bTJo6Omd.mjs → latent-ideas-DvWBRHsy.mjs} +2 -2
  21. package/dist/{latent-ideas-bTJo6Omd.mjs.map → latent-ideas-DvWBRHsy.mjs.map} +1 -1
  22. package/dist/{neighborhood-BYYbEkUJ.mjs → neighborhood-u8ytjmWq.mjs} +1 -1
  23. package/dist/{neighborhood-BYYbEkUJ.mjs.map → neighborhood-u8ytjmWq.mjs.map} +1 -1
  24. package/dist/{note-context-BK24bX8Y.mjs → note-context-CG2_e-0W.mjs} +1 -1
  25. package/dist/{note-context-BK24bX8Y.mjs.map → note-context-CG2_e-0W.mjs.map} +1 -1
  26. package/dist/{postgres-DvEPooLO.mjs → postgres-BGERehmX.mjs} +1 -1
  27. package/dist/{postgres-DvEPooLO.mjs.map → postgres-BGERehmX.mjs.map} +1 -1
  28. package/dist/{query-feedback-Dv43XKHM.mjs → query-feedback-CQSumXDy.mjs} +1 -1
  29. package/dist/{query-feedback-Dv43XKHM.mjs.map → query-feedback-CQSumXDy.mjs.map} +1 -1
  30. package/dist/skills/Reconstruct/SKILL.md +36 -0
  31. package/dist/{sqlite-l-s9xPjY.mjs → sqlite-BJrME_vg.mjs} +1 -1
  32. package/dist/{sqlite-l-s9xPjY.mjs.map → sqlite-BJrME_vg.mjs.map} +1 -1
  33. package/dist/{state-C6_vqz7w.mjs → state-BIlxNRUn.mjs} +1 -1
  34. package/dist/{state-C6_vqz7w.mjs.map → state-BIlxNRUn.mjs.map} +1 -1
  35. package/dist/{themes-BvYF0W8T.mjs → themes-9jxFn3Rf.mjs} +1 -1
  36. package/dist/{themes-BvYF0W8T.mjs.map → themes-9jxFn3Rf.mjs.map} +1 -1
  37. package/dist/{tools-C4SBZHga.mjs → tools-8t7BQrm9.mjs} +13 -104
  38. package/dist/tools-8t7BQrm9.mjs.map +1 -0
  39. package/dist/{trace-CRx9lPuc.mjs → trace-C2XrzssW.mjs} +1 -1
  40. package/dist/{trace-CRx9lPuc.mjs.map → trace-C2XrzssW.mjs.map} +1 -1
  41. package/dist/{vault-indexer-B-aJpRZC.mjs → vault-indexer-TTCl1QOL.mjs} +1 -1
  42. package/dist/{vault-indexer-B-aJpRZC.mjs.map → vault-indexer-TTCl1QOL.mjs.map} +1 -1
  43. package/dist/{zettelkasten-DhBKZQHF.mjs → zettelkasten-BdaMzTGQ.mjs} +3 -3
  44. package/dist/{zettelkasten-DhBKZQHF.mjs.map → zettelkasten-BdaMzTGQ.mjs.map} +1 -1
  45. package/package.json +1 -1
  46. package/dist/daemon-VIFoKc_z.mjs.map +0 -1
  47. package/dist/indexer-D53l5d1U.mjs +0 -1
  48. package/dist/tools-C4SBZHga.mjs.map +0 -1
@@ -26,7 +26,7 @@ async function autoRoute(registryDb, federation, cwd, context) {
26
26
  const markerResult = findMarkerUpward(registryDb, target);
27
27
  if (markerResult) return markerResult;
28
28
  if (context && context.trim().length > 0) {
29
- const { detectTopicShift } = await import("./detector-jGBuYQJM.mjs").then((n) => n.n);
29
+ const { detectTopicShift } = await import("./detector-CNU3zCwP.mjs").then((n) => n.n);
30
30
  const topicResult = await detectTopicShift(registryDb, federation, {
31
31
  context,
32
32
  threshold: .5
@@ -83,4 +83,4 @@ function formatAutoRouteJson(result) {
83
83
 
84
84
  //#endregion
85
85
  export { autoRoute, formatAutoRouteJson };
86
- //# sourceMappingURL=auto-route-C-DrW6BL.mjs.map
86
+ //# sourceMappingURL=auto-route-CruBrTf-.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"auto-route-C-DrW6BL.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n * 1. Path match — exact or parent-directory match in the project registry\n * 2. Marker walk — walk up from cwd looking for Notes/PAI.md, resolve slug\n * 3. Topic match — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works with direct DB access (no daemon\n * required), making it fast and safe to call during session startup.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\nimport { detectProject } from \"../cli/commands/detect.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n /** Project slug */\n slug: string;\n /** Human-readable project name */\n display_name: string;\n /** Absolute path to the project root */\n root_path: string;\n /** How the project was detected */\n method: AutoRouteMethod;\n /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryDb Open PAI registry database\n * @param federation Memory storage backend (needed only for topic fallback)\n * @param cwd Working directory to detect from (defaults to process.cwd())\n * @param context Optional conversation text for topic-based fallback\n * @returns Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n cwd?: string,\n context?: string\n): Promise<AutoRouteResult | null> {\n const target = resolve(cwd ?? process.cwd());\n\n // -------------------------------------------------------------------------\n // Strategy 1: Path match via registry\n // -------------------------------------------------------------------------\n\n const pathMatch = detectProject(registryDb, target);\n\n if (pathMatch) {\n return {\n slug: pathMatch.slug,\n display_name: pathMatch.display_name,\n root_path: pathMatch.root_path,\n method: \"path\",\n confidence: 1.0,\n };\n }\n\n // -------------------------------------------------------------------------\n // Strategy 2: PAI.md marker file walk\n //\n // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n // Once found, resolve the slug against the registry to get full project info.\n // -------------------------------------------------------------------------\n\n const markerResult = findMarkerUpward(registryDb, target);\n if (markerResult) {\n return markerResult;\n }\n\n // -------------------------------------------------------------------------\n // Strategy 3: Topic detection (requires context text)\n // -------------------------------------------------------------------------\n\n if (context && context.trim().length > 0) {\n // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n const { detectTopicShift } = await import(\"../topics/detector.js\");\n const topicResult = await detectTopicShift(registryDb, federation, {\n context,\n threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n });\n\n if (topicResult.suggestedProject && topicResult.confidence > 0) {\n // Look up the full project info from the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(topicResult.suggestedProject) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"topic\",\n confidence: topicResult.confidence,\n };\n }\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nfunction findMarkerUpward(\n registryDb: Database,\n startDir: string\n): AutoRouteResult | null {\n let current = startDir;\n let depth = 0;\n\n while (depth < 20) {\n const markerPath = `${current}/Notes/PAI.md`;\n\n if (existsSync(markerPath)) {\n const marker = readPaiMarker(current);\n\n if (marker && marker.status !== \"archived\") {\n // Resolve slug to full project info in the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(marker.slug) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"marker\",\n confidence: 1.0,\n };\n }\n }\n }\n\n const parent = dirname(current);\n if (parent === current) break; // Reached filesystem root\n current = parent;\n depth++;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n const lines: string[] = [\n `slug: ${result.slug}`,\n `display_name: ${result.display_name}`,\n `root_path: ${result.root_path}`,\n `method: ${result.method}`,\n `confidence: ${(result.confidence * 100).toFixed(0)}%`,\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,YACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,cAAc,YAAY,OAAO;AAEnD,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,iBAAiB,YAAY,OAAO;AACzD,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBAAiB,YAAY,YAAY;GACjE;GACA,WAAW;GACZ,CAAC;AAEF,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,YAAY,iBAAiB;AAIpC,OAAI,WACF,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,SAAS,iBACP,YACA,UACwB;CACxB,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,OAAO,KAAK;AAInB,QAAI,WACF,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}
1
+ {"version":3,"file":"auto-route-CruBrTf-.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n * 1. Path match — exact or parent-directory match in the project registry\n * 2. Marker walk — walk up from cwd looking for Notes/PAI.md, resolve slug\n * 3. Topic match — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works with direct DB access (no daemon\n * required), making it fast and safe to call during session startup.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\nimport { detectProject } from \"../cli/commands/detect.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n /** Project slug */\n slug: string;\n /** Human-readable project name */\n display_name: string;\n /** Absolute path to the project root */\n root_path: string;\n /** How the project was detected */\n method: AutoRouteMethod;\n /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryDb Open PAI registry database\n * @param federation Memory storage backend (needed only for topic fallback)\n * @param cwd Working directory to detect from (defaults to process.cwd())\n * @param context Optional conversation text for topic-based fallback\n * @returns Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n cwd?: string,\n context?: string\n): Promise<AutoRouteResult | null> {\n const target = resolve(cwd ?? process.cwd());\n\n // -------------------------------------------------------------------------\n // Strategy 1: Path match via registry\n // -------------------------------------------------------------------------\n\n const pathMatch = detectProject(registryDb, target);\n\n if (pathMatch) {\n return {\n slug: pathMatch.slug,\n display_name: pathMatch.display_name,\n root_path: pathMatch.root_path,\n method: \"path\",\n confidence: 1.0,\n };\n }\n\n // -------------------------------------------------------------------------\n // Strategy 2: PAI.md marker file walk\n //\n // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n // Once found, resolve the slug against the registry to get full project info.\n // -------------------------------------------------------------------------\n\n const markerResult = findMarkerUpward(registryDb, target);\n if (markerResult) {\n return markerResult;\n }\n\n // -------------------------------------------------------------------------\n // Strategy 3: Topic detection (requires context text)\n // -------------------------------------------------------------------------\n\n if (context && context.trim().length > 0) {\n // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n const { detectTopicShift } = await import(\"../topics/detector.js\");\n const topicResult = await detectTopicShift(registryDb, federation, {\n context,\n threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n });\n\n if (topicResult.suggestedProject && topicResult.confidence > 0) {\n // Look up the full project info from the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(topicResult.suggestedProject) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"topic\",\n confidence: topicResult.confidence,\n };\n }\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nfunction findMarkerUpward(\n registryDb: Database,\n startDir: string\n): AutoRouteResult | null {\n let current = startDir;\n let depth = 0;\n\n while (depth < 20) {\n const markerPath = `${current}/Notes/PAI.md`;\n\n if (existsSync(markerPath)) {\n const marker = readPaiMarker(current);\n\n if (marker && marker.status !== \"archived\") {\n // Resolve slug to full project info in the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(marker.slug) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"marker\",\n confidence: 1.0,\n };\n }\n }\n }\n\n const parent = dirname(current);\n if (parent === current) break; // Reached filesystem root\n current = parent;\n depth++;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n const lines: string[] = [\n `slug: ${result.slug}`,\n `display_name: ${result.display_name}`,\n `root_path: ${result.root_path}`,\n `method: ${result.method}`,\n `confidence: ${(result.confidence * 100).toFixed(0)}%`,\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,YACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,cAAc,YAAY,OAAO;AAEnD,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,iBAAiB,YAAY,OAAO;AACzD,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBAAiB,YAAY,YAAY;GACjE;GACA,WAAW;GACZ,CAAC;AAEF,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,YAAY,iBAAiB;AAIpC,OAAI,WACF,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,SAAS,iBACP,YACA,UACwB;CACxB,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,OAAO,KAAK;AAInB,QAAI,WACF,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}
@@ -10,10 +10,11 @@ import "../embeddings-DGRAPAYb.mjs";
10
10
  import { t as STOP_WORDS } from "../stop-words-BaMEGVeY.mjs";
11
11
  import { n as populateSlugs, r as searchMemory } from "../search-DC1qhkKn.mjs";
12
12
  import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-CdaA48EI.mjs";
13
- import "../indexer-D53l5d1U.mjs";
13
+ import { t as extractAndStoreTriples } from "../kg-extraction-BlGM40q7.mjs";
14
14
  import { t as PaiClient } from "../ipc-client-CoyUHPod.mjs";
15
15
  import { a as expandHome, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, t as CONFIG_DIR } from "../config-BuhHWyOK.mjs";
16
- import { t as createStorageBackend } from "../factory-e0k1HWuc.mjs";
16
+ import { t as createStorageBackend } from "../factory-DKDPRhAN.mjs";
17
+ import { i as kgQuery } from "../kg-B5ysyRLC.mjs";
17
18
  import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
18
19
  import { homedir, platform, tmpdir } from "node:os";
19
20
  import { basename, dirname, join, relative, resolve } from "node:path";
@@ -256,7 +257,7 @@ function cmdAdd(db, rawPath, opts) {
256
257
  console.log(dim(` Encoded dir: ${encodedDir}`));
257
258
  console.log(dim(` Type: ${type}`));
258
259
  }
259
- function cmdList$2(db, opts) {
260
+ function cmdList$3(db, opts) {
260
261
  let query = `
261
262
  SELECT p.*,
262
263
  (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,
@@ -1099,7 +1100,7 @@ function registerProjectCommands(projectCmd, getDb) {
1099
1100
  cmdAdd(getDb(), rawPath, opts);
1100
1101
  });
1101
1102
  projectCmd.command("list").description("List registered projects").option("--status <status>", "Filter by status: active | archived").option("--tag <tag>", "Filter by tag").option("--type <type>", "Filter by type").action((opts) => {
1102
- cmdList$2(getDb(), opts);
1103
+ cmdList$3(getDb(), opts);
1103
1104
  });
1104
1105
  projectCmd.command("info <slug>").description("Show full details for a project").action((slug) => {
1105
1106
  cmdInfo$1(getDb(), slug);
@@ -1350,7 +1351,7 @@ function getSessionTags(db, sessionId) {
1350
1351
 
1351
1352
  //#endregion
1352
1353
  //#region src/cli/commands/session/commands.ts
1353
- function cmdList$1(db, projectSlug, opts) {
1354
+ function cmdList$2(db, projectSlug, opts) {
1354
1355
  const limit = parseInt(opts.limit ?? "20", 10);
1355
1356
  const params = [];
1356
1357
  let query = `
@@ -1681,9 +1682,9 @@ function cmdActive(db, opts) {
1681
1682
  ], rows));
1682
1683
  }
1683
1684
  async function cmdAutoRoute(opts) {
1684
- const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-C-DrW6BL.mjs");
1685
+ const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-CruBrTf-.mjs");
1685
1686
  const { openRegistry } = await import("../db-BtuN768f.mjs").then((n) => n.t);
1686
- const { createStorageBackend } = await import("../factory-e0k1HWuc.mjs").then((n) => n.n);
1687
+ const { createStorageBackend } = await import("../factory-DKDPRhAN.mjs").then((n) => n.n);
1687
1688
  const { loadConfig } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
1688
1689
  const config = loadConfig();
1689
1690
  const registryDb = openRegistry();
@@ -1927,7 +1928,7 @@ function cmdHandover(db, projectSlug, numberOrLatest) {
1927
1928
  //#region src/cli/commands/session/index.ts
1928
1929
  function registerSessionCommands(sessionCmd, getDb) {
1929
1930
  sessionCmd.command("list [project-slug]").description("List sessions, optionally filtered to a single project").option("--limit <n>", "Maximum number of sessions to show", "20").option("--status <status>", "Filter by status: open | completed | compacted").action((projectSlug, opts) => {
1930
- cmdList$1(getDb(), projectSlug, opts);
1931
+ cmdList$2(getDb(), projectSlug, opts);
1931
1932
  });
1932
1933
  sessionCmd.command("info <project-slug> <number>").description("Show full details for a specific session").action((projectSlug, number) => {
1933
1934
  cmdInfo(getDb(), projectSlug, number);
@@ -2302,7 +2303,7 @@ async function countVectorDbPaths(oldPaths) {
2302
2303
  if (oldPaths.length === 0) return 0;
2303
2304
  try {
2304
2305
  const { loadConfig } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
2305
- const { PostgresBackend } = await import("../postgres-DvEPooLO.mjs");
2306
+ const { PostgresBackend } = await import("../postgres-BGERehmX.mjs");
2306
2307
  const config = loadConfig();
2307
2308
  if (config.storageBackend !== "postgres") return 0;
2308
2309
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -2323,7 +2324,7 @@ async function updateVectorDbPaths(moves) {
2323
2324
  if (moves.length === 0) return 0;
2324
2325
  try {
2325
2326
  const { loadConfig } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
2326
- const { PostgresBackend } = await import("../postgres-DvEPooLO.mjs");
2327
+ const { PostgresBackend } = await import("../postgres-BGERehmX.mjs");
2327
2328
  const config = loadConfig();
2328
2329
  if (config.storageBackend !== "postgres") return 0;
2329
2330
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -2971,7 +2972,7 @@ function cmdMigrate$1(db) {
2971
2972
 
2972
2973
  //#endregion
2973
2974
  //#region src/cli/commands/registry/index.ts
2974
- function cmdStats$1(db) {
2975
+ function cmdStats$2(db) {
2975
2976
  const totalProjects = db.prepare("SELECT COUNT(*) AS n FROM projects").get().n;
2976
2977
  const activeProjects = db.prepare("SELECT COUNT(*) AS n FROM projects WHERE status = 'active'").get().n;
2977
2978
  const archivedProjects = db.prepare("SELECT COUNT(*) AS n FROM projects WHERE status = 'archived'").get().n;
@@ -3058,7 +3059,7 @@ function registerRegistryCommands(registryCmd, getDb) {
3058
3059
  cmdMigrate$1(getDb());
3059
3060
  });
3060
3061
  registryCmd.command("stats").description("Show summary statistics for the registry").action(() => {
3061
- cmdStats$1(getDb());
3062
+ cmdStats$2(getDb());
3062
3063
  });
3063
3064
  registryCmd.command("rebuild").description("Erase all registry data and rebuild from the filesystem (destructive)").action(() => {
3064
3065
  cmdRebuild(getDb());
@@ -3793,7 +3794,7 @@ function cmdLogs(opts) {
3793
3794
  }
3794
3795
  function registerDaemonCommands(daemonCmd) {
3795
3796
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
3796
- const { serve } = await import("../daemon-VIFoKc_z.mjs").then((n) => n.t);
3797
+ const { serve } = await import("../daemon-kp49BE7u.mjs").then((n) => n.t);
3797
3798
  const { loadConfig: lc, ensureConfigDir } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
3798
3799
  ensureConfigDir();
3799
3800
  await serve(lc());
@@ -6411,7 +6412,7 @@ async function cmdExplore(note, opts) {
6411
6412
  const depth = parseInt(opts.depth ?? "3", 10);
6412
6413
  const direction = opts.direction ?? "both";
6413
6414
  const mode = opts.mode ?? "all";
6414
- const { zettelExplore } = await import("../zettelkasten-DhBKZQHF.mjs");
6415
+ const { zettelExplore } = await import("../zettelkasten-BdaMzTGQ.mjs");
6415
6416
  const result = zettelExplore(getFedDb(), {
6416
6417
  startNote: note,
6417
6418
  depth,
@@ -6475,7 +6476,7 @@ async function cmdHealth(opts) {
6475
6476
  const projectPath = opts.project;
6476
6477
  const recentDays = parseInt(opts.days ?? "30", 10);
6477
6478
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
6478
- const { zettelHealth } = await import("../zettelkasten-DhBKZQHF.mjs");
6479
+ const { zettelHealth } = await import("../zettelkasten-BdaMzTGQ.mjs");
6479
6480
  const result = zettelHealth(getFedDb(), {
6480
6481
  scope,
6481
6482
  projectPath,
@@ -6544,7 +6545,7 @@ async function cmdSurprise(note, opts) {
6544
6545
  const limit = parseInt(opts.limit ?? "10", 10);
6545
6546
  const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
6546
6547
  const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
6547
- const { zettelSurprise } = await import("../zettelkasten-DhBKZQHF.mjs");
6548
+ const { zettelSurprise } = await import("../zettelkasten-BdaMzTGQ.mjs");
6548
6549
  const db = getFedDb();
6549
6550
  console.log();
6550
6551
  console.log(header(" PAI Zettel Surprise"));
@@ -6597,7 +6598,7 @@ async function cmdSuggest(note, opts) {
6597
6598
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
6598
6599
  const limit = parseInt(opts.limit ?? "5", 10);
6599
6600
  const excludeLinked = opts.excludeLinked !== false;
6600
- const { zettelSuggest } = await import("../zettelkasten-DhBKZQHF.mjs");
6601
+ const { zettelSuggest } = await import("../zettelkasten-BdaMzTGQ.mjs");
6601
6602
  const db = getFedDb();
6602
6603
  console.log();
6603
6604
  console.log(header(" PAI Zettel Suggest"));
@@ -6648,7 +6649,7 @@ async function cmdConverse(question, opts) {
6648
6649
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
6649
6650
  const depth = parseInt(opts.depth ?? "2", 10);
6650
6651
  const limit = parseInt(opts.limit ?? "15", 10);
6651
- const { zettelConverse } = await import("../zettelkasten-DhBKZQHF.mjs");
6652
+ const { zettelConverse } = await import("../zettelkasten-BdaMzTGQ.mjs");
6652
6653
  const db = getFedDb();
6653
6654
  console.log();
6654
6655
  console.log(header(" PAI Zettel Converse"));
@@ -6709,7 +6710,7 @@ async function cmdThemes(opts) {
6709
6710
  const minClusterSize = parseInt(opts.minSize ?? "3", 10);
6710
6711
  const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
6711
6712
  const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
6712
- const { zettelThemes } = await import("../zettelkasten-DhBKZQHF.mjs");
6713
+ const { zettelThemes } = await import("../zettelkasten-BdaMzTGQ.mjs");
6713
6714
  const db = getFedDb();
6714
6715
  console.log();
6715
6716
  console.log(header(" PAI Zettel Themes"));
@@ -6838,7 +6839,7 @@ function trunc(s, maxLen) {
6838
6839
  if (s.length <= maxLen) return s;
6839
6840
  return s.slice(0, maxLen - 1) + "…";
6840
6841
  }
6841
- async function cmdList(opts) {
6842
+ async function cmdList$1(opts) {
6842
6843
  const limit = parseInt(opts.limit ?? "20", 10);
6843
6844
  const params = { limit };
6844
6845
  if (opts.project) params.project_slug = opts.project;
@@ -6935,7 +6936,7 @@ async function cmdSearch(query, opts) {
6935
6936
  console.log(dim(` ${observations.length} result(s)`));
6936
6937
  console.log();
6937
6938
  }
6938
- async function cmdStats() {
6939
+ async function cmdStats$1() {
6939
6940
  let stats;
6940
6941
  try {
6941
6942
  stats = await ipcCall("observation_stats", {});
@@ -6984,7 +6985,7 @@ async function cmdStats() {
6984
6985
  function registerObservationCommands(parent) {
6985
6986
  parent.command("list").description("List recent observations").option("--project <slug>", "Filter by project slug").option("--type <type>", "Filter by type (decision, bugfix, feature, refactor, discovery, change)").option("--session <id>", "Filter by session ID").option("--limit <n>", "Maximum results", "20").action(async (opts) => {
6986
6987
  try {
6987
- await cmdList(opts);
6988
+ await cmdList$1(opts);
6988
6989
  } catch (e) {
6989
6990
  console.error(err(` Error: ${e}`));
6990
6991
  process.exit(1);
@@ -7000,7 +7001,7 @@ function registerObservationCommands(parent) {
7000
7001
  });
7001
7002
  parent.command("stats").description("Show observation statistics: totals, by type, by project").action(async () => {
7002
7003
  try {
7003
- await cmdStats();
7004
+ await cmdStats$1();
7004
7005
  } catch (e) {
7005
7006
  console.error(err(` Error: ${e}`));
7006
7007
  process.exit(1);
@@ -7569,6 +7570,326 @@ function registerTopicCommands(topicCmd) {
7569
7570
  });
7570
7571
  }
7571
7572
 
7573
+ //#endregion
7574
+ //#region src/memory/kg-backfill.ts
7575
+ /**
7576
+ * kg-backfill.ts — Populate the temporal knowledge graph from existing
7577
+ * session notes.
7578
+ *
7579
+ * Walks `Notes/YYYY/MM/*.md` for each registered project, runs the same
7580
+ * extractor that the session-summary-worker uses on every NEW summary, and
7581
+ * stores triples in Postgres. Idempotent: a state file at
7582
+ * ~/.config/pai/kg-backfill-state.json records which note paths have been
7583
+ * processed so re-runs skip them.
7584
+ *
7585
+ * Even without the state file the operation is safe — extractAndStoreTriples
7586
+ * uses supersession logic so re-extracting a note never produces duplicates.
7587
+ */
7588
+ const STATE_FILE = join(homedir(), ".config", "pai", "kg-backfill-state.json");
7589
+ function loadState() {
7590
+ try {
7591
+ if (existsSync(STATE_FILE)) {
7592
+ const raw = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
7593
+ if (raw && typeof raw === "object" && raw.processed) return raw;
7594
+ }
7595
+ } catch {}
7596
+ return { processed: {} };
7597
+ }
7598
+ function saveState(state) {
7599
+ try {
7600
+ const dir = join(homedir(), ".config", "pai");
7601
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
7602
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
7603
+ } catch (e) {
7604
+ process.stderr.write(`[kg-backfill] failed to save state: ${e}\n`);
7605
+ }
7606
+ }
7607
+ /**
7608
+ * Find all session notes under <project_root>/Notes/YYYY/MM/*.md.
7609
+ * Falls back to <project_root>/Notes/*.md (flat layout) when no year/month
7610
+ * subdirectories are present.
7611
+ */
7612
+ function findProjectNotes(project) {
7613
+ const notesRoot = join(project.root_path, "Notes");
7614
+ if (!existsSync(notesRoot)) return [];
7615
+ const found = [];
7616
+ function walk(dir, depth) {
7617
+ if (depth > 4) return;
7618
+ let entries;
7619
+ try {
7620
+ entries = readdirSync(dir);
7621
+ } catch {
7622
+ return;
7623
+ }
7624
+ for (const name of entries) {
7625
+ const full = join(dir, name);
7626
+ let st;
7627
+ try {
7628
+ st = statSync(full);
7629
+ } catch {
7630
+ continue;
7631
+ }
7632
+ if (st.isDirectory()) walk(full, depth + 1);
7633
+ else if (st.isFile() && name.endsWith(".md") && /^\d{3,4}/.test(name)) found.push({
7634
+ path: full,
7635
+ mtime: st.mtimeMs
7636
+ });
7637
+ }
7638
+ }
7639
+ walk(notesRoot, 0);
7640
+ found.sort((a, b) => a.mtime - b.mtime);
7641
+ return found.map((f) => f.path);
7642
+ }
7643
+ /**
7644
+ * Backfill the knowledge graph from existing session notes.
7645
+ *
7646
+ * Requires the Postgres backend (KG tables live there). Throws if Postgres
7647
+ * is unavailable, since this is an explicit user action.
7648
+ */
7649
+ async function backfillKgFromNotes(options = {}) {
7650
+ const result = {
7651
+ notes_processed: 0,
7652
+ triples_extracted: 0,
7653
+ triples_added: 0,
7654
+ triples_superseded: 0,
7655
+ errors: 0
7656
+ };
7657
+ const config = loadConfig();
7658
+ if (config.storageBackend !== "postgres") throw new Error("kg backfill requires the Postgres backend. Set \"storageBackend\": \"postgres\" in ~/.config/pai/config.json.");
7659
+ const backend = await createStorageBackend(config);
7660
+ if (backend.backendType !== "postgres") throw new Error("Postgres backend unavailable — fell back to SQLite. Cannot backfill KG.");
7661
+ const pool = backend.getPool();
7662
+ const registry = openRegistry();
7663
+ let projects;
7664
+ try {
7665
+ if (options.projectSlug) {
7666
+ const row = registry.prepare("SELECT id, slug, root_path FROM projects WHERE slug = ?").get(options.projectSlug);
7667
+ if (!row) throw new Error(`Project not found: ${options.projectSlug}`);
7668
+ projects = [row];
7669
+ } else projects = registry.prepare("SELECT id, slug, root_path FROM projects WHERE status = 'active' ORDER BY slug").all();
7670
+ } finally {
7671
+ registry.close();
7672
+ }
7673
+ const state = loadState();
7674
+ const work = [];
7675
+ for (const project of projects) {
7676
+ const notes = findProjectNotes(project);
7677
+ for (const notePath of notes) {
7678
+ if (state.processed[notePath]) continue;
7679
+ work.push({
7680
+ project,
7681
+ notePath
7682
+ });
7683
+ }
7684
+ }
7685
+ const total = options.limit ? Math.min(options.limit, work.length) : work.length;
7686
+ for (let i = 0; i < total; i++) {
7687
+ const { project, notePath } = work[i];
7688
+ options.onProgress?.(i + 1, total, notePath);
7689
+ if (options.dryRun) {
7690
+ result.notes_processed++;
7691
+ continue;
7692
+ }
7693
+ let noteContent;
7694
+ try {
7695
+ noteContent = readFileSync(notePath, "utf-8");
7696
+ } catch (e) {
7697
+ process.stderr.write(`[kg-backfill] read failed ${notePath}: ${e}\n`);
7698
+ result.errors++;
7699
+ continue;
7700
+ }
7701
+ try {
7702
+ const stats = await extractAndStoreTriples(pool, {
7703
+ summaryText: noteContent,
7704
+ projectSlug: project.slug,
7705
+ projectId: project.id,
7706
+ sessionId: `backfill:${notePath}`,
7707
+ gitLog: "",
7708
+ model: "sonnet"
7709
+ });
7710
+ result.notes_processed++;
7711
+ result.triples_extracted += stats.extracted;
7712
+ result.triples_added += stats.added;
7713
+ result.triples_superseded += stats.superseded;
7714
+ state.processed[notePath] = (/* @__PURE__ */ new Date()).toISOString();
7715
+ if ((i + 1) % 5 === 0) saveState(state);
7716
+ } catch (e) {
7717
+ process.stderr.write(`[kg-backfill] extract failed ${notePath}: ${e}\n`);
7718
+ result.errors++;
7719
+ }
7720
+ }
7721
+ if (!options.dryRun) saveState(state);
7722
+ await backend.close();
7723
+ return result;
7724
+ }
7725
+
7726
+ //#endregion
7727
+ //#region src/cli/commands/kg.ts
7728
+ async function getPool() {
7729
+ const config = loadConfig();
7730
+ if (config.storageBackend !== "postgres") {
7731
+ console.error(err(" KG commands require Postgres backend."));
7732
+ console.error(dim(" Set \"storageBackend\": \"postgres\" in ~/.config/pai/config.json"));
7733
+ process.exit(1);
7734
+ }
7735
+ const backend = await createStorageBackend(config);
7736
+ if (backend.backendType !== "postgres") {
7737
+ console.error(err(" Postgres backend unavailable — fell back to SQLite."));
7738
+ process.exit(1);
7739
+ }
7740
+ return {
7741
+ pool: backend.getPool(),
7742
+ close: () => backend.close()
7743
+ };
7744
+ }
7745
+ function shorten(s, n) {
7746
+ return s.length <= n ? s : s.slice(0, n - 1) + "…";
7747
+ }
7748
+ async function cmdBackfill(opts) {
7749
+ const limit = opts.limit ? parseInt(opts.limit, 10) : void 0;
7750
+ if (limit !== void 0 && (isNaN(limit) || limit < 1)) {
7751
+ console.error(err(" --limit must be a positive integer"));
7752
+ process.exit(1);
7753
+ }
7754
+ console.log();
7755
+ console.log(header(" PAI KG Backfill"));
7756
+ console.log();
7757
+ if (opts.project) console.log(` ${bold("Project:")} ${opts.project}`);
7758
+ if (limit) console.log(` ${bold("Limit:")} ${limit}`);
7759
+ if (opts.dryRun) console.log(` ${bold("Mode:")} ${warn("dry-run")}`);
7760
+ console.log();
7761
+ try {
7762
+ const result = await backfillKgFromNotes({
7763
+ projectSlug: opts.project,
7764
+ limit,
7765
+ dryRun: opts.dryRun,
7766
+ onProgress: (current, total, note) => {
7767
+ const short = shorten(note.replace(process.env.HOME ?? "", "~"), 70);
7768
+ process.stdout.write(` [${current}/${total}] ${dim(short)}\n`);
7769
+ }
7770
+ });
7771
+ console.log();
7772
+ console.log(ok(" Backfill complete"));
7773
+ console.log(` Notes processed: ${bold(String(result.notes_processed))}`);
7774
+ console.log(` Triples extracted: ${bold(String(result.triples_extracted))}`);
7775
+ console.log(` Triples added: ${bold(String(result.triples_added))}`);
7776
+ console.log(` Triples superseded: ${bold(String(result.triples_superseded))}`);
7777
+ if (result.errors > 0) console.log(` ${warn("Errors:")} ${result.errors}`);
7778
+ console.log();
7779
+ } catch (e) {
7780
+ const msg = e instanceof Error ? e.message : String(e);
7781
+ console.error(err(` ${msg}`));
7782
+ process.exit(1);
7783
+ }
7784
+ }
7785
+ async function cmdQuery(opts) {
7786
+ const { pool, close } = await getPool();
7787
+ try {
7788
+ let projectId;
7789
+ if (opts.project) {
7790
+ const r = await pool.query("SELECT id FROM projects WHERE slug = $1 LIMIT 1", [opts.project]);
7791
+ if (r.rows.length === 0) console.error(warn(` Project not found in Postgres: ${opts.project}`));
7792
+ else projectId = r.rows[0].id;
7793
+ }
7794
+ const asOf = opts.asOf ? new Date(opts.asOf) : void 0;
7795
+ if (asOf && isNaN(asOf.getTime())) {
7796
+ console.error(err(` Invalid --as-of date: ${opts.asOf}`));
7797
+ process.exit(1);
7798
+ }
7799
+ const triples = await kgQuery(pool, {
7800
+ subject: opts.subject,
7801
+ predicate: opts.predicate,
7802
+ object: opts.object,
7803
+ project_id: projectId,
7804
+ as_of: asOf
7805
+ });
7806
+ if (opts.json) {
7807
+ console.log(JSON.stringify(triples, null, 2));
7808
+ return;
7809
+ }
7810
+ console.log();
7811
+ console.log(header(` ${triples.length} triple(s)`));
7812
+ console.log();
7813
+ for (const t of triples) {
7814
+ const validity = t.valid_to ? dim(`(invalidated ${t.valid_to.toISOString().slice(0, 10)})`) : dim(`(valid since ${t.valid_from.toISOString().slice(0, 10)})`);
7815
+ console.log(` ${bold(t.subject)} ${dim("·")} ${t.predicate} ${dim("·")} ${t.object} ${validity}`);
7816
+ }
7817
+ console.log();
7818
+ } finally {
7819
+ await close();
7820
+ }
7821
+ }
7822
+ async function cmdList(opts) {
7823
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 50;
7824
+ const { pool, close } = await getPool();
7825
+ try {
7826
+ let projectId;
7827
+ if (opts.project) {
7828
+ const r = await pool.query("SELECT id FROM projects WHERE slug = $1 LIMIT 1", [opts.project]);
7829
+ if (r.rows.length > 0) projectId = r.rows[0].id;
7830
+ }
7831
+ const triples = await kgQuery(pool, { project_id: projectId });
7832
+ const slice = triples.slice(0, limit);
7833
+ console.log();
7834
+ console.log(header(` ${slice.length} of ${triples.length} currently-valid triple(s)`));
7835
+ console.log();
7836
+ for (const t of slice) console.log(` ${bold(t.subject)} ${dim("·")} ${t.predicate} ${dim("·")} ${t.object}`);
7837
+ if (triples.length > slice.length) {
7838
+ console.log();
7839
+ console.log(dim(` (${triples.length - slice.length} more — increase --limit)`));
7840
+ }
7841
+ console.log();
7842
+ } finally {
7843
+ await close();
7844
+ }
7845
+ }
7846
+ async function cmdStats() {
7847
+ const { pool, close } = await getPool();
7848
+ try {
7849
+ const totals = await pool.query(`SELECT
7850
+ COUNT(*)::text AS total,
7851
+ COUNT(*) FILTER (WHERE valid_to IS NULL)::text AS valid,
7852
+ COUNT(*) FILTER (WHERE valid_to IS NOT NULL)::text AS invalidated,
7853
+ COUNT(DISTINCT subject)::text AS subjects,
7854
+ COUNT(DISTINCT predicate)::text AS predicates
7855
+ FROM kg_triples`);
7856
+ const contradictions = await pool.query(`SELECT COUNT(*)::text AS count FROM (
7857
+ SELECT subject, predicate
7858
+ FROM kg_triples
7859
+ WHERE valid_to IS NULL
7860
+ GROUP BY subject, predicate
7861
+ HAVING COUNT(*) > 1
7862
+ ) c`);
7863
+ const row = totals.rows[0] ?? {};
7864
+ console.log();
7865
+ console.log(header(" PAI KG Stats"));
7866
+ console.log();
7867
+ console.log(` ${bold("Total triples:")} ${row.total ?? "0"}`);
7868
+ console.log(` ${bold("Currently valid:")} ${row.valid ?? "0"}`);
7869
+ console.log(` ${bold("Invalidated:")} ${row.invalidated ?? "0"}`);
7870
+ console.log(` ${bold("Distinct subjects:")} ${row.subjects ?? "0"}`);
7871
+ console.log(` ${bold("Distinct predicates:")} ${row.predicates ?? "0"}`);
7872
+ console.log(` ${bold("Contradictions:")} ${contradictions.rows[0]?.count ?? "0"}`);
7873
+ console.log();
7874
+ } finally {
7875
+ await close();
7876
+ }
7877
+ }
7878
+ function registerKgCommands(kgCmd) {
7879
+ kgCmd.command("backfill").description("Populate the KG from existing session notes (idempotent)").option("--project <slug>", "Restrict backfill to a single project").option("--limit <n>", "Maximum number of notes to process").option("--dry-run", "List notes that would be processed without extracting").action(async (opts) => {
7880
+ await cmdBackfill(opts);
7881
+ });
7882
+ kgCmd.command("query").description("Query KG triples by subject, predicate, object, time, or project").option("--subject <s>", "Filter by subject").option("--predicate <p>", "Filter by predicate").option("--object <o>", "Filter by object").option("--as-of <date>", "Point-in-time query (YYYY-MM-DD or ISO 8601)").option("--project <slug>", "Restrict to a project slug").option("--json", "Output raw JSON").action(async (opts) => {
7883
+ await cmdQuery(opts);
7884
+ });
7885
+ kgCmd.command("list").description("List currently-valid triples").option("--project <slug>", "Restrict to a project slug").option("--limit <n>", "Maximum triples to print", "50").action(async (opts) => {
7886
+ await cmdList(opts);
7887
+ });
7888
+ kgCmd.command("stats").description("Show triple counts and contradiction count").action(async () => {
7889
+ await cmdStats();
7890
+ });
7891
+ }
7892
+
7572
7893
  //#endregion
7573
7894
  //#region src/cli/index.ts
7574
7895
  /**
@@ -7617,6 +7938,7 @@ registerSetupCommand(program);
7617
7938
  registerUpdateCommand(program);
7618
7939
  registerNotifyCommands(program.command("notify").description("Notification config: status, get, set, test, send"));
7619
7940
  registerTopicCommands(program.command("topic").description("Topic shift detection: check whether context has drifted to a different project"));
7941
+ registerKgCommands(program.command("kg").description("Temporal knowledge graph: backfill, query, list, stats"));
7620
7942
  registerObsidianCommands(program.command("obsidian").description("Obsidian vault: sync project notes, view status, open in Obsidian"), getDb);
7621
7943
  registerZettelCommands(program.command("zettel").description("Zettelkasten intelligence: explore, surprise, converse, themes, health, suggest"), getDb);
7622
7944
  registerObservationCommands(program.command("observation").description("Observation capture: list, search, and stats"));