@tobilu/qmd 2.1.0 → 2.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tobilu/qmd",
3
- "version": "2.1.0",
3
+ "version": "2.5.2",
4
4
  "description": "Query Markup Documents - On-device hybrid search for markdown files with BM25, vector search, and LLM reranking",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,13 +17,23 @@
17
17
  "files": [
18
18
  "bin/",
19
19
  "dist/",
20
+ "skills/",
21
+ "scripts/build.mjs",
22
+ "scripts/check-package-grammars.mjs",
23
+ "scripts/package-smoke.mjs",
24
+ "scripts/test-all.mjs",
20
25
  "LICENSE",
21
26
  "CHANGELOG.md"
22
27
  ],
23
28
  "scripts": {
24
29
  "prepare": "[ -d .git ] && ./scripts/install-hooks.sh || true",
25
- "build": "tsc -p tsconfig.build.json && printf '#!/usr/bin/env node\n' | cat - dist/cli/qmd.js > dist/cli/qmd.tmp && mv dist/cli/qmd.tmp dist/cli/qmd.js && chmod +x dist/cli/qmd.js",
26
- "test": "vitest run --reporter=verbose test/",
30
+ "build": "node scripts/build.mjs",
31
+ "test": "node scripts/test-all.mjs",
32
+ "test:types": "node ./node_modules/typescript/bin/tsc -p tsconfig.build.json --noEmit",
33
+ "test:node": "node ./node_modules/vitest/vitest.mjs run --reporter=verbose --testTimeout 60000",
34
+ "test:bun": "bun test --timeout 60000 --preload ./src/test-preload.ts",
35
+ "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/",
36
+ "test:package": "node scripts/package-smoke.mjs",
27
37
  "qmd": "tsx src/cli/qmd.ts",
28
38
  "index": "tsx src/cli/qmd.ts index",
29
39
  "vector": "tsx src/cli/qmd.ts vector",
@@ -31,7 +41,8 @@
31
41
  "vsearch": "tsx src/cli/qmd.ts vsearch",
32
42
  "rerank": "tsx src/cli/qmd.ts rerank",
33
43
  "inspector": "npx @modelcontextprotocol/inspector tsx src/cli/qmd.ts mcp",
34
- "release": "./scripts/release.sh"
44
+ "release": "./scripts/release.sh",
45
+ "smoke:package-grammars": "node scripts/check-package-grammars.mjs"
35
46
  },
36
47
  "publishConfig": {
37
48
  "access": "public"
@@ -46,13 +57,17 @@
46
57
  },
47
58
  "dependencies": {
48
59
  "@modelcontextprotocol/sdk": "1.29.0",
49
- "better-sqlite3": "12.8.0",
60
+ "better-sqlite3": "12.10.0",
50
61
  "fast-glob": "3.3.3",
51
62
  "node-llama-cpp": "3.18.1",
52
63
  "picomatch": "4.0.4",
53
64
  "sqlite-vec": "0.1.9",
54
- "web-tree-sitter": "0.26.7",
55
- "yaml": "2.8.3",
65
+ "tree-sitter-go": "0.25.0",
66
+ "tree-sitter-python": "0.25.0",
67
+ "tree-sitter-rust": "0.24.0",
68
+ "tree-sitter-typescript": "0.23.2",
69
+ "web-tree-sitter": "0.26.8",
70
+ "yaml": "2.9.0",
56
71
  "zod": "4.2.1"
57
72
  },
58
73
  "optionalDependencies": {
@@ -60,11 +75,7 @@
60
75
  "sqlite-vec-darwin-x64": "0.1.9",
61
76
  "sqlite-vec-linux-arm64": "0.1.9",
62
77
  "sqlite-vec-linux-x64": "0.1.9",
63
- "sqlite-vec-windows-x64": "0.1.9",
64
- "tree-sitter-go": "0.23.4",
65
- "tree-sitter-python": "0.23.4",
66
- "tree-sitter-rust": "0.24.0",
67
- "tree-sitter-typescript": "0.23.2"
78
+ "sqlite-vec-windows-x64": "0.1.9"
68
79
  },
