@wei840222/qmd 2026.8.23

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +1373 -0
  2. package/LICENSE +45 -0
  3. package/README.md +1439 -0
  4. package/THIRD_PARTY_NOTICES.md +31 -0
  5. package/bin/qmd +192 -0
  6. package/dist/ast.d.ts +65 -0
  7. package/dist/ast.js +334 -0
  8. package/dist/bench/bench.d.ts +35 -0
  9. package/dist/bench/bench.js +338 -0
  10. package/dist/bench/cjk-baseline.d.ts +36 -0
  11. package/dist/bench/cjk-baseline.js +111 -0
  12. package/dist/bench/fixture.d.ts +2 -0
  13. package/dist/bench/fixture.js +84 -0
  14. package/dist/bench/score.d.ts +38 -0
  15. package/dist/bench/score.js +107 -0
  16. package/dist/bench/types.d.ts +110 -0
  17. package/dist/bench/types.js +8 -0
  18. package/dist/cli/build-info.json +4 -0
  19. package/dist/cli/embed-lock.d.ts +24 -0
  20. package/dist/cli/embed-lock.js +94 -0
  21. package/dist/cli/embedding-owner.d.ts +10 -0
  22. package/dist/cli/embedding-owner.js +20 -0
  23. package/dist/cli/formatter.d.ts +120 -0
  24. package/dist/cli/formatter.js +355 -0
  25. package/dist/cli/mcp-pid.d.ts +25 -0
  26. package/dist/cli/mcp-pid.js +86 -0
  27. package/dist/cli/qmd.d.ts +72 -0
  28. package/dist/cli/qmd.js +4806 -0
  29. package/dist/cli/version.d.ts +42 -0
  30. package/dist/cli/version.js +80 -0
  31. package/dist/collections.d.ts +200 -0
  32. package/dist/collections.js +433 -0
  33. package/dist/db.d.ts +65 -0
  34. package/dist/db.js +143 -0
  35. package/dist/diagnostics.d.ts +62 -0
  36. package/dist/diagnostics.js +260 -0
  37. package/dist/embedding/config.d.ts +52 -0
  38. package/dist/embedding/config.js +229 -0
  39. package/dist/embedding/identity.d.ts +58 -0
  40. package/dist/embedding/identity.js +321 -0
  41. package/dist/embedding/local-identity.d.ts +1 -0
  42. package/dist/embedding/local-identity.js +15 -0
  43. package/dist/embedding/local.d.ts +34 -0
  44. package/dist/embedding/local.js +290 -0
  45. package/dist/embedding/openai.d.ts +79 -0
  46. package/dist/embedding/openai.js +477 -0
  47. package/dist/embedding/owner.d.ts +13 -0
  48. package/dist/embedding/owner.js +36 -0
  49. package/dist/embedding/provider.d.ts +68 -0
  50. package/dist/embedding/provider.js +16 -0
  51. package/dist/embedding/remote-chunking.d.ts +22 -0
  52. package/dist/embedding/remote-chunking.js +83 -0
  53. package/dist/embedding/remote-embedding.d.ts +15 -0
  54. package/dist/embedding/remote-embedding.js +77 -0
  55. package/dist/hybrid-llm.d.ts +18 -0
  56. package/dist/hybrid-llm.js +53 -0
  57. package/dist/index.d.ts +244 -0
  58. package/dist/index.js +418 -0
  59. package/dist/llm.d.ts +566 -0
  60. package/dist/llm.js +1847 -0
  61. package/dist/maintenance.d.ts +33 -0
  62. package/dist/maintenance.js +52 -0
  63. package/dist/mcp/origin-guard.d.ts +67 -0
  64. package/dist/mcp/origin-guard.js +137 -0
  65. package/dist/mcp/server.d.ts +116 -0
  66. package/dist/mcp/server.js +919 -0
  67. package/dist/paths.d.ts +1 -0
  68. package/dist/paths.js +4 -0
  69. package/dist/remote-llm.d.ts +52 -0
  70. package/dist/remote-llm.js +464 -0
  71. package/dist/search/cjk-analyzer.d.ts +33 -0
  72. package/dist/search/cjk-analyzer.js +158 -0
  73. package/dist/search/cjk-index.d.ts +104 -0
  74. package/dist/search/cjk-index.js +1031 -0
  75. package/dist/search/jieba-loader.d.ts +23 -0
  76. package/dist/search/jieba-loader.js +79 -0
  77. package/dist/search/query-expansion.d.ts +23 -0
  78. package/dist/search/query-expansion.js +43 -0
  79. package/dist/search/zh-dict.txt +624013 -0
  80. package/dist/store.d.ts +1218 -0
  81. package/dist/store.js +6076 -0
  82. package/dist/trust.d.ts +152 -0
  83. package/dist/trust.js +249 -0
  84. package/package.json +139 -0
  85. package/scripts/build.mjs +83 -0
  86. package/scripts/check-package-grammars.mjs +29 -0
  87. package/scripts/package-smoke.mjs +205 -0
  88. package/scripts/sync-zh-dict.mjs +187 -0
  89. package/scripts/test-all.mjs +45 -0
  90. package/skills/qmd/SKILL.md +324 -0
  91. package/skills/qmd/references/mcp-setup.md +119 -0
  92. package/skills/release/SKILL.md +141 -0
  93. package/skills/release/scripts/install-hooks.sh +38 -0
  94. package/skills/release/scripts/release-context.sh +129 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * formatter.ts - Output formatting utilities for QMD
