@wei840222/qmd 2026.8.23 → 2026.8.24

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 CHANGED
@@ -2,32 +2,68 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2026.8.23-1] - 2026-08-23
6
+
5
7
  ### Added
6
8
 
7
- - Added Oxlint lint fence.
8
- - Added a bundled Traditional Chinese Jieba dictionary that deterministically
9
- combines the node-rs base dictionary, APCLab's Taiwan dictionary, and
10
- zhtw-mcp technical terms. `pnpm dict:sync` verifies pinned source hashes;
11
- `pnpm dict:sync --update-pins` refreshes the upstream pins.
12
- - Added CJK-aware lexical retrieval with independent character, Jieba word, and
13
- bigram FTS5 channels, versioned rank fusion and explain traces. The bundled
14
- reviewed Traditional Chinese technical dictionary records its pinned source
15
- and MIT attribution in `THIRD_PARTY_NOTICES.md`.
16
- - Added typed local/OpenAI embedding providers, provider-qualified vector
17
- identities, resumable chunk persistence, build leases, remote preflight and
18
- acknowledgement, request-purpose guards, safe OpenAI errors, and additive
19
- diagnostics in CLI, SDK, and MCP. Local embedding remains the default.
20
- - Added `OPENAI_BASE_URL` for routing OpenAI-compatible embedding requests
21
- through a self-hosted proxy while retaining the existing remote-consent flow.
22
- - Added the native `@node-rs/jieba` dependency and package smoke coverage for its
23
- Node/Bun runtime loading and packaged dictionary data.
9
+ - **Pure Node.js + pnpm Standardized Toolchain**: Completely standardized the project on
10
+ Node.js (>=22) and pnpm (`pnpm@11.15.1`). Removed all Bun-specific runtime abstractions,
11
+ lockfile heuristics, and dual-runtime test preloads.
12
+ - **Oxlint & Code Quality Fences**: Added Oxlint static analysis rules (`oxlint`) and anti-slop
13
+ guardrails across the codebase.
14
+ - **CJK 3-Way Lexical Retrieval**: Added CJK-aware lexical retrieval with independent character,
15
+ Jieba word, and bigram FTS5 shadow channels, versioned reciprocal rank fusion (RRF), and
16
+ explain traces.
17
+ - **Bundled Traditional Chinese Jieba Dictionary**: Added a bundled Traditional Chinese dictionary
18
+ deterministically combining `@node-rs/jieba` (`2.0.2`), APCLab's Taiwan dictionary, and
19
+ `sysprog21/zhtw-mcp` technical terms. Pinned sources and hashes are recorded in
20
+ `src/search/zh-dict.sources.json` and `THIRD_PARTY_NOTICES.md`.
21
+ - **Custom User Dictionary Support**: Added `user_dict` configuration supporting custom
22
+ user dictionaries with automatic analyzer fingerprint updates.
23
+ - **Typed Remote OpenAI-Compatible Embedding Provider**: Added typed `OpenAIEmbeddingProvider`,
24
+ provider-qualified vector identities, resumable chunk persistence, build leases, remote
25
+ preflight, chunk byte budgeting, and safe OpenAI error handling.
26
+ - **Remote LLM Query Expansion & Chat Reranking**: Supported OpenAI-compatible
27
+ `/v1/chat/completions` API endpoints for remote query expansion and reranking with XML-structured
28
+ prompts and current timestamp injection.
29
+ - **Vector Table Collection Column**: Added `collection` column to vector indexes with automatic
30
+ schema migration and collection-scoped filtering.
31
+ - **Single-Item Batch Embedding Fallback**: Added single-item fallback to gracefully handle and
32
+ recover from oversized batch chunks during embedding and vector repair.
33
+ - **Agent Skill Query Syntax Reference**: Added `skills/qmd/references/query-syntax.md` detailing
34
+ formal EBNF grammar, search operators, and structured MCP JSON payloads, linked directly from
35
+ `skills/qmd/SKILL.md`.
36
+ - **Automated Jules PR Code Reviews**: Integrated Google Jules PR review GitHub Actions workflow
37
+ (`.github/workflows/jules-pr-review.yml`) with automatic `git diff` fallback for large diffs (>20k lines).
38
+ - **Trivy Vulnerability Scanner & Fast CI Checks**: Integrated Trivy filesystem and dependency security
39
+ scanner, TypeScript type checking (`pnpm run test:types`), and Tree-sitter WASM grammars smoke
40
+ (`pnpm run smoke:package-grammars`) in `.github/workflows/ci.yml`.
24
41
 
25
42
  ### Changed
26
43
 
27
- - Chat-completions reranking now prioritizes explicit query constraints such as
28
- entities, locations, versions, time, and negations. Valid empty rerank results
29
- no longer fall back to neutral scores, and parsed chat rerank output is sorted
30
- by descending relevance score.
44
+ - **Runtime Standardization & Bun Deprecation**: Standardized database layer on `better-sqlite3`
45
+ and `sqlite-vec`. Streamlined `bin/qmd` launcher to a pure Node.js / `tsx` trampoline.
46
+ Replaced all `bun` / `bunx` scripts with `node`, `pnpm`, `tsx`, and `npx`.
47
+ - **Unified `models` Configuration Block**: Replaced disjointed embedding and expand configurations
48
+ with a unified `models:` configuration block supporting `embed`, `generate`, and `rerank`
49
+ endpoints/models.
50
+ - **Generic MCP Client Documentation**: Generalized MCP server configuration instructions across
51
+ `README.md` and `skills/qmd/references/mcp-setup.md` to support all MCP-compatible clients
52
+ (Cursor, Claude Desktop, Zed, OpenClaw, etc.).
53
+ - **Standardized Multi-Holder MIT License**: Updated `LICENSE` to the standard multi-holder copyright
54
+ formatting (Tobi Lutke and Wan, Jiun Wei) for full compatibility with automated license scanners.
55
+ - **Test Suite Semver Suffix Support**: Updated CLI `--version` test in `test/esm-ambiguous-module.test.ts`
56
+ to support Semver prerelease/build suffixes (e.g. `2026.8.23-1`).
57
+ - **Remote LLM Prompt Architecture & Hardening**: Remote query expansion and chat-completions
58
+ reranking now use consistent XML prompt structures, escape untrusted prompt data, preserve
59
+ cross-language technical terms, and discard low-confidence chat rerank scores below `0.1`.
60
+ - **Direct Remote Authorization**: Streamlined remote embedding and execution by removing
61
+ legacy interactive remote consent prompts while enforcing strict identity fingerprinting.
62
+ - **Database Performance Optimizations**: Skipped redundant database write transactions when
63
+ configuration state remains unchanged.
64
+ - **Dependency Upgrades**: Upgraded `@node-rs/jieba` to `2.0.2` and updated package scope to
65
+ `@wei840222/qmd`.
66
+
31
67
  - Remote query expansion and chat-completions reranking now use a consistent
32
68
  Gemini-style XML prompt structure in both system and user messages, separating
33
69
  role, instructions, constraints, output format, context, task, and final
@@ -102,6 +138,18 @@
102
138
  reopen, keep CLI update/include settings synchronized, and use atomic durable
103
139
  YAML replacement.
104
140
 
141
+ ### Removed
142
+
143
+ - Removed Nix flake configurations (`flake.nix`, `flake.lock`) and related CI jobs.
144
+ - Removed `bun.lock`, `src/test-preload.ts`, and `test/launcher-detection.test.sh`.
145
+ - Removed legacy interactive remote embedding consent workflows in favor of direct configuration-based authorization.
146
+
147
+ ### Security
148
+
149
+ - Added Trivy filesystem and dependency security scanning in CI workflows.
150
+ - Bound MCP HTTP server explicitly to `127.0.0.1`.
151
+ - Sanitized remote LLM error handling to prevent leaking credentials or headers.
152
+
105
153
  ### Compatibility
106
154
 
107
155
  - `vectors_vec` still supports one embedding dimension at a time. Switching
package/LICENSE CHANGED
@@ -1,29 +1,6 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2024-2026 Tobi Lutke
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
22
-
23
- ---
24
-
25
- MIT License
26
-
27
4
  Copyright (c) 2026 Wan, Jiun Wei
