{"version":3,"file":"tools-BEqiJt_j.mjs","names":[],"sources":["../src/mcp/tools/types.ts","../src/mcp/tools/memory.ts","../src/mcp/tools/projects.ts","../src/mcp/tools/sessions.ts","../src/mcp/tools/registry.ts","../src/mcp/tools/zettel.ts","../src/mcp/tools/kg.ts","../src/mcp/tools/wakeup.ts","../src/memory/taxonomy.ts","../src/mcp/tools/taxonomy.ts","../src/memory/tunnels.ts","../src/mcp/tools/tunnels.ts","../src/mcp/tools/feedback.ts","../src/memory/kg-search.ts","../src/mcp/tools/kg-search.ts"],"sourcesContent":["/**\n * Shared types and project-row helpers used across all MCP tool handler modules.\n */\n\nimport { resolve } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\n\nexport interface ToolContent {\n  type: \"text\";\n  text: string;\n}\n\nexport interface ToolResult {\n  content: ToolContent[];\n  isError?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Shared row type — mirrors the projects SQLite schema\n// ---------------------------------------------------------------------------\n\nexport interface ProjectRow {\n  id: number;\n  slug: string;\n  display_name: string;\n  root_path: string;\n  type: string;\n  status: string;\n  created_at: number;\n  updated_at: number;\n  archived_at?: number | null;\n  parent_id?: number | null;\n  obsidian_link?: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: lookup project_id by slug (also checks aliases)\n// ---------------------------------------------------------------------------\n\nexport function lookupProjectId(\n  registryDb: Database,\n  slug: string\n): number | null {\n  const bySlug = registryDb\n    .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n    .get(slug) as { id: number } | undefined;\n  if (bySlug) return bySlug.id;\n\n  const byAlias = registryDb\n    .prepare(\"SELECT project_id FROM aliases WHERE alias = ?\")\n    .get(slug) as { project_id: number } | undefined;\n  if (byAlias) return byAlias.project_id;\n\n  return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: detect project from a filesystem path\n// ---------------------------------------------------------------------------\n\nexport function detectProjectFromPath(\n  registryDb: Database,\n  fsPath: string\n): ProjectRow | null {\n  const resolved = resolve(fsPath);\n\n  const exact = registryDb\n    .prepare(\n      \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects WHERE root_path = ?\"\n    )\n    .get(resolved) as ProjectRow | undefined;\n\n  if (exact) return exact;\n\n  const all = registryDb\n    .prepare(\n      \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects ORDER BY LENGTH(root_path) DESC\"\n    )\n    .all() as ProjectRow[];\n\n  for (const project of all) {\n    if (\n      resolved.startsWith(project.root_path + \"/\") ||\n      resolved === project.root_path\n    ) {\n      return project;\n    }\n  }\n\n  return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: format project row for tool output\n// ---------------------------------------------------------------------------\n\nexport function formatProject(registryDb: Database, project: ProjectRow): string {\n  const sessionCount = (\n    registryDb\n      .prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\")\n      .get(project.id) as { n: number }\n  ).n;\n\n  const lastSession = registryDb\n    .prepare(\n      \"SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1\"\n    )\n    .get(project.id) as { date: string } | undefined;\n\n  const tags = (\n    registryDb\n      .prepare(\n        `SELECT t.name FROM tags t\n         JOIN project_tags pt ON pt.tag_id = t.id\n         WHERE pt.project_id = ?\n         ORDER BY t.name`\n      )\n      .all(project.id) as Array<{ name: string }>\n  ).map((r) => r.name);\n\n  const aliases = (\n    registryDb\n      .prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\")\n      .all(project.id) as Array<{ alias: string }>\n  ).map((r) => r.alias);\n\n  const lines: string[] = [\n    `slug: ${project.slug}`,\n    `display_name: ${project.display_name}`,\n    `root_path: ${project.root_path}`,\n    `type: ${project.type}`,\n    `status: ${project.status}`,\n    `sessions: ${sessionCount}`,\n  ];\n\n  if (lastSession) lines.push(`last_session: ${lastSession.date}`);\n  if (tags.length) lines.push(`tags: ${tags.join(\", \")}`);\n  if (aliases.length) lines.push(`aliases: ${aliases.join(\", \")}`);\n  if (project.obsidian_link) lines.push(`obsidian_link: ${project.obsidian_link}`);\n  if (project.archived_at) {\n    lines.push(\n      `archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`\n    );\n  }\n\n  return lines.join(\"\\n\");\n}\n","/**\n * MCP tool handlers: memory_search, memory_get\n */\n\nimport { readFileSync, existsSync, statSync } from \"node:fs\";\nimport { join, resolve, isAbsolute } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport { populateSlugs, searchMemoryHybrid, touchChunksLastAccessed } from \"../../memory/search.js\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { SearchConfig } from \"../../daemon/config.js\";\nimport type { SearchResult } from \"../../memory/search.js\";\nimport {\n  lookupProjectId,\n  type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_search\n// ---------------------------------------------------------------------------\n\nexport interface MemorySearchParams {\n  query: string;\n  project?: string;\n  all_projects?: boolean;\n  sources?: Array<\"memory\" | \"notes\">;\n  limit?: number;\n  mode?: \"keyword\" | \"semantic\" | \"hybrid\";\n  /** Rerank results using cross-encoder model for better relevance ordering. */\n  rerank?: boolean;\n  /** Apply recency boost — score decays by half every N days. 0 = off (default). */\n  recencyBoost?: number;\n  /** Maximum characters per result snippet. Default 200.\n   *  Limit context consumption — MCP results go into Claude's context window. */\n  snippetLength?: number;\n  /** Output format: \"full\" (default, includes snippets) or \"compact\" (IDs + metadata only, ~10x fewer tokens). */\n  format?: \"full\" | \"compact\";\n}\n\nexport async function toolMemorySearch(\n  registryDb: Database,\n  federation: Database | StorageBackend,\n  params: MemorySearchParams,\n  searchDefaults?: SearchConfig,\n): Promise<ToolResult> {\n  try {\n    const projectIds: number[] | undefined = params.project\n      ? (() => {\n          const id = lookupProjectId(registryDb, params.project!);\n          return id != null ? [id] : [];\n        })()\n      : undefined;\n\n    if (params.project && (!projectIds || projectIds.length === 0)) {\n      return {\n        content: [\n          { type: \"text\", text: `Project not found: ${params.project}` },\n        ],\n        isError: true,\n      };\n    }\n\n    // NOTE: No indexAll() here — indexing is handled by the daemon scheduler.\n    // The daemon ensures the index stays fresh; the search hot path is read-only.\n\n    const mode = params.mode ?? (searchDefaults?.mode ?? \"keyword\");\n    // Limit context consumption — MCP results go into Claude's context window.\n    // Default to 5 results and 200-char snippets to keep a single search call\n    // within ~1-2K tokens rather than 5K+.\n    const snippetLength = params.snippetLength ?? (searchDefaults?.snippetLength ?? 200);\n    const searchOpts = {\n      projectIds,\n      sources: params.sources,\n      maxResults: params.limit ?? (searchDefaults?.defaultLimit ?? 5),\n    };\n\n    let results;\n\n    // Determine if federation is a StorageBackend or a raw Database\n    const isBackend = (x: Database | StorageBackend): x is StorageBackend =>\n      \"backendType\" in x;\n\n    if (isBackend(federation)) {\n      // Use the storage backend interface (works for both SQLite and Postgres)\n      if (mode === \"keyword\") {\n        results = await federation.searchKeyword(params.query, searchOpts);\n      } else if (mode === \"semantic\" || mode === \"hybrid\") {\n        const { generateEmbedding } = await import(\"../../memory/embeddings.js\");\n        const queryEmbedding = await generateEmbedding(params.query, true);\n\n        if (mode === \"semantic\") {\n          results = await federation.searchSemantic(queryEmbedding, searchOpts);\n        } else {\n          // Hybrid: combine keyword + semantic\n          const [kwResults, semResults] = await Promise.all([\n            federation.searchKeyword(params.query, { ...searchOpts, maxResults: 50 }),\n            federation.searchSemantic(queryEmbedding, { ...searchOpts, maxResults: 50 }),\n          ]); // 50 candidates is sufficient for min-max normalization\n          // Reuse the existing hybrid scoring logic\n          results = combineHybridResults(kwResults, semResults, searchOpts.maxResults ?? 10);\n        }\n      } else {\n        results = await federation.searchKeyword(params.query, searchOpts);\n      }\n    } else {\n      // Legacy path: raw better-sqlite3 Database (for direct MCP server usage)\n      const { searchMemory, searchMemorySemantic } = await import(\"../../memory/search.js\");\n\n      if (mode === \"keyword\") {\n        results = searchMemory(federation, params.query, searchOpts);\n      } else if (mode === \"semantic\" || mode === \"hybrid\") {\n        const { generateEmbedding } = await import(\"../../memory/embeddings.js\");\n        const queryEmbedding = await generateEmbedding(params.query, true);\n\n        if (mode === \"semantic\") {\n          results = searchMemorySemantic(federation, queryEmbedding, searchOpts);\n        } else {\n          results = searchMemoryHybrid(\n            federation,\n            params.query,\n            queryEmbedding,\n            searchOpts\n          );\n        }\n      } else {\n        results = searchMemory(federation, params.query, searchOpts);\n      }\n    }\n\n    // Re-rank by the corpus's own links before returning.\n    //\n    // Similarity answers \"what reads like the query\"; it cannot answer \"which\n    // of these is the one the others refer back to\", which is usually the note\n    // to read first. The wikilinks are facts a person wrote, they cost nothing\n    // to maintain, and they are correct for chunks that have no embedding yet —\n    // which matters while a large embedding backlog drains.\n    //\n    // Only EXTRACTED links count: INFERRED ones are themselves derived by\n    // similarity, so folding them in would feed the ranking its own output.\n    try {\n      const { applyLinkBoost } = await import(\"../../memory/link-boost.js\");\n      const paths = [...new Set(results.map((r) => r.path))];\n      // Bounded: one lookup per distinct result path, not per chunk, and the\n      // result set is already capped by maxResults.\n      if (isBackend(federation) && paths.length > 1) {\n        const edges: Array<{ sourcePath: string; targetPath: string }> = [];\n        for (const p of paths) {\n          for (const l of await federation.getLinksToTarget(p)) {\n            if (l.confidence && l.confidence !== \"EXTRACTED\") continue;\n            edges.push({ sourcePath: l.sourcePath, targetPath: p });\n          }\n        }\n        if (edges.length > 0) results = applyLinkBoost(results, edges);\n      }\n    } catch {\n      // A ranking aid, never a reason to fail a search.\n    }\n\n    // QW2: Update last_accessed_at for returned chunks (best-effort, non-blocking)\n    try {\n      const chunkIds = results\n        .map((r) => r.chunkId)\n        .filter((id): id is string => id != null);\n      if (chunkIds.length > 0) {\n        // Resolve a raw SQLite Database handle if available\n        const rawDb = !isBackend(federation)\n          ? federation\n          : (federation as { getSqliteDb?: () => Database }).getSqliteDb?.() ?? null;\n        if (rawDb) {\n          touchChunksLastAccessed(rawDb, chunkIds);\n        }\n      }\n    } catch {\n      // non-critical — never block search results\n    }\n\n    // Cross-encoder reranking (on by default)\n    const shouldRerank = params.rerank ?? (searchDefaults?.rerank ?? true);\n    if (shouldRerank && results.length > 0) {\n      const { rerankResults } = await import(\"../../memory/reranker.js\");\n      results = await rerankResults(params.query, results, {\n        topK: searchOpts.maxResults ?? 5,\n      });\n    }\n\n    // Recency boost (off by default, applied after reranking)\n    const recencyDays = params.recencyBoost ?? (searchDefaults?.recencyBoostDays ?? 0);\n    if (recencyDays > 0 && results.length > 0) {\n      const { applyRecencyBoost } = await import(\"../../memory/search.js\");\n      results = applyRecencyBoost(results, recencyDays);\n    }\n\n    const withSlugs = populateSlugs(results, registryDb);\n\n    if (withSlugs.length === 0) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `No results found for query: \"${params.query}\" (mode: ${mode})`,\n          },\n        ],\n      };\n    }\n\n    const rerankLabel = shouldRerank ? \" +rerank\" : \"\";\n    const useCompact = params.format === \"compact\";\n    const formatted = withSlugs\n      .map((r, i) => {\n        const slug = r.projectSlug ?? `project:${r.projectId}`;\n        const idPart = r.chunkId ? ` id=${r.chunkId}` : \"\";\n        if (useCompact) {\n          // Compact format: chunk ID, path, score — pass id to memory_feedback to rate this result\n          return `[${i + 1}]${idPart} ${slug} — ${r.path} L${r.startLine}-${r.endLine} score=${r.score.toFixed(3)}`;\n        }\n        const header = `[${i + 1}]${idPart} ${slug} — ${r.path} (lines ${r.startLine}-${r.endLine}) score=${r.score.toFixed(4)} tier=${r.tier} source=${r.source}`;\n        // Truncate snippet to snippetLength — limit context consumption.\n        // MCP results go into Claude's context window; keep each result tight.\n        const raw = r.snippet.trim();\n        const snippet = raw.length > snippetLength\n          ? raw.slice(0, snippetLength) + \"...\"\n          : raw;\n        return `${header}\\n${snippet}`;\n      })\n      .join(useCompact ? \"\\n\" : \"\\n\\n---\\n\\n\");\n\n    // Query feedback loop: save query + result metadata for future indexing\n    try {\n      const { saveQueryResult } = await import(\"../../zettelkasten/query-feedback.js\");\n      saveQueryResult({\n        query: params.query,\n        timestamp: Date.now(),\n        source: \"memory_search\",\n        sourceSlugs: withSlugs.slice(0, 5).map((r) => r.path),\n        answerPreview: withSlugs.slice(0, 3).map((r) => r.snippet.trim().slice(0, 150)).join(\" | \"),\n        resultCount: withSlugs.length,\n      });\n    } catch {\n      // Non-critical — don't fail the search if feedback logging errors\n    }\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Found ${withSlugs.length} result(s) for \"${params.query}\" (mode: ${mode}${rerankLabel}):\\n\\n${formatted}`,\n        },\n      ],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `Search error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: memory_get\n// ---------------------------------------------------------------------------\n\nexport interface MemoryGetParams {\n  project: string;\n  path: string;\n  from?: number;\n  lines?: number;\n}\n\nexport function toolMemoryGet(\n  registryDb: Database,\n  params: MemoryGetParams\n): ToolResult {\n  try {\n    const projectId = lookupProjectId(registryDb, params.project);\n    if (projectId == null) {\n      return {\n        content: [\n          { type: \"text\", text: `Project not found: ${params.project}` },\n        ],\n        isError: true,\n      };\n    }\n\n    const project = registryDb\n      .prepare(\"SELECT root_path FROM projects WHERE id = ?\")\n      .get(projectId) as { root_path: string } | undefined;\n\n    if (!project) {\n      return {\n        content: [\n          { type: \"text\", text: `Project not found: ${params.project}` },\n        ],\n        isError: true,\n      };\n    }\n\n    const requestedPath = params.path;\n    if (requestedPath.includes(\"..\") || isAbsolute(requestedPath)) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `Invalid path: ${params.path} (must be a relative path within the project root, no ../ allowed)`,\n          },\n        ],\n        isError: true,\n      };\n    }\n\n    const fullPath = join(project.root_path, requestedPath);\n    const resolvedFull = resolve(fullPath);\n    const resolvedRoot = resolve(project.root_path);\n\n    if (\n      !resolvedFull.startsWith(resolvedRoot + \"/\") &&\n      resolvedFull !== resolvedRoot\n    ) {\n      return {\n        content: [\n          { type: \"text\", text: `Path traversal blocked: ${params.path}` },\n        ],\n        isError: true,\n      };\n    }\n\n    if (!existsSync(fullPath)) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `File not found: ${requestedPath} (project: ${params.project})`,\n          },\n        ],\n        isError: true,\n      };\n    }\n\n    const stat = statSync(fullPath);\n    if (stat.size > 5 * 1024 * 1024) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum 5 MB.`,\n          },\n        ],\n      };\n    }\n\n    const content = readFileSync(fullPath, \"utf8\");\n    const allLines = content.split(\"\\n\");\n\n    const fromLine = (params.from ?? 1) - 1;\n    const toLine =\n      params.lines != null\n        ? Math.min(fromLine + params.lines, allLines.length)\n        : allLines.length;\n\n    const selectedLines = allLines.slice(fromLine, toLine);\n    const text = selectedLines.join(\"\\n\");\n\n    const header =\n      params.from != null\n        ? `${params.project}/${requestedPath} (lines ${fromLine + 1}-${toLine}):`\n        : `${params.project}/${requestedPath}:`;\n\n    return {\n      content: [{ type: \"text\", text: `${header}\\n\\n${text}` }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `Read error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search helper (backend-agnostic)\n// ---------------------------------------------------------------------------\n\n/**\n * Combine keyword + semantic results using min-max normalized scoring.\n * Mirrors the logic in searchMemoryHybrid() from memory/search.ts,\n * but works on pre-computed result arrays so it works for any backend.\n */\nexport function combineHybridResults(\n  keywordResults: SearchResult[],\n  semanticResults: SearchResult[],\n  maxResults: number,\n  keywordWeight = 0.5,\n  semanticWeight = 0.5\n): SearchResult[] {\n  if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n  const keyFor = (r: SearchResult) =>\n    `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n  function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n    if (items.length === 0) return new Map();\n    const min = Math.min(...items.map((r) => r.score));\n    const max = Math.max(...items.map((r) => r.score));\n    const range = max - min;\n    const m = new Map<string, number>();\n    for (const r of items) {\n      m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n    }\n    return m;\n  }\n\n  const kwNorm = minMaxNormalize(keywordResults);\n  const semNorm = minMaxNormalize(semanticResults);\n\n  const allKeys = new Set<string>([\n    ...keywordResults.map(keyFor),\n    ...semanticResults.map(keyFor),\n  ]);\n\n  const metaMap = new Map<string, SearchResult>();\n  for (const r of [...keywordResults, ...semanticResults]) {\n    metaMap.set(keyFor(r), r);\n  }\n\n  const combined: Array<SearchResult & { combinedScore: number }> = [];\n  for (const key of allKeys) {\n    const meta = metaMap.get(key)!;\n    const kwScore = kwNorm.get(key) ?? 0;\n    const semScore = semNorm.get(key) ?? 0;\n    const combinedScore = keywordWeight * kwScore + semanticWeight * semScore;\n    combined.push({ ...meta, score: combinedScore, combinedScore });\n  }\n\n  return combined\n    .sort((a, b) => b.score - a.score)\n    .slice(0, maxResults)\n    .map(({ combinedScore: _unused, ...r }) => r);\n}\n","/**\n * MCP tool handlers: project_info, project_list, project_detect,\n *                    project_health, project_todo\n */\n\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport { detectProject, formatDetectionJson } from \"../../cli/commands/detect.js\";\nimport {\n  lookupProjectId,\n  detectProjectFromPath,\n  formatProject,\n  type ProjectRow,\n  type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: project_info\n// ---------------------------------------------------------------------------\n\nexport interface ProjectInfoParams {\n  slug?: string;\n}\n\nexport function toolProjectInfo(\n  registryDb: Database,\n  params: ProjectInfoParams\n): ToolResult {\n  try {\n    let project: ProjectRow | null = null;\n\n    if (params.slug) {\n      const projectId = lookupProjectId(registryDb, params.slug);\n      if (projectId != null) {\n        project = registryDb\n          .prepare(\n            \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at, archived_at, parent_id, obsidian_link FROM projects WHERE id = ?\"\n          )\n          .get(projectId) as ProjectRow | null;\n      }\n    } else {\n      const cwd = process.cwd();\n      project = detectProjectFromPath(registryDb, cwd);\n    }\n\n    if (!project) {\n      const message = params.slug\n        ? `Project not found: ${params.slug}`\n        : `No PAI project found matching the current directory: ${process.cwd()}`;\n      return {\n        content: [{ type: \"text\", text: message }],\n        isError: !params.slug,\n      };\n    }\n\n    return {\n      content: [{ type: \"text\", text: formatProject(registryDb, project) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `project_info error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_list\n// ---------------------------------------------------------------------------\n\nexport interface ProjectListParams {\n  status?: \"active\" | \"archived\" | \"migrating\";\n  tag?: string;\n  limit?: number;\n}\n\nexport function toolProjectList(\n  registryDb: Database,\n  params: ProjectListParams\n): ToolResult {\n  try {\n    const conditions: string[] = [];\n    const queryParams: (string | number)[] = [];\n\n    if (params.status) {\n      conditions.push(\"p.status = ?\");\n      queryParams.push(params.status);\n    }\n\n    if (params.tag) {\n      conditions.push(\n        \"p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)\"\n      );\n      queryParams.push(params.tag);\n    }\n\n    const where =\n      conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n    const limit = params.limit ?? 50;\n    queryParams.push(limit);\n\n    const projects = registryDb\n      .prepare(\n        `SELECT p.id, p.slug, p.display_name, p.root_path, p.type, p.status, p.updated_at\n         FROM projects p\n         ${where}\n         ORDER BY p.updated_at DESC\n         LIMIT ?`\n      )\n      .all(...queryParams) as Array<{\n      id: number;\n      slug: string;\n      display_name: string;\n      root_path: string;\n      type: string;\n      status: string;\n      updated_at: number;\n    }>;\n\n    if (projects.length === 0) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: \"No projects found matching the given filters.\",\n          },\n        ],\n      };\n    }\n\n    const lines = projects.map(\n      (p) =>\n        `${p.slug}  [${p.status}]  ${p.root_path}  (updated: ${new Date(p.updated_at).toISOString().slice(0, 10)})`\n    );\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `${projects.length} project(s):\\n\\n${lines.join(\"\\n\")}`,\n        },\n      ],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `project_list error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_detect\n// ---------------------------------------------------------------------------\n\nexport interface ProjectDetectParams {\n  cwd?: string;\n}\n\nexport function toolProjectDetect(\n  registryDb: Database,\n  params: ProjectDetectParams\n): ToolResult {\n  try {\n    const detection = detectProject(registryDb, params.cwd);\n\n    if (!detection) {\n      const target = params.cwd ?? process.cwd();\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `No registered project found for path: ${target}\\n\\nRun 'pai project add .' to register this directory.`,\n          },\n        ],\n      };\n    }\n\n    return {\n      content: [{ type: \"text\", text: formatDetectionJson(detection) }],\n    };\n  } catch (e) {\n    return {\n      content: [\n        { type: \"text\", text: `project_detect error: ${String(e)}` },\n      ],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_health\n// ---------------------------------------------------------------------------\n\nexport interface ProjectHealthParams {\n  category?: \"active\" | \"stale\" | \"dead\" | \"all\";\n}\n\nexport async function toolProjectHealth(\n  registryDb: Database,\n  params: ProjectHealthParams\n): Promise<ToolResult> {\n  try {\n    const { existsSync: fsExists, readdirSync, statSync } = await import(\n      \"node:fs\"\n    );\n    const {\n      join: pathJoin,\n      basename: pathBasename,\n    } = await import(\"node:path\");\n    const { homedir } = await import(\"node:os\");\n    const { encodeDir: enc } = await import(\"../../cli/utils.js\");\n\n    interface HealthRowLocal {\n      id: number;\n      slug: string;\n      display_name: string;\n      root_path: string;\n      encoded_dir: string;\n      status: string;\n      type: string;\n      session_count: number;\n    }\n\n    const rows = registryDb\n      .prepare(\n        `SELECT p.id, p.slug, p.display_name, p.root_path, p.encoded_dir, p.status, p.type,\n           (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count\n         FROM projects p\n         ORDER BY p.slug ASC`\n      )\n      .all() as HealthRowLocal[];\n\n    const home = homedir();\n    const claudeProjects = pathJoin(home, \".claude\", \"projects\");\n\n    function suggestMoved(rootPath: string): string | undefined {\n      const name = pathBasename(rootPath);\n      const candidates = [\n        pathJoin(home, \"dev\", name),\n        pathJoin(home, \"dev\", \"ai\", name),\n        pathJoin(home, \"Desktop\", name),\n        pathJoin(home, \"Projects\", name),\n      ];\n      return candidates.find((c) => fsExists(c));\n    }\n\n    function hasClaudeNotes(encodedDir: string): boolean {\n      if (!fsExists(claudeProjects)) return false;\n      try {\n        for (const entry of readdirSync(claudeProjects)) {\n          if (entry !== encodedDir && !entry.startsWith(encodedDir)) continue;\n          const full = pathJoin(claudeProjects, entry);\n          try {\n            if (!statSync(full).isDirectory()) continue;\n          } catch {\n            continue;\n          }\n          if (fsExists(pathJoin(full, \"Notes\"))) return true;\n        }\n      } catch {\n        /* ignore */\n      }\n      return false;\n    }\n\n    interface HealthResult {\n      slug: string;\n      display_name: string;\n      root_path: string;\n      status: string;\n      type: string;\n      session_count: number;\n      health: string;\n      suggested_path: string | null;\n      has_claude_notes: boolean;\n      todo: {\n        found: boolean;\n        path: string | null;\n        has_continue: boolean;\n      };\n    }\n\n    function findTodoForProject(rootPath: string): {\n      found: boolean;\n      path: string | null;\n      has_continue: boolean;\n    } {\n      const locs = [\n        \"Notes/TODO.md\",\n        \".claude/Notes/TODO.md\",\n        \"tasks/todo.md\",\n        \"TODO.md\",\n      ];\n      for (const rel of locs) {\n        const full = pathJoin(rootPath, rel);\n        if (fsExists(full)) {\n          try {\n            const raw = readFileSync(full, \"utf8\");\n            const hasContinue = /^## Continue$/m.test(raw);\n            return { found: true, path: rel, has_continue: hasContinue };\n          } catch {\n            return { found: true, path: rel, has_continue: false };\n          }\n        }\n      }\n      return { found: false, path: null, has_continue: false };\n    }\n\n    const results: HealthResult[] = rows.map((p) => {\n      const pathExists = fsExists(p.root_path);\n      let health: string;\n      let suggestedPath: string | null = null;\n\n      if (pathExists) {\n        health = \"active\";\n      } else {\n        suggestedPath = suggestMoved(p.root_path) ?? null;\n        health = suggestedPath ? \"stale\" : \"dead\";\n      }\n\n      const todo = pathExists\n        ? findTodoForProject(p.root_path)\n        : { found: false, path: null, has_continue: false };\n\n      return {\n        slug: p.slug,\n        display_name: p.display_name,\n        root_path: p.root_path,\n        status: p.status,\n        type: p.type,\n        session_count: p.session_count,\n        health,\n        suggested_path: suggestedPath,\n        has_claude_notes: hasClaudeNotes(p.encoded_dir),\n        todo,\n      };\n    });\n\n    const filtered =\n      !params.category || params.category === \"all\"\n        ? results\n        : results.filter((r) => r.health === params.category);\n\n    const summary = {\n      total: rows.length,\n      active: results.filter((r) => r.health === \"active\").length,\n      stale: results.filter((r) => r.health === \"stale\").length,\n      dead: results.filter((r) => r.health === \"dead\").length,\n    };\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: JSON.stringify({ summary, projects: filtered }, null, 2),\n        },\n      ],\n    };\n  } catch (e) {\n    return {\n      content: [\n        { type: \"text\", text: `project_health error: ${String(e)}` },\n      ],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_todo\n// ---------------------------------------------------------------------------\n\nexport interface ProjectTodoParams {\n  project?: string;\n}\n\n/**\n * TODO candidate locations searched in priority order.\n * Returns the first one that exists, along with its label.\n */\nconst TODO_LOCATIONS = [\n  { rel: \"Notes/TODO.md\",       label: \"Notes/TODO.md\" },\n  { rel: \".claude/Notes/TODO.md\", label: \".claude/Notes/TODO.md\" },\n  { rel: \"tasks/todo.md\",       label: \"tasks/todo.md\" },\n  { rel: \"TODO.md\",             label: \"TODO.md\" },\n];\n\n/**\n * Given TODO file content, extract and surface the ## Continue section first,\n * then return the remaining content. Returns an object with:\n *   continueSection: string | null\n *   fullContent: string\n *   hasContinue: boolean\n */\nfunction parseTodoContent(raw: string): {\n  continueSection: string | null;\n  fullContent: string;\n  hasContinue: boolean;\n} {\n  const lines = raw.split(\"\\n\");\n\n  // Find the ## Continue heading\n  const continueIdx = lines.findIndex(\n    (l) => l.trim() === \"## Continue\"\n  );\n\n  if (continueIdx === -1) {\n    return { continueSection: null, fullContent: raw, hasContinue: false };\n  }\n\n  // The section ends at the first `---` separator or next `##` heading after\n  // the Continue heading (whichever comes first).\n  let endIdx = lines.length;\n  for (let i = continueIdx + 1; i < lines.length; i++) {\n    const trimmed = lines[i].trim();\n    if (trimmed === \"---\" || (trimmed.startsWith(\"##\") && trimmed !== \"## Continue\")) {\n      endIdx = i;\n      break;\n    }\n  }\n\n  const continueLines = lines.slice(continueIdx, endIdx);\n  const continueSection = continueLines.join(\"\\n\").trim();\n\n  return { continueSection, fullContent: raw, hasContinue: true };\n}\n\nexport function toolProjectTodo(\n  registryDb: Database,\n  params: ProjectTodoParams\n): ToolResult {\n  try {\n    let rootPath: string;\n    let projectSlug: string;\n\n    if (params.project) {\n      const projectId = lookupProjectId(registryDb, params.project);\n      if (projectId == null) {\n        return {\n          content: [\n            { type: \"text\", text: `Project not found: ${params.project}` },\n          ],\n          isError: true,\n        };\n      }\n\n      const row = registryDb\n        .prepare(\"SELECT root_path, slug FROM projects WHERE id = ?\")\n        .get(projectId) as { root_path: string; slug: string } | undefined;\n\n      if (!row) {\n        return {\n          content: [\n            { type: \"text\", text: `Project not found: ${params.project}` },\n          ],\n          isError: true,\n        };\n      }\n\n      rootPath = row.root_path;\n      projectSlug = row.slug;\n    } else {\n      // Auto-detect from cwd\n      const project = detectProjectFromPath(registryDb, process.cwd());\n      if (!project) {\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: `No PAI project found matching the current directory: ${process.cwd()}\\n\\nProvide a project slug or run 'pai project add .' to register this directory.`,\n            },\n          ],\n        };\n      }\n      rootPath = project.root_path;\n      projectSlug = project.slug;\n    }\n\n    // Search for TODO in priority order\n    for (const loc of TODO_LOCATIONS) {\n      const fullPath = join(rootPath, loc.rel);\n      if (existsSync(fullPath)) {\n        const raw = readFileSync(fullPath, \"utf8\");\n        const { continueSection, fullContent, hasContinue } = parseTodoContent(raw);\n\n        let output: string;\n        if (hasContinue && continueSection) {\n          // Surface the ## Continue section first, then the full content\n          output = [\n            `TODO found: ${projectSlug}/${loc.label}`,\n            \"\",\n            \"=== CONTINUE SECTION (surfaced first) ===\",\n            continueSection,\n            \"\",\n            \"=== FULL TODO CONTENT ===\",\n            fullContent,\n          ].join(\"\\n\");\n        } else {\n          output = [\n            `TODO found: ${projectSlug}/${loc.label}`,\n            \"\",\n            fullContent,\n          ].join(\"\\n\");\n        }\n\n        return {\n          content: [{ type: \"text\", text: output }],\n        };\n      }\n    }\n\n    // No TODO found in any location\n    const searched = TODO_LOCATIONS.map((l) => `  ${rootPath}/${l.rel}`).join(\"\\n\");\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: [\n            `No TODO.md found for project: ${projectSlug}`,\n            \"\",\n            \"Searched locations (in order):\",\n            searched,\n            \"\",\n            \"Create a TODO with: echo '## Tasks\\\\n- [ ] First task' > Notes/TODO.md\",\n          ].join(\"\\n\"),\n        },\n      ],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `project_todo error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n","/**\n * MCP tool handlers: session_list, session_route\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport {\n  lookupProjectId,\n  type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: session_list\n// ---------------------------------------------------------------------------\n\nexport interface SessionListParams {\n  project: string;\n  limit?: number;\n  status?: \"open\" | \"completed\" | \"compacted\";\n}\n\nexport function toolSessionList(\n  registryDb: Database,\n  params: SessionListParams\n): ToolResult {\n  try {\n    const projectId = lookupProjectId(registryDb, params.project);\n    if (projectId == null) {\n      return {\n        content: [\n          { type: \"text\", text: `Project not found: ${params.project}` },\n        ],\n        isError: true,\n      };\n    }\n\n    const conditions = [\"project_id = ?\"];\n    const queryParams: (string | number)[] = [projectId];\n\n    if (params.status) {\n      conditions.push(\"status = ?\");\n      queryParams.push(params.status);\n    }\n\n    const limit = params.limit ?? 10;\n    queryParams.push(limit);\n\n    const sessions = registryDb\n      .prepare(\n        `SELECT number, date, title, filename, status\n         FROM sessions\n         WHERE ${conditions.join(\" AND \")}\n         ORDER BY number DESC\n         LIMIT ?`\n      )\n      .all(...queryParams) as Array<{\n      number: number;\n      date: string;\n      title: string;\n      filename: string;\n      status: string;\n    }>;\n\n    if (sessions.length === 0) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `No sessions found for project: ${params.project}`,\n          },\n        ],\n      };\n    }\n\n    const lines = sessions.map(\n      (s) =>\n        `#${String(s.number).padStart(4, \"0\")}  ${s.date}  [${s.status}]  ${s.title}\\n        file: Notes/${s.filename}`\n    );\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `${sessions.length} session(s) for ${params.project}:\\n\\n${lines.join(\"\\n\\n\")}`,\n        },\n      ],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `session_list error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: session_route\n// ---------------------------------------------------------------------------\n\nexport interface SessionRouteParams {\n  /** Working directory to route from (defaults to process.cwd()) */\n  cwd?: string;\n  /** Optional conversation context for topic-based fallback routing */\n  context?: string;\n}\n\n/**\n * Automatically suggest which project a session belongs to.\n *\n * Strategy (in priority order):\n *   1. path   — exact or parent-directory match in the project registry\n *   2. marker — walk up from cwd looking for Notes/PAI.md\n *   3. topic  — BM25 keyword search against memory (requires context)\n *\n * Call this at session start (e.g., from CLAUDE.md or a session-start hook)\n * to automatically route the session to the correct project.\n */\nexport async function toolSessionRoute(\n  registryDb: Database,\n  federation: Database | StorageBackend,\n  params: SessionRouteParams\n): Promise<ToolResult> {\n  try {\n    const { autoRoute, formatAutoRouteJson } = await import(\"../../session/auto-route.js\");\n\n    const result = await autoRoute(\n      registryDb,\n      federation,\n      params.cwd,\n      params.context\n    );\n\n    if (!result) {\n      const target = params.cwd ?? process.cwd();\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: [\n              `No project match found for: ${target}`,\n              \"\",\n              \"Tried: path match, PAI.md marker walk\" +\n                (params.context ? \", topic detection\" : \"\"),\n              \"\",\n              \"Run 'pai project add .' to register this directory,\",\n              \"or provide conversation context for topic-based routing.\",\n            ].join(\"\\n\"),\n          },\n        ],\n      };\n    }\n\n    return {\n      content: [{ type: \"text\", text: formatAutoRouteJson(result) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `session_route error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n","/**\n * MCP tool handler: registry_search\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: registry_search\n// ---------------------------------------------------------------------------\n\nexport interface RegistrySearchParams {\n  query: string;\n}\n\nexport function toolRegistrySearch(\n  registryDb: Database,\n  params: RegistrySearchParams\n): ToolResult {\n  try {\n    const q = `%${params.query}%`;\n    const projects = registryDb\n      .prepare(\n        `SELECT id, slug, display_name, root_path, type, status, updated_at\n         FROM projects\n         WHERE slug LIKE ?\n            OR display_name LIKE ?\n            OR root_path LIKE ?\n         ORDER BY updated_at DESC\n         LIMIT 20`\n      )\n      .all(q, q, q) as Array<{\n      id: number;\n      slug: string;\n      display_name: string;\n      root_path: string;\n      type: string;\n      status: string;\n      updated_at: number;\n    }>;\n\n    if (projects.length === 0) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `No projects found matching: \"${params.query}\"`,\n          },\n        ],\n      };\n    }\n\n    const lines = projects.map((p) => `${p.slug}  [${p.status}]  ${p.root_path}`);\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `${projects.length} match(es) for \"${params.query}\":\\n\\n${lines.join(\"\\n\")}`,\n        },\n      ],\n    };\n  } catch (e) {\n    return {\n      content: [\n        { type: \"text\", text: `registry_search error: ${String(e)}` },\n      ],\n      isError: true,\n    };\n  }\n}\n","/**\n * MCP tool handlers: zettel_explore, zettel_health, zettel_surprise,\n *                    zettel_suggest, zettel_converse, zettel_themes,\n *                    zettel_god_notes, zettel_communities\n */\n\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_explore\n// ---------------------------------------------------------------------------\n\nexport interface ZettelExploreParams {\n  start_note: string;\n  depth?: number;\n  direction?: string;\n  mode?: string;\n}\n\nexport async function toolZettelExplore(\n  backend: StorageBackend,\n  params: ZettelExploreParams\n): Promise<ToolResult> {\n  try {\n    const { zettelExplore } = await import(\"../../zettelkasten/index.js\");\n    const result = await zettelExplore(backend, {\n      startNote: params.start_note,\n      depth: params.depth,\n      direction: params.direction as \"forward\" | \"backward\" | \"both\" | undefined,\n      mode: params.mode as \"sequential\" | \"associative\" | \"all\" | undefined,\n    });\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `zettel_explore error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_health\n// ---------------------------------------------------------------------------\n\nexport interface ZettelHealthParams {\n  scope?: string;\n  project_path?: string;\n  recent_days?: number;\n  include?: string[];\n}\n\nexport async function toolZettelHealth(\n  backend: StorageBackend,\n  params: ZettelHealthParams\n): Promise<ToolResult> {\n  try {\n    const { zettelHealth } = await import(\"../../zettelkasten/index.js\");\n    const result = await zettelHealth(backend, {\n      scope: params.scope as \"full\" | \"recent\" | \"project\" | undefined,\n      projectPath: params.project_path,\n      recentDays: params.recent_days,\n      include: params.include as Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\"> | undefined,\n    });\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `zettel_health error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_surprise\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSurpriseParams {\n  reference_path: string;\n  vault_project_id: number;\n  limit?: number;\n  min_similarity?: number;\n  min_graph_distance?: number;\n}\n\nexport async function toolZettelSurprise(\n  backend: StorageBackend,\n  params: ZettelSurpriseParams\n): Promise<ToolResult> {\n  try {\n    const { zettelSurprise } = await import(\"../../zettelkasten/index.js\");\n    const results = await zettelSurprise(backend, {\n      referencePath: params.reference_path,\n      vaultProjectId: params.vault_project_id,\n      limit: params.limit,\n      minSimilarity: params.min_similarity,\n      minGraphDistance: params.min_graph_distance,\n    });\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `zettel_surprise error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_suggest\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSuggestParams {\n  note_path: string;\n  vault_project_id: number;\n  limit?: number;\n  exclude_linked?: boolean;\n}\n\nexport async function toolZettelSuggest(\n  backend: StorageBackend,\n  params: ZettelSuggestParams\n): Promise<ToolResult> {\n  try {\n    const { zettelSuggest } = await import(\"../../zettelkasten/index.js\");\n    const results = await zettelSuggest(backend, {\n      notePath: params.note_path,\n      vaultProjectId: params.vault_project_id,\n      limit: params.limit,\n      excludeLinked: params.exclude_linked,\n    });\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `zettel_suggest error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_converse\n// ---------------------------------------------------------------------------\n\nexport interface ZettelConverseParams {\n  question: string;\n  vault_project_id: number;\n  depth?: number;\n  limit?: number;\n}\n\nexport async function toolZettelConverse(\n  backend: StorageBackend,\n  params: ZettelConverseParams\n): Promise<ToolResult> {\n  try {\n    const { zettelConverse } = await import(\"../../zettelkasten/index.js\");\n    const result = await zettelConverse(backend, {\n      question: params.question,\n      vaultProjectId: params.vault_project_id,\n      depth: params.depth,\n      limit: params.limit,\n    });\n\n    // Query feedback loop: save query + result metadata for future indexing\n    try {\n      const { saveQueryResult } = await import(\"../../zettelkasten/query-feedback.js\");\n      saveQueryResult({\n        query: params.question,\n        timestamp: Date.now(),\n        source: \"zettel_converse\",\n        sourceSlugs: result.relevantNotes.slice(0, 5).map((n) => n.path),\n        answerPreview: result.relevantNotes.slice(0, 3).map((n) => {\n          const title = n.title ?? \"(untitled)\";\n          return `${title}: ${n.snippet.trim().slice(0, 100)}`;\n        }).join(\" | \"),\n        resultCount: result.relevantNotes.length,\n      });\n    } catch {\n      // Non-critical\n    }\n\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `zettel_converse error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_themes\n// ---------------------------------------------------------------------------\n\nexport interface ZettelThemesParams {\n  vault_project_id: number;\n  lookback_days?: number;\n  min_cluster_size?: number;\n  max_themes?: number;\n  similarity_threshold?: number;\n}\n\nexport async function toolZettelThemes(\n  backend: StorageBackend,\n  params: ZettelThemesParams\n): Promise<ToolResult> {\n  try {\n    const { zettelThemes } = await import(\"../../zettelkasten/index.js\");\n    const result = await zettelThemes(backend, {\n      vaultProjectId: params.vault_project_id,\n      lookbackDays: params.lookback_days,\n      minClusterSize: params.min_cluster_size,\n      maxThemes: params.max_themes,\n      similarityThreshold: params.similarity_threshold,\n    });\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `zettel_themes error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_god_notes\n// ---------------------------------------------------------------------------\n\nexport interface ZettelGodNotesParams {\n  limit?: number;\n  min_inbound?: number;\n}\n\nexport async function toolZettelGodNotes(\n  backend: StorageBackend,\n  params: ZettelGodNotesParams\n): Promise<ToolResult> {\n  try {\n    const { zettelGodNotes } = await import(\"../../zettelkasten/index.js\");\n    const result = await zettelGodNotes(backend, {\n      limit: params.limit,\n      minInbound: params.min_inbound,\n    });\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `zettel_god_notes error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_communities\n// ---------------------------------------------------------------------------\n\nexport interface ZettelCommunitiesParams {\n  min_size?: number;\n  max_communities?: number;\n  resolution?: number;\n}\n\nexport async function toolZettelCommunities(\n  backend: StorageBackend,\n  params: ZettelCommunitiesParams\n): Promise<ToolResult> {\n  try {\n    const { zettelCommunities } = await import(\"../../zettelkasten/index.js\");\n    const result = await zettelCommunities(backend, {\n      minSize: params.min_size,\n      maxCommunities: params.max_communities,\n      resolution: params.resolution,\n    });\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `zettel_communities error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n","/**\n * MCP tool handlers for the temporal knowledge graph.\n *\n * Tools:\n *   kg_add            — insert a new (subject, predicate, object) triple\n *   kg_query          — query triples with optional temporal filter\n *   kg_invalidate     — mark a triple as no longer valid (sets valid_to)\n *   kg_contradictions — find (subject, predicate) pairs with multiple valid objects\n */\n\nimport type { Pool } from \"pg\";\nimport type { ToolResult } from \"./types.js\";\nimport {\n  kgAdd,\n  kgQuery,\n  kgInvalidate,\n  kgContradictions,\n} from \"../../memory/kg.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: kg_add\n// ---------------------------------------------------------------------------\n\nexport interface KgAddToolParams {\n  subject: string;\n  predicate: string;\n  object: string;\n  project_id?: number;\n  source_session?: string;\n  confidence?: \"EXTRACTED\" | \"INFERRED\" | \"AMBIGUOUS\";\n}\n\nexport async function toolKgAdd(\n  pool: Pool,\n  params: KgAddToolParams\n): Promise<ToolResult> {\n  try {\n    if (!params.subject || !params.predicate || !params.object) {\n      return {\n        content: [{ type: \"text\", text: \"kg_add error: subject, predicate, and object are required\" }],\n        isError: true,\n      };\n    }\n    const triple = await kgAdd(pool, params);\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(triple, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `kg_add error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_query\n// ---------------------------------------------------------------------------\n\nexport interface KgQueryToolParams {\n  subject?: string;\n  predicate?: string;\n  object?: string;\n  project_id?: number;\n  as_of?: string; // ISO 8601 string; converted to Date\n  include_invalidated?: boolean;\n}\n\nexport async function toolKgQuery(\n  pool: Pool,\n  params: KgQueryToolParams\n): Promise<ToolResult> {\n  try {\n    const asOf = params.as_of ? new Date(params.as_of) : undefined;\n    if (asOf && isNaN(asOf.getTime())) {\n      return {\n        content: [{ type: \"text\", text: `kg_query error: invalid as_of date: ${params.as_of}` }],\n        isError: true,\n      };\n    }\n    const triples = await kgQuery(pool, {\n      subject: params.subject,\n      predicate: params.predicate,\n      object: params.object,\n      project_id: params.project_id,\n      as_of: asOf,\n      include_invalidated: params.include_invalidated,\n    });\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(triples, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `kg_query error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_invalidate\n// ---------------------------------------------------------------------------\n\nexport interface KgInvalidateToolParams {\n  triple_id: number;\n}\n\nexport async function toolKgInvalidate(\n  pool: Pool,\n  params: KgInvalidateToolParams\n): Promise<ToolResult> {\n  try {\n    if (params.triple_id === undefined || params.triple_id === null) {\n      return {\n        content: [{ type: \"text\", text: \"kg_invalidate error: triple_id is required\" }],\n        isError: true,\n      };\n    }\n    await kgInvalidate(pool, params.triple_id);\n    return {\n      content: [{ type: \"text\", text: JSON.stringify({ invalidated: true, triple_id: params.triple_id }) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `kg_invalidate error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_contradictions\n// ---------------------------------------------------------------------------\n\nexport interface KgContradictionsToolParams {\n  subject: string;\n}\n\nexport async function toolKgContradictions(\n  pool: Pool,\n  params: KgContradictionsToolParams\n): Promise<ToolResult> {\n  try {\n    if (!params.subject) {\n      return {\n        content: [{ type: \"text\", text: \"kg_contradictions error: subject is required\" }],\n        isError: true,\n      };\n    }\n    const contradictions = await kgContradictions(pool, params.subject);\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(contradictions, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `kg_contradictions error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n","/**\n * MCP tool handler: memory_wakeup\n *\n * Returns the L0+L1 wake-up context block for a project:\n *   L0 — user identity from ~/.pai/identity.txt\n *   L1 — recent session note highlights (Work Done / Key Decisions / Next Steps)\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { buildWakeupContext } from \"../../memory/wakeup.js\";\nimport { detectProjectFromPath } from \"./types.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Default token budget (constant, overridable per call)\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TOKEN_BUDGET = 800;\n\n// ---------------------------------------------------------------------------\n// Params\n// ---------------------------------------------------------------------------\n\nexport interface MemoryWakeupParams {\n  /** Project slug or absolute path. Omit to auto-detect from process.cwd(). */\n  project?: string;\n  /** L1 token budget. Default 800. */\n  token_budget?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Tool handler\n// ---------------------------------------------------------------------------\n\nexport function toolMemoryWakeup(\n  registryDb: Database,\n  params: MemoryWakeupParams\n): ToolResult {\n  try {\n    const tokenBudget = params.token_budget ?? DEFAULT_TOKEN_BUDGET;\n\n    // Resolve the project root path\n    let rootPath: string | undefined;\n\n    if (params.project) {\n      // Try slug lookup first\n      const bySlug = registryDb\n        .prepare(\"SELECT root_path FROM projects WHERE slug = ?\")\n        .get(params.project) as { root_path: string } | undefined;\n\n      if (bySlug) {\n        rootPath = bySlug.root_path;\n      } else {\n        // Maybe it's an absolute path — try path-based detect\n        const detected = detectProjectFromPath(registryDb, params.project);\n        if (detected) rootPath = detected.root_path;\n      }\n    } else {\n      // Auto-detect from cwd\n      const detected = detectProjectFromPath(registryDb, process.cwd());\n      if (detected) rootPath = detected.root_path;\n    }\n\n    const wakeupBlock = buildWakeupContext(rootPath, tokenBudget);\n\n    if (!wakeupBlock) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: \"No wake-up context available. Create ~/.pai/identity.txt for L0 identity, or ensure session notes exist for L1 story.\",\n          },\n        ],\n      };\n    }\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `WAKEUP CONTEXT\\n\\n${wakeupBlock}`,\n        },\n      ],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `Wakeup context error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n","/**\n * getTaxonomy — returns the shape of stored memory without requiring a query.\n *\n * Answers \"what do I know about?\" not \"what do I know about X?\"\n *\n * Uses the registry DB (projects, sessions) and the storage backend\n * (memory_files, memory_chunks) to build a structural overview.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Return types\n// ---------------------------------------------------------------------------\n\nexport interface TaxonomyProject {\n  slug: string;\n  display_name: string;\n  session_count: number;\n  note_count: number;\n  last_activity: string | null; // ISO date string, e.g. \"2026-04-07\"\n  top_tags: string[];            // project tags from the registry\n}\n\nexport interface TaxonomyTotals {\n  projects: number;\n  sessions: number;\n  notes: number;\n  chunks: number;\n}\n\nexport interface TaxonomyRecentActivity {\n  project_slug: string;\n  action: string;\n  timestamp: string; // ISO date string\n}\n\nexport interface TaxonomyResult {\n  projects: TaxonomyProject[];\n  totals: TaxonomyTotals;\n  recent_activity: TaxonomyRecentActivity[];\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface TaxonomyOptions {\n  /** Include archived projects. Default: false. */\n  include_archived?: boolean;\n  /** Maximum projects to return. Default: 50. */\n  limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Main function\n// ---------------------------------------------------------------------------\n\n/**\n * Build a taxonomy of stored memory — what projects exist, how much is stored,\n * and what has been active recently.\n *\n * Registry queries (projects, sessions) are synchronous (better-sqlite3).\n * Storage backend queries (files, chunks) are async.\n */\nexport async function getTaxonomy(\n  registryDb: Database,\n  storage: StorageBackend,\n  options: TaxonomyOptions = {}\n): Promise<TaxonomyResult> {\n  const includeArchived = options.include_archived ?? false;\n  const limit = options.limit ?? 50;\n\n  // -------------------------------------------------------------------------\n  // 1. Load all (active) projects from the registry\n  // -------------------------------------------------------------------------\n\n  const statusFilter = includeArchived\n    ? \"status IN ('active', 'archived', 'migrating')\"\n    : \"status = 'active'\";\n\n  const projectRows = registryDb\n    .prepare(\n      `SELECT id, slug, display_name, status, created_at, updated_at\n       FROM projects\n       WHERE ${statusFilter}\n       ORDER BY updated_at DESC\n       LIMIT ?`\n    )\n    .all(limit) as Array<{\n    id: number;\n    slug: string;\n    display_name: string;\n    status: string;\n    created_at: number;\n    updated_at: number;\n  }>;\n\n  if (projectRows.length === 0) {\n    return {\n      projects: [],\n      totals: { projects: 0, sessions: 0, notes: 0, chunks: 0 },\n      recent_activity: [],\n    };\n  }\n\n  const projectIds = projectRows.map((p) => p.id);\n\n  // -------------------------------------------------------------------------\n  // 2. Session counts per project (registry, synchronous)\n  // -------------------------------------------------------------------------\n\n  const sessionCountsByProject = new Map<number, number>();\n  const lastSessionDateByProject = new Map<number, string | null>();\n\n  for (const projectId of projectIds) {\n    const countRow = registryDb\n      .prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\")\n      .get(projectId) as { n: number };\n    sessionCountsByProject.set(projectId, countRow.n);\n\n    const lastRow = registryDb\n      .prepare(\n        \"SELECT date FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1\"\n      )\n      .get(projectId) as { date: string } | undefined;\n    lastSessionDateByProject.set(projectId, lastRow?.date ?? null);\n  }\n\n  // -------------------------------------------------------------------------\n  // 3. Tags per project (registry, synchronous)\n  // -------------------------------------------------------------------------\n\n  const tagsByProject = new Map<number, string[]>();\n\n  for (const projectId of projectIds) {\n    const tags = registryDb\n      .prepare(\n        `SELECT t.name\n         FROM tags t\n         JOIN project_tags pt ON pt.tag_id = t.id\n         WHERE pt.project_id = ?\n         ORDER BY t.name`\n      )\n      .all(projectId) as Array<{ name: string }>;\n    tagsByProject.set(projectId, tags.map((t) => t.name));\n  }\n\n  // -------------------------------------------------------------------------\n  // 4. Note and chunk counts per project (storage backend, async)\n  //    We use memory_files for note count (one row per indexed file) and\n  //    memory_chunks for chunk count (may be many per file).\n  //    The StorageBackend interface exposes getStats() for totals but not\n  //    per-project breakdowns, so we cast to the raw DB when it is SQLite\n  //    and fall back to a single getStats() call for Postgres.\n  // -------------------------------------------------------------------------\n\n  const noteCountsByProject = new Map<number, number>();\n  const chunkCountsByProject = new Map<number, number>();\n\n  const isBackend = (x: StorageBackend): boolean => x.backendType === \"sqlite\";\n\n  if (isBackend(storage)) {\n    // SQLite: access raw DB via the getRawDb() escape hatch present on SQLiteBackend.\n    // We reach through the interface via a duck-type check — getRawDb is not on the\n    // interface but is documented as an escape hatch for exactly this kind of work.\n    const rawDb = (storage as unknown as { getRawDb?: () => Database }).getRawDb?.();\n    if (rawDb) {\n      for (const projectId of projectIds) {\n        const noteRow = rawDb\n          .prepare(\n            \"SELECT COUNT(*) AS n FROM memory_files WHERE project_id = ?\"\n          )\n          .get(projectId) as { n: number };\n        noteCountsByProject.set(projectId, noteRow.n);\n\n        const chunkRow = rawDb\n          .prepare(\n            \"SELECT COUNT(*) AS n FROM memory_chunks WHERE project_id = ?\"\n          )\n          .get(projectId) as { n: number };\n        chunkCountsByProject.set(projectId, chunkRow.n);\n      }\n    }\n  } else {\n    // Postgres: the storage backend interface does not expose per-project file/chunk\n    // counts, so we leave them as 0 — totals are still reported via getStats().\n    // Future: add per-project getStats(projectId?) to the interface if needed.\n    for (const projectId of projectIds) {\n      noteCountsByProject.set(projectId, 0);\n      chunkCountsByProject.set(projectId, 0);\n    }\n  }\n\n  // -------------------------------------------------------------------------\n  // 5. Global totals\n  // -------------------------------------------------------------------------\n\n  const stats = await storage.getStats();\n\n  const totalProjects = (\n    registryDb\n      .prepare(\n        `SELECT COUNT(*) AS n FROM projects WHERE ${statusFilter}`\n      )\n      .get() as { n: number }\n  ).n;\n\n  const totalSessions = (\n    registryDb.prepare(\"SELECT COUNT(*) AS n FROM sessions\").get() as { n: number }\n  ).n;\n\n  // -------------------------------------------------------------------------\n  // 6. Recent activity — last 10 sessions across all projects\n  // -------------------------------------------------------------------------\n\n  const recentSessions = registryDb\n    .prepare(\n      `SELECT s.date, s.title, p.slug\n       FROM sessions s\n       JOIN projects p ON p.id = s.project_id\n       WHERE p.${statusFilter.replace(\"status\", \"p.status\")}\n       ORDER BY s.created_at DESC\n       LIMIT 10`\n    )\n    .all() as Array<{ date: string; title: string; slug: string }>;\n\n  const recentActivity: TaxonomyRecentActivity[] = recentSessions.map((row) => ({\n    project_slug: row.slug,\n    action: `session: ${row.title || \"(untitled)\"}`,\n    timestamp: row.date,\n  }));\n\n  // -------------------------------------------------------------------------\n  // 7. Assemble result\n  // -------------------------------------------------------------------------\n\n  const projects: TaxonomyProject[] = projectRows.map((row) => ({\n    slug: row.slug,\n    display_name: row.display_name,\n    session_count: sessionCountsByProject.get(row.id) ?? 0,\n    note_count: noteCountsByProject.get(row.id) ?? 0,\n    last_activity: lastSessionDateByProject.get(row.id) ?? null,\n    top_tags: tagsByProject.get(row.id) ?? [],\n  }));\n\n  return {\n    projects,\n    totals: {\n      projects: totalProjects,\n      sessions: totalSessions,\n      notes: stats.files,\n      chunks: stats.chunks,\n    },\n    recent_activity: recentActivity,\n  };\n}\n","/**\n * MCP tool handler: memory_taxonomy\n *\n * Returns the SHAPE of stored memory without requiring a query.\n * Answers \"what do I know about?\" not \"what do I know about X?\"\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport { getTaxonomy } from \"../../memory/taxonomy.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_taxonomy\n// ---------------------------------------------------------------------------\n\nexport interface MemoryTaxonomyParams {\n  /** Include archived projects in the result. Default: false. */\n  include_archived?: boolean;\n  /** Maximum number of projects to return. Default: 50. */\n  limit?: number;\n}\n\nexport async function toolMemoryTaxonomy(\n  registryDb: Database,\n  storage: StorageBackend,\n  params: MemoryTaxonomyParams = {}\n): Promise<ToolResult> {\n  try {\n    const result = await getTaxonomy(registryDb, storage, {\n      include_archived: params.include_archived,\n      limit: params.limit,\n    });\n\n    // -----------------------------------------------------------------------\n    // Format output as human-readable text\n    // -----------------------------------------------------------------------\n\n    const lines: string[] = [];\n\n    // Totals header\n    lines.push(\n      `PAI Memory Taxonomy — ${result.totals.projects} project(s), ` +\n        `${result.totals.sessions} session(s), ` +\n        `${result.totals.notes} indexed file(s), ` +\n        `${result.totals.chunks} chunk(s)`\n    );\n    lines.push(\"\");\n\n    // Per-project breakdown\n    if (result.projects.length === 0) {\n      lines.push(\"No active projects found.\");\n    } else {\n      lines.push(\"Projects:\");\n      for (const p of result.projects) {\n        const tagStr = p.top_tags.length > 0 ? ` [${p.top_tags.join(\", \")}]` : \"\";\n        const activityStr = p.last_activity ? ` last: ${p.last_activity}` : \"\";\n        lines.push(\n          `  ${p.slug} — ${p.display_name}` +\n            `  sessions=${p.session_count}` +\n            (p.note_count > 0 ? ` files=${p.note_count}` : \"\") +\n            activityStr +\n            tagStr\n        );\n      }\n    }\n\n    // Recent activity\n    if (result.recent_activity.length > 0) {\n      lines.push(\"\");\n      lines.push(\"Recent activity (last 10 sessions across all projects):\");\n      for (const a of result.recent_activity) {\n        lines.push(`  ${a.timestamp}  ${a.project_slug}  ${a.action}`);\n      }\n    }\n\n    return {\n      content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `memory_taxonomy error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n","/**\n * tunnels.ts — cross-project concept detection (\"palace graph / tunnel detection\")\n *\n * A \"tunnel\" is a concept (word or short phrase) that appears in chunks from\n * at least two distinct projects.  These serendipitous cross-project connections\n * are surfaced so the user can discover unexpected relationships between their\n * work streams.\n *\n * Algorithm:\n *  1. Pull the top-N most frequent significant terms from memory_chunks via BM25 FTS.\n *     We use the FTS5 vocab table (if available) or fall back to term frequency\n *     aggregation over the raw text via a trigram approach.\n *  2. For each candidate term, count how many distinct projects have at least one\n *     chunk containing it and aggregate occurrence stats.\n *  3. Filter by min_projects and min_occurrences, sort by project breadth then\n *     frequency, return top limit results.\n *\n * Backend support:\n *  - SQLite  — uses `memory_fts` MATCH to count per-project occurrences.\n *  - Postgres — uses `memory_chunks` tsvector + ts_stat for term extraction and\n *               per-project term frequency counting via plainto_tsquery.\n */\n\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface Tunnel {\n  /** The shared term or phrase. */\n  concept: string;\n  /** Project slugs where this concept appears. */\n  projects: string[];\n  /** Total chunk occurrences across all projects. */\n  occurrences: number;\n  /** First time the concept appeared (Unix ms). */\n  first_seen: number;\n  /** Most recent time the concept appeared (Unix ms). */\n  last_seen: number;\n}\n\nexport interface FindTunnelsOptions {\n  /** Minimum distinct projects a concept must appear in. Default 2. */\n  min_projects?: number;\n  /** Minimum total chunk occurrences across all projects. Default 3. */\n  min_occurrences?: number;\n  /** Maximum number of tunnels to return. Default 20. */\n  limit?: number;\n}\n\nexport interface FindTunnelsResult {\n  tunnels: Tunnel[];\n  projects_analyzed: number;\n  total_concepts_evaluated: number;\n}\n\n// ---------------------------------------------------------------------------\n// Internal row types\n// ---------------------------------------------------------------------------\n\ninterface ProjectSlugMap {\n  [id: number]: string;\n}\n\n// ---------------------------------------------------------------------------\n// SQLite implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Extract candidate terms from the SQLite FTS5 index using the vocabulary\n * approach: iterate the fts5vocab table (if it exists) for the most common\n * terms, then per-term count distinct projects.\n */\nasync function findTunnelsSqlite(\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  db: any,\n  slugMap: ProjectSlugMap,\n  opts: Required<FindTunnelsOptions>\n): Promise<FindTunnelsResult> {\n  const projectIds = Object.keys(slugMap).map(Number);\n  if (projectIds.length < 2) {\n    return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n  }\n\n  // Step 1 — collect candidate terms via FTS5 vocabulary shadow table.\n  // memory_fts uses the standard fts5 content table; the vocab view is\n  // \"memory_fts_v\" if it was created, otherwise we fall back to sampling.\n  let candidateTerms: string[] = [];\n\n  try {\n    // fts5vocab \"col\" mode: (term, col, doc, cnt) — aggregate all cols.\n    const vocabRows = db\n      .prepare(\n        `SELECT term, SUM(doc) AS doc_count, SUM(cnt) AS total_cnt\n         FROM memory_fts_v\n         GROUP BY term\n         HAVING SUM(cnt) >= ?\n         ORDER BY SUM(doc) DESC\n         LIMIT 500`\n      )\n      .all(opts.min_occurrences) as Array<{ term: string; doc_count: number; total_cnt: number }>;\n\n    candidateTerms = vocabRows\n      .map((r) => r.term)\n      .filter((t) => t.length >= 3 && !STOP_WORDS.has(t));\n  } catch {\n    // Vocab table not available — fall back to sampling common words from chunks.\n    // We aggregate word tokens from a sample of chunks using SQLite string ops.\n    // This is slower but works on any SQLite federation.db.\n    const sampleRows = db\n      .prepare(\n        `SELECT LOWER(text) AS text FROM memory_chunks\n         WHERE LENGTH(text) > 20\n         ORDER BY RANDOM()\n         LIMIT 2000`\n      )\n      .all() as Array<{ text: string }>;\n\n    const freq = new Map<string, number>();\n    for (const { text } of sampleRows) {\n      const tokens = text\n        .split(/[\\s\\p{P}]+/u)\n        .filter(Boolean)\n        .filter((t: string) => t.length >= 3 && !STOP_WORDS.has(t));\n      for (const t of tokens) {\n        freq.set(t, (freq.get(t) ?? 0) + 1);\n      }\n    }\n    candidateTerms = [...freq.entries()]\n      .filter(([, n]) => n >= opts.min_occurrences)\n      .sort((a, b) => b[1] - a[1])\n      .slice(0, 200)\n      .map(([t]) => t);\n  }\n\n  if (candidateTerms.length === 0) {\n    return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n  }\n\n  // Step 2 — for each candidate, count distinct projects and occurrences.\n  const tunnels: Tunnel[] = [];\n\n  for (const term of candidateTerms) {\n    try {\n      const rows = db\n        .prepare(\n          `SELECT c.project_id, COUNT(*) AS cnt,\n                  MIN(c.updated_at) AS first_seen,\n                  MAX(c.updated_at) AS last_seen\n           FROM memory_fts f\n           JOIN memory_chunks c ON c.id = f.id\n           WHERE memory_fts MATCH ?\n             AND c.project_id IN (${projectIds.map(() => \"?\").join(\", \")})\n           GROUP BY c.project_id`\n        )\n        .all(`\"${term.replace(/\"/g, '\"\"')}\"`, ...projectIds) as Array<{\n          project_id: number;\n          cnt: number;\n          first_seen: number;\n          last_seen: number;\n        }>;\n\n      if (rows.length < opts.min_projects) continue;\n\n      const totalOccurrences = rows.reduce((s, r) => s + Number(r.cnt), 0);\n      if (totalOccurrences < opts.min_occurrences) continue;\n\n      const projects = rows\n        .map((r) => slugMap[r.project_id] ?? String(r.project_id))\n        .filter(Boolean);\n      const firstSeen = Math.min(...rows.map((r) => r.first_seen));\n      const lastSeen = Math.max(...rows.map((r) => r.last_seen));\n\n      tunnels.push({\n        concept: term,\n        projects,\n        occurrences: totalOccurrences,\n        first_seen: firstSeen,\n        last_seen: lastSeen,\n      });\n    } catch {\n      // Skip problematic terms\n      continue;\n    }\n  }\n\n  // Sort: most cross-project spread first, then by raw frequency.\n  tunnels.sort((a, b) => {\n    const byProjects = b.projects.length - a.projects.length;\n    if (byProjects !== 0) return byProjects;\n    return b.occurrences - a.occurrences;\n  });\n\n  return {\n    tunnels: tunnels.slice(0, opts.limit),\n    projects_analyzed: projectIds.length,\n    total_concepts_evaluated: candidateTerms.length,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Postgres implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Use Postgres ts_stat() + plainto_tsquery to efficiently find terms that\n * appear across multiple projects.\n */\nasync function findTunnelsPostgres(\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  pool: any,\n  slugMap: ProjectSlugMap,\n  opts: Required<FindTunnelsOptions>\n): Promise<FindTunnelsResult> {\n  const projectIds = Object.keys(slugMap).map(Number);\n  if (projectIds.length < 2) {\n    return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n  }\n\n  // Step 1 — extract top terms from the corpus using ts_stat over all chunks.\n  // ts_stat(query) returns (word, ndoc, nentry) for each lexeme in the tsvector.\n  const termResult = await pool.query<{ word: string; ndoc: string; nentry: string }>(\n    `SELECT word, ndoc, nentry\n     FROM ts_stat(\n       'SELECT to_tsvector(''simple'', text) FROM memory_chunks WHERE project_id = ANY($1)'\n     )\n     WHERE length(word) >= 3\n       AND nentry >= $2\n     ORDER BY ndoc DESC\n     LIMIT 500`,\n    [projectIds, opts.min_occurrences]\n  );\n\n  let candidateTerms = termResult.rows\n    .map((r) => r.word)\n    .filter((t) => !STOP_WORDS.has(t));\n\n  if (candidateTerms.length === 0) {\n    return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n  }\n\n  // Cap at 200 candidates for performance.\n  candidateTerms = candidateTerms.slice(0, 200);\n\n  // Step 2 — for each candidate, count distinct projects via a single batched query.\n  // We use a VALUES list + JOIN to avoid N+1 round-trips.\n  const valuesClause = candidateTerms\n    .map((t, i) => `($${i + 2}::text)`)\n    .join(\", \");\n\n  const batchResult = await pool.query<{\n    concept: string;\n    project_id: string;\n    cnt: string;\n    first_seen: string;\n    last_seen: string;\n  }>(\n    `SELECT v.concept, c.project_id::text, COUNT(*) AS cnt,\n            MIN(c.updated_at) AS first_seen,\n            MAX(c.updated_at) AS last_seen\n     FROM (VALUES ${valuesClause}) AS v(concept)\n     JOIN memory_chunks c\n       ON to_tsvector('simple', c.text) @@ plainto_tsquery('simple', v.concept)\n      AND c.project_id = ANY($1)\n     GROUP BY v.concept, c.project_id`,\n    [projectIds, ...candidateTerms]\n  );\n\n  // Aggregate by concept.\n  const byConceptMap = new Map<\n    string,\n    { projects: Set<number>; occurrences: number; firstSeen: number; lastSeen: number }\n  >();\n\n  for (const row of batchResult.rows) {\n    const existing = byConceptMap.get(row.concept) ?? {\n      projects: new Set<number>(),\n      occurrences: 0,\n      firstSeen: Infinity,\n      lastSeen: -Infinity,\n    };\n    existing.projects.add(parseInt(row.project_id, 10));\n    existing.occurrences += parseInt(row.cnt, 10);\n    const fs = parseInt(row.first_seen, 10);\n    const ls = parseInt(row.last_seen, 10);\n    if (fs < existing.firstSeen) existing.firstSeen = fs;\n    if (ls > existing.lastSeen) existing.lastSeen = ls;\n    byConceptMap.set(row.concept, existing);\n  }\n\n  const tunnels: Tunnel[] = [];\n  for (const [concept, data] of byConceptMap) {\n    if (data.projects.size < opts.min_projects) continue;\n    if (data.occurrences < opts.min_occurrences) continue;\n\n    const projects = [...data.projects]\n      .map((id) => slugMap[id] ?? String(id))\n      .filter(Boolean);\n\n    tunnels.push({\n      concept,\n      projects,\n      occurrences: data.occurrences,\n      first_seen: data.firstSeen === Infinity ? 0 : data.firstSeen,\n      last_seen: data.lastSeen === -Infinity ? 0 : data.lastSeen,\n    });\n  }\n\n  tunnels.sort((a, b) => {\n    const byProjects = b.projects.length - a.projects.length;\n    if (byProjects !== 0) return byProjects;\n    return b.occurrences - a.occurrences;\n  });\n\n  return {\n    tunnels: tunnels.slice(0, opts.limit),\n    projects_analyzed: projectIds.length,\n    total_concepts_evaluated: candidateTerms.length,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Find cross-project concept tunnels.\n *\n * Works with both SQLite and Postgres storage backends.\n * Requires the `registryDb` (better-sqlite3) for project slug resolution.\n *\n * @param backend        Active PAI storage backend.\n * @param registryDb     Registry database for project slug resolution.\n * @param options        Filter and limit options.\n */\nexport async function findTunnels(\n  backend: StorageBackend,\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  registryDb: any,\n  options?: FindTunnelsOptions\n): Promise<FindTunnelsResult> {\n  const opts: Required<FindTunnelsOptions> = {\n    min_projects: options?.min_projects ?? 2,\n    min_occurrences: options?.min_occurrences ?? 3,\n    limit: options?.limit ?? 20,\n  };\n\n  // Build project slug map from registry.\n  const projectRows = registryDb\n    .prepare(\"SELECT id, slug FROM projects WHERE status != 'archived'\")\n    .all() as Array<{ id: number; slug: string }>;\n\n  const slugMap: ProjectSlugMap = {};\n  for (const { id, slug } of projectRows) {\n    slugMap[id] = slug;\n  }\n\n  if (backend.backendType === \"postgres\") {\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const pool = (backend as any).getPool?.();\n    if (!pool) {\n      throw new Error(\"findTunnels: Postgres backend does not expose getPool()\");\n    }\n    return findTunnelsPostgres(pool, slugMap, opts);\n  }\n\n  // SQLite path — access raw db through the backend adapter.\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const rawDb = (backend as any).getRawDb?.();\n  if (!rawDb) {\n    throw new Error(\"findTunnels: SQLite backend does not expose getRawDb()\");\n  }\n  return findTunnelsSqlite(rawDb, slugMap, opts);\n}\n","/**\n * MCP tool handler: memory_tunnels\n *\n * Finds \"tunnels\" — concepts that appear across multiple projects —\n * surfacing cross-project serendipitous connections in the PAI memory graph.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { ToolResult } from \"./types.js\";\nimport { findTunnels } from \"../../memory/tunnels.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_tunnels\n// ---------------------------------------------------------------------------\n\nexport interface MemoryTunnelsParams {\n  /** Minimum distinct projects a concept must appear in. Default 2. */\n  min_projects?: number;\n  /** Minimum total chunk occurrences across all projects. Default 3. */\n  min_occurrences?: number;\n  /** Maximum number of tunnels to return. Default 20. */\n  limit?: number;\n}\n\nexport async function toolMemoryTunnels(\n  registryDb: Database,\n  backend: StorageBackend,\n  params: MemoryTunnelsParams\n): Promise<ToolResult> {\n  try {\n    const result = await findTunnels(backend, registryDb, {\n      min_projects: params.min_projects,\n      min_occurrences: params.min_occurrences,\n      limit: params.limit,\n    });\n\n    return {\n      content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `memory_tunnels error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n","/**\n * MCP tool handler: memory_feedback\n *\n * Implements the MR2 feedback weight loop:\n * - Accepts chunk IDs and a rating (1-5)\n * - Applies EMA update to memory_chunks.relevance_score\n * - Finds entity names mentioned in those chunks and updates kg_entities.feedback_weight\n *\n * EMA formula: new = old + alpha * (normalized_rating - old)\n * where normalized_rating = (rating - 1) / 4  →  maps [1,5] to [0,1]\n * and alpha = 0.1\n *\n * Integration into search scoring:\n *   final_score *= (0.5 + relevance_score)\n * This multiplier ranges from 0.5 (relevance_score=0) to 1.5 (relevance_score=1),\n * giving a ±50% boost/penalty based on accumulated feedback.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { ToolResult } from \"./types.js\";\nimport { listKgEntities, updateEntityFeedbackWeight } from \"../../memory/kg-entity.js\";\n\n// EMA learning rate\nconst FEEDBACK_ALPHA = 0.1;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MemoryFeedbackParams {\n  /** Array of chunk IDs to apply feedback to */\n  chunk_ids: string[];\n  /** Rating from 1 (not relevant) to 5 (highly relevant) */\n  rating: number;\n  /** Optional tenant ID for entity feedback scoping. Default: \"default\" */\n  tenant_id?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Tool handler\n// ---------------------------------------------------------------------------\n\n/**\n * Apply relevance feedback to memory chunks and associated KG entities.\n *\n * @param db      Federation SQLite database\n * @param params  Feedback parameters\n */\nexport function toolMemoryFeedback(\n  db: Database,\n  params: MemoryFeedbackParams\n): ToolResult {\n  try {\n    if (!Array.isArray(params.chunk_ids) || params.chunk_ids.length === 0) {\n      return {\n        content: [{ type: \"text\", text: \"memory_feedback error: chunk_ids must be a non-empty array\" }],\n        isError: true,\n      };\n    }\n\n    const rating = params.rating;\n    if (typeof rating !== \"number\" || rating < 1 || rating > 5) {\n      return {\n        content: [{ type: \"text\", text: \"memory_feedback error: rating must be a number between 1 and 5\" }],\n        isError: true,\n      };\n    }\n\n    // Normalize rating [1,5] → [0,1]\n    const normalizedRating = (rating - 1) / 4;\n\n    // Fetch current relevance scores for the given chunk IDs\n    const placeholders = params.chunk_ids.map(() => \"?\").join(\", \");\n    const chunks = db.prepare(\n      `SELECT id, text, relevance_score FROM memory_chunks WHERE id IN (${placeholders})`\n    ).all(...params.chunk_ids) as Array<{ id: string; text: string; relevance_score: number | null }>;\n\n    if (chunks.length === 0) {\n      return {\n        content: [{ type: \"text\", text: \"memory_feedback: no matching chunks found\" }],\n        isError: true,\n      };\n    }\n\n    let updatedChunks = 0;\n    const combinedText: string[] = [];\n\n    // Apply EMA update to each chunk's relevance_score\n    const updateStmt = db.prepare(\n      \"UPDATE memory_chunks SET relevance_score = ? WHERE id = ?\"\n    );\n\n    for (const chunk of chunks) {\n      const oldScore = chunk.relevance_score ?? 0.5;\n      const newScore = oldScore + FEEDBACK_ALPHA * (normalizedRating - oldScore);\n      updateStmt.run(newScore, chunk.id);\n      updatedChunks++;\n      combinedText.push(chunk.text);\n    }\n\n    // Find entity mentions in updated chunks and apply EMA to kg_entities.feedback_weight\n    const tenantId = params.tenant_id ?? \"default\";\n    const entities = listKgEntities(db, tenantId, undefined, 500);\n    const text = combinedText.join(\"\\n\").toLowerCase();\n\n    let updatedEntities = 0;\n    for (const entity of entities) {\n      if (text.includes(entity.name.toLowerCase())) {\n        updateEntityFeedbackWeight(db, entity.entity_id, normalizedRating, FEEDBACK_ALPHA);\n        updatedEntities++;\n      }\n    }\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: JSON.stringify({\n            updated_chunks: updatedChunks,\n            updated_entities: updatedEntities,\n            rating,\n            normalized_rating: normalizedRating,\n          }),\n        },\n      ],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `memory_feedback error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n","/**\n * kg-search.ts — Graph-completion retrieval (MR1).\n *\n * Implements graphCompletionSearch: a multi-phase retrieval strategy that\n * combines vector search with knowledge-graph neighborhood expansion.\n *\n * Algorithm:\n *   Phase 1: Wide vector search (50 candidates) using queryVec\n *   Phase 2: Extract entity mentions from retrieved chunks (substring match vs kg_entities)\n *   Phase 3: BFS neighborhood expansion in kg_triples (1-2 hops from matched entities)\n *   Phase 4: Re-rank all collected triples against query embedding using cosine similarity\n *\n * The result is a ranked list of KgTriple objects most relevant to the query,\n * augmented with graph-derived context that pure vector search would miss.\n */\n\nimport type { Pool } from \"pg\";\nimport type { Database } from \"better-sqlite3\";\nimport { cosineSimilarity, deserializeEmbedding } from \"./embeddings.js\";\nimport type { SearchResult, SearchOptions } from \"./search.js\";\nimport type { KgTriple } from \"./kg.js\";\nimport { kgQuery } from \"./kg.js\";\nimport { listKgEntities } from \"./kg-entity.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface GraphCompletionResult {\n  /** Ranked KG triples relevant to the query */\n  triples: Array<KgTriple & { relevanceScore: number }>;\n  /** Chunks from the initial wide vector search (Phase 1) */\n  seedChunks: SearchResult[];\n  /** Entity names that were matched and used for BFS expansion */\n  expandedEntities: string[];\n}\n\nexport interface GraphCompletionOptions {\n  /** Number of seed chunks to fetch in Phase 1. Default: 50 */\n  seedCount?: number;\n  /** BFS hop depth for neighborhood expansion. Default: 1 */\n  hops?: number;\n  /** Maximum triples to return after re-ranking. Default: 20 */\n  maxTriples?: number;\n  /** Postgres project_id filter for triples. */\n  projectId?: number;\n  /** Tenant ID for entity lookup in federation.db. Default: \"default\" */\n  tenantId?: string;\n  /** StorageBackend search options to pass to Phase 1 */\n  searchOpts?: SearchOptions;\n}\n\n// ---------------------------------------------------------------------------\n// Phase 2: Entity mention extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Find entity names (from the federation.db kg_entities table) that appear\n * as substrings in the given text chunks.\n *\n * Returns a deduplicated list of matching entity names.\n */\nfunction extractEntityMentions(\n  federationDb: Database,\n  chunks: SearchResult[],\n  tenantId = \"default\"\n): string[] {\n  // Load all entity names for this tenant (sorted by length desc so longer\n  // entity names are matched before substrings)\n  const entities = listKgEntities(federationDb, tenantId, undefined, 500);\n  if (entities.length === 0) return [];\n\n  const combinedText = chunks.map((c) => c.snippet).join(\"\\n\").toLowerCase();\n  const matched = new Set<string>();\n\n  for (const entity of entities) {\n    if (combinedText.includes(entity.name.toLowerCase())) {\n      matched.add(entity.name);\n    }\n  }\n\n  return Array.from(matched);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 3: BFS neighborhood expansion\n// ---------------------------------------------------------------------------\n\n/**\n * Perform BFS over kg_triples to find neighbors of the given entity names.\n *\n * For each entity name, fetches triples where the entity is the subject\n * or the object (bi-directional expansion). Supports 1 or 2 hop depths.\n *\n * Returns all unique triples found in the BFS expansion.\n */\nasync function bfsExpand(\n  pool: Pool,\n  entityNames: string[],\n  hops: number,\n  projectId?: number\n): Promise<KgTriple[]> {\n  const seen = new Set<number>(); // triple IDs already included\n  const tripleMap = new Map<number, KgTriple>();\n  const frontier = new Set(entityNames);\n\n  for (let hop = 0; hop < hops; hop++) {\n    const nextFrontier = new Set<string>();\n\n    for (const name of frontier) {\n      // Expand as subject\n      const asSubject = await kgQuery(pool, {\n        subject: name,\n        project_id: projectId,\n      });\n\n      // Expand as object\n      const asObject = await kgQuery(pool, {\n        object: name,\n        project_id: projectId,\n      });\n\n      for (const triple of [...asSubject, ...asObject]) {\n        if (!seen.has(triple.id)) {\n          seen.add(triple.id);\n          tripleMap.set(triple.id, triple);\n          // Add neighbors to next frontier for deeper hops\n          nextFrontier.add(triple.subject);\n          nextFrontier.add(triple.object);\n        }\n      }\n    }\n\n    // Next hop frontier: only new names not already processed\n    frontier.clear();\n    for (const name of nextFrontier) {\n      if (!entityNames.includes(name)) {\n        frontier.add(name);\n        entityNames.push(name);\n      }\n    }\n  }\n\n  return Array.from(tripleMap.values());\n}\n\n// ---------------------------------------------------------------------------\n// Phase 4: Re-rank triples against query embedding\n// ---------------------------------------------------------------------------\n\n/**\n * Score each triple by cosine similarity to the query embedding.\n *\n * The triple text representation is: \"subject predicate object\"\n * We embed this on-the-fly using generateEmbedding(), which may use a cache.\n *\n * Returns triples sorted by relevance score descending.\n */\nasync function rerankTriples(\n  triples: KgTriple[],\n  queryVec: Float32Array,\n  maxTriples: number\n): Promise<Array<KgTriple & { relevanceScore: number }>> {\n  if (triples.length === 0) return [];\n\n  const { generateEmbedding } = await import(\"./embeddings.js\");\n\n  const scored = await Promise.all(\n    triples.map(async (triple) => {\n      const text = `${triple.subject} ${triple.predicate} ${triple.object}`;\n      try {\n        const vec = await generateEmbedding(text, false);\n        const score = cosineSimilarity(queryVec, vec);\n        return { ...triple, relevanceScore: score };\n      } catch {\n        return { ...triple, relevanceScore: 0 };\n      }\n    })\n  );\n\n  return scored\n    .sort((a, b) => b.relevanceScore - a.relevanceScore)\n    .slice(0, maxTriples);\n}\n\n// ---------------------------------------------------------------------------\n// Main: graphCompletionSearch\n// ---------------------------------------------------------------------------\n\n/**\n * Graph-completion retrieval combining vector search with KG neighborhood BFS.\n *\n * @param federationDb  SQLite federation.db (for chunk/entity lookup)\n * @param pool          Postgres connection pool (for kg_triples queries)\n * @param queryVec      Pre-computed embedding for the search query\n * @param opts          Configuration options\n */\nexport async function graphCompletionSearch(\n  federationDb: Database,\n  pool: Pool,\n  queryVec: Float32Array,\n  opts: GraphCompletionOptions = {}\n): Promise<GraphCompletionResult> {\n  const {\n    seedCount = 50,\n    hops = 1,\n    maxTriples = 20,\n    projectId,\n    tenantId = \"default\",\n    searchOpts,\n  } = opts;\n\n  // Phase 1: Wide vector search over federation.db for seed chunks\n  const seedSearchOpts: SearchOptions = {\n    ...searchOpts,\n    maxResults: seedCount,\n    projectIds: projectId != null ? [projectId] : searchOpts?.projectIds,\n  };\n\n  let seedChunks: SearchResult[] = [];\n  try {\n    const { searchMemorySemantic } = await import(\"./search.js\");\n    seedChunks = searchMemorySemantic(federationDb, queryVec, seedSearchOpts);\n  } catch (e) {\n    process.stderr.write(`[kg-search] Phase 1 seed search error: ${e}\\n`);\n  }\n\n  if (seedChunks.length === 0) {\n    return { triples: [], seedChunks: [], expandedEntities: [] };\n  }\n\n  // Phase 2: Extract entity mentions from seed chunks\n  const expandedEntities = extractEntityMentions(federationDb, seedChunks, tenantId);\n\n  if (expandedEntities.length === 0) {\n    // No entities matched — return empty graph results (seed chunks still useful)\n    return { triples: [], seedChunks, expandedEntities: [] };\n  }\n\n  // Phase 3: BFS neighborhood expansion in kg_triples\n  let expandedTriples: KgTriple[] = [];\n  try {\n    expandedTriples = await bfsExpand(pool, [...expandedEntities], hops, projectId);\n  } catch (e) {\n    process.stderr.write(`[kg-search] Phase 3 BFS expansion error: ${e}\\n`);\n  }\n\n  if (expandedTriples.length === 0) {\n    return { triples: [], seedChunks, expandedEntities };\n  }\n\n  // Phase 4: Re-rank triples against query embedding\n  let rankedTriples: Array<KgTriple & { relevanceScore: number }> = [];\n  try {\n    rankedTriples = await rerankTriples(expandedTriples, queryVec, maxTriples);\n  } catch (e) {\n    process.stderr.write(`[kg-search] Phase 4 rerank error: ${e}\\n`);\n    // Fall back to unranked triples\n    rankedTriples = expandedTriples.slice(0, maxTriples).map((t) => ({\n      ...t,\n      relevanceScore: 0,\n    }));\n  }\n\n  return { triples: rankedTriples, seedChunks, expandedEntities };\n}\n","/**\n * MCP tool handler: memory_kg_search\n *\n * Exposes graphCompletionSearch as an MCP tool (MR1 — graph-completion retrieval).\n *\n * Algorithm: wide vector search → entity mention extraction → BFS neighborhood\n * expansion in kg_triples → re-rank triples by cosine similarity to query.\n *\n * Requires:\n *   - federation.db (SQLite) for chunk and entity lookups\n *   - Postgres pool for kg_triples BFS expansion\n *\n * Returns ranked KG triples with relevanceScore, plus seed chunk metadata.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { Pool } from \"pg\";\nimport type { ToolResult } from \"./types.js\";\nimport { graphCompletionSearch } from \"../../memory/kg-search.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MemoryKgSearchParams {\n  /** Free-text query — used to generate embedding for Phase 1 vector search. */\n  query: string;\n  /** Restrict seed vector search to a specific project ID. Optional. */\n  project_id?: number;\n  /** Number of seed chunks from Phase 1 vector search. Default: 50 */\n  wide_k?: number;\n  /** BFS hop depth for KG neighborhood expansion. Default: 1 */\n  neighborhood_depth?: number;\n  /** Maximum triples to return after Phase 4 re-ranking. Default: 20 */\n  top_k?: number;\n  /** Tenant ID for entity lookup in federation.db. Default: \"default\" */\n  tenant_id?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Tool handler\n// ---------------------------------------------------------------------------\n\n/**\n * Graph-completion search: vector search + KG neighborhood BFS.\n *\n * @param federationDb  SQLite federation.db for chunk/entity operations\n * @param pool          Postgres pool for kg_triples BFS expansion\n * @param params        Tool parameters\n */\nexport async function toolMemoryKgSearch(\n  federationDb: Database,\n  pool: Pool,\n  params: MemoryKgSearchParams\n): Promise<ToolResult> {\n  try {\n    if (!params.query || typeof params.query !== \"string\" || params.query.trim() === \"\") {\n      return {\n        content: [{ type: \"text\", text: \"memory_kg_search error: query is required\" }],\n        isError: true,\n      };\n    }\n\n    const { generateEmbedding } = await import(\"../../memory/embeddings.js\");\n    const queryVec = await generateEmbedding(params.query, true);\n\n    const result = await graphCompletionSearch(federationDb, pool, queryVec, {\n      seedCount: params.wide_k ?? 50,\n      hops: params.neighborhood_depth ?? 1,\n      maxTriples: params.top_k ?? 20,\n      projectId: params.project_id,\n      tenantId: params.tenant_id ?? \"default\",\n    });\n\n    if (result.triples.length === 0) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `No KG triples found for query: \"${params.query}\"\\n` +\n              `Seed chunks: ${result.seedChunks.length}, ` +\n              `Expanded entities: ${result.expandedEntities.length}`,\n          },\n        ],\n      };\n    }\n\n    const tripleLines = result.triples\n      .map((t, i) =>\n        `[${i + 1}] (score=${t.relevanceScore.toFixed(4)}) ${t.subject} — ${t.predicate} — ${t.object}`\n      )\n      .join(\"\\n\");\n\n    const entityList = result.expandedEntities.slice(0, 10).join(\", \");\n    const summary = [\n      `Found ${result.triples.length} KG triple(s) for \"${params.query}\"`,\n      `Seed chunks: ${result.seedChunks.length}`,\n      `Expanded entities (${result.expandedEntities.length}): ${entityList}`,\n      \"\",\n      tripleLines,\n    ].join(\"\\n\");\n\n    return {\n      content: [{ type: \"text\", text: summary }],\n    };\n  } catch (e) {\n    return {\n      content: [{ type: \"text\", text: `memory_kg_search error: ${String(e)}` }],\n      isError: true,\n    };\n  }\n}\n"],"mappings":"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"}