3
+ *
4
+ * Provides methods to format search results and documents into various output formats:
5
+ * JSON, CSV, XML, Markdown, files list, and CLI (colored terminal output).
6
+ */
7
+ import { extractSnippet } from "../store.js";
8
+ // =============================================================================
9
+ // Helper Functions
10
+ // =============================================================================
11
+ /**
12
+ * Add line numbers to text content.
13
+ * Each line becomes: "{lineNum}: {content}"
14
+ * @param text The text to add line numbers to
15
+ * @param startLine Optional starting line number (default: 1)
16
+ */
17
+ export function addLineNumbers(text, startLine = 1) {
18
+ const lines = text.split('\n');
19
+ return lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
20
+ }
21
+ /**
22
+ * Extract short docid from a full hash (first 6 characters).
23
+ */
24
+ export function getDocid(hash) {
25
+ return hash.slice(0, 6);
26
+ }
27
+ // =============================================================================
28
+ // Escape Helpers
29
+ // =============================================================================
30
+ export function escapeCSV(value) {
31
+ if (value === null || value === undefined)
32
+ return "";
33
+ const str = String(value);
34
+ if (str.includes(",") || str.includes('"') || str.includes("\n")) {
35
+ return `"${str.replace(/"/g, '""')}"`;
36
+ }
37
+ return str;
38
+ }
39
+ export function escapeXml(str) {
40
+ return str
41
+ .replace(/&/g, "&")
42
+ .replace(/</g, "&lt;")
43
+ .replace(/>/g, "&gt;")
44
+ .replace(/"/g, "&quot;")
45
+ .replace(/'/g, "&apos;");
46
+ }
47
+ // =============================================================================
48
+ // Search Results Formatters
49
+ // =============================================================================
50
+ /**
51
+ * Format search results as JSON
52
+ */
53
+ export function searchResultsToJson(results, opts = {}) {
54
+ const query = opts.query || "";
55
+ const output = results.map(row => {
56
+ const bodyStr = row.body || "";
57
+ const snippetInfo = bodyStr
58
+ ? extractSnippet(bodyStr, query, 300, row.chunkPos, undefined, opts.intent)
59
+ : undefined;
60
+ let body = opts.full ? bodyStr : undefined;
61
+ let snippet = !opts.full ? snippetInfo?.snippet : undefined;
62
+ if (opts.lineNumbers) {
63
+ if (body)
64
+ body = addLineNumbers(body);
65
+ if (snippet)
66
+ snippet = addLineNumbers(snippet);
67
+ }
68
+ return {
69
+ docid: `#${row.docid}`,
70
+ score: Math.round(row.score * 100) / 100,
71
+ file: row.displayPath,
72
+ ...(snippetInfo && { line: snippetInfo.line }),
73
+ title: row.title,
74
+ ...(row.context && { context: row.context }),
75
+ ...(body && { body }),
76
+ ...(snippet && { snippet }),
77
+ };
78
+ });
79
+ return JSON.stringify(output, null, 2);
80
+ }
81
+ /**
82
+ * Format search results as CSV
83
+ */
84
+ export function searchResultsToCsv(results, opts = {}) {
85
+ const query = opts.query || "";
86
+ const header = "docid,score,file,title,context,line,snippet";
87
+ const rows = results.map(row => {
88
+ const bodyStr = row.body || "";
89
+ const { line, snippet } = extractSnippet(bodyStr, query, 500, row.chunkPos, undefined, opts.intent);
90
+ let content = opts.full ? bodyStr : snippet;
91
+ if (opts.lineNumbers && content) {
92
+ content = addLineNumbers(content);
93
+ }
94
+ return [
95
+ `#${row.docid}`,
96
+ row.score.toFixed(4),
97
+ escapeCSV(row.displayPath),
98
+ escapeCSV(row.title),
99
+ escapeCSV(row.context || ""),
100
+ line,
101
+ escapeCSV(content),
102
+ ].join(",");
103
+ });
104
+ return [header, ...rows].join("\n");
105
+ }
106
+ /**
107
+ * Format search results as simple files list (docid,score,filepath,context)
108
+ */
109
+ export function searchResultsToFiles(results) {
110
+ return results.map(row => {
111
+ const ctx = row.context ? `,"${row.context.replace(/"/g, '""')}"` : "";
112
+ return `#${row.docid},${row.score.toFixed(2)},${row.displayPath}${ctx}`;
113
+ }).join("\n");
114
+ }
115
+ /**
116
+ * Format search results as Markdown
117
+ */
118
+ export function searchResultsToMarkdown(results, opts = {}) {
119
+ const query = opts.query || "";
120
+ return results.map(row => {
121
+ const heading = row.title || row.displayPath;
122
+ const bodyStr = row.body || "";
123
+ let content;
124
+ if (opts.full) {
125
+ content = bodyStr;
126
+ }
127
+ else {
128
+ content = extractSnippet(bodyStr, query, 500, row.chunkPos, undefined, opts.intent).snippet;
129
+ }
130
+ if (opts.lineNumbers) {
131
+ content = addLineNumbers(content);
132
+ }
133
+ const fileLine = `**file:** \`${row.displayPath}\`\n`;
134
+ const contextLine = row.context ? `**context:** ${row.context}\n` : "";
135
+ return `---\n# ${heading}\n\n${fileLine}**docid:** \`#${row.docid}\`\n${contextLine}\n${content}\n`;
136
+ }).join("\n");
137
+ }
138
+ /**
139
+ * Format search results as XML
140
+ */
141
+ export function searchResultsToXml(results, opts = {}) {
142
+ const query = opts.query || "";
143
+ const items = results.map(row => {
144
+ const titleAttr = row.title ? ` title="${escapeXml(row.title)}"` : "";
145
+ const bodyStr = row.body || "";
146
+ let content = opts.full ? bodyStr : extractSnippet(bodyStr, query, 500, row.chunkPos, undefined, opts.intent).snippet;
147
+ if (opts.lineNumbers) {
148
+ content = addLineNumbers(content);
149
+ }
150
+ const contextAttr = row.context ? ` context="${escapeXml(row.context)}"` : "";
151
+ return `<file docid="#${row.docid}" name="${escapeXml(row.displayPath)}"${titleAttr}${contextAttr}>\n${escapeXml(content)}\n</file>`;
152
+ });
153
+ return items.join("\n\n");
154
+ }
155
+ /**
156
+ * Format search results for MCP (simpler CSV format with pre-extracted snippets)
157
+ */
158
+ export function searchResultsToMcpCsv(results) {
159
+ const header = "docid,file,title,score,context,snippet";
160
+ const rows = results.map(r => [`#${r.docid}`, r.file, r.title, r.score, r.context || "", r.snippet].map(escapeCSV).join(","));
161
+ return [header, ...rows].join("\n");
162
+ }
163
+ // =============================================================================
164
+ // Document Formatters (for multi-get using MultiGetFile from store)
165
+ // =============================================================================
166
+ /**
167
+ * Format documents as JSON
168
+ */
169
+ export function documentsToJson(results) {
170
+ const output = results.map(r => ({
171
+ file: r.displayPath,
172
+ title: r.title,
173
+ ...(r.context && { context: r.context }),
174
+ ...(r.skipped ? { skipped: true, reason: r.skipReason } : { body: r.body }),
175
+ }));
176
+ return JSON.stringify(output, null, 2);
177
+ }
178
+ /**
179
+ * Format documents as CSV
180
+ */
181
+ export function documentsToCsv(results) {
182
+ const header = "file,title,context,skipped,body";
183
+ const rows = results.map(r => [
184
+ r.displayPath,
185
+ r.title,
186
+ r.context || "",
187
+ r.skipped ? "true" : "false",
188
+ r.skipped ? (r.skipReason || "") : r.body
189
+ ].map(escapeCSV).join(","));
190
+ return [header, ...rows].join("\n");
191
+ }
192
+ /**
193
+ * Format documents as files list
194
+ */
195
+ export function documentsToFiles(results) {
196
+ return results.map(r => {
197
+ const ctx = r.context ? `,"${r.context.replace(/"/g, '""')}"` : "";
198
+ const status = r.skipped ? ",[SKIPPED]" : "";
199
+ return `${r.displayPath}${ctx}${status}`;
200
+ }).join("\n");
201
+ }
202
+ /**
203
+ * Format documents as Markdown
204
+ */
205
+ export function documentsToMarkdown(results) {
206
+ return results.map(r => {
207
+ let md = `## ${r.displayPath}\n\n`;
208
+ if (r.title && r.title !== r.displayPath)
209
+ md += `**Title:** ${r.title}\n\n`;
210
+ if (r.context)
211
+ md += `**Context:** ${r.context}\n\n`;
212
+ if (r.skipped) {
213
+ md += `> ${r.skipReason}\n`;
214
+ }
215
+ else {
216
+ md += "```\n" + r.body + "\n```\n";
217
+ }
218
+ return md;
219
+ }).join("\n");
220
+ }
221
+ /**
222
+ * Format documents as XML
223
+ */
224
+ export function documentsToXml(results) {
225
+ const items = results.map(r => {
226
+ let xml = " <document>\n";
227
+ xml += ` <file>${escapeXml(r.displayPath)}</file>\n`;
228
+ xml += ` <title>${escapeXml(r.title)}</title>\n`;
229
+ if (r.context)
230
+ xml += ` <context>${escapeXml(r.context)}</context>\n`;
231
+ if (r.skipped) {
232
+ xml += ` <skipped>true</skipped>\n`;
233
+ xml += ` <reason>${escapeXml(r.skipReason || "")}</reason>\n`;
234
+ }
235
+ else {
236
+ xml += ` <body>${escapeXml(r.body)}</body>\n`;
237
+ }
238
+ xml += " </document>";
239
+ return xml;
240
+ });
241
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<documents>\n${items.join("\n")}\n</documents>`;
242
+ }
243
+ // =============================================================================
244
+ // Single Document Formatters
245
+ // =============================================================================
246
+ /**
247
+ * Format a single DocumentResult as JSON
248
+ */
249
+ export function documentToJson(doc) {
250
+ return JSON.stringify({
251
+ file: doc.displayPath,
252
+ title: doc.title,
253
+ ...(doc.context && { context: doc.context }),
254
+ hash: doc.hash,
255
+ modifiedAt: doc.modifiedAt,
256
+ bodyLength: doc.bodyLength,
257
+ ...(doc.body !== undefined && { body: doc.body }),
258
+ }, null, 2);
259
+ }
260
+ /**
261
+ * Format a single DocumentResult as Markdown
262
+ */
263
+ export function documentToMarkdown(doc) {
264
+ let md = `# ${doc.title || doc.displayPath}\n\n`;
265
+ if (doc.context)
266
+ md += `**Context:** ${doc.context}\n\n`;
267
+ md += `**File:** ${doc.displayPath}\n`;
268
+ md += `**Modified:** ${doc.modifiedAt}\n\n`;
269
+ if (doc.body !== undefined) {
270
+ md += "---\n\n" + doc.body + "\n";
271
+ }
272
+ return md;
273
+ }
274
+ /**
275
+ * Format a single DocumentResult as XML
276
+ */
277
+ export function documentToXml(doc) {
278
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<document>\n`;
279
+ xml += ` <file>${escapeXml(doc.displayPath)}</file>\n`;
280
+ xml += ` <title>${escapeXml(doc.title)}</title>\n`;
281
+ if (doc.context)
282
+ xml += ` <context>${escapeXml(doc.context)}</context>\n`;
283
+ xml += ` <hash>${escapeXml(doc.hash)}</hash>\n`;
284
+ xml += ` <modifiedAt>${escapeXml(doc.modifiedAt)}</modifiedAt>\n`;
285
+ xml += ` <bodyLength>${doc.bodyLength}</bodyLength>\n`;
286
+ if (doc.body !== undefined) {
287
+ xml += ` <body>${escapeXml(doc.body)}</body>\n`;
288
+ }
289
+ xml += `</document>`;
290
+ return xml;
291
+ }
292
+ /**
293
+ * Format a single document to the specified format
294
+ */
295
+ export function formatDocument(doc, format) {
296
+ switch (format) {
297
+ case "json":
298
+ return documentToJson(doc);
299
+ case "md":
300
+ return documentToMarkdown(doc);
301
+ case "xml":
302
+ return documentToXml(doc);
303
+ default:
304
+ // Default to markdown for CLI and other formats
305
+ return documentToMarkdown(doc);
306
+ }
307
+ }
308
+ // =============================================================================
309
+ // Universal Format Function
310
+ // =============================================================================
311
+ /**
312
+ * Format search results to the specified output format
313
+ */
314
+ export function formatSearchResults(results, format, opts = {}) {
315
+ switch (format) {
316
+ case "json":
317
+ return searchResultsToJson(results, opts);
318
+ case "csv":
319
+ return searchResultsToCsv(results, opts);
320
+ case "files":
321
+ return searchResultsToFiles(results);
322
+ case "md":
323
+ return searchResultsToMarkdown(results, opts);
324
+ case "xml":
325
+ return searchResultsToXml(results, opts);
326
+ case "cli":
327
+ // CLI format should be handled separately with colors
328
+ // Return a simple text version as fallback
329
+ return searchResultsToMarkdown(results, opts);
330
+ default:
331
+ return searchResultsToJson(results, opts);
332
+ }
333
+ }
334
+ /**
335
+ * Format documents to the specified output format
336
+ */
337
+ export function formatDocuments(results, format) {
338
+ switch (format) {
339
+ case "json":
340
+ return documentsToJson(results);
341
+ case "csv":
342
+ return documentsToCsv(results);
343
+ case "files":
344
+ return documentsToFiles(results);
345
+ case "md":
346
+ return documentsToMarkdown(results);
347
+ case "xml":
348
+ return documentsToXml(results);
349
+ case "cli":
350
+ // CLI format should be handled separately with colors
351
+ return documentsToMarkdown(results);
352
+ default:
353
+ return documentsToJson(results);
354
+ }
355
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * MCP daemon pidfile identity helpers.
3
+ *
4
+ * Pidfiles alone are unsafe after PID reuse (e.g. post-reboot). Callers must
5
+ * confirm a recorded PID still belongs to a qmd process before signalling it
6
+ * or treating it as "already running".
7
+ */
8
+ /**
9
+ * Pid/log filenames for the MCP HTTP daemon.
10
+ * The default index keeps `mcp.pid` / `mcp.log` for compatibility; named
11
+ * indexes are scoped so a named daemon can run alongside the default (#772).
12
+ */
13
+ export declare function mcpDaemonStateFiles(indexName?: string): {
14
+ pidFile: string;
15
+ logFile: string;
16
+ };
17
+ /** True if a process command line looks like a qmd CLI invocation. */
18
+ export declare function looksLikeQmdMcpCommand(cmdline: string): boolean;
19
+ /** Read process cmdline (Linux /proc preferred; ps fallback for macOS). */
20
+ export declare function readProcessCmdline(pid: number): string | null;
21
+ /**
22
+ * Returns true only if `pid` is alive AND its command line looks like qmd.
23
+ * If cmdline cannot be read or does not match, returns false (treat as stale).
24
+ */
25
+ export declare function isQmdMcpPid(pid: number): boolean;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * MCP daemon pidfile identity helpers.
3
+ *
4
+ * Pidfiles alone are unsafe after PID reuse (e.g. post-reboot). Callers must
5
+ * confirm a recorded PID still belongs to a qmd process before signalling it
6
+ * or treating it as "already running".
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { execFileSync } from "node:child_process";
10
+ /**
11
+ * Pid/log filenames for the MCP HTTP daemon.
12
+ * The default index keeps `mcp.pid` / `mcp.log` for compatibility; named
13
+ * indexes are scoped so a named daemon can run alongside the default (#772).
14
+ */
15
+ export function mcpDaemonStateFiles(indexName = "index") {
16
+ const suffix = !indexName || indexName === "index" ? "" : `-${indexName}`;
17
+ return {
18
+ pidFile: `mcp${suffix}.pid`,
19
+ logFile: `mcp${suffix}.log`,
20
+ };
21
+ }
22
+ /** True if a process command line looks like a qmd CLI invocation. */
23
+ export function looksLikeQmdMcpCommand(cmdline) {
24
+ const s = cmdline.trim();
25
+ if (!s)
26
+ return false;
27
+ // Match bare `qmd`, `qmd.ts`/`qmd.js`, or a path ending in /qmd(.ts|.js)
28
+ return /(?:^|[\s/\\])qmd(?:\.(?:ts|js))?(?:[\s]|$)/i.test(s);
29
+ }
30
+ /** Read process cmdline (Linux /proc preferred; ps fallback for macOS). */
31
+ export function readProcessCmdline(pid) {
32
+ if (!Number.isInteger(pid) || pid <= 0)
33
+ return null;
34
+ const procPath = `/proc/${pid}/cmdline`;
35
+ if (existsSync(procPath)) {
36
+ try {
37
+ const raw = readFileSync(procPath, "utf-8");
38
+ const cmdline = raw.replace(/\0/g, " ").trim();
39
+ if (cmdline)
40
+ return cmdline;
41
+ }
42
+ catch {
43
+ // fall through to ps
44
+ }
45
+ }
46
+ try {
47
+ let cmdline = "";
48
+ try {
49
+ cmdline = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
50
+ encoding: "utf-8",
51
+ timeout: 2000,
52
+ stdio: ["ignore", "pipe", "ignore"],
53
+ });
54
+ }
55
+ catch {
56
+ cmdline = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
57
+ encoding: "utf-8",
58
+ timeout: 2000,
59
+ stdio: ["ignore", "pipe", "ignore"],
60
+ });
61
+ }
62
+ const trimmed = cmdline.trim();
63
+ return trimmed || null;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ /**
70
+ * Returns true only if `pid` is alive AND its command line looks like qmd.
71
+ * If cmdline cannot be read or does not match, returns false (treat as stale).
72
+ */
73
+ export function isQmdMcpPid(pid) {
74
+ if (!Number.isInteger(pid) || pid <= 0)
75
+ return false;
76
+ try {
77
+ process.kill(pid, 0);
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ const cmdline = readProcessCmdline(pid);
83
+ if (!cmdline)
84
+ return false;
85
+ return looksLikeQmdMcpCommand(cmdline);
86
+ }
@@ -0,0 +1,72 @@
1
+ import { type ExpandedQuery } from "../store.js";
2
+ import type { EmbeddingProviderOwner } from "../embedding/provider.js";
3
+ import { type OutputFormat } from "./formatter.js";
4
+ export interface CliResourceCloserOptions {
5
+ getOwner(): EmbeddingProviderOwner | null;
6
+ clearOwner(): void;
7
+ disposeFallback(): Promise<void>;
8
+ closeStore(): void;
9
+ }
10
+ export declare function createCliResourceCloser(options: CliResourceCloserOptions): () => Promise<void>;
11
+ type CliLifecycleWritable = {
12
+ write(chunk: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
13
+ };
14
+ type FinishSuccessfulCliCommandOptions = {
15
+ command: string;
16
+ format?: OutputFormat;
17
+ cleanup?: () => Promise<void>;
18
+ exit?: (code: number) => void;
19
+ stdout?: CliLifecycleWritable;
20
+ stderr?: CliLifecycleWritable;
21
+ };
22
+ /**
23
+ * Finish a successful CLI command after output has been flushed.
24
+ *
25
+ * We deliberately do NOT call `process.exit(0)`. `process.exit()` skips
26
+ * Node's `beforeExit` event, and node-llama-cpp registers a `beforeExit` hook
27
+ * that auto-disposes its native handles. On darwin, without that hook firing,
28
+ * libggml-metal's static `ggml_metal_device` destructor asserts on a
29
+ * non-empty residency-set collection during `__cxa_finalize_ranges` and
30
+ * dumps a multi-kB backtrace (upstream ggml-org/llama.cpp#22593, fix open as
31
+ * PR #22595). Empirically, even with explicit `disposeDefaultLlamaCpp()` the
32
+ * direct `process.exit(0)` path still trips the assertion — letting the
33
+ * event loop drain naturally is what actually clears the rsets.
34
+ *
35
+ * So: set `process.exitCode = 0` and return. The main module finishes, the
36
+ * event loop drains, `beforeExit` fires, native resources tear down in
37
+ * order, and the process exits cleanly. The `GGML_METAL_NO_RESIDENCY=1` env
38
+ * var that `bin/qmd` exports is a defense-in-depth safety net for paths
39
+ * that still call `process.exit()` after loading the native binding
40
+ * (signal handlers, error paths, `bun test`).
41
+ *
42
+ * If the caller passes an explicit `exit` for testability, we honor it —
43
+ * the lifecycle tests verify the legacy flush → cleanup → exit ordering.
44
+ * Production callers must not pass `exit`.
45
+ */
46
+ export declare function finishSuccessfulCliCommand(options: FinishSuccessfulCliCommandOptions): Promise<void>;
47
+ export declare function resolveEmbedModelForCli(): string;
48
+ export declare function resolveGenerateModelForCli(): string;
49
+ export declare function resolveRerankModelForCli(): string;
50
+ export declare function buildEditorUri(template: string, absolutePath: string, line: number, col: number): string;
51
+ export declare function termLink(text: string, url: string, isTTY?: boolean): string;
52
+ /**
53
+ * Parse structured search query syntax.
54
+ * Lines starting with lex:, vec:, or hyde: are routed directly.
55
+ * Plain lines without prefix go through query expansion.
56
+ *
57
+ * Returns null if this is a plain query (single line, no prefix).
58
+ * Returns ExpandedQuery[] if structured syntax detected.
59
+ * Throws if multiple plain lines (ambiguous).
60
+ *
61
+ * Examples:
62
+ * "CAP theorem" -> null (plain query, use expansion)
63
+ * "lex: CAP theorem" -> null (explicit shared-policy skip)
64
+ * "lex: CAP\nvec: consistency" -> [{ type: 'lex', ... }, { type: 'vec', ... }]
65
+ * "CAP\nconsistency" -> throws (multiple plain lines)
66
+ */
67
+ interface ParsedStructuredQuery {
68
+ searches: ExpandedQuery[];
69
+ intent?: string;
70
+ }
71
+ export declare function parseStructuredQuery(query: string): ParsedStructuredQuery | null;
72
+ export {};