@tobilu/qmd 2.0.1 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +177 -0
- package/README.md +64 -1
- package/bin/qmd +49 -4
- package/dist/ast.d.ts +65 -0
- package/dist/ast.js +334 -0
- package/dist/bench/bench.d.ts +23 -0
- package/dist/bench/bench.js +280 -0
- package/dist/bench/score.d.ts +33 -0
- package/dist/bench/score.js +88 -0
- package/dist/bench/types.d.ts +80 -0
- package/dist/bench/types.js +8 -0
- package/dist/cli/formatter.js +5 -1
- package/dist/cli/qmd.d.ts +27 -0
- package/dist/cli/qmd.js +1328 -115
- package/dist/collections.d.ts +20 -0
- package/dist/collections.js +32 -7
- package/dist/db.d.ts +14 -3
- package/dist/db.js +45 -4
- package/dist/index.d.ts +11 -1
- package/dist/index.js +18 -5
- package/dist/llm.d.ts +77 -6
- package/dist/llm.js +445 -62
- package/dist/mcp/server.d.ts +6 -3
- package/dist/mcp/server.js +68 -29
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +4 -0
- package/dist/store.d.ts +148 -23
- package/dist/store.js +1018 -255
- package/package.json +48 -20
- package/scripts/build.mjs +29 -0
- package/scripts/check-package-grammars.mjs +29 -0
- package/scripts/package-smoke.mjs +65 -0
- package/scripts/test-all.mjs +27 -0
- package/skills/qmd/SKILL.md +203 -0
- package/skills/qmd/references/mcp-setup.md +102 -0
- package/skills/release/SKILL.md +139 -0
- package/skills/release/scripts/install-hooks.sh +38 -0
- package/dist/embedded-skills.d.ts +0 -6
- package/dist/embedded-skills.js +0 -14
package/dist/mcp/server.d.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Follows MCP spec 2025-06-18 for proper response types.
|
|
8
8
|
*/
|
|
9
|
-
export
|
|
9
|
+
export type McpStartupOptions = {
|
|
10
|
+
dbPath?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function startMcpServer(options?: McpStartupOptions): Promise<void>;
|
|
10
13
|
export type HttpServerHandle = {
|
|
11
14
|
httpServer: import("http").Server;
|
|
12
15
|
port: number;
|
|
@@ -16,6 +19,6 @@ export type HttpServerHandle = {
|
|
|
16
19
|
* Start MCP server over Streamable HTTP (JSON responses, no SSE).
|
|
17
20
|
* Binds to localhost only. Returns a handle for shutdown and port discovery.
|
|
18
21
|
*/
|
|
19
|
-
export declare function startMcpHttpServer(port: number, options?: {
|
|
22
|
+
export declare function startMcpHttpServer(port: number, options?: ({
|
|
20
23
|
quiet?: boolean;
|
|
21
|
-
}): Promise<HttpServerHandle>;
|
|
24
|
+
} & McpStartupOptions)): Promise<HttpServerHandle>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
11
13
|
import { fileURLToPath } from "url";
|
|
12
14
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
16
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
15
17
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
16
18
|
import { z } from "zod";
|
|
19
|
+
import { existsSync } from "fs";
|
|
17
20
|
import { createStore, extractSnippet, addLineNumbers, getDefaultDbPath, DEFAULT_MULTI_GET_MAX_BYTES, } from "../index.js";
|
|
21
|
+
import { getConfigPath } from "../collections.js";
|
|
22
|
+
import { enableProductionMode } from "../store.js";
|
|
18
23
|
// =============================================================================
|
|
19
24
|
// Helper functions
|
|
20
25
|
// =============================================================================
|
|
@@ -39,6 +44,16 @@ function formatSearchSummary(results, query) {
|
|
|
39
44
|
}
|
|
40
45
|
return lines.join('\n');
|
|
41
46
|
}
|
|
47
|
+
function getPackageVersion() {
|
|
48
|
+
try {
|
|
49
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
50
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
51
|
+
return pkg.version ?? "unknown";
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return "unknown";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
42
57
|
// =============================================================================
|
|
43
58
|
// MCP Server
|
|
44
59
|
// =============================================================================
|
|
@@ -49,7 +64,6 @@ function formatSearchSummary(results, query) {
|
|
|
49
64
|
*/
|
|
50
65
|
async function buildInstructions(store) {
|
|
51
66
|
const status = await store.getStatus();
|
|
52
|
-
const contexts = await store.listContexts();
|
|
53
67
|
const globalCtx = await store.getGlobalContext();
|
|
54
68
|
const lines = [];
|
|
55
69
|
// --- What is this? ---
|
|
@@ -57,15 +71,13 @@ async function buildInstructions(store) {
|
|
|
57
71
|
if (globalCtx)
|
|
58
72
|
lines.push(`Context: ${globalCtx}`);
|
|
59
73
|
// --- What's searchable? ---
|
|
74
|
+
// Emit names only — the per-collection doc counts and descriptions can run to ~1.5 KB
|
|
75
|
+
// across a dozen collections, and the same info is available on demand via the `status` tool.
|
|
60
76
|
if (status.collections.length > 0) {
|
|
61
77
|
lines.push("");
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const rootCtx = contexts.find(c => c.collection === col.name && (c.path === "" || c.path === "/"));
|
|
66
|
-
const desc = rootCtx ? ` — ${rootCtx.context}` : "";
|
|
67
|
-
lines.push(` - "${col.name}" (${col.documents} docs)${desc}`);
|
|
68
|
-
}
|
|
78
|
+
const names = status.collections.map(c => c.name).join(", ");
|
|
79
|
+
lines.push(`Collections (scope with \`collection\` parameter): ${names}`);
|
|
80
|
+
lines.push("Call the `status` tool for collection descriptions, paths, and per-collection doc counts.");
|
|
69
81
|
}
|
|
70
82
|
// --- Capability gaps ---
|
|
71
83
|
if (!status.hasVectorIndex) {
|
|
@@ -108,7 +120,7 @@ async function buildInstructions(store) {
|
|
|
108
120
|
* Shared by both stdio and HTTP transports.
|
|
109
121
|
*/
|
|
110
122
|
async function createMcpServer(store) {
|
|
111
|
-
const server = new McpServer({ name: "qmd", version:
|
|
123
|
+
const server = new McpServer({ name: "qmd", version: getPackageVersion() }, { instructions: await buildInstructions(store) });
|
|
112
124
|
// Pre-fetch default collection names for search tools
|
|
113
125
|
const defaultCollectionNames = await store.getDefaultCollectionNames();
|
|
114
126
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +167,8 @@ async function createMcpServer(store) {
|
|
|
155
167
|
title: "Query",
|
|
156
168
|
description: `Search the knowledge base using a query document — one or more typed sub-queries combined for best recall.
|
|
157
169
|
|
|
170
|
+
Each result includes a \`line\` field with the absolute 1-indexed line of the best match in the source markdown. To read more context around a hit, call \`get(file, fromLine = max(1, line - 20), maxLines = 80, lineNumbers = true)\`.
|
|
171
|
+
|
|
158
172
|
## Query Types
|
|
159
173
|
|
|
160
174
|
**lex** — BM25 keyword search. Fast, exact, no LLM needed.
|
|
@@ -218,8 +232,9 @@ Intent-aware lex (C++ performance, not sports):
|
|
|
218
232
|
candidateLimit: z.number().optional().describe("Maximum candidates to rerank (default: 40, lower = faster but may miss results)"),
|
|
219
233
|
collections: z.array(z.string()).optional().describe("Filter to collections (OR match)"),
|
|
220
234
|
intent: z.string().optional().describe("Background context to disambiguate the query. Example: query='performance', intent='web page load times and Core Web Vitals'. Does not search on its own."),
|
|
235
|
+
rerank: z.boolean().optional().default(true).describe("Rerank results using LLM (default: true). Set to false for faster results on CPU-only machines."),
|
|
221
236
|
},
|
|
222
|
-
}, async ({ searches, limit, minScore, candidateLimit, collections, intent }) => {
|
|
237
|
+
}, async ({ searches, limit, minScore, candidateLimit, collections, intent, rerank }) => {
|
|
223
238
|
// Map to internal format
|
|
224
239
|
const queries = searches.map(s => ({
|
|
225
240
|
type: s.type,
|
|
@@ -232,6 +247,8 @@ Intent-aware lex (C++ performance, not sports):
|
|
|
232
247
|
collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
|
|
233
248
|
limit,
|
|
234
249
|
minScore,
|
|
250
|
+
candidateLimit,
|
|
251
|
+
rerank,
|
|
235
252
|
intent,
|
|
236
253
|
});
|
|
237
254
|
// Use first lex or vec query for snippet extraction
|
|
@@ -239,13 +256,14 @@ Intent-aware lex (C++ performance, not sports):
|
|
|
239
256
|
|| searches.find(s => s.type === 'vec')?.query
|
|
240
257
|
|| searches[0]?.query || "";
|
|
241
258
|
const filtered = results.map(r => {
|
|
242
|
-
const { line, snippet } = extractSnippet(r.
|
|
259
|
+
const { line, snippet } = extractSnippet(r.body, primaryQuery, 300, r.bestChunkPos, r.bestChunk.length, intent);
|
|
243
260
|
return {
|
|
244
261
|
docid: `#${r.docid}`,
|
|
245
262
|
file: r.displayPath,
|
|
246
263
|
title: r.title,
|
|
247
264
|
score: Math.round(r.score * 100) / 100,
|
|
248
265
|
context: r.context,
|
|
266
|
+
line,
|
|
249
267
|
snippet: addLineNumbers(snippet, line),
|
|
250
268
|
};
|
|
251
269
|
});
|
|
@@ -276,6 +294,8 @@ Intent-aware lex (C++ performance, not sports):
|
|
|
276
294
|
parsedFromLine = parseInt(colonMatch[1], 10);
|
|
277
295
|
lookup = lookup.slice(0, -colonMatch[0].length);
|
|
278
296
|
}
|
|
297
|
+
if (parsedFromLine !== undefined)
|
|
298
|
+
parsedFromLine = Math.max(1, parsedFromLine);
|
|
279
299
|
const result = await store.get(lookup, { includeBody: false });
|
|
280
300
|
if ("error" in result) {
|
|
281
301
|
let msg = `Document not found: ${file}`;
|
|
@@ -387,7 +407,7 @@ Intent-aware lex (C++ performance, not sports):
|
|
|
387
407
|
` Collections: ${status.collections.length}`,
|
|
388
408
|
];
|
|
389
409
|
for (const col of status.collections) {
|
|
390
|
-
summary.push(` - ${col.path} (${col.documents} docs)`);
|
|
410
|
+
summary.push(` - ${col.name}: ${col.path} (${col.documents} docs)`);
|
|
391
411
|
}
|
|
392
412
|
return {
|
|
393
413
|
content: [{ type: "text", text: summary.join('\n') }],
|
|
@@ -396,11 +416,18 @@ Intent-aware lex (C++ performance, not sports):
|
|
|
396
416
|
});
|
|
397
417
|
return server;
|
|
398
418
|
}
|
|
399
|
-
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
419
|
+
export async function startMcpServer(options = {}) {
|
|
420
|
+
// Opt into production mode when the MCP server is actually started, not
|
|
421
|
+
// when this module is merely imported for its exports. Importing the module
|
|
422
|
+
// at the top level flipped the global production flag and broke test
|
|
423
|
+
// isolation for downstream suites that expect the default (development)
|
|
424
|
+
// database path behaviour.
|
|
425
|
+
enableProductionMode();
|
|
426
|
+
const configPath = getConfigPath();
|
|
427
|
+
const store = await createStore({
|
|
428
|
+
dbPath: options.dbPath ?? getDefaultDbPath(),
|
|
429
|
+
...(existsSync(configPath) ? { configPath } : {}),
|
|
430
|
+
});
|
|
404
431
|
const server = await createMcpServer(store);
|
|
405
432
|
const transport = new StdioServerTransport();
|
|
406
433
|
await server.connect(transport);
|
|
@@ -409,8 +436,16 @@ export async function startMcpServer() {
|
|
|
409
436
|
* Start MCP server over Streamable HTTP (JSON responses, no SSE).
|
|
410
437
|
* Binds to localhost only. Returns a handle for shutdown and port discovery.
|
|
411
438
|
*/
|
|
412
|
-
export async function startMcpHttpServer(port, options) {
|
|
413
|
-
|
|
439
|
+
export async function startMcpHttpServer(port, options = {}) {
|
|
440
|
+
// See startMcpServer() for the rationale — flip production mode here so the
|
|
441
|
+
// HTTP transport resolves the real database path, without leaking state into
|
|
442
|
+
// callers that only import this module for its exports (e.g. tests).
|
|
443
|
+
enableProductionMode();
|
|
444
|
+
const configPath = getConfigPath();
|
|
445
|
+
const store = await createStore({
|
|
446
|
+
dbPath: options.dbPath ?? getDefaultDbPath(),
|
|
447
|
+
...(existsSync(configPath) ? { configPath } : {}),
|
|
448
|
+
});
|
|
414
449
|
// Pre-fetch default collection names for REST endpoint
|
|
415
450
|
const defaultCollectionNames = await store.getDefaultCollectionNames();
|
|
416
451
|
// Session map: each client gets its own McpServer + Transport pair (MCP spec requirement).
|
|
@@ -442,7 +477,7 @@ export async function startMcpHttpServer(port, options) {
|
|
|
442
477
|
}
|
|
443
478
|
/** Extract a human-readable label from a JSON-RPC body */
|
|
444
479
|
function describeRequest(body) {
|
|
445
|
-
const method = body
|
|
480
|
+
const method = typeof body.method === "string" ? body.method : "unknown";
|
|
446
481
|
if (method === "tools/call") {
|
|
447
482
|
const tool = body.params?.name ?? "?";
|
|
448
483
|
const args = body.params?.arguments;
|
|
@@ -493,31 +528,35 @@ export async function startMcpHttpServer(port, options) {
|
|
|
493
528
|
return;
|
|
494
529
|
}
|
|
495
530
|
// Map to internal format
|
|
496
|
-
const
|
|
531
|
+
const searches = params.searches;
|
|
532
|
+
const queries = searches.map((s) => ({
|
|
497
533
|
type: s.type,
|
|
498
534
|
query: String(s.query || ""),
|
|
499
535
|
}));
|
|
500
536
|
// Use default collections if none specified
|
|
501
|
-
const effectiveCollections = params.collections
|
|
537
|
+
const effectiveCollections = Array.isArray(params.collections) ? params.collections.map(String) : defaultCollectionNames;
|
|
502
538
|
const results = await store.search({
|
|
503
539
|
queries,
|
|
504
540
|
collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
|
|
505
|
-
limit: params.limit
|
|
506
|
-
minScore: params.minScore
|
|
507
|
-
|
|
541
|
+
limit: typeof params.limit === "number" ? params.limit : 10,
|
|
542
|
+
minScore: typeof params.minScore === "number" ? params.minScore : 0,
|
|
543
|
+
candidateLimit: typeof params.candidateLimit === "number" ? params.candidateLimit : undefined,
|
|
544
|
+
intent: typeof params.intent === "string" ? params.intent : undefined,
|
|
545
|
+
rerank: typeof params.rerank === "boolean" ? params.rerank : undefined,
|
|
508
546
|
});
|
|
509
547
|
// Use first lex or vec query for snippet extraction
|
|
510
|
-
const primaryQuery =
|
|
511
|
-
||
|
|
512
|
-
||
|
|
548
|
+
const primaryQuery = searches.find((s) => s.type === 'lex')?.query
|
|
549
|
+
|| searches.find((s) => s.type === 'vec')?.query
|
|
550
|
+
|| searches[0]?.query || "";
|
|
513
551
|
const formatted = results.map(r => {
|
|
514
|
-
const { line, snippet } = extractSnippet(r.
|
|
552
|
+
const { line, snippet } = extractSnippet(r.body, String(primaryQuery), 300, r.bestChunkPos, r.bestChunk.length, typeof params.intent === "string" ? params.intent : undefined);
|
|
515
553
|
return {
|
|
516
554
|
docid: `#${r.docid}`,
|
|
517
555
|
file: r.displayPath,
|
|
518
556
|
title: r.title,
|
|
519
557
|
score: Math.round(r.score * 100) / 100,
|
|
520
558
|
context: r.context,
|
|
559
|
+
line,
|
|
521
560
|
snippet: addLineNumbers(snippet, line),
|
|
522
561
|
};
|
|
523
562
|
});
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function qmdHomedir(): string;
|
package/dist/paths.js
ADDED
package/dist/store.d.ts
CHANGED
|
@@ -13,17 +13,20 @@
|
|
|
13
13
|
import type { Database } from "./db.js";
|
|
14
14
|
import { LlamaCpp, formatQueryForEmbedding, formatDocForEmbedding, type ILLMSession } from "./llm.js";
|
|
15
15
|
import type { NamedCollection, Collection, CollectionConfig } from "./collections.js";
|
|
16
|
-
export declare const DEFAULT_EMBED_MODEL = "embeddinggemma";
|
|
17
|
-
export declare const DEFAULT_RERANK_MODEL = "
|
|
18
|
-
export declare const DEFAULT_QUERY_MODEL = "
|
|
16
|
+
export declare const DEFAULT_EMBED_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
|
|
17
|
+
export declare const DEFAULT_RERANK_MODEL = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
|
|
18
|
+
export declare const DEFAULT_QUERY_MODEL = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
|
|
19
19
|
export declare const DEFAULT_GLOB = "**/*.md";
|
|
20
20
|
export declare const DEFAULT_MULTI_GET_MAX_BYTES: number;
|
|
21
|
+
export declare const DEFAULT_EMBED_MAX_DOCS_PER_BATCH = 64;
|
|
22
|
+
export declare const DEFAULT_EMBED_MAX_BATCH_BYTES: number;
|
|
21
23
|
export declare const CHUNK_SIZE_TOKENS = 900;
|
|
22
24
|
export declare const CHUNK_OVERLAP_TOKENS: number;
|
|
23
25
|
export declare const CHUNK_SIZE_CHARS: number;
|
|
24
26
|
export declare const CHUNK_OVERLAP_CHARS: number;
|
|
25
27
|
export declare const CHUNK_WINDOW_TOKENS = 200;
|
|
26
28
|
export declare const CHUNK_WINDOW_CHARS: number;
|
|
29
|
+
export declare function getEmbeddingFingerprint(model?: string): string;
|
|
27
30
|
/**
|
|
28
31
|
* A potential break point in the document with a base score indicating quality.
|
|
29
32
|
*/
|
|
@@ -76,6 +79,20 @@ export declare function isInsideCodeFence(pos: number, fences: CodeFenceRegion[]
|
|
|
76
79
|
* @returns The best position to cut at
|
|
77
80
|
*/
|
|
78
81
|
export declare function findBestCutoff(breakPoints: BreakPoint[], targetCharPos: number, windowChars?: number, decayFactor?: number, codeFences?: CodeFenceRegion[]): number;
|
|
82
|
+
export type ChunkStrategy = "auto" | "regex";
|
|
83
|
+
/**
|
|
84
|
+
* Merge two sets of break points (e.g. regex + AST), keeping the highest
|
|
85
|
+
* score at each position. Result is sorted by position.
|
|
86
|
+
*/
|
|
87
|
+
export declare function mergeBreakPoints(a: BreakPoint[], b: BreakPoint[]): BreakPoint[];
|
|
88
|
+
/**
|
|
89
|
+
* Core chunk algorithm that operates on precomputed break points and code fences.
|
|
90
|
+
* This is the shared implementation used by both regex-only and AST-aware chunking.
|
|
91
|
+
*/
|
|
92
|
+
export declare function chunkDocumentWithBreakPoints(content: string, breakPoints: BreakPoint[], codeFences: CodeFenceRegion[], maxChars?: number, overlapChars?: number, windowChars?: number): {
|
|
93
|
+
text: string;
|
|
94
|
+
pos: number;
|
|
95
|
+
}[];
|
|
79
96
|
export declare const STRONG_SIGNAL_MIN_SCORE = 0.85;
|
|
80
97
|
export declare const STRONG_SIGNAL_MIN_GAP = 0.15;
|
|
81
98
|
export declare const RERANK_CANDIDATE_LIMIT = 40;
|
|
@@ -118,12 +135,15 @@ export declare function normalizePathSeparators(path: string): string;
|
|
|
118
135
|
export declare function getRelativePathFromPrefix(path: string, prefix: string): string | null;
|
|
119
136
|
export declare function resolve(...paths: string[]): string;
|
|
120
137
|
export declare function enableProductionMode(): void;
|
|
138
|
+
/** Reset production mode flag — only for testing. */
|
|
139
|
+
export declare function _resetProductionModeForTesting(): void;
|
|
121
140
|
export declare function getDefaultDbPath(indexName?: string): string;
|
|
122
141
|
export declare function getPwd(): string;
|
|
123
142
|
export declare function getRealPath(path: string): string;
|
|
124
143
|
export type VirtualPath = {
|
|
125
144
|
collectionName: string;
|
|
126
145
|
path: string;
|
|
146
|
+
indexName?: string;
|
|
127
147
|
};
|
|
128
148
|
/**
|
|
129
149
|
* Normalize explicit virtual path formats to standard qmd:// format.
|
|
@@ -146,7 +166,7 @@ export declare function parseVirtualPath(virtualPath: string): VirtualPath | nul
|
|
|
146
166
|
/**
|
|
147
167
|
* Build a virtual path from collection name and relative path.
|
|
148
168
|
*/
|
|
149
|
-
export declare function buildVirtualPath(collectionName: string, path: string): string;
|
|
169
|
+
export declare function buildVirtualPath(collectionName: string, path: string, indexName?: string): string;
|
|
150
170
|
/**
|
|
151
171
|
* Check if a path is explicitly a virtual path.
|
|
152
172
|
* Only recognizes explicit virtual path formats:
|
|
@@ -167,6 +187,12 @@ export declare function resolveVirtualPath(db: Database, virtualPath: string): s
|
|
|
167
187
|
*/
|
|
168
188
|
export declare function toVirtualPath(db: Database, absolutePath: string): string | null;
|
|
169
189
|
export declare function verifySqliteVecLoaded(db: Database): void;
|
|
190
|
+
/**
|
|
191
|
+
* FTS5's unicode61 tokenizer does not segment CJK text into searchable words.
|
|
192
|
+
* Normalize CJK runs by spacing every character so exact CJK queries can be
|
|
193
|
+
* translated into phrase queries while Latin text keeps the default tokenizer.
|
|
194
|
+
*/
|
|
195
|
+
export declare function normalizeCjkForFTS(text: string): string;
|
|
170
196
|
export declare function getStoreCollections(db: Database): NamedCollection[];
|
|
171
197
|
export declare function getStoreCollection(db: Database, name: string): NamedCollection | null;
|
|
172
198
|
export declare function getStoreGlobalContext(db: Database): string | undefined;
|
|
@@ -196,9 +222,9 @@ export type Store = {
|
|
|
196
222
|
llm?: LlamaCpp;
|
|
197
223
|
close: () => void;
|
|
198
224
|
ensureVecTable: (dimensions: number) => void;
|
|
199
|
-
getHashesNeedingEmbedding: () => number;
|
|
200
|
-
getIndexHealth: () => IndexHealthInfo;
|
|
201
|
-
getStatus: () => IndexStatus;
|
|
225
|
+
getHashesNeedingEmbedding: (model?: string) => number;
|
|
226
|
+
getIndexHealth: (model?: string) => IndexHealthInfo;
|
|
227
|
+
getStatus: (model?: string) => IndexStatus;
|
|
202
228
|
getCacheKey: typeof getCacheKey;
|
|
203
229
|
getCachedResult: (cacheKey: string) => string | null;
|
|
204
230
|
setCachedResult: (cacheKey: string, result: string) => void;
|
|
@@ -266,6 +292,11 @@ export type Store = {
|
|
|
266
292
|
hash: string;
|
|
267
293
|
title: string;
|
|
268
294
|
} | null;
|
|
295
|
+
findOrMigrateLegacyDocument: (collectionName: string, path: string) => {
|
|
296
|
+
id: number;
|
|
297
|
+
hash: string;
|
|
298
|
+
title: string;
|
|
299
|
+
} | null;
|
|
269
300
|
updateDocumentTitle: (documentId: number, title: string, modifiedAt: string) => void;
|
|
270
301
|
updateDocument: (documentId: number, title: string, hash: string, modifiedAt: string) => void;
|
|
271
302
|
deactivateDocument: (collectionName: string, path: string) => void;
|
|
@@ -276,7 +307,7 @@ export type Store = {
|
|
|
276
307
|
path: string;
|
|
277
308
|
}[];
|
|
278
309
|
clearAllEmbeddings: () => void;
|
|
279
|
-
insertEmbedding: (hash: string, seq: number, pos: number, embedding: Float32Array, model: string, embeddedAt: string) => void;
|
|
310
|
+
insertEmbedding: (hash: string, seq: number, pos: number, embedding: Float32Array, model: string, embeddedAt: string, totalChunks?: number, fingerprint?: string) => void;
|
|
280
311
|
};
|
|
281
312
|
export type ReindexProgress = {
|
|
282
313
|
file: string;
|
|
@@ -298,29 +329,49 @@ export declare function reindexCollection(store: Store, collectionPath: string,
|
|
|
298
329
|
ignorePatterns?: string[];
|
|
299
330
|
onProgress?: (info: ReindexProgress) => void;
|
|
300
331
|
}): Promise<ReindexResult>;
|
|
332
|
+
export type EmbedFailure = {
|
|
333
|
+
path: string;
|
|
334
|
+
hash: string;
|
|
335
|
+
seq: number;
|
|
336
|
+
attempts: number;
|
|
337
|
+
reason: string;
|
|
338
|
+
};
|
|
301
339
|
export type EmbedProgress = {
|
|
302
340
|
chunksEmbedded: number;
|
|
303
341
|
totalChunks: number;
|
|
304
342
|
bytesProcessed: number;
|
|
305
343
|
totalBytes: number;
|
|
344
|
+
/** Active failed chunks still awaiting a successful retry. */
|
|
306
345
|
errors: number;
|
|
346
|
+
failures?: EmbedFailure[];
|
|
307
347
|
};
|
|
308
348
|
export type EmbedResult = {
|
|
309
349
|
docsProcessed: number;
|
|
310
350
|
chunksEmbedded: number;
|
|
351
|
+
/** Active failed chunks that did not recover after retries. */
|
|
311
352
|
errors: number;
|
|
353
|
+
failures?: EmbedFailure[];
|
|
312
354
|
durationMs: number;
|
|
313
355
|
};
|
|
356
|
+
export type EmbedOptions = {
|
|
357
|
+
force?: boolean;
|
|
358
|
+
model?: string;
|
|
359
|
+
/**
|
|
360
|
+
* Restrict embedding to documents in a single collection.
|
|
361
|
+
* When omitted, all pending documents across every collection are embedded.
|
|
362
|
+
*/
|
|
363
|
+
collection?: string;
|
|
364
|
+
maxDocsPerBatch?: number;
|
|
365
|
+
maxBatchBytes?: number;
|
|
366
|
+
chunkStrategy?: ChunkStrategy;
|
|
367
|
+
onProgress?: (info: EmbedProgress) => void;
|
|
368
|
+
};
|
|
314
369
|
/**
|
|
315
370
|
* Generate vector embeddings for documents that need them.
|
|
316
371
|
* Pure function — no console output, no db lifecycle management.
|
|
317
372
|
* Uses the store's LlamaCpp instance if set, otherwise the global singleton.
|
|
318
373
|
*/
|
|
319
|
-
export declare function generateEmbeddings(store: Store, options?:
|
|
320
|
-
force?: boolean;
|
|
321
|
-
model?: string;
|
|
322
|
-
onProgress?: (info: EmbedProgress) => void;
|
|
323
|
-
}): Promise<EmbedResult>;
|
|
374
|
+
export declare function generateEmbeddings(store: Store, options?: EmbedOptions): Promise<EmbedResult>;
|
|
324
375
|
/**
|
|
325
376
|
* Create a new store instance with the given database path.
|
|
326
377
|
* If no path is provided, uses the default path (~/.cache/qmd/index.sqlite).
|
|
@@ -432,13 +483,19 @@ export type IndexStatus = {
|
|
|
432
483
|
hasVectorIndex: boolean;
|
|
433
484
|
collections: CollectionInfo[];
|
|
434
485
|
};
|
|
435
|
-
export declare function getHashesNeedingEmbedding(db: Database): number;
|
|
486
|
+
export declare function getHashesNeedingEmbedding(db: Database, collection?: string, model?: string): number;
|
|
436
487
|
export type IndexHealthInfo = {
|
|
437
488
|
needsEmbedding: number;
|
|
438
489
|
totalDocs: number;
|
|
439
490
|
daysStale: number | null;
|
|
440
491
|
};
|
|
441
|
-
export
|
|
492
|
+
export type LegacyFingerprintAdoptionResult = {
|
|
493
|
+
checked: boolean;
|
|
494
|
+
adopted: number;
|
|
495
|
+
reason: string;
|
|
496
|
+
};
|
|
497
|
+
export declare function maybeAdoptLegacyEmbeddingFingerprint(store: Store, model?: string): Promise<LegacyFingerprintAdoptionResult>;
|
|
498
|
+
export declare function getIndexHealth(db: Database, model?: string): IndexHealthInfo;
|
|
442
499
|
export declare function getCacheKey(url: string, body: object): string;
|
|
443
500
|
export declare function getCachedResult(db: Database, cacheKey: string): string | null;
|
|
444
501
|
export declare function setCachedResult(db: Database, cacheKey: string, result: string): void;
|
|
@@ -454,7 +511,9 @@ export declare function deleteLLMCache(db: Database): number;
|
|
|
454
511
|
*/
|
|
455
512
|
export declare function deleteInactiveDocuments(db: Database): number;
|
|
456
513
|
/**
|
|
457
|
-
* Remove orphaned content hashes that are not referenced by any
|
|
514
|
+
* Remove orphaned content hashes that are not referenced by any document.
|
|
515
|
+
* Inactive documents are soft-deleted tombstones, so their content rows must
|
|
516
|
+
* remain referenced until deleteInactiveDocuments() hard-deletes them.
|
|
458
517
|
* Returns the number of orphaned content hashes deleted.
|
|
459
518
|
*/
|
|
460
519
|
export declare function cleanupOrphanedContent(db: Database): number;
|
|
@@ -487,6 +546,20 @@ export declare function findActiveDocument(db: Database, collectionName: string,
|
|
|
487
546
|
hash: string;
|
|
488
547
|
title: string;
|
|
489
548
|
} | null;
|
|
549
|
+
/**
|
|
550
|
+
* Find an active document, falling back to a case-insensitive path match.
|
|
551
|
+
* If found under a different casing, renames it in-place and rebuilds the
|
|
552
|
+
* FTS entry. Embeddings are keyed by content hash, so the rename is
|
|
553
|
+
* safe — no re-embedding required.
|
|
554
|
+
*
|
|
555
|
+
* @internal Used by reindexCollection and indexFiles during qmd update.
|
|
556
|
+
* Returns null if the document does not exist under either path.
|
|
557
|
+
*/
|
|
558
|
+
export declare function findOrMigrateLegacyDocument(db: Database, collectionName: string, path: string): {
|
|
559
|
+
id: number;
|
|
560
|
+
hash: string;
|
|
561
|
+
title: string;
|
|
562
|
+
} | null;
|
|
490
563
|
/**
|
|
491
564
|
* Update the title and modified_at timestamp for a document.
|
|
492
565
|
*/
|
|
@@ -505,15 +578,34 @@ export declare function deactivateDocument(db: Database, collectionName: string,
|
|
|
505
578
|
*/
|
|
506
579
|
export declare function getActiveDocumentPaths(db: Database, collectionName: string): string[];
|
|
507
580
|
export { formatQueryForEmbedding, formatDocForEmbedding };
|
|
581
|
+
/**
|
|
582
|
+
* Chunk a document using regex-only break point detection.
|
|
583
|
+
* This is the sync, backward-compatible API used by tests and legacy callers.
|
|
584
|
+
*/
|
|
508
585
|
export declare function chunkDocument(content: string, maxChars?: number, overlapChars?: number, windowChars?: number): {
|
|
509
586
|
text: string;
|
|
510
587
|
pos: number;
|
|
511
588
|
}[];
|
|
589
|
+
/**
|
|
590
|
+
* Async AST-aware chunking. Detects language from filepath, computes AST
|
|
591
|
+
* break points for supported code files, merges with regex break points,
|
|
592
|
+
* and delegates to the shared chunk algorithm.
|
|
593
|
+
*
|
|
594
|
+
* Falls back to regex-only when strategy is "regex", filepath is absent,
|
|
595
|
+
* or language is unsupported.
|
|
596
|
+
*/
|
|
597
|
+
export declare function chunkDocumentAsync(content: string, maxChars?: number, overlapChars?: number, windowChars?: number, filepath?: string, chunkStrategy?: ChunkStrategy): Promise<{
|
|
598
|
+
text: string;
|
|
599
|
+
pos: number;
|
|
600
|
+
}[]>;
|
|
512
601
|
/**
|
|
513
602
|
* Chunk a document by actual token count using the LLM tokenizer.
|
|
514
603
|
* More accurate than character-based chunking but requires async.
|
|
604
|
+
*
|
|
605
|
+
* When filepath and chunkStrategy are provided, uses AST-aware break points
|
|
606
|
+
* for supported code files.
|
|
515
607
|
*/
|
|
516
|
-
export declare function chunkDocumentByTokens(content: string, maxTokens?: number, overlapTokens?: number, windowTokens?: number): Promise<{
|
|
608
|
+
export declare function chunkDocumentByTokens(content: string, maxTokens?: number, overlapTokens?: number, windowTokens?: number, filepath?: string, chunkStrategy?: ChunkStrategy, signal?: AbortSignal): Promise<{
|
|
517
609
|
text: string;
|
|
518
610
|
pos: number;
|
|
519
611
|
tokens: number;
|
|
@@ -640,6 +732,7 @@ export declare function getCollectionsWithoutContext(db: Database): {
|
|
|
640
732
|
* Useful for suggesting where context might be needed.
|
|
641
733
|
*/
|
|
642
734
|
export declare function getTopLevelPathsWithoutContext(db: Database, collectionName: string): string[];
|
|
735
|
+
export declare function sanitizeFTS5Term(term: string): string;
|
|
643
736
|
/**
|
|
644
737
|
* Validate that a vec/hyde query doesn't use lex-only syntax.
|
|
645
738
|
* Returns error message if invalid, null if valid.
|
|
@@ -652,21 +745,39 @@ export declare function searchVec(db: Database, query: string, model: string, li
|
|
|
652
745
|
* Get all unique content hashes that need embeddings (from active documents).
|
|
653
746
|
* Returns hash, document body, and a sample path for display purposes.
|
|
654
747
|
*/
|
|
655
|
-
export declare function getHashesForEmbedding(db: Database): {
|
|
748
|
+
export declare function getHashesForEmbedding(db: Database, model?: string): {
|
|
656
749
|
hash: string;
|
|
657
750
|
body: string;
|
|
658
751
|
path: string;
|
|
659
752
|
}[];
|
|
660
753
|
/**
|
|
661
|
-
* Clear
|
|
662
|
-
*
|
|
754
|
+
* Clear embeddings for the whole index, or just for one collection.
|
|
755
|
+
*
|
|
756
|
+
* When `collection` is omitted the entire content_vectors table is emptied and
|
|
757
|
+
* the vectors_vec virtual table is dropped (it is recreated with the right
|
|
758
|
+
* dimensions on the next embed run).
|
|
759
|
+
*
|
|
760
|
+
* When `collection` is provided, only vectors whose hash is referenced
|
|
761
|
+
* exclusively by active documents in that collection are removed. Hashes
|
|
762
|
+
* shared with active documents in other collections are left in place so
|
|
763
|
+
* vector search keeps working there (content_vectors is keyed globally by
|
|
764
|
+
* content hash; identical document bodies across collections share a row).
|
|
765
|
+
* vectors_vec is preserved so other collections keep working unless the scoped
|
|
766
|
+
* clear empties content_vectors entirely, in which case it is dropped so the
|
|
767
|
+
* next embed can recreate the table with the current dimensions.
|
|
663
768
|
*/
|
|
664
|
-
export declare function clearAllEmbeddings(db: Database): void;
|
|
769
|
+
export declare function clearAllEmbeddings(db: Database, collection?: string): void;
|
|
665
770
|
/**
|
|
666
771
|
* Insert a single embedding into both content_vectors and vectors_vec tables.
|
|
667
772
|
* The hash_seq key is formatted as "hash_seq" for the vectors_vec table.
|
|
773
|
+
*
|
|
774
|
+
* content_vectors is inserted first so that getHashesForEmbedding (which checks
|
|
775
|
+
* only content_vectors) won't re-select the hash on a crash between the two inserts.
|
|
776
|
+
*
|
|
777
|
+
* vectors_vec uses DELETE + INSERT instead of INSERT OR REPLACE because sqlite-vec's
|
|
778
|
+
* vec0 virtual tables silently ignore the OR REPLACE conflict clause.
|
|
668
779
|
*/
|
|
669
|
-
export declare function insertEmbedding(db: Database, hash: string, seq: number, pos: number, embedding: Float32Array, model: string, embeddedAt: string): void;
|
|
780
|
+
export declare function insertEmbedding(db: Database, hash: string, seq: number, pos: number, embedding: Float32Array, model: string, embeddedAt: string, totalChunks?: number, fingerprint?: string): void;
|
|
670
781
|
export declare function expandQuery(query: string, model: string | undefined, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise<ExpandedQuery[]>;
|
|
671
782
|
export declare function rerank(query: string, documents: {
|
|
672
783
|
file: string;
|
|
@@ -711,7 +822,7 @@ export declare function findDocuments(db: Database, pattern: string, options?: {
|
|
|
711
822
|
docs: MultiGetResult[];
|
|
712
823
|
errors: string[];
|
|
713
824
|
};
|
|
714
|
-
export declare function getStatus(db: Database): IndexStatus;
|
|
825
|
+
export declare function getStatus(db: Database, model?: string): IndexStatus;
|
|
715
826
|
export type SnippetResult = {
|
|
716
827
|
line: number;
|
|
717
828
|
snippet: string;
|
|
@@ -763,6 +874,7 @@ export interface HybridQueryOptions {
|
|
|
763
874
|
explain?: boolean;
|
|
764
875
|
intent?: string;
|
|
765
876
|
skipRerank?: boolean;
|
|
877
|
+
chunkStrategy?: ChunkStrategy;
|
|
766
878
|
hooks?: SearchHooks;
|
|
767
879
|
}
|
|
768
880
|
export interface HybridQueryResult {
|
|
@@ -782,6 +894,18 @@ export type RankedListMeta = {
|
|
|
782
894
|
queryType: "original" | "lex" | "vec" | "hyde";
|
|
783
895
|
query: string;
|
|
784
896
|
};
|
|
897
|
+
/**
|
|
898
|
+
* RRF list weights for hybridQuery.
|
|
899
|
+
*
|
|
900
|
+
* Original-query retrieval paths are the primary evidence and get 2x weight:
|
|
901
|
+
* - original FTS
|
|
902
|
+
* - original vector search
|
|
903
|
+
*
|
|
904
|
+
* Expansion-derived lists (lex/vec/hyde) stay at 1x regardless of list order,
|
|
905
|
+
* so a lex expansion inserted before original vector search cannot steal the
|
|
906
|
+
* original vector boost.
|
|
907
|
+
*/
|
|
908
|
+
export declare function getHybridRrfWeights(rankedListMeta: RankedListMeta[]): number[];
|
|
785
909
|
/**
|
|
786
910
|
* Hybrid search: BM25 + vector + query expansion + RRF + chunked reranking.
|
|
787
911
|
*
|
|
@@ -836,6 +960,7 @@ export interface StructuredSearchOptions {
|
|
|
836
960
|
intent?: string;
|
|
837
961
|
/** Skip LLM reranking, use only RRF scores */
|
|
838
962
|
skipRerank?: boolean;
|
|
963
|
+
chunkStrategy?: ChunkStrategy;
|
|
839
964
|
hooks?: SearchHooks;
|
|
840
965
|
}
|
|
841
966
|
/**
|