28
5
 
29
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
package/README.md CHANGED
@@ -28,14 +28,13 @@ You can read more about QMD's progress in the [CHANGELOG](CHANGELOG.md).
28
28
  ## Quick Start (Local Embedding Default)
29
29
 
30
30
  ```sh
31
- # Install globally (Node or Bun)
31
+ # Install globally
32
32
  npm install -g @wei840222/qmd
33
33
  # or
34
- bun install -g @wei840222/qmd
34
+ pnpm add -g @wei840222/qmd
35
35
 
36
36
  # Or run directly
37
37
  npx @wei840222/qmd ...
38
- bunx @wei840222/qmd ...
39
38
 
40
39
  # Create collections for your notes, docs, and meeting transcripts
41
40
  qmd collection add ~/notes --name notes
@@ -96,27 +95,9 @@ Although the tool works perfectly fine when you just tell your agent to use it o
96
95
  - `multi_get` — Batch retrieve by glob pattern, comma-separated list, or docids
97
96
  - `status` — Index health and collection info
98
97
 
99
- **Claude Desktop configuration** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
98
+ #### Stdio Transport (Default)
100
99
 
101
- ```json
102
- {
103
- "mcpServers": {
104
- "qmd": {
105
- "command": "qmd",
106
- "args": ["mcp"]
107
- }
108
- }
109
- }
110
- ```
111
-
112
- **Claude Code** — Install the plugin (recommended):
113
-
114
- ```bash
115
- claude plugin marketplace add tobi/qmd
116
- claude plugin install qmd@qmd
117
- ```
118
-
119
- Or configure MCP manually in `~/.claude/settings.json`:
100
+ Add QMD to your MCP client configuration (e.g. Cursor, Claude Desktop, Zed, or other MCP-compatible clients):
120
101
 
121
102
  ```json
