Selaa lähdekoodia

refactor: move CLI and MCP to subdirectories, MCP consumes SDK

Move frontends into src/cli/ and src/mcp/ to separate them from the
core library. The MCP server is fully rewritten to import only from
the SDK (src/index.ts) — zero direct store.ts/collections.ts/llm.ts
access.

- src/qmd.ts → src/cli/qmd.ts
- src/formatter.ts → src/cli/formatter.ts
- src/mcp.ts → src/mcp/server.ts (rewritten to use QMDStore SDK)
- New src/maintenance.ts: Maintenance class for CLI housekeeping
- SDK gains: getDocumentBody(), getDefaultCollectionNames(),
  extractSnippet/addLineNumbers/DEFAULT_MULTI_GET_MAX_BYTES exports,
  getDefaultDbPath re-export, InternalStore type export
- package.json bin/scripts updated for new paths
- All 692 tests pass
Tobi Lutke 2 kuukautta sitten
vanhempi
commit
c68904fe08
14 muutettua tiedostoa jossa 197 lisäystä ja 106 poistoa
  1. 1 1
      CLAUDE.md
  2. 9 9
      package.json
  3. 2 2
      src/cli/formatter.ts
  4. 14 14
      src/cli/qmd.ts
  5. 37 1
      src/index.ts
  6. 19 0
      src/llm.ts
  7. 54 0
      src/maintenance.ts
  8. 50 69
      src/mcp/server.ts
  9. 3 2
      src/store.ts
  10. 2 2
      test/cli.test.ts
  11. 3 3
      test/eval-harness.ts
  12. 1 1
      test/formatter.test.ts
  13. 1 1
      test/intent.test.ts
  14. 1 1
      test/mcp.test.ts

+ 1 - 1
CLAUDE.md

@@ -118,7 +118,7 @@ qmd multi-get "#abc123, #def456"
 ## Development
 
 ```sh
-bun src/qmd.ts <command>   # Run from source
+bun src/cli/qmd.ts <command>   # Run from source
 bun link               # Install globally as 'qmd'
 ```
 

+ 9 - 9
package.json

@@ -12,7 +12,7 @@
     }
   },
   "bin": {
-    "qmd": "dist/qmd.js"
+    "qmd": "dist/cli/qmd.js"
   },
   "files": [
     "dist/",
@@ -21,15 +21,15 @@
   ],
   "scripts": {
     "prepare": "[ -d .git ] && ./scripts/install-hooks.sh || true",
-    "build": "tsc -p tsconfig.build.json && printf '#!/usr/bin/env node\n' | cat - dist/qmd.js > dist/qmd.tmp && mv dist/qmd.tmp dist/qmd.js && chmod +x dist/qmd.js",
+    "build": "tsc -p tsconfig.build.json && printf '#!/usr/bin/env node\n' | cat - dist/cli/qmd.js > dist/cli/qmd.tmp && mv dist/cli/qmd.tmp dist/cli/qmd.js && chmod +x dist/cli/qmd.js",
     "test": "vitest run --reporter=verbose test/",
-    "qmd": "tsx src/qmd.ts",
-    "index": "tsx src/qmd.ts index",
-    "vector": "tsx src/qmd.ts vector",
-    "search": "tsx src/qmd.ts search",
-    "vsearch": "tsx src/qmd.ts vsearch",
-    "rerank": "tsx src/qmd.ts rerank",
-    "inspector": "npx @modelcontextprotocol/inspector tsx src/qmd.ts mcp",
+    "qmd": "tsx src/cli/qmd.ts",
+    "index": "tsx src/cli/qmd.ts index",
+    "vector": "tsx src/cli/qmd.ts vector",
+    "search": "tsx src/cli/qmd.ts search",
+    "vsearch": "tsx src/cli/qmd.ts vsearch",
+    "rerank": "tsx src/cli/qmd.ts rerank",
+    "inspector": "npx @modelcontextprotocol/inspector tsx src/cli/qmd.ts mcp",
     "release": "./scripts/release.sh"
   },
   "publishConfig": {

+ 2 - 2
src/formatter.ts → src/cli/formatter.ts

@@ -5,8 +5,8 @@
  * JSON, CSV, XML, Markdown, files list, and CLI (colored terminal output).
  */
 
-import { extractSnippet } from "./store.js";
-import type { SearchResult, MultiGetResult, DocumentResult } from "./store.js";
+import { extractSnippet } from "../store.js";
+import type { SearchResult, MultiGetResult, DocumentResult } from "../store.js";
 
 // =============================================================================
 // Types

+ 14 - 14
src/qmd.ts → src/cli/qmd.ts

@@ -1,5 +1,5 @@
-import { openDatabase } from "./db.js";
-import type { Database } from "./db.js";
+import { openDatabase } from "../db.js";
+import type { Database } from "../db.js";
 import fastGlob from "fast-glob";
 import { execSync, spawn as nodeSpawn } from "child_process";
 import { fileURLToPath } from "url";
@@ -73,8 +73,8 @@ import {
   generateEmbeddings,
   syncConfigToDb,
   type ReindexResult,
-} from "./store.js";
-import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "./llm.js";
+} from "../store.js";
+import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js";
 import {
   formatSearchResults,
   formatDocuments,
@@ -94,7 +94,7 @@ import {
   listAllContexts,
   setConfigIndexName,
   loadConfig,
-} from "./collections.js";
+} from "../collections.js";
 
 // Enable production mode - allows using default database path
 // Tests must set INDEX_PATH or use createStore() with explicit path
