@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.
- package/ARCHITECTURE.md +121 -0
- package/FEATURE.md +5 -0
- package/README.md +54 -0
- package/dist/{auto-route-C-DrW6BL.mjs → auto-route-CruBrTf-.mjs} +2 -2
- package/dist/{auto-route-C-DrW6BL.mjs.map → auto-route-CruBrTf-.mjs.map} +1 -1
- package/dist/cli/index.mjs +345 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{clusters-JIDQW65f.mjs → clusters-CRlPBpq8.mjs} +1 -1
- package/dist/{clusters-JIDQW65f.mjs.map → clusters-CRlPBpq8.mjs.map} +1 -1
- package/dist/daemon/index.mjs +6 -6
- package/dist/{daemon-BaYX-w_d.mjs → daemon-kp49BE7u.mjs} +93 -19
- package/dist/daemon-kp49BE7u.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +51 -0
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detector-jGBuYQJM.mjs → detector-CNU3zCwP.mjs} +1 -1
- package/dist/{detector-jGBuYQJM.mjs.map → detector-CNU3zCwP.mjs.map} +1 -1
- package/dist/{factory-BzWfxsvK.mjs → factory-DKDPRhAN.mjs} +3 -3
- package/dist/{factory-BzWfxsvK.mjs.map → factory-DKDPRhAN.mjs.map} +1 -1
- package/dist/hooks/load-project-context.mjs +276 -89
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/stop-hook.mjs +152 -2
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/{indexer-backend-jcJFsmB4.mjs → indexer-backend-CIIlrYh6.mjs} +1 -1
- package/dist/{indexer-backend-jcJFsmB4.mjs.map → indexer-backend-CIIlrYh6.mjs.map} +1 -1
- package/dist/kg-B5ysyRLC.mjs +94 -0
- package/dist/kg-B5ysyRLC.mjs.map +1 -0
- package/dist/kg-extraction-BlGM40q7.mjs +211 -0
- package/dist/kg-extraction-BlGM40q7.mjs.map +1 -0
- package/dist/{latent-ideas-bTJo6Omd.mjs → latent-ideas-DvWBRHsy.mjs} +2 -2
- package/dist/{latent-ideas-bTJo6Omd.mjs.map → latent-ideas-DvWBRHsy.mjs.map} +1 -1
- package/dist/{neighborhood-BYYbEkUJ.mjs → neighborhood-u8ytjmWq.mjs} +1 -1
- package/dist/{neighborhood-BYYbEkUJ.mjs.map → neighborhood-u8ytjmWq.mjs.map} +1 -1
- package/dist/{note-context-BK24bX8Y.mjs → note-context-CG2_e-0W.mjs} +1 -1
- package/dist/{note-context-BK24bX8Y.mjs.map → note-context-CG2_e-0W.mjs.map} +1 -1
- package/dist/{postgres-DbUXNuy_.mjs → postgres-BGERehmX.mjs} +22 -1
- package/dist/{postgres-DbUXNuy_.mjs.map → postgres-BGERehmX.mjs.map} +1 -1
- package/dist/{query-feedback-Dv43XKHM.mjs → query-feedback-CQSumXDy.mjs} +1 -1
- package/dist/{query-feedback-Dv43XKHM.mjs.map → query-feedback-CQSumXDy.mjs.map} +1 -1
- package/dist/skills/Reconstruct/SKILL.md +36 -0
- package/dist/{sqlite-l-s9xPjY.mjs → sqlite-BJrME_vg.mjs} +1 -1
- package/dist/{sqlite-l-s9xPjY.mjs.map → sqlite-BJrME_vg.mjs.map} +1 -1
- package/dist/{state-C6_vqz7w.mjs → state-BIlxNRUn.mjs} +1 -1
- package/dist/{state-C6_vqz7w.mjs.map → state-BIlxNRUn.mjs.map} +1 -1
- package/dist/{themes-BvYF0W8T.mjs → themes-9jxFn3Rf.mjs} +1 -1
- package/dist/{themes-BvYF0W8T.mjs.map → themes-9jxFn3Rf.mjs.map} +1 -1
- package/dist/{tools-BXSwlzeH.mjs → tools-8t7BQrm9.mjs} +717 -15
- package/dist/tools-8t7BQrm9.mjs.map +1 -0
- package/dist/{trace-CRx9lPuc.mjs → trace-C2XrzssW.mjs} +1 -1
- package/dist/{trace-CRx9lPuc.mjs.map → trace-C2XrzssW.mjs.map} +1 -1
- package/dist/{vault-indexer-B-aJpRZC.mjs → vault-indexer-TTCl1QOL.mjs} +1 -1
- package/dist/{vault-indexer-B-aJpRZC.mjs.map → vault-indexer-TTCl1QOL.mjs.map} +1 -1
- package/dist/{zettelkasten-DhBKZQHF.mjs → zettelkasten-BdaMzTGQ.mjs} +3 -3
- package/dist/{zettelkasten-DhBKZQHF.mjs.map → zettelkasten-BdaMzTGQ.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/session-start/load-project-context.ts +36 -0
- package/src/hooks/ts/stop/stop-hook.ts +203 -1
- package/dist/daemon-BaYX-w_d.mjs.map +0 -1
- package/dist/indexer-D53l5d1U.mjs +0 -1
- package/dist/tools-BXSwlzeH.mjs.map +0 -1
|
@@ -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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"}
|
package/dist/daemon/index.mjs
CHANGED
|
@@ -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 "../
|
|
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-
|
|
11
|
-
import { n as serve } from "../daemon-
|
|
12
|
-
import "../state-
|
|
13
|
-
import "../tools-
|
|
14
|
-
import "../detector-
|
|
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-
|
|
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-
|
|
9
|
-
import { a as
|
|
10
|
-
import { t as detectTopicShift } from "./detector-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
3139
|
+
//# sourceMappingURL=daemon-kp49BE7u.mjs.map
|