@tekmidian/pai 0.8.5 → 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 (59) hide show
  1. package/ARCHITECTURE.md +121 -0
  2. package/FEATURE.md +5 -0
  3. package/README.md +54 -0
  4. package/dist/{auto-route-C-DrW6BL.mjs → auto-route-CruBrTf-.mjs} +2 -2
  5. package/dist/{auto-route-C-DrW6BL.mjs.map → auto-route-CruBrTf-.mjs.map} +1 -1
  6. package/dist/cli/index.mjs +345 -23
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/{clusters-JIDQW65f.mjs → clusters-CRlPBpq8.mjs} +1 -1
  9. package/dist/{clusters-JIDQW65f.mjs.map → clusters-CRlPBpq8.mjs.map} +1 -1
  10. package/dist/daemon/index.mjs +6 -6
  11. package/dist/{daemon-BaYX-w_d.mjs → daemon-kp49BE7u.mjs} +93 -19
  12. package/dist/daemon-kp49BE7u.mjs.map +1 -0
  13. package/dist/daemon-mcp/index.mjs +51 -0
  14. package/dist/daemon-mcp/index.mjs.map +1 -1
  15. package/dist/{detector-jGBuYQJM.mjs → detector-CNU3zCwP.mjs} +1 -1
  16. package/dist/{detector-jGBuYQJM.mjs.map → detector-CNU3zCwP.mjs.map} +1 -1
  17. package/dist/{factory-BzWfxsvK.mjs → factory-DKDPRhAN.mjs} +3 -3
  18. package/dist/{factory-BzWfxsvK.mjs.map → factory-DKDPRhAN.mjs.map} +1 -1
  19. package/dist/hooks/load-project-context.mjs +276 -89
  20. package/dist/hooks/load-project-context.mjs.map +4 -4
  21. package/dist/hooks/stop-hook.mjs +152 -2
  22. package/dist/hooks/stop-hook.mjs.map +3 -3
  23. package/dist/{indexer-backend-jcJFsmB4.mjs → indexer-backend-CIIlrYh6.mjs} +1 -1
  24. package/dist/{indexer-backend-jcJFsmB4.mjs.map → indexer-backend-CIIlrYh6.mjs.map} +1 -1
  25. package/dist/kg-B5ysyRLC.mjs +94 -0
  26. package/dist/kg-B5ysyRLC.mjs.map +1 -0
  27. package/dist/kg-extraction-BlGM40q7.mjs +211 -0
  28. package/dist/kg-extraction-BlGM40q7.mjs.map +1 -0
  29. package/dist/{latent-ideas-bTJo6Omd.mjs → latent-ideas-DvWBRHsy.mjs} +2 -2
  30. package/dist/{latent-ideas-bTJo6Omd.mjs.map → latent-ideas-DvWBRHsy.mjs.map} +1 -1
  31. package/dist/{neighborhood-BYYbEkUJ.mjs → neighborhood-u8ytjmWq.mjs} +1 -1
  32. package/dist/{neighborhood-BYYbEkUJ.mjs.map → neighborhood-u8ytjmWq.mjs.map} +1 -1
  33. package/dist/{note-context-BK24bX8Y.mjs → note-context-CG2_e-0W.mjs} +1 -1
  34. package/dist/{note-context-BK24bX8Y.mjs.map → note-context-CG2_e-0W.mjs.map} +1 -1
  35. package/dist/{postgres-DbUXNuy_.mjs → postgres-BGERehmX.mjs} +22 -1
  36. package/dist/{postgres-DbUXNuy_.mjs.map → postgres-BGERehmX.mjs.map} +1 -1
  37. package/dist/{query-feedback-Dv43XKHM.mjs → query-feedback-CQSumXDy.mjs} +1 -1
  38. package/dist/{query-feedback-Dv43XKHM.mjs.map → query-feedback-CQSumXDy.mjs.map} +1 -1
  39. package/dist/skills/Reconstruct/SKILL.md +36 -0
  40. package/dist/{sqlite-l-s9xPjY.mjs → sqlite-BJrME_vg.mjs} +1 -1
  41. package/dist/{sqlite-l-s9xPjY.mjs.map → sqlite-BJrME_vg.mjs.map} +1 -1
  42. package/dist/{state-C6_vqz7w.mjs → state-BIlxNRUn.mjs} +1 -1
  43. package/dist/{state-C6_vqz7w.mjs.map → state-BIlxNRUn.mjs.map} +1 -1
  44. package/dist/{themes-BvYF0W8T.mjs → themes-9jxFn3Rf.mjs} +1 -1
  45. package/dist/{themes-BvYF0W8T.mjs.map → themes-9jxFn3Rf.mjs.map} +1 -1
  46. package/dist/{tools-BXSwlzeH.mjs → tools-8t7BQrm9.mjs} +717 -15
  47. package/dist/tools-8t7BQrm9.mjs.map +1 -0
  48. package/dist/{trace-CRx9lPuc.mjs → trace-C2XrzssW.mjs} +1 -1
  49. package/dist/{trace-CRx9lPuc.mjs.map → trace-C2XrzssW.mjs.map} +1 -1
  50. package/dist/{vault-indexer-B-aJpRZC.mjs → vault-indexer-TTCl1QOL.mjs} +1 -1
  51. package/dist/{vault-indexer-B-aJpRZC.mjs.map → vault-indexer-TTCl1QOL.mjs.map} +1 -1
  52. package/dist/{zettelkasten-DhBKZQHF.mjs → zettelkasten-BdaMzTGQ.mjs} +3 -3
  53. package/dist/{zettelkasten-DhBKZQHF.mjs.map → zettelkasten-BdaMzTGQ.mjs.map} +1 -1
  54. package/package.json +1 -1
  55. package/src/hooks/ts/session-start/load-project-context.ts +36 -0
  56. package/src/hooks/ts/stop/stop-hook.ts +203 -1
  57. package/dist/daemon-BaYX-w_d.mjs.map +0 -1
  58. package/dist/indexer-D53l5d1U.mjs +0 -1
  59. package/dist/tools-BXSwlzeH.mjs.map +0 -1