@@ -1400,7 +1400,7 @@ async function collectionAdd(pwd: string, globPattern: string, name?: string): P
   }
 
   // Add to YAML config + sync to SQLite
-  const { addCollection } = await import("./collections.js");
+  const { addCollection } = await import("../collections.js");
   addCollection(collName, pwd, globPattern);
   resyncConfig();
 
@@ -2395,7 +2395,7 @@ function parseCLI() {
 function showSkill(): void {
   const scriptDir = dirname(fileURLToPath(import.meta.url));
   const relativePath = pathJoin("skills", "qmd", "SKILL.md");
-  const skillPath = pathJoin(scriptDir, "..", relativePath);
+  const skillPath = pathJoin(scriptDir, "..", "..", relativePath);
 
   console.log(`QMD Skill (${relativePath})`);
   console.log(`Location: ${skillPath}`);
@@ -2499,7 +2499,7 @@ function showHelp(): void {
 
 async function showVersion(): Promise<void> {
   const scriptDir = dirname(fileURLToPath(import.meta.url));
-  const pkgPath = resolve(scriptDir, "..", "package.json");
+  const pkgPath = resolve(scriptDir, "..", "..", "package.json");
   const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
 
   let commit = "";
@@ -2694,7 +2694,7 @@ if (isMain) {
             console.error("  Omit command to clear it");
             process.exit(1);
           }
-          const { updateCollectionSettings, getCollection } = await import("./collections.js");
+          const { updateCollectionSettings, getCollection } = await import("../collections.js");
           const col = getCollection(name);
           if (!col) {
             console.error(`Collection not found: ${name}`);
@@ -2717,7 +2717,7 @@ if (isMain) {
             console.error(`  ${subcommand === 'include' ? 'Include' : 'Exclude'} collection in default queries`);
             process.exit(1);
           }
-          const { updateCollectionSettings, getCollection } = await import("./collections.js");
+          const { updateCollectionSettings, getCollection } = await import("../collections.js");
           const col = getCollection(name);
           if (!col) {
             console.error(`Collection not found: ${name}`);
@@ -2736,7 +2736,7 @@ if (isMain) {
             console.error("Usage: qmd collection show <name>");
             process.exit(1);
           }
-          const { getCollection } = await import("./collections.js");
+          const { getCollection } = await import("../collections.js");
           const col = getCollection(name);
           if (!col) {
             console.error(`Collection not found: ${name}`);
@@ -2896,7 +2896,7 @@ if (isMain) {
           const logFd = openSync(logPath, "w"); // truncate — fresh log per daemon run
           const selfPath = fileURLToPath(import.meta.url);
           const spawnArgs = selfPath.endsWith(".ts")
-            ? ["--import", pathJoin(dirname(selfPath), "..", "node_modules", "tsx", "dist", "esm", "index.mjs"), selfPath, "mcp", "--http", "--port", String(port)]
+            ? ["--import", pathJoin(dirname(selfPath), "..", "..", "node_modules", "tsx", "dist", "esm", "index.mjs"), selfPath, "mcp", "--http", "--port", String(port)]
             : [selfPath, "mcp", "--http", "--port", String(port)];
           const child = nodeSpawn(process.execPath, spawnArgs, {
             stdio: ["ignore", logFd, logFd],
@@ -2915,7 +2915,7 @@ if (isMain) {
         // async cleanup handlers in startMcpHttpServer actually run.
         process.removeAllListeners("SIGTERM");
         process.removeAllListeners("SIGINT");
-        const { startMcpHttpServer } = await import("./mcp.js");
+        const { startMcpHttpServer } = await import("../mcp/server.js");
         try {
           await startMcpHttpServer(port);
         } catch (e: any) {
@@ -2927,7 +2927,7 @@ if (isMain) {
         }
       } else {
         // Default: stdio transport
-        const { startMcpServer } = await import("./mcp.js");
+        const { startMcpServer } = await import("../mcp/server.js");
         await startMcpServer();
       }
       break;

+ 37 - 1
src/index.ts

@@ -21,7 +21,10 @@ import {
   createStore as createStoreInternal,
   hybridQuery,
   structuredSearch,
+  extractSnippet,
+  addLineNumbers,
   DEFAULT_EMBED_MODEL,
+  DEFAULT_MULTI_GET_MAX_BYTES,
   reindexCollection,
   generateEmbeddings,
   listCollections as storeListCollections,
@@ -36,6 +39,12 @@ import {
   updateStoreContext,
   removeStoreContext,
   setStoreGlobalContext,
+  vacuumDatabase,
+  cleanupOrphanedContent,
+  cleanupOrphanedVectors,
+  deleteLLMCache,
+  deleteInactiveDocuments,
+  clearAllEmbeddings,
   type Store as InternalStore,
   type DocumentResult,
   type DocumentNotFound,
@@ -96,6 +105,18 @@ export type {
   ContextMap,
 };
 
+// Re-export the internal Store type for advanced consumers
+export type { InternalStore };
+
+// Re-export utility functions used by frontends
+export { extractSnippet, addLineNumbers, DEFAULT_MULTI_GET_MAX_BYTES };
+
+// Re-export getDefaultDbPath for CLI/MCP that need the default database location
+export { getDefaultDbPath } from "./store.js";
+
+// Re-export Maintenance class for CLI housekeeping operations
+export { Maintenance } from "./maintenance.js";
+
 /**
  * Progress info emitted during update() for each file processed.
  */
@@ -213,6 +234,9 @@ export interface QMDStore {
   /** Get a single document by path or docid */
   get(pathOrDocid: string, options?: { includeBody?: boolean }): Promise<DocumentResult | DocumentNotFound>;
 
+  /** Get the body content of a document, optionally sliced by line range */
+  getDocumentBody(pathOrDocid: string, opts?: { fromLine?: number; maxLines?: number }): Promise<string | null>;
+
   /** Get multiple documents by glob pattern or comma-separated list */
   multiGet(pattern: string, options?: { includeBody?: boolean; maxBytes?: number }): Promise<{ docs: MultiGetResult[]; errors: string[] }>;
 
@@ -228,7 +252,10 @@ export interface QMDStore {
   renameCollection(oldName: string, newName: string): Promise<boolean>;
 
   /** List all collections with document stats */
-  listCollections(): Promise<{ name: string; pwd: string; glob_pattern: string; doc_count: number; active_count: number; last_modified: string | null }[]>;
+  listCollections(): Promise<{ name: string; pwd: string; glob_pattern: string; doc_count: number; active_count: number; last_modified: string | null; includeByDefault: boolean }[]>;
+
+  /** Get names of collections included by default in queries */
+  getDefaultCollectionNames(): Promise<string[]>;
 
   // ── Context Management ──────────────────────────────────────────────
 
@@ -379,6 +406,11 @@ export async function createStore(options: StoreOptions): Promise<QMDStore> {
     searchVector: async (q, opts) => internal.searchVec(q, DEFAULT_EMBED_MODEL, opts?.limit, opts?.collection),
     expandQuery: async (q, opts) => internal.expandQuery(q, undefined, opts?.intent),
     get: async (pathOrDocid, opts) => internal.findDocument(pathOrDocid, opts),
+    getDocumentBody: async (pathOrDocid, opts) => {
+      const result = internal.findDocument(pathOrDocid, { includeBody: false });
+      if ("error" in result) return null;
+      return internal.getDocumentBody(result, opts?.fromLine, opts?.maxLines);
+    },
     multiGet: async (pattern, opts) => internal.findDocuments(pattern, opts),
 
     // Collection Management — write to SQLite + write-through to YAML/inline if configured
@@ -403,6 +435,10 @@ export async function createStore(options: StoreOptions): Promise<QMDStore> {
       return result;
     },
     listCollections: async () => storeListCollections(db),
+    getDefaultCollectionNames: async () => {
+      const collections = storeListCollections(db);
+      return collections.filter(c => c.includeByDefault).map(c => c.name);
+    },
 
     // Context Management — write to SQLite + write-through to YAML/inline if configured
     addContext: async (collectionName, pathPrefix, contextText) => {

+ 19 - 0
src/llm.ts

@@ -1440,6 +1440,25 @@ export async function withLLMSession<T>(
   }
 }
 
+/**
+ * Execute a function with a scoped LLM session using a specific LlamaCpp instance.
+ * Unlike withLLMSession, this does not use the global singleton.
+ */
+export async function withLLMSessionForLlm<T>(
+  llm: LlamaCpp,
+  fn: (session: ILLMSession) => Promise<T>,
+  options?: LLMSessionOptions
+): Promise<T> {
+  const manager = new LLMSessionManager(llm);
+  const session = new LLMSession(manager, options);
+
+  try {
+    return await fn(session);
+  } finally {
+    session.release();
+  }
+}
+
 /**
  * Check if idle unload is safe (no active sessions or operations).
  * Used internally by LlamaCpp idle timer.

+ 54 - 0
src/maintenance.ts

@@ -0,0 +1,54 @@
+/**
+ * Maintenance - Database cleanup operations for QMD.
+ *
+ * Wraps low-level store operations that the CLI needs for housekeeping.
+ * Takes an internal Store in the constructor — allowed to access DB directly.
+ */
+
+import type { Store } from "./store.js";
+import {
+  vacuumDatabase,
+  cleanupOrphanedContent,
+  cleanupOrphanedVectors,
+  deleteLLMCache,
+  deleteInactiveDocuments,
+  clearAllEmbeddings,
+} from "./store.js";
+
+export class Maintenance {
+  private store: Store;
+
+  constructor(store: Store) {
+    this.store = store;
+  }
+
+  /** Run VACUUM on the SQLite database to reclaim space */
+  vacuum(): void {
+    vacuumDatabase(this.store.db);
+  }
+
+  /** Remove content rows that are no longer referenced by any document */
+  cleanupOrphanedContent(): number {
+    return cleanupOrphanedContent(this.store.db);
+  }
+
+  /** Remove vector embeddings for content that no longer exists */
+  cleanupOrphanedVectors(): number {
+    return cleanupOrphanedVectors(this.store.db);
+  }
+
+  /** Clear the LLM response cache (query expansion, reranking) */
+  clearLLMCache(): number {
+    return deleteLLMCache(this.store.db);
+  }
+
+  /** Delete documents marked as inactive (removed from filesystem) */
+  deleteInactiveDocs(): number {
+    return deleteInactiveDocuments(this.store.db);
+  }
+
+  /** Clear all vector embeddings (forces re-embedding) */
+  clearEmbeddings(): void {
+    clearAllEmbeddings(this.store.db);
+  }
+}

+ 50 - 69
src/mcp.ts → src/mcp/server.ts

@@ -20,12 +20,12 @@ import {
   createStore,
   extractSnippet,
   addLineNumbers,
-  structuredSearch,
+  getDefaultDbPath,
   DEFAULT_MULTI_GET_MAX_BYTES,
-} from "./store.js";
-import type { Store, ExpandedQuery } from "./store.js";
-import { getCollection, getGlobalContext, getDefaultCollectionNames } from "./collections.js";
-import { disposeDefaultLlamaCpp } from "./llm.js";
+  type QMDStore,
+  type ExpandedQuery,
+  type IndexStatus,
+} from "../index.js";
 
 // =============================================================================
 // Types for structured content
@@ -46,8 +46,8 @@ type StatusResult = {
   hasVectorIndex: boolean;
   collections: {
     name: string;
-    path: string;
-    pattern: string;
+    path: string | null;
+    pattern: string | null;
     documents: number;
     lastUpdated: string;
   }[];
@@ -89,12 +89,13 @@ function formatSearchSummary(results: SearchResultItem[], query: string): string
  * Injected into the LLM's system prompt via MCP initialize response —
  * gives the LLM immediate context about what's searchable without a tool call.
  */
-function buildInstructions(store: Store): string {
-  const status = store.getStatus();
+async function buildInstructions(store: QMDStore): Promise<string> {
+  const status = await store.getStatus();
+  const contexts = await store.listContexts();
+  const globalCtx = await store.getGlobalContext();
   const lines: string[] = [];
 
   // --- What is this? ---
-  const globalCtx = getGlobalContext();
   lines.push(`QMD is your local search engine over ${status.totalDocuments} markdown documents.`);
   if (globalCtx) lines.push(`Context: ${globalCtx}`);
 
@@ -103,9 +104,9 @@ function buildInstructions(store: Store): string {
     lines.push("");
     lines.push("Collections (scope with `collection` parameter):");
     for (const col of status.collections) {
-      const collConfig = getCollection(col.name);
-      const rootCtx = collConfig?.context?.[""] || collConfig?.context?.["/"];
-      const desc = rootCtx ? ` — ${rootCtx}` : "";
+      // Find root context for this collection
+      const rootCtx = contexts.find(c => c.collection === col.name && (c.path === "" || c.path === "/"));
+      const desc = rootCtx ? ` — ${rootCtx.context}` : "";
       lines.push(`  - "${col.name}" (${col.documents} docs)${desc}`);
     }
   }
@@ -154,12 +155,15 @@ function buildInstructions(store: Store): string {
  * Create an MCP server with all QMD tools, resources, and prompts registered.
  * Shared by both stdio and HTTP transports.
  */
-function createMcpServer(store: Store): McpServer {
+async function createMcpServer(store: QMDStore): Promise<McpServer> {
   const server = new McpServer(
     { name: "qmd", version: "0.9.9" },
-    { instructions: buildInstructions(store) },
+    { instructions: await buildInstructions(store) },
   );
 
+  // Pre-fetch default collection names for search tools
+  const defaultCollectionNames = await store.getDefaultCollectionNames();
+
   // ---------------------------------------------------------------------------
   // Resource: qmd://{path} - read-only access to documents by path
   // Note: No list() - documents are discovered via search tools
@@ -178,49 +182,23 @@ function createMcpServer(store: Store): McpServer {
       const pathStr = Array.isArray(path) ? path.join('/') : (path || '');
       const decodedPath = decodeURIComponent(pathStr);
 
-      // Parse virtual path: collection/relative/path
-      const parts = decodedPath.split('/');
-      const collection = parts[0] || '';
-      const relativePath = parts.slice(1).join('/');
-
-      // Find document by collection and path, join with content table
-      let doc = store.db.prepare(`
-        SELECT d.collection, d.path, d.title, c.doc as body
-        FROM documents d
-        JOIN content c ON c.hash = d.hash
-        WHERE d.collection = ? AND d.path = ? AND d.active = 1
-      `).get(collection, relativePath) as { collection: string; path: string; title: string; body: string } | null;
-
-      // Try suffix match if exact match fails
-      if (!doc) {
-        doc = store.db.prepare(`
-          SELECT d.collection, d.path, d.title, c.doc as body
-          FROM documents d
-          JOIN content c ON c.hash = d.hash
-          WHERE d.path LIKE ? AND d.active = 1
-          LIMIT 1
-        `).get(`%${relativePath}`) as { collection: string; path: string; title: string; body: string } | null;
-      }
+      // Use SDK to find document — findDocument handles collection/path resolution
+      const result = await store.get(decodedPath, { includeBody: true });
 
-      if (!doc) {
+      if ("error" in result) {
         return { contents: [{ uri: uri.href, text: `Document not found: ${decodedPath}` }] };
       }
 
-      // Construct virtual path for context lookup
-      const virtualPath = `qmd://${doc.collection}/${doc.path}`;
-      const context = store.getContextForFile(virtualPath);
-
-      let text = addLineNumbers(doc.body);  // Default to line numbers
-      if (context) {
-        text = `<!-- Context: ${context} -->\n\n` + text;
+      let text = addLineNumbers(result.body || "");  // Default to line numbers
+      if (result.context) {
+        text = `<!-- Context: ${result.context} -->\n\n` + text;
       }
 
-      const displayName = `${doc.collection}/${doc.path}`;
       return {
         contents: [{
           uri: uri.href,
-          name: displayName,
-          title: doc.title || doc.path,
+          name: result.displayPath,
+          title: result.title || result.displayPath,
           mimeType: "text/markdown",
           text,
         }],
@@ -322,19 +300,19 @@ Intent-aware lex (C++ performance, not sports):
     },
     async ({ searches, limit, minScore, candidateLimit, collections, intent }) => {
       // Map to internal format
-      const subSearches: ExpandedQuery[] = searches.map(s => ({
+      const queries: ExpandedQuery[] = searches.map(s => ({
         type: s.type,
         query: s.query,
       }));
 
       // Use default collections if none specified
-      const effectiveCollections = collections ?? getDefaultCollectionNames();
+      const effectiveCollections = collections ?? defaultCollectionNames;
 
-      const results = await structuredSearch(store, subSearches, {
+      const results = await store.search({
+        queries,
         collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
         limit,
         minScore,
-        candidateLimit,
         intent,
       });
 
@@ -389,7 +367,7 @@ Intent-aware lex (C++ performance, not sports):
         lookup = lookup.slice(0, -colonMatch[0].length);
       }
 
-      const result = store.findDocument(lookup, { includeBody: false });
+      const result = await store.get(lookup, { includeBody: false });
 
       if ("error" in result) {
         let msg = `Document not found: ${file}`;
@@ -402,7 +380,7 @@ Intent-aware lex (C++ performance, not sports):
         };
       }
 
-      const body = store.getDocumentBody(result, parsedFromLine, maxLines) ?? "";
+      const body = await store.getDocumentBody(result.filepath, { fromLine: parsedFromLine, maxLines }) ?? "";
       let text = body;
       if (lineNumbers) {
         const startLine = parsedFromLine || 1;
@@ -445,7 +423,7 @@ Intent-aware lex (C++ performance, not sports):
       },
     },
     async ({ pattern, maxLines, maxBytes, lineNumbers }) => {
-      const { docs, errors } = store.findDocuments(pattern, { includeBody: true, maxBytes: maxBytes || DEFAULT_MULTI_GET_MAX_BYTES });
+      const { docs, errors } = await store.multiGet(pattern, { includeBody: true, maxBytes: maxBytes || DEFAULT_MULTI_GET_MAX_BYTES });
 
       if (docs.length === 0 && errors.length === 0) {
         return {
@@ -513,7 +491,7 @@ Intent-aware lex (C++ performance, not sports):
       inputSchema: {},
     },
     async () => {
-      const status: StatusResult = store.getStatus();
+      const status: StatusResult = await store.getStatus();
 
       const summary = [
         `QMD Index Status:`,
@@ -542,8 +520,8 @@ Intent-aware lex (C++ performance, not sports):
 // =============================================================================
 
 export async function startMcpServer(): Promise<void> {
-  const store = createStore();
-  const server = createMcpServer(store);
+  const store = await createStore({ dbPath: getDefaultDbPath() });
+  const server = await createMcpServer(store);
   const transport = new StdioServerTransport();
   await server.connect(transport);
 }
@@ -563,7 +541,10 @@ export type HttpServerHandle = {
  * Binds to localhost only. Returns a handle for shutdown and port discovery.
  */
 export async function startMcpHttpServer(port: number, options?: { quiet?: boolean }): Promise<HttpServerHandle> {
-  const store = createStore();
+  const store = await createStore({ dbPath: getDefaultDbPath() });
+
+  // Pre-fetch default collection names for REST endpoint
+  const defaultCollectionNames = await store.getDefaultCollectionNames();
 
   // Session map: each client gets its own McpServer + Transport pair (MCP spec requirement).
   // The store is shared — it's stateless SQLite, safe for concurrent access.
@@ -578,7 +559,7 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
         log(`${ts()} New session ${sessionId} (${sessions.size} active)`);
       },
     });
-    const server = createMcpServer(store);
+    const server = await createMcpServer(store);
     await server.connect(transport);
 
     transport.onclose = () => {
@@ -645,7 +626,7 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
       if ((pathname === "/query" || pathname === "/search") && nodeReq.method === "POST") {
         const rawBody = await collectBody(nodeReq);
         const params = JSON.parse(rawBody);
-        
+
         // Validate required fields
         if (!params.searches || !Array.isArray(params.searches)) {
           nodeRes.writeHead(400, { "Content-Type": "application/json" });
@@ -654,19 +635,20 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
         }
 
         // Map to internal format
-        const subSearches: ExpandedQuery[] = params.searches.map((s: any) => ({
+        const queries: ExpandedQuery[] = params.searches.map((s: any) => ({
           type: s.type as 'lex' | 'vec' | 'hyde',
           query: String(s.query || ""),
         }));
 
         // Use default collections if none specified
-        const effectiveCollections = params.collections ?? getDefaultCollectionNames();
+        const effectiveCollections = params.collections ?? defaultCollectionNames;
 
-        const results = await structuredSearch(store, subSearches, {
+        const results = await store.search({
+          queries,
           collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
           limit: params.limit ?? 10,
           minScore: params.minScore ?? 0,
-          candidateLimit: params.candidateLimit,
+          intent: params.intent,
         });
 
         // Use first lex or vec query for snippet extraction
@@ -801,8 +783,7 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
     }
     sessions.clear();
     httpServer.close();
-    store.close();
-    await disposeDefaultLlamaCpp();
+    await store.close();
   };
 
   process.on("SIGTERM", async () => {
@@ -821,6 +802,6 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
 }
 
 // Run if this is the main module
-if (fileURLToPath(import.meta.url) === process.argv[1] || process.argv[1]?.endsWith("/mcp.ts") || process.argv[1]?.endsWith("/mcp.js")) {
+if (fileURLToPath(import.meta.url) === process.argv[1] || process.argv[1]?.endsWith("/server.ts") || process.argv[1]?.endsWith("/server.js")) {
   startMcpServer().catch(console.error);
 }

+ 3 - 2
src/store.ts

@@ -16,7 +16,7 @@ import type { Database } from "./db.js";
 import picomatch from "picomatch";
 import { createHash } from "crypto";
 import { readFileSync, realpathSync, statSync, mkdirSync } from "node:fs";
-import { resolve } from "node:path";
+// Note: node:path resolve is not imported — we export our own cross-platform resolve()
 import fastGlob from "fast-glob";
 import {
   LlamaCpp,
@@ -2267,7 +2267,7 @@ export function getCollectionByName(db: Database, name: string): { name: string;
  * List all collections with document counts from database.
  * Merges store_collections config with database statistics.
  */
-export function listCollections(db: Database): { name: string; pwd: string; glob_pattern: string; doc_count: number; active_count: number; last_modified: string | null }[] {
+export function listCollections(db: Database): { name: string; pwd: string; glob_pattern: string; doc_count: number; active_count: number; last_modified: string | null; includeByDefault: boolean }[] {
   const collections = getStoreCollections(db);
 
   // Get document counts from database for each collection
@@ -2288,6 +2288,7 @@ export function listCollections(db: Database): { name: string; pwd: string; glob
       doc_count: stats?.doc_count || 0,
       active_count: stats?.active_count || 0,
       last_modified: stats?.last_modified || null,
+      includeByDefault: coll.includeByDefault !== false,
     };
   });
 

+ 2 - 2
test/cli.test.ts

@@ -24,7 +24,7 @@ let testCounter = 0; // Unique counter for each test run
 // Get the directory where this test file lives
 const thisDir = dirname(fileURLToPath(import.meta.url));
 const projectRoot = join(thisDir, "..");
-const qmdScript = join(projectRoot, "src", "qmd.ts");
+const qmdScript = join(projectRoot, "src", "cli", "qmd.ts");
 // Resolve tsx binary from project's node_modules (not cwd-dependent)
 const tsxBin = (() => {
   const candidate = join(projectRoot, "node_modules", ".bin", "tsx");
@@ -485,7 +485,7 @@ ${token}
 
     const update = await runQmd(["update"], { dbPath, configDir });
     expect(update.exitCode).toBe(0);
-    expect(update.stdout).toContain("No files found matching pattern.");
+    expect(update.stdout).toContain("0 new, 0 updated, 0 unchanged, 1 removed");
 
     const after = await runQmd(["get", "qmd://empty-check/only.md"], { dbPath, configDir });
     expect(after.exitCode).toBe(1);

+ 3 - 3
test/eval-harness.ts

@@ -138,7 +138,7 @@ interface SearchResult {
 function runSearch(query: string): SearchResult[] {
   try {
     const output = execSync(
-      `bun src/qmd.ts search "${query.replace(/"/g, '\\"')}" --json -n 5 2>/dev/null`,
+      `bun src/cli/qmd.ts search "${query.replace(/"/g, '\\"')}" --json -n 5 2>/dev/null`,
       { encoding: "utf-8", timeout: 30000 }
     );
     return JSON.parse(output);
@@ -150,7 +150,7 @@ function runSearch(query: string): SearchResult[] {
 function runQuery(query: string): SearchResult[] {
   try {
     const output = execSync(
-      `bun src/qmd.ts query "${query.replace(/"/g, '\\"')}" --json -n 5 2>/dev/null`,
+      `bun src/cli/qmd.ts query "${query.replace(/"/g, '\\"')}" --json -n 5 2>/dev/null`,
       { encoding: "utf-8", timeout: 60000 }
     );
     return JSON.parse(output);
@@ -207,7 +207,7 @@ console.log(`Testing ${evalQueries.length} queries across 6 documents`);
 
 // Check if eval-docs collection exists
 try {
-  const status = execSync("bun src/qmd.ts status --json 2>/dev/null", { encoding: "utf-8" });
+  const status = execSync("bun src/cli/qmd.ts status --json 2>/dev/null", { encoding: "utf-8" });
   if (!status.includes("eval-docs")) {
     console.log("\n⚠️  eval-docs collection not found. Run:");
     console.log("   qmd collection add test/eval-docs --name eval-docs");

+ 1 - 1
test/formatter.test.ts

@@ -27,7 +27,7 @@ import {
   documentToXml,
   formatDocument,
   type MultiGetFile,
-} from "../src/formatter.js";
+} from "../src/cli/formatter.js";
 import type { SearchResult, DocumentResult } from "../src/store.js";
 
 // =============================================================================

+ 1 - 1
test/intent.test.ts

@@ -22,7 +22,7 @@ import {
 } from "../src/store.js";
 
 // =============================================================================
-// parseStructuredQuery — duplicated from src/qmd.ts for unit testing
+// parseStructuredQuery — duplicated from src/cli/qmd.ts for unit testing
 // (qmd.ts doesn't export it since it's a CLI internal)
 // =============================================================================
 

+ 1 - 1
test/mcp.test.ts

@@ -894,7 +894,7 @@ describe("MCP Server", () => {
 // HTTP Transport Tests
 // =============================================================================
 
-import { startMcpHttpServer, type HttpServerHandle } from "../src/mcp";
+import { startMcpHttpServer, type HttpServerHandle } from "../src/mcp/server";
 import { enableProductionMode } from "../src/store";
 
 describe("MCP HTTP Transport", () => {