69
80
  "devDependencies": {
70
81
  "@types/better-sqlite3": "7.6.13",
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { chmodSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = join(fileURLToPath(new URL("..", import.meta.url)));
8
+
9
+ function run(command, args, options = {}) {
10
+ const result = spawnSync(command, args, {
11
+ cwd: root,
12
+ stdio: "inherit",
13
+ shell: process.platform === "win32",
14
+ ...options,
15
+ });
16
+ if (result.status !== 0) {
17
+ process.exit(result.status ?? 1);
18
+ }
19
+ }
20
+
21
+ run(process.execPath, [join(root, "node_modules", "typescript", "bin", "tsc"), "-p", "tsconfig.build.json"]);
22
+
23
+ const cliPath = join(root, "dist", "cli", "qmd.js");
24
+ const tmpPath = `${cliPath}.tmp`;
25
+ const built = readFileSync(cliPath, "utf8");
26
+ const withoutExistingShebang = built.startsWith("#!") ? built.slice(built.indexOf("\n") + 1) : built;
27
+ writeFileSync(tmpPath, `#!/usr/bin/env node\n${withoutExistingShebang}`);
28
+ renameSync(tmpPath, cliPath);
29
+ chmodSync(cliPath, 0o755);
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ const grammars = [
7
+ "tree-sitter-typescript/tree-sitter-typescript.wasm",
8
+ "tree-sitter-typescript/tree-sitter-tsx.wasm",
9
+ "tree-sitter-python/tree-sitter-python.wasm",
10
+ "tree-sitter-go/tree-sitter-go.wasm",
11
+ "tree-sitter-rust/tree-sitter-rust.wasm",
12
+ ];
13
+
14
+ let ok = true;
15
+ for (const grammar of grammars) {
16
+ try {
17
+ const resolved = require.resolve(grammar);
18
+ console.log(`ok ${grammar} -> ${resolved}`);
19
+ } catch (err) {
20
+ ok = false;
21
+ console.error(`missing ${grammar}`);
22
+ console.error(err instanceof Error ? err.message : String(err));
23
+ }
24
+ }
25
+
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`.");
28
+ process.exit(1);
29
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = fileURLToPath(new URL("..", import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
9
+
10
+ function run(label, command, args, options = {}) {
11
+ console.log(`==> ${label}`);
12
+ const { quiet, ...spawnOptions } = options;
13
+ const result = spawnSync(command, args, {
14
+ cwd: root,
15
+ stdio: quiet ? "pipe" : "inherit",
16
+ shell: process.platform === "win32",
17
+ ...spawnOptions,
18
+ });
19
+ if (result.status !== 0) {
20
+ console.error(`Package smoke failed: ${label}`);
21
+ if (quiet) {
22
+ if (result.stdout) process.stderr.write(result.stdout);
23
+ if (result.stderr) process.stderr.write(result.stderr);
24
+ }
25
+ process.exit(result.status ?? 1);
26
+ }
27
+ }
28
+
29
+ function assertPath(path, label = path) {
30
+ const full = join(root, path);
31
+ if (!existsSync(full)) {
32
+ console.error(`Package smoke failed: missing ${label} (${path})`);
33
+ process.exit(1);
34
+ }
35
+ return full;
36
+ }
37
+
38
+ run("build compiled package", process.execPath, ["scripts/build.mjs"]);
39
+ run("AST grammar runtime packages", process.execPath, ["scripts/check-package-grammars.mjs"]);
40
+
41
+ for (const entry of pkg.files ?? []) {
42
+ assertPath(entry.replace(/\/$/, ""), `package.json files[] entry ${entry}`);
43
+ }
44
+
45
+ for (const [name, binPath] of Object.entries(pkg.bin ?? {})) {
46
+ const full = assertPath(binPath, `bin ${name}`);
47
+ const mode = statSync(full).mode;
48
+ if ((mode & 0o111) === 0) {
49
+ console.error(`Package smoke failed: bin ${name} is not executable (${binPath})`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ assertPath("dist/index.js", "compiled main export");
55
+ assertPath("dist/index.d.ts", "compiled type export");
56
+ assertPath("dist/cli/qmd.js", "compiled CLI");
57
+
58
+ run("compiled CLI under Node", process.execPath, ["dist/cli/qmd.js", "--help"], { quiet: true });
59
+ run("package wrapper", "sh", ["bin/qmd", "--help"], { quiet: true });
60
+
61
+ if (process.env.QMD_SKIP_BUN_SMOKE === "1") {
62
+ console.log("==> compiled CLI under Bun (skipped by QMD_SKIP_BUN_SMOKE=1)");
63
+ } else {
64
+ run("compiled CLI under Bun", "bun", ["dist/cli/qmd.js", "--help"], { quiet: true });
65
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = fileURLToPath(new URL("..", import.meta.url));
7
+
8
+ function run(label, command, args, options = {}) {
9
+ console.log(`==> ${label}`);
10
+ const { env: extraEnv, ...spawnOptions } = options;
11
+ const result = spawnSync(command, args, {
12
+ cwd: root,
13
+ stdio: "inherit",
14
+ shell: process.platform === "win32",
15
+ env: { ...process.env, ...(extraEnv ?? {}) },
16
+ ...spawnOptions,
17
+ });
18
+ if (result.status !== 0) {
19
+ console.error(`Test task failed: ${label}`);
20
+ process.exit(result.status ?? 1);
21
+ }
22
+ }
23
+
24
+ run("TypeScript build typecheck", process.execPath, [join(root, "node_modules", "typescript", "bin", "tsc"), "-p", "tsconfig.build.json", "--noEmit"]);
25
+ run("Vitest suite under Node", process.execPath, [join(root, "node_modules", "vitest", "vitest.mjs"), "run", "--reporter=verbose", "--testTimeout", "60000", "test/"], { env: { CI: "true" } });
26
+ run("Bun test suite", "bun", ["test", "--timeout", "60000", "--preload", "./src/test-preload.ts", "test/"], { env: { CI: "true" } });
27
+ run("Package smoke", process.execPath, ["scripts/package-smoke.mjs"]);
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: qmd
3
+ description: Search local markdown knowledge bases, notes, docs, and wikis with QMD. Use when users ask to find notes, retrieve documents, inspect a wiki, answer from indexed markdown, or set up QMD access.
4
+ license: MIT
5
+ compatibility: Requires qmd CLI or MCP server. Install via `npm install -g @tobilu/qmd`.
6
+ metadata:
7
+ author: tobi
8
+ version: "2.1.0"
9
+ allowed-tools: Bash(qmd:*), mcp__qmd__*
10
+ ---
11
+
12
+ # QMD - Query Markdown Documents
13
+
14
+ ## How search works
15
+
16
+ QMD searches local markdown collections: notes, docs, wikis, transcripts, and
17
+ project knowledge bases. Use it before web search when the answer may already be
18
+ in indexed local files.
19
+
20
+ The workflow is always:
21
+
22
+ 1. Search for candidate documents.
23
+ 2. Retrieve the full source with `qmd get` or `qmd multi-get`.
24
+ 3. Answer from retrieved text, citing paths or docids.
25
+
26
+ Do not answer from snippets alone when the user needs facts, decisions, quotes,
27
+ or nuance. Snippets are only leads.
28
+
29
+ Typical loop:
30
+
31
+ ```bash
32
+ qmd search "merchant reality support interviews" -n 5
33
+ # leads: #abc123 concepts/customer-proximity.md; #def432 sources/merchant-call.md
34
+ qmd multi-get "#abc123,#def432" --md
35
+ ```
36
+
37
+ For harder searches, use `qmd query` structured queries with `intent:`, `lex:`,
38
+ `vec:`, and `hyde:` fields.
39
+
40
+ When reporting what you retrieved, a compact note is enough; do not paste whole
41
+ files unless needed:
42
+
43
+ ```text
44
+ Retrieved:
45
+ - #abc123 concepts/customer-proximity.md
46
+ - #def432 sources/merchant-call.md
47
+ ```
48
+
49
+ ## Pick the right search mode
50
+
51
+ Use **BM25 lexical search** when you know exact words, titles, names, code
52
+ symbols, or rare phrases:
53
+
54
+ ```bash
55
+ qmd search "cockpit OKR Goodhart" -n 10
56
+ qmd search '"AI Before Headcount"' -c concepts -n 5
57
+ ```
58
+
59
+ Use **hybrid semantic search** when the user describes an idea indirectly, uses
60
+ different wording than the source, or needs conceptual recall:
61
+
62
+ ```bash
63
+ qmd query "decision quality depends on surfacing assumptions and context" -n 10
64
+ qmd query --json --explain "metrics as cockpit instruments but not OKRs"
65
+ ```
66
+
67
+ Use **structured queries** for hard searches. They combine exact anchors with
68
+ semantic recall:
69
+
70
+ ```bash
71
+ qmd query $'intent: Find the concept note about metrics as instruments without letting OKRs replace judgment.\nlex: cockpit instruments OKR Goodhart metrics judgment\nvec: data informed not metric driven product judgment\nhyde: A concept note says metrics are useful like cockpit instruments, but leaders should remain data-informed rather than metric-driven because OKRs and dashboards can Goodhart product judgment.'
72
+ ```
73
+
74
+ Structured query fields:
75
+
76
+ - `intent:` states what you are trying to find and what to avoid.
77
+ - `lex:` uses exact terms, aliases, titles, and rare words.
78
+ - `vec:` paraphrases the idea in natural language.
79
+ - `hyde:` describes the document or answer that would satisfy the request.
80
+
81
+ If `qmd query` is slow or model/GPU setup fails, fall back to `qmd search` with
82
+ better lexical terms.
83
+
84
+ ## Retrieve sources
85
+
86
+ Search results include docids like `#abc123` and `qmd://...` paths. Fetch them:
87
+
88
+ ```bash
89
+ qmd get "#abc123"
90
+ qmd get qmd://concepts/ai-before-headcount.md --full
91
+ qmd multi-get "#abc123,#def432" --md
92
+ qmd multi-get 'concepts/{ai-before-headcount.md,data-informed-not-metric-driven.md}' --md
93
+ qmd multi-get 'sources/podcast-2025-*.md' -l 80
94
+ ```
95
+
96
+ Use `multi-get` when comparing several hits or gathering context across pages.
97
+ Use `--full` when the exact source matters.
98
+
99
+ ## Discover what is indexed
100
+
101
+ ```bash
102
+ qmd collection list
103
+ qmd ls
104
+ qmd status
105
+ ```
106
+
107
+ Add collection filters when broad searches drift into the wrong corpus:
108
+
109
+ ```bash
110
+ qmd search "headcount autonomous agents" -c concepts -n 10
111
+ qmd query "merchant support product reality" -c concepts -c sources -n 10
112
+ ```
113
+
114
+ Omit `-c` to search everything.
115
+
116
+ ## MCP Tool: `query`
117
+
118
+ When using the MCP server, prefer structured searches:
119
+
120
+ ```json
121
+ {
122
+ "searches": [
123
+ { "type": "lex", "query": "cockpit OKR Goodhart" },
124
+ { "type": "vec", "query": "data informed not metric driven product judgment" },
125
+ { "type": "hyde", "query": "A concept note explains that metrics are useful as instruments, but leaders should not let OKRs or dashboards replace judgment." }
126
+ ],
127
+ "intent": "Find the concept note about using metrics as instruments without becoming metric-driven.",
128
+ "collections": ["concepts"],
129
+ "limit": 10
130
+ }
131
+ ```
132
+
133
+ Query types:
134
+
135
+ - `lex` — BM25 keyword search. Best for exact terms, names, titles, and code.
136
+ - `vec` — vector semantic search. Best for natural-language concepts.
137
+ - `hyde` — vector search using a hypothetical answer/document passage.
138
+
139
+ ## Query craft
140
+
141
+ Good QMD searches mix three things:
142
+
143
+ 1. **Title/alias anchors:** exact page titles, named entities, phrases.
144
+ 2. **Semantic paraphrase:** how a human would describe the idea.
145
+ 3. **Negative space:** enough intent to avoid nearby-but-wrong concepts.
146
+
147
+ Examples:
148
+
149
+ ```bash
150
+ # Exact-ish title lookup
151
+ qmd search '"arm the rebels" merchants tools big companies' -c concepts
152
+
153
+ # Semantic concept lookup
154
+ qmd query $'intent: Find the customer proximity concept, not generic customer delight.\nlex: support pseudonymous merchant customer interviews\nvec: founder stays close to merchant reality through support and product use'
155
+
156
+ # Source lookup
157
+ qmd search "six-week cadence WhatsApp merchant relationships Shawn Ryan" -c sources -n 10
158
+ ```
159
+
160
+ ## Setup and maintenance
161
+
162
+ Only mutate indexes when the user asked for setup or maintenance. Searching and
163
+ retrieving are safe; collection/index mutation is not a casual first step.
164
+
165
+ ```bash
166
+ npm install -g @tobilu/qmd
167
+ qmd collection add ~/notes --name notes
168
+ qmd update
169
+ qmd embed
170
+ ```
171
+
172
+ Health and diagnostics:
173
+
174
+ ```bash
175
+ qmd doctor
176
+ qmd status
177
+ qmd pull
178
+ ```
179
+
180
+ `qmd doctor` checks config, model cache, device/GPU setup, vector fingerprints,
181
+ and common environment overrides. If a model-backed command fails, run it before
182
+ changing configuration.
183
+
184
+ ## MCP setup
185
+
186
+ See `references/mcp-setup.md` for Claude Code, Claude Desktop, OpenClaw, and HTTP
187
+ server configuration.
188
+
189
+ ## Pitfalls
190
+
191
+ - **Do not stop at snippets.** Fetch documents before making claims.
192
+ - **Do not overuse semantic search.** If you know exact titles or terms, BM25 is
193
+ faster and often better.
194
+ - **Do not mutate indexes casually.** `qmd collection add`, `qmd update`, and
195
+ `qmd embed` change local state and can be expensive.
196
+ - **Model-backed commands can be environment-sensitive.** If `qmd query`,
197
+ `qmd vsearch`, or reranking fails because local models/GPU are unavailable,
198
+ use `qmd search` and stronger lexical/structured terms.
199
+ - **Ambiguous user wording needs intent.** Add `intent:` rather than hoping query
200
+ expansion guesses the right domain.
201
+ - **Collection names matter.** Search `concepts` for synthesized wiki pages,
202
+ `sources` for transcripts/raw source pages, and docs collections for code or
203
+ project documentation.
@@ -0,0 +1,102 @@
1
+ # QMD MCP Server Setup
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm install -g @tobilu/qmd
7
+ qmd collection add ~/path/to/markdown --name myknowledge
8
+ qmd embed
9
+ ```
10
+
11
+ ## Configure MCP Client
12
+
13
+ **Claude Code** (`~/.claude/settings.json`):
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "qmd": { "command": "qmd", "args": ["mcp"] }
18
+ }
19
+ }
20
+ ```
21
+
22
+ **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
23
+ ```json
24
+ {
25
+ "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"] }
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## HTTP Mode
43
+
44
+ ```bash
45
+ qmd mcp --http # Port 8181
46
+ qmd mcp --http --daemon # Background
47
+ qmd mcp stop # Stop daemon
48
+ ```
49
+
50
+ ## Tools
51
+
52
+ ### structured_search
53
+
54
+ Search with pre-expanded queries.
55
+
56
+ ```json
57
+ {
58
+ "searches": [
59
+ { "type": "lex", "query": "keyword phrases" },
60
+ { "type": "vec", "query": "natural language question" },
61
+ { "type": "hyde", "query": "hypothetical answer passage..." }
62
+ ],
63
+ "limit": 10,
64
+ "collection": "optional",
65
+ "minScore": 0.0
66
+ }
67
+ ```
68
+
69
+ | Type | Method | Input |
70
+ |------|--------|-------|
71
+ | `lex` | BM25 | Keywords (2-5 terms) |
72
+ | `vec` | Vector | Question |
73
+ | `hyde` | Vector | Answer passage (50-100 words) |
74
+
75
+ ### get
76
+
77
+ Retrieve document by path or `#docid`.
78
+
79
+ | Param | Type | Description |
80
+ |-------|------|-------------|
81
+ | `path` | string | File path or `#docid` |
82
+ | `full` | bool? | Return full content |
83
+ | `lineNumbers` | bool? | Add line numbers |
84
+
85
+ ### multi_get
86
+
87
+ Retrieve multiple documents.
88
+
89
+ | Param | Type | Description |
90
+ |-------|------|-------------|
91
+ | `pattern` | string | Glob or comma-separated list |
92
+ | `maxBytes` | number? | Skip large files (default 10KB) |
93
+
94
+ ### status
95
+
96
+ Index health and collections. No params.
97
+
98
+ ## Troubleshooting
99
+
100
+ - **Not starting**: `which qmd`, `qmd mcp` manually
101
+ - **No results**: `qmd collection list`, `qmd embed`
102
+ - **Slow first search**: Normal, models loading (~3GB)
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: release
3
+ description: Manage releases for this project. Validates changelog, installs git hooks, and cuts releases. Use when user says "/release", "release 1.0.5", "cut a release", or asks about the release process. NOT auto-invoked by the model.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Release
8
+
9
+ Cut a release, validate the changelog, and ensure git hooks are installed.
10
+
11
+ ## Usage
12
+
13
+ `/release 1.0.5` or `/release patch` (bumps patch from current version).
14
+
15
+ ## Process
16
+
17
+ When the user triggers `/release <version>`:
18
+
19
+ 1. **Gather context** — run `skills/release/scripts/release-context.sh <version>`.
20
+ This silently installs git hooks and prints everything needed: version info,
21
+ working directory status, commits since last release, files changed, current
22
+ `[Unreleased]` content, and the previous release entry for style reference.
23
+
24
+ 2. **Commit outstanding work** — if the context shows staged, modified, or
25
+ untracked files that belong in this release, commit them first. Use the
26
+ /commit skill or make well-formed commits directly.
27
+
28
+ 3. **Write the changelog** — if `[Unreleased]` is empty, write it now using
29
+ the commits and file changes from the context output. Follow the changelog
30
+ standard below. Re-run the context script after committing if needed.
31
+
32
+ 4. **Cut the release** — run `scripts/release.sh <version>`. This renames
33
+ `[Unreleased]` → `[X.Y.Z] - date`, inserts a fresh `[Unreleased]`,
34
+ bumps `package.json`, commits, and tags.
35
+
36
+ 5. **Show the final changelog** — print the full `[Unreleased]` +
37
+ minor series rollup via `scripts/extract-changelog.sh <version>`.
38
+ Ask the user to confirm before pushing.
39
+
40
+ 6. **Push** — after explicit confirmation, run `git push origin main --tags`.
41
+
42
+ 7. **Watch CI** — after the push, start a background dispatch to watch the
43
+ publish workflow. Use `interactive_shell` in dispatch mode with:
44
+ ```
45
+ gh run watch $(gh run list --workflow=publish.yml --limit=1 --json databaseId --jq '.[0].databaseId') --exit-status
46
+ ```
47
+ The agent will be notified when CI completes and should report the result.
48
+
49
+ 7. **Check dependency updates** — before cutting the release, check for
50
+ updates to `sqlite-vec` (and platform packages), `node-llama-cpp`,
51
+ and `better-sqlite3`. Run `pnpm outdated` and report any available
52
+ updates for these packages. If updates exist, bump them (pinned, no
53
+ `^` ranges) and re-run tests before proceeding.
54
+
55
+ If any step fails, stop and explain. Never force-push or skip validation.
56
+
57
+ ## Dependency Policy
58
+
59
+ All dependencies must be pinned to exact versions (no `^` or `~` ranges).
60
+ The lockfile ensures reproducible installs. When adding or updating any
61
+ dependency, always use the exact version string (e.g. `"3.18.1"` not
62
+ `"^3.18.1"`).
63
+
64
+ ## Changelog Standard
65
+
66
+ The changelog lives in `CHANGELOG.md` and follows [Keep a Changelog](https://keepachangelog.com/) conventions.
67
+
68
+ ### Heading format
69
+
70
+ - `## [Unreleased]` — accumulates entries between releases
71
+ - `## [X.Y.Z] - YYYY-MM-DD` — released versions
72
+
73
+ ### Structure of a release entry
74
+
75
+ Each version entry has two parts:
76
+
77
+ **1. Highlights (optional, 1-4 sentences of prose)**
78
+
79
+ Immediately after the version heading, before any `###` section. The elevator
80
+ pitch — what would you tell someone in 30 seconds? Only for significant
81
+ releases; skip for small patches.
82
+
83
+ ```markdown
84
+ ## [1.1.0] - 2026-03-01
85
+
86
+ QMD now runs on both Node.js and Bun, with up to 2.7x faster reranking
87
+ through parallel contexts. GPU auto-detection replaces the unreliable
88
+ `gpu: "auto"` with explicit CUDA/Metal/Vulkan probing.
89
+ ```
90
+
91
+ **2. Detailed changelog (`### Changes` and `### Fixes`)**
92
+
93
+ ```markdown
94
+ ### Changes
95
+
96
+ - Runtime: support Node.js (>=22) alongside Bun. The `qmd` wrapper
97
+ auto-detects a suitable install via PATH. #149 (thanks @igrigorik)
98
+ - Performance: parallel embedding & reranking — up to 2.7x faster on
99
+ multi-core machines.
100
+
101
+ ### Fixes
102
+
103
+ - Prevent VRAM waste from duplicate context creation during concurrent
104
+ `embedBatch` calls. #152 (thanks @jkrems)
105
+ ```
106
+
107
+ ### Writing guidelines
108
+
109
+ - **Explain the why, not just the what.** The changelog is for users.
110
+ - **Include numbers.** "2.7x faster", "17x less memory".
111
+ - **Group by theme, not by file.** "Performance" not "Changes to llm.ts".
112
+ - **Don't list every commit.** Aggregate related changes.
113
+ - **Credit contributors:** end bullets with `#NNN (thanks @username)` for
114
+ external PRs. No need to credit the repo owner.
115
+
116
+ ### What not to include
117
+
118
+ - Internal refactors with no user-visible effect
119
+ - Dependency bumps (unless fixing a user-facing bug)
120
+ - CI/tooling changes (unless affecting the release artifact)
121
+ - Test additions (unless validating a fix worth mentioning)
122
+
123
+ ## GitHub Release Notes
124
+
125
+ Each GitHub release includes the full changelog for the **minor series** back
126
+ to x.x.0. The `scripts/extract-changelog.sh` script handles this, and the
127
+ publish workflow (`publish.yml`) calls it to populate the GitHub release.
128
+
129
+ ## Git Hooks
130
+
131
+ The pre-push hook (`scripts/pre-push`) blocks `v*` tag pushes unless:
132
+
133
+ 1. `package.json` version matches the tag
134
+ 2. `CHANGELOG.md` has a `## [X.Y.Z] - date` entry for the version
135
+ 3. CI passed on GitHub (warns in non-interactive shells, blocks in terminals)
136
+
137
+ Hooks are installed silently by the context script. They can also be installed
138
+ manually via `skills/release/scripts/install-hooks.sh` or automatically via
139
+ `bun install` (prepare script).