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
This commit is contained in:
parent
839d774a06
commit
c68904fe08
@ -118,7 +118,7 @@ qmd multi-get "#abc123, #def456"
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bun src/qmd.ts <command> # Run from source
|
bun src/cli/qmd.ts <command> # Run from source
|
||||||
bun link # Install globally as 'qmd'
|
bun link # Install globally as 'qmd'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
18
package.json
18
package.json
@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"qmd": "dist/qmd.js"
|
"qmd": "dist/cli/qmd.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/",
|
"dist/",
|
||||||
@ -21,15 +21,15 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "[ -d .git ] && ./scripts/install-hooks.sh || true",
|
"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/",
|
"test": "vitest run --reporter=verbose test/",
|
||||||
"qmd": "tsx src/qmd.ts",
|
"qmd": "tsx src/cli/qmd.ts",
|
||||||
"index": "tsx src/qmd.ts index",
|
"index": "tsx src/cli/qmd.ts index",
|
||||||
"vector": "tsx src/qmd.ts vector",
|
"vector": "tsx src/cli/qmd.ts vector",
|
||||||
"search": "tsx src/qmd.ts search",
|
"search": "tsx src/cli/qmd.ts search",
|
||||||
"vsearch": "tsx src/qmd.ts vsearch",
|
"vsearch": "tsx src/cli/qmd.ts vsearch",
|
||||||
"rerank": "tsx src/qmd.ts rerank",
|
"rerank": "tsx src/cli/qmd.ts rerank",
|
||||||
"inspector": "npx @modelcontextprotocol/inspector tsx src/qmd.ts mcp",
|
"inspector": "npx @modelcontextprotocol/inspector tsx src/cli/qmd.ts mcp",
|
||||||
"release": "./scripts/release.sh"
|
"release": "./scripts/release.sh"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
* JSON, CSV, XML, Markdown, files list, and CLI (colored terminal output).
|
* JSON, CSV, XML, Markdown, files list, and CLI (colored terminal output).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extractSnippet } from "./store.js";
|
import { extractSnippet } from "../store.js";
|
||||||
import type { SearchResult, MultiGetResult, DocumentResult } from "./store.js";
|
import type { SearchResult, MultiGetResult, DocumentResult } from "../store.js";
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Types
|
// Types
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { openDatabase } from "./db.js";
|
import { openDatabase } from "../db.js";
|
||||||
import type { Database } from "./db.js";
|
import type { Database } from "../db.js";
|
||||||
import fastGlob from "fast-glob";
|
import fastGlob from "fast-glob";
|
||||||
import { execSync, spawn as nodeSpawn } from "child_process";
|
import { execSync, spawn as nodeSpawn } from "child_process";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
@ -73,8 +73,8 @@ import {
|
|||||||
generateEmbeddings,
|
generateEmbeddings,
|
||||||
syncConfigToDb,
|
syncConfigToDb,
|
||||||
type ReindexResult,
|
type ReindexResult,
|
||||||
} from "./store.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 { disposeDefaultLlamaCpp, getDefaultLlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js";
|
||||||
import {
|
import {
|
||||||
formatSearchResults,
|
formatSearchResults,
|
||||||
formatDocuments,
|
formatDocuments,
|
||||||
@ -94,7 +94,7 @@ import {
|
|||||||
listAllContexts,
|
listAllContexts,
|
||||||
setConfigIndexName,
|
setConfigIndexName,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
} from "./collections.js";
|
} from "../collections.js";
|
||||||
|
|
||||||
// Enable production mode - allows using default database path
|
// Enable production mode - allows using default database path
|
||||||
// Tests must set INDEX_PATH or use createStore() with explicit 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
|
// Add to YAML config + sync to SQLite
|
||||||
const { addCollection } = await import("./collections.js");
|
const { addCollection } = await import("../collections.js");
|
||||||
addCollection(collName, pwd, globPattern);
|
addCollection(collName, pwd, globPattern);
|
||||||
resyncConfig();
|
resyncConfig();
|
||||||
|
|
||||||
@ -2395,7 +2395,7 @@ function parseCLI() {
|
|||||||
function showSkill(): void {
|
function showSkill(): void {
|
||||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
const relativePath = pathJoin("skills", "qmd", "SKILL.md");
|
const relativePath = pathJoin("skills", "qmd", "SKILL.md");
|
||||||
const skillPath = pathJoin(scriptDir, "..", relativePath);
|
const skillPath = pathJoin(scriptDir, "..", "..", relativePath);
|
||||||
|
|
||||||
console.log(`QMD Skill (${relativePath})`);
|
console.log(`QMD Skill (${relativePath})`);
|
||||||
console.log(`Location: ${skillPath}`);
|
console.log(`Location: ${skillPath}`);
|
||||||
@ -2499,7 +2499,7 @@ function showHelp(): void {
|
|||||||
|
|
||||||
async function showVersion(): Promise<void> {
|
async function showVersion(): Promise<void> {
|
||||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
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"));
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||||
|
|
||||||
let commit = "";
|
let commit = "";
|
||||||
@ -2694,7 +2694,7 @@ if (isMain) {
|
|||||||
console.error(" Omit command to clear it");
|
console.error(" Omit command to clear it");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const { updateCollectionSettings, getCollection } = await import("./collections.js");
|
const { updateCollectionSettings, getCollection } = await import("../collections.js");
|
||||||
const col = getCollection(name);
|
const col = getCollection(name);
|
||||||
if (!col) {
|
if (!col) {
|
||||||
console.error(`Collection not found: ${name}`);
|
console.error(`Collection not found: ${name}`);
|
||||||
@ -2717,7 +2717,7 @@ if (isMain) {
|
|||||||
console.error(` ${subcommand === 'include' ? 'Include' : 'Exclude'} collection in default queries`);
|
console.error(` ${subcommand === 'include' ? 'Include' : 'Exclude'} collection in default queries`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const { updateCollectionSettings, getCollection } = await import("./collections.js");
|
const { updateCollectionSettings, getCollection } = await import("../collections.js");
|
||||||
const col = getCollection(name);
|
const col = getCollection(name);
|
||||||
if (!col) {
|
if (!col) {
|
||||||
console.error(`Collection not found: ${name}`);
|
console.error(`Collection not found: ${name}`);
|
||||||
@ -2736,7 +2736,7 @@ if (isMain) {
|
|||||||
console.error("Usage: qmd collection show <name>");
|
console.error("Usage: qmd collection show <name>");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const { getCollection } = await import("./collections.js");
|
const { getCollection } = await import("../collections.js");
|
||||||
const col = getCollection(name);
|
const col = getCollection(name);
|
||||||
if (!col) {
|
if (!col) {
|
||||||
console.error(`Collection not found: ${name}`);
|
console.error(`Collection not found: ${name}`);
|
||||||
@ -2896,7 +2896,7 @@ if (isMain) {
|
|||||||
const logFd = openSync(logPath, "w"); // truncate — fresh log per daemon run
|
const logFd = openSync(logPath, "w"); // truncate — fresh log per daemon run
|
||||||
const selfPath = fileURLToPath(import.meta.url);
|
const selfPath = fileURLToPath(import.meta.url);
|
||||||
const spawnArgs = selfPath.endsWith(".ts")
|
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)];
|
: [selfPath, "mcp", "--http", "--port", String(port)];
|
||||||
const child = nodeSpawn(process.execPath, spawnArgs, {
|
const child = nodeSpawn(process.execPath, spawnArgs, {
|
||||||
stdio: ["ignore", logFd, logFd],
|
stdio: ["ignore", logFd, logFd],
|
||||||
@ -2915,7 +2915,7 @@ if (isMain) {
|
|||||||
// async cleanup handlers in startMcpHttpServer actually run.
|
// async cleanup handlers in startMcpHttpServer actually run.
|
||||||
process.removeAllListeners("SIGTERM");
|
process.removeAllListeners("SIGTERM");
|
||||||
process.removeAllListeners("SIGINT");
|
process.removeAllListeners("SIGINT");
|
||||||
const { startMcpHttpServer } = await import("./mcp.js");
|
const { startMcpHttpServer } = await import("../mcp/server.js");
|
||||||
try {
|
try {
|
||||||
await startMcpHttpServer(port);
|
await startMcpHttpServer(port);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -2927,7 +2927,7 @@ if (isMain) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default: stdio transport
|
// Default: stdio transport
|
||||||
const { startMcpServer } = await import("./mcp.js");
|
const { startMcpServer } = await import("../mcp/server.js");
|
||||||
await startMcpServer();
|
await startMcpServer();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
38
src/index.ts
38
src/index.ts
@ -21,7 +21,10 @@ import {
|
|||||||
createStore as createStoreInternal,
|
createStore as createStoreInternal,
|
||||||
hybridQuery,
|
hybridQuery,
|
||||||
structuredSearch,
|
structuredSearch,
|
||||||
|
extractSnippet,
|
||||||
|
addLineNumbers,
|
||||||
DEFAULT_EMBED_MODEL,
|
DEFAULT_EMBED_MODEL,
|
||||||
|
DEFAULT_MULTI_GET_MAX_BYTES,
|
||||||
reindexCollection,
|
reindexCollection,
|
||||||
generateEmbeddings,
|
generateEmbeddings,
|
||||||
listCollections as storeListCollections,
|
listCollections as storeListCollections,
|
||||||
@ -36,6 +39,12 @@ import {
|
|||||||
updateStoreContext,
|
updateStoreContext,
|
||||||
removeStoreContext,
|
removeStoreContext,
|
||||||
setStoreGlobalContext,
|
setStoreGlobalContext,
|
||||||
|
vacuumDatabase,
|
||||||
|
cleanupOrphanedContent,
|
||||||
|
cleanupOrphanedVectors,
|
||||||
|
deleteLLMCache,
|
||||||
|
deleteInactiveDocuments,
|
||||||
|
clearAllEmbeddings,
|
||||||
type Store as InternalStore,
|
type Store as InternalStore,
|
||||||
type DocumentResult,
|
type DocumentResult,
|
||||||
type DocumentNotFound,
|
type DocumentNotFound,
|
||||||
@ -96,6 +105,18 @@ export type {
|
|||||||
ContextMap,
|
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.
|
* 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 a single document by path or docid */
|
||||||
get(pathOrDocid: string, options?: { includeBody?: boolean }): Promise<DocumentResult | DocumentNotFound>;
|
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 */
|
/** Get multiple documents by glob pattern or comma-separated list */
|
||||||
multiGet(pattern: string, options?: { includeBody?: boolean; maxBytes?: number }): Promise<{ docs: MultiGetResult[]; errors: string[] }>;
|
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>;
|
renameCollection(oldName: string, newName: string): Promise<boolean>;
|
||||||
|
|
||||||
/** List all collections with document stats */
|
/** 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 ──────────────────────────────────────────────
|
// ── 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),
|
searchVector: async (q, opts) => internal.searchVec(q, DEFAULT_EMBED_MODEL, opts?.limit, opts?.collection),
|
||||||
expandQuery: async (q, opts) => internal.expandQuery(q, undefined, opts?.intent),
|
expandQuery: async (q, opts) => internal.expandQuery(q, undefined, opts?.intent),
|
||||||
get: async (pathOrDocid, opts) => internal.findDocument(pathOrDocid, opts),
|
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),
|
multiGet: async (pattern, opts) => internal.findDocuments(pattern, opts),
|
||||||
|
|
||||||
// Collection Management — write to SQLite + write-through to YAML/inline if configured
|
// 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;
|
return result;
|
||||||
},
|
},
|
||||||
listCollections: async () => storeListCollections(db),
|
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
|
// Context Management — write to SQLite + write-through to YAML/inline if configured
|
||||||
addContext: async (collectionName, pathPrefix, contextText) => {
|
addContext: async (collectionName, pathPrefix, contextText) => {
|
||||||
|
|||||||
19
src/llm.ts
19
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).
|
* Check if idle unload is safe (no active sessions or operations).
|
||||||
* Used internally by LlamaCpp idle timer.
|
* Used internally by LlamaCpp idle timer.
|
||||||
|
|||||||
54
src/maintenance.ts
Normal file
54
src/maintenance.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,12 +20,12 @@ import {
|
|||||||
createStore,
|
createStore,
|
||||||
extractSnippet,
|
extractSnippet,
|
||||||
addLineNumbers,
|
addLineNumbers,
|
||||||
structuredSearch,
|
getDefaultDbPath,
|
||||||
DEFAULT_MULTI_GET_MAX_BYTES,
|
DEFAULT_MULTI_GET_MAX_BYTES,
|
||||||
} from "./store.js";
|
type QMDStore,
|
||||||
import type { Store, ExpandedQuery } from "./store.js";
|
type ExpandedQuery,
|
||||||
import { getCollection, getGlobalContext, getDefaultCollectionNames } from "./collections.js";
|
type IndexStatus,
|
||||||
import { disposeDefaultLlamaCpp } from "./llm.js";
|
} from "../index.js";
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Types for structured content
|
// Types for structured content
|
||||||
@ -46,8 +46,8 @@ type StatusResult = {
|
|||||||
hasVectorIndex: boolean;
|
hasVectorIndex: boolean;
|
||||||
collections: {
|
collections: {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string | null;
|
||||||
pattern: string;
|
pattern: string | null;
|
||||||
documents: number;
|
documents: number;
|
||||||
lastUpdated: string;
|
lastUpdated: string;
|
||||||
}[];
|
}[];
|
||||||
@ -89,12 +89,13 @@ function formatSearchSummary(results: SearchResultItem[], query: string): string
|
|||||||
* Injected into the LLM's system prompt via MCP initialize response —
|
* Injected into the LLM's system prompt via MCP initialize response —
|
||||||
* gives the LLM immediate context about what's searchable without a tool call.
|
* gives the LLM immediate context about what's searchable without a tool call.
|
||||||
*/
|
*/
|
||||||
function buildInstructions(store: Store): string {
|
async function buildInstructions(store: QMDStore): Promise<string> {
|
||||||
const status = store.getStatus();
|
const status = await store.getStatus();
|
||||||
|
const contexts = await store.listContexts();
|
||||||
|
const globalCtx = await store.getGlobalContext();
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
// --- What is this? ---
|
// --- What is this? ---
|
||||||
const globalCtx = getGlobalContext();
|
|
||||||
lines.push(`QMD is your local search engine over ${status.totalDocuments} markdown documents.`);
|
lines.push(`QMD is your local search engine over ${status.totalDocuments} markdown documents.`);
|
||||||
if (globalCtx) lines.push(`Context: ${globalCtx}`);
|
if (globalCtx) lines.push(`Context: ${globalCtx}`);
|
||||||
|
|
||||||
@ -103,9 +104,9 @@ function buildInstructions(store: Store): string {
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Collections (scope with `collection` parameter):");
|
lines.push("Collections (scope with `collection` parameter):");
|
||||||
for (const col of status.collections) {
|
for (const col of status.collections) {
|
||||||
const collConfig = getCollection(col.name);
|
// Find root context for this collection
|
||||||
const rootCtx = collConfig?.context?.[""] || collConfig?.context?.["/"];
|
const rootCtx = contexts.find(c => c.collection === col.name && (c.path === "" || c.path === "/"));
|
||||||
const desc = rootCtx ? ` — ${rootCtx}` : "";
|
const desc = rootCtx ? ` — ${rootCtx.context}` : "";
|
||||||
lines.push(` - "${col.name}" (${col.documents} docs)${desc}`);
|
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.
|
* Create an MCP server with all QMD tools, resources, and prompts registered.
|
||||||
* Shared by both stdio and HTTP transports.
|
* Shared by both stdio and HTTP transports.
|
||||||
*/
|
*/
|
||||||
function createMcpServer(store: Store): McpServer {
|
async function createMcpServer(store: QMDStore): Promise<McpServer> {
|
||||||
const server = new McpServer(
|
const server = new McpServer(
|
||||||
{ name: "qmd", version: "0.9.9" },
|
{ 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
|
// Resource: qmd://{path} - read-only access to documents by path
|
||||||
// Note: No list() - documents are discovered via search tools
|
// 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 pathStr = Array.isArray(path) ? path.join('/') : (path || '');
|
||||||
const decodedPath = decodeURIComponent(pathStr);
|
const decodedPath = decodeURIComponent(pathStr);
|
||||||
|
|
||||||
// Parse virtual path: collection/relative/path
|
// Use SDK to find document — findDocument handles collection/path resolution
|
||||||
const parts = decodedPath.split('/');
|
const result = await store.get(decodedPath, { includeBody: true });
|
||||||
const collection = parts[0] || '';
|
|
||||||
const relativePath = parts.slice(1).join('/');
|
|
||||||
|
|
||||||
// Find document by collection and path, join with content table
|
if ("error" in result) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!doc) {
|
|
||||||
return { contents: [{ uri: uri.href, text: `Document not found: ${decodedPath}` }] };
|
return { contents: [{ uri: uri.href, text: `Document not found: ${decodedPath}` }] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct virtual path for context lookup
|
let text = addLineNumbers(result.body || ""); // Default to line numbers
|
||||||
const virtualPath = `qmd://${doc.collection}/${doc.path}`;
|
if (result.context) {
|
||||||
const context = store.getContextForFile(virtualPath);
|
text = `<!-- Context: ${result.context} -->\n\n` + text;
|
||||||
|
|
||||||
let text = addLineNumbers(doc.body); // Default to line numbers
|
|
||||||
if (context) {
|
|
||||||
text = `<!-- Context: ${context} -->\n\n` + text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = `${doc.collection}/${doc.path}`;
|
|
||||||
return {
|
return {
|
||||||
contents: [{
|
contents: [{
|
||||||
uri: uri.href,
|
uri: uri.href,
|
||||||
name: displayName,
|
name: result.displayPath,
|
||||||
title: doc.title || doc.path,
|
title: result.title || result.displayPath,
|
||||||
mimeType: "text/markdown",
|
mimeType: "text/markdown",
|
||||||
text,
|
text,
|
||||||
}],
|
}],
|
||||||
@ -322,19 +300,19 @@ Intent-aware lex (C++ performance, not sports):
|
|||||||
},
|
},
|
||||||
async ({ searches, limit, minScore, candidateLimit, collections, intent }) => {
|
async ({ searches, limit, minScore, candidateLimit, collections, intent }) => {
|
||||||
// Map to internal format
|
// Map to internal format
|
||||||
const subSearches: ExpandedQuery[] = searches.map(s => ({
|
const queries: ExpandedQuery[] = searches.map(s => ({
|
||||||
type: s.type,
|
type: s.type,
|
||||||
query: s.query,
|
query: s.query,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Use default collections if none specified
|
// 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,
|
collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
|
||||||
limit,
|
limit,
|
||||||
minScore,
|
minScore,
|
||||||
candidateLimit,
|
|
||||||
intent,
|
intent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -389,7 +367,7 @@ Intent-aware lex (C++ performance, not sports):
|
|||||||
lookup = lookup.slice(0, -colonMatch[0].length);
|
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) {
|
if ("error" in result) {
|
||||||
let msg = `Document not found: ${file}`;
|
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;
|
let text = body;
|
||||||
if (lineNumbers) {
|
if (lineNumbers) {
|
||||||
const startLine = parsedFromLine || 1;
|
const startLine = parsedFromLine || 1;
|
||||||
@ -445,7 +423,7 @@ Intent-aware lex (C++ performance, not sports):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ pattern, maxLines, maxBytes, lineNumbers }) => {
|
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) {
|
if (docs.length === 0 && errors.length === 0) {
|
||||||
return {
|
return {
|
||||||
@ -513,7 +491,7 @@ Intent-aware lex (C++ performance, not sports):
|
|||||||
inputSchema: {},
|
inputSchema: {},
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const status: StatusResult = store.getStatus();
|
const status: StatusResult = await store.getStatus();
|
||||||
|
|
||||||
const summary = [
|
const summary = [
|
||||||
`QMD Index Status:`,
|
`QMD Index Status:`,
|
||||||
@ -542,8 +520,8 @@ Intent-aware lex (C++ performance, not sports):
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export async function startMcpServer(): Promise<void> {
|
export async function startMcpServer(): Promise<void> {
|
||||||
const store = createStore();
|
const store = await createStore({ dbPath: getDefaultDbPath() });
|
||||||
const server = createMcpServer(store);
|
const server = await createMcpServer(store);
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
}
|
}
|
||||||
@ -563,7 +541,10 @@ export type HttpServerHandle = {
|
|||||||
* Binds to localhost only. Returns a handle for shutdown and port discovery.
|
* Binds to localhost only. Returns a handle for shutdown and port discovery.
|
||||||
*/
|
*/
|
||||||
export async function startMcpHttpServer(port: number, options?: { quiet?: boolean }): Promise<HttpServerHandle> {
|
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).
|
// 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.
|
// 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)`);
|
log(`${ts()} New session ${sessionId} (${sessions.size} active)`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const server = createMcpServer(store);
|
const server = await createMcpServer(store);
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
|
|
||||||
transport.onclose = () => {
|
transport.onclose = () => {
|
||||||
@ -645,7 +626,7 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
|
|||||||
if ((pathname === "/query" || pathname === "/search") && nodeReq.method === "POST") {
|
if ((pathname === "/query" || pathname === "/search") && nodeReq.method === "POST") {
|
||||||
const rawBody = await collectBody(nodeReq);
|
const rawBody = await collectBody(nodeReq);
|
||||||
const params = JSON.parse(rawBody);
|
const params = JSON.parse(rawBody);
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!params.searches || !Array.isArray(params.searches)) {
|
if (!params.searches || !Array.isArray(params.searches)) {
|
||||||
nodeRes.writeHead(400, { "Content-Type": "application/json" });
|
nodeRes.writeHead(400, { "Content-Type": "application/json" });
|
||||||
@ -654,19 +635,20 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map to internal format
|
// 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',
|
type: s.type as 'lex' | 'vec' | 'hyde',
|
||||||
query: String(s.query || ""),
|
query: String(s.query || ""),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Use default collections if none specified
|
// 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,
|
collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
|
||||||
limit: params.limit ?? 10,
|
limit: params.limit ?? 10,
|
||||||
minScore: params.minScore ?? 0,
|
minScore: params.minScore ?? 0,
|
||||||
candidateLimit: params.candidateLimit,
|
intent: params.intent,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use first lex or vec query for snippet extraction
|
// Use first lex or vec query for snippet extraction
|
||||||
@ -801,8 +783,7 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
|
|||||||
}
|
}
|
||||||
sessions.clear();
|
sessions.clear();
|
||||||
httpServer.close();
|
httpServer.close();
|
||||||
store.close();
|
await store.close();
|
||||||
await disposeDefaultLlamaCpp();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGTERM", async () => {
|
process.on("SIGTERM", async () => {
|
||||||
@ -821,6 +802,6 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run if this is the main module
|
// 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);
|
startMcpServer().catch(console.error);
|
||||||
}
|
}
|
||||||
@ -16,7 +16,7 @@ import type { Database } from "./db.js";
|
|||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { readFileSync, realpathSync, statSync, mkdirSync } from "node:fs";
|
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 fastGlob from "fast-glob";
|
||||||
import {
|
import {
|
||||||
LlamaCpp,
|
LlamaCpp,
|
||||||
@ -2267,7 +2267,7 @@ export function getCollectionByName(db: Database, name: string): { name: string;
|
|||||||
* List all collections with document counts from database.
|
* List all collections with document counts from database.
|
||||||
* Merges store_collections config with database statistics.
|
* 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);
|
const collections = getStoreCollections(db);
|
||||||
|
|
||||||
// Get document counts from database for each collection
|
// 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,
|
doc_count: stats?.doc_count || 0,
|
||||||
active_count: stats?.active_count || 0,
|
active_count: stats?.active_count || 0,
|
||||||
last_modified: stats?.last_modified || null,
|
last_modified: stats?.last_modified || null,
|
||||||
|
includeByDefault: coll.includeByDefault !== false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ let testCounter = 0; // Unique counter for each test run
|
|||||||
// Get the directory where this test file lives
|
// Get the directory where this test file lives
|
||||||
const thisDir = dirname(fileURLToPath(import.meta.url));
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
||||||
const projectRoot = join(thisDir, "..");
|
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)
|
// Resolve tsx binary from project's node_modules (not cwd-dependent)
|
||||||
const tsxBin = (() => {
|
const tsxBin = (() => {
|
||||||
const candidate = join(projectRoot, "node_modules", ".bin", "tsx");
|
const candidate = join(projectRoot, "node_modules", ".bin", "tsx");
|
||||||
@ -485,7 +485,7 @@ ${token}
|
|||||||
|
|
||||||
const update = await runQmd(["update"], { dbPath, configDir });
|
const update = await runQmd(["update"], { dbPath, configDir });
|
||||||
expect(update.exitCode).toBe(0);
|
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 });
|
const after = await runQmd(["get", "qmd://empty-check/only.md"], { dbPath, configDir });
|
||||||
expect(after.exitCode).toBe(1);
|
expect(after.exitCode).toBe(1);
|
||||||
|
|||||||
@ -138,7 +138,7 @@ interface SearchResult {
|
|||||||
function runSearch(query: string): SearchResult[] {
|
function runSearch(query: string): SearchResult[] {
|
||||||
try {
|
try {
|
||||||
const output = execSync(
|
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 }
|
{ encoding: "utf-8", timeout: 30000 }
|
||||||
);
|
);
|
||||||
return JSON.parse(output);
|
return JSON.parse(output);
|
||||||
@ -150,7 +150,7 @@ function runSearch(query: string): SearchResult[] {
|
|||||||
function runQuery(query: string): SearchResult[] {
|
function runQuery(query: string): SearchResult[] {
|
||||||
try {
|
try {
|
||||||
const output = execSync(
|
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 }
|
{ encoding: "utf-8", timeout: 60000 }
|
||||||
);
|
);
|
||||||
return JSON.parse(output);
|
return JSON.parse(output);
|
||||||
@ -207,7 +207,7 @@ console.log(`Testing ${evalQueries.length} queries across 6 documents`);
|
|||||||
|
|
||||||
// Check if eval-docs collection exists
|
// Check if eval-docs collection exists
|
||||||
try {
|
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")) {
|
if (!status.includes("eval-docs")) {
|
||||||
console.log("\n⚠️ eval-docs collection not found. Run:");
|
console.log("\n⚠️ eval-docs collection not found. Run:");
|
||||||
console.log(" qmd collection add test/eval-docs --name eval-docs");
|
console.log(" qmd collection add test/eval-docs --name eval-docs");
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import {
|
|||||||
documentToXml,
|
documentToXml,
|
||||||
formatDocument,
|
formatDocument,
|
||||||
type MultiGetFile,
|
type MultiGetFile,
|
||||||
} from "../src/formatter.js";
|
} from "../src/cli/formatter.js";
|
||||||
import type { SearchResult, DocumentResult } from "../src/store.js";
|
import type { SearchResult, DocumentResult } from "../src/store.js";
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
} from "../src/store.js";
|
} 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)
|
// (qmd.ts doesn't export it since it's a CLI internal)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
|||||||
@ -894,7 +894,7 @@ describe("MCP Server", () => {
|
|||||||
// HTTP Transport Tests
|
// HTTP Transport Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
import { startMcpHttpServer, type HttpServerHandle } from "../src/mcp";
|
import { startMcpHttpServer, type HttpServerHandle } from "../src/mcp/server";
|
||||||
import { enableProductionMode } from "../src/store";
|
import { enableProductionMode } from "../src/store";
|
||||||
|
|
||||||
describe("MCP HTTP Transport", () => {
|
describe("MCP HTTP Transport", () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user