@tekmidian/pai 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -9
- package/dist/{auto-route-B5MSUJZK.mjs → auto-route-BG6I_4B1.mjs} +2 -2
- package/dist/{auto-route-B5MSUJZK.mjs.map → auto-route-BG6I_4B1.mjs.map} +1 -1
- package/dist/cli/index.mjs +103 -18
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{config-B4brrHHE.mjs → config-Cf92lGX_.mjs} +17 -3
- package/dist/config-Cf92lGX_.mjs.map +1 -0
- package/dist/daemon/index.mjs +6 -6
- package/dist/{daemon-s868Paua.mjs → daemon-D9evGlgR.mjs} +11 -11
- package/dist/{daemon-s868Paua.mjs.map → daemon-D9evGlgR.mjs.map} +1 -1
- package/dist/daemon-mcp/index.mjs +5 -3
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detect-CdaA48EI.mjs → detect-BU3Nx_2L.mjs} +1 -1
- package/dist/{detect-CdaA48EI.mjs.map → detect-BU3Nx_2L.mjs.map} +1 -1
- package/dist/{factory-CeXQzlwn.mjs → factory-Bzcy70G9.mjs} +3 -3
- package/dist/{factory-CeXQzlwn.mjs.map → factory-Bzcy70G9.mjs.map} +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/{indexer-CKQcgKsz.mjs → indexer-CMPOiY1r.mjs} +22 -1
- package/dist/{indexer-CKQcgKsz.mjs.map → indexer-CMPOiY1r.mjs.map} +1 -1
- package/dist/{indexer-backend-DQO-FqAI.mjs → indexer-backend-CIMXedqk.mjs} +26 -9
- package/dist/indexer-backend-CIMXedqk.mjs.map +1 -0
- package/dist/{ipc-client-CgSpwHDC.mjs → ipc-client-Bjg_a1dc.mjs} +1 -1
- package/dist/{ipc-client-CgSpwHDC.mjs.map → ipc-client-Bjg_a1dc.mjs.map} +1 -1
- package/dist/mcp/index.mjs +7 -3
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{postgres-CIxeqf_n.mjs → postgres-FXrHDPcE.mjs} +36 -13
- package/dist/postgres-FXrHDPcE.mjs.map +1 -0
- package/dist/{sqlite-CymLKiDE.mjs → sqlite-WWBq7_2C.mjs} +18 -1
- package/dist/{sqlite-CymLKiDE.mjs.map → sqlite-WWBq7_2C.mjs.map} +1 -1
- package/dist/{tools-Dx7GjOHd.mjs → tools-DV_lsiCc.mjs} +15 -13
- package/dist/tools-DV_lsiCc.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/config-B4brrHHE.mjs.map +0 -1
- package/dist/indexer-backend-DQO-FqAI.mjs.map +0 -1
- package/dist/postgres-CIxeqf_n.mjs.map +0 -1
- package/dist/tools-Dx7GjOHd.mjs.map +0 -1
package/dist/mcp/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["z\n .string","z\n .boolean","z\n .array","z.enum","z\n .number","z\n .enum","z.string","z\n .record","z.unknown"],"sources":["../../src/mcp/server.ts","../../src/mcp/index.ts"],"sourcesContent":["/**\n * PAI Knowledge OS — MCP Server (Phase 3)\n *\n * Exposes PAI registry and memory as MCP tools callable by Claude Code.\n *\n * Tools:\n * memory_search — BM25 search across indexed memory/notes\n * memory_get — Read a specific file or lines from a project\n * project_info — Get details for a project (by slug or current dir)\n * project_list — List projects with optional filters\n * session_list — List sessions for a project\n * registry_search — Full-text search over project slugs/names/paths\n * project_detect — Detect which project a path belongs to\n * project_health — Audit all projects for moved/deleted directories\n * session_route — Auto-route session to project (path/marker/topic)\n *\n * NOTE: All tool logic lives in tools.ts (shared with the daemon).\n * This file wires MCP schema definitions to those pure functions.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { openRegistry } from \"../registry/db.js\";\nimport { openFederation } from \"../memory/db.js\";\nimport {\n toolMemorySearch,\n toolMemoryGet,\n toolProjectInfo,\n toolProjectList,\n toolSessionList,\n toolRegistrySearch,\n toolProjectDetect,\n toolProjectHealth,\n toolNotificationConfig,\n toolTopicDetect,\n toolSessionRoute,\n toolZettelExplore,\n toolZettelHealth,\n toolZettelSurprise,\n toolZettelSuggest,\n toolZettelConverse,\n toolZettelThemes,\n} from \"./tools.js\";\n\n// ---------------------------------------------------------------------------\n// Database singletons (opened lazily, once per MCP server process)\n// ---------------------------------------------------------------------------\n\nlet _registryDb: ReturnType<typeof openRegistry> | null = null;\nlet _federationDb: ReturnType<typeof openFederation> | null = null;\n\nfunction getRegistryDb() {\n if (!_registryDb) _registryDb = openRegistry();\n return _registryDb;\n}\n\nfunction getFederationDb() {\n if (!_federationDb) _federationDb = openFederation();\n return _federationDb;\n}\n\n// ---------------------------------------------------------------------------\n// MCP server startup\n// ---------------------------------------------------------------------------\n\nexport async function startMcpServer(): Promise<void> {\n const server = new McpServer({\n name: \"pai\",\n version: \"0.1.0\",\n });\n\n // -------------------------------------------------------------------------\n // Tool: memory_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_search\",\n [\n \"Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.\",\n \"\",\n \"Use this BEFORE answering questions about past work, decisions, dates, people,\",\n \"preferences, project status, todos, technical choices, or anything that might\",\n \"have been recorded in session notes or memory files.\",\n \"\",\n \"Modes:\",\n \" keyword — BM25 full-text search (default, fast, no embeddings required)\",\n \" semantic — Cosine similarity over vector embeddings (requires prior embed run)\",\n \" hybrid — Normalized combination of BM25 + cosine (best quality)\",\n \"\",\n \"Reranking is ON by default — results are re-scored with a cross-encoder model for better relevance.\",\n \"Set rerank=false to skip reranking (faster but less accurate ordering).\",\n \"\",\n \"Recency boost optionally down-weights older results (recency_boost=90 means scores halve every 90 days).\",\n \"\",\n \"Returns ranked snippets with project slug, file path, line range, and score.\",\n \"Higher score = more relevant.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\"Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance.\"),\n project: z\n .string()\n .optional()\n .describe(\n \"Scope search to a single project by slug. Omit to search all projects.\"\n ),\n all_projects: z\n .boolean()\n .optional()\n .describe(\n \"Explicitly search all projects (default behaviour when project is omitted).\"\n ),\n sources: z\n .array(z.enum([\"memory\", \"notes\"]))\n .optional()\n .describe(\"Restrict to specific source types: 'memory' or 'notes'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum results to return. Default: 10.\"),\n mode: z\n .enum([\"keyword\", \"semantic\", \"hybrid\"])\n .optional()\n .describe(\n \"Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).\"\n ),\n rerank: z\n .boolean()\n .optional()\n .describe(\n \"Rerank results using a cross-encoder model for better relevance. Default: true. Set to false to skip reranking for faster but less accurate results.\"\n ),\n recency_boost: z\n .number()\n .int()\n .min(0)\n .max(365)\n .optional()\n .describe(\n \"Apply recency boost: score halves every N days. 0 = off (default). Recommended: 90 (3 months). Applied after reranking.\"\n ),\n },\n async (args) => {\n const result = await toolMemorySearch(\n getRegistryDb(),\n getFederationDb(),\n { ...args, recencyBoost: args.recency_boost }\n );\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: memory_get\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_get\",\n [\n \"Read the content of a specific file from a registered PAI project.\",\n \"\",\n \"Use this to read a full memory file, session note, or document after finding\",\n \"it via memory_search. Optionally restrict to a line range.\",\n \"\",\n \"The path must be a relative path within the project root (no ../ traversal).\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .describe(\"Project slug identifying which project's files to read from.\"),\n path: z\n .string()\n .describe(\n \"Relative path within the project root (e.g. 'Notes/0001 - 2026-01-01 - Example.md').\"\n ),\n from: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Starting line number (1-based, inclusive). Default: 1.\"),\n lines: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Number of lines to return. Default: entire file.\"),\n },\n async (args) => {\n const result = toolMemoryGet(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_info\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_info\",\n [\n \"Get detailed information about a PAI registered project.\",\n \"\",\n \"Use this to look up a project's root path, type, status, tags, session count,\",\n \"and last active date. If no slug is provided, attempts to detect the current\",\n \"project from the caller's working directory.\",\n ].join(\"\\n\"),\n {\n slug: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => {\n const result = toolProjectInfo(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_list\",\n [\n \"List registered PAI projects with optional filters.\",\n \"\",\n \"Use this to browse all known projects, find projects by status or tag,\",\n \"or get a quick overview of the PAI registry.\",\n ].join(\"\\n\"),\n {\n status: z\n .enum([\"active\", \"archived\", \"migrating\"])\n .optional()\n .describe(\"Filter by project status. Default: all statuses.\"),\n tag: z\n .string()\n .optional()\n .describe(\"Filter by tag name (exact match).\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum number of projects to return. Default: 50.\"),\n },\n async (args) => {\n const result = toolProjectList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_list\",\n [\n \"List session notes for a PAI project.\",\n \"\",\n \"Use this to find what sessions exist for a project, see their dates and titles,\",\n \"and identify specific session notes to read via memory_get.\",\n ].join(\"\\n\"),\n {\n project: z.string().describe(\"Project slug to list sessions for.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum sessions to return. Default: 10 (most recent first).\"),\n status: z\n .enum([\"open\", \"completed\", \"compacted\"])\n .optional()\n .describe(\"Filter by session status.\"),\n },\n async (args) => {\n const result = toolSessionList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: registry_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"registry_search\",\n [\n \"Search PAI project registry by slug, display name, or path.\",\n \"\",\n \"Use this to find the slug for a project when you know its name or path,\",\n \"or to check if a project is registered. Returns matching project entries.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\n \"Search term matched against project slugs, display names, and root paths (case-insensitive substring match).\"\n ),\n },\n async (args) => {\n const result = toolRegistrySearch(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_detect\",\n [\n \"Detect which registered PAI project a filesystem path belongs to.\",\n \"\",\n \"Use this at session start to auto-identify the current project from the\",\n \"working directory, or to map any path back to its registered project.\",\n \"\",\n \"Returns: slug, display_name, root_path, type, status, match_type (exact|parent),\",\n \"relative_path (if the given path is inside a project), and session stats.\",\n \"\",\n \"match_type 'exact' means the path IS the project root.\",\n \"match_type 'parent' means the path is a subdirectory of the project root.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Absolute path to detect project for. Defaults to the MCP server's process.cwd().\"\n ),\n },\n async (args) => {\n const result = toolProjectDetect(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_health\",\n [\n \"Audit all registered PAI projects to find moved or deleted directories.\",\n \"\",\n \"Returns a JSON report categorising every project as:\",\n \" active — root_path exists on disk\",\n \" stale — root_path missing, but a directory with the same name was found nearby\",\n \" dead — root_path missing, no candidate found\",\n \"\",\n \"Use this to diagnose orphaned sessions or missing project paths.\",\n ].join(\"\\n\"),\n {\n category: z\n .enum([\"active\", \"stale\", \"dead\", \"all\"])\n .optional()\n .describe(\"Filter results to a specific health category. Default: all.\"),\n },\n async (args) => {\n const result = await toolProjectHealth(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: notification_config\n // -------------------------------------------------------------------------\n\n server.tool(\n \"notification_config\",\n [\n \"Query or update the PAI unified notification configuration.\",\n \"\",\n \"Actions:\",\n \" get — Return the current notification mode, active channels, and routing table.\",\n \" set — Change the notification mode or update channel/routing config.\",\n \" send — Send a notification through the configured channels.\",\n \"\",\n \"Notification modes:\",\n \" auto — Use the per-event routing table (default)\",\n \" voice — All events sent as WhatsApp voice (TTS)\",\n \" whatsapp — All events sent as WhatsApp text\",\n \" ntfy — All events sent to ntfy.sh\",\n \" macos — All events sent as macOS notifications\",\n \" cli — All events written to CLI output only\",\n \" off — Suppress all notifications\",\n \"\",\n \"Event types for send: error | progress | completion | info | debug\",\n \"\",\n \"Examples:\",\n ' { \"action\": \"get\" }',\n ' { \"action\": \"set\", \"mode\": \"voice\" }',\n ' { \"action\": \"send\", \"event\": \"completion\", \"message\": \"Done!\" }',\n ].join(\"\\n\"),\n {\n action: z\n .enum([\"get\", \"set\", \"send\"])\n .describe(\"Action: 'get' (read config), 'set' (update config), 'send' (send notification).\"),\n mode: z\n .enum([\"auto\", \"voice\", \"whatsapp\", \"ntfy\", \"macos\", \"cli\", \"off\"])\n .optional()\n .describe(\"For action=set: new notification mode.\"),\n channels: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial channel config overrides as a JSON object. \" +\n 'E.g. { \"whatsapp\": { \"enabled\": true }, \"macos\": { \"enabled\": false } }'\n ),\n routing: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial routing overrides as a JSON object. \" +\n 'E.g. { \"error\": [\"whatsapp\", \"macos\"], \"progress\": [\"cli\"] }'\n ),\n event: z\n .enum([\"error\", \"progress\", \"completion\", \"info\", \"debug\"])\n .optional()\n .describe(\"For action=send: event type. Default: 'info'.\"),\n message: z\n .string()\n .optional()\n .describe(\"For action=send: the notification message body.\"),\n title: z\n .string()\n .optional()\n .describe(\"For action=send: optional notification title (used by macOS and ntfy).\"),\n },\n async (args) => {\n const result = await toolNotificationConfig(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: topic_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"topic_detect\",\n [\n \"Detect whether recent conversation context has shifted to a different project.\",\n \"\",\n \"Call this when the conversation may have drifted away from the initially-routed project.\",\n \"Provide a short summary of the recent context (last few messages or tool call results).\",\n \"\",\n \"Returns:\",\n \" shifted — true if a topic shift was detected\",\n \" current_project — the project the session is currently routed to\",\n \" suggested_project — the project that best matches the context\",\n \" confidence — [0,1] fraction of memory mass held by suggested_project\",\n \" chunks_scored — number of memory chunks that contributed to scoring\",\n \" top_matches — top-3 projects with their confidence percentages\",\n \"\",\n \"A shift is reported when confidence >= threshold (default 0.6) and the\",\n \"best-matching project differs from current_project.\",\n \"\",\n \"Use cases:\",\n \" - Call at session start to confirm routing is correct\",\n \" - Call periodically when working across multiple concerns\",\n \" - Integrate with pre-tool hooks for automatic drift detection\",\n ].join(\"\\n\"),\n {\n context: z\n .string()\n .describe(\n \"Recent conversation context: a few sentences summarising what the session has been discussing. \" +\n \"Can include file paths, feature names, commands run, or any relevant text.\"\n ),\n current_project: z\n .string()\n .optional()\n .describe(\n \"The project slug this session is currently routed to. \" +\n \"If omitted, the tool still returns the best-matching project but shifted will always be false.\"\n ),\n threshold: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe(\n \"Minimum confidence [0,1] to declare a shift. Default: 0.6. \" +\n \"Increase to reduce false positives. Decrease to catch subtle drifts.\"\n ),\n },\n async (args) => {\n const result = await toolTopicDetect(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_route\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_route\",\n [\n \"Automatically detect which project this session belongs to.\",\n \"\",\n \"Call this at session start (e.g., from CLAUDE.md or a session-start hook)\",\n \"to route the session to the correct project automatically.\",\n \"\",\n \"Detection 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 marker files\",\n \" 3. topic — BM25 keyword search against memory (only if context provided)\",\n \"\",\n \"Returns:\",\n \" slug — the matched project slug\",\n \" display_name — human-readable project name\",\n \" root_path — absolute path to the project root\",\n \" method — how it was detected: 'path', 'marker', or 'topic'\",\n \" confidence — 1.0 for path/marker matches, BM25 fraction for topic\",\n \"\",\n \"If no match is found, returns a message explaining what was tried.\",\n \"Run 'pai project add .' to register the current directory.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Working directory to detect from. Defaults to process.cwd(). \" +\n \"Pass the session's actual working directory for accurate detection.\"\n ),\n context: z\n .string()\n .optional()\n .describe(\n \"Optional conversation context for topic-based fallback routing. \" +\n \"A few sentences summarising what the session will work on. \" +\n \"Only used if path and marker detection both fail.\"\n ),\n },\n async (args) => {\n const result = await toolSessionRoute(getRegistryDb(), getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_explore\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_explore\",\n [\n \"Explore the vault's knowledge graph using Luhmann's Folgezettel traversal.\",\n \"Follow trains of thought forward, backward, or both from a starting note.\",\n \"Classifies links as sequential (same-folder) or associative (cross-folder).\",\n ].join(\"\\n\"),\n {\n start_note: z\n .string()\n .describe(\"Path or title of the note to start traversal from.\"),\n depth: z\n .number()\n .int()\n .min(1)\n .max(10)\n .optional()\n .describe(\"How many link hops to traverse. Default: 3.\"),\n direction: z\n .enum([\"forward\", \"backward\", \"both\"])\n .optional()\n .describe(\"Traversal direction: 'forward' (outlinks), 'backward' (backlinks), or 'both'. Default: both.\"),\n mode: z\n .enum([\"sequential\", \"associative\", \"all\"])\n .optional()\n .describe(\"Link type filter: 'sequential' (same-folder), 'associative' (cross-folder), or 'all'. Default: all.\"),\n },\n async (args) => {\n const result = await toolZettelExplore(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_health\",\n [\n \"Audit the structural health of the Obsidian vault.\",\n \"Reports dead links, orphan notes, disconnected clusters, low-connectivity files, and an overall health score.\",\n ].join(\"\\n\"),\n {\n scope: z\n .enum([\"full\", \"recent\", \"project\"])\n .optional()\n .describe(\"Audit scope: 'full' (entire vault), 'recent' (recently modified), or 'project' (specific path). Default: full.\"),\n project_path: z\n .string()\n .optional()\n .describe(\"Absolute path to the project/folder to audit when scope='project'.\"),\n recent_days: z\n .number()\n .int()\n .optional()\n .describe(\"Number of days to look back when scope='recent'. Default: 30.\"),\n include: z\n .array(z.enum([\"dead_links\", \"orphans\", \"disconnected\", \"low_connectivity\"]))\n .optional()\n .describe(\"Specific checks to include. Omit to run all checks.\"),\n },\n async (args) => {\n const result = await toolZettelHealth(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_surprise\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_surprise\",\n [\n \"Find surprising connections — notes that are semantically similar to a reference note but far away in the link graph.\",\n \"High surprise = unexpected relevance.\",\n ].join(\"\\n\"),\n {\n reference_path: z\n .string()\n .describe(\"Path to the reference note to find surprising connections for.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to search within.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of surprising notes to return. Default: 10.\"),\n min_similarity: z\n .number()\n .optional()\n .describe(\"Minimum semantic similarity [0,1] for a note to be considered. Default: 0.5.\"),\n min_graph_distance: z\n .number()\n .int()\n .optional()\n .describe(\"Minimum link hops away from the reference note. Default: 3.\"),\n },\n async (args) => {\n const result = await toolZettelSurprise(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_suggest\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_suggest\",\n [\n \"Suggest new connections for a note using semantic similarity, shared tags, and graph neighborhood (friends-of-friends).\",\n ].join(\"\\n\"),\n {\n note_path: z\n .string()\n .describe(\"Path to the note to generate link suggestions for.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to search within.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of suggestions to return. Default: 10.\"),\n exclude_linked: z\n .boolean()\n .optional()\n .describe(\"Exclude notes already linked from this note. Default: true.\"),\n },\n async (args) => {\n const result = await toolZettelSuggest(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_converse\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_converse\",\n [\n \"Use the vault as a Zettelkasten communication partner.\",\n \"Ask a question, get relevant notes with cross-domain connections and a synthesis prompt for generating new insights.\",\n ].join(\"\\n\"),\n {\n question: z\n .string()\n .describe(\"The question or topic to explore in the vault.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to query.\"),\n depth: z\n .number()\n .int()\n .optional()\n .describe(\"How many link hops to follow from seed notes. Default: 2.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of relevant notes to retrieve. Default: 10.\"),\n },\n async (args) => {\n const result = await toolZettelConverse(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_themes\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_themes\",\n [\n \"Detect emerging themes by clustering recent notes with similar embeddings.\",\n \"Reveals forming idea clusters and suggests index notes for unlinked clusters.\",\n ].join(\"\\n\"),\n {\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to analyse.\"),\n lookback_days: z\n .number()\n .int()\n .optional()\n .describe(\"Number of days of recent notes to cluster. Default: 30.\"),\n min_cluster_size: z\n .number()\n .int()\n .optional()\n .describe(\"Minimum notes required to form a theme cluster. Default: 3.\"),\n max_themes: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of theme clusters to return. Default: 10.\"),\n similarity_threshold: z\n .number()\n .optional()\n .describe(\"Minimum cosine similarity to group notes into a cluster [0,1]. Default: 0.7.\"),\n },\n async (args) => {\n const result = await toolZettelThemes(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Connect transport and start serving\n // -------------------------------------------------------------------------\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // Keep the process alive — the server runs until stdin closes\n}\n","#!/usr/bin/env node\n/**\n * PAI Knowledge OS — MCP server entry point\n *\n * When invoked as `node dist/mcp/index.mjs` (or via the `pai-mcp` bin),\n * starts the PAI MCP server on stdio transport so Claude Code can call\n * memory_search, memory_get, project_info, project_list, session_list,\n * and registry_search tools directly during conversations.\n */\n\nimport { startMcpServer } from \"./server.js\";\n\nstartMcpServer().catch((err) => {\n // Write errors to stderr only — stdout is reserved for JSON-RPC messages\n process.stderr.write(`PAI MCP server fatal error: ${String(err)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["z\n .string","z\n .boolean","z\n .array","z.enum","z\n .number","z\n .enum","z.string","z\n .record","z.unknown"],"sources":["../../src/mcp/server.ts","../../src/mcp/index.ts"],"sourcesContent":["/**\n * PAI Knowledge OS — MCP Server (Phase 3)\n *\n * Exposes PAI registry and memory as MCP tools callable by Claude Code.\n *\n * Tools:\n * memory_search — BM25 search across indexed memory/notes\n * memory_get — Read a specific file or lines from a project\n * project_info — Get details for a project (by slug or current dir)\n * project_list — List projects with optional filters\n * session_list — List sessions for a project\n * registry_search — Full-text search over project slugs/names/paths\n * project_detect — Detect which project a path belongs to\n * project_health — Audit all projects for moved/deleted directories\n * session_route — Auto-route session to project (path/marker/topic)\n *\n * NOTE: All tool logic lives in tools.ts (shared with the daemon).\n * This file wires MCP schema definitions to those pure functions.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { openRegistry } from \"../registry/db.js\";\nimport { openFederation } from \"../memory/db.js\";\nimport { loadConfig } from \"../daemon/config.js\";\nimport {\n toolMemorySearch,\n toolMemoryGet,\n toolProjectInfo,\n toolProjectList,\n toolSessionList,\n toolRegistrySearch,\n toolProjectDetect,\n toolProjectHealth,\n toolNotificationConfig,\n toolTopicDetect,\n toolSessionRoute,\n toolZettelExplore,\n toolZettelHealth,\n toolZettelSurprise,\n toolZettelSuggest,\n toolZettelConverse,\n toolZettelThemes,\n} from \"./tools.js\";\n\n// ---------------------------------------------------------------------------\n// Database singletons (opened lazily, once per MCP server process)\n// ---------------------------------------------------------------------------\n\nlet _registryDb: ReturnType<typeof openRegistry> | null = null;\nlet _federationDb: ReturnType<typeof openFederation> | null = null;\n\nfunction getRegistryDb() {\n if (!_registryDb) _registryDb = openRegistry();\n return _registryDb;\n}\n\nfunction getFederationDb() {\n if (!_federationDb) _federationDb = openFederation();\n return _federationDb;\n}\n\n// ---------------------------------------------------------------------------\n// MCP server startup\n// ---------------------------------------------------------------------------\n\nexport async function startMcpServer(): Promise<void> {\n const server = new McpServer({\n name: \"pai\",\n version: \"0.1.0\",\n });\n\n // -------------------------------------------------------------------------\n // Tool: memory_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_search\",\n [\n \"Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.\",\n \"\",\n \"Use this BEFORE answering questions about past work, decisions, dates, people,\",\n \"preferences, project status, todos, technical choices, or anything that might\",\n \"have been recorded in session notes or memory files.\",\n \"\",\n \"Modes:\",\n \" keyword — BM25 full-text search (default, fast, no embeddings required)\",\n \" semantic — Cosine similarity over vector embeddings (requires prior embed run)\",\n \" hybrid — Normalized combination of BM25 + cosine (best quality)\",\n \"\",\n \"Reranking is ON by default — results are re-scored with a cross-encoder model for better relevance.\",\n \"Set rerank=false to skip reranking (faster but less accurate ordering).\",\n \"\",\n \"Recency boost optionally down-weights older results (recency_boost=90 means scores halve every 90 days).\",\n \"\",\n \"Defaults come from ~/.config/pai/config.json (search section). Per-call parameters override config defaults.\",\n \"\",\n \"Returns ranked snippets with project slug, file path, line range, and score.\",\n \"Higher score = more relevant.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\"Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance.\"),\n project: z\n .string()\n .optional()\n .describe(\n \"Scope search to a single project by slug. Omit to search all projects.\"\n ),\n all_projects: z\n .boolean()\n .optional()\n .describe(\n \"Explicitly search all projects (default behaviour when project is omitted).\"\n ),\n sources: z\n .array(z.enum([\"memory\", \"notes\"]))\n .optional()\n .describe(\"Restrict to specific source types: 'memory' or 'notes'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum results to return. Default: 10.\"),\n mode: z\n .enum([\"keyword\", \"semantic\", \"hybrid\"])\n .optional()\n .describe(\n \"Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).\"\n ),\n rerank: z\n .boolean()\n .optional()\n .describe(\n \"Rerank results using a cross-encoder model for better relevance. Default: true. Set to false to skip reranking for faster but less accurate results.\"\n ),\n recency_boost: z\n .number()\n .int()\n .min(0)\n .max(365)\n .optional()\n .describe(\n \"Apply recency boost: score halves every N days. 0 = off. Default from config (typically 90). Applied after reranking.\"\n ),\n },\n async (args) => {\n const config = loadConfig();\n const result = await toolMemorySearch(\n getRegistryDb(),\n getFederationDb(),\n { ...args, recencyBoost: args.recency_boost },\n config.search,\n );\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: memory_get\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_get\",\n [\n \"Read the content of a specific file from a registered PAI project.\",\n \"\",\n \"Use this to read a full memory file, session note, or document after finding\",\n \"it via memory_search. Optionally restrict to a line range.\",\n \"\",\n \"The path must be a relative path within the project root (no ../ traversal).\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .describe(\"Project slug identifying which project's files to read from.\"),\n path: z\n .string()\n .describe(\n \"Relative path within the project root (e.g. 'Notes/0001 - 2026-01-01 - Example.md').\"\n ),\n from: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Starting line number (1-based, inclusive). Default: 1.\"),\n lines: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Number of lines to return. Default: entire file.\"),\n },\n async (args) => {\n const result = toolMemoryGet(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_info\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_info\",\n [\n \"Get detailed information about a PAI registered project.\",\n \"\",\n \"Use this to look up a project's root path, type, status, tags, session count,\",\n \"and last active date. If no slug is provided, attempts to detect the current\",\n \"project from the caller's working directory.\",\n ].join(\"\\n\"),\n {\n slug: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => {\n const result = toolProjectInfo(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_list\",\n [\n \"List registered PAI projects with optional filters.\",\n \"\",\n \"Use this to browse all known projects, find projects by status or tag,\",\n \"or get a quick overview of the PAI registry.\",\n ].join(\"\\n\"),\n {\n status: z\n .enum([\"active\", \"archived\", \"migrating\"])\n .optional()\n .describe(\"Filter by project status. Default: all statuses.\"),\n tag: z\n .string()\n .optional()\n .describe(\"Filter by tag name (exact match).\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum number of projects to return. Default: 50.\"),\n },\n async (args) => {\n const result = toolProjectList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_list\",\n [\n \"List session notes for a PAI project.\",\n \"\",\n \"Use this to find what sessions exist for a project, see their dates and titles,\",\n \"and identify specific session notes to read via memory_get.\",\n ].join(\"\\n\"),\n {\n project: z.string().describe(\"Project slug to list sessions for.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum sessions to return. Default: 10 (most recent first).\"),\n status: z\n .enum([\"open\", \"completed\", \"compacted\"])\n .optional()\n .describe(\"Filter by session status.\"),\n },\n async (args) => {\n const result = toolSessionList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: registry_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"registry_search\",\n [\n \"Search PAI project registry by slug, display name, or path.\",\n \"\",\n \"Use this to find the slug for a project when you know its name or path,\",\n \"or to check if a project is registered. Returns matching project entries.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\n \"Search term matched against project slugs, display names, and root paths (case-insensitive substring match).\"\n ),\n },\n async (args) => {\n const result = toolRegistrySearch(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_detect\",\n [\n \"Detect which registered PAI project a filesystem path belongs to.\",\n \"\",\n \"Use this at session start to auto-identify the current project from the\",\n \"working directory, or to map any path back to its registered project.\",\n \"\",\n \"Returns: slug, display_name, root_path, type, status, match_type (exact|parent),\",\n \"relative_path (if the given path is inside a project), and session stats.\",\n \"\",\n \"match_type 'exact' means the path IS the project root.\",\n \"match_type 'parent' means the path is a subdirectory of the project root.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Absolute path to detect project for. Defaults to the MCP server's process.cwd().\"\n ),\n },\n async (args) => {\n const result = toolProjectDetect(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_health\",\n [\n \"Audit all registered PAI projects to find moved or deleted directories.\",\n \"\",\n \"Returns a JSON report categorising every project as:\",\n \" active — root_path exists on disk\",\n \" stale — root_path missing, but a directory with the same name was found nearby\",\n \" dead — root_path missing, no candidate found\",\n \"\",\n \"Use this to diagnose orphaned sessions or missing project paths.\",\n ].join(\"\\n\"),\n {\n category: z\n .enum([\"active\", \"stale\", \"dead\", \"all\"])\n .optional()\n .describe(\"Filter results to a specific health category. Default: all.\"),\n },\n async (args) => {\n const result = await toolProjectHealth(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: notification_config\n // -------------------------------------------------------------------------\n\n server.tool(\n \"notification_config\",\n [\n \"Query or update the PAI unified notification configuration.\",\n \"\",\n \"Actions:\",\n \" get — Return the current notification mode, active channels, and routing table.\",\n \" set — Change the notification mode or update channel/routing config.\",\n \" send — Send a notification through the configured channels.\",\n \"\",\n \"Notification modes:\",\n \" auto — Use the per-event routing table (default)\",\n \" voice — All events sent as WhatsApp voice (TTS)\",\n \" whatsapp — All events sent as WhatsApp text\",\n \" ntfy — All events sent to ntfy.sh\",\n \" macos — All events sent as macOS notifications\",\n \" cli — All events written to CLI output only\",\n \" off — Suppress all notifications\",\n \"\",\n \"Event types for send: error | progress | completion | info | debug\",\n \"\",\n \"Examples:\",\n ' { \"action\": \"get\" }',\n ' { \"action\": \"set\", \"mode\": \"voice\" }',\n ' { \"action\": \"send\", \"event\": \"completion\", \"message\": \"Done!\" }',\n ].join(\"\\n\"),\n {\n action: z\n .enum([\"get\", \"set\", \"send\"])\n .describe(\"Action: 'get' (read config), 'set' (update config), 'send' (send notification).\"),\n mode: z\n .enum([\"auto\", \"voice\", \"whatsapp\", \"ntfy\", \"macos\", \"cli\", \"off\"])\n .optional()\n .describe(\"For action=set: new notification mode.\"),\n channels: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial channel config overrides as a JSON object. \" +\n 'E.g. { \"whatsapp\": { \"enabled\": true }, \"macos\": { \"enabled\": false } }'\n ),\n routing: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial routing overrides as a JSON object. \" +\n 'E.g. { \"error\": [\"whatsapp\", \"macos\"], \"progress\": [\"cli\"] }'\n ),\n event: z\n .enum([\"error\", \"progress\", \"completion\", \"info\", \"debug\"])\n .optional()\n .describe(\"For action=send: event type. Default: 'info'.\"),\n message: z\n .string()\n .optional()\n .describe(\"For action=send: the notification message body.\"),\n title: z\n .string()\n .optional()\n .describe(\"For action=send: optional notification title (used by macOS and ntfy).\"),\n },\n async (args) => {\n const result = await toolNotificationConfig(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: topic_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"topic_detect\",\n [\n \"Detect whether recent conversation context has shifted to a different project.\",\n \"\",\n \"Call this when the conversation may have drifted away from the initially-routed project.\",\n \"Provide a short summary of the recent context (last few messages or tool call results).\",\n \"\",\n \"Returns:\",\n \" shifted — true if a topic shift was detected\",\n \" current_project — the project the session is currently routed to\",\n \" suggested_project — the project that best matches the context\",\n \" confidence — [0,1] fraction of memory mass held by suggested_project\",\n \" chunks_scored — number of memory chunks that contributed to scoring\",\n \" top_matches — top-3 projects with their confidence percentages\",\n \"\",\n \"A shift is reported when confidence >= threshold (default 0.6) and the\",\n \"best-matching project differs from current_project.\",\n \"\",\n \"Use cases:\",\n \" - Call at session start to confirm routing is correct\",\n \" - Call periodically when working across multiple concerns\",\n \" - Integrate with pre-tool hooks for automatic drift detection\",\n ].join(\"\\n\"),\n {\n context: z\n .string()\n .describe(\n \"Recent conversation context: a few sentences summarising what the session has been discussing. \" +\n \"Can include file paths, feature names, commands run, or any relevant text.\"\n ),\n current_project: z\n .string()\n .optional()\n .describe(\n \"The project slug this session is currently routed to. \" +\n \"If omitted, the tool still returns the best-matching project but shifted will always be false.\"\n ),\n threshold: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe(\n \"Minimum confidence [0,1] to declare a shift. Default: 0.6. \" +\n \"Increase to reduce false positives. Decrease to catch subtle drifts.\"\n ),\n },\n async (args) => {\n const result = await toolTopicDetect(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_route\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_route\",\n [\n \"Automatically detect which project this session belongs to.\",\n \"\",\n \"Call this at session start (e.g., from CLAUDE.md or a session-start hook)\",\n \"to route the session to the correct project automatically.\",\n \"\",\n \"Detection 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 marker files\",\n \" 3. topic — BM25 keyword search against memory (only if context provided)\",\n \"\",\n \"Returns:\",\n \" slug — the matched project slug\",\n \" display_name — human-readable project name\",\n \" root_path — absolute path to the project root\",\n \" method — how it was detected: 'path', 'marker', or 'topic'\",\n \" confidence — 1.0 for path/marker matches, BM25 fraction for topic\",\n \"\",\n \"If no match is found, returns a message explaining what was tried.\",\n \"Run 'pai project add .' to register the current directory.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Working directory to detect from. Defaults to process.cwd(). \" +\n \"Pass the session's actual working directory for accurate detection.\"\n ),\n context: z\n .string()\n .optional()\n .describe(\n \"Optional conversation context for topic-based fallback routing. \" +\n \"A few sentences summarising what the session will work on. \" +\n \"Only used if path and marker detection both fail.\"\n ),\n },\n async (args) => {\n const result = await toolSessionRoute(getRegistryDb(), getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_explore\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_explore\",\n [\n \"Explore the vault's knowledge graph using Luhmann's Folgezettel traversal.\",\n \"Follow trains of thought forward, backward, or both from a starting note.\",\n \"Classifies links as sequential (same-folder) or associative (cross-folder).\",\n ].join(\"\\n\"),\n {\n start_note: z\n .string()\n .describe(\"Path or title of the note to start traversal from.\"),\n depth: z\n .number()\n .int()\n .min(1)\n .max(10)\n .optional()\n .describe(\"How many link hops to traverse. Default: 3.\"),\n direction: z\n .enum([\"forward\", \"backward\", \"both\"])\n .optional()\n .describe(\"Traversal direction: 'forward' (outlinks), 'backward' (backlinks), or 'both'. Default: both.\"),\n mode: z\n .enum([\"sequential\", \"associative\", \"all\"])\n .optional()\n .describe(\"Link type filter: 'sequential' (same-folder), 'associative' (cross-folder), or 'all'. Default: all.\"),\n },\n async (args) => {\n const result = await toolZettelExplore(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_health\",\n [\n \"Audit the structural health of the Obsidian vault.\",\n \"Reports dead links, orphan notes, disconnected clusters, low-connectivity files, and an overall health score.\",\n ].join(\"\\n\"),\n {\n scope: z\n .enum([\"full\", \"recent\", \"project\"])\n .optional()\n .describe(\"Audit scope: 'full' (entire vault), 'recent' (recently modified), or 'project' (specific path). Default: full.\"),\n project_path: z\n .string()\n .optional()\n .describe(\"Absolute path to the project/folder to audit when scope='project'.\"),\n recent_days: z\n .number()\n .int()\n .optional()\n .describe(\"Number of days to look back when scope='recent'. Default: 30.\"),\n include: z\n .array(z.enum([\"dead_links\", \"orphans\", \"disconnected\", \"low_connectivity\"]))\n .optional()\n .describe(\"Specific checks to include. Omit to run all checks.\"),\n },\n async (args) => {\n const result = await toolZettelHealth(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_surprise\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_surprise\",\n [\n \"Find surprising connections — notes that are semantically similar to a reference note but far away in the link graph.\",\n \"High surprise = unexpected relevance.\",\n ].join(\"\\n\"),\n {\n reference_path: z\n .string()\n .describe(\"Path to the reference note to find surprising connections for.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to search within.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of surprising notes to return. Default: 10.\"),\n min_similarity: z\n .number()\n .optional()\n .describe(\"Minimum semantic similarity [0,1] for a note to be considered. Default: 0.5.\"),\n min_graph_distance: z\n .number()\n .int()\n .optional()\n .describe(\"Minimum link hops away from the reference note. Default: 3.\"),\n },\n async (args) => {\n const result = await toolZettelSurprise(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_suggest\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_suggest\",\n [\n \"Suggest new connections for a note using semantic similarity, shared tags, and graph neighborhood (friends-of-friends).\",\n ].join(\"\\n\"),\n {\n note_path: z\n .string()\n .describe(\"Path to the note to generate link suggestions for.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to search within.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of suggestions to return. Default: 10.\"),\n exclude_linked: z\n .boolean()\n .optional()\n .describe(\"Exclude notes already linked from this note. Default: true.\"),\n },\n async (args) => {\n const result = await toolZettelSuggest(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_converse\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_converse\",\n [\n \"Use the vault as a Zettelkasten communication partner.\",\n \"Ask a question, get relevant notes with cross-domain connections and a synthesis prompt for generating new insights.\",\n ].join(\"\\n\"),\n {\n question: z\n .string()\n .describe(\"The question or topic to explore in the vault.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to query.\"),\n depth: z\n .number()\n .int()\n .optional()\n .describe(\"How many link hops to follow from seed notes. Default: 2.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of relevant notes to retrieve. Default: 10.\"),\n },\n async (args) => {\n const result = await toolZettelConverse(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_themes\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_themes\",\n [\n \"Detect emerging themes by clustering recent notes with similar embeddings.\",\n \"Reveals forming idea clusters and suggests index notes for unlinked clusters.\",\n ].join(\"\\n\"),\n {\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to analyse.\"),\n lookback_days: z\n .number()\n .int()\n .optional()\n .describe(\"Number of days of recent notes to cluster. Default: 30.\"),\n min_cluster_size: z\n .number()\n .int()\n .optional()\n .describe(\"Minimum notes required to form a theme cluster. Default: 3.\"),\n max_themes: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of theme clusters to return. Default: 10.\"),\n similarity_threshold: z\n .number()\n .optional()\n .describe(\"Minimum cosine similarity to group notes into a cluster [0,1]. Default: 0.7.\"),\n },\n async (args) => {\n const result = await toolZettelThemes(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Connect transport and start serving\n // -------------------------------------------------------------------------\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // Keep the process alive — the server runs until stdin closes\n}\n","#!/usr/bin/env node\n/**\n * PAI Knowledge OS — MCP server entry point\n *\n * When invoked as `node dist/mcp/index.mjs` (or via the `pai-mcp` bin),\n * starts the PAI MCP server on stdio transport so Claude Code can call\n * memory_search, memory_get, project_info, project_list, session_list,\n * and registry_search tools directly during conversations.\n */\n\nimport { startMcpServer } from \"./server.js\";\n\nstartMcpServer().catch((err) => {\n // Write errors to stderr only — stdout is reserved for JSON-RPC messages\n process.stderr.write(`PAI MCP server fatal error: ${String(err)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
@@ -90,7 +90,9 @@ var PostgresBackend = class {
|
|
|
90
90
|
const client = await this.pool.connect();
|
|
91
91
|
try {
|
|
92
92
|
await client.query("BEGIN");
|
|
93
|
-
for (const c of chunks)
|
|
93
|
+
for (const c of chunks) {
|
|
94
|
+
const safeText = c.text.replace(/\0/g, "");
|
|
95
|
+
await client.query(`INSERT INTO pai_chunks
|
|
94
96
|
(id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at, fts_vector)
|
|
95
97
|
VALUES
|
|
96
98
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
|
@@ -106,17 +108,38 @@ var PostgresBackend = class {
|
|
|
106
108
|
text = EXCLUDED.text,
|
|
107
109
|
updated_at = EXCLUDED.updated_at,
|
|
108
110
|
fts_vector = EXCLUDED.fts_vector`, [
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
c.id,
|
|
112
|
+
c.projectId,
|
|
113
|
+
c.source,
|
|
114
|
+
c.tier,
|
|
115
|
+
c.path,
|
|
116
|
+
c.startLine,
|
|
117
|
+
c.endLine,
|
|
118
|
+
c.hash,
|
|
119
|
+
safeText,
|
|
120
|
+
c.updatedAt
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
await client.query("COMMIT");
|
|
124
|
+
} catch (e) {
|
|
125
|
+
await client.query("ROLLBACK");
|
|
126
|
+
throw e;
|
|
127
|
+
} finally {
|
|
128
|
+
client.release();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async getDistinctChunkPaths(projectId) {
|
|
132
|
+
return (await this.pool.query("SELECT DISTINCT path FROM pai_chunks WHERE project_id = $1", [projectId])).rows.map((r) => r.path);
|
|
133
|
+
}
|
|
134
|
+
async deletePaths(projectId, paths) {
|
|
135
|
+
if (paths.length === 0) return;
|
|
136
|
+
const client = await this.pool.connect();
|
|
137
|
+
try {
|
|
138
|
+
await client.query("BEGIN");
|
|
139
|
+
for (const path of paths) {
|
|
140
|
+
await client.query("DELETE FROM pai_chunks WHERE project_id = $1 AND path = $2", [projectId, path]);
|
|
141
|
+
await client.query("DELETE FROM pai_files WHERE project_id = $1 AND path = $2", [projectId, path]);
|
|
142
|
+
}
|
|
120
143
|
await client.query("COMMIT");
|
|
121
144
|
} catch (e) {
|
|
122
145
|
await client.query("ROLLBACK");
|
|
@@ -332,4 +355,4 @@ function buildPgTsQuery(query) {
|
|
|
332
355
|
|
|
333
356
|
//#endregion
|
|
334
357
|
export { PostgresBackend };
|
|
335
|
-
//# sourceMappingURL=postgres-
|
|
358
|
+
//# sourceMappingURL=postgres-FXrHDPcE.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-FXrHDPcE.mjs","names":[],"sources":["../src/storage/postgres.ts"],"sourcesContent":["/**\n * PostgresBackend — implements StorageBackend using PostgreSQL + pgvector.\n *\n * Vector similarity: pgvector's <=> cosine distance operator\n * Full-text search: PostgreSQL tsvector/tsquery (replaces SQLite FTS5)\n * Connection pooling: node-postgres Pool\n *\n * Schema is initialized via docker/init.sql.\n * This module only handles runtime queries — schema creation is external.\n */\n\nimport pg from \"pg\";\nimport type { Pool, PoolClient } from \"pg\";\nimport type { StorageBackend, ChunkRow, FileRow, FederationStats } from \"./interface.js\";\nimport type { SearchResult, SearchOptions } from \"../memory/search.js\";\nimport { buildFtsQuery } from \"../memory/search.js\";\n\nconst { Pool: PgPool } = pg;\n\n// ---------------------------------------------------------------------------\n// Postgres config\n// ---------------------------------------------------------------------------\n\nexport interface PostgresConfig {\n connectionString?: string;\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n password?: string;\n /** Maximum pool connections. Default 5 */\n maxConnections?: number;\n /** Connection timeout in ms. Default 5000 */\n connectionTimeoutMs?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\nexport class PostgresBackend implements StorageBackend {\n readonly backendType = \"postgres\" as const;\n\n private pool: Pool;\n\n constructor(config: PostgresConfig) {\n const connStr =\n config.connectionString ??\n `postgresql://${config.user ?? \"pai\"}:${config.password ?? \"pai\"}@${config.host ?? \"localhost\"}:${config.port ?? 5432}/${config.database ?? \"pai\"}`;\n\n this.pool = new PgPool({\n connectionString: connStr,\n max: config.maxConnections ?? 5,\n connectionTimeoutMillis: config.connectionTimeoutMs ?? 5000,\n idleTimeoutMillis: 30_000,\n });\n\n // Log pool errors so they don't crash the process silently\n this.pool.on(\"error\", (err) => {\n process.stderr.write(`[pai-postgres] Pool error: ${err.message}\\n`);\n });\n }\n\n // -------------------------------------------------------------------------\n // Lifecycle\n // -------------------------------------------------------------------------\n\n async close(): Promise<void> {\n await this.pool.end();\n }\n\n async getStats(): Promise<FederationStats> {\n const client = await this.pool.connect();\n try {\n const filesResult = await client.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM pai_files\"\n );\n const chunksResult = await client.query<{ n: string }>(\n \"SELECT COUNT(*)::text AS n FROM pai_chunks\"\n );\n return {\n files: parseInt(filesResult.rows[0]?.n ?? \"0\", 10),\n chunks: parseInt(chunksResult.rows[0]?.n ?? \"0\", 10),\n };\n } finally {\n client.release();\n }\n }\n\n /**\n * Test the connection by running a trivial query.\n * Returns null on success, error message on failure.\n */\n async testConnection(): Promise<string | null> {\n let client: PoolClient | null = null;\n try {\n client = await this.pool.connect();\n await client.query(\"SELECT 1\");\n return null;\n } catch (e) {\n return e instanceof Error ? e.message : String(e);\n } finally {\n client?.release();\n }\n }\n\n // -------------------------------------------------------------------------\n // File tracking\n // -------------------------------------------------------------------------\n\n async getFileHash(projectId: number, path: string): Promise<string | undefined> {\n const result = await this.pool.query<{ hash: string }>(\n \"SELECT hash FROM pai_files WHERE project_id = $1 AND path = $2\",\n [projectId, path]\n );\n return result.rows[0]?.hash;\n }\n\n async upsertFile(file: FileRow): Promise<void> {\n await this.pool.query(\n `INSERT INTO pai_files (project_id, path, source, tier, hash, mtime, size)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (project_id, path) DO UPDATE SET\n source = EXCLUDED.source,\n tier = EXCLUDED.tier,\n hash = EXCLUDED.hash,\n mtime = EXCLUDED.mtime,\n size = EXCLUDED.size`,\n [file.projectId, file.path, file.source, file.tier, file.hash, file.mtime, file.size]\n );\n }\n\n // -------------------------------------------------------------------------\n // Chunk management\n // -------------------------------------------------------------------------\n\n async getChunkIds(projectId: number, path: string): Promise<string[]> {\n const result = await this.pool.query<{ id: string }>(\n \"SELECT id FROM pai_chunks WHERE project_id = $1 AND path = $2\",\n [projectId, path]\n );\n return result.rows.map((r) => r.id);\n }\n\n async deleteChunksForFile(projectId: number, path: string): Promise<void> {\n // Foreign key CASCADE handles pai_chunks deletion automatically\n // but we don't have FK to pai_chunks from pai_files, so delete explicitly\n await this.pool.query(\n \"DELETE FROM pai_chunks WHERE project_id = $1 AND path = $2\",\n [projectId, path]\n );\n }\n\n async insertChunks(chunks: ChunkRow[]): Promise<void> {\n if (chunks.length === 0) return;\n\n const client = await this.pool.connect();\n try {\n await client.query(\"BEGIN\");\n\n for (const c of chunks) {\n // Strip null bytes — Postgres rejects \\0 in text columns\n const safeText = c.text.replace(/\\0/g, \"\");\n\n // embedding is null at insert time; updated separately via updateEmbedding()\n await client.query(\n `INSERT INTO pai_chunks\n (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at, fts_vector)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,\n to_tsvector('simple', $9))\n ON CONFLICT (id) DO UPDATE SET\n project_id = EXCLUDED.project_id,\n source = EXCLUDED.source,\n tier = EXCLUDED.tier,\n path = EXCLUDED.path,\n start_line = EXCLUDED.start_line,\n end_line = EXCLUDED.end_line,\n hash = EXCLUDED.hash,\n text = EXCLUDED.text,\n updated_at = EXCLUDED.updated_at,\n fts_vector = EXCLUDED.fts_vector`,\n [\n c.id,\n c.projectId,\n c.source,\n c.tier,\n c.path,\n c.startLine,\n c.endLine,\n c.hash,\n safeText,\n c.updatedAt,\n ]\n );\n }\n\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n }\n\n async getDistinctChunkPaths(projectId: number): Promise<string[]> {\n const result = await this.pool.query<{ path: string }>(\n \"SELECT DISTINCT path FROM pai_chunks WHERE project_id = $1\",\n [projectId]\n );\n return result.rows.map((r) => r.path);\n }\n\n async deletePaths(projectId: number, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const client = await this.pool.connect();\n try {\n await client.query(\"BEGIN\");\n for (const path of paths) {\n await client.query(\n \"DELETE FROM pai_chunks WHERE project_id = $1 AND path = $2\",\n [projectId, path]\n );\n await client.query(\n \"DELETE FROM pai_files WHERE project_id = $1 AND path = $2\",\n [projectId, path]\n );\n }\n await client.query(\"COMMIT\");\n } catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n } finally {\n client.release();\n }\n }\n\n async getUnembeddedChunkIds(projectId?: number): Promise<Array<{ id: string; text: string }>> {\n if (projectId !== undefined) {\n const result = await this.pool.query<{ id: string; text: string }>(\n \"SELECT id, text FROM pai_chunks WHERE embedding IS NULL AND project_id = $1 ORDER BY id\",\n [projectId]\n );\n return result.rows;\n }\n const result = await this.pool.query<{ id: string; text: string }>(\n \"SELECT id, text FROM pai_chunks WHERE embedding IS NULL ORDER BY id\"\n );\n return result.rows;\n }\n\n async updateEmbedding(chunkId: string, embedding: Buffer): Promise<void> {\n // Deserialize the Buffer (Float32Array LE bytes) to a number[] for pgvector\n const vec = bufferToVector(embedding);\n const vecStr = \"[\" + vec.join(\",\") + \"]\";\n await this.pool.query(\n \"UPDATE pai_chunks SET embedding = $1::vector WHERE id = $2\",\n [vecStr, chunkId]\n );\n }\n\n // -------------------------------------------------------------------------\n // Search — keyword (tsvector/tsquery)\n // -------------------------------------------------------------------------\n\n async searchKeyword(query: string, opts?: SearchOptions): Promise<SearchResult[]> {\n const maxResults = opts?.maxResults ?? 10;\n\n // Build tsquery from the same token logic as buildFtsQuery, but for Postgres\n const tsQuery = buildPgTsQuery(query);\n if (!tsQuery) return [];\n\n // Use 'simple' dictionary: preserves tokens as-is, no language-specific\n // stemming. Works reliably with any language (German, French, etc.).\n const conditions: string[] = [\"fts_vector @@ to_tsquery('simple', $1)\"];\n const params: (string | number)[] = [tsQuery];\n let paramIdx = 2;\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => `$${paramIdx++}`).join(\", \");\n conditions.push(`project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => `$${paramIdx++}`).join(\", \");\n conditions.push(`source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => `$${paramIdx++}`).join(\", \");\n conditions.push(`tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n params.push(maxResults);\n const limitParam = `$${paramIdx}`;\n\n const sql = `\n SELECT\n project_id,\n path,\n start_line,\n end_line,\n text AS snippet,\n tier,\n source,\n ts_rank(fts_vector, to_tsquery('simple', $1)) AS rank_score\n FROM pai_chunks\n WHERE ${conditions.join(\" AND \")}\n ORDER BY rank_score DESC\n LIMIT ${limitParam}\n `;\n\n try {\n const result = await this.pool.query<{\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n snippet: string;\n tier: string;\n source: string;\n rank_score: number;\n }>(sql, params);\n\n return result.rows.map((row) => ({\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.snippet,\n score: row.rank_score,\n tier: row.tier,\n source: row.source,\n }));\n } catch (e) {\n process.stderr.write(`[pai-postgres] searchKeyword error: ${e}\\n`);\n return [];\n }\n }\n\n // -------------------------------------------------------------------------\n // Search — semantic (pgvector cosine distance)\n // -------------------------------------------------------------------------\n\n async searchSemantic(queryEmbedding: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n const maxResults = opts?.maxResults ?? 10;\n\n const conditions: string[] = [\"embedding IS NOT NULL\"];\n const params: (string | number | string)[] = [];\n let paramIdx = 1;\n\n // pgvector vector literal\n const vecStr = \"[\" + Array.from(queryEmbedding).join(\",\") + \"]\";\n params.push(vecStr);\n const vecParam = `$${paramIdx++}`;\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => `$${paramIdx++}`).join(\", \");\n conditions.push(`project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => `$${paramIdx++}`).join(\", \");\n conditions.push(`source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => `$${paramIdx++}`).join(\", \");\n conditions.push(`tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n params.push(maxResults);\n const limitParam = `$${paramIdx}`;\n\n // <=> is cosine distance; 1 - distance = cosine similarity\n const sql = `\n SELECT\n project_id,\n path,\n start_line,\n end_line,\n text AS snippet,\n tier,\n source,\n 1 - (embedding <=> ${vecParam}::vector) AS cosine_similarity\n FROM pai_chunks\n WHERE ${conditions.join(\" AND \")}\n ORDER BY embedding <=> ${vecParam}::vector\n LIMIT ${limitParam}\n `;\n\n try {\n const result = await this.pool.query<{\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n snippet: string;\n tier: string;\n source: string;\n cosine_similarity: number;\n }>(sql, params);\n\n const minScore = opts?.minScore ?? -Infinity;\n\n return result.rows\n .map((row) => ({\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.snippet,\n score: row.cosine_similarity,\n tier: row.tier,\n source: row.source,\n }))\n .filter((r) => r.score >= minScore);\n } catch (e) {\n process.stderr.write(`[pai-postgres] searchSemantic error: ${e}\\n`);\n return [];\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a Buffer of Float32 LE bytes (as stored in SQLite) to number[].\n */\nfunction bufferToVector(buf: Buffer): number[] {\n const floats: number[] = [];\n for (let i = 0; i < buf.length; i += 4) {\n floats.push(buf.readFloatLE(i));\n }\n return floats;\n}\n\n/**\n * Convert a free-text query to a Postgres tsquery string.\n *\n * Uses OR (|) semantics so that a chunk matching ANY query term is returned,\n * ranked by ts_rank (which scores higher when more terms match). AND (&)\n * semantics are too strict for multi-word queries because all terms rarely\n * co-occur in a single chunk.\n *\n * Example: \"Synchrotech interview follow-up Gilles\"\n * → \"synchrotech | interview | follow | gilles\"\n * → returns chunks containing any of these words, highest-matching first\n */\nfunction buildPgTsQuery(query: string): string {\n const STOP_WORDS = new Set([\n \"a\", \"an\", \"and\", \"are\", \"as\", \"at\", \"be\", \"been\", \"but\", \"by\",\n \"do\", \"for\", \"from\", \"has\", \"have\", \"he\", \"her\", \"him\", \"his\",\n \"how\", \"i\", \"if\", \"in\", \"is\", \"it\", \"its\", \"me\", \"my\", \"not\",\n \"of\", \"on\", \"or\", \"our\", \"out\", \"she\", \"so\", \"that\", \"the\",\n \"their\", \"them\", \"they\", \"this\", \"to\", \"up\", \"us\", \"was\", \"we\",\n \"were\", \"what\", \"when\", \"who\", \"will\", \"with\", \"you\", \"your\",\n ]);\n\n const tokens = query\n .toLowerCase()\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t) => t.length >= 2)\n .filter((t) => !STOP_WORDS.has(t))\n // Sanitize: strip tsquery special characters to prevent syntax errors\n .map((t) => t.replace(/'/g, \"''\").replace(/[&|!():]/g, \"\"))\n .filter(Boolean);\n\n if (tokens.length === 0) {\n // Fallback: sanitize the raw query and use it as a single term\n const raw = query.replace(/[^a-z0-9]/gi, \" \").trim().split(/\\s+/).filter(Boolean).join(\" | \");\n return raw || \"\";\n }\n\n // Use OR (|) so that chunks matching ANY term are returned.\n // ts_rank naturally scores chunks higher when more terms match, so the\n // most relevant results still bubble to the top.\n return tokens.join(\" | \");\n}\n\n// Re-export buildFtsQuery so it is accessible without importing search.ts\nexport { buildPgTsQuery };\n"],"mappings":";;;;;;;;;;;;;AAiBA,MAAM,EAAE,MAAM,WAAW;AAuBzB,IAAa,kBAAb,MAAuD;CACrD,AAAS,cAAc;CAEvB,AAAQ;CAER,YAAY,QAAwB;AAKlC,OAAK,OAAO,IAAI,OAAO;GACrB,kBAJA,OAAO,oBACP,gBAAgB,OAAO,QAAQ,MAAM,GAAG,OAAO,YAAY,MAAM,GAAG,OAAO,QAAQ,YAAY,GAAG,OAAO,QAAQ,KAAK,GAAG,OAAO,YAAY;GAI5I,KAAK,OAAO,kBAAkB;GAC9B,yBAAyB,OAAO,uBAAuB;GACvD,mBAAmB;GACpB,CAAC;AAGF,OAAK,KAAK,GAAG,UAAU,QAAQ;AAC7B,WAAQ,OAAO,MAAM,8BAA8B,IAAI,QAAQ,IAAI;IACnE;;CAOJ,MAAM,QAAuB;AAC3B,QAAM,KAAK,KAAK,KAAK;;CAGvB,MAAM,WAAqC;EACzC,MAAM,SAAS,MAAM,KAAK,KAAK,SAAS;AACxC,MAAI;GACF,MAAM,cAAc,MAAM,OAAO,MAC/B,4CACD;GACD,MAAM,eAAe,MAAM,OAAO,MAChC,6CACD;AACD,UAAO;IACL,OAAO,SAAS,YAAY,KAAK,IAAI,KAAK,KAAK,GAAG;IAClD,QAAQ,SAAS,aAAa,KAAK,IAAI,KAAK,KAAK,GAAG;IACrD;YACO;AACR,UAAO,SAAS;;;;;;;CAQpB,MAAM,iBAAyC;EAC7C,IAAI,SAA4B;AAChC,MAAI;AACF,YAAS,MAAM,KAAK,KAAK,SAAS;AAClC,SAAM,OAAO,MAAM,WAAW;AAC9B,UAAO;WACA,GAAG;AACV,UAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;YACzC;AACR,WAAQ,SAAS;;;CAQrB,MAAM,YAAY,WAAmB,MAA2C;AAK9E,UAJe,MAAM,KAAK,KAAK,MAC7B,kEACA,CAAC,WAAW,KAAK,CAClB,EACa,KAAK,IAAI;;CAGzB,MAAM,WAAW,MAA8B;AAC7C,QAAM,KAAK,KAAK,MACd;;;;;;;kCAQA;GAAC,KAAK;GAAW,KAAK;GAAM,KAAK;GAAQ,KAAK;GAAM,KAAK;GAAM,KAAK;GAAO,KAAK;GAAK,CACtF;;CAOH,MAAM,YAAY,WAAmB,MAAiC;AAKpE,UAJe,MAAM,KAAK,KAAK,MAC7B,iEACA,CAAC,WAAW,KAAK,CAClB,EACa,KAAK,KAAK,MAAM,EAAE,GAAG;;CAGrC,MAAM,oBAAoB,WAAmB,MAA6B;AAGxE,QAAM,KAAK,KAAK,MACd,8DACA,CAAC,WAAW,KAAK,CAClB;;CAGH,MAAM,aAAa,QAAmC;AACpD,MAAI,OAAO,WAAW,EAAG;EAEzB,MAAM,SAAS,MAAM,KAAK,KAAK,SAAS;AACxC,MAAI;AACF,SAAM,OAAO,MAAM,QAAQ;AAE3B,QAAK,MAAM,KAAK,QAAQ;IAEtB,MAAM,WAAW,EAAE,KAAK,QAAQ,OAAO,GAAG;AAG1C,UAAM,OAAO,MACX;;;;;;;;;;;;;;;gDAgBA;KACE,EAAE;KACF,EAAE;KACF,EAAE;KACF,EAAE;KACF,EAAE;KACF,EAAE;KACF,EAAE;KACF,EAAE;KACF;KACA,EAAE;KACH,CACF;;AAGH,SAAM,OAAO,MAAM,SAAS;WACrB,GAAG;AACV,SAAM,OAAO,MAAM,WAAW;AAC9B,SAAM;YACE;AACR,UAAO,SAAS;;;CAIpB,MAAM,sBAAsB,WAAsC;AAKhE,UAJe,MAAM,KAAK,KAAK,MAC7B,8DACA,CAAC,UAAU,CACZ,EACa,KAAK,KAAK,MAAM,EAAE,KAAK;;CAGvC,MAAM,YAAY,WAAmB,OAAgC;AACnE,MAAI,MAAM,WAAW,EAAG;EACxB,MAAM,SAAS,MAAM,KAAK,KAAK,SAAS;AACxC,MAAI;AACF,SAAM,OAAO,MAAM,QAAQ;AAC3B,QAAK,MAAM,QAAQ,OAAO;AACxB,UAAM,OAAO,MACX,8DACA,CAAC,WAAW,KAAK,CAClB;AACD,UAAM,OAAO,MACX,6DACA,CAAC,WAAW,KAAK,CAClB;;AAEH,SAAM,OAAO,MAAM,SAAS;WACrB,GAAG;AACV,SAAM,OAAO,MAAM,WAAW;AAC9B,SAAM;YACE;AACR,UAAO,SAAS;;;CAIpB,MAAM,sBAAsB,WAAkE;AAC5F,MAAI,cAAc,OAKhB,SAJe,MAAM,KAAK,KAAK,MAC7B,2FACA,CAAC,UAAU,CACZ,EACa;AAKhB,UAHe,MAAM,KAAK,KAAK,MAC7B,sEACD,EACa;;CAGhB,MAAM,gBAAgB,SAAiB,WAAkC;EAGvE,MAAM,SAAS,MADH,eAAe,UAAU,CACZ,KAAK,IAAI,GAAG;AACrC,QAAM,KAAK,KAAK,MACd,8DACA,CAAC,QAAQ,QAAQ,CAClB;;CAOH,MAAM,cAAc,OAAe,MAA+C;EAChF,MAAM,aAAa,MAAM,cAAc;EAGvC,MAAM,UAAU,eAAe,MAAM;AACrC,MAAI,CAAC,QAAS,QAAO,EAAE;EAIvB,MAAM,aAAuB,CAAC,yCAAyC;EACvE,MAAM,SAA8B,CAAC,QAAQ;EAC7C,IAAI,WAAW;AAEf,MAAI,MAAM,cAAc,KAAK,WAAW,SAAS,GAAG;GAClD,MAAM,eAAe,KAAK,WAAW,UAAU,IAAI,aAAa,CAAC,KAAK,KAAK;AAC3E,cAAW,KAAK,kBAAkB,aAAa,GAAG;AAClD,UAAO,KAAK,GAAG,KAAK,WAAW;;AAGjC,MAAI,MAAM,WAAW,KAAK,QAAQ,SAAS,GAAG;GAC5C,MAAM,eAAe,KAAK,QAAQ,UAAU,IAAI,aAAa,CAAC,KAAK,KAAK;AACxE,cAAW,KAAK,cAAc,aAAa,GAAG;AAC9C,UAAO,KAAK,GAAG,KAAK,QAAQ;;AAG9B,MAAI,MAAM,SAAS,KAAK,MAAM,SAAS,GAAG;GACxC,MAAM,eAAe,KAAK,MAAM,UAAU,IAAI,aAAa,CAAC,KAAK,KAAK;AACtE,cAAW,KAAK,YAAY,aAAa,GAAG;AAC5C,UAAO,KAAK,GAAG,KAAK,MAAM;;AAG5B,SAAO,KAAK,WAAW;EACvB,MAAM,aAAa,IAAI;EAEvB,MAAM,MAAM;;;;;;;;;;;cAWF,WAAW,KAAK,QAAQ,CAAC;;cAEzB,WAAW;;AAGrB,MAAI;AAYF,WAXe,MAAM,KAAK,KAAK,MAS5B,KAAK,OAAO,EAED,KAAK,KAAK,SAAS;IAC/B,WAAW,IAAI;IACf,MAAM,IAAI;IACV,WAAW,IAAI;IACf,SAAS,IAAI;IACb,SAAS,IAAI;IACb,OAAO,IAAI;IACX,MAAM,IAAI;IACV,QAAQ,IAAI;IACb,EAAE;WACI,GAAG;AACV,WAAQ,OAAO,MAAM,uCAAuC,EAAE,IAAI;AAClE,UAAO,EAAE;;;CAQb,MAAM,eAAe,gBAA8B,MAA+C;EAChG,MAAM,aAAa,MAAM,cAAc;EAEvC,MAAM,aAAuB,CAAC,wBAAwB;EACtD,MAAM,SAAuC,EAAE;EAC/C,IAAI,WAAW;EAGf,MAAM,SAAS,MAAM,MAAM,KAAK,eAAe,CAAC,KAAK,IAAI,GAAG;AAC5D,SAAO,KAAK,OAAO;EACnB,MAAM,WAAW,IAAI;AAErB,MAAI,MAAM,cAAc,KAAK,WAAW,SAAS,GAAG;GAClD,MAAM,eAAe,KAAK,WAAW,UAAU,IAAI,aAAa,CAAC,KAAK,KAAK;AAC3E,cAAW,KAAK,kBAAkB,aAAa,GAAG;AAClD,UAAO,KAAK,GAAG,KAAK,WAAW;;AAGjC,MAAI,MAAM,WAAW,KAAK,QAAQ,SAAS,GAAG;GAC5C,MAAM,eAAe,KAAK,QAAQ,UAAU,IAAI,aAAa,CAAC,KAAK,KAAK;AACxE,cAAW,KAAK,cAAc,aAAa,GAAG;AAC9C,UAAO,KAAK,GAAG,KAAK,QAAQ;;AAG9B,MAAI,MAAM,SAAS,KAAK,MAAM,SAAS,GAAG;GACxC,MAAM,eAAe,KAAK,MAAM,UAAU,IAAI,aAAa,CAAC,KAAK,KAAK;AACtE,cAAW,KAAK,YAAY,aAAa,GAAG;AAC5C,UAAO,KAAK,GAAG,KAAK,MAAM;;AAG5B,SAAO,KAAK,WAAW;EACvB,MAAM,aAAa,IAAI;EAGvB,MAAM,MAAM;;;;;;;;;6BASa,SAAS;;cAExB,WAAW,KAAK,QAAQ,CAAC;+BACR,SAAS;cAC1B,WAAW;;AAGrB,MAAI;GACF,MAAM,SAAS,MAAM,KAAK,KAAK,MAS5B,KAAK,OAAO;GAEf,MAAM,WAAW,MAAM,YAAY;AAEnC,UAAO,OAAO,KACX,KAAK,SAAS;IACb,WAAW,IAAI;IACf,MAAM,IAAI;IACV,WAAW,IAAI;IACf,SAAS,IAAI;IACb,SAAS,IAAI;IACb,OAAO,IAAI;IACX,MAAM,IAAI;IACV,QAAQ,IAAI;IACb,EAAE,CACF,QAAQ,MAAM,EAAE,SAAS,SAAS;WAC9B,GAAG;AACV,WAAQ,OAAO,MAAM,wCAAwC,EAAE,IAAI;AACnE,UAAO,EAAE;;;;;;;AAYf,SAAS,eAAe,KAAuB;CAC7C,MAAM,SAAmB,EAAE;AAC3B,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,EACnC,QAAO,KAAK,IAAI,YAAY,EAAE,CAAC;AAEjC,QAAO;;;;;;;;;;;;;;AAeT,SAAS,eAAe,OAAuB;CAC7C,MAAM,aAAa,IAAI,IAAI;EACzB;EAAK;EAAM;EAAO;EAAO;EAAM;EAAM;EAAM;EAAQ;EAAO;EAC1D;EAAM;EAAO;EAAQ;EAAO;EAAQ;EAAM;EAAO;EAAO;EACxD;EAAO;EAAK;EAAM;EAAM;EAAM;EAAM;EAAO;EAAM;EAAM;EACvD;EAAM;EAAM;EAAM;EAAO;EAAO;EAAO;EAAM;EAAQ;EACrD;EAAS;EAAQ;EAAQ;EAAQ;EAAM;EAAM;EAAM;EAAO;EAC1D;EAAQ;EAAQ;EAAQ;EAAO;EAAQ;EAAQ;EAAO;EACvD,CAAC;CAEF,MAAM,SAAS,MACZ,aAAa,CACb,MAAM,cAAc,CACpB,OAAO,QAAQ,CACf,QAAQ,MAAM,EAAE,UAAU,EAAE,CAC5B,QAAQ,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAEjC,KAAK,MAAM,EAAE,QAAQ,MAAM,KAAK,CAAC,QAAQ,aAAa,GAAG,CAAC,CAC1D,OAAO,QAAQ;AAElB,KAAI,OAAO,WAAW,EAGpB,QADY,MAAM,QAAQ,eAAe,IAAI,CAAC,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ,CAAC,KAAK,MAAM,IAC/E;AAMhB,QAAO,OAAO,KAAK,MAAM"}
|
|
@@ -64,6 +64,23 @@ var SQLiteBackend = class {
|
|
|
64
64
|
}
|
|
65
65
|
})();
|
|
66
66
|
}
|
|
67
|
+
async getDistinctChunkPaths(projectId) {
|
|
68
|
+
return this.db.prepare("SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?").all(projectId).map((r) => r.path);
|
|
69
|
+
}
|
|
70
|
+
async deletePaths(projectId, paths) {
|
|
71
|
+
if (paths.length === 0) return;
|
|
72
|
+
const deleteFts = this.db.prepare("DELETE FROM memory_fts WHERE id = ?");
|
|
73
|
+
const deleteChunks = this.db.prepare("DELETE FROM memory_chunks WHERE project_id = ? AND path = ?");
|
|
74
|
+
const deleteFile = this.db.prepare("DELETE FROM memory_files WHERE project_id = ? AND path = ?");
|
|
75
|
+
this.db.transaction(() => {
|
|
76
|
+
for (const path of paths) {
|
|
77
|
+
const ids = this.db.prepare("SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?").all(projectId, path);
|
|
78
|
+
for (const { id } of ids) deleteFts.run(id);
|
|
79
|
+
deleteChunks.run(projectId, path);
|
|
80
|
+
deleteFile.run(projectId, path);
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
67
84
|
async getUnembeddedChunkIds(projectId) {
|
|
68
85
|
const conditions = ["embedding IS NULL"];
|
|
69
86
|
const params = [];
|
|
@@ -87,4 +104,4 @@ var SQLiteBackend = class {
|
|
|
87
104
|
|
|
88
105
|
//#endregion
|
|
89
106
|
export { SQLiteBackend };
|
|
90
|
-
//# sourceMappingURL=sqlite-
|
|
107
|
+
//# sourceMappingURL=sqlite-WWBq7_2C.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-
|
|
1
|
+
{"version":3,"file":"sqlite-WWBq7_2C.mjs","names":[],"sources":["../src/storage/sqlite.ts"],"sourcesContent":["/**\n * SQLiteBackend — wraps the existing better-sqlite3 federation.db\n * behind the StorageBackend interface.\n *\n * This is a thin adapter. The heavy lifting is all in the existing\n * memory/indexer.ts and memory/search.ts code; we just provide a\n * backend-agnostic surface so the daemon and tools can call either\n * SQLite or Postgres transparently.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend, ChunkRow, FileRow, FederationStats } from \"./interface.js\";\nimport type { SearchResult, SearchOptions } from \"../memory/search.js\";\nimport { searchMemory, searchMemorySemantic } from \"../memory/search.js\";\n\nexport class SQLiteBackend implements StorageBackend {\n readonly backendType = \"sqlite\" as const;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /**\n * Expose the raw better-sqlite3 Database handle.\n * Used by the daemon to pass to indexAll() which still uses the synchronous API directly.\n */\n getRawDb(): Database {\n return this.db;\n }\n\n // -------------------------------------------------------------------------\n // Lifecycle\n // -------------------------------------------------------------------------\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async getStats(): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files\").get() as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks\").get() as { n: number }\n ).n;\n return { files, chunks };\n }\n\n // -------------------------------------------------------------------------\n // File tracking\n // -------------------------------------------------------------------------\n\n async getFileHash(projectId: number, path: string): Promise<string | undefined> {\n const row = this.db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, path) as { hash: string } | undefined;\n return row?.hash;\n }\n\n async upsertFile(file: FileRow): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size`\n )\n .run(file.projectId, file.path, file.source, file.tier, file.hash, file.mtime, file.size);\n }\n\n // -------------------------------------------------------------------------\n // Chunk management\n // -------------------------------------------------------------------------\n\n async getChunkIds(projectId: number, path: string): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n }\n\n async deleteChunksForFile(projectId: number, path: string): Promise<void> {\n const ids = await this.getChunkIds(projectId, path);\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const id of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n })();\n }\n\n async insertChunks(chunks: ChunkRow[]): Promise<void> {\n if (chunks.length === 0) return;\n\n const insertChunk = this.db.prepare(\n `INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`\n );\n const insertFts = this.db.prepare(\n `INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n );\n\n this.db.transaction(() => {\n for (const c of chunks) {\n insertChunk.run(\n c.id,\n c.projectId,\n c.source,\n c.tier,\n c.path,\n c.startLine,\n c.endLine,\n c.hash,\n c.text,\n c.updatedAt\n );\n insertFts.run(\n c.text,\n c.id,\n c.projectId,\n c.path,\n c.source,\n c.tier,\n c.startLine,\n c.endLine\n );\n }\n })();\n }\n\n async getDistinctChunkPaths(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n return rows.map((r) => r.path);\n }\n\n async deletePaths(projectId: number, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n const deleteFile = this.db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const path of paths) {\n const ids = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n for (const { id } of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n deleteFile.run(projectId, path);\n }\n })();\n }\n\n async getUnembeddedChunkIds(projectId?: number): Promise<Array<{ id: string; text: string }>> {\n const conditions = [\"embedding IS NULL\"];\n const params: (string | number)[] = [];\n\n if (projectId !== undefined) {\n conditions.push(\"project_id = ?\");\n params.push(projectId);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n const rows = this.db\n .prepare(`SELECT id, text FROM memory_chunks ${where} ORDER BY id`)\n .all(...params) as Array<{ id: string; text: string }>;\n return rows;\n }\n\n async updateEmbedding(chunkId: string, embedding: Buffer): Promise<void> {\n this.db\n .prepare(\"UPDATE memory_chunks SET embedding = ? WHERE id = ?\")\n .run(embedding, chunkId);\n }\n\n // -------------------------------------------------------------------------\n // Search\n // -------------------------------------------------------------------------\n\n async searchKeyword(query: string, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemory(this.db, query, opts);\n }\n\n async searchSemantic(queryEmbedding: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemorySemantic(this.db, queryEmbedding, opts);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
2
|
import { i as searchMemoryHybrid, n as populateSlugs } from "./search-_oHfguA5.mjs";
|
|
3
|
-
import { r as formatDetectionJson, t as detectProject } from "./detect-
|
|
3
|
+
import { r as formatDetectionJson, t as detectProject } from "./detect-BU3Nx_2L.mjs";
|
|
4
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
|
|
@@ -75,7 +75,7 @@ function formatProject(registryDb, project) {
|
|
|
75
75
|
if (project.archived_at) lines.push(`archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`);
|
|
76
76
|
return lines.join("\n");
|
|
77
77
|
}
|
|
78
|
-
async function toolMemorySearch(registryDb, federation, params) {
|
|
78
|
+
async function toolMemorySearch(registryDb, federation, params, searchDefaults) {
|
|
79
79
|
try {
|
|
80
80
|
const projectIds = params.project ? (() => {
|
|
81
81
|
const id = lookupProjectId(registryDb, params.project);
|
|
@@ -88,12 +88,12 @@ async function toolMemorySearch(registryDb, federation, params) {
|
|
|
88
88
|
}],
|
|
89
89
|
isError: true
|
|
90
90
|
};
|
|
91
|
-
const mode = params.mode ?? "keyword";
|
|
92
|
-
const snippetLength = params.snippetLength ?? 200;
|
|
91
|
+
const mode = params.mode ?? searchDefaults?.mode ?? "keyword";
|
|
92
|
+
const snippetLength = params.snippetLength ?? searchDefaults?.snippetLength ?? 200;
|
|
93
93
|
const searchOpts = {
|
|
94
94
|
projectIds,
|
|
95
95
|
sources: params.sources,
|
|
96
|
-
maxResults: params.limit ?? 5
|
|
96
|
+
maxResults: params.limit ?? searchDefaults?.defaultLimit ?? 5
|
|
97
97
|
};
|
|
98
98
|
let results;
|
|
99
99
|
const isBackend = (x) => "backendType" in x;
|
|
@@ -123,20 +123,22 @@ async function toolMemorySearch(registryDb, federation, params) {
|
|
|
123
123
|
else results = searchMemoryHybrid(federation, params.query, queryEmbedding, searchOpts);
|
|
124
124
|
} else results = searchMemory(federation, params.query, searchOpts);
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
const shouldRerank = params.rerank ?? searchDefaults?.rerank ?? true;
|
|
127
|
+
if (shouldRerank && results.length > 0) {
|
|
127
128
|
const { rerankResults } = await import("./reranker-D7bRAHi6.mjs").then((n) => n.r);
|
|
128
129
|
results = await rerankResults(params.query, results, { topK: searchOpts.maxResults ?? 5 });
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
const recencyDays = params.recencyBoost ?? searchDefaults?.recencyBoostDays ?? 0;
|
|
132
|
+
if (recencyDays > 0 && results.length > 0) {
|
|
131
133
|
const { applyRecencyBoost } = await import("./search-_oHfguA5.mjs").then((n) => n.o);
|
|
132
|
-
results = applyRecencyBoost(results,
|
|
134
|
+
results = applyRecencyBoost(results, recencyDays);
|
|
133
135
|
}
|
|
134
136
|
const withSlugs = populateSlugs(results, registryDb);
|
|
135
137
|
if (withSlugs.length === 0) return { content: [{
|
|
136
138
|
type: "text",
|
|
137
139
|
text: `No results found for query: "${params.query}" (mode: ${mode})`
|
|
138
140
|
}] };
|
|
139
|
-
const rerankLabel =
|
|
141
|
+
const rerankLabel = shouldRerank ? " +rerank" : "";
|
|
140
142
|
const formatted = withSlugs.map((r, i) => {
|
|
141
143
|
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}`;
|
|
142
144
|
const raw = r.snippet.trim();
|
|
@@ -634,7 +636,7 @@ function toolProjectTodo(registryDb, params) {
|
|
|
634
636
|
*/
|
|
635
637
|
async function toolNotificationConfig(params) {
|
|
636
638
|
try {
|
|
637
|
-
const { PaiClient } = await import("./ipc-client-
|
|
639
|
+
const { PaiClient } = await import("./ipc-client-Bjg_a1dc.mjs").then((n) => n.n);
|
|
638
640
|
const client = new PaiClient();
|
|
639
641
|
if (params.action === "get") {
|
|
640
642
|
const { config, activeChannels } = await client.getNotificationConfig();
|
|
@@ -721,7 +723,7 @@ async function toolNotificationConfig(params) {
|
|
|
721
723
|
*/
|
|
722
724
|
async function toolTopicDetect(params) {
|
|
723
725
|
try {
|
|
724
|
-
const { PaiClient } = await import("./ipc-client-
|
|
726
|
+
const { PaiClient } = await import("./ipc-client-Bjg_a1dc.mjs").then((n) => n.n);
|
|
725
727
|
const result = await new PaiClient().topicCheck({
|
|
726
728
|
context: params.context,
|
|
727
729
|
currentProject: params.current_project,
|
|
@@ -770,7 +772,7 @@ async function toolTopicDetect(params) {
|
|
|
770
772
|
*/
|
|
771
773
|
async function toolSessionRoute(registryDb, federation, params) {
|
|
772
774
|
try {
|
|
773
|
-
const { autoRoute, formatAutoRouteJson } = await import("./auto-route-
|
|
775
|
+
const { autoRoute, formatAutoRouteJson } = await import("./auto-route-BG6I_4B1.mjs");
|
|
774
776
|
const result = await autoRoute(registryDb, federation, params.cwd, params.context);
|
|
775
777
|
if (!result) return { content: [{
|
|
776
778
|
type: "text",
|
|
@@ -975,4 +977,4 @@ function combineHybridResults(keywordResults, semanticResults, maxResults, keywo
|
|
|
975
977
|
|
|
976
978
|
//#endregion
|
|
977
979
|
export { toolZettelSurprise as _, toolProjectHealth as a, toolProjectTodo as c, toolSessionRoute as d, toolTopicDetect as f, toolZettelSuggest as g, toolZettelHealth as h, toolProjectDetect as i, toolRegistrySearch as l, toolZettelExplore as m, toolMemorySearch as n, toolProjectInfo as o, toolZettelConverse as p, toolNotificationConfig as r, toolProjectList as s, toolMemoryGet as t, toolSessionList as u, toolZettelThemes as v, tools_exports as y };
|
|
978
|
-
//# sourceMappingURL=tools-
|
|
980
|
+
//# sourceMappingURL=tools-DV_lsiCc.mjs.map
|