@tekmidian/pai 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{auto-route-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-VIFoKc_z.mjs → daemon-kp49BE7u.mjs} +74 -21
- package/dist/daemon-kp49BE7u.mjs.map +1 -0
- 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-e0k1HWuc.mjs → factory-DKDPRhAN.mjs} +3 -3
- package/dist/{factory-e0k1HWuc.mjs.map → factory-DKDPRhAN.mjs.map} +1 -1
- package/dist/hooks/stop-hook.mjs +6 -1
- package/dist/hooks/stop-hook.mjs.map +2 -2
- 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-DvEPooLO.mjs → postgres-BGERehmX.mjs} +1 -1
- package/dist/{postgres-DvEPooLO.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-C4SBZHga.mjs → tools-8t7BQrm9.mjs} +13 -104
- 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/stop/stop-hook.ts +11 -1
- package/dist/daemon-VIFoKc_z.mjs.map +0 -1
- package/dist/indexer-D53l5d1U.mjs +0 -1
- package/dist/tools-C4SBZHga.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 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-
|
|
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);
|
|
@@ -702,7 +703,7 @@ async function dispatchTool(method, params) {
|
|
|
702
703
|
case "zettel_themes":
|
|
703
704
|
case "zettel_god_notes":
|
|
704
705
|
case "zettel_communities": {
|
|
705
|
-
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);
|
|
706
707
|
switch (method) {
|
|
707
708
|
case "zettel_explore": return toolZettelExplore(storageBackend, p);
|
|
708
709
|
case "zettel_health": return toolZettelHealth(storageBackend, p);
|
|
@@ -716,27 +717,27 @@ async function dispatchTool(method, params) {
|
|
|
716
717
|
break;
|
|
717
718
|
}
|
|
718
719
|
case "graph_clusters": {
|
|
719
|
-
const { handleGraphClusters } = await import("./clusters-
|
|
720
|
+
const { handleGraphClusters } = await import("./clusters-CRlPBpq8.mjs");
|
|
720
721
|
return handleGraphClusters(storageBackend.getPool?.() ?? null, storageBackend, p);
|
|
721
722
|
}
|
|
722
723
|
case "graph_neighborhood": {
|
|
723
|
-
const { handleGraphNeighborhood } = await import("./neighborhood-
|
|
724
|
+
const { handleGraphNeighborhood } = await import("./neighborhood-u8ytjmWq.mjs");
|
|
724
725
|
return handleGraphNeighborhood(storageBackend.getPool?.() ?? null, storageBackend, p);
|
|
725
726
|
}
|
|
726
727
|
case "graph_note_context": {
|
|
727
|
-
const { handleGraphNoteContext } = await import("./note-context-
|
|
728
|
+
const { handleGraphNoteContext } = await import("./note-context-CG2_e-0W.mjs");
|
|
728
729
|
return handleGraphNoteContext(storageBackend.getPool?.() ?? null, storageBackend, p);
|
|
729
730
|
}
|
|
730
731
|
case "graph_trace": {
|
|
731
|
-
const { handleGraphTrace } = await import("./trace-
|
|
732
|
+
const { handleGraphTrace } = await import("./trace-C2XrzssW.mjs");
|
|
732
733
|
return handleGraphTrace(storageBackend, p);
|
|
733
734
|
}
|
|
734
735
|
case "graph_latent_ideas": {
|
|
735
|
-
const { handleGraphLatentIdeas } = await import("./latent-ideas-
|
|
736
|
+
const { handleGraphLatentIdeas } = await import("./latent-ideas-DvWBRHsy.mjs");
|
|
736
737
|
return handleGraphLatentIdeas(storageBackend, p);
|
|
737
738
|
}
|
|
738
739
|
case "idea_materialize": {
|
|
739
|
-
const { handleIdeaMaterialize } = await import("./latent-ideas-
|
|
740
|
+
const { handleIdeaMaterialize } = await import("./latent-ideas-DvWBRHsy.mjs");
|
|
740
741
|
if (!daemonConfig.vaultPath) throw new Error("idea_materialize requires vaultPath to be configured in the daemon config");
|
|
741
742
|
return handleIdeaMaterialize(p, daemonConfig.vaultPath);
|
|
742
743
|
}
|
|
@@ -744,7 +745,7 @@ async function dispatchTool(method, params) {
|
|
|
744
745
|
case "kg_query":
|
|
745
746
|
case "kg_invalidate":
|
|
746
747
|
case "kg_contradictions": {
|
|
747
|
-
const { toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions } = await import("./tools-
|
|
748
|
+
const { toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions } = await import("./tools-8t7BQrm9.mjs").then((n) => n.t);
|
|
748
749
|
const pgPool = storageBackend.getPool?.() ?? null;
|
|
749
750
|
if (!pgPool) throw new Error(`${method} requires a Postgres storage backend`);
|
|
750
751
|
switch (method) {
|
|
@@ -756,7 +757,7 @@ async function dispatchTool(method, params) {
|
|
|
756
757
|
break;
|
|
757
758
|
}
|
|
758
759
|
case "memory_tunnels": {
|
|
759
|
-
const { toolMemoryTunnels } = await import("./tools-
|
|
760
|
+
const { toolMemoryTunnels } = await import("./tools-8t7BQrm9.mjs").then((n) => n.t);
|
|
760
761
|
return toolMemoryTunnels(registryDb, storageBackend, p);
|
|
761
762
|
}
|
|
762
763
|
default: throw new Error(`Unknown method: ${method}`);
|
|
@@ -2073,6 +2074,50 @@ ${aiBody}
|
|
|
2073
2074
|
}
|
|
2074
2075
|
}
|
|
2075
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
|
+
/**
|
|
2076
2121
|
* Process a `session-summary` work item.
|
|
2077
2122
|
*
|
|
2078
2123
|
* This is the main function called by work-queue-worker.ts.
|
|
@@ -2133,6 +2178,14 @@ async function handleSessionSummary(payload) {
|
|
|
2133
2178
|
process.stderr.write(`[session-summary] ${selectedModel} produced ${summaryText.length} char summary.\n`);
|
|
2134
2179
|
const notePath = writeSessionNote(cwd, summaryText, extracted.filesModified);
|
|
2135
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
|
+
});
|
|
2136
2189
|
markCooldown(cwd);
|
|
2137
2190
|
process.stderr.write("[session-summary] Done.\n");
|
|
2138
2191
|
}
|
|
@@ -3015,7 +3068,7 @@ async function serve(config) {
|
|
|
3015
3068
|
process.stderr.write("[pai-daemon] Starting daemon...\n");
|
|
3016
3069
|
process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\n`);
|
|
3017
3070
|
process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\n`);
|
|
3018
|
-
const { notificationConfig } = await import("./state-
|
|
3071
|
+
const { notificationConfig } = await import("./state-BIlxNRUn.mjs").then((n) => n.D);
|
|
3019
3072
|
process.stderr.write(`[pai-daemon] Notification mode: ${notificationConfig.mode}\n`);
|
|
3020
3073
|
try {
|
|
3021
3074
|
setPriority(process.pid, 10);
|
|
@@ -3083,4 +3136,4 @@ var daemon_exports = /* @__PURE__ */ __exportAll({ serve: () => serve });
|
|
|
3083
3136
|
|
|
3084
3137
|
//#endregion
|
|
3085
3138
|
export { serve as n, daemon_exports as t };
|
|
3086
|
-
//# sourceMappingURL=daemon-
|
|
3139
|
+
//# sourceMappingURL=daemon-kp49BE7u.mjs.map
|