@@ -198,4 +198,4 @@ async function handleGraphClusters(pool, backend, params) {
198
198
 
199
199
  //#endregion
200
200
  export { handleGraphClusters };
201
- //# sourceMappingURL=clusters-JIDQW65f.mjs.map
201
+ //# sourceMappingURL=clusters-CRlPBpq8.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"clusters-JIDQW65f.mjs","names":[],"sources":["../src/graph/clusters.ts"],"sourcesContent":["/**\n * clusters.ts — graph_clusters endpoint handler\n *\n * Reuses the zettelThemes() agglomerative clustering algorithm and enriches\n * each cluster with observation-type statistics, avg_recency from member\n * timestamps, and helper flags for the Obsidian knowledge plugin.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { Pool } from \"pg\";\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphClustersParams {\n project_id?: number;\n min_size?: number;\n max_clusters?: number;\n lookback_days?: number;\n similarity_threshold?: number;\n}\n\nexport interface ClusterNode {\n id: number;\n label: string;\n size: number;\n folder_diversity: number;\n avg_recency: number;\n linked_ratio: number;\n dominant_observation_type: string;\n observation_type_counts: Record<string, number>;\n suggest_index_note: boolean;\n has_idea_note: boolean;\n notes: Array<{ vault_path: string; title: string; indexed_at: number }>;\n}\n\nexport interface GraphClustersResult {\n clusters: ClusterNode[];\n total_notes_analyzed: number;\n time_window: { from: number; to: number };\n}\n\n// ---------------------------------------------------------------------------\n// Observation type enrichment\n// ---------------------------------------------------------------------------\n\n/**\n * Query pai_observations (Postgres) for observation types associated with\n * the given file paths. Returns a map from vault_path → type counts.\n *\n * Falls back to an empty map when the pool is not available or the query fails.\n */\nasync function fetchObservationTypes(\n pool: Pool,\n filePaths: string[],\n projectId?: number\n): Promise<Map<string, Record<string, number>>> {\n if (filePaths.length === 0) return new Map();\n\n try {\n const params: (string | number)[] = [...filePaths];\n let projectFilter = \"\";\n if (projectId !== undefined) {\n params.push(projectId);\n projectFilter = `AND project_id = $${params.length}`;\n }\n\n const result = await pool.query<{ path: string; type: string; cnt: string }>(\n `SELECT unnested_path AS path, type, COUNT(*) AS cnt\n FROM pai_observations,\n LATERAL unnest(files_modified || files_read) AS unnested_path\n WHERE unnested_path = ANY($1::text[])\n ${projectFilter}\n GROUP BY unnested_path, type`,\n [filePaths, ...params.slice(filePaths.length)]\n );\n\n const byPath = new Map<string, Record<string, number>>();\n for (const row of result.rows) {\n const existing = byPath.get(row.path) ?? {};\n existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);\n byPath.set(row.path, existing);\n }\n return byPath;\n } catch {\n return new Map();\n }\n}\n\n/**\n * Aggregate per-path observation type counts into cluster-level counts,\n * then pick the dominant type.\n */\nfunction aggregateObservationTypes(\n paths: string[],\n byPath: Map<string, Record<string, number>>\n): { dominant: string; counts: Record<string, number> } {\n const counts: Record<string, number> = {};\n for (const path of paths) {\n const pathCounts = byPath.get(path);\n if (!pathCounts) continue;\n for (const [type, n] of Object.entries(pathCounts)) {\n counts[type] = (counts[type] ?? 0) + n;\n }\n }\n\n let dominant = \"unknown\";\n let maxCount = 0;\n for (const [type, n] of Object.entries(counts)) {\n if (n > maxCount) {\n maxCount = n;\n dominant = type;\n }\n }\n\n return { dominant, counts };\n}\n\n// ---------------------------------------------------------------------------\n// Link-based fallback clustering (wikilink connected components)\n// ---------------------------------------------------------------------------\n\nconst SKIP_PREFIXES = [\n \"Attachments/\", \"🗓️ Daily Notes/\", \"Copilot/copilot-conversations/\",\n \"Z - Zettelkasten/Tweets/\",\n];\n\n/**\n * Cluster vault notes by wikilink connectivity when embeddings aren't available.\n * Uses BFS to find connected components in the link graph, then picks the\n * largest components as clusters. Labels are derived from the most common\n * title words in each component.\n */\nasync function clusterByLinks(\n backend: StorageBackend,\n lookbackDays: number,\n minSize: number,\n maxClusters: number,\n): Promise<{ themes: Array<{ id: number; label: string; notes: Array<{ path: string; title: string | null }>; size: number; folderDiversity: number; avgRecency: number; linkedRatio: number; suggestIndexNote: boolean }>; totalNotesAnalyzed: number; timeWindow: { from: number; to: number } }> {\n const now = Date.now();\n const from = now - lookbackDays * 86400000;\n\n // Get recent notes\n const recentFiles = await backend.getRecentVaultFiles(from);\n const recentNotes = recentFiles.filter(f => f.vaultPath.endsWith(\".md\"));\n\n const noteMap = new Map<string, { title: string | null; indexed_at: number }>();\n for (const n of recentNotes) {\n noteMap.set(n.vaultPath, { title: n.title, indexed_at: n.indexedAt });\n }\n\n // Build adjacency list from vault_links (only for recent notes)\n const adj = new Map<string, Set<string>>();\n for (const path of noteMap.keys()) {\n if (!adj.has(path)) adj.set(path, new Set());\n }\n\n const linkGraph = await backend.getVaultLinkGraph();\n\n for (const { source_path, target_path } of linkGraph) {\n if (noteMap.has(source_path) && noteMap.has(target_path)) {\n adj.get(source_path)!.add(target_path);\n adj.get(target_path)!.add(source_path);\n }\n }\n\n // Remove hub nodes before BFS\n const degrees = [...adj.entries()].map(([p, s]) => ({ path: p, degree: s.size }));\n degrees.sort((a, b) => b.degree - a.degree);\n const hubThreshold = Math.max(10, degrees[Math.floor(degrees.length * 0.05)]?.degree ?? 10);\n const hubNodes = new Set<string>();\n for (const { path, degree } of degrees) {\n if (degree >= hubThreshold) hubNodes.add(path);\n else break;\n }\n\n for (const hub of hubNodes) {\n adj.delete(hub);\n }\n for (const [, neighbors] of adj) {\n for (const hub of hubNodes) {\n neighbors.delete(hub);\n }\n }\n\n // BFS connected components\n const visited = new Set<string>();\n const components: string[][] = [];\n\n for (const path of noteMap.keys()) {\n if (visited.has(path) || hubNodes.has(path)) continue;\n if (SKIP_PREFIXES.some(p => path.startsWith(p))) { visited.add(path); continue; }\n const component: string[] = [];\n const queue = [path];\n visited.add(path);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n component.push(current);\n const neighbors = adj.get(current);\n if (!neighbors) continue;\n for (const neighbor of neighbors) {\n if (!visited.has(neighbor) && !SKIP_PREFIXES.some(p => neighbor.startsWith(p))) {\n visited.add(neighbor);\n queue.push(neighbor);\n }\n }\n }\n\n if (component.length >= minSize) {\n components.push(component);\n }\n }\n\n components.sort((a, b) => b.length - a.length);\n const topComponents = components.slice(0, maxClusters);\n\n // STOP_WORDS imported from utils/stop-words.ts (module-level import)\n\n function generateLinkLabel(paths: string[]): string {\n const wordCounts = new Map<string, number>();\n for (const p of paths) {\n const title = noteMap.get(p)?.title;\n if (!title) continue;\n const words = title.toLowerCase().replace(/[^a-z0-9äöüàéèêëçñß\\s]/g, \" \").split(/\\s+/)\n .filter(w => w.length > 2 && !STOP_WORDS.has(w));\n for (const word of words) {\n wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);\n }\n }\n const sorted = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]);\n return sorted.slice(0, 3).map(([w]) => w).join(\" / \") || \"Linked Notes\";\n }\n\n const themes = topComponents.map((component, idx) => {\n const notes = component.map(p => ({\n path: p,\n title: noteMap.get(p)?.title ?? null,\n }));\n const avgRecency = component.reduce((sum, p) => sum + (noteMap.get(p)?.indexed_at ?? 0), 0) / component.length;\n const uniqueFolders = new Set(component.map(p => p.split(\"/\")[0]));\n\n return {\n id: idx,\n label: generateLinkLabel(component),\n notes,\n size: component.length,\n folderDiversity: uniqueFolders.size / component.length,\n avgRecency,\n linkedRatio: 1.0,\n suggestIndexNote: component.length >= 10,\n };\n });\n\n return {\n themes,\n totalNotesAnalyzed: recentNotes.length,\n timeWindow: { from, to: now },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphClusters(\n pool: Pool | null,\n backend: StorageBackend,\n params: GraphClustersParams\n): Promise<GraphClustersResult> {\n const minSize = params.min_size ?? 3;\n const maxClusters = params.max_clusters ?? 20;\n const lookbackDays = params.lookback_days ?? 90;\n\n const vaultProjectId = params.project_id ?? 0;\n\n if (!vaultProjectId) {\n throw new Error(\n \"graph_clusters: project_id is required (pass the vault project's numeric ID)\"\n );\n }\n\n const themeResult = await clusterByLinks(backend, lookbackDays, minSize, maxClusters);\n\n const allPaths = themeResult.themes.flatMap((t) => t.notes.map((n) => n.path));\n\n const observationsByPath =\n pool !== null\n ? await fetchObservationTypes(pool, allPaths, params.project_id)\n : new Map<string, Record<string, number>>();\n\n // Fetch indexed_at timestamps for all notes in bulk\n const fileRows = await backend.getVaultFilesByPaths(allPaths);\n const indexedAtMap = new Map<string, number>(fileRows.map(f => [f.vaultPath, f.indexedAt]));\n\n const clusters: ClusterNode[] = themeResult.themes.map((theme) => {\n const notePaths = theme.notes.map((n) => n.path);\n\n const notesWithTimestamps = theme.notes.map((n) => ({\n vault_path: n.path,\n title: n.title ?? n.path.split(\"/\").pop() ?? n.path,\n indexed_at: indexedAtMap.get(n.path) ?? 0,\n }));\n\n const avgRecency = theme.avgRecency;\n\n const { dominant, counts } = aggregateObservationTypes(\n notePaths,\n observationsByPath\n );\n\n return {\n id: theme.id,\n label: theme.label,\n size: theme.size,\n folder_diversity: theme.folderDiversity,\n avg_recency: avgRecency,\n linked_ratio: theme.linkedRatio,\n dominant_observation_type: dominant,\n observation_type_counts: counts,\n suggest_index_note: theme.suggestIndexNote,\n has_idea_note: false,\n notes: notesWithTimestamps,\n };\n });\n\n clusters.sort((a, b) => b.size - a.size);\n\n return {\n clusters: clusters.slice(0, maxClusters),\n total_notes_analyzed: themeResult.totalNotesAnalyzed,\n time_window: themeResult.timeWindow,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"clusters-CRlPBpq8.mjs","names":[],"sources":["../src/graph/clusters.ts"],"sourcesContent":["/**\n * clusters.ts — graph_clusters endpoint handler\n *\n * Reuses the zettelThemes() agglomerative clustering algorithm and enriches\n * each cluster with observation-type statistics, avg_recency from member\n * timestamps, and helper flags for the Obsidian knowledge plugin.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { Pool } from \"pg\";\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphClustersParams {\n project_id?: number;\n min_size?: number;\n max_clusters?: number;\n lookback_days?: number;\n similarity_threshold?: number;\n}\n\nexport interface ClusterNode {\n id: number;\n label: string;\n size: number;\n folder_diversity: number;\n avg_recency: number;\n linked_ratio: number;\n dominant_observation_type: string;\n observation_type_counts: Record<string, number>;\n suggest_index_note: boolean;\n has_idea_note: boolean;\n notes: Array<{ vault_path: string; title: string; indexed_at: number }>;\n}\n\nexport interface GraphClustersResult {\n clusters: ClusterNode[];\n total_notes_analyzed: number;\n time_window: { from: number; to: number };\n}\n\n// ---------------------------------------------------------------------------\n// Observation type enrichment\n// ---------------------------------------------------------------------------\n\n/**\n * Query pai_observations (Postgres) for observation types associated with\n * the given file paths. Returns a map from vault_path → type counts.\n *\n * Falls back to an empty map when the pool is not available or the query fails.\n */\nasync function fetchObservationTypes(\n pool: Pool,\n filePaths: string[],\n projectId?: number\n): Promise<Map<string, Record<string, number>>> {\n if (filePaths.length === 0) return new Map();\n\n try {\n const params: (string | number)[] = [...filePaths];\n let projectFilter = \"\";\n if (projectId !== undefined) {\n params.push(projectId);\n projectFilter = `AND project_id = $${params.length}`;\n }\n\n const result = await pool.query<{ path: string; type: string; cnt: string }>(\n `SELECT unnested_path AS path, type, COUNT(*) AS cnt\n FROM pai_observations,\n LATERAL unnest(files_modified || files_read) AS unnested_path\n WHERE unnested_path = ANY($1::text[])\n ${projectFilter}\n GROUP BY unnested_path, type`,\n [filePaths, ...params.slice(filePaths.length)]\n );\n\n const byPath = new Map<string, Record<string, number>>();\n for (const row of result.rows) {\n const existing = byPath.get(row.path) ?? {};\n existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);\n byPath.set(row.path, existing);\n }\n return byPath;\n } catch {\n return new Map();\n }\n}\n\n/**\n * Aggregate per-path observation type counts into cluster-level counts,\n * then pick the dominant type.\n */\nfunction aggregateObservationTypes(\n paths: string[],\n byPath: Map<string, Record<string, number>>\n): { dominant: string; counts: Record<string, number> } {\n const counts: Record<string, number> = {};\n for (const path of paths) {\n const pathCounts = byPath.get(path);\n if (!pathCounts) continue;\n for (const [type, n] of Object.entries(pathCounts)) {\n counts[type] = (counts[type] ?? 0) + n;\n }\n }\n\n let dominant = \"unknown\";\n let maxCount = 0;\n for (const [type, n] of Object.entries(counts)) {\n if (n > maxCount) {\n maxCount = n;\n dominant = type;\n }\n }\n\n return { dominant, counts };\n}\n\n// ---------------------------------------------------------------------------\n// Link-based fallback clustering (wikilink connected components)\n// ---------------------------------------------------------------------------\n\nconst SKIP_PREFIXES = [\n \"Attachments/\", \"🗓️ Daily Notes/\", \"Copilot/copilot-conversations/\",\n \"Z - Zettelkasten/Tweets/\",\n];\n\n/**\n * Cluster vault notes by wikilink connectivity when embeddings aren't available.\n * Uses BFS to find connected components in the link graph, then picks the\n * largest components as clusters. Labels are derived from the most common\n * title words in each component.\n */\nasync function clusterByLinks(\n backend: StorageBackend,\n lookbackDays: number,\n minSize: number,\n maxClusters: number,\n): Promise<{ themes: Array<{ id: number; label: string; notes: Array<{ path: string; title: string | null }>; size: number; folderDiversity: number; avgRecency: number; linkedRatio: number; suggestIndexNote: boolean }>; totalNotesAnalyzed: number; timeWindow: { from: number; to: number } }> {\n const now = Date.now();\n const from = now - lookbackDays * 86400000;\n\n // Get recent notes\n const recentFiles = await backend.getRecentVaultFiles(from);\n const recentNotes = recentFiles.filter(f => f.vaultPath.endsWith(\".md\"));\n\n const noteMap = new Map<string, { title: string | null; indexed_at: number }>();\n for (const n of recentNotes) {\n noteMap.set(n.vaultPath, { title: n.title, indexed_at: n.indexedAt });\n }\n\n // Build adjacency list from vault_links (only for recent notes)\n const adj = new Map<string, Set<string>>();\n for (const path of noteMap.keys()) {\n if (!adj.has(path)) adj.set(path, new Set());\n }\n\n const linkGraph = await backend.getVaultLinkGraph();\n\n for (const { source_path, target_path } of linkGraph) {\n if (noteMap.has(source_path) && noteMap.has(target_path)) {\n adj.get(source_path)!.add(target_path);\n adj.get(target_path)!.add(source_path);\n }\n }\n\n // Remove hub nodes before BFS\n const degrees = [...adj.entries()].map(([p, s]) => ({ path: p, degree: s.size }));\n degrees.sort((a, b) => b.degree - a.degree);\n const hubThreshold = Math.max(10, degrees[Math.floor(degrees.length * 0.05)]?.degree ?? 10);\n const hubNodes = new Set<string>();\n for (const { path, degree } of degrees) {\n if (degree >= hubThreshold) hubNodes.add(path);\n else break;\n }\n\n for (const hub of hubNodes) {\n adj.delete(hub);\n }\n for (const [, neighbors] of adj) {\n for (const hub of hubNodes) {\n neighbors.delete(hub);\n }\n }\n\n // BFS connected components\n const visited = new Set<string>();\n const components: string[][] = [];\n\n for (const path of noteMap.keys()) {\n if (visited.has(path) || hubNodes.has(path)) continue;\n if (SKIP_PREFIXES.some(p => path.startsWith(p))) { visited.add(path); continue; }\n const component: string[] = [];\n const queue = [path];\n visited.add(path);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n component.push(current);\n const neighbors = adj.get(current);\n if (!neighbors) continue;\n for (const neighbor of neighbors) {\n if (!visited.has(neighbor) && !SKIP_PREFIXES.some(p => neighbor.startsWith(p))) {\n visited.add(neighbor);\n queue.push(neighbor);\n }\n }\n }\n\n if (component.length >= minSize) {\n components.push(component);\n }\n }\n\n components.sort((a, b) => b.length - a.length);\n const topComponents = components.slice(0, maxClusters);\n\n // STOP_WORDS imported from utils/stop-words.ts (module-level import)\n\n function generateLinkLabel(paths: string[]): string {\n const wordCounts = new Map<string, number>();\n for (const p of paths) {\n const title = noteMap.get(p)?.title;\n if (!title) continue;\n const words = title.toLowerCase().replace(/[^a-z0-9äöüàéèêëçñß\\s]/g, \" \").split(/\\s+/)\n .filter(w => w.length > 2 && !STOP_WORDS.has(w));\n for (const word of words) {\n wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);\n }\n }\n const sorted = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]);\n return sorted.slice(0, 3).map(([w]) => w).join(\" / \") || \"Linked Notes\";\n }\n\n const themes = topComponents.map((component, idx) => {\n const notes = component.map(p => ({\n path: p,\n title: noteMap.get(p)?.title ?? null,\n }));\n const avgRecency = component.reduce((sum, p) => sum + (noteMap.get(p)?.indexed_at ?? 0), 0) / component.length;\n const uniqueFolders = new Set(component.map(p => p.split(\"/\")[0]));\n\n return {\n id: idx,\n label: generateLinkLabel(component),\n notes,\n size: component.length,\n folderDiversity: uniqueFolders.size / component.length,\n avgRecency,\n linkedRatio: 1.0,\n suggestIndexNote: component.length >= 10,\n };\n });\n\n return {\n themes,\n totalNotesAnalyzed: recentNotes.length,\n timeWindow: { from, to: now },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphClusters(\n pool: Pool | null,\n backend: StorageBackend,\n params: GraphClustersParams\n): Promise<GraphClustersResult> {\n const minSize = params.min_size ?? 3;\n const maxClusters = params.max_clusters ?? 20;\n const lookbackDays = params.lookback_days ?? 90;\n\n const vaultProjectId = params.project_id ?? 0;\n\n if (!vaultProjectId) {\n throw new Error(\n \"graph_clusters: project_id is required (pass the vault project's numeric ID)\"\n );\n }\n\n const themeResult = await clusterByLinks(backend, lookbackDays, minSize, maxClusters);\n\n const allPaths = themeResult.themes.flatMap((t) => t.notes.map((n) => n.path));\n\n const observationsByPath =\n pool !== null\n ? await fetchObservationTypes(pool, allPaths, params.project_id)\n : new Map<string, Record<string, number>>();\n\n // Fetch indexed_at timestamps for all notes in bulk\n const fileRows = await backend.getVaultFilesByPaths(allPaths);\n const indexedAtMap = new Map<string, number>(fileRows.map(f => [f.vaultPath, f.indexedAt]));\n\n const clusters: ClusterNode[] = themeResult.themes.map((theme) => {\n const notePaths = theme.notes.map((n) => n.path);\n\n const notesWithTimestamps = theme.notes.map((n) => ({\n vault_path: n.path,\n title: n.title ?? n.path.split(\"/\").pop() ?? n.path,\n indexed_at: indexedAtMap.get(n.path) ?? 0,\n }));\n\n const avgRecency = theme.avgRecency;\n\n const { dominant, counts } = aggregateObservationTypes(\n notePaths,\n observationsByPath\n );\n\n return {\n id: theme.id,\n label: theme.label,\n size: theme.size,\n folder_diversity: theme.folderDiversity,\n avg_recency: avgRecency,\n linked_ratio: theme.linkedRatio,\n dominant_observation_type: dominant,\n observation_type_counts: counts,\n suggest_index_note: theme.suggestIndexNote,\n has_idea_note: false,\n notes: notesWithTimestamps,\n };\n });\n\n clusters.sort((a, b) => b.size - a.size);\n\n return {\n clusters: clusters.slice(0, maxClusters),\n total_notes_analyzed: themeResult.totalNotesAnalyzed,\n time_window: themeResult.timeWindow,\n };\n}\n"],"mappings":"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"}
@@ -4,14 +4,14 @@ import "../helpers-BEST-4Gx.mjs";
4
4
  import "../sync-BOsnEj2-.mjs";
5
5
  import "../embeddings-DGRAPAYb.mjs";
6
6
  import "../search-DC1qhkKn.mjs";
7
- import "../indexer-D53l5d1U.mjs";
7
+ import "../kg-extraction-BlGM40q7.mjs";
8
8
  import { t as PaiClient } from "../ipc-client-CoyUHPod.mjs";
9
9
  import { i as ensureConfigDir, o as loadConfig } from "../config-BuhHWyOK.mjs";
10
- import "../factory-BzWfxsvK.mjs";
11
- import { n as serve } from "../daemon-BaYX-w_d.mjs";
12
- import "../state-C6_vqz7w.mjs";
13
- import "../tools-BXSwlzeH.mjs";
14
- import "../detector-jGBuYQJM.mjs";
10
+ import "../factory-DKDPRhAN.mjs";
11
+ import { n as serve } from "../daemon-kp49BE7u.mjs";
12
+ import "../state-BIlxNRUn.mjs";
13
+ import "../tools-8t7BQrm9.mjs";
14
+ import "../detector-CNU3zCwP.mjs";
15
15
  import { Command } from "commander";
16
16
 
17
17
  //#region src/daemon/index.ts
@@ -3,11 +3,12 @@ import { n as openRegistry } from "./db-BtuN768f.mjs";
3
3
  import { d as sha256 } from "./helpers-BEST-4Gx.mjs";
4
4
  import { n as indexAll } from "./sync-BOsnEj2-.mjs";
5
5
  import { t as configureEmbeddingModel } from "./embeddings-DGRAPAYb.mjs";
6
+ import { t as extractAndStoreTriples$1 } from "./kg-extraction-BlGM40q7.mjs";
6
7
  import { n as CONFIG_FILE, s as DEFAULT_NOTIFICATION_CONFIG, t as CONFIG_DIR } from "./config-BuhHWyOK.mjs";
7
- import { t as createStorageBackend } from "./factory-BzWfxsvK.mjs";
8
- import { C as setStorageBackend, E as startTime, O as storageBackend, S as setStartTime, T as shutdownRequested, _ as setLastIndexTime, a as indexSchedulerTimer, b as setRegistryDb, c as lastVaultIndexTime, d as setDaemonConfig, f as setEmbedInProgress, g as setLastEmbedTime, h as setIndexSchedulerTimer, i as indexInProgress, k as vaultIndexInProgress, l as notificationConfig, m as setIndexInProgress, n as embedInProgress, o as lastEmbedTime, p as setEmbedSchedulerTimer, r as embedSchedulerTimer, s as lastIndexTime, t as daemonConfig, u as registryDb, v as setLastVaultIndexTime, w as setVaultIndexInProgress, x as setShutdownRequested, y as setNotificationConfig } from "./state-C6_vqz7w.mjs";
9
- import { a as toolProjectDetect, c as toolProjectList, d as toolMemorySearch, i as toolSessionRoute, l as toolProjectTodo, n as toolRegistrySearch, o as toolProjectHealth, r as toolSessionList, s as toolProjectInfo, u as toolMemoryGet } from "./tools-BXSwlzeH.mjs";
10
- import { t as detectTopicShift } from "./detector-jGBuYQJM.mjs";
8
+ import { t as createStorageBackend } from "./factory-DKDPRhAN.mjs";
9
+ import { C as setStorageBackend, E as startTime, O as storageBackend, S as setStartTime, T as shutdownRequested, _ as setLastIndexTime, a as indexSchedulerTimer, b as setRegistryDb, c as lastVaultIndexTime, d as setDaemonConfig, f as setEmbedInProgress, g as setLastEmbedTime, h as setIndexSchedulerTimer, i as indexInProgress, k as vaultIndexInProgress, l as notificationConfig, m as setIndexInProgress, n as embedInProgress, o as lastEmbedTime, p as setEmbedSchedulerTimer, r as embedSchedulerTimer, s as lastIndexTime, t as daemonConfig, u as registryDb, v as setLastVaultIndexTime, w as setVaultIndexInProgress, x as setShutdownRequested, y as setNotificationConfig } from "./state-BIlxNRUn.mjs";
10
+ import { a as toolSessionList, c as toolProjectHealth, d as toolProjectTodo, f as toolMemoryGet, i as toolRegistrySearch, l as toolProjectInfo, n as toolMemoryTaxonomy, o as toolSessionRoute, p as toolMemorySearch, r as toolMemoryWakeup, s as toolProjectDetect, u as toolProjectList } from "./tools-8t7BQrm9.mjs";
11
+ import { t as detectTopicShift } from "./detector-CNU3zCwP.mjs";
11
12
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
13
  import { homedir, setPriority } from "node:os";
13
14
  import { basename, dirname, join } from "node:path";
@@ -114,7 +115,7 @@ async function runIndex() {
114
115
  try {
115
116
  process.stderr.write("[pai-daemon] Starting scheduled index run...\n");
116
117
  if (storageBackend.backendType === "sqlite") {
117
- const { SQLiteBackend } = await import("./sqlite-l-s9xPjY.mjs");
118
+ const { SQLiteBackend } = await import("./sqlite-BJrME_vg.mjs");
118
119
  if (storageBackend instanceof SQLiteBackend) {
119
120
  const { projects, result } = await indexAll(storageBackend.getRawDb(), registryDb);
120
121
  const elapsed = Date.now() - t0;
@@ -122,7 +123,7 @@ async function runIndex() {
122
123
  process.stderr.write(`[pai-daemon] Index complete: ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
123
124
  }
124
125
  } else {
125
- const { indexAllWithBackend } = await import("./indexer-backend-jcJFsmB4.mjs");
126
+ const { indexAllWithBackend } = await import("./indexer-backend-CIIlrYh6.mjs");
126
127
  const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);
127
128
  const elapsed = Date.now() - t0;
128
129
  setLastIndexTime(Date.now());
@@ -149,7 +150,7 @@ async function runVaultIndex() {
149
150
  process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
150
151
  return;
151
152
  }
152
- const { lastVaultIndexTime } = await import("./state-C6_vqz7w.mjs").then((n) => n.D);
153
+ const { lastVaultIndexTime } = await import("./state-BIlxNRUn.mjs").then((n) => n.D);
153
154
  if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) return;
154
155
  let vaultProjectId = daemonConfig.vaultProjectId;
155
156
  if (!vaultProjectId) {
@@ -161,7 +162,7 @@ async function runVaultIndex() {
161
162
  const t0 = Date.now();
162
163
  process.stderr.write("[pai-daemon] Starting vault index run...\n");
163
164
  try {
164
- const { indexVault } = await import("./vault-indexer-B-aJpRZC.mjs");
165
+ const { indexVault } = await import("./vault-indexer-TTCl1QOL.mjs");
165
166
  const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath);
166
167
  const elapsed = Date.now() - t0;
167
168
  setLastVaultIndexTime(Date.now());
@@ -214,11 +215,11 @@ async function runEmbed() {
214
215
  const rows = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active'").all();
215
216
  for (const r of rows) projectNames.set(r.id, r.slug);
216
217
  } catch {}
217
- const { embedChunksWithBackend } = await import("./indexer-backend-jcJFsmB4.mjs");
218
+ const { embedChunksWithBackend } = await import("./indexer-backend-CIIlrYh6.mjs");
218
219
  const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames);
219
220
  let vaultEmbedCount = 0;
220
221
  if (daemonConfig.vaultPath) try {
221
- const { SQLiteBackend } = await import("./sqlite-l-s9xPjY.mjs");
222
+ const { SQLiteBackend } = await import("./sqlite-BJrME_vg.mjs");
222
223
  const { openFederation } = await import("./db-DdUperSl.mjs").then((n) => n.t);
223
224
  const federationDb = openFederation();
224
225
  const vaultSqliteBackend = new SQLiteBackend(federationDb);
@@ -690,6 +691,8 @@ async function dispatchTool(method, params) {
690
691
  case "project_detect": return toolProjectDetect(registryDb, p);
691
692
  case "project_health": return toolProjectHealth(registryDb, p);
692
693
  case "project_todo": return toolProjectTodo(registryDb, p);
694
+ case "memory_wakeup": return toolMemoryWakeup(registryDb, p);
695
+ case "memory_taxonomy": return toolMemoryTaxonomy(registryDb, storageBackend, p);
693
696
  case "topic_check": return detectTopicShift(registryDb, storageBackend, p);
694
697
  case "session_auto_route": return toolSessionRoute(registryDb, storageBackend, p);
695
698
  case "zettel_explore":
@@ -700,7 +703,7 @@ async function dispatchTool(method, params) {
700
703
  case "zettel_themes":
701
704
  case "zettel_god_notes":
702
705
  case "zettel_communities": {
703
- const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes, toolZettelGodNotes, toolZettelCommunities } = await import("./tools-BXSwlzeH.mjs").then((n) => n.t);
706
+ const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes, toolZettelGodNotes, toolZettelCommunities } = await import("./tools-8t7BQrm9.mjs").then((n) => n.t);
704
707
  switch (method) {
705
708
  case "zettel_explore": return toolZettelExplore(storageBackend, p);
706
709
  case "zettel_health": return toolZettelHealth(storageBackend, p);
@@ -714,30 +717,49 @@ async function dispatchTool(method, params) {
714
717
  break;
715
718
  }
716
719
  case "graph_clusters": {
717
- const { handleGraphClusters } = await import("./clusters-JIDQW65f.mjs");
720
+ const { handleGraphClusters } = await import("./clusters-CRlPBpq8.mjs");
718
721
  return handleGraphClusters(storageBackend.getPool?.() ?? null, storageBackend, p);
719
722
  }
720
723
  case "graph_neighborhood": {
721
- const { handleGraphNeighborhood } = await import("./neighborhood-BYYbEkUJ.mjs");
724
+ const { handleGraphNeighborhood } = await import("./neighborhood-u8ytjmWq.mjs");
722
725
  return handleGraphNeighborhood(storageBackend.getPool?.() ?? null, storageBackend, p);
723
726
  }
724
727
  case "graph_note_context": {
725
- const { handleGraphNoteContext } = await import("./note-context-BK24bX8Y.mjs");
728
+ const { handleGraphNoteContext } = await import("./note-context-CG2_e-0W.mjs");
726
729
  return handleGraphNoteContext(storageBackend.getPool?.() ?? null, storageBackend, p);
727
730
  }
728
731
  case "graph_trace": {
729
- const { handleGraphTrace } = await import("./trace-CRx9lPuc.mjs");
732
+ const { handleGraphTrace } = await import("./trace-C2XrzssW.mjs");
730
733
  return handleGraphTrace(storageBackend, p);
731
734
  }
732
735
  case "graph_latent_ideas": {
733
- const { handleGraphLatentIdeas } = await import("./latent-ideas-bTJo6Omd.mjs");
736
+ const { handleGraphLatentIdeas } = await import("./latent-ideas-DvWBRHsy.mjs");
734
737
  return handleGraphLatentIdeas(storageBackend, p);
735
738
  }
736
739
  case "idea_materialize": {
737
- const { handleIdeaMaterialize } = await import("./latent-ideas-bTJo6Omd.mjs");
740
+ const { handleIdeaMaterialize } = await import("./latent-ideas-DvWBRHsy.mjs");
738
741
  if (!daemonConfig.vaultPath) throw new Error("idea_materialize requires vaultPath to be configured in the daemon config");
739
742
  return handleIdeaMaterialize(p, daemonConfig.vaultPath);
740
743
  }
744
+ case "kg_add":
745
+ case "kg_query":
746
+ case "kg_invalidate":
747
+ case "kg_contradictions": {
748
+ const { toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions } = await import("./tools-8t7BQrm9.mjs").then((n) => n.t);
749
+ const pgPool = storageBackend.getPool?.() ?? null;
750
+ if (!pgPool) throw new Error(`${method} requires a Postgres storage backend`);
751
+ switch (method) {
752
+ case "kg_add": return toolKgAdd(pgPool, p);
753
+ case "kg_query": return toolKgQuery(pgPool, p);
754
+ case "kg_invalidate": return toolKgInvalidate(pgPool, p);
755
+ case "kg_contradictions": return toolKgContradictions(pgPool, p);
756
+ }
757
+ break;
758
+ }
759
+ case "memory_tunnels": {
760
+ const { toolMemoryTunnels } = await import("./tools-8t7BQrm9.mjs").then((n) => n.t);
761
+ return toolMemoryTunnels(registryDb, storageBackend, p);
762
+ }
741
763
  default: throw new Error(`Unknown method: ${method}`);
742
764
  }
743
765
  }
@@ -2052,6 +2074,50 @@ ${aiBody}
2052
2074
  }
2053
2075
  }
2054
2076
  /**
2077
+ * Look up the integer project_id from the registry DB for a given slug.
2078
+ * Returns null if not found or registryDb is not yet initialized.
2079
+ */
2080
+ function lookupProjectId(slug) {
2081
+ try {
2082
+ if (!registryDb) return null;
2083
+ return registryDb.prepare("SELECT id FROM projects WHERE slug = ? LIMIT 1").get(slug)?.id ?? null;
2084
+ } catch {
2085
+ return null;
2086
+ }
2087
+ }
2088
+ /**
2089
+ * Extract structured KG triples from a session summary and store them.
2090
+ *
2091
+ * This is best-effort: any error is logged but never propagated.
2092
+ * Requires Postgres backend — silently no-ops on SQLite.
2093
+ */
2094
+ async function extractAndStoreTriples(params) {
2095
+ try {
2096
+ if (!storageBackend || storageBackend.backendType !== "postgres") return;
2097
+ const pool = storageBackend.getPool?.();
2098
+ if (!pool) {
2099
+ process.stderr.write("[session-summary] Triple extraction: no pool available.\n");
2100
+ return;
2101
+ }
2102
+ const cfg = daemonConfig;
2103
+ if (cfg && cfg.kg_extraction_enabled === false) {
2104
+ process.stderr.write("[session-summary] Triple extraction disabled via kg_extraction_enabled=false.\n");
2105
+ return;
2106
+ }
2107
+ const result = await extractAndStoreTriples$1(pool, {
2108
+ summaryText: params.summaryText,
2109
+ projectSlug: params.projectSlug,
2110
+ projectId: params.projectId,
2111
+ sessionId: params.sessionId,
2112
+ gitLog: params.gitLog,
2113
+ model: "sonnet"
2114
+ });
2115
+ process.stderr.write(`[session-summary] Triple extraction complete: ${result.extracted} extracted, ${result.added} added, ${result.superseded} superseded.\n`);
2116
+ } catch (err) {
2117
+ process.stderr.write(`[session-summary] Triple extraction failed: ${err}\n`);
2118
+ }
2119
+ }
2120
+ /**
2055
2121
  * Process a `session-summary` work item.
2056
2122
  *
2057
2123
  * This is the main function called by work-queue-worker.ts.
@@ -2112,6 +2178,14 @@ async function handleSessionSummary(payload) {
2112
2178
  process.stderr.write(`[session-summary] ${selectedModel} produced ${summaryText.length} char summary.\n`);
2113
2179
  const notePath = writeSessionNote(cwd, summaryText, extracted.filesModified);
2114
2180
  if (notePath) process.stderr.write(`[session-summary] Session note written: ${basename(notePath)}\n`);
2181
+ await extractAndStoreTriples({
2182
+ summaryText,
2183
+ projectSlug: projectSlug ?? basename(cwd),
2184
+ projectId: projectSlug ? lookupProjectId(projectSlug) : null,
2185
+ sessionId: sessionId ?? cwd,
2186
+ gitLog,
2187
+ model: selectedModel
2188
+ });
2115
2189
  markCooldown(cwd);
2116
2190
  process.stderr.write("[session-summary] Done.\n");
2117
2191
  }
@@ -2994,7 +3068,7 @@ async function serve(config) {
2994
3068
  process.stderr.write("[pai-daemon] Starting daemon...\n");
2995
3069
  process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\n`);
2996
3070
  process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\n`);
2997
- const { notificationConfig } = await import("./state-C6_vqz7w.mjs").then((n) => n.D);
3071
+ const { notificationConfig } = await import("./state-BIlxNRUn.mjs").then((n) => n.D);
2998
3072
  process.stderr.write(`[pai-daemon] Notification mode: ${notificationConfig.mode}\n`);
2999
3073
  try {
3000
3074
  setPriority(process.pid, 10);
@@ -3062,4 +3136,4 @@ var daemon_exports = /* @__PURE__ */ __exportAll({ serve: () => serve });
3062
3136
 
3063
3137
  //#endregion
3064
3138
  export { serve as n, daemon_exports as t };
3065
- //# sourceMappingURL=daemon-BaYX-w_d.mjs.map
3139
+ //# sourceMappingURL=daemon-kp49BE7u.mjs.map