@tekmidian/pai 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/{auto-route-C-DrW6BL.mjs → auto-route-CruBrTf-.mjs} +2 -2
  2. package/dist/{auto-route-C-DrW6BL.mjs.map → auto-route-CruBrTf-.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +345 -23
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/{clusters-JIDQW65f.mjs → clusters-CRlPBpq8.mjs} +1 -1
  6. package/dist/{clusters-JIDQW65f.mjs.map → clusters-CRlPBpq8.mjs.map} +1 -1
  7. package/dist/daemon/index.mjs +6 -6
  8. package/dist/{daemon-VIFoKc_z.mjs → daemon-kp49BE7u.mjs} +74 -21
  9. package/dist/daemon-kp49BE7u.mjs.map +1 -0
  10. package/dist/{detector-jGBuYQJM.mjs → detector-CNU3zCwP.mjs} +1 -1
  11. package/dist/{detector-jGBuYQJM.mjs.map → detector-CNU3zCwP.mjs.map} +1 -1
  12. package/dist/{factory-e0k1HWuc.mjs → factory-DKDPRhAN.mjs} +3 -3
  13. package/dist/{factory-e0k1HWuc.mjs.map → factory-DKDPRhAN.mjs.map} +1 -1
  14. package/dist/{indexer-backend-jcJFsmB4.mjs → indexer-backend-CIIlrYh6.mjs} +1 -1
  15. package/dist/{indexer-backend-jcJFsmB4.mjs.map → indexer-backend-CIIlrYh6.mjs.map} +1 -1
  16. package/dist/kg-B5ysyRLC.mjs +94 -0
  17. package/dist/kg-B5ysyRLC.mjs.map +1 -0
  18. package/dist/kg-extraction-BlGM40q7.mjs +211 -0
  19. package/dist/kg-extraction-BlGM40q7.mjs.map +1 -0
  20. package/dist/{latent-ideas-bTJo6Omd.mjs → latent-ideas-DvWBRHsy.mjs} +2 -2
  21. package/dist/{latent-ideas-bTJo6Omd.mjs.map → latent-ideas-DvWBRHsy.mjs.map} +1 -1
  22. package/dist/{neighborhood-BYYbEkUJ.mjs → neighborhood-u8ytjmWq.mjs} +1 -1
  23. package/dist/{neighborhood-BYYbEkUJ.mjs.map → neighborhood-u8ytjmWq.mjs.map} +1 -1
  24. package/dist/{note-context-BK24bX8Y.mjs → note-context-CG2_e-0W.mjs} +1 -1
  25. package/dist/{note-context-BK24bX8Y.mjs.map → note-context-CG2_e-0W.mjs.map} +1 -1
  26. package/dist/{postgres-DvEPooLO.mjs → postgres-BGERehmX.mjs} +1 -1
  27. package/dist/{postgres-DvEPooLO.mjs.map → postgres-BGERehmX.mjs.map} +1 -1
  28. package/dist/{query-feedback-Dv43XKHM.mjs → query-feedback-CQSumXDy.mjs} +1 -1
  29. package/dist/{query-feedback-Dv43XKHM.mjs.map → query-feedback-CQSumXDy.mjs.map} +1 -1
  30. package/dist/skills/Reconstruct/SKILL.md +36 -0
  31. package/dist/{sqlite-l-s9xPjY.mjs → sqlite-BJrME_vg.mjs} +1 -1
  32. package/dist/{sqlite-l-s9xPjY.mjs.map → sqlite-BJrME_vg.mjs.map} +1 -1
  33. package/dist/{state-C6_vqz7w.mjs → state-BIlxNRUn.mjs} +1 -1
  34. package/dist/{state-C6_vqz7w.mjs.map → state-BIlxNRUn.mjs.map} +1 -1
  35. package/dist/{themes-BvYF0W8T.mjs → themes-9jxFn3Rf.mjs} +1 -1
  36. package/dist/{themes-BvYF0W8T.mjs.map → themes-9jxFn3Rf.mjs.map} +1 -1
  37. package/dist/{tools-C4SBZHga.mjs → tools-8t7BQrm9.mjs} +13 -104
  38. package/dist/tools-8t7BQrm9.mjs.map +1 -0
  39. package/dist/{trace-CRx9lPuc.mjs → trace-C2XrzssW.mjs} +1 -1
  40. package/dist/{trace-CRx9lPuc.mjs.map → trace-C2XrzssW.mjs.map} +1 -1
  41. package/dist/{vault-indexer-B-aJpRZC.mjs → vault-indexer-TTCl1QOL.mjs} +1 -1
  42. package/dist/{vault-indexer-B-aJpRZC.mjs.map → vault-indexer-TTCl1QOL.mjs.map} +1 -1
  43. package/dist/{zettelkasten-DhBKZQHF.mjs → zettelkasten-BdaMzTGQ.mjs} +3 -3
  44. package/dist/{zettelkasten-DhBKZQHF.mjs.map → zettelkasten-BdaMzTGQ.mjs.map} +1 -1
  45. package/package.json +1 -1
  46. package/dist/daemon-VIFoKc_z.mjs.map +0 -1
  47. package/dist/indexer-D53l5d1U.mjs +0 -1
  48. package/dist/tools-C4SBZHga.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools-8t7BQrm9.mjs","names":[],"sources":["../src/mcp/tools/types.ts","../src/mcp/tools/memory.ts","../src/mcp/tools/projects.ts","../src/mcp/tools/sessions.ts","../src/mcp/tools/registry.ts","../src/mcp/tools/zettel.ts","../src/mcp/tools/kg.ts","../src/memory/wakeup.ts","../src/mcp/tools/wakeup.ts","../src/memory/taxonomy.ts","../src/mcp/tools/taxonomy.ts","../src/memory/tunnels.ts","../src/mcp/tools/tunnels.ts","../src/mcp/tools.ts"],"sourcesContent":["/**\n * Shared types and project-row helpers used across all MCP tool handler modules.\n */\n\nimport { resolve } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\n\nexport interface ToolContent {\n type: \"text\";\n text: string;\n}\n\nexport interface ToolResult {\n content: ToolContent[];\n isError?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Shared row type — mirrors the projects SQLite schema\n// ---------------------------------------------------------------------------\n\nexport interface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n created_at: number;\n updated_at: number;\n archived_at?: number | null;\n parent_id?: number | null;\n obsidian_link?: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: lookup project_id by slug (also checks aliases)\n// ---------------------------------------------------------------------------\n\nexport function lookupProjectId(\n registryDb: Database,\n slug: string\n): number | null {\n const bySlug = registryDb\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(slug) as { id: number } | undefined;\n if (bySlug) return bySlug.id;\n\n const byAlias = registryDb\n .prepare(\"SELECT project_id FROM aliases WHERE alias = ?\")\n .get(slug) as { project_id: number } | undefined;\n if (byAlias) return byAlias.project_id;\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: detect project from a filesystem path\n// ---------------------------------------------------------------------------\n\nexport function detectProjectFromPath(\n registryDb: Database,\n fsPath: string\n): ProjectRow | null {\n const resolved = resolve(fsPath);\n\n const exact = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects WHERE root_path = ?\"\n )\n .get(resolved) as ProjectRow | undefined;\n\n if (exact) return exact;\n\n const all = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects ORDER BY LENGTH(root_path) DESC\"\n )\n .all() as ProjectRow[];\n\n for (const project of all) {\n if (\n resolved.startsWith(project.root_path + \"/\") ||\n resolved === project.root_path\n ) {\n return project;\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: format project row for tool output\n// ---------------------------------------------------------------------------\n\nexport function formatProject(registryDb: Database, project: ProjectRow): string {\n const sessionCount = (\n registryDb\n .prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\")\n .get(project.id) as { n: number }\n ).n;\n\n const lastSession = registryDb\n .prepare(\n \"SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1\"\n )\n .get(project.id) as { date: string } | undefined;\n\n const tags = (\n registryDb\n .prepare(\n `SELECT t.name FROM tags t\n JOIN project_tags pt ON pt.tag_id = t.id\n WHERE pt.project_id = ?\n ORDER BY t.name`\n )\n .all(project.id) as Array<{ name: string }>\n ).map((r) => r.name);\n\n const aliases = (\n registryDb\n .prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\")\n .all(project.id) as Array<{ alias: string }>\n ).map((r) => r.alias);\n\n const lines: string[] = [\n `slug: ${project.slug}`,\n `display_name: ${project.display_name}`,\n `root_path: ${project.root_path}`,\n `type: ${project.type}`,\n `status: ${project.status}`,\n `sessions: ${sessionCount}`,\n ];\n\n if (lastSession) lines.push(`last_session: ${lastSession.date}`);\n if (tags.length) lines.push(`tags: ${tags.join(\", \")}`);\n if (aliases.length) lines.push(`aliases: ${aliases.join(\", \")}`);\n if (project.obsidian_link) lines.push(`obsidian_link: ${project.obsidian_link}`);\n if (project.archived_at) {\n lines.push(\n `archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`\n );\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * MCP tool handlers: memory_search, memory_get\n */\n\nimport { readFileSync, existsSync, statSync } from \"node:fs\";\nimport { join, resolve, isAbsolute } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport { populateSlugs, searchMemoryHybrid } from \"../../memory/search.js\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { SearchConfig } from \"../../daemon/config.js\";\nimport type { SearchResult } from \"../../memory/search.js\";\nimport {\n lookupProjectId,\n type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_search\n// ---------------------------------------------------------------------------\n\nexport interface MemorySearchParams {\n query: string;\n project?: string;\n all_projects?: boolean;\n sources?: Array<\"memory\" | \"notes\">;\n limit?: number;\n mode?: \"keyword\" | \"semantic\" | \"hybrid\";\n /** Rerank results using cross-encoder model for better relevance ordering. */\n rerank?: boolean;\n /** Apply recency boost — score decays by half every N days. 0 = off (default). */\n recencyBoost?: number;\n /** Maximum characters per result snippet. Default 200.\n * Limit context consumption — MCP results go into Claude's context window. */\n snippetLength?: number;\n}\n\nexport async function toolMemorySearch(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: MemorySearchParams,\n searchDefaults?: SearchConfig,\n): Promise<ToolResult> {\n try {\n const projectIds: number[] | undefined = params.project\n ? (() => {\n const id = lookupProjectId(registryDb, params.project!);\n return id != null ? [id] : [];\n })()\n : undefined;\n\n if (params.project && (!projectIds || projectIds.length === 0)) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n // NOTE: No indexAll() here — indexing is handled by the daemon scheduler.\n // The daemon ensures the index stays fresh; the search hot path is read-only.\n\n const mode = params.mode ?? (searchDefaults?.mode ?? \"keyword\");\n // Limit context consumption — MCP results go into Claude's context window.\n // Default to 5 results and 200-char snippets to keep a single search call\n // within ~1-2K tokens rather than 5K+.\n const snippetLength = params.snippetLength ?? (searchDefaults?.snippetLength ?? 200);\n const searchOpts = {\n projectIds,\n sources: params.sources,\n maxResults: params.limit ?? (searchDefaults?.defaultLimit ?? 5),\n };\n\n let results;\n\n // Determine if federation is a StorageBackend or a raw Database\n const isBackend = (x: Database | StorageBackend): x is StorageBackend =>\n \"backendType\" in x;\n\n if (isBackend(federation)) {\n // Use the storage backend interface (works for both SQLite and Postgres)\n if (mode === \"keyword\") {\n results = await federation.searchKeyword(params.query, searchOpts);\n } else if (mode === \"semantic\" || mode === \"hybrid\") {\n const { generateEmbedding } = await import(\"../../memory/embeddings.js\");\n const queryEmbedding = await generateEmbedding(params.query, true);\n\n if (mode === \"semantic\") {\n results = await federation.searchSemantic(queryEmbedding, searchOpts);\n } else {\n // Hybrid: combine keyword + semantic\n const [kwResults, semResults] = await Promise.all([\n federation.searchKeyword(params.query, { ...searchOpts, maxResults: 50 }),\n federation.searchSemantic(queryEmbedding, { ...searchOpts, maxResults: 50 }),\n ]); // 50 candidates is sufficient for min-max normalization\n // Reuse the existing hybrid scoring logic\n results = combineHybridResults(kwResults, semResults, searchOpts.maxResults ?? 10);\n }\n } else {\n results = await federation.searchKeyword(params.query, searchOpts);\n }\n } else {\n // Legacy path: raw better-sqlite3 Database (for direct MCP server usage)\n const { searchMemory, searchMemorySemantic } = await import(\"../../memory/search.js\");\n\n if (mode === \"keyword\") {\n results = searchMemory(federation, params.query, searchOpts);\n } else if (mode === \"semantic\" || mode === \"hybrid\") {\n const { generateEmbedding } = await import(\"../../memory/embeddings.js\");\n const queryEmbedding = await generateEmbedding(params.query, true);\n\n if (mode === \"semantic\") {\n results = searchMemorySemantic(federation, queryEmbedding, searchOpts);\n } else {\n results = searchMemoryHybrid(\n federation,\n params.query,\n queryEmbedding,\n searchOpts\n );\n }\n } else {\n results = searchMemory(federation, params.query, searchOpts);\n }\n }\n\n // Cross-encoder reranking (on by default)\n const shouldRerank = params.rerank ?? (searchDefaults?.rerank ?? true);\n if (shouldRerank && results.length > 0) {\n const { rerankResults } = await import(\"../../memory/reranker.js\");\n results = await rerankResults(params.query, results, {\n topK: searchOpts.maxResults ?? 5,\n });\n }\n\n // Recency boost (off by default, applied after reranking)\n const recencyDays = params.recencyBoost ?? (searchDefaults?.recencyBoostDays ?? 0);\n if (recencyDays > 0 && results.length > 0) {\n const { applyRecencyBoost } = await import(\"../../memory/search.js\");\n results = applyRecencyBoost(results, recencyDays);\n }\n\n const withSlugs = populateSlugs(results, registryDb);\n\n if (withSlugs.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No results found for query: \"${params.query}\" (mode: ${mode})`,\n },\n ],\n };\n }\n\n const rerankLabel = shouldRerank ? \" +rerank\" : \"\";\n const formatted = withSlugs\n .map((r, i) => {\n const header = `[${i + 1}] ${r.projectSlug ?? `project:${r.projectId}`} — ${r.path} (lines ${r.startLine}-${r.endLine}) score=${r.score.toFixed(4)} tier=${r.tier} source=${r.source}`;\n // Truncate snippet to snippetLength — limit context consumption.\n // MCP results go into Claude's context window; keep each result tight.\n const raw = r.snippet.trim();\n const snippet = raw.length > snippetLength\n ? raw.slice(0, snippetLength) + \"...\"\n : raw;\n return `${header}\\n${snippet}`;\n })\n .join(\"\\n\\n---\\n\\n\");\n\n // Query feedback loop: save query + result metadata for future indexing\n try {\n const { saveQueryResult } = await import(\"../../zettelkasten/query-feedback.js\");\n saveQueryResult({\n query: params.query,\n timestamp: Date.now(),\n source: \"memory_search\",\n sourceSlugs: withSlugs.slice(0, 5).map((r) => r.path),\n answerPreview: withSlugs.slice(0, 3).map((r) => r.snippet.trim().slice(0, 150)).join(\" | \"),\n resultCount: withSlugs.length,\n });\n } catch {\n // Non-critical — don't fail the search if feedback logging errors\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `Found ${withSlugs.length} result(s) for \"${params.query}\" (mode: ${mode}${rerankLabel}):\\n\\n${formatted}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Search error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: memory_get\n// ---------------------------------------------------------------------------\n\nexport interface MemoryGetParams {\n project: string;\n path: string;\n from?: number;\n lines?: number;\n}\n\nexport function toolMemoryGet(\n registryDb: Database,\n params: MemoryGetParams\n): ToolResult {\n try {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const project = registryDb\n .prepare(\"SELECT root_path FROM projects WHERE id = ?\")\n .get(projectId) as { root_path: string } | undefined;\n\n if (!project) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const requestedPath = params.path;\n if (requestedPath.includes(\"..\") || isAbsolute(requestedPath)) {\n return {\n content: [\n {\n type: \"text\",\n text: `Invalid path: ${params.path} (must be a relative path within the project root, no ../ allowed)`,\n },\n ],\n isError: true,\n };\n }\n\n const fullPath = join(project.root_path, requestedPath);\n const resolvedFull = resolve(fullPath);\n const resolvedRoot = resolve(project.root_path);\n\n if (\n !resolvedFull.startsWith(resolvedRoot + \"/\") &&\n resolvedFull !== resolvedRoot\n ) {\n return {\n content: [\n { type: \"text\", text: `Path traversal blocked: ${params.path}` },\n ],\n isError: true,\n };\n }\n\n if (!existsSync(fullPath)) {\n return {\n content: [\n {\n type: \"text\",\n text: `File not found: ${requestedPath} (project: ${params.project})`,\n },\n ],\n isError: true,\n };\n }\n\n const stat = statSync(fullPath);\n if (stat.size > 5 * 1024 * 1024) {\n return {\n content: [\n {\n type: \"text\",\n text: `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum 5 MB.`,\n },\n ],\n };\n }\n\n const content = readFileSync(fullPath, \"utf8\");\n const allLines = content.split(\"\\n\");\n\n const fromLine = (params.from ?? 1) - 1;\n const toLine =\n params.lines != null\n ? Math.min(fromLine + params.lines, allLines.length)\n : allLines.length;\n\n const selectedLines = allLines.slice(fromLine, toLine);\n const text = selectedLines.join(\"\\n\");\n\n const header =\n params.from != null\n ? `${params.project}/${requestedPath} (lines ${fromLine + 1}-${toLine}):`\n : `${params.project}/${requestedPath}:`;\n\n return {\n content: [{ type: \"text\", text: `${header}\\n\\n${text}` }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Read error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search helper (backend-agnostic)\n// ---------------------------------------------------------------------------\n\n/**\n * Combine keyword + semantic results using min-max normalized scoring.\n * Mirrors the logic in searchMemoryHybrid() from memory/search.ts,\n * but works on pre-computed result arrays so it works for any backend.\n */\nexport function combineHybridResults(\n keywordResults: SearchResult[],\n semanticResults: SearchResult[],\n maxResults: number,\n keywordWeight = 0.5,\n semanticWeight = 0.5\n): SearchResult[] {\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n const combined: Array<SearchResult & { combinedScore: number }> = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const kwScore = kwNorm.get(key) ?? 0;\n const semScore = semNorm.get(key) ?? 0;\n const combinedScore = keywordWeight * kwScore + semanticWeight * semScore;\n combined.push({ ...meta, score: combinedScore, combinedScore });\n }\n\n return combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults)\n .map(({ combinedScore: _unused, ...r }) => r);\n}\n","/**\n * MCP tool handlers: project_info, project_list, project_detect,\n * project_health, project_todo\n */\n\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport { detectProject, formatDetectionJson } from \"../../cli/commands/detect.js\";\nimport {\n lookupProjectId,\n detectProjectFromPath,\n formatProject,\n type ProjectRow,\n type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: project_info\n// ---------------------------------------------------------------------------\n\nexport interface ProjectInfoParams {\n slug?: string;\n}\n\nexport function toolProjectInfo(\n registryDb: Database,\n params: ProjectInfoParams\n): ToolResult {\n try {\n let project: ProjectRow | null = null;\n\n if (params.slug) {\n const projectId = lookupProjectId(registryDb, params.slug);\n if (projectId != null) {\n project = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at, archived_at, parent_id, obsidian_link FROM projects WHERE id = ?\"\n )\n .get(projectId) as ProjectRow | null;\n }\n } else {\n const cwd = process.cwd();\n project = detectProjectFromPath(registryDb, cwd);\n }\n\n if (!project) {\n const message = params.slug\n ? `Project not found: ${params.slug}`\n : `No PAI project found matching the current directory: ${process.cwd()}`;\n return {\n content: [{ type: \"text\", text: message }],\n isError: !params.slug,\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatProject(registryDb, project) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_info error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_list\n// ---------------------------------------------------------------------------\n\nexport interface ProjectListParams {\n status?: \"active\" | \"archived\" | \"migrating\";\n tag?: string;\n limit?: number;\n}\n\nexport function toolProjectList(\n registryDb: Database,\n params: ProjectListParams\n): ToolResult {\n try {\n const conditions: string[] = [];\n const queryParams: (string | number)[] = [];\n\n if (params.status) {\n conditions.push(\"p.status = ?\");\n queryParams.push(params.status);\n }\n\n if (params.tag) {\n conditions.push(\n \"p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)\"\n );\n queryParams.push(params.tag);\n }\n\n const where =\n conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n const limit = params.limit ?? 50;\n queryParams.push(limit);\n\n const projects = registryDb\n .prepare(\n `SELECT p.id, p.slug, p.display_name, p.root_path, p.type, p.status, p.updated_at\n FROM projects p\n ${where}\n ORDER BY p.updated_at DESC\n LIMIT ?`\n )\n .all(...queryParams) as Array<{\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n updated_at: number;\n }>;\n\n if (projects.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: \"No projects found matching the given filters.\",\n },\n ],\n };\n }\n\n const lines = projects.map(\n (p) =>\n `${p.slug} [${p.status}] ${p.root_path} (updated: ${new Date(p.updated_at).toISOString().slice(0, 10)})`\n );\n\n return {\n content: [\n {\n type: \"text\",\n text: `${projects.length} project(s):\\n\\n${lines.join(\"\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_list error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_detect\n// ---------------------------------------------------------------------------\n\nexport interface ProjectDetectParams {\n cwd?: string;\n}\n\nexport function toolProjectDetect(\n registryDb: Database,\n params: ProjectDetectParams\n): ToolResult {\n try {\n const detection = detectProject(registryDb, params.cwd);\n\n if (!detection) {\n const target = params.cwd ?? process.cwd();\n return {\n content: [\n {\n type: \"text\",\n text: `No registered project found for path: ${target}\\n\\nRun 'pai project add .' to register this directory.`,\n },\n ],\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatDetectionJson(detection) }],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `project_detect error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_health\n// ---------------------------------------------------------------------------\n\nexport interface ProjectHealthParams {\n category?: \"active\" | \"stale\" | \"dead\" | \"all\";\n}\n\nexport async function toolProjectHealth(\n registryDb: Database,\n params: ProjectHealthParams\n): Promise<ToolResult> {\n try {\n const { existsSync: fsExists, readdirSync, statSync } = await import(\n \"node:fs\"\n );\n const {\n join: pathJoin,\n basename: pathBasename,\n } = await import(\"node:path\");\n const { homedir } = await import(\"node:os\");\n const { encodeDir: enc } = await import(\"../../cli/utils.js\");\n\n interface HealthRowLocal {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n status: string;\n type: string;\n session_count: number;\n }\n\n const rows = registryDb\n .prepare(\n `SELECT p.id, p.slug, p.display_name, p.root_path, p.encoded_dir, p.status, p.type,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count\n FROM projects p\n ORDER BY p.slug ASC`\n )\n .all() as HealthRowLocal[];\n\n const home = homedir();\n const claudeProjects = pathJoin(home, \".claude\", \"projects\");\n\n function suggestMoved(rootPath: string): string | undefined {\n const name = pathBasename(rootPath);\n const candidates = [\n pathJoin(home, \"dev\", name),\n pathJoin(home, \"dev\", \"ai\", name),\n pathJoin(home, \"Desktop\", name),\n pathJoin(home, \"Projects\", name),\n ];\n return candidates.find((c) => fsExists(c));\n }\n\n function hasClaudeNotes(encodedDir: string): boolean {\n if (!fsExists(claudeProjects)) return false;\n try {\n for (const entry of readdirSync(claudeProjects)) {\n if (entry !== encodedDir && !entry.startsWith(encodedDir)) continue;\n const full = pathJoin(claudeProjects, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n if (fsExists(pathJoin(full, \"Notes\"))) return true;\n }\n } catch {\n /* ignore */\n }\n return false;\n }\n\n interface HealthResult {\n slug: string;\n display_name: string;\n root_path: string;\n status: string;\n type: string;\n session_count: number;\n health: string;\n suggested_path: string | null;\n has_claude_notes: boolean;\n todo: {\n found: boolean;\n path: string | null;\n has_continue: boolean;\n };\n }\n\n function findTodoForProject(rootPath: string): {\n found: boolean;\n path: string | null;\n has_continue: boolean;\n } {\n const locs = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n ];\n for (const rel of locs) {\n const full = pathJoin(rootPath, rel);\n if (fsExists(full)) {\n try {\n const raw = readFileSync(full, \"utf8\");\n const hasContinue = /^## Continue$/m.test(raw);\n return { found: true, path: rel, has_continue: hasContinue };\n } catch {\n return { found: true, path: rel, has_continue: false };\n }\n }\n }\n return { found: false, path: null, has_continue: false };\n }\n\n const results: HealthResult[] = rows.map((p) => {\n const pathExists = fsExists(p.root_path);\n let health: string;\n let suggestedPath: string | null = null;\n\n if (pathExists) {\n health = \"active\";\n } else {\n suggestedPath = suggestMoved(p.root_path) ?? null;\n health = suggestedPath ? \"stale\" : \"dead\";\n }\n\n const todo = pathExists\n ? findTodoForProject(p.root_path)\n : { found: false, path: null, has_continue: false };\n\n return {\n slug: p.slug,\n display_name: p.display_name,\n root_path: p.root_path,\n status: p.status,\n type: p.type,\n session_count: p.session_count,\n health,\n suggested_path: suggestedPath,\n has_claude_notes: hasClaudeNotes(p.encoded_dir),\n todo,\n };\n });\n\n const filtered =\n !params.category || params.category === \"all\"\n ? results\n : results.filter((r) => r.health === params.category);\n\n const summary = {\n total: rows.length,\n active: results.filter((r) => r.health === \"active\").length,\n stale: results.filter((r) => r.health === \"stale\").length,\n dead: results.filter((r) => r.health === \"dead\").length,\n };\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify({ summary, projects: filtered }, null, 2),\n },\n ],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `project_health error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_todo\n// ---------------------------------------------------------------------------\n\nexport interface ProjectTodoParams {\n project?: string;\n}\n\n/**\n * TODO candidate locations searched in priority order.\n * Returns the first one that exists, along with its label.\n */\nconst TODO_LOCATIONS = [\n { rel: \"Notes/TODO.md\", label: \"Notes/TODO.md\" },\n { rel: \".claude/Notes/TODO.md\", label: \".claude/Notes/TODO.md\" },\n { rel: \"tasks/todo.md\", label: \"tasks/todo.md\" },\n { rel: \"TODO.md\", label: \"TODO.md\" },\n];\n\n/**\n * Given TODO file content, extract and surface the ## Continue section first,\n * then return the remaining content. Returns an object with:\n * continueSection: string | null\n * fullContent: string\n * hasContinue: boolean\n */\nfunction parseTodoContent(raw: string): {\n continueSection: string | null;\n fullContent: string;\n hasContinue: boolean;\n} {\n const lines = raw.split(\"\\n\");\n\n // Find the ## Continue heading\n const continueIdx = lines.findIndex(\n (l) => l.trim() === \"## Continue\"\n );\n\n if (continueIdx === -1) {\n return { continueSection: null, fullContent: raw, hasContinue: false };\n }\n\n // The section ends at the first `---` separator or next `##` heading after\n // the Continue heading (whichever comes first).\n let endIdx = lines.length;\n for (let i = continueIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (trimmed === \"---\" || (trimmed.startsWith(\"##\") && trimmed !== \"## Continue\")) {\n endIdx = i;\n break;\n }\n }\n\n const continueLines = lines.slice(continueIdx, endIdx);\n const continueSection = continueLines.join(\"\\n\").trim();\n\n return { continueSection, fullContent: raw, hasContinue: true };\n}\n\nexport function toolProjectTodo(\n registryDb: Database,\n params: ProjectTodoParams\n): ToolResult {\n try {\n let rootPath: string;\n let projectSlug: string;\n\n if (params.project) {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const row = registryDb\n .prepare(\"SELECT root_path, slug FROM projects WHERE id = ?\")\n .get(projectId) as { root_path: string; slug: string } | undefined;\n\n if (!row) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n rootPath = row.root_path;\n projectSlug = row.slug;\n } else {\n // Auto-detect from cwd\n const project = detectProjectFromPath(registryDb, process.cwd());\n if (!project) {\n return {\n content: [\n {\n type: \"text\",\n text: `No PAI project found matching the current directory: ${process.cwd()}\\n\\nProvide a project slug or run 'pai project add .' to register this directory.`,\n },\n ],\n };\n }\n rootPath = project.root_path;\n projectSlug = project.slug;\n }\n\n // Search for TODO in priority order\n for (const loc of TODO_LOCATIONS) {\n const fullPath = join(rootPath, loc.rel);\n if (existsSync(fullPath)) {\n const raw = readFileSync(fullPath, \"utf8\");\n const { continueSection, fullContent, hasContinue } = parseTodoContent(raw);\n\n let output: string;\n if (hasContinue && continueSection) {\n // Surface the ## Continue section first, then the full content\n output = [\n `TODO found: ${projectSlug}/${loc.label}`,\n \"\",\n \"=== CONTINUE SECTION (surfaced first) ===\",\n continueSection,\n \"\",\n \"=== FULL TODO CONTENT ===\",\n fullContent,\n ].join(\"\\n\");\n } else {\n output = [\n `TODO found: ${projectSlug}/${loc.label}`,\n \"\",\n fullContent,\n ].join(\"\\n\");\n }\n\n return {\n content: [{ type: \"text\", text: output }],\n };\n }\n }\n\n // No TODO found in any location\n const searched = TODO_LOCATIONS.map((l) => ` ${rootPath}/${l.rel}`).join(\"\\n\");\n return {\n content: [\n {\n type: \"text\",\n text: [\n `No TODO.md found for project: ${projectSlug}`,\n \"\",\n \"Searched locations (in order):\",\n searched,\n \"\",\n \"Create a TODO with: echo '## Tasks\\\\n- [ ] First task' > Notes/TODO.md\",\n ].join(\"\\n\"),\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_todo error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handlers: session_list, session_route\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport {\n lookupProjectId,\n type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: session_list\n// ---------------------------------------------------------------------------\n\nexport interface SessionListParams {\n project: string;\n limit?: number;\n status?: \"open\" | \"completed\" | \"compacted\";\n}\n\nexport function toolSessionList(\n registryDb: Database,\n params: SessionListParams\n): ToolResult {\n try {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const conditions = [\"project_id = ?\"];\n const queryParams: (string | number)[] = [projectId];\n\n if (params.status) {\n conditions.push(\"status = ?\");\n queryParams.push(params.status);\n }\n\n const limit = params.limit ?? 10;\n queryParams.push(limit);\n\n const sessions = registryDb\n .prepare(\n `SELECT number, date, title, filename, status\n FROM sessions\n WHERE ${conditions.join(\" AND \")}\n ORDER BY number DESC\n LIMIT ?`\n )\n .all(...queryParams) as Array<{\n number: number;\n date: string;\n title: string;\n filename: string;\n status: string;\n }>;\n\n if (sessions.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No sessions found for project: ${params.project}`,\n },\n ],\n };\n }\n\n const lines = sessions.map(\n (s) =>\n `#${String(s.number).padStart(4, \"0\")} ${s.date} [${s.status}] ${s.title}\\n file: Notes/${s.filename}`\n );\n\n return {\n content: [\n {\n type: \"text\",\n text: `${sessions.length} session(s) for ${params.project}:\\n\\n${lines.join(\"\\n\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `session_list error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: session_route\n// ---------------------------------------------------------------------------\n\nexport interface SessionRouteParams {\n /** Working directory to route from (defaults to process.cwd()) */\n cwd?: string;\n /** Optional conversation context for topic-based fallback routing */\n context?: string;\n}\n\n/**\n * Automatically suggest which project a session belongs to.\n *\n * Strategy (in priority order):\n * 1. path — exact or parent-directory match in the project registry\n * 2. marker — walk up from cwd looking for Notes/PAI.md\n * 3. topic — BM25 keyword search against memory (requires context)\n *\n * Call this at session start (e.g., from CLAUDE.md or a session-start hook)\n * to automatically route the session to the correct project.\n */\nexport async function toolSessionRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: SessionRouteParams\n): Promise<ToolResult> {\n try {\n const { autoRoute, formatAutoRouteJson } = await import(\"../../session/auto-route.js\");\n\n const result = await autoRoute(\n registryDb,\n federation,\n params.cwd,\n params.context\n );\n\n if (!result) {\n const target = params.cwd ?? process.cwd();\n return {\n content: [\n {\n type: \"text\",\n text: [\n `No project match found for: ${target}`,\n \"\",\n \"Tried: path match, PAI.md marker walk\" +\n (params.context ? \", topic detection\" : \"\"),\n \"\",\n \"Run 'pai project add .' to register this directory,\",\n \"or provide conversation context for topic-based routing.\",\n ].join(\"\\n\"),\n },\n ],\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatAutoRouteJson(result) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `session_route error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handler: registry_search\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: registry_search\n// ---------------------------------------------------------------------------\n\nexport interface RegistrySearchParams {\n query: string;\n}\n\nexport function toolRegistrySearch(\n registryDb: Database,\n params: RegistrySearchParams\n): ToolResult {\n try {\n const q = `%${params.query}%`;\n const projects = registryDb\n .prepare(\n `SELECT id, slug, display_name, root_path, type, status, updated_at\n FROM projects\n WHERE slug LIKE ?\n OR display_name LIKE ?\n OR root_path LIKE ?\n ORDER BY updated_at DESC\n LIMIT 20`\n )\n .all(q, q, q) as Array<{\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n updated_at: number;\n }>;\n\n if (projects.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No projects found matching: \"${params.query}\"`,\n },\n ],\n };\n }\n\n const lines = projects.map((p) => `${p.slug} [${p.status}] ${p.root_path}`);\n\n return {\n content: [\n {\n type: \"text\",\n text: `${projects.length} match(es) for \"${params.query}\":\\n\\n${lines.join(\"\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `registry_search error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handlers: zettel_explore, zettel_health, zettel_surprise,\n * zettel_suggest, zettel_converse, zettel_themes,\n * zettel_god_notes, zettel_communities\n */\n\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_explore\n// ---------------------------------------------------------------------------\n\nexport interface ZettelExploreParams {\n start_note: string;\n depth?: number;\n direction?: string;\n mode?: string;\n}\n\nexport async function toolZettelExplore(\n backend: StorageBackend,\n params: ZettelExploreParams\n): Promise<ToolResult> {\n try {\n const { zettelExplore } = await import(\"../../zettelkasten/index.js\");\n const result = await zettelExplore(backend, {\n startNote: params.start_note,\n depth: params.depth,\n direction: params.direction as \"forward\" | \"backward\" | \"both\" | undefined,\n mode: params.mode as \"sequential\" | \"associative\" | \"all\" | undefined,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_explore error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_health\n// ---------------------------------------------------------------------------\n\nexport interface ZettelHealthParams {\n scope?: string;\n project_path?: string;\n recent_days?: number;\n include?: string[];\n}\n\nexport async function toolZettelHealth(\n backend: StorageBackend,\n params: ZettelHealthParams\n): Promise<ToolResult> {\n try {\n const { zettelHealth } = await import(\"../../zettelkasten/index.js\");\n const result = await zettelHealth(backend, {\n scope: params.scope as \"full\" | \"recent\" | \"project\" | undefined,\n projectPath: params.project_path,\n recentDays: params.recent_days,\n include: params.include as Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\"> | undefined,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_health error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_surprise\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSurpriseParams {\n reference_path: string;\n vault_project_id: number;\n limit?: number;\n min_similarity?: number;\n min_graph_distance?: number;\n}\n\nexport async function toolZettelSurprise(\n backend: StorageBackend,\n params: ZettelSurpriseParams\n): Promise<ToolResult> {\n try {\n const { zettelSurprise } = await import(\"../../zettelkasten/index.js\");\n const results = await zettelSurprise(backend, {\n referencePath: params.reference_path,\n vaultProjectId: params.vault_project_id,\n limit: params.limit,\n minSimilarity: params.min_similarity,\n minGraphDistance: params.min_graph_distance,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_surprise error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_suggest\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSuggestParams {\n note_path: string;\n vault_project_id: number;\n limit?: number;\n exclude_linked?: boolean;\n}\n\nexport async function toolZettelSuggest(\n backend: StorageBackend,\n params: ZettelSuggestParams\n): Promise<ToolResult> {\n try {\n const { zettelSuggest } = await import(\"../../zettelkasten/index.js\");\n const results = await zettelSuggest(backend, {\n notePath: params.note_path,\n vaultProjectId: params.vault_project_id,\n limit: params.limit,\n excludeLinked: params.exclude_linked,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_suggest error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_converse\n// ---------------------------------------------------------------------------\n\nexport interface ZettelConverseParams {\n question: string;\n vault_project_id: number;\n depth?: number;\n limit?: number;\n}\n\nexport async function toolZettelConverse(\n backend: StorageBackend,\n params: ZettelConverseParams\n): Promise<ToolResult> {\n try {\n const { zettelConverse } = await import(\"../../zettelkasten/index.js\");\n const result = await zettelConverse(backend, {\n question: params.question,\n vaultProjectId: params.vault_project_id,\n depth: params.depth,\n limit: params.limit,\n });\n\n // Query feedback loop: save query + result metadata for future indexing\n try {\n const { saveQueryResult } = await import(\"../../zettelkasten/query-feedback.js\");\n saveQueryResult({\n query: params.question,\n timestamp: Date.now(),\n source: \"zettel_converse\",\n sourceSlugs: result.relevantNotes.slice(0, 5).map((n) => n.path),\n answerPreview: result.relevantNotes.slice(0, 3).map((n) => {\n const title = n.title ?? \"(untitled)\";\n return `${title}: ${n.snippet.trim().slice(0, 100)}`;\n }).join(\" | \"),\n resultCount: result.relevantNotes.length,\n });\n } catch {\n // Non-critical\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_converse error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_themes\n// ---------------------------------------------------------------------------\n\nexport interface ZettelThemesParams {\n vault_project_id: number;\n lookback_days?: number;\n min_cluster_size?: number;\n max_themes?: number;\n similarity_threshold?: number;\n}\n\nexport async function toolZettelThemes(\n backend: StorageBackend,\n params: ZettelThemesParams\n): Promise<ToolResult> {\n try {\n const { zettelThemes } = await import(\"../../zettelkasten/index.js\");\n const result = await zettelThemes(backend, {\n vaultProjectId: params.vault_project_id,\n lookbackDays: params.lookback_days,\n minClusterSize: params.min_cluster_size,\n maxThemes: params.max_themes,\n similarityThreshold: params.similarity_threshold,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_themes error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_god_notes\n// ---------------------------------------------------------------------------\n\nexport interface ZettelGodNotesParams {\n limit?: number;\n min_inbound?: number;\n}\n\nexport async function toolZettelGodNotes(\n backend: StorageBackend,\n params: ZettelGodNotesParams\n): Promise<ToolResult> {\n try {\n const { zettelGodNotes } = await import(\"../../zettelkasten/index.js\");\n const result = await zettelGodNotes(backend, {\n limit: params.limit,\n minInbound: params.min_inbound,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_god_notes error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_communities\n// ---------------------------------------------------------------------------\n\nexport interface ZettelCommunitiesParams {\n min_size?: number;\n max_communities?: number;\n resolution?: number;\n}\n\nexport async function toolZettelCommunities(\n backend: StorageBackend,\n params: ZettelCommunitiesParams\n): Promise<ToolResult> {\n try {\n const { zettelCommunities } = await import(\"../../zettelkasten/index.js\");\n const result = await zettelCommunities(backend, {\n minSize: params.min_size,\n maxCommunities: params.max_communities,\n resolution: params.resolution,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_communities error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handlers for the temporal knowledge graph.\n *\n * Tools:\n * kg_add — insert a new (subject, predicate, object) triple\n * kg_query — query triples with optional temporal filter\n * kg_invalidate — mark a triple as no longer valid (sets valid_to)\n * kg_contradictions — find (subject, predicate) pairs with multiple valid objects\n */\n\nimport type { Pool } from \"pg\";\nimport type { ToolResult } from \"./types.js\";\nimport {\n kgAdd,\n kgQuery,\n kgInvalidate,\n kgContradictions,\n} from \"../../memory/kg.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: kg_add\n// ---------------------------------------------------------------------------\n\nexport interface KgAddToolParams {\n subject: string;\n predicate: string;\n object: string;\n project_id?: number;\n source_session?: string;\n confidence?: \"EXTRACTED\" | \"INFERRED\" | \"AMBIGUOUS\";\n}\n\nexport async function toolKgAdd(\n pool: Pool,\n params: KgAddToolParams\n): Promise<ToolResult> {\n try {\n if (!params.subject || !params.predicate || !params.object) {\n return {\n content: [{ type: \"text\", text: \"kg_add error: subject, predicate, and object are required\" }],\n isError: true,\n };\n }\n const triple = await kgAdd(pool, params);\n return {\n content: [{ type: \"text\", text: JSON.stringify(triple, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `kg_add error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_query\n// ---------------------------------------------------------------------------\n\nexport interface KgQueryToolParams {\n subject?: string;\n predicate?: string;\n object?: string;\n project_id?: number;\n as_of?: string; // ISO 8601 string; converted to Date\n include_invalidated?: boolean;\n}\n\nexport async function toolKgQuery(\n pool: Pool,\n params: KgQueryToolParams\n): Promise<ToolResult> {\n try {\n const asOf = params.as_of ? new Date(params.as_of) : undefined;\n if (asOf && isNaN(asOf.getTime())) {\n return {\n content: [{ type: \"text\", text: `kg_query error: invalid as_of date: ${params.as_of}` }],\n isError: true,\n };\n }\n const triples = await kgQuery(pool, {\n subject: params.subject,\n predicate: params.predicate,\n object: params.object,\n project_id: params.project_id,\n as_of: asOf,\n include_invalidated: params.include_invalidated,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(triples, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `kg_query error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_invalidate\n// ---------------------------------------------------------------------------\n\nexport interface KgInvalidateToolParams {\n triple_id: number;\n}\n\nexport async function toolKgInvalidate(\n pool: Pool,\n params: KgInvalidateToolParams\n): Promise<ToolResult> {\n try {\n if (params.triple_id === undefined || params.triple_id === null) {\n return {\n content: [{ type: \"text\", text: \"kg_invalidate error: triple_id is required\" }],\n isError: true,\n };\n }\n await kgInvalidate(pool, params.triple_id);\n return {\n content: [{ type: \"text\", text: JSON.stringify({ invalidated: true, triple_id: params.triple_id }) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `kg_invalidate error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_contradictions\n// ---------------------------------------------------------------------------\n\nexport interface KgContradictionsToolParams {\n subject: string;\n}\n\nexport async function toolKgContradictions(\n pool: Pool,\n params: KgContradictionsToolParams\n): Promise<ToolResult> {\n try {\n if (!params.subject) {\n return {\n content: [{ type: \"text\", text: \"kg_contradictions error: subject is required\" }],\n isError: true,\n };\n }\n const contradictions = await kgContradictions(pool, params.subject);\n return {\n content: [{ type: \"text\", text: JSON.stringify(contradictions, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `kg_contradictions error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * Wake-up context system — progressive context loading inspired by mempalace.\n *\n * Layers:\n * L0 Identity (~100 tokens) — user identity from ~/.pai/identity.txt. Always loaded.\n * L1 Essential Story (~500-800t) — top session notes for the project, key lines extracted.\n * L2 On-Demand — triggered by topic queries (handled by memory_search).\n * L3 Deep Search — unlimited federated memory search (memory_search tool).\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Maximum tokens for the L1 essential story block. Approx 4 chars/token. */\nconst L1_TOKEN_BUDGET = 800;\nconst L1_CHAR_BUDGET = L1_TOKEN_BUDGET * 4; // ~3200 chars\n\n/** Maximum session notes to scan when building L1. */\nconst L1_MAX_NOTES = 10;\n\n/** Sections to extract from session notes (in priority order). */\nconst EXTRACT_SECTIONS = [\n \"Work Done\",\n \"Key Decisions\",\n \"Next Steps\",\n \"Checkpoint\",\n];\n\n/** Identity file location. */\nconst IDENTITY_FILE = join(homedir(), \".pai\", \"identity.txt\");\n\n// ---------------------------------------------------------------------------\n// L0: Identity\n// ---------------------------------------------------------------------------\n\n/**\n * Load L0 identity from ~/.pai/identity.txt.\n * Returns the file content, or an empty string if the file does not exist.\n * Never throws.\n */\nexport function loadL0Identity(): string {\n if (!existsSync(IDENTITY_FILE)) return \"\";\n try {\n return readFileSync(IDENTITY_FILE, \"utf-8\").trim();\n } catch {\n return \"\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// L1: Essential Story\n// ---------------------------------------------------------------------------\n\n/**\n * Find the Notes directory for a project given its root_path from the registry.\n * Checks local Notes/ first, then central ~/.claude/projects/... path.\n */\nfunction findNotesDirForProject(rootPath: string): string | null {\n // Check local Notes directories first\n const localCandidates = [\n join(rootPath, \"Notes\"),\n join(rootPath, \"notes\"),\n join(rootPath, \".claude\", \"Notes\"),\n ];\n for (const p of localCandidates) {\n if (existsSync(p)) return p;\n }\n\n // Fall back to central ~/.claude/projects/{encoded}/Notes\n const encoded = rootPath\n .replace(/\\//g, \"-\")\n .replace(/\\./g, \"-\")\n .replace(/ /g, \"-\");\n const centralNotes = join(\n homedir(),\n \".claude\",\n \"projects\",\n encoded,\n \"Notes\"\n );\n if (existsSync(centralNotes)) return centralNotes;\n\n return null;\n}\n\n/**\n * Recursively find all .md session note files in a Notes directory.\n * Handles both flat layout (Notes/*.md) and month-subdirectory layout\n * (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename\n * (note numbers are monotonically increasing, so lexicographic = newest-last,\n * so we reverse).\n */\nfunction findSessionNotes(notesDir: string): string[] {\n const result: string[] = [];\n\n const scanDir = (dir: string) => {\n if (!existsSync(dir)) return;\n let entries: string[];\n try {\n entries = readdirSync(dir, { withFileTypes: true } as Parameters<typeof readdirSync>[1] as any)\n .map((e: any) => ({ name: e.name, isDir: e.isDirectory() }));\n } catch {\n return;\n }\n\n for (const entry of entries as Array<{ name: string; isDir: boolean }>) {\n const fullPath = join(dir, entry.name);\n if (entry.isDir) {\n // Recurse into YYYY/MM subdirectories\n scanDir(fullPath);\n } else if (entry.name.match(/^\\d{3,4}[\\s_-].*\\.md$/)) {\n result.push(fullPath);\n }\n }\n };\n\n scanDir(notesDir);\n\n // Sort: extract leading number, highest = most recent\n result.sort((a, b) => {\n const numA = parseInt(basename(a).match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n const numB = parseInt(basename(b).match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n return numB - numA; // newest first\n });\n\n return result;\n}\n\n/**\n * Extract the most important lines from a session note.\n * Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.\n * Returns a condensed string under maxChars.\n */\nfunction extractKeyLines(content: string, maxChars: number): string {\n const lines = content.split(\"\\n\");\n const selected: string[] = [];\n let inTargetSection = false;\n let currentSection = \"\";\n let charCount = 0;\n\n // First pass: collect lines from priority sections\n for (const line of lines) {\n // Detect section headers\n const h2Match = line.match(/^## (.+)$/);\n const h3Match = line.match(/^### (.+)$/);\n if (h2Match) {\n currentSection = h2Match[1];\n inTargetSection = EXTRACT_SECTIONS.some((s) =>\n currentSection.toLowerCase().includes(s.toLowerCase())\n );\n continue;\n }\n if (h3Match) {\n // Checkpoints / sub-sections — include heading as label\n if (inTargetSection) {\n const label = `[${h3Match[1]}]`;\n if (charCount + label.length < maxChars) {\n selected.push(label);\n charCount += label.length + 1;\n }\n }\n continue;\n }\n\n if (!inTargetSection) continue;\n\n // Skip blank lines and HTML comments\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"<!--\") || trimmed === \"---\") continue;\n\n // Include checkbox items, bold text, and plain text lines\n if (\n trimmed.startsWith(\"- \") ||\n trimmed.startsWith(\"* \") ||\n trimmed.match(/^\\d+\\./) ||\n trimmed.startsWith(\"**\")\n ) {\n if (charCount + trimmed.length + 1 > maxChars) break;\n selected.push(trimmed);\n charCount += trimmed.length + 1;\n }\n }\n\n return selected.join(\"\\n\");\n}\n\n/**\n * Build the L1 essential story block.\n *\n * Reads the most recent session notes for the project and extracts the key\n * lines (Work Done, Key Decisions, Next Steps) within the token budget.\n *\n * @param rootPath The project root path (from the registry).\n * @param tokenBudget Max tokens to consume. Default 800 (~3200 chars).\n * @returns Formatted L1 block, or empty string if no notes found.\n */\nexport function buildL1EssentialStory(\n rootPath: string,\n tokenBudget = L1_TOKEN_BUDGET\n): string {\n const charBudget = tokenBudget * 4;\n const notesDir = findNotesDirForProject(rootPath);\n if (!notesDir) return \"\";\n\n const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);\n if (noteFiles.length === 0) return \"\";\n\n const sections: string[] = [];\n let remaining = charBudget;\n\n for (const noteFile of noteFiles) {\n if (remaining <= 50) break;\n\n let content: string;\n try {\n content = readFileSync(noteFile, \"utf-8\");\n } catch {\n continue;\n }\n\n // Extract the note date and title from the filename\n const name = basename(noteFile);\n const titleMatch = name.match(/^\\d+ - (\\d{4}-\\d{2}-\\d{2}) - (.+)\\.md$/);\n const dateLabel = titleMatch ? titleMatch[1] : \"\";\n const titleLabel = titleMatch\n ? titleMatch[2]\n : name.replace(/^\\d+ - /, \"\").replace(/\\.md$/, \"\");\n\n // Skip if nothing useful extracted from this note\n const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);\n const extracted = extractKeyLines(content, perNoteChars);\n if (!extracted) continue;\n\n const noteBlock = `[${dateLabel} - ${titleLabel}]\\n${extracted}`;\n sections.push(noteBlock);\n remaining -= noteBlock.length + 1;\n }\n\n if (sections.length === 0) return \"\";\n\n return sections.join(\"\\n\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Combined: buildWakeupContext\n// ---------------------------------------------------------------------------\n\n/**\n * Build the combined wake-up context block (L0 + L1).\n *\n * Returns a formatted string suitable for injection as a system-reminder,\n * or an empty string if both layers are empty.\n *\n * @param rootPath Project root path for L1 note lookup. Optional.\n * @param tokenBudget L1 token budget. Default 800.\n */\nexport function buildWakeupContext(\n rootPath?: string,\n tokenBudget = L1_TOKEN_BUDGET\n): string {\n const identity = loadL0Identity();\n const essentialStory = rootPath\n ? buildL1EssentialStory(rootPath, tokenBudget)\n : \"\";\n\n if (!identity && !essentialStory) return \"\";\n\n const parts: string[] = [];\n\n if (identity) {\n parts.push(`## L0 Identity\\n\\n${identity}`);\n }\n\n if (essentialStory) {\n parts.push(`## L1 Essential Story\\n\\n${essentialStory}`);\n }\n\n return parts.join(\"\\n\\n\");\n}\n","/**\n * MCP tool handler: memory_wakeup\n *\n * Returns the L0+L1 wake-up context block for a project:\n * L0 — user identity from ~/.pai/identity.txt\n * L1 — recent session note highlights (Work Done / Key Decisions / Next Steps)\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { buildWakeupContext } from \"../../memory/wakeup.js\";\nimport { detectProjectFromPath } from \"./types.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Default token budget (constant, overridable per call)\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TOKEN_BUDGET = 800;\n\n// ---------------------------------------------------------------------------\n// Params\n// ---------------------------------------------------------------------------\n\nexport interface MemoryWakeupParams {\n /** Project slug or absolute path. Omit to auto-detect from process.cwd(). */\n project?: string;\n /** L1 token budget. Default 800. */\n token_budget?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Tool handler\n// ---------------------------------------------------------------------------\n\nexport function toolMemoryWakeup(\n registryDb: Database,\n params: MemoryWakeupParams\n): ToolResult {\n try {\n const tokenBudget = params.token_budget ?? DEFAULT_TOKEN_BUDGET;\n\n // Resolve the project root path\n let rootPath: string | undefined;\n\n if (params.project) {\n // Try slug lookup first\n const bySlug = registryDb\n .prepare(\"SELECT root_path FROM projects WHERE slug = ?\")\n .get(params.project) as { root_path: string } | undefined;\n\n if (bySlug) {\n rootPath = bySlug.root_path;\n } else {\n // Maybe it's an absolute path — try path-based detect\n const detected = detectProjectFromPath(registryDb, params.project);\n if (detected) rootPath = detected.root_path;\n }\n } else {\n // Auto-detect from cwd\n const detected = detectProjectFromPath(registryDb, process.cwd());\n if (detected) rootPath = detected.root_path;\n }\n\n const wakeupBlock = buildWakeupContext(rootPath, tokenBudget);\n\n if (!wakeupBlock) {\n return {\n content: [\n {\n type: \"text\",\n text: \"No wake-up context available. Create ~/.pai/identity.txt for L0 identity, or ensure session notes exist for L1 story.\",\n },\n ],\n };\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `WAKEUP CONTEXT\\n\\n${wakeupBlock}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Wakeup context error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * getTaxonomy — returns the shape of stored memory without requiring a query.\n *\n * Answers \"what do I know about?\" not \"what do I know about X?\"\n *\n * Uses the registry DB (projects, sessions) and the storage backend\n * (memory_files, memory_chunks) to build a structural overview.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Return types\n// ---------------------------------------------------------------------------\n\nexport interface TaxonomyProject {\n slug: string;\n display_name: string;\n session_count: number;\n note_count: number;\n last_activity: string | null; // ISO date string, e.g. \"2026-04-07\"\n top_tags: string[]; // project tags from the registry\n}\n\nexport interface TaxonomyTotals {\n projects: number;\n sessions: number;\n notes: number;\n chunks: number;\n}\n\nexport interface TaxonomyRecentActivity {\n project_slug: string;\n action: string;\n timestamp: string; // ISO date string\n}\n\nexport interface TaxonomyResult {\n projects: TaxonomyProject[];\n totals: TaxonomyTotals;\n recent_activity: TaxonomyRecentActivity[];\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface TaxonomyOptions {\n /** Include archived projects. Default: false. */\n include_archived?: boolean;\n /** Maximum projects to return. Default: 50. */\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Main function\n// ---------------------------------------------------------------------------\n\n/**\n * Build a taxonomy of stored memory — what projects exist, how much is stored,\n * and what has been active recently.\n *\n * Registry queries (projects, sessions) are synchronous (better-sqlite3).\n * Storage backend queries (files, chunks) are async.\n */\nexport async function getTaxonomy(\n registryDb: Database,\n storage: StorageBackend,\n options: TaxonomyOptions = {}\n): Promise<TaxonomyResult> {\n const includeArchived = options.include_archived ?? false;\n const limit = options.limit ?? 50;\n\n // -------------------------------------------------------------------------\n // 1. Load all (active) projects from the registry\n // -------------------------------------------------------------------------\n\n const statusFilter = includeArchived\n ? \"status IN ('active', 'archived', 'migrating')\"\n : \"status = 'active'\";\n\n const projectRows = registryDb\n .prepare(\n `SELECT id, slug, display_name, status, created_at, updated_at\n FROM projects\n WHERE ${statusFilter}\n ORDER BY updated_at DESC\n LIMIT ?`\n )\n .all(limit) as Array<{\n id: number;\n slug: string;\n display_name: string;\n status: string;\n created_at: number;\n updated_at: number;\n }>;\n\n if (projectRows.length === 0) {\n return {\n projects: [],\n totals: { projects: 0, sessions: 0, notes: 0, chunks: 0 },\n recent_activity: [],\n };\n }\n\n const projectIds = projectRows.map((p) => p.id);\n\n // -------------------------------------------------------------------------\n // 2. Session counts per project (registry, synchronous)\n // -------------------------------------------------------------------------\n\n const sessionCountsByProject = new Map<number, number>();\n const lastSessionDateByProject = new Map<number, string | null>();\n\n for (const projectId of projectIds) {\n const countRow = registryDb\n .prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\")\n .get(projectId) as { n: number };\n sessionCountsByProject.set(projectId, countRow.n);\n\n const lastRow = registryDb\n .prepare(\n \"SELECT date FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1\"\n )\n .get(projectId) as { date: string } | undefined;\n lastSessionDateByProject.set(projectId, lastRow?.date ?? null);\n }\n\n // -------------------------------------------------------------------------\n // 3. Tags per project (registry, synchronous)\n // -------------------------------------------------------------------------\n\n const tagsByProject = new Map<number, string[]>();\n\n for (const projectId of projectIds) {\n const tags = registryDb\n .prepare(\n `SELECT t.name\n FROM tags t\n JOIN project_tags pt ON pt.tag_id = t.id\n WHERE pt.project_id = ?\n ORDER BY t.name`\n )\n .all(projectId) as Array<{ name: string }>;\n tagsByProject.set(projectId, tags.map((t) => t.name));\n }\n\n // -------------------------------------------------------------------------\n // 4. Note and chunk counts per project (storage backend, async)\n // We use memory_files for note count (one row per indexed file) and\n // memory_chunks for chunk count (may be many per file).\n // The StorageBackend interface exposes getStats() for totals but not\n // per-project breakdowns, so we cast to the raw DB when it is SQLite\n // and fall back to a single getStats() call for Postgres.\n // -------------------------------------------------------------------------\n\n const noteCountsByProject = new Map<number, number>();\n const chunkCountsByProject = new Map<number, number>();\n\n const isBackend = (x: StorageBackend): boolean => x.backendType === \"sqlite\";\n\n if (isBackend(storage)) {\n // SQLite: access raw DB via the getRawDb() escape hatch present on SQLiteBackend.\n // We reach through the interface via a duck-type check — getRawDb is not on the\n // interface but is documented as an escape hatch for exactly this kind of work.\n const rawDb = (storage as unknown as { getRawDb?: () => Database }).getRawDb?.();\n if (rawDb) {\n for (const projectId of projectIds) {\n const noteRow = rawDb\n .prepare(\n \"SELECT COUNT(*) AS n FROM memory_files WHERE project_id = ?\"\n )\n .get(projectId) as { n: number };\n noteCountsByProject.set(projectId, noteRow.n);\n\n const chunkRow = rawDb\n .prepare(\n \"SELECT COUNT(*) AS n FROM memory_chunks WHERE project_id = ?\"\n )\n .get(projectId) as { n: number };\n chunkCountsByProject.set(projectId, chunkRow.n);\n }\n }\n } else {\n // Postgres: the storage backend interface does not expose per-project file/chunk\n // counts, so we leave them as 0 — totals are still reported via getStats().\n // Future: add per-project getStats(projectId?) to the interface if needed.\n for (const projectId of projectIds) {\n noteCountsByProject.set(projectId, 0);\n chunkCountsByProject.set(projectId, 0);\n }\n }\n\n // -------------------------------------------------------------------------\n // 5. Global totals\n // -------------------------------------------------------------------------\n\n const stats = await storage.getStats();\n\n const totalProjects = (\n registryDb\n .prepare(\n `SELECT COUNT(*) AS n FROM projects WHERE ${statusFilter}`\n )\n .get() as { n: number }\n ).n;\n\n const totalSessions = (\n registryDb.prepare(\"SELECT COUNT(*) AS n FROM sessions\").get() as { n: number }\n ).n;\n\n // -------------------------------------------------------------------------\n // 6. Recent activity — last 10 sessions across all projects\n // -------------------------------------------------------------------------\n\n const recentSessions = registryDb\n .prepare(\n `SELECT s.date, s.title, p.slug\n FROM sessions s\n JOIN projects p ON p.id = s.project_id\n WHERE p.${statusFilter.replace(\"status\", \"p.status\")}\n ORDER BY s.created_at DESC\n LIMIT 10`\n )\n .all() as Array<{ date: string; title: string; slug: string }>;\n\n const recentActivity: TaxonomyRecentActivity[] = recentSessions.map((row) => ({\n project_slug: row.slug,\n action: `session: ${row.title || \"(untitled)\"}`,\n timestamp: row.date,\n }));\n\n // -------------------------------------------------------------------------\n // 7. Assemble result\n // -------------------------------------------------------------------------\n\n const projects: TaxonomyProject[] = projectRows.map((row) => ({\n slug: row.slug,\n display_name: row.display_name,\n session_count: sessionCountsByProject.get(row.id) ?? 0,\n note_count: noteCountsByProject.get(row.id) ?? 0,\n last_activity: lastSessionDateByProject.get(row.id) ?? null,\n top_tags: tagsByProject.get(row.id) ?? [],\n }));\n\n return {\n projects,\n totals: {\n projects: totalProjects,\n sessions: totalSessions,\n notes: stats.files,\n chunks: stats.chunks,\n },\n recent_activity: recentActivity,\n };\n}\n","/**\n * MCP tool handler: memory_taxonomy\n *\n * Returns the SHAPE of stored memory without requiring a query.\n * Answers \"what do I know about?\" not \"what do I know about X?\"\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport { getTaxonomy } from \"../../memory/taxonomy.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_taxonomy\n// ---------------------------------------------------------------------------\n\nexport interface MemoryTaxonomyParams {\n /** Include archived projects in the result. Default: false. */\n include_archived?: boolean;\n /** Maximum number of projects to return. Default: 50. */\n limit?: number;\n}\n\nexport async function toolMemoryTaxonomy(\n registryDb: Database,\n storage: StorageBackend,\n params: MemoryTaxonomyParams = {}\n): Promise<ToolResult> {\n try {\n const result = await getTaxonomy(registryDb, storage, {\n include_archived: params.include_archived,\n limit: params.limit,\n });\n\n // -----------------------------------------------------------------------\n // Format output as human-readable text\n // -----------------------------------------------------------------------\n\n const lines: string[] = [];\n\n // Totals header\n lines.push(\n `PAI Memory Taxonomy — ${result.totals.projects} project(s), ` +\n `${result.totals.sessions} session(s), ` +\n `${result.totals.notes} indexed file(s), ` +\n `${result.totals.chunks} chunk(s)`\n );\n lines.push(\"\");\n\n // Per-project breakdown\n if (result.projects.length === 0) {\n lines.push(\"No active projects found.\");\n } else {\n lines.push(\"Projects:\");\n for (const p of result.projects) {\n const tagStr = p.top_tags.length > 0 ? ` [${p.top_tags.join(\", \")}]` : \"\";\n const activityStr = p.last_activity ? ` last: ${p.last_activity}` : \"\";\n lines.push(\n ` ${p.slug} — ${p.display_name}` +\n ` sessions=${p.session_count}` +\n (p.note_count > 0 ? ` files=${p.note_count}` : \"\") +\n activityStr +\n tagStr\n );\n }\n }\n\n // Recent activity\n if (result.recent_activity.length > 0) {\n lines.push(\"\");\n lines.push(\"Recent activity (last 10 sessions across all projects):\");\n for (const a of result.recent_activity) {\n lines.push(` ${a.timestamp} ${a.project_slug} ${a.action}`);\n }\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `memory_taxonomy error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * tunnels.ts — cross-project concept detection (\"palace graph / tunnel detection\")\n *\n * A \"tunnel\" is a concept (word or short phrase) that appears in chunks from\n * at least two distinct projects. These serendipitous cross-project connections\n * are surfaced so the user can discover unexpected relationships between their\n * work streams.\n *\n * Algorithm:\n * 1. Pull the top-N most frequent significant terms from memory_chunks via BM25 FTS.\n * We use the FTS5 vocab table (if available) or fall back to term frequency\n * aggregation over the raw text via a trigram approach.\n * 2. For each candidate term, count how many distinct projects have at least one\n * chunk containing it and aggregate occurrence stats.\n * 3. Filter by min_projects and min_occurrences, sort by project breadth then\n * frequency, return top limit results.\n *\n * Backend support:\n * - SQLite — uses `memory_fts` MATCH to count per-project occurrences.\n * - Postgres — uses `memory_chunks` tsvector + ts_stat for term extraction and\n * per-project term frequency counting via plainto_tsquery.\n */\n\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface Tunnel {\n /** The shared term or phrase. */\n concept: string;\n /** Project slugs where this concept appears. */\n projects: string[];\n /** Total chunk occurrences across all projects. */\n occurrences: number;\n /** First time the concept appeared (Unix ms). */\n first_seen: number;\n /** Most recent time the concept appeared (Unix ms). */\n last_seen: number;\n}\n\nexport interface FindTunnelsOptions {\n /** Minimum distinct projects a concept must appear in. Default 2. */\n min_projects?: number;\n /** Minimum total chunk occurrences across all projects. Default 3. */\n min_occurrences?: number;\n /** Maximum number of tunnels to return. Default 20. */\n limit?: number;\n}\n\nexport interface FindTunnelsResult {\n tunnels: Tunnel[];\n projects_analyzed: number;\n total_concepts_evaluated: number;\n}\n\n// ---------------------------------------------------------------------------\n// Internal row types\n// ---------------------------------------------------------------------------\n\ninterface ProjectSlugMap {\n [id: number]: string;\n}\n\n// ---------------------------------------------------------------------------\n// SQLite implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Extract candidate terms from the SQLite FTS5 index using the vocabulary\n * approach: iterate the fts5vocab table (if it exists) for the most common\n * terms, then per-term count distinct projects.\n */\nasync function findTunnelsSqlite(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n db: any,\n slugMap: ProjectSlugMap,\n opts: Required<FindTunnelsOptions>\n): Promise<FindTunnelsResult> {\n const projectIds = Object.keys(slugMap).map(Number);\n if (projectIds.length < 2) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 1 — collect candidate terms via FTS5 vocabulary shadow table.\n // memory_fts uses the standard fts5 content table; the vocab view is\n // \"memory_fts_v\" if it was created, otherwise we fall back to sampling.\n let candidateTerms: string[] = [];\n\n try {\n // fts5vocab \"col\" mode: (term, col, doc, cnt) — aggregate all cols.\n const vocabRows = db\n .prepare(\n `SELECT term, SUM(doc) AS doc_count, SUM(cnt) AS total_cnt\n FROM memory_fts_v\n GROUP BY term\n HAVING SUM(cnt) >= ?\n ORDER BY SUM(doc) DESC\n LIMIT 500`\n )\n .all(opts.min_occurrences) as Array<{ term: string; doc_count: number; total_cnt: number }>;\n\n candidateTerms = vocabRows\n .map((r) => r.term)\n .filter((t) => t.length >= 3 && !STOP_WORDS.has(t));\n } catch {\n // Vocab table not available — fall back to sampling common words from chunks.\n // We aggregate word tokens from a sample of chunks using SQLite string ops.\n // This is slower but works on any SQLite federation.db.\n const sampleRows = db\n .prepare(\n `SELECT LOWER(text) AS text FROM memory_chunks\n WHERE LENGTH(text) > 20\n ORDER BY RANDOM()\n LIMIT 2000`\n )\n .all() as Array<{ text: string }>;\n\n const freq = new Map<string, number>();\n for (const { text } of sampleRows) {\n const tokens = text\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t: string) => t.length >= 3 && !STOP_WORDS.has(t));\n for (const t of tokens) {\n freq.set(t, (freq.get(t) ?? 0) + 1);\n }\n }\n candidateTerms = [...freq.entries()]\n .filter(([, n]) => n >= opts.min_occurrences)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 200)\n .map(([t]) => t);\n }\n\n if (candidateTerms.length === 0) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 2 — for each candidate, count distinct projects and occurrences.\n const tunnels: Tunnel[] = [];\n\n for (const term of candidateTerms) {\n try {\n const rows = db\n .prepare(\n `SELECT c.project_id, COUNT(*) AS cnt,\n MIN(c.updated_at) AS first_seen,\n MAX(c.updated_at) AS last_seen\n FROM memory_fts f\n JOIN memory_chunks c ON c.id = f.id\n WHERE memory_fts MATCH ?\n AND c.project_id IN (${projectIds.map(() => \"?\").join(\", \")})\n GROUP BY c.project_id`\n )\n .all(`\"${term.replace(/\"/g, '\"\"')}\"`, ...projectIds) as Array<{\n project_id: number;\n cnt: number;\n first_seen: number;\n last_seen: number;\n }>;\n\n if (rows.length < opts.min_projects) continue;\n\n const totalOccurrences = rows.reduce((s, r) => s + Number(r.cnt), 0);\n if (totalOccurrences < opts.min_occurrences) continue;\n\n const projects = rows\n .map((r) => slugMap[r.project_id] ?? String(r.project_id))\n .filter(Boolean);\n const firstSeen = Math.min(...rows.map((r) => r.first_seen));\n const lastSeen = Math.max(...rows.map((r) => r.last_seen));\n\n tunnels.push({\n concept: term,\n projects,\n occurrences: totalOccurrences,\n first_seen: firstSeen,\n last_seen: lastSeen,\n });\n } catch {\n // Skip problematic terms\n continue;\n }\n }\n\n // Sort: most cross-project spread first, then by raw frequency.\n tunnels.sort((a, b) => {\n const byProjects = b.projects.length - a.projects.length;\n if (byProjects !== 0) return byProjects;\n return b.occurrences - a.occurrences;\n });\n\n return {\n tunnels: tunnels.slice(0, opts.limit),\n projects_analyzed: projectIds.length,\n total_concepts_evaluated: candidateTerms.length,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Postgres implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Use Postgres ts_stat() + plainto_tsquery to efficiently find terms that\n * appear across multiple projects.\n */\nasync function findTunnelsPostgres(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n pool: any,\n slugMap: ProjectSlugMap,\n opts: Required<FindTunnelsOptions>\n): Promise<FindTunnelsResult> {\n const projectIds = Object.keys(slugMap).map(Number);\n if (projectIds.length < 2) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 1 — extract top terms from the corpus using ts_stat over all chunks.\n // ts_stat(query) returns (word, ndoc, nentry) for each lexeme in the tsvector.\n const termResult = await pool.query<{ word: string; ndoc: string; nentry: string }>(\n `SELECT word, ndoc, nentry\n FROM ts_stat(\n 'SELECT to_tsvector(''simple'', text) FROM memory_chunks WHERE project_id = ANY($1)'\n )\n WHERE length(word) >= 3\n AND nentry >= $2\n ORDER BY ndoc DESC\n LIMIT 500`,\n [projectIds, opts.min_occurrences]\n );\n\n let candidateTerms = termResult.rows\n .map((r) => r.word)\n .filter((t) => !STOP_WORDS.has(t));\n\n if (candidateTerms.length === 0) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Cap at 200 candidates for performance.\n candidateTerms = candidateTerms.slice(0, 200);\n\n // Step 2 — for each candidate, count distinct projects via a single batched query.\n // We use a VALUES list + JOIN to avoid N+1 round-trips.\n const valuesClause = candidateTerms\n .map((t, i) => `($${i + 2}::text)`)\n .join(\", \");\n\n const batchResult = await pool.query<{\n concept: string;\n project_id: string;\n cnt: string;\n first_seen: string;\n last_seen: string;\n }>(\n `SELECT v.concept, c.project_id::text, COUNT(*) AS cnt,\n MIN(c.updated_at) AS first_seen,\n MAX(c.updated_at) AS last_seen\n FROM (VALUES ${valuesClause}) AS v(concept)\n JOIN memory_chunks c\n ON to_tsvector('simple', c.text) @@ plainto_tsquery('simple', v.concept)\n AND c.project_id = ANY($1)\n GROUP BY v.concept, c.project_id`,\n [projectIds, ...candidateTerms]\n );\n\n // Aggregate by concept.\n const byConceptMap = new Map<\n string,\n { projects: Set<number>; occurrences: number; firstSeen: number; lastSeen: number }\n >();\n\n for (const row of batchResult.rows) {\n const existing = byConceptMap.get(row.concept) ?? {\n projects: new Set<number>(),\n occurrences: 0,\n firstSeen: Infinity,\n lastSeen: -Infinity,\n };\n existing.projects.add(parseInt(row.project_id, 10));\n existing.occurrences += parseInt(row.cnt, 10);\n const fs = parseInt(row.first_seen, 10);\n const ls = parseInt(row.last_seen, 10);\n if (fs < existing.firstSeen) existing.firstSeen = fs;\n if (ls > existing.lastSeen) existing.lastSeen = ls;\n byConceptMap.set(row.concept, existing);\n }\n\n const tunnels: Tunnel[] = [];\n for (const [concept, data] of byConceptMap) {\n if (data.projects.size < opts.min_projects) continue;\n if (data.occurrences < opts.min_occurrences) continue;\n\n const projects = [...data.projects]\n .map((id) => slugMap[id] ?? String(id))\n .filter(Boolean);\n\n tunnels.push({\n concept,\n projects,\n occurrences: data.occurrences,\n first_seen: data.firstSeen === Infinity ? 0 : data.firstSeen,\n last_seen: data.lastSeen === -Infinity ? 0 : data.lastSeen,\n });\n }\n\n tunnels.sort((a, b) => {\n const byProjects = b.projects.length - a.projects.length;\n if (byProjects !== 0) return byProjects;\n return b.occurrences - a.occurrences;\n });\n\n return {\n tunnels: tunnels.slice(0, opts.limit),\n projects_analyzed: projectIds.length,\n total_concepts_evaluated: candidateTerms.length,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Find cross-project concept tunnels.\n *\n * Works with both SQLite and Postgres storage backends.\n * Requires the `registryDb` (better-sqlite3) for project slug resolution.\n *\n * @param backend Active PAI storage backend.\n * @param registryDb Registry database for project slug resolution.\n * @param options Filter and limit options.\n */\nexport async function findTunnels(\n backend: StorageBackend,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n registryDb: any,\n options?: FindTunnelsOptions\n): Promise<FindTunnelsResult> {\n const opts: Required<FindTunnelsOptions> = {\n min_projects: options?.min_projects ?? 2,\n min_occurrences: options?.min_occurrences ?? 3,\n limit: options?.limit ?? 20,\n };\n\n // Build project slug map from registry.\n const projectRows = registryDb\n .prepare(\"SELECT id, slug FROM projects WHERE status != 'archived'\")\n .all() as Array<{ id: number; slug: string }>;\n\n const slugMap: ProjectSlugMap = {};\n for (const { id, slug } of projectRows) {\n slugMap[id] = slug;\n }\n\n if (backend.backendType === \"postgres\") {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = (backend as any).getPool?.();\n if (!pool) {\n throw new Error(\"findTunnels: Postgres backend does not expose getPool()\");\n }\n return findTunnelsPostgres(pool, slugMap, opts);\n }\n\n // SQLite path — access raw db through the backend adapter.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const rawDb = (backend as any).getRawDb?.();\n if (!rawDb) {\n throw new Error(\"findTunnels: SQLite backend does not expose getRawDb()\");\n }\n return findTunnelsSqlite(rawDb, slugMap, opts);\n}\n","/**\n * MCP tool handler: memory_tunnels\n *\n * Finds \"tunnels\" — concepts that appear across multiple projects —\n * surfacing cross-project serendipitous connections in the PAI memory graph.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { ToolResult } from \"./types.js\";\nimport { findTunnels } from \"../../memory/tunnels.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_tunnels\n// ---------------------------------------------------------------------------\n\nexport interface MemoryTunnelsParams {\n /** Minimum distinct projects a concept must appear in. Default 2. */\n min_projects?: number;\n /** Minimum total chunk occurrences across all projects. Default 3. */\n min_occurrences?: number;\n /** Maximum number of tunnels to return. Default 20. */\n limit?: number;\n}\n\nexport async function toolMemoryTunnels(\n registryDb: Database,\n backend: StorageBackend,\n params: MemoryTunnelsParams\n): Promise<ToolResult> {\n try {\n const result = await findTunnels(backend, registryDb, {\n min_projects: params.min_projects,\n min_occurrences: params.min_occurrences,\n limit: params.limit,\n });\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `memory_tunnels error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * PAI Knowledge OS — MCP tool handlers (barrel re-export)\n *\n * All tool implementations have been split into domain modules under\n * src/mcp/tools/. This file re-exports everything so existing imports\n * (from \"../mcp/tools.js\") continue to work unchanged.\n *\n * Domain modules:\n * tools/types.ts — shared types + project-row helpers\n * tools/memory.ts — memory_search, memory_get\n * tools/projects.ts — project_info, project_list, project_detect,\n * project_health, project_todo\n * tools/sessions.ts — session_list, session_route\n * tools/registry.ts — registry_search\n * tools/notifications.ts — notification_config\n * tools/topics.ts — topic_detect\n * tools/zettel.ts — zettel_explore, zettel_health, zettel_surprise,\n * zettel_suggest, zettel_converse, zettel_themes,\n * zettel_god_notes, zettel_communities\n * tools/observations.ts — observation_search, observation_timeline\n */\n\nexport * from \"./tools/index.js\";\n"],"mappings":"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"}
