@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.
- package/CHANGELOG.md +1373 -0
- package/LICENSE +45 -0
- package/README.md +1439 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/bin/qmd +192 -0
- package/dist/ast.d.ts +65 -0
- package/dist/ast.js +334 -0
- package/dist/bench/bench.d.ts +35 -0
- package/dist/bench/bench.js +338 -0
- package/dist/bench/cjk-baseline.d.ts +36 -0
- package/dist/bench/cjk-baseline.js +111 -0
- package/dist/bench/fixture.d.ts +2 -0
- package/dist/bench/fixture.js +84 -0
- package/dist/bench/score.d.ts +38 -0
- package/dist/bench/score.js +107 -0
- package/dist/bench/types.d.ts +110 -0
- package/dist/bench/types.js +8 -0
- package/dist/cli/build-info.json +4 -0
- package/dist/cli/embed-lock.d.ts +24 -0
- package/dist/cli/embed-lock.js +94 -0
- package/dist/cli/embedding-owner.d.ts +10 -0
- package/dist/cli/embedding-owner.js +20 -0
- package/dist/cli/formatter.d.ts +120 -0
- package/dist/cli/formatter.js +355 -0
- package/dist/cli/mcp-pid.d.ts +25 -0
- package/dist/cli/mcp-pid.js +86 -0
- package/dist/cli/qmd.d.ts +72 -0
- package/dist/cli/qmd.js +4806 -0
- package/dist/cli/version.d.ts +42 -0
- package/dist/cli/version.js +80 -0
- package/dist/collections.d.ts +200 -0
- package/dist/collections.js +433 -0
- package/dist/db.d.ts +65 -0
- package/dist/db.js +143 -0
- package/dist/diagnostics.d.ts +62 -0
- package/dist/diagnostics.js +260 -0
- package/dist/embedding/config.d.ts +52 -0
- package/dist/embedding/config.js +229 -0
- package/dist/embedding/identity.d.ts +58 -0
- package/dist/embedding/identity.js +321 -0
- package/dist/embedding/local-identity.d.ts +1 -0
- package/dist/embedding/local-identity.js +15 -0
- package/dist/embedding/local.d.ts +34 -0
- package/dist/embedding/local.js +290 -0
- package/dist/embedding/openai.d.ts +79 -0
- package/dist/embedding/openai.js +477 -0
- package/dist/embedding/owner.d.ts +13 -0
- package/dist/embedding/owner.js +36 -0
- package/dist/embedding/provider.d.ts +68 -0
- package/dist/embedding/provider.js +16 -0
- package/dist/embedding/remote-chunking.d.ts +22 -0
- package/dist/embedding/remote-chunking.js +83 -0
- package/dist/embedding/remote-embedding.d.ts +15 -0
- package/dist/embedding/remote-embedding.js +77 -0
- package/dist/hybrid-llm.d.ts +18 -0
- package/dist/hybrid-llm.js +53 -0
- package/dist/index.d.ts +244 -0
- package/dist/index.js +418 -0
- package/dist/llm.d.ts +566 -0
- package/dist/llm.js +1847 -0
- package/dist/maintenance.d.ts +33 -0
- package/dist/maintenance.js +52 -0
- package/dist/mcp/origin-guard.d.ts +67 -0
- package/dist/mcp/origin-guard.js +137 -0
- package/dist/mcp/server.d.ts +116 -0
- package/dist/mcp/server.js +919 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +4 -0
- package/dist/remote-llm.d.ts +52 -0
- package/dist/remote-llm.js +464 -0
- package/dist/search/cjk-analyzer.d.ts +33 -0
- package/dist/search/cjk-analyzer.js +158 -0
- package/dist/search/cjk-index.d.ts +104 -0
- package/dist/search/cjk-index.js +1031 -0
- package/dist/search/jieba-loader.d.ts +23 -0
- package/dist/search/jieba-loader.js +79 -0
- package/dist/search/query-expansion.d.ts +23 -0
- package/dist/search/query-expansion.js +43 -0
- package/dist/search/zh-dict.txt +624013 -0
- package/dist/store.d.ts +1218 -0
- package/dist/store.js +6076 -0
- package/dist/trust.d.ts +152 -0
- package/dist/trust.js +249 -0
- package/package.json +139 -0
- package/scripts/build.mjs +83 -0
- package/scripts/check-package-grammars.mjs +29 -0
- package/scripts/package-smoke.mjs +205 -0
- package/scripts/sync-zh-dict.mjs +187 -0
- package/scripts/test-all.mjs +45 -0
- package/skills/qmd/SKILL.md +324 -0
- package/skills/qmd/references/mcp-setup.md +119 -0
- package/skills/release/SKILL.md +141 -0
- package/skills/release/scripts/install-hooks.sh +38 -0
- package/skills/release/scripts/release-context.sh +129 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoring functions for the QMD benchmark harness.
|
|
3
|
+
*
|
|
4
|
+
* Computes precision@k, recall, MRR, and F1 for search results
|
|
5
|
+
* against ground-truth expected files.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a file path for comparison.
|
|
9
|
+
* Strips qmd:// prefix, lowercases, removes leading/trailing slashes.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizePath(p) {
|
|
12
|
+
if (p.startsWith("qmd://")) {
|
|
13
|
+
// qmd://collection/docs/readme.md → docs/readme.md
|
|
14
|
+
const withoutScheme = p.slice("qmd://".length);
|
|
15
|
+
const slashIdx = withoutScheme.indexOf("/");
|
|
16
|
+
p = slashIdx >= 0 ? withoutScheme.slice(slashIdx + 1) : withoutScheme;
|
|
17
|
+
}
|
|
18
|
+
return p.toLowerCase().replace(/^\/+|\/+$/g, "");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if two paths refer to the same file.
|
|
22
|
+
* Handles different path formats by comparing normalized suffixes.
|
|
23
|
+
*/
|
|
24
|
+
export function pathsMatch(result, expected) {
|
|
25
|
+
const nr = normalizePath(result);
|
|
26
|
+
const ne = normalizePath(expected);
|
|
27
|
+
if (nr === ne)
|
|
28
|
+
return true;
|
|
29
|
+
if (nr.endsWith(`/${ne}`) || ne.endsWith(`/${nr}`))
|
|
30
|
+
return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export function percentile(values, quantile) {
|
|
34
|
+
if (!Number.isFinite(quantile) || quantile <= 0 || quantile > 1) {
|
|
35
|
+
throw new RangeError("quantile must be greater than 0 and at most 1");
|
|
36
|
+
}
|
|
37
|
+
if (values.length === 0)
|
|
38
|
+
return 0;
|
|
39
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
40
|
+
return sorted[Math.ceil(quantile * sorted.length) - 1];
|
|
41
|
+
}
|
|
42
|
+
function hitsWithin(resultFiles, expectedFiles, k) {
|
|
43
|
+
const topKResults = resultFiles.slice(0, k);
|
|
44
|
+
let hits = 0;
|
|
45
|
+
for (const expected of expectedFiles) {
|
|
46
|
+
if (topKResults.some(r => pathsMatch(r, expected))) {
|
|
47
|
+
hits++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return hits;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Score a set of search results against expected files.
|
|
54
|
+
*/
|
|
55
|
+
export function scoreResults(resultFiles, expectedFiles, topK, mustNotMatchFiles = []) {
|
|
56
|
+
// Count hits in top-k
|
|
57
|
+
const hitsAtK = hitsWithin(resultFiles, expectedFiles, topK);
|
|
58
|
+
const matchedFiles = [];
|
|
59
|
+
const unmatchedExpectedFiles = [];
|
|
60
|
+
for (const expected of expectedFiles) {
|
|
61
|
+
if (resultFiles.some(r => pathsMatch(r, expected))) {
|
|
62
|
+
matchedFiles.push(expected);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
unmatchedExpectedFiles.push(expected);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// MRR: reciprocal rank of first relevant result
|
|
69
|
+
let mrr = 0;
|
|
70
|
+
let mrrAt10 = 0;
|
|
71
|
+
for (let i = 0; i < resultFiles.length; i++) {
|
|
72
|
+
if (expectedFiles.some(e => pathsMatch(resultFiles[i], e))) {
|
|
73
|
+
mrr = 1 / (i + 1);
|
|
74
|
+
if (i < 10)
|
|
75
|
+
mrrAt10 = mrr;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const top10Results = resultFiles.slice(0, 10);
|
|
80
|
+
const falsePositiveFiles = mustNotMatchFiles.filter(expected => top10Results.some(result => pathsMatch(result, expected)));
|
|
81
|
+
const denominator = Math.min(topK, expectedFiles.length);
|
|
82
|
+
const precision_at_k = denominator > 0 ? hitsAtK / denominator : 0;
|
|
83
|
+
const recall = expectedFiles.length > 0 ? matchedFiles.length / expectedFiles.length : 0;
|
|
84
|
+
const recall_at_1 = expectedFiles.length > 0 ? hitsWithin(resultFiles, expectedFiles, 1) / expectedFiles.length : 0;
|
|
85
|
+
const recall_at_3 = expectedFiles.length > 0 ? hitsWithin(resultFiles, expectedFiles, 3) / expectedFiles.length : 0;
|
|
86
|
+
const recall_at_5 = expectedFiles.length > 0 ? hitsWithin(resultFiles, expectedFiles, 5) / expectedFiles.length : 0;
|
|
87
|
+
const recall_at_10 = expectedFiles.length > 0 ? hitsWithin(resultFiles, expectedFiles, 10) / expectedFiles.length : 0;
|
|
88
|
+
const f1 = precision_at_k + recall > 0
|
|
89
|
+
? 2 * (precision_at_k * recall) / (precision_at_k + recall)
|
|
90
|
+
: 0;
|
|
91
|
+
return {
|
|
92
|
+
precision_at_k,
|
|
93
|
+
recall,
|
|
94
|
+
recall_at_1,
|
|
95
|
+
recall_at_3,
|
|
96
|
+
recall_at_5,
|
|
97
|
+
recall_at_10,
|
|
98
|
+
mrr,
|
|
99
|
+
mrr_at_10: mrrAt10,
|
|
100
|
+
f1,
|
|
101
|
+
hits_at_k: hitsAtK,
|
|
102
|
+
false_positive_count: falsePositiveFiles.length,
|
|
103
|
+
false_positive_files: falsePositiveFiles,
|
|
104
|
+
matched_files: matchedFiles,
|
|
105
|
+
unmatched_expected_files: unmatchedExpectedFiles,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the QMD benchmark harness.
|
|
3
|
+
*
|
|
4
|
+
* A benchmark fixture defines queries with expected results.
|
|
5
|
+
* The harness runs each query through multiple search backends
|
|
6
|
+
* and measures precision, recall, MRR, and latency.
|
|
7
|
+
*/
|
|
8
|
+
export interface BenchmarkQuery {
|
|
9
|
+
/** Unique identifier for the query */
|
|
10
|
+
id: string;
|
|
11
|
+
/** The search query text */
|
|
12
|
+
query: string;
|
|
13
|
+
/** Query difficulty/type for grouping results */
|
|
14
|
+
type: "exact" | "semantic" | "topical" | "cross-domain" | "alias";
|
|
15
|
+
/** Human-readable description of what this tests */
|
|
16
|
+
description: string;
|
|
17
|
+
/** Legacy file paths (relative to collection) that should appear in results */
|
|
18
|
+
expected_files?: string[];
|
|
19
|
+
/** Stable document IDs that should appear in results */
|
|
20
|
+
relevant_doc_ids?: string[];
|
|
21
|
+
/** Stable document IDs that must not appear in the top 10 results */
|
|
22
|
+
must_not_match_doc_ids?: string[];
|
|
23
|
+
/** Scenario labels used to group benchmark results */
|
|
24
|
+
scenario_tags?: string[];
|
|
25
|
+
/** How many of expected_files should appear in top-k results */
|
|
26
|
+
expected_in_top_k: number;
|
|
27
|
+
}
|
|
28
|
+
export interface BenchmarkDocument {
|
|
29
|
+
/** Stable identifier used by query relevance judgements */
|
|
30
|
+
id: string;
|
|
31
|
+
/** File path relative to the benchmark collection */
|
|
32
|
+
file: string;
|
|
33
|
+
}
|
|
34
|
+
export interface BenchmarkFixture {
|
|
35
|
+
/** Description of the benchmark */
|
|
36
|
+
description: string;
|
|
37
|
+
/** Fixture format version */
|
|
38
|
+
version: number;
|
|
39
|
+
/** Optional collection to search within */
|
|
40
|
+
collection?: string;
|
|
41
|
+
/** Stable document catalogue for version 2 relevance judgements */
|
|
42
|
+
documents?: BenchmarkDocument[];
|
|
43
|
+
/** The test queries */
|
|
44
|
+
queries: BenchmarkQuery[];
|
|
45
|
+
}
|
|
46
|
+
export interface ResolvedBenchmarkQuery extends Omit<BenchmarkQuery, "expected_files"> {
|
|
47
|
+
expected_files: string[];
|
|
48
|
+
must_not_match_files: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface BackendResult {
|
|
51
|
+
/** Fraction of top-k results that are relevant */
|
|
52
|
+
precision_at_k: number;
|
|
53
|
+
/** Fraction of expected files found anywhere in results */
|
|
54
|
+
recall: number;
|
|
55
|
+
/** Fraction of expected files found in the first result */
|
|
56
|
+
recall_at_1: number;
|
|
57
|
+
/** Fraction of expected files found in the top 3 results */
|
|
58
|
+
recall_at_3: number;
|
|
59
|
+
/** Fraction of expected files found in the top 5 results */
|
|
60
|
+
recall_at_5: number;
|
|
61
|
+
/** Fraction of expected files found in the top 10 results */
|
|
62
|
+
recall_at_10: number;
|
|
63
|
+
/** Reciprocal rank of first relevant result (1/rank, 0 if not found) */
|
|
64
|
+
mrr: number;
|
|
65
|
+
/** Reciprocal rank of first relevant result within the top 10 */
|
|
66
|
+
mrr_at_10: number;
|
|
67
|
+
/** Harmonic mean of precision_at_k and recall */
|
|
68
|
+
f1: number;
|
|
69
|
+
/** Number of expected files found in top-k */
|
|
70
|
+
hits_at_k: number;
|
|
71
|
+
/** Number of prohibited files found in the top 10 */
|
|
72
|
+
false_positive_count: number;
|
|
73
|
+
/** Prohibited files found in the top 10 */
|
|
74
|
+
false_positive_files: string[];
|
|
75
|
+
/** Total expected files */
|
|
76
|
+
total_expected: number;
|
|
77
|
+
/** Wall-clock latency in milliseconds */
|
|
78
|
+
latency_ms: number;
|
|
79
|
+
/** Top result file paths (for inspection) */
|
|
80
|
+
top_files: string[];
|
|
81
|
+
/** Expected files that were found anywhere in the returned result set */
|
|
82
|
+
matched_files: string[];
|
|
83
|
+
/** Expected files missing from the returned result set */
|
|
84
|
+
unmatched_expected_files: string[];
|
|
85
|
+
}
|
|
86
|
+
export interface QueryResult {
|
|
87
|
+
id: string;
|
|
88
|
+
query: string;
|
|
89
|
+
type: string;
|
|
90
|
+
scenario_tags?: string[];
|
|
91
|
+
backends: Record<string, BackendResult>;
|
|
92
|
+
}
|
|
93
|
+
export interface BenchmarkResult {
|
|
94
|
+
timestamp: string;
|
|
95
|
+
fixture: string;
|
|
96
|
+
results: QueryResult[];
|
|
97
|
+
summary: Record<string, {
|
|
98
|
+
avg_precision: number;
|
|
99
|
+
avg_recall: number;
|
|
100
|
+
avg_recall_at_1: number;
|
|
101
|
+
avg_recall_at_3: number;
|
|
102
|
+
avg_recall_at_5: number;
|
|
103
|
+
avg_recall_at_10: number;
|
|
104
|
+
avg_mrr: number;
|
|
105
|
+
avg_mrr_at_10: number;
|
|
106
|
+
false_positive_count: number;
|
|
107
|
+
avg_f1: number;
|
|
108
|
+
avg_latency_ms: number;
|
|
109
|
+
}>;
|
|
110
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level exclusive lock for `qmd embed`.
|
|
3
|
+
*
|
|
4
|
+
* Concurrent embed runs against the same index can race on vectors_vec
|
|
5
|
+
* (UNIQUE constraint on hash_seq). This lockfile keeps a second process from
|
|
6
|
+
* starting while another embed holds the lock. Stale files left by crashed
|
|
7
|
+
* processes are recovered via PID identity checks (same spirit as mcp-pid.ts).
|
|
8
|
+
*/
|
|
9
|
+
export type EmbedLockHandle = {
|
|
10
|
+
lockPath: string;
|
|
11
|
+
release: () => void;
|
|
12
|
+
};
|
|
13
|
+
/** Lockfile path sibling to the index database. */
|
|
14
|
+
export declare function embedLockPathForDb(dbPath: string): string;
|
|
15
|
+
/** True if `pid` still owns a live embed/qmd process (or is this process). */
|
|
16
|
+
export declare function isLiveEmbedLockHolder(pid: number): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Try to acquire an exclusive embed lock at `lockPath`.
|
|
19
|
+
* Returns a handle with `release()` on success, or `null` if another live
|
|
20
|
+
* qmd process already holds the lock.
|
|
21
|
+
*/
|
|
22
|
+
export declare function tryAcquireEmbedLock(lockPath: string): EmbedLockHandle | null;
|
|
23
|
+
/** User-facing message when a second embed is skipped. */
|
|
24
|
+
export declare const EMBED_LOCK_BUSY_MESSAGE = "Another embed process is already running. Skipping.";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level exclusive lock for `qmd embed`.
|
|
3
|
+
*
|
|
4
|
+
* Concurrent embed runs against the same index can race on vectors_vec
|
|
5
|
+
* (UNIQUE constraint on hash_seq). This lockfile keeps a second process from
|
|
6
|
+
* starting while another embed holds the lock. Stale files left by crashed
|
|
7
|
+
* processes are recovered via PID identity checks (same spirit as mcp-pid.ts).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { isQmdMcpPid } from "./mcp-pid.js";
|
|
12
|
+
/** Lockfile path sibling to the index database. */
|
|
13
|
+
export function embedLockPathForDb(dbPath) {
|
|
14
|
+
return join(dirname(dbPath), ".qmd-embed.lock");
|
|
15
|
+
}
|
|
16
|
+
function readLockPid(lockPath) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = readFileSync(lockPath, "utf-8").trim();
|
|
19
|
+
const pid = parseInt(raw, 10);
|
|
20
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
21
|
+
return null;
|
|
22
|
+
return pid;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** True if `pid` still owns a live embed/qmd process (or is this process). */
|
|
29
|
+
export function isLiveEmbedLockHolder(pid) {
|
|
30
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
31
|
+
return false;
|
|
32
|
+
// Same-process re-check: we obviously still hold our own lock.
|
|
33
|
+
if (pid === process.pid)
|
|
34
|
+
return true;
|
|
35
|
+
return isQmdMcpPid(pid);
|
|
36
|
+
}
|
|
37
|
+
function createOwnedLock(lockPath) {
|
|
38
|
+
writeFileSync(lockPath, `${process.pid}\n`, { flag: "wx" });
|
|
39
|
+
let released = false;
|
|
40
|
+
const release = () => {
|
|
41
|
+
if (released)
|
|
42
|
+
return;
|
|
43
|
+
released = true;
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(lockPath))
|
|
46
|
+
return;
|
|
47
|
+
const written = readLockPid(lockPath);
|
|
48
|
+
if (written === process.pid)
|
|
49
|
+
unlinkSync(lockPath);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// best-effort cleanup
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return { lockPath, release };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Try to acquire an exclusive embed lock at `lockPath`.
|
|
59
|
+
* Returns a handle with `release()` on success, or `null` if another live
|
|
60
|
+
* qmd process already holds the lock.
|
|
61
|
+
*/
|
|
62
|
+
export function tryAcquireEmbedLock(lockPath) {
|
|
63
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
64
|
+
try {
|
|
65
|
+
return createOwnedLock(lockPath);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const code = typeof err === "object" && err !== null && "code" in err
|
|
69
|
+
? err.code
|
|
70
|
+
: undefined;
|
|
71
|
+
if (code !== "EEXIST")
|
|
72
|
+
throw err;
|
|
73
|
+
const holderPid = readLockPid(lockPath);
|
|
74
|
+
if (holderPid !== null && isLiveEmbedLockHolder(holderPid)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// Stale / unreadable / recycled PID — remove and retry once.
|
|
78
|
+
try {
|
|
79
|
+
unlinkSync(lockPath);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Another process may have claimed it; loop and try wx again.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Final attempt lost the race to a live holder (or repeated EEXIST).
|
|
87
|
+
const holderPid = readLockPid(lockPath);
|
|
88
|
+
if (holderPid !== null && isLiveEmbedLockHolder(holderPid)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/** User-facing message when a second embed is skipped. */
|
|
94
|
+
export const EMBED_LOCK_BUSY_MESSAGE = "Another embed process is already running. Skipping.";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LocalEmbeddingProviderOwner } from "../embedding/local.js";
|
|
2
|
+
import type { EmbeddingProvider, EmbeddingProviderOwner } from "../embedding/provider.js";
|
|
3
|
+
type LocalEmbeddingRuntime = ConstructorParameters<typeof LocalEmbeddingProviderOwner>[0];
|
|
4
|
+
export interface CliEmbeddingProviderConfig {
|
|
5
|
+
provider: "local" | "openai";
|
|
6
|
+
model: string;
|
|
7
|
+
dimension: number | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function createCliEmbeddingProviderOwner(config: CliEmbeddingProviderConfig, runtime: LocalEmbeddingRuntime, remoteProvider?: EmbeddingProvider): EmbeddingProviderOwner;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LocalEmbeddingProviderOwner } from "../embedding/local.js";
|
|
2
|
+
import { CompositeEmbeddingProviderOwner } from "../embedding/owner.js";
|
|
3
|
+
import { waitForLLMSessionsToDrain } from "../llm.js";
|
|
4
|
+
export function createCliEmbeddingProviderOwner(config, runtime, remoteProvider) {
|
|
5
|
+
if (config.provider === "local") {
|
|
6
|
+
return new LocalEmbeddingProviderOwner(runtime, {
|
|
7
|
+
model: config.model,
|
|
8
|
+
...(config.dimension == null ? {} : { dimension: config.dimension }),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!remoteProvider?.remote) {
|
|
12
|
+
throw new Error("OpenAI CLI composition requires a remote embedding provider.");
|
|
13
|
+
}
|
|
14
|
+
return new CompositeEmbeddingProviderOwner(remoteProvider, {
|
|
15
|
+
dispose: async () => {
|
|
16
|
+
await waitForLLMSessionsToDrain(runtime);
|
|
17
|
+
await runtime.dispose();
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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 type { SearchResult, MultiGetResult, DocumentResult } from "../store.js";
|
|
8
|
+
export type { SearchResult, MultiGetResult, DocumentResult };
|
|
9
|
+
export type MultiGetFile = {
|
|
10
|
+
filepath: string;
|
|
11
|
+
displayPath: string;
|
|
12
|
+
title: string;
|
|
13
|
+
body: string;
|
|
14
|
+
context?: string | null;
|
|
15
|
+
skipped: false;
|
|
16
|
+
} | {
|
|
17
|
+
filepath: string;
|
|
18
|
+
displayPath: string;
|
|
19
|
+
title: string;
|
|
20
|
+
body: string;
|
|
21
|
+
context?: string | null;
|
|
22
|
+
skipped: true;
|
|
23
|
+
skipReason: string;
|
|
24
|
+
};
|
|
25
|
+
export type OutputFormat = "cli" | "csv" | "md" | "xml" | "files" | "json";
|
|
26
|
+
export type FormatOptions = {
|
|
27
|
+
full?: boolean;
|
|
28
|
+
query?: string;
|
|
29
|
+
useColor?: boolean;
|
|
30
|
+
lineNumbers?: boolean;
|
|
31
|
+
intent?: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Add line numbers to text content.
|
|
35
|
+
* Each line becomes: "{lineNum}: {content}"
|
|
36
|
+
* @param text The text to add line numbers to
|
|
37
|
+
* @param startLine Optional starting line number (default: 1)
|
|
38
|
+
*/
|
|
39
|
+
export declare function addLineNumbers(text: string, startLine?: number): string;
|
|
40
|
+
/**
|
|
41
|
+
* Extract short docid from a full hash (first 6 characters).
|
|
42
|
+
*/
|
|
43
|
+
export declare function getDocid(hash: string): string;
|
|
44
|
+
export declare function escapeCSV(value: string | null | number): string;
|
|
45
|
+
export declare function escapeXml(str: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Format search results as JSON
|
|
48
|
+
*/
|
|
49
|
+
export declare function searchResultsToJson(results: SearchResult[], opts?: FormatOptions): string;
|
|
50
|
+
/**
|
|
51
|
+
* Format search results as CSV
|
|
52
|
+
*/
|
|
53
|
+
export declare function searchResultsToCsv(results: SearchResult[], opts?: FormatOptions): string;
|
|
54
|
+
/**
|
|
55
|
+
* Format search results as simple files list (docid,score,filepath,context)
|
|
56
|
+
*/
|
|
57
|
+
export declare function searchResultsToFiles(results: SearchResult[]): string;
|
|
58
|
+
/**
|
|
59
|
+
* Format search results as Markdown
|
|
60
|
+
*/
|
|
61
|
+
export declare function searchResultsToMarkdown(results: SearchResult[], opts?: FormatOptions): string;
|
|
62
|
+
/**
|
|
63
|
+
* Format search results as XML
|
|
64
|
+
*/
|
|
65
|
+
export declare function searchResultsToXml(results: SearchResult[], opts?: FormatOptions): string;
|
|
66
|
+
/**
|
|
67
|
+
* Format search results for MCP (simpler CSV format with pre-extracted snippets)
|
|
68
|
+
*/
|
|
69
|
+
export declare function searchResultsToMcpCsv(results: {
|
|
70
|
+
docid: string;
|
|
71
|
+
file: string;
|
|
72
|
+
title: string;
|
|
73
|
+
score: number;
|
|
74
|
+
context: string | null;
|
|
75
|
+
snippet: string;
|
|
76
|
+
}[]): string;
|
|
77
|
+
/**
|
|
78
|
+
* Format documents as JSON
|
|
79
|
+
*/
|
|
80
|
+
export declare function documentsToJson(results: MultiGetFile[]): string;
|
|
81
|
+
/**
|
|
82
|
+
* Format documents as CSV
|
|
83
|
+
*/
|
|
84
|
+
export declare function documentsToCsv(results: MultiGetFile[]): string;
|
|
85
|
+
/**
|
|
86
|
+
* Format documents as files list
|
|
87
|
+
*/
|
|
88
|
+
export declare function documentsToFiles(results: MultiGetFile[]): string;
|
|
89
|
+
/**
|
|
90
|
+
* Format documents as Markdown
|
|
91
|
+
*/
|
|
92
|
+
export declare function documentsToMarkdown(results: MultiGetFile[]): string;
|
|
93
|
+
/**
|
|
94
|
+
* Format documents as XML
|
|
95
|
+
*/
|
|
96
|
+
export declare function documentsToXml(results: MultiGetFile[]): string;
|
|
97
|
+
/**
|
|
98
|
+
* Format a single DocumentResult as JSON
|
|
99
|
+
*/
|
|
100
|
+
export declare function documentToJson(doc: DocumentResult): string;
|
|
101
|
+
/**
|
|
102
|
+
* Format a single DocumentResult as Markdown
|
|
103
|
+
*/
|
|
104
|
+
export declare function documentToMarkdown(doc: DocumentResult): string;
|
|
105
|
+
/**
|
|
106
|
+
* Format a single DocumentResult as XML
|
|
107
|
+
*/
|
|
108
|
+
export declare function documentToXml(doc: DocumentResult): string;
|
|
109
|
+
/**
|
|
110
|
+
* Format a single document to the specified format
|
|
111
|
+
*/
|
|
112
|
+
export declare function formatDocument(doc: DocumentResult, format: OutputFormat): string;
|
|
113
|
+
/**
|
|
114
|
+
* Format search results to the specified output format
|
|
115
|
+
*/
|
|
116
|
+
export declare function formatSearchResults(results: SearchResult[], format: OutputFormat, opts?: FormatOptions): string;
|
|
117
|
+
/**
|
|
118
|
+
* Format documents to the specified output format
|
|
119
|
+
*/
|
|
120
|
+
export declare function formatDocuments(results: MultiGetFile[], format: OutputFormat): string;
|