122
103
  {
@@ -211,7 +192,7 @@ results seem unscoped. The HTTP `/query` and `/search` endpoints return
211
192
 
212
193
  ### SDK / Library Usage
213
194
 
214
- Use QMD as a library in your own Node.js or Bun applications.
195
+ Use QMD as a library in your own Node.js applications.
215
196
 
216
197
  #### Installation
217
198
 
@@ -552,7 +533,6 @@ The `query` command uses **Reciprocal Rank Fusion (RRF)** with position-aware bl
552
533
  ### System Requirements
553
534
 
554
535
  - **Node.js** >= 22
555
- - **Bun** >= 1.0.0
556
536
  - **macOS**: Homebrew SQLite (for extension support)
557
537
  ```sh
558
538
  brew install sqlite
@@ -729,16 +709,16 @@ Review the resulting pin and dictionary diff before committing an update.
729
709
  ```sh
730
710
  npm install -g @wei840222/qmd
731
711
  # or
732
- bun install -g @wei840222/qmd
712
+ pnpm add -g @wei840222/qmd
733
713
  ```
734
714
 
735
715
  ### Development
736
716
 
737
717
  ```sh
738
- git clone https://github.com/tobi/qmd
718
+ git clone https://github.com/wei840222/qmd
739
719
  cd qmd
740
- bun install
741
- bun link
720
+ pnpm install
721
+ npm link
742
722
  ```
743
723
 
744
724
  ## Usage
@@ -811,7 +791,7 @@ opt in. Run `qmd status` to verify which grammars are available.
811
791
 
812
792
  > **Note:** Tree-sitter grammars are optional dependencies. If they are not
813
793
  > installed, `--chunk-strategy auto` falls back to regex-only chunking
814
- > automatically. Tested on both Node.js and Bun.
794
+ > automatically.
815
795
 
816
796
  ### Context Management
817
797
 
@@ -4,9 +4,9 @@
4
4
 
5
5
  The bundled Traditional Chinese Jieba dictionary combines the following MIT-licensed sources:
6
6
 
7
- - `@node-rs/jieba` 2.0.1 (`dict.txt`), Copyright (c) LongYinan.
7
+ - `@node-rs/jieba` 2.0.2 (`dict.txt`), Copyright (c) LongYinan.
8
8
  - `APCLab/jieba-tw` (`jieba/dict.txt`) at commit `2bd4c0913cdadc865879b21a00c78e28ef1ba2c5`, Copyright (c) 2013 Sun Junyi.
9
- - `sysprog21/zhtw-mcp` (`assets/ruleset.json`) at commit `2e0f4e4912a8ffdacf7fa3a155cb20c29cba043b`, Copyright (c) 2026 National Cheng Kung University, Taiwan.
9
+ - `sysprog21/zhtw-mcp` (`assets/ruleset.json`) at commit `40e669a769bfcafc5f21f9ca6e2f5152b54c8831`, Copyright (c) 2026 National Cheng Kung University, Taiwan.
10
10
 
11
11
  The source pins and content fingerprints are recorded in `src/search/zh-dict.sources.json` in the qmd source distribution. qmd has no runtime source dependency on these upstream repositories.
12
12
 
package/bin/qmd CHANGED
@@ -1,19 +1,16 @@
1
1
  #!/usr/bin/env node
2
- // 2>/dev/null; if command -v node >/dev/null 2>&1; then exec node "$0" "$@"; else exec bun "$0" "$@"; fi
2
+ // 2>/dev/null; exec node "$0" "$@"
3
3
  // Cross-platform launcher for qmd.
4
4
  //
5
- // Previously this was a POSIX shell script with `#!/bin/sh`, which meant npm
6
- // on Windows generated shims that tried to route through `/bin/sh` — a path
7
- // that doesn't exist on Windows, so `qmd` failed immediately after a global
8
- // install. Rewriting the launcher in Node.js lets npm generate native
5
+ // Rewriting the launcher in Node.js lets npm/pnpm generate native
9
6
  // cmd/ps1/sh shims that invoke `node` directly on every platform.
10
7
 
11
- import { spawn, spawnSync } from "node:child_process";
8
+ import { spawn } from "node:child_process";
12
9
  import { existsSync, realpathSync } from "node:fs";
13
- import { dirname, resolve, sep } from "node:path";
10
+ import { dirname, resolve } from "node:path";
14
11
  import { fileURLToPath } from "node:url";
15
12
 
16
- // Resolve symlinks so global installs (npm link / npm install -g) can find
13
+ // Resolve symlinks so global installs (npm link / npm install -g / pnpm add -g) can find
17
14
  // the actual package directory instead of the global bin directory.
18
15
  const self = realpathSync(fileURLToPath(import.meta.url));
19
16
  const pkgDir = resolve(dirname(self), "..");
@@ -22,7 +19,7 @@ const tsEntry = resolve(pkgDir, "src/cli/qmd.ts");
22
19
 
23
20
  // MCP stdio reserves stdout exclusively for JSON-RPC frames. node-llama-cpp
24
21
  // / llama.cpp / ggml can write native logs directly to stdout before JS-level
25
- // log handlers are attached, so seed the native quiet env before Node/Bun imports
22
+ // log handlers are attached, so seed the native quiet env before Node imports
26
23
  // the CLI and its LLM modules. Preserve explicit user values when provided.
27
24
  if (process.argv[2] === "mcp") {
28
25
  process.env.LLAMA_LOG_LEVEL = process.env.LLAMA_LOG_LEVEL || "error";
@@ -36,145 +33,49 @@ if (process.argv[2] === "mcp") {
36
33
  // residency set is empty (ggml-org/llama.cpp#22593); the keep_alive hasn't
37
34
  // expired by exit, so the assertion fails and ggml_abort dumps a multi-kB
38
35
  // stack trace to stderr even when the user-visible results were already
39
- // emitted correctly. No JS-side dispose can prevent it because the static
40
- // destructor runs in __cxa_finalize_ranges, after every JS-reachable cleanup.
36
+ // emitted correctly.
41
37
  //
42
38
  // For QMD's short-lived CLI workflow, residency sets provide no observable
43
- // performance benefit (subsequent requests don't reuse the warm mapping —
44
- // measured: identical wall time with and without on M3 Pro), so disable them
45
- // by default on darwin. The env var must be set BEFORE the native llama.cpp
46
- // binding loads, which is why it lives here in the launcher rather than in
47
- // the JS entry point. Opt back in with QMD_METAL_KEEP_RESIDENCY=1 if you
48
- // run long-lived qmd processes (the MCP daemon may benefit on hot reload)
49
- // or are triaging an upstream Metal teardown fix.
39
+ // performance benefit, so disable them by default on darwin.
50
40
  if (process.platform === "darwin" && process.env.QMD_METAL_KEEP_RESIDENCY !== "1") {
51
41
  process.env.GGML_METAL_NO_RESIDENCY = process.env.GGML_METAL_NO_RESIDENCY || "1";
52
42
  }
53
43
 
54
- function hasBun() {
55
- try {
56
- const res = spawnSync("bun", ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
57
- return res.status === 0;
58
- } catch {
59
- return false;
60
- }
61
- }
62
-
63
44
  // In published packages, bin/qmd must run dist/. In a git checkout, however,
64
45
  // dist/ is often ignored and can be stale after git reset or branch switches.
65
46
  // Prefer source mode only for checkouts so ./bin/qmd reflects the checked-out
66
47
  // source without changing packaged/runtime behavior.
67
- //
68
- // Critical: source-mode detection must NOT trigger when a package manager
69
- // installed us. `pnpm install -g .` (and `npm install -g .`) copy the entire
70
- // working tree — including .git/, bun.lock, package-lock.json, src/, and even
71
- // node_modules/ — into <prefix>/node_modules/@tobilu/qmd/, so .git and a
72
- // lockfile being present is not a reliable "this is a working tree" signal.
73
- // What IS reliable: a package-manager install always lands the package
74
- // directory inside a `node_modules/` segment; a bare working-tree checkout
75
- // (with `bun link` or a direct path invocation) does not. Gate source mode
76
- // on that. Allow QMD_SOURCE_MODE=1 / =0 as an explicit override for the
77
- // rare case where the heuristic disagrees with the user.
78
48
  const sourceOverride = process.env.QMD_SOURCE_MODE;
79
49
  const looksInstalled = pkgDir.split("/").includes("node_modules");
80
50
  const sourceAllowed = sourceOverride === "1"
81
51
  || (sourceOverride !== "0" && !looksInstalled);
82
52
 
83
53
  let useSourceMode = false;
84
- let sourceRunner = null;
85
- let sourceArgs = [];
54
+ let runnerArgs = [];
86
55
 
87
56
  if (sourceAllowed && existsSync(resolve(pkgDir, ".git")) && existsSync(tsEntry)) {
88
- // Lockfile-driven runner selection — mirror the dist-mode logic below so
89
- // source mode picks the same runtime the user's deps were installed for.
90
- // package-lock.json wins over bun.lock when both are present: pnpm/npm
91
- // installs ship the Node-ABI native modules (better-sqlite3, sqlite-vec),
92
- // and running Bun against them produces ABI mismatches. This also fixes
93
- // pnpm-global installs, which copy the whole working tree — including .git
94
- // and bun.lock — into the install dir and used to route through Bun even
95
- // when the user installed via npm/pnpm.
96
- const hasNpmLock = existsSync(resolve(pkgDir, "package-lock.json"));
97
- const hasBunLock = existsSync(resolve(pkgDir, "bun.lock")) || existsSync(resolve(pkgDir, "bun.lockb"));
98
57
  const tsxEntry = resolve(pkgDir, "node_modules/tsx/dist/cli.mjs");
99
- const tsxAvailable = existsSync(tsxEntry);
100
-
101
- if (hasNpmLock && tsxAvailable) {
102
- useSourceMode = true;
103
- sourceRunner = "node";
104
- sourceArgs = [tsxEntry, tsEntry, ...process.argv.slice(2)];
105
- } else if (hasBunLock && hasBun()) {
58
+ if (existsSync(tsxEntry)) {
106
59
  useSourceMode = true;
107
- sourceRunner = "bun";
108
- sourceArgs = [tsEntry, ...process.argv.slice(2)];
109
- } else if (tsxAvailable) {
110
- useSourceMode = true;
111
- sourceRunner = "node";
112
- sourceArgs = [tsxEntry, tsEntry, ...process.argv.slice(2)];
60
+ runnerArgs = [tsxEntry, tsEntry, ...process.argv.slice(2)];
113
61
  }
114
62
  }
115
63
 
116
64
  if (!useSourceMode && !existsSync(jsEntry)) {
117
65
  console.error(`qmd is not built: missing ${jsEntry}`);
118
- console.error("Run: bun install && bun run build");
66
+ console.error("Run: pnpm install && pnpm run build");
119
67
  console.error("Or: npm install && npm run build");
120
68
  console.error("After building, run: qmd doctor");
121
69
  process.exit(1);
122
70
  }
123
71
 
124
- // Detect the package manager that installed dependencies by checking lockfiles.
125
- // $BUN_INSTALL is intentionally NOT checked — it only indicates that bun exists
126
- // on the system, not that it was used to install this package (see #361).
127
- //
128
- // package-lock.json takes priority: if it exists, npm installed the native
129
- // modules for Node. The repo ships bun.lock, so without this check, source
130
- // builds that use npm would be incorrectly routed to bun, causing ABI
131
- // mismatches with better-sqlite3 / sqlite-vec (see #381).
132
- //
133
- // Package-manager installs keep their lockfile at the install root, above
134
- // node_modules/ — never inside the package directory itself. A bun global
135
- // install writes $BUN_INSTALL/install/global/bun.lock while the package
136
- // lands in .../install/global/node_modules/@tobilu/qmd/, so the package-dir
137
- // checks below can't match and qmd would run under whatever node is first
138
- // on PATH. The native modules were built by the installing runtime; after
139
- // the next Node major upgrade they fail to load (ERR_DLOPEN_FAILED,
140
- // NODE_MODULE_VERSION mismatch). Apply the same npm-priority rule at the
141
- // install root when the package directory itself has no lockfile.
142
- const pathParts = pkgDir.split(sep);
143
- const nodeModulesIndex = pathParts.lastIndexOf("node_modules");
144
- const installRoot = nodeModulesIndex === -1 ? null : pathParts.slice(0, nodeModulesIndex).join(sep);
145
- const hasRootNpmLock = installRoot !== null && existsSync(resolve(installRoot, "package-lock.json"));
146
- const hasRootBunLock = installRoot !== null
147
- && (existsSync(resolve(installRoot, "bun.lock")) || existsSync(resolve(installRoot, "bun.lockb")));
148
-
149
- let runnerName = "node";
150
- if (existsSync(resolve(pkgDir, "package-lock.json"))) {
151
- runnerName = "node";
152
- } else if (existsSync(resolve(pkgDir, "bun.lock")) || existsSync(resolve(pkgDir, "bun.lockb"))) {
153
- runnerName = "bun";
154
- } else if (hasRootNpmLock) {
155
- runnerName = "node";
156
- } else if (hasRootBunLock) {
157
- runnerName = "bun";
158
- } else {
159
- runnerName = "node";
160
- }
161
-
162
- const selected = useSourceMode ? sourceRunner : (runnerName === "node" ? "node" : "bun");
163
72
  // Pin Node to the binary that launched this trampoline. Native addons
164
- // (better-sqlite3) are compiled for that install's ABI; spawning PATH
165
- // `node` picks up nvm/fnm/mise shims in the project directory and fails
166
- // with NODE_MODULE_VERSION / ERR_DLOPEN_FAILED (leftover from #577; #319).
167
- // If the trampoline itself is running under bun (`bun bin/qmd`), keep
168
- // PATH `node` so we don't load Node-ABI addons into bun.
169
- const runner = selected === "node" && typeof process.versions.bun !== "string"
170
- ? process.execPath
171
- : selected;
172
- const args = useSourceMode ? sourceArgs : [jsEntry, ...process.argv.slice(2)];
173
- const needsShell = (runner === "bun") && process.platform === "win32";
73
+ // (better-sqlite3) are compiled for that install's ABI.
74
+ const runner = process.execPath;
75
+ const args = useSourceMode ? runnerArgs : [jsEntry, ...process.argv.slice(2)];
174
76
 
175
77
  const child = spawn(runner, args, {
176
78
  stdio: "inherit",
177
- shell: needsShell,
178
79
  });
179
80
 
180
81
  child.on("exit", (code, signal) => {
@@ -186,7 +87,6 @@ child.on("exit", (code, signal) => {
186
87
  });
187
88
 
188
89
  child.on("error", (err) => {
189
- const name = useSourceMode ? sourceRunner : runnerName;
190
- console.error(`qmd: failed to launch ${name}: ${err.message}`);
90
+ console.error(`qmd: failed to launch node: ${err.message}`);
191
91
  process.exit(1);
192
92
  });
package/dist/ast.js CHANGED
@@ -58,7 +58,7 @@ export function formatGrammarLoadError(language, err) {
58
58
  const grammar = GRAMMAR_MAP[language];
59
59
  const detail = err instanceof Error ? err.message : String(err);
60
60
  return `${grammar.pkg}/${grammar.wasm} failed to load (${detail}); falling back to regex chunking. ` +
61
- `Repair a broken global install with: bun add ${grammar.pkg}@${grammar.version}`;
61
+ `Repair a broken global install with: pnpm add ${grammar.pkg}@${grammar.version}`;
62
62
  }
63
63
  // =============================================================================
64
64
  // Per-Language Query Definitions
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "f7e2cfc",
3
- "builtAt": "2026-08-23T03:21:48.505Z"
2
+ "commit": "1e728bc",
3
+ "builtAt": "2026-08-23T06:13:30.733Z"
4
4
  }
package/dist/cli/qmd.d.ts CHANGED
@@ -37,7 +37,7 @@ type FinishSuccessfulCliCommandOptions = {
37
37
  * order, and the process exits cleanly. The `GGML_METAL_NO_RESIDENCY=1` env
38
38
  * var that `bin/qmd` exports is a defense-in-depth safety net for paths
39
39
  * that still call `process.exit()` after loading the native binding
40
- * (signal handlers, error paths, `bun test`).
40
+ * (signal handlers, error paths, test runners).
41
41
  *
42
42
  * If the caller passes an explicit `exit` for testability, we honor it —
43
43
  * the lifecycle tests verify the legacy flush → cleanup → exit ordering.
package/dist/cli/qmd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { isBun, openDatabase, loadSqliteVec } from "../db.js";
2
+ import { openDatabase, loadSqliteVec } from "../db.js";
3
3
  import fastGlob from "fast-glob";
4
4
  import { spawn as nodeSpawn } from "child_process";
5
5
  import { isQmdMcpPid, mcpDaemonStateFiles } from "./mcp-pid.js";
@@ -270,7 +270,7 @@ async function flushWritable(stream) {
270
270
  * order, and the process exits cleanly. The `GGML_METAL_NO_RESIDENCY=1` env
271
271
  * var that `bin/qmd` exports is a defense-in-depth safety net for paths
272
272
  * that still call `process.exit()` after loading the native binding
273
- * (signal handlers, error paths, `bun test`).
273
+ * (signal handlers, error paths, test runners).
274
274
  *
275
275
  * If the caller passes an explicit `exit` for testability, we honor it —
276
276
  * the lifecycle tests verify the legacy flush → cleanup → exit ordering.
@@ -3933,7 +3933,7 @@ async function showDoctor() {
3933
3933
  const nextSteps = [];
3934
3934
  console.log(`${c.bold}QMD Doctor${c.reset}\n`);
3935
3935
  console.log(`Index: ${getDbPath()}`);
3936
- console.log(`Runtime: ${isBun ? "bun:sqlite" : "better-sqlite3"}`);
3936
+ console.log(`Runtime: better-sqlite3`);
3937
3937
  try {
3938
3938
  const row = db.prepare(`SELECT sqlite_version() AS version`).get();
3939
3939
  doctorCheck("SQLite runtime", true, row.version);
@@ -4632,11 +4632,8 @@ if (isMain) {
4632
4632
  const selfPath = fileURLToPath(import.meta.url);
4633
4633
  const indexArgs = cli.values.index ? ["--index", String(cli.values.index)] : [];
4634
4634
  const hostArgs = host ? ["--host", host] : [];
4635
- const isBunRuntime = typeof process.versions.bun === "string";
4636
4635
  const spawnArgs = selfPath.endsWith(".ts")
4637
- ? isBunRuntime
4638
- ? [selfPath, ...indexArgs, "mcp", "--http", "--port", String(port), ...hostArgs]
4639
- : ["--import", pathJoin(dirname(selfPath), "..", "..", "node_modules", "tsx", "dist", "esm", "index.mjs"), selfPath, ...indexArgs, "mcp", "--http", "--port", String(port), ...hostArgs]
4636
+ ? ["--import", pathJoin(dirname(selfPath), "..", "..", "node_modules", "tsx", "dist", "esm", "index.mjs"), selfPath, ...indexArgs, "mcp", "--http", "--port", String(port), ...hostArgs]
4640
4637
  : [selfPath, ...indexArgs, "mcp", "--http", "--port", String(port), ...hostArgs];
4641
4638
  const child = nodeSpawn(process.execPath, spawnArgs, {
4642
4639
  stdio: ["ignore", logFd, logFd],
package/dist/db.d.ts CHANGED
@@ -1,27 +1,19 @@
1
1
  /**
2
- * db.ts - Cross-runtime SQLite compatibility layer
2
+ * db.ts - SQLite database connection and extension management
3
3
  *
4
- * Provides a unified Database export that works under both Bun (bun:sqlite)
5
- * and Node.js (better-sqlite3). The APIs are nearly identical — the main
6
- * difference is the import path.
7
- *
8
- * On macOS, Apple's system SQLite is compiled with SQLITE_OMIT_LOAD_EXTENSION,
9
- * which prevents loading native extensions like sqlite-vec. When running under
10
- * Bun we call Database.setCustomSQLite() to swap in Homebrew's full-featured
11
- * SQLite build before creating any database instances.
4
+ * Provides Database export and connection management using better-sqlite3
5
+ * and sqlite-vec.
12
6
  */
13
- export declare const isBun: boolean;
7
+ import BetterSqlite3 from "better-sqlite3";
14
8
  export type SQLiteValue = string | number | bigint | Buffer | Uint8Array | Float32Array | null;
15
9
  export type SQLiteParams = readonly SQLiteValue[];
16
10
  /**
17
- * Open a SQLite database. Works with both bun:sqlite and better-sqlite3.
11
+ * Open a SQLite database using better-sqlite3.
18
12
  *
19
- * `bun:sqlite` and `better-sqlite3` both default `busy_timeout` to 0, so
20
- * concurrent writers throw `SQLITE_BUSY` instead of waiting. WAL improves
21
- * read-while-write concurrency but does not serialise writers. Setting the
22
- * timeout at connection open makes parallel processes (e.g. an `update` or
23
- * `query` racing a long `embed`, or a first-open schema migration racing any
24
- * routine command) queue at batch boundaries instead of failing on contact.
13
+ * `better-sqlite3` defaults `busy_timeout` to 0, so concurrent writers throw
14
+ * `SQLITE_BUSY` instead of waiting. WAL improves read-while-write concurrency
15
+ * but does not serialise writers. Setting the timeout at connection open makes
16
+ * parallel processes queue at batch boundaries instead of failing on contact.
25
17
  *
26
18
  * WAL is enabled here too (with a bounded retry) so connection-level pragmas
27
19
  * live in one place and the cold-database journal migration survives concurrent
@@ -29,37 +21,19 @@ export type SQLiteParams = readonly SQLiteValue[];
29
21
  *
30
22
  * Default 120_000 ms outlasts the worst-case batch commit on a multi-GB
31
23
  * index. Override with `QMD_SQLITE_BUSY_TIMEOUT` (value in milliseconds; `0`
32
- * restores the upstream fail-fast behaviour). See
33
- * https://bun.sh/docs/api/sqlite#busy-timeout.
24
+ * restores the upstream fail-fast behaviour).
34
25
  */
35
26
  export declare function openDatabase(path: string): Database;
36
27
  /** Open an existing database without changing journal mode, schema, or user data. */
37
28
  export declare function openReadOnlyDatabase(path: string): Database;
38
29
  /**
39
- * Common subset of the Database interface used throughout QMD.
30
+ * Database and Statement types used throughout QMD.
40
31
  */
41
- export interface Database {
42
- exec(sql: string): void;
43
- prepare(sql: string): Statement;
44
- loadExtension(path: string): void;
45
- transaction<T extends (...args: SQLiteValue[]) => unknown>(fn: T): T & {
46
- immediate: T;
47
- };
48
- close(): void;
49
- }
50
- export interface Statement {
51
- run(...params: SQLiteValue[]): {
52
- changes: number;
53
- lastInsertRowid: number | bigint;
54
- };
55
- get<T = unknown>(...params: SQLiteValue[]): T | undefined;
56
- all<T = unknown>(...params: SQLiteValue[]): T[];
57
- iterate<T = unknown>(...params: SQLiteValue[]): IterableIterator<T>;
58
- }
32
+ export type Database = BetterSqlite3.Database;
33
+ export type Statement<T extends SQLiteParams = SQLiteParams> = BetterSqlite3.Statement<T>;
59
34
  /**
60
35
  * Load the sqlite-vec extension into a database.
61
36
  *
62
- * Throws with platform-specific fix instructions when the extension is
63
- * unavailable.
37
+ * Throws with fix instructions when the extension is unavailable.
64
38
  */
65
39
  export declare function loadSqliteVec(db: Database): void;
package/dist/db.js CHANGED
@@ -1,57 +1,11 @@
1
1
  /**
2
- * db.ts - Cross-runtime SQLite compatibility layer
2
+ * db.ts - SQLite database connection and extension management
3
3
  *
4
- * Provides a unified Database export that works under both Bun (bun:sqlite)
5
- * and Node.js (better-sqlite3). The APIs are nearly identical — the main
6
- * difference is the import path.
7
- *
8
- * On macOS, Apple's system SQLite is compiled with SQLITE_OMIT_LOAD_EXTENSION,
9
- * which prevents loading native extensions like sqlite-vec. When running under
10
- * Bun we call Database.setCustomSQLite() to swap in Homebrew's full-featured
11
- * SQLite build before creating any database instances.
4
+ * Provides Database export and connection management using better-sqlite3
5
+ * and sqlite-vec.
12
6
  */
13
- export const isBun = "Bun" in globalThis;
14
- let _Database;
15
- let _sqliteVecLoad;
16
- if (isBun) {
17
- // Dynamic string prevents tsc from resolving bun:sqlite on Node.js builds
18
- const bunSqlite = "bun:" + "sqlite";
19
- const BunDatabase = (await import(/* @vite-ignore */ bunSqlite)).Database;
20
- // See: https://bun.com/docs/runtime/sqlite#setcustomsqlite
21
- if (process.platform === "darwin") {
22
- const homebrewPaths = [
23
- "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib", // Apple Silicon
24
- "/usr/local/opt/sqlite/lib/libsqlite3.dylib", // Intel
25
- ];
26
- for (const p of homebrewPaths) {
27
- try {
28
- BunDatabase.setCustomSQLite(p);
29
- break;
30
- }
31
- catch { }
32
- }
33
- }
34
- _Database = BunDatabase;
35
- // setCustomSQLite may have silently failed — test that extensions actually work.
36
- try {
37
- const { getLoadablePath } = await import("sqlite-vec");
38
- const vecPath = getLoadablePath();
39
- const testDb = new BunDatabase(":memory:");
40
- testDb.loadExtension(vecPath);
41
- testDb.close();
42
- _sqliteVecLoad = (db) => db.loadExtension(vecPath);
43
- }
44
- catch {
45
- // Vector search won't work, but BM25 and other operations are unaffected.
46
- _sqliteVecLoad = null;
47
- }
48
- }
49
- else {
50
- // Dual-runtime: better-sqlite3 matches Database at runtime; published types do not share an interface with bun:sqlite.
51
- _Database = (await import("better-sqlite3")).default;
52
- const sqliteVec = await import("sqlite-vec");
53
- _sqliteVecLoad = (db) => sqliteVec.load(db);
54
- }
7
+ import BetterSqlite3 from "better-sqlite3";
8
+ import * as sqliteVec from "sqlite-vec";
55
9
  function isBusyError(err) {
56
10
  if (typeof err !== "object" || err === null)
57
11
  return false;
@@ -86,14 +40,12 @@ function enableWal(db, budgetMs) {
86
40
  }
87
41
  }
88
42
  /**
89
- * Open a SQLite database. Works with both bun:sqlite and better-sqlite3.
43
+ * Open a SQLite database using better-sqlite3.
90
44
  *
91
- * `bun:sqlite` and `better-sqlite3` both default `busy_timeout` to 0, so
92
- * concurrent writers throw `SQLITE_BUSY` instead of waiting. WAL improves
93
- * read-while-write concurrency but does not serialise writers. Setting the
94
- * timeout at connection open makes parallel processes (e.g. an `update` or
95
- * `query` racing a long `embed`, or a first-open schema migration racing any
96
- * routine command) queue at batch boundaries instead of failing on contact.
45
+ * `better-sqlite3` defaults `busy_timeout` to 0, so concurrent writers throw
46
+ * `SQLITE_BUSY` instead of waiting. WAL improves read-while-write concurrency
47
+ * but does not serialise writers. Setting the timeout at connection open makes
48
+ * parallel processes queue at batch boundaries instead of failing on contact.
97
49
  *
98
50
  * WAL is enabled here too (with a bounded retry) so connection-level pragmas
99
51
  * live in one place and the cold-database journal migration survives concurrent
@@ -101,11 +53,10 @@ function enableWal(db, budgetMs) {
101
53
  *
102
54
  * Default 120_000 ms outlasts the worst-case batch commit on a multi-GB
103
55
  * index. Override with `QMD_SQLITE_BUSY_TIMEOUT` (value in milliseconds; `0`
104
- * restores the upstream fail-fast behaviour). See
105
- * https://bun.sh/docs/api/sqlite#busy-timeout.
56
+ * restores the upstream fail-fast behaviour).
106
57
  */
107
58
  export function openDatabase(path) {
108
- const db = new _Database(path);
59
+ const db = new BetterSqlite3(path);
109
60
  const raw = process.env.QMD_SQLITE_BUSY_TIMEOUT;
110
61
  const parsed = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
111
62
  const busyTimeoutMs = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 120_000;
@@ -115,10 +66,8 @@ export function openDatabase(path) {
115
66
  }
116
67
  /** Open an existing database without changing journal mode, schema, or user data. */
117
68
  export function openReadOnlyDatabase(path) {
118
- const options = isBun
119
- ? { readonly: true, create: false }
120
- : { readonly: true, fileMustExist: true };
121
- const db = new _Database(path, options);
69
+ const options = { readonly: true, fileMustExist: true };
70
+ const db = new BetterSqlite3(path, options);
122
71
  const raw = process.env.QMD_SQLITE_BUSY_TIMEOUT;
123
72
  const parsed = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
124
73
  const busyTimeoutMs = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 120_000;
@@ -128,16 +77,14 @@ export function openReadOnlyDatabase(path) {
128
77
  /**
129
78
  * Load the sqlite-vec extension into a database.
130
79
  *
131
- * Throws with platform-specific fix instructions when the extension is
132
- * unavailable.
80
+ * Throws with fix instructions when the extension is unavailable.
133
81
  */
134
82
  export function loadSqliteVec(db) {
135
- if (!_sqliteVecLoad) {
136
- const hint = isBun && process.platform === "darwin"
137
- ? "On macOS with Bun, install Homebrew SQLite: brew install sqlite\n" +
138
- "Or install qmd with npm instead: npm install -g @wei840222/qmd"
139
- : "Ensure the sqlite-vec native module is installed correctly.";
140
- throw new Error(`sqlite-vec extension is unavailable. ${hint}`);
83
+ try {
84
+ sqliteVec.load(db);
85
+ }
86
+ catch (err) {
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ throw new Error(`sqlite-vec extension is unavailable. Ensure the sqlite-vec native module is installed correctly: ${message}`);
141
89
  }
142
- _sqliteVecLoad(db);
143
90
  }
@@ -46617,6 +46617,7 @@ c++ 3 nz
46617
46617
  來歷不明 5 Vi
46618
46618
  來歸 4 Vi
46619
46619
  來源 1909 N
46620
+ 來源位置 1000000 nz
46620
46621
  來源國 18 N
46621
46622
  來源地 3 N
46622
46623
  來源場 4 N
@@ -53444,7 +53445,7 @@ c++ 3 nz
53444
53445
  停養 5 Vt
53445
53446
  停養區 1 N
53446
53447
  停餐 18 N
53447
- 停駐 45 Vi
53448
+ 停駐 1000000 nz
53448
53449
  停駕 6 Vi
53449
53450
  停駛 271 Vi
53450
53451
  停驗 2 Vt
@@ -151253,6 +151254,7 @@ c++ 3 nz
151253
151254
  多鹽 1 N
151254
151255
  多麗 1 N
151255
151256
  多麼 338 ADV
151257
+ 多點傳送 1000000 nz
151256
151258
  多黨 19 N
151257
151259
  多黨制 1 N
151258
151260
  夛 198 zg
@@ -181443,6 +181445,7 @@ c++ 3 nz
181443
181445
  封前朝 2 nr
181444
181446
  封包 1000000 nz
181445
181447
  封包交換 1000000 nz
181448
+ 封包遺失 1000000 nz
181446
181449
  封华歆 2 nr
181447
181450
  封博望 3 nr
181448
181451
  封博陵 2 nr
@@ -223286,6 +223289,7 @@ c++ 3 nz
223286
223289
  快反 22 v
223287
223290
  快取 1000000 nz
223288
223291
  快取記憶體 1000000 nz
223292
+ 快取項目 1000000 nz
223289
223293
  快咬 3 v
223290
223294
  快嘴 11 N
223291
223295
  快嘴利舌 3 i
@@ -327168,7 +327172,7 @@ c++ 3 nz
327168
327172
  比容 7 n
327169
327173
  比对 88 d
327170
327174
  比对法 3 n
327171
- 比對 811 Vt
327175
+ 比對 1000000 nz
327172
327176
  比對市 1 N
327173
327177
  比對戰 1 N
327174
327178
  比小孔 2 nr
@@ -345178,7 +345182,7 @@ c++ 3 nz
345178
345182
  流离颠疐 3 i
345179
345183
  流离颠顿 3 i
345180
345184
  流移失所 3 i
345181
- 流程 780 N
345185
+ 流程 1000000 nz
345182
345186
  流程再造 1000000 nz
345183
345187
  流程化 4 n
345184
345188
  流程图 18 n
@@ -386962,6 +386966,7 @@ c++ 3 nz
386962
386966
  生命觀 2 N
386963
386967
  生命诚可贵 3 i
386964
386968
  生命财产 3 l
386969
+ 生命週期 1000000 nz
386965
386970
  生命體 10 N
386966
386971
  生員 3 N
386967
386972
  生唐 2 t
@@ -402286,7 +402291,7 @@ c++ 3 nz
402286
402291
  相究 1 Vt
402287
402292
  相空间 3 n
402288
402293
  相竞 16 v
402289
- 相符 220 Vi
402294
+ 相符 1000000 nz
402290
402295
  相符合 3 nr
402291
402296
  相等 18 Vi
402292
402297
  相等于 3 l
@@ -448252,6 +448257,7 @@ c++ 3 nz
448252
448257
  群憤 1 Vi
448253
448258
  群戲 1 N
448254
448259
  群批 1 N
448260
+ 群播 1000000 nz
448255
448261
  群擋球 1 N
448256
448262
  群攀 1 N
448257
448263
  群攻 1 Vt
@@ -456405,7 +456411,7 @@ c++ 3 nz
456405
456411
  背日 3 n
456406
456412
  背日性 3 n
456407
456413
  背旮旯儿 3 z
456408
- 背景 1419 N
456414
+ 背景 1000000 nz
456409
456415
  背景值 15 N
456410
456416
  背景光 1 N
456411
456417
  背景噪声 10 n
@@ -501378,7 +501384,7 @@ c++ 3 nz
501378
501384
  註消 1 N
501379
501385
  註生娘娘 44 N
501380
501386
  註腳 10 N
501381
- 註解 58 N
501387
+ 註解 1000000 nz
501382
501388
  註記 184 N
501383
501389
  註語 1 N
501384
501390
  註載 1 N
@@ -542725,7 +542731,7 @@ c++ 3 nz
542725
542731
  遮空蔽日 3 i
542726
542732
  遮簷 2 N
542727
542733
  遮簾 1 N
542728
- 遮罩 3 N
542734
+ 遮罩 1000000 nz
542729
542735
  遮羞 14 Vi
542730
542736
  遮羞布 37 N
542731
542737
  遮羞費 18 N
package/dist/store.js CHANGED
@@ -3018,7 +3018,7 @@ export function countOrphanedVectors(db) {
3018
3018
  * Returns the number of orphaned embedding chunks deleted.
3019
3019
  */
3020
3020
  export function cleanupOrphanedVectors(db) {
3021
- // sqlite-vec may not be loaded (e.g. Bun's bun:sqlite lacks loadExtension).
3021
+ // sqlite-vec may not be loaded if extension failed to initialize.
3022
3022
  // The vectors_vec virtual table can appear in sqlite_master from a prior
3023
3023
  // session, but querying it without the vec0 module loaded will crash (#380).
3024
3024
  if (!isSqliteVecAvailable()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wei840222/qmd",
3
- "version": "2026.8.23",
3
+ "version": "2026.8.24",
4
4
  "packageManager": "pnpm@11.15.1",
5
5
  "description": "Query Markup Documents - On-device hybrid search for markdown files with BM25, vector search, and LLM reranking",
6
6
  "type": "module",
@@ -34,17 +34,15 @@
34
34
  "lint": "oxlint",
35
35
  "test": "node scripts/test-all.mjs",
36
36
  "test:types": "node ./node_modules/typescript/bin/tsc -p tsconfig.build.json --noEmit",
37
- "test:node": "bun scripts/test-runtime.mjs node",
38
- "test:bun": "bun scripts/test-runtime.mjs bun",
39
- "test:models:node": "bun scripts/test-runtime.mjs node --models-only",
40
- "test:models:bun": "bun scripts/test-runtime.mjs bun --models-only",
41
- "test:unit": "CI=true node ./node_modules/vitest/vitest.mjs run --reporter=verbose --testTimeout 60000 test/ && CI=true bun test --timeout 60000 --preload ./src/test-preload.ts test/",
37
+ "test:models": "node scripts/test-runtime.mjs",
38
+ "test:models:only": "node scripts/test-runtime.mjs --models-only",
39
+ "test:unit": "CI=true node ./node_modules/vitest/vitest.mjs run --reporter=verbose --testTimeout 60000 test/",
42
40
  "test:quality": "node ./node_modules/vitest/vitest.mjs run --reporter=verbose test/eval-cjk.test.ts",
43
41
  "test:package": "node scripts/package-smoke.mjs",
44
42
  "smoke:package-grammars": "node scripts/check-package-grammars.mjs",
45
43
  "dict:sync": "node scripts/sync-zh-dict.mjs",
46
- "measure:cjk": "bun scripts/measure-cjk.ts",
47
- "inspector": "bunx @modelcontextprotocol/inspector tsx src/cli/qmd.ts mcp",
44
+ "measure:cjk": "tsx scripts/measure-cjk.ts",
45
+ "inspector": "npx @modelcontextprotocol/inspector tsx src/cli/qmd.ts mcp",
48
46
  "release": "./scripts/release.sh",
49
47
  "qmd": "tsx src/cli/qmd.ts",
50
48
  "index": "tsx src/cli/qmd.ts index",
@@ -52,7 +50,7 @@
52
50
  "search": "tsx src/cli/qmd.ts search",
53
51
  "vsearch": "tsx src/cli/qmd.ts vsearch",
54
52
  "rerank": "tsx src/cli/qmd.ts rerank",
55
- "bench": "bun src/cli/qmd.ts bench"
53
+ "bench": "tsx src/cli/qmd.ts bench"
56
54
  },
57
55
  "publishConfig": {
58
56
  "access": "public"
@@ -67,7 +65,7 @@
67
65
  },
68
66
  "dependencies": {
69
67
  "@modelcontextprotocol/server": "2.0.0",
70
- "@node-rs/jieba": "2.0.1",
68
+ "@node-rs/jieba": "2.0.2",
71
69
  "better-sqlite3": "^13.0.3",
72
70
  "fast-glob": "3.3.3",
73
71
  "node-llama-cpp": "3.20.0",
@@ -112,7 +110,7 @@
112
110
  "vite": "7.3.5"
113
111
  },
114
112
  "peerDependencies": {
115
- "typescript": "^5.9.3"
113
+ "typescript": "^5.9.3 || ^6.0.0-0"
116
114
  },
117
115
  "engines": {
118
116
  "node": ">=22.0.0"
@@ -134,6 +132,6 @@
134
132
  "local-ai",
135
133
  "llm"
136
134
  ],
137
- "author": "Tobi Lutke <tobi@lutke.com>",
135
+ "author": "Wan, Jiun Wei <wei840222@gmail.com>",
138
136
  "license": "MIT"
139
137
  }
@@ -24,6 +24,6 @@ for (const grammar of grammars) {
24
24
  }
25
25
 
26
26
  if (!ok) {
27
- console.error("\nAST grammar package smoke check failed. Run `bun install` locally or repair a broken global install with the matching `bun add tree-sitter-...@<version>` command shown by `qmd status`.");
27
+ console.error("\nAST grammar package smoke check failed. Run `pnpm install` locally or repair a broken global install with the matching `pnpm add tree-sitter-...@<version>` command shown by `qmd status`.");
28
28
  process.exit(1);
29
29
  }
@@ -99,12 +99,6 @@ assertPath("THIRD_PARTY_NOTICES.md", "third-party notices");
99
99
  run("compiled CLI under Node", process.execPath, ["dist/cli/qmd.js", "--help"], { quiet: true });
100
100
  run("package wrapper", "sh", ["bin/qmd", "--help"], { quiet: true });
101
101
 
102
- if (process.env.QMD_SKIP_BUN_SMOKE === "1") {
103
- console.log("==> compiled CLI under Bun (skipped by QMD_SKIP_BUN_SMOKE=1)");
104
- } else {
105
- run("compiled CLI under Bun", "bun", ["dist/cli/qmd.js", "--help"], { quiet: true });
106
- }
107
-
108
102
  const packageSmokeRoot = process.env.QMD_PACKAGE_SMOKE_TMPDIR || join(root, ".tmp");
109
103
  mkdirSync(packageSmokeRoot, { recursive: true });
110
104
  const packageSmokeDir = mkdtempSync(join(packageSmokeRoot, "qmd-package-smoke-"));
@@ -129,9 +123,9 @@ try {
129
123
  `${JSON.stringify({ private: true, type: "module" }, null, 2)}\n`,
130
124
  );
131
125
  run(
132
- "install packed tarball with Bun",
133
- "bun",
134
- ["add", "--ignore-scripts", join(packageSmokeDir, tarballName)],
126
+ "install packed tarball with npm",
127
+ "npm",
128
+ ["install", "--ignore-scripts", "--no-package-lock", join(packageSmokeDir, tarballName)],
135
129
  { cwd: consumerDir, quiet: true },
136
130
  );
137
131
 
@@ -179,14 +173,6 @@ try {
179
173
  ["--input-type=module", "--eval", jiebaSmoke],
180
174
  { cwd: consumerDir, env: smokeEnv, quiet: true },
181
175
  );
182
- if (process.env.QMD_SKIP_BUN_SMOKE !== "1") {
183
- run(
184
- "packed jieba capability under Bun",
185
- "bun",
186
- ["--eval", jiebaSmoke],
187
- { cwd: consumerDir, env: smokeEnv, quiet: true },
188
- );
189
- }
190
176
  } finally {
191
177
  rmSync(packageSmokeDir, { recursive: true, force: true });
192
178
  }
@@ -41,5 +41,4 @@ function run(label, command, args, options = {}) {
41
41
 
42
42
  run("TypeScript build typecheck", process.execPath, [join(root, "node_modules", "typescript", "bin", "tsc"), "-p", "tsconfig.build.json", "--noEmit"]);
43
43
  run("Vitest suite under Node", process.execPath, [join(root, "node_modules", "vitest", "vitest.mjs"), "run", "--reporter=verbose", "--testTimeout", "60000", "test/"], { env: { CI: "true" } });
44
- run("Bun test suite", "bun", ["test", "--timeout", "60000", "--preload", "./src/test-preload.ts", "test/"], { env: { CI: "true" } });
45
44
  run("Package smoke", process.execPath, ["scripts/package-smoke.mjs"]);
@@ -4,8 +4,8 @@ description: Search local markdown knowledge bases, notes, docs, and wikis with
4
4
  license: MIT
5
5
  compatibility: Requires qmd CLI or MCP server. Install via `npm install -g @wei840222/qmd`.
6
6
  metadata:
7
- author: tobi
8
- version: "2.6.3"
7
+ author: wei840222
8
+ version: 2026.8.23-1
9
9
  allowed-tools: Bash(qmd:*), mcp__qmd__*
10
10
  ---
11
11
 
@@ -209,8 +209,14 @@ When using the MCP server, prefer structured searches:
209
209
  {
210
210
  "searches": [
211
211
  { "type": "lex", "query": "cockpit OKR Goodhart" },
212
- { "type": "vec", "query": "data informed not metric driven product judgment" },
213
- { "type": "hyde", "query": "A concept note explains that metrics are useful as instruments, but leaders should not let OKRs or dashboards replace judgment." }
212
+ {
213
+ "type": "vec",
214
+ "query": "data informed not metric driven product judgment"
215
+ },
216
+ {
217
+ "type": "hyde",
218
+ "query": "A concept note explains that metrics are useful as instruments, but leaders should not let OKRs or dashboards replace judgment."
219
+ }
214
220
  ],
215
221
  "intent": "Find the concept note about using metrics as instruments without becoming metric-driven.",
216
222
  "collections": ["concepts"],
@@ -245,6 +251,8 @@ qmd query $'intent: Find the customer proximity concept, not generic customer de
245
251
  qmd search "six-week cadence WhatsApp merchant relationships Shawn Ryan" -c sources -n 10
246
252
  ```
247
253
 
254
+ For the complete EBNF grammar, search operators, and JSON payload specifications, see [Query Syntax Reference](references/query-syntax.md).
255
+
248
256
  ## Setup and maintenance
249
257
 
250
258
  Only mutate indexes when the user asked for setup or maintenance. Searching and
@@ -264,17 +272,17 @@ Configure models and custom endpoints in `~/.config/qmd/index.yml` under the `mo
264
272
  ```yaml
265
273
  models:
266
274
  embed: hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf
267
- embed_api_url: https://api.example.com/v1 # Both embed_api_url and embed_api_model enable remote embeddings
268
- embed_api_model: text-embedding-3-small # or text-embedding-3-large
269
- embed_dimension: 1536 # Optional: expected vector dimension; validates local output
275
+ embed_api_url: https://api.example.com/v1 # Both embed_api_url and embed_api_model enable remote embeddings
276
+ embed_api_model: text-embedding-3-small # or text-embedding-3-large
277
+ embed_dimension: 1536 # Optional: expected vector dimension; validates local output
270
278
 
271
279
  # Optional: Remote LLM Query Expansion (aliases: generate_url, generate_base_url, generate_api_url)
272
- generate_api_url: https://api.example.com/v1 # Base URL (appends /chat/completions) or full endpoint
273
- generate_api_model: qwen3-7b-instruct # or your-model-name
280
+ generate_api_url: https://api.example.com/v1 # Base URL (appends /chat/completions) or full endpoint
281
+ generate_api_model: qwen3-7b-instruct # or your-model-name
274
282
 
275
283
  # Optional: Remote Reranking (supports rerank_url / rerank_base_url / rerank_api_url)
276
284
  rerank_api_url: https://api.example.com/v1/chat/completions # Supports both /v1/rerank and /v1/chat/completions LLM endpoints
277
- rerank_api_model: bge-reranker-v2-m3 # or gpt-4o-mini / qwen3-7b-instruct
285
+ rerank_api_model: bge-reranker-v2-m3 # or gpt-4o-mini / qwen3-7b-instruct
278
286
 
279
287
  # Optional: Custom User Dictionary for CJK segmentation
280
288
  dictionary: ~/.config/qmd/dictionary.txt
@@ -10,30 +10,14 @@ qmd embed
10
10
 
11
11
  ## Configure MCP Client
12
12
 
13
- **Claude Code** (`~/.claude/settings.json`):
14
- ```json
15
- {
16
- "mcpServers": {
17
- "qmd": { "command": "qmd", "args": ["mcp"] }
18
- }
19
- }
20
- ```
13
+ Add QMD to your MCP client configuration (e.g. Cursor, Claude Desktop, Zed, OpenClaw, or other MCP-compatible clients):
21
14
 
22
- **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
23
15
  ```json
24
16
  {
25
17
  "mcpServers": {
26
- "qmd": { "command": "qmd", "args": ["mcp"] }
27
- }
28
- }
29
- ```
30
-
31
- **OpenClaw** (`~/.openclaw/openclaw.json`):
32
- ```json
33
- {
34
- "mcp": {
35
- "servers": {
36
- "qmd": { "command": "qmd", "args": ["mcp"] }
18
+ "qmd": {
19
+ "command": "qmd",
20
+ "args": ["mcp"]
37
21
  }
38
22
  }
39
23
  }
@@ -0,0 +1,158 @@
1
+ # QMD Query Syntax Reference
2
+
3
+ QMD queries are structured documents composed of typed sub-queries. Each line specifies a search type and query text. The hybrid retrieval engine combines results via Reciprocal Rank Fusion (RRF) and reranks them.
4
+
5
+ ## Grammar
6
+
7
+ ```ebnf
8
+ query = policy_query | query_document ;
9
+ policy_query = [ policy_prefix ] text ;
10
+ policy_prefix = "lex:" | "expand:" ;
11
+ query_document = [ intent_line ] { typed_line } ;
12
+ intent_line = "intent:" text newline ;
13
+ typed_line = type ":" text newline ;
14
+ type = "lex" | "vec" | "hyde" ;
15
+ text = quoted_phrase | plain_text ;
16
+ quoted_phrase = '"' { character } '"' ;
17
+ plain_text = { character } ;
18
+ newline = "\n" ;
19
+ ```
20
+
21
+ ## Query Types
22
+
23
+ | Type | Method | Best For | Description |
24
+ |------|--------|----------|-------------|
25
+ | `lex` | BM25 (FTS5) | Exact terms, identifiers, code, titles | Keyword search with prefix, phrase, and negation support |
26
+ | `vec` | Vector | Natural language concepts | Semantic similarity search using local/remote embeddings |
27
+ | `hyde` | Vector | Complex conceptual questions | Hypothetical Document Embedding (generate expected passage) |
28
+
29
+ ## Default Policy & Expansion Behavior
30
+
31
+ A query is either a single policy query or a multi-line query document:
32
+ - **`auto` (Default)**: CJK queries and strong lexical matches automatically bypass model expansion. Other plain queries expand into `lex`, `vec`, and `hyde` variants.
33
+ - **`expand:` / `--expand` (`force`)**: Explicitly forces expansion even if bypass heuristics apply.
34
+ - **`lex:` (`skip`)**: Explicitly disables expansion and performs direct BM25 search.
35
+
36
+ ```bash
37
+ # Automatic policy:
38
+ qmd query "how does authentication work"
39
+
40
+ # Force expansion:
41
+ qmd query "expand: how does authentication work"
42
+ # or: qmd query --expand "資料庫同步"
43
+
44
+ # Explicitly skip expansion:
45
+ qmd query "lex: authentication"
46
+ ```
47
+
48
+ ## Lexical Search Syntax (`lex:`)
49
+
50
+ Lex queries support powerful search operators:
51
+
52
+ | Syntax | Meaning | Example | Notes |
53
+ |--------|---------|---------|-------|
54
+ | `word` | Prefix match | `perf` | Matches "performance", "perform", etc. |
55
+ | `"phrase"` | Exact phrase match | `"rate limiter"` | Terms must appear consecutively in order |
56
+ | `-word` | Exclude term | `-sports` | Documents containing this word are excluded |
57
+ | `-"phrase"` | Exclude phrase | `-"test data"` | Documents containing this phrase are excluded |
58
+
59
+ ### Examples
60
+
61
+ ```
62
+ lex: CAP theorem consistency
63
+ lex: "machine learning" -"deep learning"
64
+ lex: auth -oauth -saml
65
+ ```
66
+
67
+ ## Vector Search Syntax (`vec:`)
68
+
69
+ Natural language questions or descriptive phrases:
70
+
71
+ ```
72
+ vec: how does the rate limiter handle burst traffic
73
+ vec: what is the tradeoff between consistency and availability
74
+ ```
75
+
76
+ ## Hypothetical Document Embeddings (`hyde:`)
77
+
78
+ A 50–100 word hypothetical answer passage representing what the target document likely says:
79
+
80
+ ```
81
+ hyde: The rate limiter uses a sliding window counter algorithm with a 60-second window. When a client exceeds 100 requests per minute, subsequent requests return 429 Too Many Requests.
82
+ ```
83
+
84
+ ## Multi-Line Structured Queries
85
+
86
+ Combine multiple sub-query types for optimal retrieval. The first sub-query receives **2x weight** during Reciprocal Rank Fusion:
87
+
88
+ ```
89
+ lex: rate limiter algorithm
90
+ vec: how does rate limiting work in the API
91
+ hyde: The API implements rate limiting using a token bucket algorithm...
92
+ ```
93
+
94
+ ## Disambiguating with `intent:`
95
+
96
+ An optional `intent:` line provides background context to disambiguate ambiguous queries. It steers query expansion, reranking, and snippet selection without generating search vectors itself:
97
+
98
+ - At most one `intent:` line per query document.
99
+ - Must be combined with at least one `lex:`, `vec:`, or `hyde:` line.
100
+ - Can also be passed via the `--intent` CLI flag or MCP `intent` parameter.
101
+
102
+ ```
103
+ intent: web page load times and Core Web Vitals
104
+ lex: performance
105
+ vec: how to improve performance
106
+ ```
107
+
108
+ ## Collection Scoping
109
+
110
+ Scope search to specific collections using `-c` (CLI) or `collections` (MCP/SDK):
111
+
112
+ ```bash
113
+ # CLI:
114
+ qmd query -c docs "how does auth work"
115
+ qmd query -c docs -c notes $'lex: auth\nvec: authentication flow'
116
+ ```
117
+
118
+ ## MCP Tool Call Payloads
119
+
120
+ When calling the `qmd` MCP server's `query` tool, provide a structured `searches` array:
121
+
122
+ ### Standard Multi-Modal Query
123
+
124
+ ```json
125
+ {
126
+ "searches": [
127
+ { "type": "lex", "query": "CAP theorem" },
128
+ { "type": "vec", "query": "consistency vs availability tradeoffs in distributed storage" }
129
+ ],
130
+ "collections": ["docs"],
131
+ "limit": 10
132
+ }
133
+ ```
134
+
135
+ ### Query with Disambiguating Intent
136
+
137
+ ```json
138
+ {
139
+ "searches": [
140
+ { "type": "lex", "query": "performance metrics" },
141
+ { "type": "vec", "query": "page load and network latency optimization" }
142
+ ],
143
+ "intent": "Front-end web performance and Core Web Vitals optimization",
144
+ "collections": ["frontend", "notes"],
145
+ "limit": 5
146
+ }
147
+ ```
148
+
149
+ ### Plain Query with Policy Control & Explain Trace
150
+
151
+ ```json
152
+ {
153
+ "query": "CAP theorem consistency",
154
+ "expansion": "auto",
155
+ "explain": true,
156
+ "collections": ["docs"]
157
+ }
158
+ ```