@@ -134,4 +134,4 @@ async function handleGraphTrace(backend, params) {
134
134
 
135
135
  //#endregion
136
136
  export { handleGraphTrace };
137
- //# sourceMappingURL=trace-CRx9lPuc.mjs.map
137
+ //# sourceMappingURL=trace-C2XrzssW.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"trace-CRx9lPuc.mjs","names":[],"sources":["../src/graph/trace.ts"],"sourcesContent":["/**\n * trace.ts — graph_trace endpoint handler\n *\n * Given a topic/keyword query, searches vault notes for appearances of that topic\n * and returns a chronological timeline showing how the idea evolved over time.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphTraceParams {\n query: string;\n project_id: number;\n max_results?: number;\n lookback_days?: number;\n}\n\nexport interface TraceEntry {\n vault_path: string;\n title: string;\n folder: string;\n indexed_at: number;\n snippet: string;\n dominant_type: string;\n}\n\nexport interface TraceConnection {\n from_path: string;\n to_path: string;\n type: \"temporal\" | \"wikilink\";\n}\n\nexport interface GraphTraceResult {\n query: string;\n entries: TraceEntry[];\n connections: TraceConnection[];\n time_span: { from: number; to: number };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction folderFromPath(vaultPath: string): string {\n const lastSlash = vaultPath.lastIndexOf(\"/\");\n return lastSlash === -1 ? \"\" : vaultPath.slice(0, lastSlash);\n}\n\nfunction extractSnippet(text: string, query: string): string {\n const lowerText = text.toLowerCase();\n const lowerQuery = query.toLowerCase();\n const idx = lowerText.indexOf(lowerQuery);\n if (idx === -1) {\n return text.slice(0, 160).trimEnd() + (text.length > 160 ? \"…\" : \"\");\n }\n\n const CONTEXT = 70;\n const start = Math.max(0, idx - CONTEXT);\n const end = Math.min(text.length, idx + query.length + CONTEXT);\n\n let snippet = text.slice(start, end).replace(/\\s+/g, \" \").trim();\n if (start > 0) snippet = \"…\" + snippet;\n if (end < text.length) snippet = snippet + \"…\";\n\n return snippet;\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphTrace(\n backend: StorageBackend,\n params: GraphTraceParams\n): Promise<GraphTraceResult> {\n const query = (params.query ?? \"\").trim();\n if (!query) {\n return { query: \"\", entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n const maxResults = params.max_results ?? 30;\n const lookbackDays = params.lookback_days ?? 365;\n const cutoffTimestamp = Math.floor(Date.now() / 1000) - lookbackDays * 86400;\n\n // ---------------------------------------------------------------------------\n // Step 1: Collect matching vault paths via multiple search strategies\n // ---------------------------------------------------------------------------\n\n const matchedPaths = new Map<string, string>(); // path → best snippet text\n\n // Strategy A: title / alias match in vault_name_index\n try {\n const namePaths = await backend.searchVaultNameIndex(query, 100);\n for (const vaultPath of namePaths) {\n if (!matchedPaths.has(vaultPath)) {\n matchedPaths.set(vaultPath, \"\");\n }\n }\n } catch {\n // vault_name_index may not exist in all schema versions\n }\n\n // Strategy B: content search in memory_chunks\n try {\n const chunkRows = await backend.searchChunksByText(params.project_id, query, 200);\n for (const row of chunkRows) {\n if (!matchedPaths.has(row.path)) {\n matchedPaths.set(row.path, row.text);\n } else if (!matchedPaths.get(row.path)) {\n matchedPaths.set(row.path, row.text);\n }\n }\n } catch {\n // Best-effort — continue with what we have\n }\n\n if (matchedPaths.size === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 2: Fetch vault_files metadata for all matched paths, filtered by cutoff\n // ---------------------------------------------------------------------------\n\n const allPaths = Array.from(matchedPaths.keys());\n let fileRows: Array<{ vaultPath: string; title: string | null; indexedAt: number }> = [];\n try {\n fileRows = await backend.getVaultFilesByPathsAfter(allPaths, cutoffTimestamp * 1000);\n // Sort chronologically\n fileRows.sort((a, b) => a.indexedAt - b.indexedAt);\n } catch {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n if (fileRows.length === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 3: Build TraceEntry array\n // ---------------------------------------------------------------------------\n\n const entries: TraceEntry[] = fileRows.slice(0, maxResults).map((row) => {\n const fileName = row.vaultPath.split(\"/\").pop() ?? row.vaultPath;\n const title = row.title ?? fileName.replace(/\\.md$/i, \"\");\n const chunkText = matchedPaths.get(row.vaultPath) ?? \"\";\n const snippet = extractSnippet(chunkText, query);\n\n return {\n vault_path: row.vaultPath,\n title,\n folder: folderFromPath(row.vaultPath),\n indexed_at: row.indexedAt,\n snippet,\n dominant_type: \"unknown\",\n };\n });\n\n if (entries.length === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 4: Build connections\n // ---------------------------------------------------------------------------\n\n const connections: TraceConnection[] = [];\n const entryPathSet = new Set(entries.map((e) => e.vault_path));\n\n // Temporal edges: consecutive entries (oldest → next)\n for (let i = 0; i < entries.length - 1; i++) {\n connections.push({\n from_path: entries[i].vault_path,\n to_path: entries[i + 1].vault_path,\n type: \"temporal\",\n });\n }\n\n // Wikilink edges\n try {\n const entryPaths = entries.map(e => e.vault_path);\n const linkRows = await backend.getVaultLinksFromPaths(entryPaths);\n\n const wikiEdgeKeys = new Set<string>();\n for (const row of linkRows) {\n if (!row.targetPath) continue;\n if (!entryPathSet.has(row.targetPath)) continue;\n\n const key = `${row.sourcePath}|||${row.targetPath}`;\n if (wikiEdgeKeys.has(key)) continue;\n wikiEdgeKeys.add(key);\n\n connections.push({\n from_path: row.sourcePath,\n to_path: row.targetPath,\n type: \"wikilink\",\n });\n }\n } catch {\n // vault_links may not exist — temporal edges are sufficient\n }\n\n // ---------------------------------------------------------------------------\n // Step 5: Compute time span\n // ---------------------------------------------------------------------------\n\n const timestamps = entries.map((e) => e.indexed_at).filter((t) => t > 0);\n const timeFrom = timestamps.length > 0 ? Math.min(...timestamps) : 0;\n const timeTo = timestamps.length > 0 ? Math.max(...timestamps) : 0;\n\n return {\n query,\n entries,\n connections,\n time_span: { from: timeFrom, to: timeTo },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"trace-C2XrzssW.mjs","names":[],"sources":["../src/graph/trace.ts"],"sourcesContent":["/**\n * trace.ts — graph_trace endpoint handler\n *\n * Given a topic/keyword query, searches vault notes for appearances of that topic\n * and returns a chronological timeline showing how the idea evolved over time.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphTraceParams {\n query: string;\n project_id: number;\n max_results?: number;\n lookback_days?: number;\n}\n\nexport interface TraceEntry {\n vault_path: string;\n title: string;\n folder: string;\n indexed_at: number;\n snippet: string;\n dominant_type: string;\n}\n\nexport interface TraceConnection {\n from_path: string;\n to_path: string;\n type: \"temporal\" | \"wikilink\";\n}\n\nexport interface GraphTraceResult {\n query: string;\n entries: TraceEntry[];\n connections: TraceConnection[];\n time_span: { from: number; to: number };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction folderFromPath(vaultPath: string): string {\n const lastSlash = vaultPath.lastIndexOf(\"/\");\n return lastSlash === -1 ? \"\" : vaultPath.slice(0, lastSlash);\n}\n\nfunction extractSnippet(text: string, query: string): string {\n const lowerText = text.toLowerCase();\n const lowerQuery = query.toLowerCase();\n const idx = lowerText.indexOf(lowerQuery);\n if (idx === -1) {\n return text.slice(0, 160).trimEnd() + (text.length > 160 ? \"…\" : \"\");\n }\n\n const CONTEXT = 70;\n const start = Math.max(0, idx - CONTEXT);\n const end = Math.min(text.length, idx + query.length + CONTEXT);\n\n let snippet = text.slice(start, end).replace(/\\s+/g, \" \").trim();\n if (start > 0) snippet = \"…\" + snippet;\n if (end < text.length) snippet = snippet + \"…\";\n\n return snippet;\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphTrace(\n backend: StorageBackend,\n params: GraphTraceParams\n): Promise<GraphTraceResult> {\n const query = (params.query ?? \"\").trim();\n if (!query) {\n return { query: \"\", entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n const maxResults = params.max_results ?? 30;\n const lookbackDays = params.lookback_days ?? 365;\n const cutoffTimestamp = Math.floor(Date.now() / 1000) - lookbackDays * 86400;\n\n // ---------------------------------------------------------------------------\n // Step 1: Collect matching vault paths via multiple search strategies\n // ---------------------------------------------------------------------------\n\n const matchedPaths = new Map<string, string>(); // path → best snippet text\n\n // Strategy A: title / alias match in vault_name_index\n try {\n const namePaths = await backend.searchVaultNameIndex(query, 100);\n for (const vaultPath of namePaths) {\n if (!matchedPaths.has(vaultPath)) {\n matchedPaths.set(vaultPath, \"\");\n }\n }\n } catch {\n // vault_name_index may not exist in all schema versions\n }\n\n // Strategy B: content search in memory_chunks\n try {\n const chunkRows = await backend.searchChunksByText(params.project_id, query, 200);\n for (const row of chunkRows) {\n if (!matchedPaths.has(row.path)) {\n matchedPaths.set(row.path, row.text);\n } else if (!matchedPaths.get(row.path)) {\n matchedPaths.set(row.path, row.text);\n }\n }\n } catch {\n // Best-effort — continue with what we have\n }\n\n if (matchedPaths.size === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 2: Fetch vault_files metadata for all matched paths, filtered by cutoff\n // ---------------------------------------------------------------------------\n\n const allPaths = Array.from(matchedPaths.keys());\n let fileRows: Array<{ vaultPath: string; title: string | null; indexedAt: number }> = [];\n try {\n fileRows = await backend.getVaultFilesByPathsAfter(allPaths, cutoffTimestamp * 1000);\n // Sort chronologically\n fileRows.sort((a, b) => a.indexedAt - b.indexedAt);\n } catch {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n if (fileRows.length === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 3: Build TraceEntry array\n // ---------------------------------------------------------------------------\n\n const entries: TraceEntry[] = fileRows.slice(0, maxResults).map((row) => {\n const fileName = row.vaultPath.split(\"/\").pop() ?? row.vaultPath;\n const title = row.title ?? fileName.replace(/\\.md$/i, \"\");\n const chunkText = matchedPaths.get(row.vaultPath) ?? \"\";\n const snippet = extractSnippet(chunkText, query);\n\n return {\n vault_path: row.vaultPath,\n title,\n folder: folderFromPath(row.vaultPath),\n indexed_at: row.indexedAt,\n snippet,\n dominant_type: \"unknown\",\n };\n });\n\n if (entries.length === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 4: Build connections\n // ---------------------------------------------------------------------------\n\n const connections: TraceConnection[] = [];\n const entryPathSet = new Set(entries.map((e) => e.vault_path));\n\n // Temporal edges: consecutive entries (oldest → next)\n for (let i = 0; i < entries.length - 1; i++) {\n connections.push({\n from_path: entries[i].vault_path,\n to_path: entries[i + 1].vault_path,\n type: \"temporal\",\n });\n }\n\n // Wikilink edges\n try {\n const entryPaths = entries.map(e => e.vault_path);\n const linkRows = await backend.getVaultLinksFromPaths(entryPaths);\n\n const wikiEdgeKeys = new Set<string>();\n for (const row of linkRows) {\n if (!row.targetPath) continue;\n if (!entryPathSet.has(row.targetPath)) continue;\n\n const key = `${row.sourcePath}|||${row.targetPath}`;\n if (wikiEdgeKeys.has(key)) continue;\n wikiEdgeKeys.add(key);\n\n connections.push({\n from_path: row.sourcePath,\n to_path: row.targetPath,\n type: \"wikilink\",\n });\n }\n } catch {\n // vault_links may not exist — temporal edges are sufficient\n }\n\n // ---------------------------------------------------------------------------\n // Step 5: Compute time span\n // ---------------------------------------------------------------------------\n\n const timestamps = entries.map((e) => e.indexed_at).filter((t) => t > 0);\n const timeFrom = timestamps.length > 0 ? Math.min(...timestamps) : 0;\n const timeTo = timestamps.length > 0 ? Math.max(...timestamps) : 0;\n\n return {\n query,\n entries,\n connections,\n time_span: { from: timeFrom, to: timeTo },\n };\n}\n"],"mappings":"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"}
@@ -533,4 +533,4 @@ async function indexVault(backend, vaultProjectId, vaultRoot) {
533
533
 
534
534
  //#endregion
535
535
  export { indexVault };
536
- //# sourceMappingURL=vault-indexer-B-aJpRZC.mjs.map
536
+ //# sourceMappingURL=vault-indexer-TTCl1QOL.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"vault-indexer-B-aJpRZC.mjs","names":[],"sources":["../src/memory/vault/walk.ts","../src/memory/vault/deduplicate.ts","../src/memory/vault/parse-links.ts","../src/memory/vault/name-index.ts","../src/memory/vault/resolve.ts","../src/memory/vault/indexer.ts"],"sourcesContent":["/**\n * Vault directory walker — follows symlinks with cycle detection.\n */\n\nimport { statSync, readdirSync, existsSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/** Maximum number of .md files to collect from a vault. */\nconst VAULT_MAX_FILES = 10_000;\n\n/** Maximum recursion depth for vault directory walks. */\nconst VAULT_MAX_DEPTH = 10;\n\n/**\n * Directories to always skip, at any depth, during vault walks.\n * Includes standard build/VCS noise plus Obsidian-specific directories.\n */\nconst VAULT_SKIP_DIRS = new Set([\n // Version control\n \".git\",\n // Dependency directories (any language)\n \"node_modules\",\n \"vendor\",\n \"Pods\",\n // Build / compile output\n \"dist\",\n \"build\",\n \"out\",\n \"DerivedData\",\n \".next\",\n // Python virtual environments and caches\n \".venv\",\n \"venv\",\n \"__pycache__\",\n // General caches\n \".cache\",\n \".bun\",\n // Obsidian internals\n \".obsidian\",\n \".trash\",\n]);\n\n/**\n * Recursively collect all .md files under a vault root, following symlinks.\n *\n * Symlink-following behaviour:\n * - Symbolic links to files: followed if the target is a .md file\n * - Symbolic links to directories: followed with cycle detection via inode\n *\n * Cycle detection is based on the real inode of each visited directory.\n * Using the real stat (not lstat) ensures that symlinked dirs resolve to\n * their actual inode, preventing infinite loops.\n *\n * @param vaultRoot Absolute path to the vault root directory.\n * @param opts Optional overrides for maxFiles and maxDepth.\n */\nexport function walkVaultMdFiles(\n vaultRoot: string,\n opts?: { maxFiles?: number; maxDepth?: number },\n): VaultFile[] {\n const maxFiles = opts?.maxFiles ?? VAULT_MAX_FILES;\n const maxDepth = opts?.maxDepth ?? VAULT_MAX_DEPTH;\n\n const results: VaultFile[] = [];\n const visitedDirs = new Set<string>();\n\n function walk(dir: string, depth: number): void {\n if (results.length >= maxFiles) return;\n if (depth > maxDepth) return;\n\n // Get the real inode of this directory (follows symlinks on the dir itself)\n let dirStat: ReturnType<typeof statSync>;\n try {\n dirStat = statSync(dir);\n } catch {\n return; // Unreadable or broken symlink — skip\n }\n\n const dirKey = `${dirStat.dev}:${dirStat.ino}`;\n if (visitedDirs.has(dirKey)) return; // Cycle detected\n visitedDirs.add(dirKey);\n\n let entries: import(\"node:fs\").Dirent<string>[];\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: \"utf8\" });\n } catch {\n return; // Unreadable directory — skip\n }\n\n for (const entry of entries) {\n if (results.length >= maxFiles) break;\n if (VAULT_SKIP_DIRS.has(entry.name)) continue;\n\n const full = join(dir, entry.name);\n\n if (entry.isSymbolicLink()) {\n // Follow the symlink — resolve to real target\n let targetStat: ReturnType<typeof statSync>;\n try {\n targetStat = statSync(full); // statSync follows symlinks\n } catch {\n continue; // Broken symlink — skip\n }\n\n if (targetStat.isDirectory()) {\n if (!VAULT_SKIP_DIRS.has(entry.name)) {\n walk(full, depth + 1);\n }\n } else if (targetStat.isFile() && entry.name.endsWith(\".md\")) {\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: targetStat.ino,\n device: targetStat.dev,\n });\n }\n } else if (entry.isDirectory()) {\n walk(full, depth + 1);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n let fileStat: ReturnType<typeof statSync>;\n try {\n fileStat = statSync(full);\n } catch {\n continue;\n }\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: fileStat.ino,\n device: fileStat.dev,\n });\n }\n }\n }\n\n if (existsSync(vaultRoot)) {\n walk(vaultRoot, 0);\n }\n\n return results;\n}\n","/**\n * Inode-based deduplication for vault files.\n */\n\nimport type { VaultFile, InodeGroup } from \"./types.js\";\n\n/**\n * Group vault files by inode identity (device + inode).\n *\n * Within each group, the canonical file is chosen as the one with the\n * fewest path separators (shallowest), breaking ties by shortest string.\n * All other group members become aliases.\n */\nexport function deduplicateByInode(files: VaultFile[]): InodeGroup[] {\n const groups = new Map<string, VaultFile[]>();\n\n for (const file of files) {\n const key = `${file.device}:${file.inode}`;\n const existing = groups.get(key);\n if (existing) {\n existing.push(file);\n } else {\n groups.set(key, [file]);\n }\n }\n\n const result: InodeGroup[] = [];\n\n for (const group of groups.values()) {\n if (group.length === 0) continue;\n\n // Sort: fewest path separators first, then shortest string\n const sorted = [...group].sort((a, b) => {\n const aDepth = (a.vaultRelPath.match(/\\//g) ?? []).length;\n const bDepth = (b.vaultRelPath.match(/\\//g) ?? []).length;\n if (aDepth !== bDepth) return aDepth - bDepth;\n return a.vaultRelPath.length - b.vaultRelPath.length;\n });\n\n const [canonical, ...aliases] = sorted as [VaultFile, ...VaultFile[]];\n result.push({ canonical, aliases });\n }\n\n return result;\n}\n","/**\n * Markdown link parser for vault files.\n *\n * Handles wikilinks ([[target]]), markdown links ([text](path)),\n * embeds (![[target]]), and frontmatter wikilinks.\n */\n\nimport type { ParsedLink } from \"./types.js\";\n\n/**\n * Parse all links from markdown content.\n *\n * Handles:\n * - Standard wikilinks: [[Target Note]]\n * - Aliased wikilinks: [[Target Note|Display Text]]\n * - Heading anchors: [[Target Note#Heading]] (stripped for resolution)\n * - Embeds: ![[Target Note]]\n * - Frontmatter wikilinks (YAML between --- delimiters)\n * - Markdown links: [text](path/to/note.md)\n * - Markdown embeds: ![alt](image.png)\n *\n * External URLs (http://, https://, mailto:, etc.) are excluded — only\n * relative paths are treated as vault links.\n *\n * @param content Raw markdown file content.\n * @returns Array of parsed links in document order.\n */\nexport function parseLinks(content: string): ParsedLink[] {\n const links: ParsedLink[] = [];\n const lines = content.split(\"\\n\");\n\n // Determine frontmatter range (YAML between opening and closing ---)\n let frontmatterEnd = 0;\n if (content.startsWith(\"---\")) {\n const closingIdx = content.indexOf(\"\\n---\", 3);\n if (closingIdx !== -1) {\n frontmatterEnd = content.slice(0, closingIdx + 4).split(\"\\n\").length - 1;\n }\n }\n\n // Regex for [[wikilinks]] and ![[embeds]]\n const wikilinkRe = /(!?)\\[\\[([^\\]]+?)\\]\\]/g;\n\n // Regex for markdown links [text](target) and embeds ![alt](target)\n const mdLinkRe = /(!)?\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx]!;\n const lineNumber = lineIdx + 1; // 1-indexed\n const isFrontmatter = lineIdx < frontmatterEnd;\n\n // --- Wikilinks ---\n wikilinkRe.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = wikilinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const inner = match[2]!;\n\n // Split on first | for alias\n const pipeIdx = inner.indexOf(\"|\");\n const beforePipe = pipeIdx === -1 ? inner : inner.slice(0, pipeIdx);\n const alias = pipeIdx === -1 ? null : inner.slice(pipeIdx + 1);\n\n // Strip heading anchor (everything after #)\n const hashIdx = beforePipe.indexOf(\"#\");\n const raw = hashIdx === -1 ? beforePipe.trim() : beforePipe.slice(0, hashIdx).trim();\n\n if (!raw) continue; // Skip links with empty targets (e.g. [[#Heading]])\n\n links.push({\n raw,\n alias: alias?.trim() ?? null,\n lineNumber,\n isEmbed: isEmbed && !isFrontmatter,\n isMdLink: false,\n });\n }\n\n // --- Markdown links --- (skip inside frontmatter)\n if (!isFrontmatter) {\n mdLinkRe.lastIndex = 0;\n while ((match = mdLinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const displayText = match[2]!;\n let target = match[3]!.trim();\n\n // Skip external URLs\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue;\n\n // Skip pure anchor links (#heading)\n if (target.startsWith(\"#\")) continue;\n\n // Strip heading anchor from target\n const hashIdx = target.indexOf(\"#\");\n if (hashIdx !== -1) target = target.slice(0, hashIdx);\n\n // URL-decode (Obsidian encodes spaces as %20 in md links)\n try {\n target = decodeURIComponent(target);\n } catch {\n // Malformed encoding — use as-is\n }\n\n // Strip .md extension for resolution (resolveWikilink adds it back)\n const raw = target.replace(/\\.md$/i, \"\").trim();\n if (!raw) continue;\n\n links.push({\n raw,\n alias: displayText || null,\n lineNumber,\n isEmbed,\n isMdLink: true,\n });\n }\n }\n }\n\n return links;\n}\n\n/** @deprecated Use {@link parseLinks} instead. */\nexport const parseWikilinks = parseLinks;\n","/**\n * Name index builder for Obsidian wikilink resolution.\n */\n\nimport { basename } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/**\n * Build a name lookup index for Obsidian wikilink resolution.\n *\n * Maps lowercase filename (without .md extension) to all vault-relative paths\n * that share that name. Includes both canonical paths and alias paths so that\n * wikilinks resolve regardless of which path the file is accessed through.\n */\nexport function buildNameIndex(files: VaultFile[]): Map<string, string[]> {\n const index = new Map<string, string[]>();\n\n for (const file of files) {\n const name = basename(file.vaultRelPath, \".md\").toLowerCase();\n const existing = index.get(name);\n if (existing) {\n existing.push(file.vaultRelPath);\n } else {\n index.set(name, [file.vaultRelPath]);\n }\n }\n\n return index;\n}\n","/**\n * Wikilink resolver using Obsidian's shortest-match algorithm.\n */\n\nimport { normalize, basename, dirname } from \"node:path\";\n\n/**\n * Resolve a wikilink target to a vault-relative path using Obsidian's rules.\n *\n * Resolution algorithm:\n * 1. If raw contains \"/\", attempt exact path match (with and without .md).\n * 2. Normalize: lowercase the raw target, strip .md extension.\n * 3. Look up in the name index (all files with that basename).\n * 4. If exactly one match, return it.\n * 5. If multiple matches, pick the one closest to the source file\n * (longest common directory prefix, then shortest overall path).\n * 6. If no matches, return null (dead link).\n *\n * @param raw The raw link target (heading-stripped, pipe-stripped).\n * @param nameIndex Map from lowercase basename-without-ext to vault paths.\n * @param sourcePath Vault-relative path of the file containing the link.\n * @returns Vault-relative path of the resolved target, or null.\n */\nexport function resolveWikilink(\n raw: string,\n nameIndex: Map<string, string[]>,\n sourcePath: string,\n): string | null {\n if (!raw) return null;\n\n // Case 1: path contains \"/\" — try exact match with and without .md\n if (raw.includes(\"/\")) {\n const normalized = normalize(raw);\n const normalizedMd = normalized.endsWith(\".md\") ? normalized : normalized + \".md\";\n\n // Check if any indexed path matches (case-insensitive for macOS compatibility)\n for (const [, paths] of nameIndex) {\n for (const p of paths) {\n if (p === normalizedMd || p === normalized) return p;\n if (p.toLowerCase() === normalizedMd.toLowerCase()) return p;\n }\n }\n // Fall through to name lookup in case the path prefix was wrong\n }\n\n // Normalize the raw target for name lookup.\n // Use the basename only — Obsidian resolves by filename, not full path.\n const rawBase = basename(raw)\n .replace(/\\.md$/i, \"\")\n .toLowerCase()\n .trim();\n\n if (!rawBase) return null;\n\n const candidates = nameIndex.get(rawBase);\n\n if (!candidates || candidates.length === 0) {\n return null; // Dead link\n }\n\n if (candidates.length === 1) {\n return candidates[0]!;\n }\n\n // Multiple matches — pick the one closest to the source file\n const sourceDir = dirname(sourcePath);\n\n let bestPath: string | null = null;\n let bestPrefixLen = -1;\n let bestPathLen = Infinity;\n\n for (const candidate of candidates) {\n const candidateDir = dirname(candidate);\n const prefixLen = commonPrefixLength(sourceDir, candidateDir);\n const pathLen = candidate.length;\n\n if (\n prefixLen > bestPrefixLen ||\n (prefixLen === bestPrefixLen && pathLen < bestPathLen)\n ) {\n bestPrefixLen = prefixLen;\n bestPathLen = pathLen;\n bestPath = candidate;\n }\n }\n\n return bestPath;\n}\n\n/**\n * Compute the length of the common prefix between two directory paths,\n * measured in path segments (not raw characters).\n *\n * Example: \"a/b/c\" and \"a/b/d\" → 2 (common: \"a\", \"b\")\n */\nfunction commonPrefixLength(a: string, b: string): number {\n if (a === \".\" && b === \".\") return 0;\n const aParts = a === \".\" ? [] : a.split(\"/\");\n const bParts = b === \".\" ? [] : b.split(\"/\");\n let count = 0;\n const len = Math.min(aParts.length, bParts.length);\n for (let i = 0; i < len; i++) {\n if (aParts[i] === bParts[i]) {\n count++;\n } else {\n break;\n }\n }\n return count;\n}\n","/**\n * Main vault indexing orchestrator.\n *\n * Indexes an entire Obsidian vault (or any markdown knowledge base), following\n * symlinks, deduplicating files by inode, parsing wikilinks, and computing\n * per-file health metrics (orphan detection, dead links).\n *\n * Key differences from the project indexer (indexer/sync.ts):\n * - Follows symbolic links (project indexer skips them)\n * - Deduplicates files with the same inode (same content reachable via multiple paths)\n * - Parses [[wikilinks]] and builds a directed link graph\n * - Resolves wikilinks using Obsidian's shortest-match algorithm\n * - Computes health metrics per file: inbound/outbound link counts, dead links, orphans\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport type {\n StorageBackend,\n VaultFileRow,\n VaultAliasRow,\n VaultLinkRow,\n VaultHealthRow,\n VaultNameEntry,\n ChunkRow,\n} from \"../../storage/interface.js\";\nimport { chunkMarkdown } from \"../chunker.js\";\nimport { sha256File, chunkId, yieldToEventLoop } from \"../indexer/helpers.js\";\nimport { walkVaultMdFiles } from \"./walk.js\";\nimport { deduplicateByInode } from \"./deduplicate.js\";\nimport { parseLinks } from \"./parse-links.js\";\nimport { buildNameIndex } from \"./name-index.js\";\nimport { resolveWikilink } from \"./resolve.js\";\nimport type { VaultIndexResult } from \"./types.js\";\n\n/** Number of files to process before yielding to the event loop. */\nconst VAULT_YIELD_EVERY = 1;\n\n/**\n * Index an entire Obsidian vault (or markdown knowledge base) using the\n * async StorageBackend interface.\n *\n * Steps:\n * 1. Walk vault root, following symlinks.\n * 2. Deduplicate by inode — each unique file is indexed once.\n * 3. Build a name index for wikilink resolution.\n * 4. For each canonical file:\n * a. SHA-256 hash for change detection — skip unchanged files.\n * b. Read content, chunk with chunkMarkdown().\n * c. Insert chunks into backend (memory_chunks and memory_fts).\n * d. Upsert vault_files row.\n * 5. Record aliases in vault_aliases.\n * 6. Rebuild vault_name_index table.\n * 7. Rebuild vault_links:\n * a. Parse [[wikilinks]] from each canonical file.\n * b. Resolve each link with resolveWikilink().\n * c. Insert into vault_links.\n * 8. Compute and upsert health metrics (vault_health).\n * 9. Return statistics.\n *\n * @param backend StorageBackend to write to.\n * @param vaultProjectId Registry project ID for the vault \"project\".\n * @param vaultRoot Absolute path to the vault root directory.\n */\nexport async function indexVault(\n backend: StorageBackend,\n vaultProjectId: number,\n vaultRoot: string,\n): Promise<VaultIndexResult> {\n const startTime = Date.now();\n\n const result: VaultIndexResult = {\n filesIndexed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n aliasesRecorded: 0,\n linksExtracted: 0,\n deadLinksFound: 0,\n orphansFound: 0,\n elapsed: 0,\n };\n\n // Step 1: Walk vault, collecting all .md files (follows symlinks)\n const allFiles = walkVaultMdFiles(vaultRoot);\n\n // Step 2: Deduplicate by inode\n const inodeGroups = deduplicateByInode(allFiles);\n\n // Step 3: Build name index (from all files including aliases, for resolution)\n const nameIndex = buildNameIndex(allFiles);\n\n // Step 4: Index each canonical file\n await yieldToEventLoop();\n let filesSinceYield = 0;\n\n for (const group of inodeGroups) {\n if (filesSinceYield >= VAULT_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const { canonical } = group;\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n result.filesSkipped++;\n continue;\n }\n\n const hash = sha256File(content);\n\n // Change detection: skip if hash is unchanged\n const existing = await backend.getVaultFile(canonical.vaultRelPath);\n if (existing?.hash === hash) {\n result.filesSkipped++;\n continue;\n }\n\n // Delete old chunks for this vault path\n await backend.deleteChunksForFile(vaultProjectId, canonical.vaultRelPath);\n\n // Chunk the content\n const chunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n // Extract title from first H1 heading or filename\n const titleMatch = /^#\\s+(.+)$/m.exec(content);\n const title = titleMatch\n ? titleMatch[1]!.trim()\n : basename(canonical.vaultRelPath, \".md\");\n\n // Build chunk rows\n const chunkRows: ChunkRow[] = [];\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(\n vaultProjectId,\n canonical.vaultRelPath,\n i,\n chunk.startLine,\n chunk.endLine,\n );\n chunkRows.push({\n id,\n projectId: vaultProjectId,\n source: \"vault\",\n tier: \"topic\",\n path: canonical.vaultRelPath,\n startLine: chunk.startLine,\n endLine: chunk.endLine,\n hash: chunk.hash,\n text: chunk.text,\n updatedAt,\n });\n }\n\n if (chunkRows.length > 0) {\n await backend.insertChunks(chunkRows);\n }\n\n // Upsert vault file record\n const vaultFileRow: VaultFileRow = {\n vaultPath: canonical.vaultRelPath,\n inode: canonical.inode,\n device: canonical.device,\n hash,\n title,\n indexedAt: updatedAt,\n };\n await backend.upsertVaultFile(vaultFileRow);\n\n result.filesIndexed++;\n result.chunksCreated += chunks.length;\n }\n\n // Step 5: Record aliases in vault_aliases\n await yieldToEventLoop();\n\n const allAliases: VaultAliasRow[] = [];\n for (const group of inodeGroups) {\n for (const alias of group.aliases) {\n allAliases.push({\n vaultPath: alias.vaultRelPath,\n canonicalPath: group.canonical.vaultRelPath,\n inode: alias.inode,\n device: alias.device,\n });\n result.aliasesRecorded++;\n }\n }\n\n const canonicalPaths = new Set(inodeGroups.map((g) => g.canonical.vaultRelPath));\n for (const canonPath of canonicalPaths) {\n await backend.deleteVaultAliases(canonPath);\n }\n if (allAliases.length > 0) {\n await backend.upsertVaultAliases(allAliases);\n }\n\n // Step 6: Rebuild vault_name_index\n await yieldToEventLoop();\n\n const nameEntries: VaultNameEntry[] = [];\n for (const [name, paths] of nameIndex) {\n for (const path of paths) {\n nameEntries.push({ name, vaultPath: path });\n }\n }\n await backend.replaceNameIndex(nameEntries);\n\n // Step 7: Rebuild vault_links\n await yieldToEventLoop();\n\n const linkRows: VaultLinkRow[] = [];\n const allSourcePaths: string[] = [];\n\n let linkParseYield = 0;\n for (const group of inodeGroups) {\n if (linkParseYield++ % VAULT_YIELD_EVERY === 0) {\n await yieldToEventLoop();\n }\n\n const { canonical } = group;\n allSourcePaths.push(canonical.vaultRelPath);\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n continue;\n }\n\n const parsedLinks = parseLinks(content);\n for (const link of parsedLinks) {\n const target = resolveWikilink(link.raw, nameIndex, canonical.vaultRelPath);\n let linkType: string;\n if (link.isMdLink) {\n linkType = link.isEmbed ? \"md-embed\" : \"md-link\";\n } else {\n linkType = link.isEmbed ? \"embed\" : \"wikilink\";\n }\n linkRows.push({\n sourcePath: canonical.vaultRelPath,\n targetRaw: link.raw,\n targetPath: target,\n linkType,\n lineNumber: link.lineNumber,\n confidence: \"EXTRACTED\",\n });\n }\n }\n\n // Replace all links for all sources in batches of 500\n const LINK_BATCH_SIZE = 500;\n for (let i = 0; i < allSourcePaths.length; i += LINK_BATCH_SIZE) {\n const batchSources = allSourcePaths.slice(i, i + LINK_BATCH_SIZE);\n const batchLinks = linkRows.filter((r) => batchSources.includes(r.sourcePath));\n await backend.replaceLinksForSources(batchSources, batchLinks);\n await yieldToEventLoop();\n }\n\n result.linksExtracted = linkRows.length;\n result.deadLinksFound = linkRows.filter((r) => r.targetPath === null).length;\n\n // Step 8: Compute and upsert vault_health metrics\n await yieldToEventLoop();\n\n const outboundMap = new Map<string, number>();\n const deadMap = new Map<string, number>();\n const inboundMap = new Map<string, number>();\n\n for (const row of linkRows) {\n outboundMap.set(row.sourcePath, (outboundMap.get(row.sourcePath) ?? 0) + 1);\n if (row.targetPath === null) {\n deadMap.set(row.sourcePath, (deadMap.get(row.sourcePath) ?? 0) + 1);\n } else {\n inboundMap.set(row.targetPath, (inboundMap.get(row.targetPath) ?? 0) + 1);\n }\n }\n\n const computedAt = Date.now();\n let orphanCount = 0;\n\n const HEALTH_BATCH_SIZE = 500;\n for (let i = 0; i < inodeGroups.length; i += HEALTH_BATCH_SIZE) {\n const batch = inodeGroups.slice(i, i + HEALTH_BATCH_SIZE);\n const healthRows: VaultHealthRow[] = batch.map((group) => {\n const path = group.canonical.vaultRelPath;\n const inbound = inboundMap.get(path) ?? 0;\n const outbound = outboundMap.get(path) ?? 0;\n const dead = deadMap.get(path) ?? 0;\n const isOrphan = inbound === 0;\n if (isOrphan) orphanCount++;\n return {\n vaultPath: path,\n inboundCount: inbound,\n outboundCount: outbound,\n deadLinkCount: dead,\n isOrphan,\n computedAt,\n };\n });\n await backend.upsertVaultHealth(healthRows);\n await yieldToEventLoop();\n }\n\n result.orphansFound = orphanCount;\n result.elapsed = Date.now() - startTime;\n\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"vault-indexer-TTCl1QOL.mjs","names":[],"sources":["../src/memory/vault/walk.ts","../src/memory/vault/deduplicate.ts","../src/memory/vault/parse-links.ts","../src/memory/vault/name-index.ts","../src/memory/vault/resolve.ts","../src/memory/vault/indexer.ts"],"sourcesContent":["/**\n * Vault directory walker — follows symlinks with cycle detection.\n */\n\nimport { statSync, readdirSync, existsSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/** Maximum number of .md files to collect from a vault. */\nconst VAULT_MAX_FILES = 10_000;\n\n/** Maximum recursion depth for vault directory walks. */\nconst VAULT_MAX_DEPTH = 10;\n\n/**\n * Directories to always skip, at any depth, during vault walks.\n * Includes standard build/VCS noise plus Obsidian-specific directories.\n */\nconst VAULT_SKIP_DIRS = new Set([\n // Version control\n \".git\",\n // Dependency directories (any language)\n \"node_modules\",\n \"vendor\",\n \"Pods\",\n // Build / compile output\n \"dist\",\n \"build\",\n \"out\",\n \"DerivedData\",\n \".next\",\n // Python virtual environments and caches\n \".venv\",\n \"venv\",\n \"__pycache__\",\n // General caches\n \".cache\",\n \".bun\",\n // Obsidian internals\n \".obsidian\",\n \".trash\",\n]);\n\n/**\n * Recursively collect all .md files under a vault root, following symlinks.\n *\n * Symlink-following behaviour:\n * - Symbolic links to files: followed if the target is a .md file\n * - Symbolic links to directories: followed with cycle detection via inode\n *\n * Cycle detection is based on the real inode of each visited directory.\n * Using the real stat (not lstat) ensures that symlinked dirs resolve to\n * their actual inode, preventing infinite loops.\n *\n * @param vaultRoot Absolute path to the vault root directory.\n * @param opts Optional overrides for maxFiles and maxDepth.\n */\nexport function walkVaultMdFiles(\n vaultRoot: string,\n opts?: { maxFiles?: number; maxDepth?: number },\n): VaultFile[] {\n const maxFiles = opts?.maxFiles ?? VAULT_MAX_FILES;\n const maxDepth = opts?.maxDepth ?? VAULT_MAX_DEPTH;\n\n const results: VaultFile[] = [];\n const visitedDirs = new Set<string>();\n\n function walk(dir: string, depth: number): void {\n if (results.length >= maxFiles) return;\n if (depth > maxDepth) return;\n\n // Get the real inode of this directory (follows symlinks on the dir itself)\n let dirStat: ReturnType<typeof statSync>;\n try {\n dirStat = statSync(dir);\n } catch {\n return; // Unreadable or broken symlink — skip\n }\n\n const dirKey = `${dirStat.dev}:${dirStat.ino}`;\n if (visitedDirs.has(dirKey)) return; // Cycle detected\n visitedDirs.add(dirKey);\n\n let entries: import(\"node:fs\").Dirent<string>[];\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: \"utf8\" });\n } catch {\n return; // Unreadable directory — skip\n }\n\n for (const entry of entries) {\n if (results.length >= maxFiles) break;\n if (VAULT_SKIP_DIRS.has(entry.name)) continue;\n\n const full = join(dir, entry.name);\n\n if (entry.isSymbolicLink()) {\n // Follow the symlink — resolve to real target\n let targetStat: ReturnType<typeof statSync>;\n try {\n targetStat = statSync(full); // statSync follows symlinks\n } catch {\n continue; // Broken symlink — skip\n }\n\n if (targetStat.isDirectory()) {\n if (!VAULT_SKIP_DIRS.has(entry.name)) {\n walk(full, depth + 1);\n }\n } else if (targetStat.isFile() && entry.name.endsWith(\".md\")) {\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: targetStat.ino,\n device: targetStat.dev,\n });\n }\n } else if (entry.isDirectory()) {\n walk(full, depth + 1);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n let fileStat: ReturnType<typeof statSync>;\n try {\n fileStat = statSync(full);\n } catch {\n continue;\n }\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: fileStat.ino,\n device: fileStat.dev,\n });\n }\n }\n }\n\n if (existsSync(vaultRoot)) {\n walk(vaultRoot, 0);\n }\n\n return results;\n}\n","/**\n * Inode-based deduplication for vault files.\n */\n\nimport type { VaultFile, InodeGroup } from \"./types.js\";\n\n/**\n * Group vault files by inode identity (device + inode).\n *\n * Within each group, the canonical file is chosen as the one with the\n * fewest path separators (shallowest), breaking ties by shortest string.\n * All other group members become aliases.\n */\nexport function deduplicateByInode(files: VaultFile[]): InodeGroup[] {\n const groups = new Map<string, VaultFile[]>();\n\n for (const file of files) {\n const key = `${file.device}:${file.inode}`;\n const existing = groups.get(key);\n if (existing) {\n existing.push(file);\n } else {\n groups.set(key, [file]);\n }\n }\n\n const result: InodeGroup[] = [];\n\n for (const group of groups.values()) {\n if (group.length === 0) continue;\n\n // Sort: fewest path separators first, then shortest string\n const sorted = [...group].sort((a, b) => {\n const aDepth = (a.vaultRelPath.match(/\\//g) ?? []).length;\n const bDepth = (b.vaultRelPath.match(/\\//g) ?? []).length;\n if (aDepth !== bDepth) return aDepth - bDepth;\n return a.vaultRelPath.length - b.vaultRelPath.length;\n });\n\n const [canonical, ...aliases] = sorted as [VaultFile, ...VaultFile[]];\n result.push({ canonical, aliases });\n }\n\n return result;\n}\n","/**\n * Markdown link parser for vault files.\n *\n * Handles wikilinks ([[target]]), markdown links ([text](path)),\n * embeds (![[target]]), and frontmatter wikilinks.\n */\n\nimport type { ParsedLink } from \"./types.js\";\n\n/**\n * Parse all links from markdown content.\n *\n * Handles:\n * - Standard wikilinks: [[Target Note]]\n * - Aliased wikilinks: [[Target Note|Display Text]]\n * - Heading anchors: [[Target Note#Heading]] (stripped for resolution)\n * - Embeds: ![[Target Note]]\n * - Frontmatter wikilinks (YAML between --- delimiters)\n * - Markdown links: [text](path/to/note.md)\n * - Markdown embeds: ![alt](image.png)\n *\n * External URLs (http://, https://, mailto:, etc.) are excluded — only\n * relative paths are treated as vault links.\n *\n * @param content Raw markdown file content.\n * @returns Array of parsed links in document order.\n */\nexport function parseLinks(content: string): ParsedLink[] {\n const links: ParsedLink[] = [];\n const lines = content.split(\"\\n\");\n\n // Determine frontmatter range (YAML between opening and closing ---)\n let frontmatterEnd = 0;\n if (content.startsWith(\"---\")) {\n const closingIdx = content.indexOf(\"\\n---\", 3);\n if (closingIdx !== -1) {\n frontmatterEnd = content.slice(0, closingIdx + 4).split(\"\\n\").length - 1;\n }\n }\n\n // Regex for [[wikilinks]] and ![[embeds]]\n const wikilinkRe = /(!?)\\[\\[([^\\]]+?)\\]\\]/g;\n\n // Regex for markdown links [text](target) and embeds ![alt](target)\n const mdLinkRe = /(!)?\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx]!;\n const lineNumber = lineIdx + 1; // 1-indexed\n const isFrontmatter = lineIdx < frontmatterEnd;\n\n // --- Wikilinks ---\n wikilinkRe.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = wikilinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const inner = match[2]!;\n\n // Split on first | for alias\n const pipeIdx = inner.indexOf(\"|\");\n const beforePipe = pipeIdx === -1 ? inner : inner.slice(0, pipeIdx);\n const alias = pipeIdx === -1 ? null : inner.slice(pipeIdx + 1);\n\n // Strip heading anchor (everything after #)\n const hashIdx = beforePipe.indexOf(\"#\");\n const raw = hashIdx === -1 ? beforePipe.trim() : beforePipe.slice(0, hashIdx).trim();\n\n if (!raw) continue; // Skip links with empty targets (e.g. [[#Heading]])\n\n links.push({\n raw,\n alias: alias?.trim() ?? null,\n lineNumber,\n isEmbed: isEmbed && !isFrontmatter,\n isMdLink: false,\n });\n }\n\n // --- Markdown links --- (skip inside frontmatter)\n if (!isFrontmatter) {\n mdLinkRe.lastIndex = 0;\n while ((match = mdLinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const displayText = match[2]!;\n let target = match[3]!.trim();\n\n // Skip external URLs\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue;\n\n // Skip pure anchor links (#heading)\n if (target.startsWith(\"#\")) continue;\n\n // Strip heading anchor from target\n const hashIdx = target.indexOf(\"#\");\n if (hashIdx !== -1) target = target.slice(0, hashIdx);\n\n // URL-decode (Obsidian encodes spaces as %20 in md links)\n try {\n target = decodeURIComponent(target);\n } catch {\n // Malformed encoding — use as-is\n }\n\n // Strip .md extension for resolution (resolveWikilink adds it back)\n const raw = target.replace(/\\.md$/i, \"\").trim();\n if (!raw) continue;\n\n links.push({\n raw,\n alias: displayText || null,\n lineNumber,\n isEmbed,\n isMdLink: true,\n });\n }\n }\n }\n\n return links;\n}\n\n/** @deprecated Use {@link parseLinks} instead. */\nexport const parseWikilinks = parseLinks;\n","/**\n * Name index builder for Obsidian wikilink resolution.\n */\n\nimport { basename } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/**\n * Build a name lookup index for Obsidian wikilink resolution.\n *\n * Maps lowercase filename (without .md extension) to all vault-relative paths\n * that share that name. Includes both canonical paths and alias paths so that\n * wikilinks resolve regardless of which path the file is accessed through.\n */\nexport function buildNameIndex(files: VaultFile[]): Map<string, string[]> {\n const index = new Map<string, string[]>();\n\n for (const file of files) {\n const name = basename(file.vaultRelPath, \".md\").toLowerCase();\n const existing = index.get(name);\n if (existing) {\n existing.push(file.vaultRelPath);\n } else {\n index.set(name, [file.vaultRelPath]);\n }\n }\n\n return index;\n}\n","/**\n * Wikilink resolver using Obsidian's shortest-match algorithm.\n */\n\nimport { normalize, basename, dirname } from \"node:path\";\n\n/**\n * Resolve a wikilink target to a vault-relative path using Obsidian's rules.\n *\n * Resolution algorithm:\n * 1. If raw contains \"/\", attempt exact path match (with and without .md).\n * 2. Normalize: lowercase the raw target, strip .md extension.\n * 3. Look up in the name index (all files with that basename).\n * 4. If exactly one match, return it.\n * 5. If multiple matches, pick the one closest to the source file\n * (longest common directory prefix, then shortest overall path).\n * 6. If no matches, return null (dead link).\n *\n * @param raw The raw link target (heading-stripped, pipe-stripped).\n * @param nameIndex Map from lowercase basename-without-ext to vault paths.\n * @param sourcePath Vault-relative path of the file containing the link.\n * @returns Vault-relative path of the resolved target, or null.\n */\nexport function resolveWikilink(\n raw: string,\n nameIndex: Map<string, string[]>,\n sourcePath: string,\n): string | null {\n if (!raw) return null;\n\n // Case 1: path contains \"/\" — try exact match with and without .md\n if (raw.includes(\"/\")) {\n const normalized = normalize(raw);\n const normalizedMd = normalized.endsWith(\".md\") ? normalized : normalized + \".md\";\n\n // Check if any indexed path matches (case-insensitive for macOS compatibility)\n for (const [, paths] of nameIndex) {\n for (const p of paths) {\n if (p === normalizedMd || p === normalized) return p;\n if (p.toLowerCase() === normalizedMd.toLowerCase()) return p;\n }\n }\n // Fall through to name lookup in case the path prefix was wrong\n }\n\n // Normalize the raw target for name lookup.\n // Use the basename only — Obsidian resolves by filename, not full path.\n const rawBase = basename(raw)\n .replace(/\\.md$/i, \"\")\n .toLowerCase()\n .trim();\n\n if (!rawBase) return null;\n\n const candidates = nameIndex.get(rawBase);\n\n if (!candidates || candidates.length === 0) {\n return null; // Dead link\n }\n\n if (candidates.length === 1) {\n return candidates[0]!;\n }\n\n // Multiple matches — pick the one closest to the source file\n const sourceDir = dirname(sourcePath);\n\n let bestPath: string | null = null;\n let bestPrefixLen = -1;\n let bestPathLen = Infinity;\n\n for (const candidate of candidates) {\n const candidateDir = dirname(candidate);\n const prefixLen = commonPrefixLength(sourceDir, candidateDir);\n const pathLen = candidate.length;\n\n if (\n prefixLen > bestPrefixLen ||\n (prefixLen === bestPrefixLen && pathLen < bestPathLen)\n ) {\n bestPrefixLen = prefixLen;\n bestPathLen = pathLen;\n bestPath = candidate;\n }\n }\n\n return bestPath;\n}\n\n/**\n * Compute the length of the common prefix between two directory paths,\n * measured in path segments (not raw characters).\n *\n * Example: \"a/b/c\" and \"a/b/d\" → 2 (common: \"a\", \"b\")\n */\nfunction commonPrefixLength(a: string, b: string): number {\n if (a === \".\" && b === \".\") return 0;\n const aParts = a === \".\" ? [] : a.split(\"/\");\n const bParts = b === \".\" ? [] : b.split(\"/\");\n let count = 0;\n const len = Math.min(aParts.length, bParts.length);\n for (let i = 0; i < len; i++) {\n if (aParts[i] === bParts[i]) {\n count++;\n } else {\n break;\n }\n }\n return count;\n}\n","/**\n * Main vault indexing orchestrator.\n *\n * Indexes an entire Obsidian vault (or any markdown knowledge base), following\n * symlinks, deduplicating files by inode, parsing wikilinks, and computing\n * per-file health metrics (orphan detection, dead links).\n *\n * Key differences from the project indexer (indexer/sync.ts):\n * - Follows symbolic links (project indexer skips them)\n * - Deduplicates files with the same inode (same content reachable via multiple paths)\n * - Parses [[wikilinks]] and builds a directed link graph\n * - Resolves wikilinks using Obsidian's shortest-match algorithm\n * - Computes health metrics per file: inbound/outbound link counts, dead links, orphans\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport type {\n StorageBackend,\n VaultFileRow,\n VaultAliasRow,\n VaultLinkRow,\n VaultHealthRow,\n VaultNameEntry,\n ChunkRow,\n} from \"../../storage/interface.js\";\nimport { chunkMarkdown } from \"../chunker.js\";\nimport { sha256File, chunkId, yieldToEventLoop } from \"../indexer/helpers.js\";\nimport { walkVaultMdFiles } from \"./walk.js\";\nimport { deduplicateByInode } from \"./deduplicate.js\";\nimport { parseLinks } from \"./parse-links.js\";\nimport { buildNameIndex } from \"./name-index.js\";\nimport { resolveWikilink } from \"./resolve.js\";\nimport type { VaultIndexResult } from \"./types.js\";\n\n/** Number of files to process before yielding to the event loop. */\nconst VAULT_YIELD_EVERY = 1;\n\n/**\n * Index an entire Obsidian vault (or markdown knowledge base) using the\n * async StorageBackend interface.\n *\n * Steps:\n * 1. Walk vault root, following symlinks.\n * 2. Deduplicate by inode — each unique file is indexed once.\n * 3. Build a name index for wikilink resolution.\n * 4. For each canonical file:\n * a. SHA-256 hash for change detection — skip unchanged files.\n * b. Read content, chunk with chunkMarkdown().\n * c. Insert chunks into backend (memory_chunks and memory_fts).\n * d. Upsert vault_files row.\n * 5. Record aliases in vault_aliases.\n * 6. Rebuild vault_name_index table.\n * 7. Rebuild vault_links:\n * a. Parse [[wikilinks]] from each canonical file.\n * b. Resolve each link with resolveWikilink().\n * c. Insert into vault_links.\n * 8. Compute and upsert health metrics (vault_health).\n * 9. Return statistics.\n *\n * @param backend StorageBackend to write to.\n * @param vaultProjectId Registry project ID for the vault \"project\".\n * @param vaultRoot Absolute path to the vault root directory.\n */\nexport async function indexVault(\n backend: StorageBackend,\n vaultProjectId: number,\n vaultRoot: string,\n): Promise<VaultIndexResult> {\n const startTime = Date.now();\n\n const result: VaultIndexResult = {\n filesIndexed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n aliasesRecorded: 0,\n linksExtracted: 0,\n deadLinksFound: 0,\n orphansFound: 0,\n elapsed: 0,\n };\n\n // Step 1: Walk vault, collecting all .md files (follows symlinks)\n const allFiles = walkVaultMdFiles(vaultRoot);\n\n // Step 2: Deduplicate by inode\n const inodeGroups = deduplicateByInode(allFiles);\n\n // Step 3: Build name index (from all files including aliases, for resolution)\n const nameIndex = buildNameIndex(allFiles);\n\n // Step 4: Index each canonical file\n await yieldToEventLoop();\n let filesSinceYield = 0;\n\n for (const group of inodeGroups) {\n if (filesSinceYield >= VAULT_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const { canonical } = group;\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n result.filesSkipped++;\n continue;\n }\n\n const hash = sha256File(content);\n\n // Change detection: skip if hash is unchanged\n const existing = await backend.getVaultFile(canonical.vaultRelPath);\n if (existing?.hash === hash) {\n result.filesSkipped++;\n continue;\n }\n\n // Delete old chunks for this vault path\n await backend.deleteChunksForFile(vaultProjectId, canonical.vaultRelPath);\n\n // Chunk the content\n const chunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n // Extract title from first H1 heading or filename\n const titleMatch = /^#\\s+(.+)$/m.exec(content);\n const title = titleMatch\n ? titleMatch[1]!.trim()\n : basename(canonical.vaultRelPath, \".md\");\n\n // Build chunk rows\n const chunkRows: ChunkRow[] = [];\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(\n vaultProjectId,\n canonical.vaultRelPath,\n i,\n chunk.startLine,\n chunk.endLine,\n );\n chunkRows.push({\n id,\n projectId: vaultProjectId,\n source: \"vault\",\n tier: \"topic\",\n path: canonical.vaultRelPath,\n startLine: chunk.startLine,\n endLine: chunk.endLine,\n hash: chunk.hash,\n text: chunk.text,\n updatedAt,\n });\n }\n\n if (chunkRows.length > 0) {\n await backend.insertChunks(chunkRows);\n }\n\n // Upsert vault file record\n const vaultFileRow: VaultFileRow = {\n vaultPath: canonical.vaultRelPath,\n inode: canonical.inode,\n device: canonical.device,\n hash,\n title,\n indexedAt: updatedAt,\n };\n await backend.upsertVaultFile(vaultFileRow);\n\n result.filesIndexed++;\n result.chunksCreated += chunks.length;\n }\n\n // Step 5: Record aliases in vault_aliases\n await yieldToEventLoop();\n\n const allAliases: VaultAliasRow[] = [];\n for (const group of inodeGroups) {\n for (const alias of group.aliases) {\n allAliases.push({\n vaultPath: alias.vaultRelPath,\n canonicalPath: group.canonical.vaultRelPath,\n inode: alias.inode,\n device: alias.device,\n });\n result.aliasesRecorded++;\n }\n }\n\n const canonicalPaths = new Set(inodeGroups.map((g) => g.canonical.vaultRelPath));\n for (const canonPath of canonicalPaths) {\n await backend.deleteVaultAliases(canonPath);\n }\n if (allAliases.length > 0) {\n await backend.upsertVaultAliases(allAliases);\n }\n\n // Step 6: Rebuild vault_name_index\n await yieldToEventLoop();\n\n const nameEntries: VaultNameEntry[] = [];\n for (const [name, paths] of nameIndex) {\n for (const path of paths) {\n nameEntries.push({ name, vaultPath: path });\n }\n }\n await backend.replaceNameIndex(nameEntries);\n\n // Step 7: Rebuild vault_links\n await yieldToEventLoop();\n\n const linkRows: VaultLinkRow[] = [];\n const allSourcePaths: string[] = [];\n\n let linkParseYield = 0;\n for (const group of inodeGroups) {\n if (linkParseYield++ % VAULT_YIELD_EVERY === 0) {\n await yieldToEventLoop();\n }\n\n const { canonical } = group;\n allSourcePaths.push(canonical.vaultRelPath);\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n continue;\n }\n\n const parsedLinks = parseLinks(content);\n for (const link of parsedLinks) {\n const target = resolveWikilink(link.raw, nameIndex, canonical.vaultRelPath);\n let linkType: string;\n if (link.isMdLink) {\n linkType = link.isEmbed ? \"md-embed\" : \"md-link\";\n } else {\n linkType = link.isEmbed ? \"embed\" : \"wikilink\";\n }\n linkRows.push({\n sourcePath: canonical.vaultRelPath,\n targetRaw: link.raw,\n targetPath: target,\n linkType,\n lineNumber: link.lineNumber,\n confidence: \"EXTRACTED\",\n });\n }\n }\n\n // Replace all links for all sources in batches of 500\n const LINK_BATCH_SIZE = 500;\n for (let i = 0; i < allSourcePaths.length; i += LINK_BATCH_SIZE) {\n const batchSources = allSourcePaths.slice(i, i + LINK_BATCH_SIZE);\n const batchLinks = linkRows.filter((r) => batchSources.includes(r.sourcePath));\n await backend.replaceLinksForSources(batchSources, batchLinks);\n await yieldToEventLoop();\n }\n\n result.linksExtracted = linkRows.length;\n result.deadLinksFound = linkRows.filter((r) => r.targetPath === null).length;\n\n // Step 8: Compute and upsert vault_health metrics\n await yieldToEventLoop();\n\n const outboundMap = new Map<string, number>();\n const deadMap = new Map<string, number>();\n const inboundMap = new Map<string, number>();\n\n for (const row of linkRows) {\n outboundMap.set(row.sourcePath, (outboundMap.get(row.sourcePath) ?? 0) + 1);\n if (row.targetPath === null) {\n deadMap.set(row.sourcePath, (deadMap.get(row.sourcePath) ?? 0) + 1);\n } else {\n inboundMap.set(row.targetPath, (inboundMap.get(row.targetPath) ?? 0) + 1);\n }\n }\n\n const computedAt = Date.now();\n let orphanCount = 0;\n\n const HEALTH_BATCH_SIZE = 500;\n for (let i = 0; i < inodeGroups.length; i += HEALTH_BATCH_SIZE) {\n const batch = inodeGroups.slice(i, i + HEALTH_BATCH_SIZE);\n const healthRows: VaultHealthRow[] = batch.map((group) => {\n const path = group.canonical.vaultRelPath;\n const inbound = inboundMap.get(path) ?? 0;\n const outbound = outboundMap.get(path) ?? 0;\n const dead = deadMap.get(path) ?? 0;\n const isOrphan = inbound === 0;\n if (isOrphan) orphanCount++;\n return {\n vaultPath: path,\n inboundCount: inbound,\n outboundCount: outbound,\n deadLinkCount: dead,\n isOrphan,\n computedAt,\n };\n });\n await backend.upsertVaultHealth(healthRows);\n await yieldToEventLoop();\n }\n\n result.orphansFound = orphanCount;\n result.elapsed = Date.now() - startTime;\n\n return result;\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,kBAAkB;;AAGxB,MAAM,kBAAkB;;;;;AAMxB,MAAM,kBAAkB,IAAI,IAAI;CAE9B;CAEA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAEA;CACA;CAEA;CACA;CACD,CAAC;;;;;;;;;;;;;;;AAgBF,SAAgB,iBACd,WACA,MACa;CACb,MAAM,WAAW,MAAM,YAAY;CACnC,MAAM,WAAW,MAAM,YAAY;CAEnC,MAAM,UAAuB,EAAE;CAC/B,MAAM,8BAAc,IAAI,KAAa;CAErC,SAAS,KAAK,KAAa,OAAqB;AAC9C,MAAI,QAAQ,UAAU,SAAU;AAChC,MAAI,QAAQ,SAAU;EAGtB,IAAI;AACJ,MAAI;AACF,aAAU,SAAS,IAAI;UACjB;AACN;;EAGF,MAAM,SAAS,GAAG,QAAQ,IAAI,GAAG,QAAQ;AACzC,MAAI,YAAY,IAAI,OAAO,CAAE;AAC7B,cAAY,IAAI,OAAO;EAEvB,IAAI;AACJ,MAAI;AACF,aAAU,YAAY,KAAK;IAAE,eAAe;IAAM,UAAU;IAAQ,CAAC;UAC/D;AACN;;AAGF,OAAK,MAAM,SAAS,SAAS;AAC3B,OAAI,QAAQ,UAAU,SAAU;AAChC,OAAI,gBAAgB,IAAI,MAAM,KAAK,CAAE;GAErC,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK;AAElC,OAAI,MAAM,gBAAgB,EAAE;IAE1B,IAAI;AACJ,QAAI;AACF,kBAAa,SAAS,KAAK;YACrB;AACN;;AAGF,QAAI,WAAW,aAAa,EAC1B;SAAI,CAAC,gBAAgB,IAAI,MAAM,KAAK,CAClC,MAAK,MAAM,QAAQ,EAAE;eAEd,WAAW,QAAQ,IAAI,MAAM,KAAK,SAAS,MAAM,CAC1D,SAAQ,KAAK;KACX,SAAS;KACT,cAAc,SAAS,WAAW,KAAK;KACvC,OAAO,WAAW;KAClB,QAAQ,WAAW;KACpB,CAAC;cAEK,MAAM,aAAa,CAC5B,MAAK,MAAM,QAAQ,EAAE;YACZ,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,MAAM,EAAE;IACvD,IAAI;AACJ,QAAI;AACF,gBAAW,SAAS,KAAK;YACnB;AACN;;AAEF,YAAQ,KAAK;KACX,SAAS;KACT,cAAc,SAAS,WAAW,KAAK;KACvC,OAAO,SAAS;KAChB,QAAQ,SAAS;KAClB,CAAC;;;;AAKR,KAAI,WAAW,UAAU,CACvB,MAAK,WAAW,EAAE;AAGpB,QAAO;;;;;;;;;;;;AC/HT,SAAgB,mBAAmB,OAAkC;CACnE,MAAM,yBAAS,IAAI,KAA0B;AAE7C,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,GAAG,KAAK,OAAO,GAAG,KAAK;EACnC,MAAM,WAAW,OAAO,IAAI,IAAI;AAChC,MAAI,SACF,UAAS,KAAK,KAAK;MAEnB,QAAO,IAAI,KAAK,CAAC,KAAK,CAAC;;CAI3B,MAAM,SAAuB,EAAE;AAE/B,MAAK,MAAM,SAAS,OAAO,QAAQ,EAAE;AACnC,MAAI,MAAM,WAAW,EAAG;EAUxB,MAAM,CAAC,WAAW,GAAG,WAPN,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM;GACvC,MAAM,UAAU,EAAE,aAAa,MAAM,MAAM,IAAI,EAAE,EAAE;GACnD,MAAM,UAAU,EAAE,aAAa,MAAM,MAAM,IAAI,EAAE,EAAE;AACnD,OAAI,WAAW,OAAQ,QAAO,SAAS;AACvC,UAAO,EAAE,aAAa,SAAS,EAAE,aAAa;IAC9C;AAGF,SAAO,KAAK;GAAE;GAAW;GAAS,CAAC;;AAGrC,QAAO;;;;;;;;;;;;;;;;;;;;;;;AChBT,SAAgB,WAAW,SAA+B;CACxD,MAAM,QAAsB,EAAE;CAC9B,MAAM,QAAQ,QAAQ,MAAM,KAAK;CAGjC,IAAI,iBAAiB;AACrB,KAAI,QAAQ,WAAW,MAAM,EAAE;EAC7B,MAAM,aAAa,QAAQ,QAAQ,SAAS,EAAE;AAC9C,MAAI,eAAe,GACjB,kBAAiB,QAAQ,MAAM,GAAG,aAAa,EAAE,CAAC,MAAM,KAAK,CAAC,SAAS;;CAK3E,MAAM,aAAa;CAGnB,MAAM,WAAW;AAEjB,MAAK,IAAI,UAAU,GAAG,UAAU,MAAM,QAAQ,WAAW;EACvD,MAAM,OAAO,MAAM;EACnB,MAAM,aAAa,UAAU;EAC7B,MAAM,gBAAgB,UAAU;AAGhC,aAAW,YAAY;EACvB,IAAI;AACJ,UAAQ,QAAQ,WAAW,KAAK,KAAK,MAAM,MAAM;GAC/C,MAAM,UAAU,MAAM,OAAO;GAC7B,MAAM,QAAQ,MAAM;GAGpB,MAAM,UAAU,MAAM,QAAQ,IAAI;GAClC,MAAM,aAAa,YAAY,KAAK,QAAQ,MAAM,MAAM,GAAG,QAAQ;GACnE,MAAM,QAAQ,YAAY,KAAK,OAAO,MAAM,MAAM,UAAU,EAAE;GAG9D,MAAM,UAAU,WAAW,QAAQ,IAAI;GACvC,MAAM,MAAM,YAAY,KAAK,WAAW,MAAM,GAAG,WAAW,MAAM,GAAG,QAAQ,CAAC,MAAM;AAEpF,OAAI,CAAC,IAAK;AAEV,SAAM,KAAK;IACT;IACA,OAAO,OAAO,MAAM,IAAI;IACxB;IACA,SAAS,WAAW,CAAC;IACrB,UAAU;IACX,CAAC;;AAIJ,MAAI,CAAC,eAAe;AAClB,YAAS,YAAY;AACrB,WAAQ,QAAQ,SAAS,KAAK,KAAK,MAAM,MAAM;IAC7C,MAAM,UAAU,MAAM,OAAO;IAC7B,MAAM,cAAc,MAAM;IAC1B,IAAI,SAAS,MAAM,GAAI,MAAM;AAG7B,QAAI,uBAAuB,KAAK,OAAO,CAAE;AAGzC,QAAI,OAAO,WAAW,IAAI,CAAE;IAG5B,MAAM,UAAU,OAAO,QAAQ,IAAI;AACnC,QAAI,YAAY,GAAI,UAAS,OAAO,MAAM,GAAG,QAAQ;AAGrD,QAAI;AACF,cAAS,mBAAmB,OAAO;YAC7B;IAKR,MAAM,MAAM,OAAO,QAAQ,UAAU,GAAG,CAAC,MAAM;AAC/C,QAAI,CAAC,IAAK;AAEV,UAAM,KAAK;KACT;KACA,OAAO,eAAe;KACtB;KACA;KACA,UAAU;KACX,CAAC;;;;AAKR,QAAO;;;;;;;;;;;;;;;ACxGT,SAAgB,eAAe,OAA2C;CACxE,MAAM,wBAAQ,IAAI,KAAuB;AAEzC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,SAAS,KAAK,cAAc,MAAM,CAAC,aAAa;EAC7D,MAAM,WAAW,MAAM,IAAI,KAAK;AAChC,MAAI,SACF,UAAS,KAAK,KAAK,aAAa;MAEhC,OAAM,IAAI,MAAM,CAAC,KAAK,aAAa,CAAC;;AAIxC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;ACJT,SAAgB,gBACd,KACA,WACA,YACe;AACf,KAAI,CAAC,IAAK,QAAO;AAGjB,KAAI,IAAI,SAAS,IAAI,EAAE;EACrB,MAAM,aAAa,UAAU,IAAI;EACjC,MAAM,eAAe,WAAW,SAAS,MAAM,GAAG,aAAa,aAAa;AAG5E,OAAK,MAAM,GAAG,UAAU,UACtB,MAAK,MAAM,KAAK,OAAO;AACrB,OAAI,MAAM,gBAAgB,MAAM,WAAY,QAAO;AACnD,OAAI,EAAE,aAAa,KAAK,aAAa,aAAa,CAAE,QAAO;;;CAQjE,MAAM,UAAU,SAAS,IAAI,CAC1B,QAAQ,UAAU,GAAG,CACrB,aAAa,CACb,MAAM;AAET,KAAI,CAAC,QAAS,QAAO;CAErB,MAAM,aAAa,UAAU,IAAI,QAAQ;AAEzC,KAAI,CAAC,cAAc,WAAW,WAAW,EACvC,QAAO;AAGT,KAAI,WAAW,WAAW,EACxB,QAAO,WAAW;CAIpB,MAAM,YAAY,QAAQ,WAAW;CAErC,IAAI,WAA0B;CAC9B,IAAI,gBAAgB;CACpB,IAAI,cAAc;AAElB,MAAK,MAAM,aAAa,YAAY;EAElC,MAAM,YAAY,mBAAmB,WADhB,QAAQ,UAAU,CACsB;EAC7D,MAAM,UAAU,UAAU;AAE1B,MACE,YAAY,iBACX,cAAc,iBAAiB,UAAU,aAC1C;AACA,mBAAgB;AAChB,iBAAc;AACd,cAAW;;;AAIf,QAAO;;;;;;;;AAST,SAAS,mBAAmB,GAAW,GAAmB;AACxD,KAAI,MAAM,OAAO,MAAM,IAAK,QAAO;CACnC,MAAM,SAAS,MAAM,MAAM,EAAE,GAAG,EAAE,MAAM,IAAI;CAC5C,MAAM,SAAS,MAAM,MAAM,EAAE,GAAG,EAAE,MAAM,IAAI;CAC5C,IAAI,QAAQ;CACZ,MAAM,MAAM,KAAK,IAAI,OAAO,QAAQ,OAAO,OAAO;AAClD,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,IACvB,KAAI,OAAO,OAAO,OAAO,GACvB;KAEA;AAGJ,QAAO;;;;;;;;;;;;;;;;;;;;ACxET,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4B1B,eAAsB,WACpB,SACA,gBACA,WAC2B;CAC3B,MAAM,YAAY,KAAK,KAAK;CAE5B,MAAM,SAA2B;EAC/B,cAAc;EACd,eAAe;EACf,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,gBAAgB;EAChB,cAAc;EACd,SAAS;EACV;CAGD,MAAM,WAAW,iBAAiB,UAAU;CAG5C,MAAM,cAAc,mBAAmB,SAAS;CAGhD,MAAM,YAAY,eAAe,SAAS;AAG1C,OAAM,kBAAkB;CACxB,IAAI,kBAAkB;AAEtB,MAAK,MAAM,SAAS,aAAa;AAC/B,MAAI,mBAAmB,mBAAmB;AACxC,SAAM,kBAAkB;AACxB,qBAAkB;;AAEpB;EAEA,MAAM,EAAE,cAAc;EAEtB,IAAI;AACJ,MAAI;AACF,aAAU,aAAa,UAAU,SAAS,OAAO;UAC3C;AACN,UAAO;AACP;;EAGF,MAAM,OAAO,WAAW,QAAQ;AAIhC,OADiB,MAAM,QAAQ,aAAa,UAAU,aAAa,GACrD,SAAS,MAAM;AAC3B,UAAO;AACP;;AAIF,QAAM,QAAQ,oBAAoB,gBAAgB,UAAU,aAAa;EAGzE,MAAM,SAAS,cAAc,QAAQ;EACrC,MAAM,YAAY,KAAK,KAAK;EAG5B,MAAM,aAAa,cAAc,KAAK,QAAQ;EAC9C,MAAM,QAAQ,aACV,WAAW,GAAI,MAAM,GACrB,SAAS,UAAU,cAAc,MAAM;EAG3C,MAAM,YAAwB,EAAE;AAChC,OAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;GACtC,MAAM,QAAQ,OAAO;GACrB,MAAM,KAAK,QACT,gBACA,UAAU,cACV,GACA,MAAM,WACN,MAAM,QACP;AACD,aAAU,KAAK;IACb;IACA,WAAW;IACX,QAAQ;IACR,MAAM;IACN,MAAM,UAAU;IAChB,WAAW,MAAM;IACjB,SAAS,MAAM;IACf,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ;IACD,CAAC;;AAGJ,MAAI,UAAU,SAAS,EACrB,OAAM,QAAQ,aAAa,UAAU;EAIvC,MAAM,eAA6B;GACjC,WAAW,UAAU;GACrB,OAAO,UAAU;GACjB,QAAQ,UAAU;GAClB;GACA;GACA,WAAW;GACZ;AACD,QAAM,QAAQ,gBAAgB,aAAa;AAE3C,SAAO;AACP,SAAO,iBAAiB,OAAO;;AAIjC,OAAM,kBAAkB;CAExB,MAAM,aAA8B,EAAE;AACtC,MAAK,MAAM,SAAS,YAClB,MAAK,MAAM,SAAS,MAAM,SAAS;AACjC,aAAW,KAAK;GACd,WAAW,MAAM;GACjB,eAAe,MAAM,UAAU;GAC/B,OAAO,MAAM;GACb,QAAQ,MAAM;GACf,CAAC;AACF,SAAO;;CAIX,MAAM,iBAAiB,IAAI,IAAI,YAAY,KAAK,MAAM,EAAE,UAAU,aAAa,CAAC;AAChF,MAAK,MAAM,aAAa,eACtB,OAAM,QAAQ,mBAAmB,UAAU;AAE7C,KAAI,WAAW,SAAS,EACtB,OAAM,QAAQ,mBAAmB,WAAW;AAI9C,OAAM,kBAAkB;CAExB,MAAM,cAAgC,EAAE;AACxC,MAAK,MAAM,CAAC,MAAM,UAAU,UAC1B,MAAK,MAAM,QAAQ,MACjB,aAAY,KAAK;EAAE;EAAM,WAAW;EAAM,CAAC;AAG/C,OAAM,QAAQ,iBAAiB,YAAY;AAG3C,OAAM,kBAAkB;CAExB,MAAM,WAA2B,EAAE;CACnC,MAAM,iBAA2B,EAAE;CAEnC,IAAI,iBAAiB;AACrB,MAAK,MAAM,SAAS,aAAa;AAC/B,MAAI,mBAAmB,sBAAsB,EAC3C,OAAM,kBAAkB;EAG1B,MAAM,EAAE,cAAc;AACtB,iBAAe,KAAK,UAAU,aAAa;EAE3C,IAAI;AACJ,MAAI;AACF,aAAU,aAAa,UAAU,SAAS,OAAO;UAC3C;AACN;;EAGF,MAAM,cAAc,WAAW,QAAQ;AACvC,OAAK,MAAM,QAAQ,aAAa;GAC9B,MAAM,SAAS,gBAAgB,KAAK,KAAK,WAAW,UAAU,aAAa;GAC3E,IAAI;AACJ,OAAI,KAAK,SACP,YAAW,KAAK,UAAU,aAAa;OAEvC,YAAW,KAAK,UAAU,UAAU;AAEtC,YAAS,KAAK;IACZ,YAAY,UAAU;IACtB,WAAW,KAAK;IAChB,YAAY;IACZ;IACA,YAAY,KAAK;IACjB,YAAY;IACb,CAAC;;;CAKN,MAAM,kBAAkB;AACxB,MAAK,IAAI,IAAI,GAAG,IAAI,eAAe,QAAQ,KAAK,iBAAiB;EAC/D,MAAM,eAAe,eAAe,MAAM,GAAG,IAAI,gBAAgB;EACjE,MAAM,aAAa,SAAS,QAAQ,MAAM,aAAa,SAAS,EAAE,WAAW,CAAC;AAC9E,QAAM,QAAQ,uBAAuB,cAAc,WAAW;AAC9D,QAAM,kBAAkB;;AAG1B,QAAO,iBAAiB,SAAS;AACjC,QAAO,iBAAiB,SAAS,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;AAGtE,OAAM,kBAAkB;CAExB,MAAM,8BAAc,IAAI,KAAqB;CAC7C,MAAM,0BAAU,IAAI,KAAqB;CACzC,MAAM,6BAAa,IAAI,KAAqB;AAE5C,MAAK,MAAM,OAAO,UAAU;AAC1B,cAAY,IAAI,IAAI,aAAa,YAAY,IAAI,IAAI,WAAW,IAAI,KAAK,EAAE;AAC3E,MAAI,IAAI,eAAe,KACrB,SAAQ,IAAI,IAAI,aAAa,QAAQ,IAAI,IAAI,WAAW,IAAI,KAAK,EAAE;MAEnE,YAAW,IAAI,IAAI,aAAa,WAAW,IAAI,IAAI,WAAW,IAAI,KAAK,EAAE;;CAI7E,MAAM,aAAa,KAAK,KAAK;CAC7B,IAAI,cAAc;CAElB,MAAM,oBAAoB;AAC1B,MAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK,mBAAmB;EAE9D,MAAM,aADQ,YAAY,MAAM,GAAG,IAAI,kBAAkB,CACd,KAAK,UAAU;GACxD,MAAM,OAAO,MAAM,UAAU;GAC7B,MAAM,UAAU,WAAW,IAAI,KAAK,IAAI;GACxC,MAAM,WAAW,YAAY,IAAI,KAAK,IAAI;GAC1C,MAAM,OAAO,QAAQ,IAAI,KAAK,IAAI;GAClC,MAAM,WAAW,YAAY;AAC7B,OAAI,SAAU;AACd,UAAO;IACL,WAAW;IACX,cAAc;IACd,eAAe;IACf,eAAe;IACf;IACA;IACD;IACD;AACF,QAAM,QAAQ,kBAAkB,WAAW;AAC3C,QAAM,kBAAkB;;AAG1B,QAAO,eAAe;AACtB,QAAO,UAAU,KAAK,KAAK,GAAG;AAE9B,QAAO"}
@@ -1,6 +1,6 @@
1
1
  import { a as generateEmbedding, n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-DGRAPAYb.mjs";
2
- import { t as zettelThemes } from "./themes-BvYF0W8T.mjs";
3
- import { n as saveQueryResult } from "./query-feedback-Dv43XKHM.mjs";
2
+ import { t as zettelThemes } from "./themes-9jxFn3Rf.mjs";
3
+ import { n as saveQueryResult } from "./query-feedback-CQSumXDy.mjs";
4
4
  import { basename, dirname } from "node:path";
5
5
 
6
6
  //#region src/zettelkasten/explore.ts
@@ -1060,4 +1060,4 @@ async function zettelCommunities(backend, opts) {
1060
1060
 
1061
1061
  //#endregion
1062
1062
  export { zettelCommunities, zettelConverse, zettelExplore, zettelGodNotes, zettelHealth, zettelSuggest, zettelSurprise, zettelThemes };
1063
- //# sourceMappingURL=zettelkasten-DhBKZQHF.mjs.map
1063
+ //# sourceMappingURL=zettelkasten-BdaMzTGQ.mjs.map