ko-qmd 2.8.3-ko.10 → 2.8.3-ko.11

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,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.8.3-ko.11] - 2026-10-31
6
+
7
+ - Rerank now caps each document at 256 tokens by default
8
+ (`DEFAULT_RERANK_MAX_DOC_TOKENS` in `src/llm.ts`); `QMD_RERANK_MAX_DOC_TOKENS=0`
9
+ restores whole chunks, and an unset or invalid value takes the default. Replaying
10
+ 29 rerank queries from the daemon query log with the rerank cache cleared
11
+ (M3 Max): query p50 5323 → 1682 ms, p90 8508 → 2668 ms, rerank stage p50
12
+ 4968 → 1343 ms. ko-vault seam bench: `full_mrr` 0.9440 → 0.9451, hard line
13
+ unchanged (`full_r1`=0.6630). The cap is part of the rerank cache key, so
14
+ uncapped cache rows miss once after upgrading.
15
+ - Release: the publish workflow runs the package smoke test before
16
+ `npm publish`, so a tarball that fails to install or start never ships.
17
+ - CI: the Windows git-install job installs the pushed ref instead of `main`;
18
+ `docs/vault-search/WINDOWS.md` records the accepted Windows test failures.
19
+ - Deps: zod 4.6.7, oxlint 1.88.0, actions/checkout and setup-node v7.
20
+ - Docs: REFERENCE documents the memory MCP tools and `grep`, and corrects the
21
+ `multi_get` `maxBytes` default to 65536.
22
+
5
23
  ## [2.8.3-ko.10] - 2026-10-15
6
24
 
7
25
  ko-qmd 2.8.3-ko.10 adds a shared working memory for agents: three MCP tools
package/README.md CHANGED
@@ -79,8 +79,9 @@ flowchart LR
79
79
  그 경로의 `score` 는 1/순위라 임계값을 걸 수 없기 때문이다.
80
80
  - **모델 상주** — `QMD_LLM_IDLE_TIMEOUT_MS=0` 이면 모델을 내리지 않는다. 기본은 5분 idle 뒤 내린다.
81
81
  - **리랭크 입력 cap** — 리랭커에 보내는 문서당 토큰을 자른다. 데몬 전체는 `QMD_RERANK_MAX_DOC_TOKENS`,
82
- 요청 하나는 REST `rerankMaxDocTokens` 로 건다(요청 값이 우선). KB 골드셋에서 128토큰 cap 은 품질을 지키면서
83
- 콜드 p90 을 3.6s → 0.9s 로 줄였다(Apple M3 Max, 후보 15).
82
+ 요청 하나는 REST `rerankMaxDocTokens` 로 건다(요청 값이 우선). 기본은 256토큰이고 `QMD_RERANK_MAX_DOC_TOKENS=0` 이면
83
+ 자르지 않는다. 요청 값은 양의 정수만 받으므로 요청 하나에서 cap 을 풀려면 큰 값(예: 100000)을 보낸다. 데몬 쿼리 29건 재생에서 256 cap 은 쿼리 p50 을 5.3s → 1.7s 로 줄였고 ko-vault bench 지표는 같거나
84
+ 올랐다(Apple M3 Max, 리랭크 캐시 비움). KB 골드셋에서는 128토큰도 품질을 지켰다(콜드 p90 3.6s → 0.9s, 후보 15).
84
85
 
85
86
  ## 설치
86
87
 
@@ -171,7 +172,7 @@ API 전체: [docs/REFERENCE.md § SDK / Library Usage](docs/REFERENCE.md#sdk--li
171
172
  | `QMD_LLM_IDLE_TIMEOUT_MS` | 모델을 내리기까지 idle ms (`0` = 내리지 않음) |
172
173
  | `QMD_LLM_LOAD_TIMEOUT_MS` | 모델 로드 대기의 상한 ms (기본 120000, `0` = 무한 대기). 넘으면 그 단계 없이 답하고 로드는 백그라운드에서 계속된다 |
173
174
  | `QMD_RERANK_CASCADE` | `0`/`false`/`off`/`no` 면 리랭크 cascade를 끄고 후보 전체를 한 번에 채점한다 |
174
- | `QMD_RERANK_MAX_DOC_TOKENS` | 리랭커에 보내는 문서당 토큰 cap (요청의 `rerankMaxDocTokens` 가 우선) |
175
+ | `QMD_RERANK_MAX_DOC_TOKENS` | 리랭커에 보내는 문서당 토큰 cap (기본 256, `0` = 자르지 않음, 요청의 `rerankMaxDocTokens` 가 우선) |
175
176
  | `QMD_RERANK_CONTEXT_SIZE` · `QMD_EMBED_CONTEXT_SIZE` · `QMD_EXPAND_CONTEXT_SIZE` | 역할별 context 크기 |
176
177
  | `QMD_EMBED_PARALLELISM` | 병렬 context 수 (높으면 RAM/VRAM 고갈) |
177
178
  | `QMD_LLAMA_GPU` · `QMD_FORCE_CPU` | GPU 백엔드 선택·끄기 |
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "0f8df3f",
3
- "builtAt": "2026-10-15T04:39:30.671Z"
2
+ "commit": "ad0bc45",
3
+ "builtAt": "2026-10-30T16:53:21.509Z"
4
4
  }
package/dist/cli/qmd.js CHANGED
@@ -145,7 +145,7 @@ Then follow those instructions. In short: search first, fetch full sources with
145
145
  --- ${h.relativePath} ---
146
146
  `),process.stdout.write(h.content.endsWith(`
147
147
  `)?h.content:h.content+`
148
- `)});return}case"path":{const i=e[1];if(!i){const $=ao(!0);t?De({success:!0,data:{paths:$}}):$.forEach(p=>console.log(p));return}const m=Ge(i,!0);if(!m)throw new Error(`Skill not found: ${i}`);t?De({success:!0,data:{name:m.name,path:m.dir}}):console.log(m.dir);return}case"help":{po();return}default:throw new Error(`Unknown skills subcommand: ${r}`)}}a(or,"runSkillsCommand");function po(){console.log("Usage: qmd skills <list|get|path> [options]"),console.log(""),console.log("Commands:"),console.log(" list List bundled runtime skills"),console.log(" get <name> Print a bundled runtime skill"),console.log(" get <name> --full Include references/templates/scripts"),console.log(" get --all Print all bundled runtime skills"),console.log(" path [name] Print runtime skill directory path(s)"),console.log(""),console.log("Options:"),console.log(" --json Print structured JSON")}a(po,"showSkillsHelp");function nr(e,t,o){const s=X(e);if(pt(s)){const l=se(X(t)),i=se(s);if(l===i)return!1}const r=kt(s,t)||".";if(Pe(s,{recursive:!0}),pt(e)){if(Ke(e).isSymbolicLink()&&Fo(e)===r)return!0;if(!o)throw new Error(`Claude skill path already exists: ${e} (use --force to replace it)`);io(e)}return Ao(r,e,"dir"),!0}a(nr,"ensureClaudeSymlink");async function sr(e,t){if(t)return!0;if(!process.stdin.isTTY||!process.stdout.isTTY)return console.log(`Tip: create a Claude symlink manually at ${e}`),!1;const o=Qo({input:process.stdin,output:process.stdout});try{const r=(await o.question(`Create a symlink in ${e}? [y/N] `)).trim().toLowerCase();return r==="y"||r==="yes"}finally{o.close()}}a(sr,"shouldCreateClaudeSymlink");async function rr(e,t,o){const s=zs(e);tr(s,t),console.log(`\u2713 Installed QMD skill to ${s}`);const r=Js(e);if(!await sr(r,o))return;const l=nr(r,s,t);console.log(l?`\u2713 Linked Claude skill at ${r}`:`\u2713 Claude already sees the skill via ${X(r)}`)}a(rr,"installSkill");function lr(){console.log("qmd \u2014 Quick Markdown Search"),console.log(""),console.log("Usage:"),console.log(" qmd <command> [options]"),console.log(""),console.log("Primary commands:"),console.log(" qmd query <query> - Hybrid search with auto expansion + reranking (recommended)"),console.log(" qmd query 'lex:..\\nvec:...' - Structured query document (you provide lex/vec/hyde lines)"),console.log(" qmd search <query> - Full-text BM25 keywords (no LLM)"),console.log(" qmd vsearch <query> - Vector similarity only"),console.log(" qmd grep <pattern> - Exact/regex match over indexed bodies (no ranking)"),console.log(" qmd get <file>[:from[:count]] - Show a document (line-numbered; #docid in header)"),console.log(" qmd multi-get <pattern> - Batch fetch via glob or comma-separated list"),console.log(" qmd skills list/get/path - List and retrieve bundled runtime skills"),console.log(" qmd skill show/install - Show or install the QMD skill"),console.log(" qmd mcp - Start the MCP server (stdio transport for AI agents)"),console.log(" qmd bench <fixture.json> - Run search quality benchmarks against a fixture file"),console.log(""),console.log("Collections & context:"),console.log(" qmd collection add/list/remove/rename/show - Manage indexed folders"),console.log(" qmd context add/list/rm - Attach human-written summaries"),console.log(" qmd ls [collection[/path]] - Inspect indexed files"),console.log(""),console.log("Maintenance:"),console.log(" qmd init - Create a project-local .qmd index"),console.log(" qmd status - View index + collection health"),console.log(" qmd update [--pull] - Re-index collections (optionally git pull first)"),console.log(" qmd trust [list|revoke] - Approve a checked-in .qmd config's hooks/paths/models"),console.log(" qmd embed [-f] [-c <name>] - Generate/refresh vector embeddings"),console.log(" --max-docs-per-batch <n> - Cap docs loaded into memory per embedding batch"),console.log(" --max-batch-mb <n> - Cap UTF-8 MB loaded into memory per embedding batch"),console.log(" --timeout <minutes> - Embed session cap in minutes (0 = no limit; default 30)"),console.log(" qmd pull [--refresh] [--progress] - Download embedding/generation/rerank models"),console.log(" qmd cleanup [--dry-run] - Drop inactive docs/orphans, compact FTS, vacuum"),console.log(""),console.log("Query syntax (qmd query):"),console.log(" QMD queries are either a single expand query (no prefix) or a multi-line"),console.log(" document where every line is typed with lex:, vec:, or hyde:. This grammar"),console.log(" matches the docs in docs/SYNTAX.md and is enforced in the CLI."),console.log("");const e=["query = expand_query | query_document ;","expand_query = text | explicit_expand ;",'explicit_expand= "expand:" text ;',"query_document = [ intent_line ] { typed_line } ;",'intent_line = "intent:" text newline ;','typed_line = type ":" text newline ;','type = "lex" | "vec" | "hyde" ;',"text = quoted_phrase | plain_text ;",`quoted_phrase = '"' { character } '"' ;`,"plain_text = { character } ;",'newline = "\\n" ;'];console.log(" Grammar:");for(const t of e)console.log(` ${t}`);console.log(""),console.log(" Examples:"),console.log(' qmd query "how does auth work" # single-line \u2192 implicit expand'),console.log(" qmd query $'lex: CAP theorem\\nvec: consistency' # typed query document"),console.log(` qmd query $'lex: "exact matches" sports -baseball' # phrase + negation lex search`),console.log(" qmd query $'hyde: Hypothetical answer text' # hyde-only document"),console.log(""),console.log(" Constraints:"),console.log(" - Standalone expand queries cannot mix with typed lines."),console.log(" - Query documents allow only lex:, vec:, hyde:, or intent: prefixes."),console.log(" - Each typed line must be single-line text with balanced quotes."),console.log(""),console.log("AI agents & integrations:"),console.log(" - Run `qmd mcp` to expose the MCP server (stdio) to agents/IDEs."),console.log(" - Run `qmd skills get qmd --full` for version-matched agent instructions."),console.log(" - `qmd skill install` installs the QMD skill into ./.agents/skills/qmd."),console.log(" - Use `qmd skill install --global` for ~/.agents/skills/qmd."),console.log(" - `qmd --skill` is kept as an alias for `qmd skill show`."),console.log(" - Advanced: `qmd mcp --http ...` and `qmd mcp --http --daemon` are optional for custom transports."),console.log(""),console.log("Global options:"),console.log(" --index <name> - Use a named index (default: index)"),console.log(" QMD_EDITOR_URI - Editor link template for clickable TTY search output"),console.log(""),console.log("Search options:"),console.log(" -n <num> - Max results (default 5, or 20 for --format files|json)"),console.log(" --all - Return all matches (pair with --min-score)"),console.log(" --min-score <num> - Minimum similarity score"),console.log(" --full - Output full document instead of snippet"),console.log(" -C, --candidate-limit <n> - Max candidates to rerank (default 40, lower = faster)"),console.log(" --no-rerank - Skip LLM reranking (use RRF scores only, much faster on CPU)"),console.log(" --no-gpu - Force CPU mode for llama.cpp operations (same as QMD_FORCE_CPU=1)"),console.log(" --line-numbers - Include line numbers (search; get/multi-get are on by default)"),console.log(" --no-line-numbers - Disable line numbers for get/multi-get"),console.log(" --full-path - Show on-disk paths instead of qmd:// + docid (get/multi-get/search/query)"),console.log(" Paths are ./-prefixed when under $PWD, absolute otherwise"),console.log(" Results whose file is gone keep qmd:// + docid and warn on stderr"),console.log(" --explain - Include retrieval score traces (query, CLI/--format json)"),console.log(" --format <kind> - Output format: cli (default) | json | csv | md | xml | files"),console.log(" -c, --collection <name> - Filter by one or more collections"),console.log(" --path <glob> - Only documents matching the glob; repeatable, '!' excludes"),console.log(" Matched against collection/path, as printed in results"),console.log(" --since <when> - Only documents modified at or after (7d, 3h, 2w, 2026-09-01)"),console.log(" --until <when> - Only documents modified at or before (same formats)"),console.log(" --filter <json> - Metadata filter (recursive JSON AST; search/vsearch/query/grep)"),console.log(` e.g. '{"key":"status","operator":"eq","value":"published"}'`),console.log(""),console.log("Grep options:"),console.log(" -i, --ignore-case - Match either case (default: smart case)"),console.log(" -S, --case-sensitive - Match case exactly"),console.log(" -F, --fixed-strings - Treat the pattern as a literal string, not a regex"),console.log(" --max-lines <num> - Max reported lines per file (default 20)"),console.log(" -n <num> - Max files (default 20); --all for no cap"),console.log(""),console.log("Embed/query options:"),console.log(" --chunk-strategy <auto|regex> - Chunking mode (default: regex; auto uses AST for code files)"),console.log(" --timeout <minutes> - Embed session cap in minutes (0 = no limit; default 30)"),console.log(""),console.log("Multi-get options:"),console.log(" -l <num> - Maximum lines per file"),console.log(" --max-bytes <num> - Skip files larger than N bytes (default 65536)"),console.log(" --format <kind> - Same formats as search"),console.log(""),console.log(`Index: ${te()}`)}a(lr,"showHelp");function C(e,t,o){const s=t?`${n.green}\u2713${n.reset}`:`${n.yellow}\u26A0${n.reset}`;console.log(`${s} ${e}: ${o}`)}a(C,"doctorCheck");function I(e){return e.toLocaleString("en-US")}a(I,"formatCount");function $t(e){return e.startsWith("hf:")?e.split("/").pop()||e:e.length>56?`${e.slice(0,53)}...`:e}a($t,"shortModelName");function ir(e){const t=Array.from(new Set(e));return t.some(s=>s.includes("qmd embed --force"))?t.filter(s=>!s.includes("qmd embed")||s.startsWith("Run `qmd embed --force`")):t}a(ir,"normalizedDoctorNextSteps");function We(e){const t=e.lastIndexOf("_");return t<0?e.length>18?`${e.slice(0,18)}...`:e:`${e.slice(0,12)}_${e.slice(t+1)}`}a(We,"shortHashSeq");function cr(e){return new Float32Array(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength))}a(cr,"decodeStoredEmbedding");function ar(e,t){if(e.length!==t.length||e.length===0)return Number.POSITIVE_INFINITY;let o=0,s=0,r=0;for(let l=0;l<e.length;l++){const i=e[l]??0,m=t[l]??0;o+=i*m,s+=i*i,r+=m*m}return s===0||r===0?Number.POSITIVE_INFINITY:1-o/(Math.sqrt(s)*Math.sqrt(r))}a(ar,"cosineDistance");function ho(e){return oe(e)}a(ho,"formatModelDiagnosticPath");function dr(e){const t=[];if(e.startsWith("hf:")){const s=e.split("/").pop();if(!s||!R(Ie))return{path:null,invalid:t};const r=me(Ie,{withFileTypes:!0});for(const l of r){if(!l.isFile()||!l.name.endsWith(".gguf")||!l.name.includes(s))continue;const i=A(Ie,l.name),m=Nt(i);if(m.valid)return{path:i,invalid:t};t.push(`${ho(i)}: ${m.details}`)}return{path:null,invalid:t}}const o=Nt(e);return o.valid?{path:e,invalid:t}:(o.exists&&t.push(`${ho(e)}: ${o.details}`),{path:null,invalid:t})}a(dr,"findCachedModelInspection");function $o(e){const t=oe(e);return t.length>96?`${t.slice(0,93)}...`:t}a($o,"envValueForDisplay");function ur(e,t={}){const o=[],s=a((l,i)=>{const m=process.env[l]?.trim();m&&o.push({name:l,value:$o(m),consequence:i})},"add"),r=a((l,i,m)=>{const $=process.env[l]?.trim();if(!$)return;const p=t[i],u=p&&p!==$?`set but ignored because index models.${i} is configured as ${p}`:`sets the active ${i} model to ${m}; changes embedding/search semantics and may require \`qmd pull\` plus \`qmd embed\``;o.push({name:l,value:$o($),consequence:u})},"addModel");return s("INDEX_PATH","overrides the SQLite index path; QMD reads/writes a different database"),s("QMD_SQLITE_BUSY_TIMEOUT","overrides the SQLite busy-timeout in ms (default 120000, 0 = fail-fast); too low can surface SQLITE_BUSY under concurrent writers"),s("QMD_CONFIG_DIR","overrides the QMD config directory and takes precedence over XDG_CONFIG_HOME"),s("XDG_CONFIG_HOME","moves QMD config to $XDG_CONFIG_HOME/qmd when QMD_CONFIG_DIR is not set"),s("XDG_CACHE_HOME","moves the default index cache, model cache, MCP daemon PID files, and query logs"),s("QMD_QUERY_LOG","1/true/yes makes the HTTP daemon append each search to queries-YYYY-MM.jsonl in the cache dir"),s("QMD_HOST","overrides the MCP HTTP server bind host (default localhost); 0.0.0.0 accepts off-host connections"),r("QMD_EMBED_MODEL","embed",e.embed),r("QMD_GENERATE_MODEL","generate",e.generate),r("QMD_RERANK_MODEL","rerank",e.rerank),s("QMD_FORCE_CPU","forces llama.cpp to bypass GPU backends; embeddings/query will be slower but GPU crashes are avoided"),s("QMD_LLAMA_GPU","selects llama.cpp GPU backend (metal/cuda/vulkan) or disables GPU when set to false/off/0"),s("QMD_DOCTOR_DEVICE_PROBE","controls qmd doctor native device probing; 0/off skips GPU probing"),s("QMD_EMBED_PARALLELISM","overrides embedding parallel context count; too high can exhaust RAM/VRAM"),s("QMD_LLM_IDLE_TIMEOUT_MS","ms of idle before models unload (0 = never); keeping them loaded holds their memory"),s("QMD_LLM_LOAD_TIMEOUT_MS","caps the model-load wait per stage (default 120000, 0 = wait forever); past it the stage is skipped"),s("QMD_RERANK_CASCADE","0/false/off/no disables head-first cascade rerank and scores all candidates at once"),s("QMD_EXPAND_CONTEXT_SIZE","overrides query expansion context size; larger values use more memory"),s("QMD_RERANK_CONTEXT_SIZE","overrides reranker context size; larger values use more memory"),s("QMD_RERANK_MAX_DOC_TOKENS","caps tokens per document sent to the reranker; lower is faster but scores less text"),s("QMD_EMBED_CONTEXT_SIZE","overrides embed context size; larger values use more memory"),s("QMD_EDITOR_URI","overrides clickable editor link template in terminal output"),s("QMD_SKILLS_DIR","overrides where qmd skills are discovered from"),s("QMD_METAL_KEEP_RESIDENCY","opts back into libggml-metal residency sets on darwin; restores ~0ms perf wins for long-lived processes but re-exposes the static-destructor backtrace dump at process exit (ggml-org/llama.cpp#22593)"),s("GGML_METAL_NO_RESIDENCY","set automatically by the launcher on darwin to disable Metal residency sets (avoids ggml-org/llama.cpp#22593); override via QMD_METAL_KEEP_RESIDENCY=1"),s("NO_COLOR","disables colored terminal output"),s("CI","disables real LLM operations inside QMD's LlamaCpp wrapper"),s("HF_ENDPOINT","changes Hugging Face download endpoint used when pulling models"),s("QMD_WRAPPER_CAPTURE","test/debug hook for the qmd shell wrapper; should not be set in normal use"),s("QMD_TRUST_LOCAL_CONFIG","trusts project-local .qmd config hooks/paths/models without prompting; runs unreviewed commands on update/query"),s("QMD_TRUST_UPDATE_HOOKS","runs project-local update hooks without prompting; runs unreviewed commands on update"),s("WSL_DISTRO_NAME","enables WSL path handling heuristics"),s("WSL_INTEROP","enables WSL path handling heuristics"),o}a(ur,"collectEnvironmentOverrides");function mr(e){try{const t=ie(),o=Object.keys(t.collections??{}).length;return o===0?(C("index config",!1,"no collections configured. Next: `qmd collection add .`"),e.push("Run `qmd collection add . --name <name>` from the folder you want to index, or edit .qmd/index.yml manually.")):C("index config",!0,`${I(o)} ${o===1?"collection":"collections"} configured`),{config:t,valid:!0}}catch(t){const o=t instanceof Error?oe(t.message):oe(String(t)),s=ce();return C("index config",!1,`invalid index.yml at ${s}: ${o}. Next: fix the YAML and rerun \`qmd doctor\``),e.push(`Fix invalid YAML in ${s}, then rerun \`qmd doctor\`.`),{config:null,valid:!1}}}a(mr,"checkDoctorIndexConfig");function fr(e,t={}){const o=ur(e,t);if(o.length===0){C("environment overrides",!0,"none");return}C("environment overrides",!1,`${o.length} set`);for(const s of o)console.log(` - ${s.name}=${s.value}: ${s.consequence}`)}a(fr,"checkEnvironmentOverrides");function gr(e,t={}){const o=[{role:"embedding",key:"embed",active:e.embed,configured:t.embed,defaultModel:Te,envName:"QMD_EMBED_MODEL",envValue:process.env.QMD_EMBED_MODEL},{role:"generation",key:"generate",active:e.generate,configured:t.generate,defaultModel:_t,envName:"QMD_GENERATE_MODEL",envValue:process.env.QMD_GENERATE_MODEL},{role:"reranking",key:"rerank",active:e.rerank,configured:t.rerank,defaultModel:Xe,envName:"QMD_RERANK_MODEL",envValue:process.env.QMD_RERANK_MODEL}],s=[];for(const r of o){const l=r.envValue?.trim();l&&r.active===l?s.push(`${r.role}: env ${r.envName}=${r.active} (default ${r.defaultModel}; might be ok)`):r.configured&&r.configured!==r.defaultModel?s.push(`${r.role}: index ${r.configured} (default ${r.defaultModel}; might be ok)`):l&&r.active!==l&&s.push(`${r.role}: ${r.envName} is set to ${l} but index config uses ${r.active}`)}if(s.length===0){C("model defaults",!0,"using QMD codebase defaults");return}C("model defaults",!1,`non-default model configuration: ${s.join("; ")}`)}a(gr,"checkModelDefaults");function pr(e,t){const o=[["embedding",e.embed],["generation",e.generate],["reranking",e.rerank]],s=new Map;for(const[p,u]of o)s.set(u,[...s.get(u)??[],p]);const r=[],l=[],i=[];for(const[p,u]of s){const c=`${u.join("+")}: ${p}`,d=dr(p);i.push(...d.invalid.map(h=>`${c} (${h})`)),d.path?l.push(c):r.push(c)}if(r.length===0&&i.length===0){C("model cache",!0,`${l.length} active ${l.length===1?"model is":"models are"} downloaded and valid GGUF`);return}const m=[];i.length>0&&m.push(`invalid ${i.length}: ${i.join("; ")}`),r.length>0&&m.push(`missing ${r.length}/${s.size}: ${r.join("; ")}`);const $=i.length>0?"Next: run `qmd pull --refresh` (or remove the bad cached file)":"Next: run `qmd pull`";C("model cache",!1,`${m.join("; ")}. ${$}`),i.length>0?t.push("Run `qmd pull --refresh` to replace invalid cached model files, or delete the listed file and rerun `qmd pull`."):t.push("Run `qmd pull` to download missing embedding/generation/reranking models before `qmd embed` or `qmd query`.")}a(pr,"checkModelCache");async function hr(e,t,o,s=3){if(e.prepare("SELECT COUNT(*) AS count FROM documents WHERE active = 1").get().count===0)return{ok:!0,details:"no active documents indexed",empty:!0};if(!e.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='vectors_vec'").get())return{ok:!1,details:"no vector table to test; please run qmd embed again",empty:!0};const i=e.prepare(`
148
+ `)});return}case"path":{const i=e[1];if(!i){const $=ao(!0);t?De({success:!0,data:{paths:$}}):$.forEach(p=>console.log(p));return}const m=Ge(i,!0);if(!m)throw new Error(`Skill not found: ${i}`);t?De({success:!0,data:{name:m.name,path:m.dir}}):console.log(m.dir);return}case"help":{po();return}default:throw new Error(`Unknown skills subcommand: ${r}`)}}a(or,"runSkillsCommand");function po(){console.log("Usage: qmd skills <list|get|path> [options]"),console.log(""),console.log("Commands:"),console.log(" list List bundled runtime skills"),console.log(" get <name> Print a bundled runtime skill"),console.log(" get <name> --full Include references/templates/scripts"),console.log(" get --all Print all bundled runtime skills"),console.log(" path [name] Print runtime skill directory path(s)"),console.log(""),console.log("Options:"),console.log(" --json Print structured JSON")}a(po,"showSkillsHelp");function nr(e,t,o){const s=X(e);if(pt(s)){const l=se(X(t)),i=se(s);if(l===i)return!1}const r=kt(s,t)||".";if(Pe(s,{recursive:!0}),pt(e)){if(Ke(e).isSymbolicLink()&&Fo(e)===r)return!0;if(!o)throw new Error(`Claude skill path already exists: ${e} (use --force to replace it)`);io(e)}return Ao(r,e,"dir"),!0}a(nr,"ensureClaudeSymlink");async function sr(e,t){if(t)return!0;if(!process.stdin.isTTY||!process.stdout.isTTY)return console.log(`Tip: create a Claude symlink manually at ${e}`),!1;const o=Qo({input:process.stdin,output:process.stdout});try{const r=(await o.question(`Create a symlink in ${e}? [y/N] `)).trim().toLowerCase();return r==="y"||r==="yes"}finally{o.close()}}a(sr,"shouldCreateClaudeSymlink");async function rr(e,t,o){const s=zs(e);tr(s,t),console.log(`\u2713 Installed QMD skill to ${s}`);const r=Js(e);if(!await sr(r,o))return;const l=nr(r,s,t);console.log(l?`\u2713 Linked Claude skill at ${r}`:`\u2713 Claude already sees the skill via ${X(r)}`)}a(rr,"installSkill");function lr(){console.log("qmd \u2014 Quick Markdown Search"),console.log(""),console.log("Usage:"),console.log(" qmd <command> [options]"),console.log(""),console.log("Primary commands:"),console.log(" qmd query <query> - Hybrid search with auto expansion + reranking (recommended)"),console.log(" qmd query 'lex:..\\nvec:...' - Structured query document (you provide lex/vec/hyde lines)"),console.log(" qmd search <query> - Full-text BM25 keywords (no LLM)"),console.log(" qmd vsearch <query> - Vector similarity only"),console.log(" qmd grep <pattern> - Exact/regex match over indexed bodies (no ranking)"),console.log(" qmd get <file>[:from[:count]] - Show a document (line-numbered; #docid in header)"),console.log(" qmd multi-get <pattern> - Batch fetch via glob or comma-separated list"),console.log(" qmd skills list/get/path - List and retrieve bundled runtime skills"),console.log(" qmd skill show/install - Show or install the QMD skill"),console.log(" qmd mcp - Start the MCP server (stdio transport for AI agents)"),console.log(" qmd bench <fixture.json> - Run search quality benchmarks against a fixture file"),console.log(""),console.log("Collections & context:"),console.log(" qmd collection add/list/remove/rename/show - Manage indexed folders"),console.log(" qmd context add/list/rm - Attach human-written summaries"),console.log(" qmd ls [collection[/path]] - Inspect indexed files"),console.log(""),console.log("Maintenance:"),console.log(" qmd init - Create a project-local .qmd index"),console.log(" qmd status - View index + collection health"),console.log(" qmd update [--pull] - Re-index collections (optionally git pull first)"),console.log(" qmd trust [list|revoke] - Approve a checked-in .qmd config's hooks/paths/models"),console.log(" qmd embed [-f] [-c <name>] - Generate/refresh vector embeddings"),console.log(" --max-docs-per-batch <n> - Cap docs loaded into memory per embedding batch"),console.log(" --max-batch-mb <n> - Cap UTF-8 MB loaded into memory per embedding batch"),console.log(" --timeout <minutes> - Embed session cap in minutes (0 = no limit; default 30)"),console.log(" qmd pull [--refresh] [--progress] - Download embedding/generation/rerank models"),console.log(" qmd cleanup [--dry-run] - Drop inactive docs/orphans, compact FTS, vacuum"),console.log(""),console.log("Query syntax (qmd query):"),console.log(" QMD queries are either a single expand query (no prefix) or a multi-line"),console.log(" document where every line is typed with lex:, vec:, or hyde:. This grammar"),console.log(" matches the docs in docs/SYNTAX.md and is enforced in the CLI."),console.log("");const e=["query = expand_query | query_document ;","expand_query = text | explicit_expand ;",'explicit_expand= "expand:" text ;',"query_document = [ intent_line ] { typed_line } ;",'intent_line = "intent:" text newline ;','typed_line = type ":" text newline ;','type = "lex" | "vec" | "hyde" ;',"text = quoted_phrase | plain_text ;",`quoted_phrase = '"' { character } '"' ;`,"plain_text = { character } ;",'newline = "\\n" ;'];console.log(" Grammar:");for(const t of e)console.log(` ${t}`);console.log(""),console.log(" Examples:"),console.log(' qmd query "how does auth work" # single-line \u2192 implicit expand'),console.log(" qmd query $'lex: CAP theorem\\nvec: consistency' # typed query document"),console.log(` qmd query $'lex: "exact matches" sports -baseball' # phrase + negation lex search`),console.log(" qmd query $'hyde: Hypothetical answer text' # hyde-only document"),console.log(""),console.log(" Constraints:"),console.log(" - Standalone expand queries cannot mix with typed lines."),console.log(" - Query documents allow only lex:, vec:, hyde:, or intent: prefixes."),console.log(" - Each typed line must be single-line text with balanced quotes."),console.log(""),console.log("AI agents & integrations:"),console.log(" - Run `qmd mcp` to expose the MCP server (stdio) to agents/IDEs."),console.log(" - Run `qmd skills get qmd --full` for version-matched agent instructions."),console.log(" - `qmd skill install` installs the QMD skill into ./.agents/skills/qmd."),console.log(" - Use `qmd skill install --global` for ~/.agents/skills/qmd."),console.log(" - `qmd --skill` is kept as an alias for `qmd skill show`."),console.log(" - Advanced: `qmd mcp --http ...` and `qmd mcp --http --daemon` are optional for custom transports."),console.log(""),console.log("Global options:"),console.log(" --index <name> - Use a named index (default: index)"),console.log(" QMD_EDITOR_URI - Editor link template for clickable TTY search output"),console.log(""),console.log("Search options:"),console.log(" -n <num> - Max results (default 5, or 20 for --format files|json)"),console.log(" --all - Return all matches (pair with --min-score)"),console.log(" --min-score <num> - Minimum similarity score"),console.log(" --full - Output full document instead of snippet"),console.log(" -C, --candidate-limit <n> - Max candidates to rerank (default 40, lower = faster)"),console.log(" --no-rerank - Skip LLM reranking (use RRF scores only, much faster on CPU)"),console.log(" --no-gpu - Force CPU mode for llama.cpp operations (same as QMD_FORCE_CPU=1)"),console.log(" --line-numbers - Include line numbers (search; get/multi-get are on by default)"),console.log(" --no-line-numbers - Disable line numbers for get/multi-get"),console.log(" --full-path - Show on-disk paths instead of qmd:// + docid (get/multi-get/search/query)"),console.log(" Paths are ./-prefixed when under $PWD, absolute otherwise"),console.log(" Results whose file is gone keep qmd:// + docid and warn on stderr"),console.log(" --explain - Include retrieval score traces (query, CLI/--format json)"),console.log(" --format <kind> - Output format: cli (default) | json | csv | md | xml | files"),console.log(" -c, --collection <name> - Filter by one or more collections"),console.log(" --path <glob> - Only documents matching the glob; repeatable, '!' excludes"),console.log(" Matched against collection/path, as printed in results"),console.log(" --since <when> - Only documents modified at or after (7d, 3h, 2w, 2026-09-01)"),console.log(" --until <when> - Only documents modified at or before (same formats)"),console.log(" --filter <json> - Metadata filter (recursive JSON AST; search/vsearch/query/grep)"),console.log(` e.g. '{"key":"status","operator":"eq","value":"published"}'`),console.log(""),console.log("Grep options:"),console.log(" -i, --ignore-case - Match either case (default: smart case)"),console.log(" -S, --case-sensitive - Match case exactly"),console.log(" -F, --fixed-strings - Treat the pattern as a literal string, not a regex"),console.log(" --max-lines <num> - Max reported lines per file (default 20)"),console.log(" -n <num> - Max files (default 20); --all for no cap"),console.log(""),console.log("Embed/query options:"),console.log(" --chunk-strategy <auto|regex> - Chunking mode (default: regex; auto uses AST for code files)"),console.log(" --timeout <minutes> - Embed session cap in minutes (0 = no limit; default 30)"),console.log(""),console.log("Multi-get options:"),console.log(" -l <num> - Maximum lines per file"),console.log(" --max-bytes <num> - Skip files larger than N bytes (default 65536)"),console.log(" --format <kind> - Same formats as search"),console.log(""),console.log(`Index: ${te()}`)}a(lr,"showHelp");function C(e,t,o){const s=t?`${n.green}\u2713${n.reset}`:`${n.yellow}\u26A0${n.reset}`;console.log(`${s} ${e}: ${o}`)}a(C,"doctorCheck");function I(e){return e.toLocaleString("en-US")}a(I,"formatCount");function $t(e){return e.startsWith("hf:")?e.split("/").pop()||e:e.length>56?`${e.slice(0,53)}...`:e}a($t,"shortModelName");function ir(e){const t=Array.from(new Set(e));return t.some(s=>s.includes("qmd embed --force"))?t.filter(s=>!s.includes("qmd embed")||s.startsWith("Run `qmd embed --force`")):t}a(ir,"normalizedDoctorNextSteps");function We(e){const t=e.lastIndexOf("_");return t<0?e.length>18?`${e.slice(0,18)}...`:e:`${e.slice(0,12)}_${e.slice(t+1)}`}a(We,"shortHashSeq");function cr(e){return new Float32Array(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength))}a(cr,"decodeStoredEmbedding");function ar(e,t){if(e.length!==t.length||e.length===0)return Number.POSITIVE_INFINITY;let o=0,s=0,r=0;for(let l=0;l<e.length;l++){const i=e[l]??0,m=t[l]??0;o+=i*m,s+=i*i,r+=m*m}return s===0||r===0?Number.POSITIVE_INFINITY:1-o/(Math.sqrt(s)*Math.sqrt(r))}a(ar,"cosineDistance");function ho(e){return oe(e)}a(ho,"formatModelDiagnosticPath");function dr(e){const t=[];if(e.startsWith("hf:")){const s=e.split("/").pop();if(!s||!R(Ie))return{path:null,invalid:t};const r=me(Ie,{withFileTypes:!0});for(const l of r){if(!l.isFile()||!l.name.endsWith(".gguf")||!l.name.includes(s))continue;const i=A(Ie,l.name),m=Nt(i);if(m.valid)return{path:i,invalid:t};t.push(`${ho(i)}: ${m.details}`)}return{path:null,invalid:t}}const o=Nt(e);return o.valid?{path:e,invalid:t}:(o.exists&&t.push(`${ho(e)}: ${o.details}`),{path:null,invalid:t})}a(dr,"findCachedModelInspection");function $o(e){const t=oe(e);return t.length>96?`${t.slice(0,93)}...`:t}a($o,"envValueForDisplay");function ur(e,t={}){const o=[],s=a((l,i)=>{const m=process.env[l]?.trim();m&&o.push({name:l,value:$o(m),consequence:i})},"add"),r=a((l,i,m)=>{const $=process.env[l]?.trim();if(!$)return;const p=t[i],u=p&&p!==$?`set but ignored because index models.${i} is configured as ${p}`:`sets the active ${i} model to ${m}; changes embedding/search semantics and may require \`qmd pull\` plus \`qmd embed\``;o.push({name:l,value:$o($),consequence:u})},"addModel");return s("INDEX_PATH","overrides the SQLite index path; QMD reads/writes a different database"),s("QMD_SQLITE_BUSY_TIMEOUT","overrides the SQLite busy-timeout in ms (default 120000, 0 = fail-fast); too low can surface SQLITE_BUSY under concurrent writers"),s("QMD_CONFIG_DIR","overrides the QMD config directory and takes precedence over XDG_CONFIG_HOME"),s("XDG_CONFIG_HOME","moves QMD config to $XDG_CONFIG_HOME/qmd when QMD_CONFIG_DIR is not set"),s("XDG_CACHE_HOME","moves the default index cache, model cache, MCP daemon PID files, and query logs"),s("QMD_QUERY_LOG","1/true/yes makes the HTTP daemon append each search to queries-YYYY-MM.jsonl in the cache dir"),s("QMD_HOST","overrides the MCP HTTP server bind host (default localhost); 0.0.0.0 accepts off-host connections"),r("QMD_EMBED_MODEL","embed",e.embed),r("QMD_GENERATE_MODEL","generate",e.generate),r("QMD_RERANK_MODEL","rerank",e.rerank),s("QMD_FORCE_CPU","forces llama.cpp to bypass GPU backends; embeddings/query will be slower but GPU crashes are avoided"),s("QMD_LLAMA_GPU","selects llama.cpp GPU backend (metal/cuda/vulkan) or disables GPU when set to false/off/0"),s("QMD_DOCTOR_DEVICE_PROBE","controls qmd doctor native device probing; 0/off skips GPU probing"),s("QMD_EMBED_PARALLELISM","overrides embedding parallel context count; too high can exhaust RAM/VRAM"),s("QMD_LLM_IDLE_TIMEOUT_MS","ms of idle before models unload (0 = never); keeping them loaded holds their memory"),s("QMD_LLM_LOAD_TIMEOUT_MS","caps the model-load wait per stage (default 120000, 0 = wait forever); past it the stage is skipped"),s("QMD_RERANK_CASCADE","0/false/off/no disables head-first cascade rerank and scores all candidates at once"),s("QMD_EXPAND_CONTEXT_SIZE","overrides query expansion context size; larger values use more memory"),s("QMD_RERANK_CONTEXT_SIZE","overrides reranker context size; larger values use more memory"),s("QMD_RERANK_MAX_DOC_TOKENS","caps tokens per document sent to the reranker (default 256, 0 = no cap); lower is faster but scores less text"),s("QMD_EMBED_CONTEXT_SIZE","overrides embed context size; larger values use more memory"),s("QMD_EDITOR_URI","overrides clickable editor link template in terminal output"),s("QMD_SKILLS_DIR","overrides where qmd skills are discovered from"),s("QMD_METAL_KEEP_RESIDENCY","opts back into libggml-metal residency sets on darwin; restores ~0ms perf wins for long-lived processes but re-exposes the static-destructor backtrace dump at process exit (ggml-org/llama.cpp#22593)"),s("GGML_METAL_NO_RESIDENCY","set automatically by the launcher on darwin to disable Metal residency sets (avoids ggml-org/llama.cpp#22593); override via QMD_METAL_KEEP_RESIDENCY=1"),s("NO_COLOR","disables colored terminal output"),s("CI","disables real LLM operations inside QMD's LlamaCpp wrapper"),s("HF_ENDPOINT","changes Hugging Face download endpoint used when pulling models"),s("QMD_WRAPPER_CAPTURE","test/debug hook for the qmd shell wrapper; should not be set in normal use"),s("QMD_TRUST_LOCAL_CONFIG","trusts project-local .qmd config hooks/paths/models without prompting; runs unreviewed commands on update/query"),s("QMD_TRUST_UPDATE_HOOKS","runs project-local update hooks without prompting; runs unreviewed commands on update"),s("WSL_DISTRO_NAME","enables WSL path handling heuristics"),s("WSL_INTEROP","enables WSL path handling heuristics"),o}a(ur,"collectEnvironmentOverrides");function mr(e){try{const t=ie(),o=Object.keys(t.collections??{}).length;return o===0?(C("index config",!1,"no collections configured. Next: `qmd collection add .`"),e.push("Run `qmd collection add . --name <name>` from the folder you want to index, or edit .qmd/index.yml manually.")):C("index config",!0,`${I(o)} ${o===1?"collection":"collections"} configured`),{config:t,valid:!0}}catch(t){const o=t instanceof Error?oe(t.message):oe(String(t)),s=ce();return C("index config",!1,`invalid index.yml at ${s}: ${o}. Next: fix the YAML and rerun \`qmd doctor\``),e.push(`Fix invalid YAML in ${s}, then rerun \`qmd doctor\`.`),{config:null,valid:!1}}}a(mr,"checkDoctorIndexConfig");function fr(e,t={}){const o=ur(e,t);if(o.length===0){C("environment overrides",!0,"none");return}C("environment overrides",!1,`${o.length} set`);for(const s of o)console.log(` - ${s.name}=${s.value}: ${s.consequence}`)}a(fr,"checkEnvironmentOverrides");function gr(e,t={}){const o=[{role:"embedding",key:"embed",active:e.embed,configured:t.embed,defaultModel:Te,envName:"QMD_EMBED_MODEL",envValue:process.env.QMD_EMBED_MODEL},{role:"generation",key:"generate",active:e.generate,configured:t.generate,defaultModel:_t,envName:"QMD_GENERATE_MODEL",envValue:process.env.QMD_GENERATE_MODEL},{role:"reranking",key:"rerank",active:e.rerank,configured:t.rerank,defaultModel:Xe,envName:"QMD_RERANK_MODEL",envValue:process.env.QMD_RERANK_MODEL}],s=[];for(const r of o){const l=r.envValue?.trim();l&&r.active===l?s.push(`${r.role}: env ${r.envName}=${r.active} (default ${r.defaultModel}; might be ok)`):r.configured&&r.configured!==r.defaultModel?s.push(`${r.role}: index ${r.configured} (default ${r.defaultModel}; might be ok)`):l&&r.active!==l&&s.push(`${r.role}: ${r.envName} is set to ${l} but index config uses ${r.active}`)}if(s.length===0){C("model defaults",!0,"using QMD codebase defaults");return}C("model defaults",!1,`non-default model configuration: ${s.join("; ")}`)}a(gr,"checkModelDefaults");function pr(e,t){const o=[["embedding",e.embed],["generation",e.generate],["reranking",e.rerank]],s=new Map;for(const[p,u]of o)s.set(u,[...s.get(u)??[],p]);const r=[],l=[],i=[];for(const[p,u]of s){const c=`${u.join("+")}: ${p}`,d=dr(p);i.push(...d.invalid.map(h=>`${c} (${h})`)),d.path?l.push(c):r.push(c)}if(r.length===0&&i.length===0){C("model cache",!0,`${l.length} active ${l.length===1?"model is":"models are"} downloaded and valid GGUF`);return}const m=[];i.length>0&&m.push(`invalid ${i.length}: ${i.join("; ")}`),r.length>0&&m.push(`missing ${r.length}/${s.size}: ${r.join("; ")}`);const $=i.length>0?"Next: run `qmd pull --refresh` (or remove the bad cached file)":"Next: run `qmd pull`";C("model cache",!1,`${m.join("; ")}. ${$}`),i.length>0?t.push("Run `qmd pull --refresh` to replace invalid cached model files, or delete the listed file and rerun `qmd pull`."):t.push("Run `qmd pull` to download missing embedding/generation/reranking models before `qmd embed` or `qmd query`.")}a(pr,"checkModelCache");async function hr(e,t,o,s=3){if(e.prepare("SELECT COUNT(*) AS count FROM documents WHERE active = 1").get().count===0)return{ok:!0,details:"no active documents indexed",empty:!0};if(!e.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='vectors_vec'").get())return{ok:!1,details:"no vector table to test; please run qmd embed again",empty:!0};const i=e.prepare(`
149
149
  SELECT cv.hash, cv.seq, c.doc AS body, MIN(d.path) AS path
150
150
  FROM content_vectors cv
151
151
  JOIN documents d ON d.hash = cv.hash AND d.active = 1
package/dist/llm.d.ts CHANGED
@@ -118,6 +118,13 @@ export type GenerateOptions = {
118
118
  */
119
119
  /** A rerank doc token cap is a positive integer; anything else means "no cap from this source". */
120
120
  export declare const normalizeRerankMaxDocTokens: (value: unknown) => number | undefined;
121
+ /**
122
+ * ko-qmd: default rerank doc token cap. On 29 replayed daemon queries (M3 Max, rerank cache
123
+ * cleared) 256 cut query p50 5323 → 1682 ms with ko-vault bench lines equal or higher.
124
+ */
125
+ export declare const DEFAULT_RERANK_MAX_DOC_TOKENS = 256;
126
+ /** QMD_RERANK_MAX_DOC_TOKENS → cap: unset or invalid gives the default, 0 gives no cap. */
127
+ export declare const resolveRerankMaxDocTokens: (raw: string | undefined) => number | undefined;
121
128
  export type RerankOptions = {
122
129
  model?: string;
123
130
  /** Per-call doc token cap; overrides QMD_RERANK_MAX_DOC_TOKENS, never raises the context budget. */
package/dist/llm.js CHANGED
@@ -1,8 +1,8 @@
1
- var me=Object.defineProperty;var o=(n,e)=>me(n,"name",{value:e,configurable:!0});let N=null;async function v(){return N??=K(()=>import("node-llama-cpp")),N}o(v,"loadNodeLlamaCpp");function Ke(n){N=n?Promise.resolve(n):null,D.clear(),Q=!1,q=!1,O=void 0}o(Ke,"setNodeLlamaCppModuleForTest");let S=0,P=null;async function K(n){S===0&&(P=process.stdout.write.bind(process.stdout),process.stdout.write=((e,t,r)=>typeof t=="function"?process.stderr.write(e,t):process.stderr.write(e,t,r))),S++;try{return await n()}finally{S--,S===0&&P&&(process.stdout.write=P,P=null)}}o(K,"withNativeStdoutRedirectedToStderr");import{homedir as he}from"os";import{dirname as fe,join as k}from"path";import{accessSync as pe,constants as Me,existsSync as y,mkdirSync as H,statSync as G,unlinkSync as A,readdirSync as ge,readFileSync as xe,writeFileSync as we,openSync as Ee,readSync as ye,closeSync as Ce}from"fs";import{createRequire as be}from"node:module";import{hangulBigramTail as Le,sharesHangulBigram as ke}from"./hangul.js";function Z(n){return/qwen.*embed/i.test(n)||/embed.*qwen/i.test(n)}o(Z,"isQwen3EmbeddingModel");function Je(n,e){const t=e??R();return Z(t)?`Instruct: Retrieve relevant documents for the given query
2
- Query: ${n}`:`task: search result | query: ${n}`}o(Je,"formatQueryForEmbedding");function et(n,e,t){const r=t??R();return Z(r)?e?`${e}
3
- ${n}`:n:`title: ${e||"none"} | text: ${n}`}o(et,"formatDocForEmbedding");const Te=be(import.meta.url);let O;function tt(n){O=n}o(tt,"setLlamaDirWritableForTest");function ve(n){if(O!==void 0)return O;try{const e=n??fe(Te.resolve("node-llama-cpp/package.json"));return pe(k(e,"llama"),Me.W_OK),!0}catch{return!1}}o(ve,"canWriteLlamaDir");const _e=o(n=>typeof n=="number"&&Number.isInteger(n)&&n>0?n:void 0,"normalizeRerankMaxDocTokens");function De(n){return n.replace(/<think>[\s\S]*?<\/think>/g," ").replace(/<\/?think>/g," ").replace(/\s+/g," ").trim()}o(De,"stripThinkTags");function Se(n,e){const t=n.toLowerCase().replace(/[^a-z0-9\s]/g," ").split(/\s+/).filter(l=>l.length>=2),r=Le(n).length>0,i=o(l=>{if(t.length===0&&!r)return!0;const h=l.toLowerCase();return t.some(s=>h.includes(s))?!0:r&&ke(n,l)},"onQuery"),a=new Set,d=[];for(const l of e.trim().split(`
4
- `)){const h=l.indexOf(":");if(h===-1)continue;const s=l.slice(0,h).trim();if(s!=="lex"&&s!=="vec"&&s!=="hyde")continue;const u=De(l.slice(h+1));if(!u||!i(u))continue;const c=`${s}
5
- ${u.toLowerCase()}`;a.has(c)||(a.add(c),d.push({type:s,text:u}))}return d}o(Se,"parseExpansionLines");const V="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf",j="hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",Y="hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf",nt=V,rt=j,it=Y;function R(n){return n?.embed||process.env.QMD_EMBED_MODEL||V}o(R,"resolveEmbedModel");function J(n){return n?.generate||process.env.QMD_GENERATE_MODEL||Y}o(J,"resolveGenerateModel");function ee(n){return n?.rerank||process.env.QMD_RERANK_MODEL||j}o(ee,"resolveRerankModel");function ot(n){return{embed:R(n),generate:J(n),rerank:ee(n)}}o(ot,"resolveModels");const U=process.env.XDG_CACHE_HOME?k(process.env.XDG_CACHE_HOME,"qmd","models"):k(he(),".cache","qmd","models"),st=U;function Pe(n){if(!n.startsWith("hf:"))return null;const t=n.slice(3).split("/");if(t.length<3)return null;const r=t.slice(0,2).join("/"),i=t.slice(2).join("/");return{repo:r,file:i}}o(Pe,"parseHfUri");async function te(n){const e=`https://huggingface.co/${n.repo}/resolve/main/${n.file}`;try{const t=await fetch(e,{method:"HEAD"});return t.ok&&t.headers.get("etag")||null}catch{return null}}o(te,"getRemoteEtag");const Ae=Buffer.from("GGUF");function _(n){return`${(n/1024).toFixed(0)} KB`}o(_,"formatModelFileSize");function Oe(n){const e=n.toString("utf-8");return/^[\x20-\x7e]{1,4}$/.test(e)?e:`0x${n.toString("hex")}`}o(Oe,"printableMagic");function Re(n){if(!y(n))return{exists:!1,valid:!1,kind:"missing",details:"file does not exist"};let e=0;try{e=G(n).size;const t=Ee(n,"r"),r=Buffer.alloc(512);try{ye(t,r,0,512,0)}finally{Ce(t)}const i=r.subarray(0,4);if(i.equals(Ae))return{exists:!0,valid:!0,kind:"gguf",sizeBytes:e,magic:"GGUF",details:`valid GGUF (${_(e)})`};const a=Oe(i),d=r.toString("utf-8").toLowerCase();return d.includes("<!doctype")||d.includes("<html")?{exists:!0,valid:!1,kind:"html",sizeBytes:e,magic:a,details:`HTML page, not a GGUF model (${_(e)}); likely proxy/firewall/captive portal response`}:{exists:!0,valid:!1,kind:"invalid",sizeBytes:e,magic:a,details:`not valid GGUF (expected magic "GGUF", got "${a}", ${_(e)})`}}catch(t){return{exists:!0,valid:!1,kind:"unreadable",sizeBytes:e,details:`cannot read model file: ${t instanceof Error?t.message:String(t)}`}}}o(Re,"inspectGgufFile");function ne(n,e){const t=Re(n);if(!t.exists||t.valid)return;if(t.kind==="unreadable")throw new Error(`Model file could not be read, so it could not be validated (${t.details}).
1
+ var me=Object.defineProperty;var o=(n,e)=>me(n,"name",{value:e,configurable:!0});let N=null;async function v(){return N??=W(()=>import("node-llama-cpp")),N}o(v,"loadNodeLlamaCpp");function Ze(n){N=n?Promise.resolve(n):null,D.clear(),Q=!1,q=!1,R=void 0}o(Ze,"setNodeLlamaCppModuleForTest");let S=0,P=null;async function W(n){S===0&&(P=process.stdout.write.bind(process.stdout),process.stdout.write=((e,t,r)=>typeof t=="function"?process.stderr.write(e,t):process.stderr.write(e,t,r))),S++;try{return await n()}finally{S--,S===0&&P&&(process.stdout.write=P,P=null)}}o(W,"withNativeStdoutRedirectedToStderr");import{homedir as he}from"os";import{dirname as fe,join as k}from"path";import{accessSync as pe,constants as Me,existsSync as y,mkdirSync as H,statSync as G,unlinkSync as A,readdirSync as ge,readFileSync as xe,writeFileSync as Ee,openSync as we,readSync as ye,closeSync as Ce}from"fs";import{createRequire as be}from"node:module";import{hangulBigramTail as Le,sharesHangulBigram as ke}from"./hangul.js";function Z(n){return/qwen.*embed/i.test(n)||/embed.*qwen/i.test(n)}o(Z,"isQwen3EmbeddingModel");function tt(n,e){const t=e??O();return Z(t)?`Instruct: Retrieve relevant documents for the given query
2
+ Query: ${n}`:`task: search result | query: ${n}`}o(tt,"formatQueryForEmbedding");function nt(n,e,t){const r=t??O();return Z(r)?e?`${e}
3
+ ${n}`:n:`title: ${e||"none"} | text: ${n}`}o(nt,"formatDocForEmbedding");const Te=be(import.meta.url);let R;function rt(n){R=n}o(rt,"setLlamaDirWritableForTest");function ve(n){if(R!==void 0)return R;try{const e=n??fe(Te.resolve("node-llama-cpp/package.json"));return pe(k(e,"llama"),Me.W_OK),!0}catch{return!1}}o(ve,"canWriteLlamaDir");const _e=o(n=>typeof n=="number"&&Number.isInteger(n)&&n>0?n:void 0,"normalizeRerankMaxDocTokens"),De=256,Se=o(n=>{if(/^\s*0\s*$/.test(n??""))return;const e=parseInt(n??"",10);return Number.isFinite(e)&&e>0?e:De},"resolveRerankMaxDocTokens");function Pe(n){return n.replace(/<think>[\s\S]*?<\/think>/g," ").replace(/<\/?think>/g," ").replace(/\s+/g," ").trim()}o(Pe,"stripThinkTags");function Ae(n,e){const t=n.toLowerCase().replace(/[^a-z0-9\s]/g," ").split(/\s+/).filter(l=>l.length>=2),r=Le(n).length>0,i=o(l=>{if(t.length===0&&!r)return!0;const h=l.toLowerCase();return t.some(s=>h.includes(s))?!0:r&&ke(n,l)},"onQuery"),a=new Set,d=[];for(const l of e.trim().split(`
4
+ `)){const h=l.indexOf(":");if(h===-1)continue;const s=l.slice(0,h).trim();if(s!=="lex"&&s!=="vec"&&s!=="hyde")continue;const u=Pe(l.slice(h+1));if(!u||!i(u))continue;const c=`${s}
5
+ ${u.toLowerCase()}`;a.has(c)||(a.add(c),d.push({type:s,text:u}))}return d}o(Ae,"parseExpansionLines");const V="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf",j="hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",Y="hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf",it=V,ot=j,st=Y;function O(n){return n?.embed||process.env.QMD_EMBED_MODEL||V}o(O,"resolveEmbedModel");function J(n){return n?.generate||process.env.QMD_GENERATE_MODEL||Y}o(J,"resolveGenerateModel");function ee(n){return n?.rerank||process.env.QMD_RERANK_MODEL||j}o(ee,"resolveRerankModel");function at(n){return{embed:O(n),generate:J(n),rerank:ee(n)}}o(at,"resolveModels");const U=process.env.XDG_CACHE_HOME?k(process.env.XDG_CACHE_HOME,"qmd","models"):k(he(),".cache","qmd","models"),lt=U;function Re(n){if(!n.startsWith("hf:"))return null;const t=n.slice(3).split("/");if(t.length<3)return null;const r=t.slice(0,2).join("/"),i=t.slice(2).join("/");return{repo:r,file:i}}o(Re,"parseHfUri");async function te(n){const e=`https://huggingface.co/${n.repo}/resolve/main/${n.file}`;try{const t=await fetch(e,{method:"HEAD"});return t.ok&&t.headers.get("etag")||null}catch{return null}}o(te,"getRemoteEtag");const Oe=Buffer.from("GGUF");function _(n){return`${(n/1024).toFixed(0)} KB`}o(_,"formatModelFileSize");function Ie(n){const e=n.toString("utf-8");return/^[\x20-\x7e]{1,4}$/.test(e)?e:`0x${n.toString("hex")}`}o(Ie,"printableMagic");function Fe(n){if(!y(n))return{exists:!1,valid:!1,kind:"missing",details:"file does not exist"};let e=0;try{e=G(n).size;const t=we(n,"r"),r=Buffer.alloc(512);try{ye(t,r,0,512,0)}finally{Ce(t)}const i=r.subarray(0,4);if(i.equals(Oe))return{exists:!0,valid:!0,kind:"gguf",sizeBytes:e,magic:"GGUF",details:`valid GGUF (${_(e)})`};const a=Ie(i),d=r.toString("utf-8").toLowerCase();return d.includes("<!doctype")||d.includes("<html")?{exists:!0,valid:!1,kind:"html",sizeBytes:e,magic:a,details:`HTML page, not a GGUF model (${_(e)}); likely proxy/firewall/captive portal response`}:{exists:!0,valid:!1,kind:"invalid",sizeBytes:e,magic:a,details:`not valid GGUF (expected magic "GGUF", got "${a}", ${_(e)})`}}catch(t){return{exists:!0,valid:!1,kind:"unreadable",sizeBytes:e,details:`cannot read model file: ${t instanceof Error?t.message:String(t)}`}}}o(Fe,"inspectGgufFile");function ne(n,e){const t=Fe(n);if(!t.exists||t.valid)return;if(t.kind==="unreadable")throw new Error(`Model file could not be read, so it could not be validated (${t.details}).
6
6
  Model: ${e}
7
7
  Path: ${n}
8
8
 
@@ -21,20 +21,20 @@ Note: 'qmd search' works without any model downloads.`):new Error(`Model file is
21
21
  Model: ${e}
22
22
  Path: ${n}
23
23
 
24
- `+(r?"The file has been removed. Run the command again to re-download.":"The file could NOT be removed. Delete it manually, then run the command again."))}o(ne,"validateGgufFile");function re(n,e=!1){return{directory:n,cli:e}}o(re,"resolveModelFileArgs");async function at(n,e={}){const t=e.cacheDir||U;y(t)||H(t,{recursive:!0});const r=[];for(const i of n){let a=!1;const d=Pe(i),l=i.split("/").pop(),h=ge(t,{withFileTypes:!0}),s=l?h.filter(f=>f.isFile()&&f.name.includes(l)).map(f=>k(t,f.name)):[];if(d&&l){const f=k(t,`${l}.etag`),g=await te(d),w=y(f)?xe(f,"utf-8").trim():null,E=e.refresh||g!==null&&(g!==w||s.length===0);if(!e.refresh&&g===null&&s.length>0&&console.warn(`Could not check for a newer ${l} (offline or unreachable remote). Keeping the cached model; it may be stale.`),E){for(const I of s)y(I)&&A(I);y(f)&&A(f),a=s.length>0}}else if(e.refresh&&l)for(const f of s)y(f)&&A(f),a=!0;const{resolveModelFile:u}=await v(),c=await u(i,re(t,e.cli===!0));ne(c,i);const p=y(c)?G(c).size:0;if(d&&l){const f=await te(d);if(f){const g=k(t,`${l}.etag`);we(g,f+`
25
- `,"utf-8")}}r.push({model:i,path:c,sizeBytes:p,refreshed:a})}return r}o(at,"pullModels");const Ie=300*1e3;function Fe(n=process.env.QMD_LLM_IDLE_TIMEOUT_MS){const e=n?.trim()??"";if(e){if(!/^\d+$/.test(e)){process.stderr.write(`QMD Warning: invalid QMD_LLM_IDLE_TIMEOUT_MS="${n}", using the default idle timeout.
26
- `);return}return Number(e)}}o(Fe,"resolveIdleTimeoutOverride");const B=2048;function Ne(n=process.env.QMD_EMBED_PARALLELISM){const e=n?.trim()??"";if(!e)return;const t=Number(e);if(!Number.isInteger(t)||t<1){process.stderr.write(`QMD Warning: invalid QMD_EMBED_PARALLELISM="${n}", using automatic parallelism.
27
- `);return}return Math.min(8,t)}o(Ne,"resolveParallelismOverride");function $(n){const e=Ne(n.envValue);return e!==void 0?e:(n.platform??process.platform)==="win32"&&n.gpu==="cuda"?1:Math.max(1,n.computed)}o($,"resolveSafeParallelism");const z=150,Ge=1e3;function Ue(n){const e=Math.max(1,n.perContextMB),t=Math.max(0,n.reserveMB??0),r=n.cap??8,i=Math.max(0,n.freeMB-t),a=Math.floor(i*.25/e);return Math.max(1,Math.min(r,a))}o(Ue,"computeGpuContextPoolSize");function Be(n){const e=n.contextSize&&n.contextSize>0?n.contextSize:2048,t=n.modelBytes/(1024*1024),r=e/2048;return!(t>0)||!Number.isFinite(t)||t<=350?Math.max(1,Math.round(z*r)):Math.max(1,Math.round(t*1.85*r))}o(Be,"estimateEmbedContextMB");function ie(n=process.env.QMD_LLAMA_GPU,e=process.env.QMD_FORCE_CPU){const t=e?.trim().toLowerCase()??"";if(t&&!["false","off","none","disable","disabled","0"].includes(t))return!1;const r=n?.trim().toLowerCase()??"";return r?["false","off","none","disable","disabled","0"].includes(r)?!1:r==="metal"||r==="vulkan"||r==="cuda"?r:(process.stderr.write(`QMD Warning: invalid QMD_LLAMA_GPU="${n}", using auto GPU selection.
24
+ `+(r?"The file has been removed. Run the command again to re-download.":"The file could NOT be removed. Delete it manually, then run the command again."))}o(ne,"validateGgufFile");function re(n,e=!1){return{directory:n,cli:e}}o(re,"resolveModelFileArgs");async function ct(n,e={}){const t=e.cacheDir||U;y(t)||H(t,{recursive:!0});const r=[];for(const i of n){let a=!1;const d=Re(i),l=i.split("/").pop(),h=ge(t,{withFileTypes:!0}),s=l?h.filter(f=>f.isFile()&&f.name.includes(l)).map(f=>k(t,f.name)):[];if(d&&l){const f=k(t,`${l}.etag`),g=await te(d),E=y(f)?xe(f,"utf-8").trim():null,w=e.refresh||g!==null&&(g!==E||s.length===0);if(!e.refresh&&g===null&&s.length>0&&console.warn(`Could not check for a newer ${l} (offline or unreachable remote). Keeping the cached model; it may be stale.`),w){for(const I of s)y(I)&&A(I);y(f)&&A(f),a=s.length>0}}else if(e.refresh&&l)for(const f of s)y(f)&&A(f),a=!0;const{resolveModelFile:u}=await v(),c=await u(i,re(t,e.cli===!0));ne(c,i);const p=y(c)?G(c).size:0;if(d&&l){const f=await te(d);if(f){const g=k(t,`${l}.etag`);Ee(g,f+`
25
+ `,"utf-8")}}r.push({model:i,path:c,sizeBytes:p,refreshed:a})}return r}o(ct,"pullModels");const Ne=300*1e3;function Ge(n=process.env.QMD_LLM_IDLE_TIMEOUT_MS){const e=n?.trim()??"";if(e){if(!/^\d+$/.test(e)){process.stderr.write(`QMD Warning: invalid QMD_LLM_IDLE_TIMEOUT_MS="${n}", using the default idle timeout.
26
+ `);return}return Number(e)}}o(Ge,"resolveIdleTimeoutOverride");const $=2048;function Ue(n=process.env.QMD_EMBED_PARALLELISM){const e=n?.trim()??"";if(!e)return;const t=Number(e);if(!Number.isInteger(t)||t<1){process.stderr.write(`QMD Warning: invalid QMD_EMBED_PARALLELISM="${n}", using automatic parallelism.
27
+ `);return}return Math.min(8,t)}o(Ue,"resolveParallelismOverride");function B(n){const e=Ue(n.envValue);return e!==void 0?e:(n.platform??process.platform)==="win32"&&n.gpu==="cuda"?1:Math.max(1,n.computed)}o(B,"resolveSafeParallelism");const z=150,$e=1e3;function Be(n){const e=Math.max(1,n.perContextMB),t=Math.max(0,n.reserveMB??0),r=n.cap??8,i=Math.max(0,n.freeMB-t),a=Math.floor(i*.25/e);return Math.max(1,Math.min(r,a))}o(Be,"computeGpuContextPoolSize");function ze(n){const e=n.contextSize&&n.contextSize>0?n.contextSize:2048,t=n.modelBytes/(1024*1024),r=e/2048;return!(t>0)||!Number.isFinite(t)||t<=350?Math.max(1,Math.round(z*r)):Math.max(1,Math.round(t*1.85*r))}o(ze,"estimateEmbedContextMB");function ie(n=process.env.QMD_LLAMA_GPU,e=process.env.QMD_FORCE_CPU){const t=e?.trim().toLowerCase()??"";if(t&&!["false","off","none","disable","disabled","0"].includes(t))return!1;const r=n?.trim().toLowerCase()??"";return r?["false","off","none","disable","disabled","0"].includes(r)?!1:r==="metal"||r==="vulkan"||r==="cuda"?r:(process.stderr.write(`QMD Warning: invalid QMD_LLAMA_GPU="${n}", using auto GPU selection.
28
28
  `),"auto"):"auto"}o(ie,"resolveLlamaGpuMode");async function oe(n,e){n&&await n.dispose(),await e.dispose()}o(oe,"disposeSequenceThenContext");async function b(n,e,t=1e3){const r=new Promise(i=>{setTimeout(()=>i("timeout"),t).unref()});try{await Promise.race([e(),r])==="timeout"&&process.stderr.write(`QMD Warning: timed out disposing ${n}; continuing shutdown.
29
29
  `)}catch(i){process.stderr.write(`QMD Warning: failed to dispose ${n} (${i instanceof Error?i.message:String(i)}); continuing shutdown.
30
- `)}}o(b,"disposeWithTimeout");function $e(n){if(n!==void 0){if(!Number.isInteger(n)||n<=0)throw new Error(`Invalid expandContextSize: ${n}. Must be a positive integer.`);return n}const e=process.env.QMD_EXPAND_CONTEXT_SIZE?.trim();if(!e)return B;const t=Number.parseInt(e,10);return!Number.isInteger(t)||t<=0?(process.stderr.write(`QMD Warning: invalid QMD_EXPAND_CONTEXT_SIZE="${e}", using default ${B}.
31
- `),B):t}o($e,"resolveExpandContextSize");const D=new Set;let Q=!1,q=!1;function ze(){return ie()===!1}o(ze,"isCpuModeRequested");class x{static{o(this,"LlamaCpp")}_ciMode=!!process.env.CI;llama=null;embedModel=null;embedModelPath=null;embedContexts=[];generateModel=null;rerankModel=null;rerankContexts=[];embedModelUri;generateModelUri;rerankModelUri;rerankMaxDocTokens;modelCacheDir;expandContextSize;embedModelLoadPromise=null;generateModelLoadPromise=null;rerankModelLoadPromise=null;rerankContextsCreatePromise=null;llamaLoadPromise=null;inactivityTimer=null;inactivityTimeoutMs;disposeModelsOnInactivity;disposed=!1;constructor(e={}){this.embedModelUri=R({embed:e.embedModel}),this.generateModelUri=J({generate:e.generateModel}),this.rerankModelUri=ee({rerank:e.rerankModel});const t=parseInt(process.env.QMD_RERANK_MAX_DOC_TOKENS??"",10);this.rerankMaxDocTokens=Number.isFinite(t)&&t>0?t:void 0,this.modelCacheDir=e.modelCacheDir||U,this.expandContextSize=$e(e.expandContextSize),this.inactivityTimeoutMs=Fe()??e.inactivityTimeoutMs??Ie,this.disposeModelsOnInactivity=e.disposeModelsOnInactivity??!1}get embedModelName(){return this.embedModelUri}get generateModelName(){return this.generateModelUri}get rerankModelName(){return this.rerankModelUri}touchActivity(){this.inactivityTimer&&(clearTimeout(this.inactivityTimer),this.inactivityTimer=null),this.inactivityTimeoutMs>0&&this.hasLoadedContexts()&&(this.inactivityTimer=setTimeout(()=>{if(typeof ce=="function"&&!ce(this)){this.touchActivity();return}this.unloadIdleResources().catch(e=>{console.error("Error unloading idle resources:",e)})},this.inactivityTimeoutMs),this.inactivityTimer.unref())}hasLoadedContexts(){return this.embedContexts.length>0||this.rerankContexts.length>0}async unloadIdleResources(){if(!this.disposed){this.inactivityTimer&&(clearTimeout(this.inactivityTimer),this.inactivityTimer=null);for(const e of this.embedContexts)await e.dispose();this.embedContexts=[];for(const e of this.rerankContexts)await e.dispose();this.rerankContexts=[],this.disposeModelsOnInactivity&&(this.embedModel&&(await this.embedModel.dispose(),this.embedModel=null,this.embedModelPath=null),this.generateModel&&(await this.generateModel.dispose(),this.generateModel=null),this.rerankModel&&(await this.rerankModel.dispose(),this.rerankModel=null),this.embedModelLoadPromise=null,this.generateModelLoadPromise=null,this.rerankModelLoadPromise=null,this.rerankContextsCreatePromise=null)}}ensureModelCacheDir(){y(this.modelCacheDir)||H(this.modelCacheDir,{recursive:!0})}async ensureLlama(e=!0){if(this.llama)return this.llama;if(this.llamaLoadPromise)return await this.llamaLoadPromise;this.llamaLoadPromise=this.loadLlamaRuntime(e);try{return await this.llamaLoadPromise}finally{this.llamaLoadPromise=null}}async loadLlamaRuntime(e=!0){if(!this.llama){const t=ie(),r=e&&ve(),{getLlama:i,getLlamaGpuTypes:a,LlamaLogLevel:d}=await v(),l=o(async(u,c=r,p)=>await K(()=>i({build:p??(c?"auto":"never"),logLevel:d.error,gpu:u,progressLogs:!1,skipDownload:!c})),"loadLlama"),h=o(async()=>{try{return await l(!1,!1)}catch(u){return q||(q=!0,process.stderr.write(`QMD Warning: CPU-only llama.cpp prebuilt not available (${u instanceof Error?u.message:String(u)}); using packaged backend with GPU offloading disabled.
30
+ `)}}o(b,"disposeWithTimeout");function Qe(n){if(n!==void 0){if(!Number.isInteger(n)||n<=0)throw new Error(`Invalid expandContextSize: ${n}. Must be a positive integer.`);return n}const e=process.env.QMD_EXPAND_CONTEXT_SIZE?.trim();if(!e)return $;const t=Number.parseInt(e,10);return!Number.isInteger(t)||t<=0?(process.stderr.write(`QMD Warning: invalid QMD_EXPAND_CONTEXT_SIZE="${e}", using default ${$}.
31
+ `),$):t}o(Qe,"resolveExpandContextSize");const D=new Set;let Q=!1,q=!1;function qe(){return ie()===!1}o(qe,"isCpuModeRequested");class x{static{o(this,"LlamaCpp")}_ciMode=!!process.env.CI;llama=null;embedModel=null;embedModelPath=null;embedContexts=[];generateModel=null;rerankModel=null;rerankContexts=[];embedModelUri;generateModelUri;rerankModelUri;rerankMaxDocTokens;modelCacheDir;expandContextSize;embedModelLoadPromise=null;generateModelLoadPromise=null;rerankModelLoadPromise=null;rerankContextsCreatePromise=null;llamaLoadPromise=null;inactivityTimer=null;inactivityTimeoutMs;disposeModelsOnInactivity;disposed=!1;constructor(e={}){this.embedModelUri=O({embed:e.embedModel}),this.generateModelUri=J({generate:e.generateModel}),this.rerankModelUri=ee({rerank:e.rerankModel}),this.rerankMaxDocTokens=Se(process.env.QMD_RERANK_MAX_DOC_TOKENS),this.modelCacheDir=e.modelCacheDir||U,this.expandContextSize=Qe(e.expandContextSize),this.inactivityTimeoutMs=Ge()??e.inactivityTimeoutMs??Ne,this.disposeModelsOnInactivity=e.disposeModelsOnInactivity??!1}get embedModelName(){return this.embedModelUri}get generateModelName(){return this.generateModelUri}get rerankModelName(){return this.rerankModelUri}touchActivity(){this.inactivityTimer&&(clearTimeout(this.inactivityTimer),this.inactivityTimer=null),this.inactivityTimeoutMs>0&&this.hasLoadedContexts()&&(this.inactivityTimer=setTimeout(()=>{if(typeof ce=="function"&&!ce(this)){this.touchActivity();return}this.unloadIdleResources().catch(e=>{console.error("Error unloading idle resources:",e)})},this.inactivityTimeoutMs),this.inactivityTimer.unref())}hasLoadedContexts(){return this.embedContexts.length>0||this.rerankContexts.length>0}async unloadIdleResources(){if(!this.disposed){this.inactivityTimer&&(clearTimeout(this.inactivityTimer),this.inactivityTimer=null);for(const e of this.embedContexts)await e.dispose();this.embedContexts=[];for(const e of this.rerankContexts)await e.dispose();this.rerankContexts=[],this.disposeModelsOnInactivity&&(this.embedModel&&(await this.embedModel.dispose(),this.embedModel=null,this.embedModelPath=null),this.generateModel&&(await this.generateModel.dispose(),this.generateModel=null),this.rerankModel&&(await this.rerankModel.dispose(),this.rerankModel=null),this.embedModelLoadPromise=null,this.generateModelLoadPromise=null,this.rerankModelLoadPromise=null,this.rerankContextsCreatePromise=null)}}ensureModelCacheDir(){y(this.modelCacheDir)||H(this.modelCacheDir,{recursive:!0})}async ensureLlama(e=!0){if(this.llama)return this.llama;if(this.llamaLoadPromise)return await this.llamaLoadPromise;this.llamaLoadPromise=this.loadLlamaRuntime(e);try{return await this.llamaLoadPromise}finally{this.llamaLoadPromise=null}}async loadLlamaRuntime(e=!0){if(!this.llama){const t=ie(),r=e&&ve(),{getLlama:i,getLlamaGpuTypes:a,LlamaLogLevel:d}=await v(),l=o(async(u,c=r,p)=>await W(()=>i({build:p??(c?"auto":"never"),logLevel:d.error,gpu:u,progressLogs:!1,skipDownload:!c})),"loadLlama"),h=o(async()=>{try{return await l(!1,!1)}catch(u){return q||(q=!0,process.stderr.write(`QMD Warning: CPU-only llama.cpp prebuilt not available (${u instanceof Error?u.message:String(u)}); using packaged backend with GPU offloading disabled.
32
32
  `)),await l("auto",!1)}},"loadCpuCompatibleLlama");let s;if(t===!1)s=await h();else if(D.has(t))process.stderr.write(`QMD Warning: skipping previously failed GPU init${t==="auto"?"":` for QMD_LLAMA_GPU=${t}`}, using CPU.
33
33
  `),s=await h();else try{if(s=await l(t),t==="auto"&&s.gpu===!1&&a){const u=(await a("allValid")).filter(c=>c!==!1&&c!=="auto");for(const c of u)if(!D.has(c))try{const p=await l(c,!1,"never");if(p.gpu!==!1){await b("CPU llama runtime",()=>s.dispose()),s=p;break}await b(`${c} probe runtime`,()=>p.dispose())}catch{D.add(c)}}}catch(u){D.add(t),process.stderr.write(`QMD Warning: GPU init failed${t==="auto"?"":` for QMD_LLAMA_GPU=${t}`} (${u instanceof Error?u.message:String(u)}), falling back to CPU.
34
34
  `),s=await h()}s.gpu===!1&&!Q&&(Q=!0,process.stderr.write(`QMD Warning: no GPU acceleration, running on CPU (slow). Run 'qmd doctor' for device diagnostics.
35
- `)),this.llama=s}return this.llama}isCpuOffloadForced(){return ze()}modelLoadOptions(e){return{modelPath:e,...this.isCpuOffloadForced()?{gpuLayers:0}:{}}}async resolveModel(e){this.ensureModelCacheDir();const{resolveModelFile:t}=await v(),r=await t(e,re(this.modelCacheDir));return ne(r,e),r}async ensureEmbedModel(){if(this.embedModel)return this.embedModel;if(this.embedModelLoadPromise)return await this.embedModelLoadPromise;this.embedModelLoadPromise=(async()=>{const e=await this.ensureLlama(),t=await this.resolveModel(this.embedModelUri),r=await e.loadModel(this.modelLoadOptions(t));return this.embedModel=r,this.embedModelPath=t,this.touchActivity(),r})();try{return await this.embedModelLoadPromise}finally{this.embedModelLoadPromise=null}}async computeParallelism(e,t=0){const r=await this.ensureLlama();if(!this.isCpuOffloadForced()&&r.gpu)try{const h=(await r.getVramState()).free/(1024*1024),s=Ue({freeMB:h,perContextMB:e,reserveMB:t});return $({gpu:r.gpu,computed:s})}catch{return $({gpu:r.gpu,computed:2})}const i=r.cpuMathCores||4,a=Math.floor(i/4),d=Math.max(1,Math.min(4,a));return $({gpu:!1,computed:d})}async threadsPerContext(e){const t=await this.ensureLlama();if(!this.isCpuOffloadForced()&&t.gpu)return 0;const r=t.cpuMathCores||4;return Math.max(1,Math.floor(r/e))}embedContextsCreatePromise=null;async ensureEmbedContexts(){if(this.embedContexts.length>0)return this.touchActivity(),this.embedContexts;if(this.embedContextsCreatePromise)return await this.embedContextsCreatePromise;this.embedContextsCreatePromise=(async()=>{const e=await this.ensureEmbedModel();let t=z;if(this.embedModelPath)try{t=Be({modelBytes:G(this.embedModelPath).size,contextSize:x.EMBED_CONTEXT_SIZE})}catch{}const r=await this.computeParallelism(t,Ge),i=await this.threadsPerContext(r);for(let a=0;a<r;a++)try{this.embedContexts.push(await e.createEmbeddingContext({contextSize:x.EMBED_CONTEXT_SIZE,...i>0?{threads:i}:{}}))}catch{if(this.embedContexts.length===0)throw new Error("Failed to create any embedding context");break}return this.touchActivity(),this.embedContexts})();try{return await this.embedContextsCreatePromise}finally{this.embedContextsCreatePromise=null}}async ensureEmbedContext(){return(await this.ensureEmbedContexts())[0]}async ensureGenerateModel(){if(!this.generateModel){if(this.generateModelLoadPromise)return await this.generateModelLoadPromise;this.generateModelLoadPromise=(async()=>{const e=await this.ensureLlama(),t=await this.resolveModel(this.generateModelUri),r=await e.loadModel(this.modelLoadOptions(t));return this.generateModel=r,r})();try{await this.generateModelLoadPromise}finally{this.generateModelLoadPromise=null}}if(this.touchActivity(),!this.generateModel)throw new Error("Generate model not loaded");return this.generateModel}async ensureRerankModel(){if(this.rerankModel)return this.rerankModel;if(this.rerankModelLoadPromise)return await this.rerankModelLoadPromise;this.rerankModelLoadPromise=(async()=>{const e=await this.ensureLlama(),t=await this.resolveModel(this.rerankModelUri),r=await e.loadModel(this.modelLoadOptions(t));return this.rerankModel=r,this.touchActivity(),r})();try{return await this.rerankModelLoadPromise}finally{this.rerankModelLoadPromise=null}}static RERANK_CONTEXT_SIZE=(()=>{const e=parseInt(process.env.QMD_RERANK_CONTEXT_SIZE??"",10);return Number.isFinite(e)&&e>0?e:4096})();static EMBED_CONTEXT_SIZE=(()=>{const e=parseInt(process.env.QMD_EMBED_CONTEXT_SIZE??"",10);return Number.isFinite(e)&&e>0?e:2048})();async ensureRerankContexts(){if(this.rerankContexts.length>0)return this.touchActivity(),this.rerankContexts;if(this.rerankContextsCreatePromise)return await this.rerankContextsCreatePromise;this.rerankContextsCreatePromise=(async()=>{this.touchActivity();const e=await this.ensureRerankModel(),t=Math.min(await this.computeParallelism(1e3),4),r=await this.threadsPerContext(t);for(let i=0;i<t;i++)try{this.rerankContexts.push(await e.createRankingContext({contextSize:x.RERANK_CONTEXT_SIZE,...r>0?{threads:r}:{}}))}catch(a){if(this.rerankContexts.length===0){const d=a instanceof Error?a.message:String(a);return console.warn(`Reranker unavailable \u2014 skipping reranking (${d}). Use --no-rerank to silence this warning.`),[]}break}return this.touchActivity(),this.rerankContexts})();try{return await this.rerankContextsCreatePromise}finally{this.rerankContextsCreatePromise=null}}async tokenize(e){if(await this.ensureEmbedContext(),!this.embedModel)throw new Error("Embed model not loaded");return this.embedModel.tokenize(e)}async countTokens(e){return(await this.tokenize(e)).length}async detokenize(e){if(await this.ensureEmbedContext(),!this.embedModel)throw new Error("Embed model not loaded");return this.embedModel.detokenize(e)}resolveEmbedTokenLimit(){const e=this.embedModel?.trainContextSize;return typeof e=="number"&&Number.isFinite(e)&&e>0?Math.max(1,Math.min(x.EMBED_CONTEXT_SIZE,e)):x.EMBED_CONTEXT_SIZE}async truncateToContextSize(e){if(!this.embedModel)return{text:e,truncated:!1,limit:x.EMBED_CONTEXT_SIZE};const t=this.resolveEmbedTokenLimit();if(t<=0)return{text:e,truncated:!1,limit:t};const r=this.embedModel.tokenize(e);if(r.length<=t)return{text:e,truncated:!1,limit:t};const i=Math.max(1,t-4),a=r.slice(0,i);return{text:this.embedModel.detokenize(a),truncated:!0,limit:t}}async embed(e,t={}){this.touchActivity();try{const r=await this.ensureEmbedContext(),{text:i,truncated:a,limit:d}=await this.truncateToContextSize(e);a&&console.warn(`\u26A0 Text truncated to fit embedding context (${d} tokens)`);const l=await r.getEmbeddingFor(i);return{embedding:Array.from(l.vector),model:t.model??this.embedModelUri}}catch(r){return console.error("Embedding error:",r),null}}async embedBatch(e,t={}){if(this._ciMode)throw new Error("LLM operations are disabled in CI (set CI=true)");if(this.touchActivity(),e.length===0)return[];try{const r=await this.ensureEmbedContexts(),i=r.length;if(i===1){const h=r[0],s=[];for(const u of e)try{const{text:c,truncated:p,limit:f}=await this.truncateToContextSize(u);p&&console.warn(`\u26A0 Batch text truncated to fit embedding context (${f} tokens)`);const g=await h.getEmbeddingFor(c);this.touchActivity(),s.push({embedding:Array.from(g.vector),model:t.model??this.embedModelUri})}catch(c){console.error("Embedding error for text:",c),s.push(null)}return s}const a=Math.ceil(e.length/i),d=Array.from({length:i},(h,s)=>e.slice(s*a,(s+1)*a));return(await Promise.all(d.map(async(h,s)=>{const u=r[s],c=[];for(const p of h)try{const{text:f,truncated:g,limit:w}=await this.truncateToContextSize(p);g&&console.warn(`\u26A0 Batch text truncated to fit embedding context (${w} tokens)`);const E=await u.getEmbeddingFor(f);this.touchActivity(),c.push({embedding:Array.from(E.vector),model:t.model??this.embedModelUri})}catch(f){console.error("Embedding error for text:",f),c.push(null)}return c}))).flat()}catch(r){return console.error("Batch embedding error:",r),e.map(()=>null)}}async generate(e,t={}){if(this._ciMode)throw new Error("LLM operations are disabled in CI (set CI=true)");this.touchActivity(),await this.ensureGenerateModel();const r=await this.generateModel.createContext(),i=r.getSequence(),{LlamaChatSession:a}=await v(),d=new a({contextSequence:i}),l=t.maxTokens??150,h=t.temperature??.7;let s="";try{return await d.prompt(e,{maxTokens:l,temperature:h,topK:20,topP:.8,onTextChunk:o(u=>{s+=u},"onTextChunk")}),{text:s,model:this.generateModelUri,done:!0}}finally{await oe(i,r)}}async modelExists(e){if(e.startsWith("hf:"))return{name:e,exists:!0};const t=y(e);return{name:e,exists:t,path:t?e:void 0}}async expandQuery(e,t={}){if(this._ciMode)throw new Error("LLM operations are disabled in CI (set CI=true)");this.touchActivity();const r=await this.ensureLlama();await this.ensureGenerateModel(),t.onModelReady?.();const i=t.includeLexical??!0,a=t.context,d=`/no_think Expand this search query: ${e}`;let l,h;try{const s=await r.createGrammar({grammar:`
35
+ `)),this.llama=s}return this.llama}isCpuOffloadForced(){return qe()}modelLoadOptions(e){return{modelPath:e,...this.isCpuOffloadForced()?{gpuLayers:0}:{}}}async resolveModel(e){this.ensureModelCacheDir();const{resolveModelFile:t}=await v(),r=await t(e,re(this.modelCacheDir));return ne(r,e),r}async ensureEmbedModel(){if(this.embedModel)return this.embedModel;if(this.embedModelLoadPromise)return await this.embedModelLoadPromise;this.embedModelLoadPromise=(async()=>{const e=await this.ensureLlama(),t=await this.resolveModel(this.embedModelUri),r=await e.loadModel(this.modelLoadOptions(t));return this.embedModel=r,this.embedModelPath=t,this.touchActivity(),r})();try{return await this.embedModelLoadPromise}finally{this.embedModelLoadPromise=null}}async computeParallelism(e,t=0){const r=await this.ensureLlama();if(!this.isCpuOffloadForced()&&r.gpu)try{const h=(await r.getVramState()).free/(1024*1024),s=Be({freeMB:h,perContextMB:e,reserveMB:t});return B({gpu:r.gpu,computed:s})}catch{return B({gpu:r.gpu,computed:2})}const i=r.cpuMathCores||4,a=Math.floor(i/4),d=Math.max(1,Math.min(4,a));return B({gpu:!1,computed:d})}async threadsPerContext(e){const t=await this.ensureLlama();if(!this.isCpuOffloadForced()&&t.gpu)return 0;const r=t.cpuMathCores||4;return Math.max(1,Math.floor(r/e))}embedContextsCreatePromise=null;async ensureEmbedContexts(){if(this.embedContexts.length>0)return this.touchActivity(),this.embedContexts;if(this.embedContextsCreatePromise)return await this.embedContextsCreatePromise;this.embedContextsCreatePromise=(async()=>{const e=await this.ensureEmbedModel();let t=z;if(this.embedModelPath)try{t=ze({modelBytes:G(this.embedModelPath).size,contextSize:x.EMBED_CONTEXT_SIZE})}catch{}const r=await this.computeParallelism(t,$e),i=await this.threadsPerContext(r);for(let a=0;a<r;a++)try{this.embedContexts.push(await e.createEmbeddingContext({contextSize:x.EMBED_CONTEXT_SIZE,...i>0?{threads:i}:{}}))}catch{if(this.embedContexts.length===0)throw new Error("Failed to create any embedding context");break}return this.touchActivity(),this.embedContexts})();try{return await this.embedContextsCreatePromise}finally{this.embedContextsCreatePromise=null}}async ensureEmbedContext(){return(await this.ensureEmbedContexts())[0]}async ensureGenerateModel(){if(!this.generateModel){if(this.generateModelLoadPromise)return await this.generateModelLoadPromise;this.generateModelLoadPromise=(async()=>{const e=await this.ensureLlama(),t=await this.resolveModel(this.generateModelUri),r=await e.loadModel(this.modelLoadOptions(t));return this.generateModel=r,r})();try{await this.generateModelLoadPromise}finally{this.generateModelLoadPromise=null}}if(this.touchActivity(),!this.generateModel)throw new Error("Generate model not loaded");return this.generateModel}async ensureRerankModel(){if(this.rerankModel)return this.rerankModel;if(this.rerankModelLoadPromise)return await this.rerankModelLoadPromise;this.rerankModelLoadPromise=(async()=>{const e=await this.ensureLlama(),t=await this.resolveModel(this.rerankModelUri),r=await e.loadModel(this.modelLoadOptions(t));return this.rerankModel=r,this.touchActivity(),r})();try{return await this.rerankModelLoadPromise}finally{this.rerankModelLoadPromise=null}}static RERANK_CONTEXT_SIZE=(()=>{const e=parseInt(process.env.QMD_RERANK_CONTEXT_SIZE??"",10);return Number.isFinite(e)&&e>0?e:4096})();static EMBED_CONTEXT_SIZE=(()=>{const e=parseInt(process.env.QMD_EMBED_CONTEXT_SIZE??"",10);return Number.isFinite(e)&&e>0?e:2048})();async ensureRerankContexts(){if(this.rerankContexts.length>0)return this.touchActivity(),this.rerankContexts;if(this.rerankContextsCreatePromise)return await this.rerankContextsCreatePromise;this.rerankContextsCreatePromise=(async()=>{this.touchActivity();const e=await this.ensureRerankModel(),t=Math.min(await this.computeParallelism(1e3),4),r=await this.threadsPerContext(t);for(let i=0;i<t;i++)try{this.rerankContexts.push(await e.createRankingContext({contextSize:x.RERANK_CONTEXT_SIZE,...r>0?{threads:r}:{}}))}catch(a){if(this.rerankContexts.length===0){const d=a instanceof Error?a.message:String(a);return console.warn(`Reranker unavailable \u2014 skipping reranking (${d}). Use --no-rerank to silence this warning.`),[]}break}return this.touchActivity(),this.rerankContexts})();try{return await this.rerankContextsCreatePromise}finally{this.rerankContextsCreatePromise=null}}async tokenize(e){if(await this.ensureEmbedContext(),!this.embedModel)throw new Error("Embed model not loaded");return this.embedModel.tokenize(e)}async countTokens(e){return(await this.tokenize(e)).length}async detokenize(e){if(await this.ensureEmbedContext(),!this.embedModel)throw new Error("Embed model not loaded");return this.embedModel.detokenize(e)}resolveEmbedTokenLimit(){const e=this.embedModel?.trainContextSize;return typeof e=="number"&&Number.isFinite(e)&&e>0?Math.max(1,Math.min(x.EMBED_CONTEXT_SIZE,e)):x.EMBED_CONTEXT_SIZE}async truncateToContextSize(e){if(!this.embedModel)return{text:e,truncated:!1,limit:x.EMBED_CONTEXT_SIZE};const t=this.resolveEmbedTokenLimit();if(t<=0)return{text:e,truncated:!1,limit:t};const r=this.embedModel.tokenize(e);if(r.length<=t)return{text:e,truncated:!1,limit:t};const i=Math.max(1,t-4),a=r.slice(0,i);return{text:this.embedModel.detokenize(a),truncated:!0,limit:t}}async embed(e,t={}){this.touchActivity();try{const r=await this.ensureEmbedContext(),{text:i,truncated:a,limit:d}=await this.truncateToContextSize(e);a&&console.warn(`\u26A0 Text truncated to fit embedding context (${d} tokens)`);const l=await r.getEmbeddingFor(i);return{embedding:Array.from(l.vector),model:t.model??this.embedModelUri}}catch(r){return console.error("Embedding error:",r),null}}async embedBatch(e,t={}){if(this._ciMode)throw new Error("LLM operations are disabled in CI (set CI=true)");if(this.touchActivity(),e.length===0)return[];try{const r=await this.ensureEmbedContexts(),i=r.length;if(i===1){const h=r[0],s=[];for(const u of e)try{const{text:c,truncated:p,limit:f}=await this.truncateToContextSize(u);p&&console.warn(`\u26A0 Batch text truncated to fit embedding context (${f} tokens)`);const g=await h.getEmbeddingFor(c);this.touchActivity(),s.push({embedding:Array.from(g.vector),model:t.model??this.embedModelUri})}catch(c){console.error("Embedding error for text:",c),s.push(null)}return s}const a=Math.ceil(e.length/i),d=Array.from({length:i},(h,s)=>e.slice(s*a,(s+1)*a));return(await Promise.all(d.map(async(h,s)=>{const u=r[s],c=[];for(const p of h)try{const{text:f,truncated:g,limit:E}=await this.truncateToContextSize(p);g&&console.warn(`\u26A0 Batch text truncated to fit embedding context (${E} tokens)`);const w=await u.getEmbeddingFor(f);this.touchActivity(),c.push({embedding:Array.from(w.vector),model:t.model??this.embedModelUri})}catch(f){console.error("Embedding error for text:",f),c.push(null)}return c}))).flat()}catch(r){return console.error("Batch embedding error:",r),e.map(()=>null)}}async generate(e,t={}){if(this._ciMode)throw new Error("LLM operations are disabled in CI (set CI=true)");this.touchActivity(),await this.ensureGenerateModel();const r=await this.generateModel.createContext(),i=r.getSequence(),{LlamaChatSession:a}=await v(),d=new a({contextSequence:i}),l=t.maxTokens??150,h=t.temperature??.7;let s="";try{return await d.prompt(e,{maxTokens:l,temperature:h,topK:20,topP:.8,onTextChunk:o(u=>{s+=u},"onTextChunk")}),{text:s,model:this.generateModelUri,done:!0}}finally{await oe(i,r)}}async modelExists(e){if(e.startsWith("hf:"))return{name:e,exists:!0};const t=y(e);return{name:e,exists:t,path:t?e:void 0}}async expandQuery(e,t={}){if(this._ciMode)throw new Error("LLM operations are disabled in CI (set CI=true)");this.touchActivity();const r=await this.ensureLlama();await this.ensureGenerateModel(),t.onModelReady?.();const i=t.includeLexical??!0,a=t.context,d=`/no_think Expand this search query: ${e}`;let l,h;try{const s=await r.createGrammar({grammar:`
36
36
  root ::= line+
37
37
  line ::= type ": " content "\\n"
38
38
  type ::= "lex" | "vec" | "hyde"
39
39
  content ::= [^\\n]+
40
- `});l=await this.generateModel.createContext({contextSize:this.expandContextSize}),h=l.getSequence();const{LlamaChatSession:u}=await v(),p=await new u({contextSequence:h}).prompt(d,{grammar:s,maxTokens:600,temperature:.7,topK:20,topP:.8,repeatPenalty:{lastTokens:64,presencePenalty:.5}}),f=Se(e,p),g=i?f:f.filter(E=>E.type!=="lex");if(g.length>0)return g;const w=[{type:"hyde",text:`Information about ${e}`},{type:"lex",text:e},{type:"vec",text:e}];return i?w:w.filter(E=>E.type!=="lex")}catch(s){console.error("Structured query expansion failed:",s);const u=[{type:"vec",text:e}];return i&&u.unshift({type:"lex",text:e}),u}finally{l&&await oe(h,l)}}static RERANK_TEMPLATE_OVERHEAD=512;static RERANK_TARGET_DOCS_PER_CONTEXT=10;async rerank(e,t,r={}){if(this._ciMode)throw new Error("LLM operations are disabled in CI (set CI=true)");this.touchActivity();const i=await this.ensureRerankContexts();if(r.onModelReady?.(),i.length===0)return{results:t.map(m=>({...m,score:.5,index:0})),model:"fallback"};const a=await this.ensureRerankModel(),d=a.tokenize(e).length,l=x.RERANK_CONTEXT_SIZE-x.RERANK_TEMPLATE_OVERHEAD-d,h=Math.min(l,_e(r.maxDocTokens)??this.rerankMaxDocTokens??l),s=new Map,u=t.map(m=>{const M=s.get(m.text);if(M!==void 0)return M===m.text?m:{...m,text:M};const C=a.tokenize(m.text),F=C.length<=h?m.text:a.detokenize(C.slice(0,h));return s.set(m.text,F),F===m.text?m:{...m,text:F}}),c=new Map;u.forEach((m,M)=>{const C=c.get(m.text);C?C.push({file:m.file,index:M}):c.set(m.text,[{file:m.file,index:M}])});const p=Array.from(c.keys()),f=Math.max(1,Math.min(i.length,Math.ceil(p.length/x.RERANK_TARGET_DOCS_PER_CONTEXT))),g=i.slice(0,f),w=Math.ceil(p.length/g.length),E=Array.from({length:g.length},(m,M)=>p.slice(M*w,(M+1)*w)).filter(m=>m.length>0),de=(await Promise.all(E.map((m,M)=>g[M].rankAll(e,m)))).flat(),ue=p.map((m,M)=>({document:m,score:de[M]})).sort((m,M)=>M.score-m.score),W=[];for(const m of ue){const M=c.get(m.document)??[];for(const C of M)W.push({file:C.file,score:m.score,index:C.index})}return{results:W,model:this.rerankModelUri}}async getDeviceInfo(e={}){const t=await this.ensureLlama(e.allowBuild??!0),r=this.isCpuOffloadForced(),i=r?[]:await t.getGpuDeviceNames();let a;if(!r&&t.gpu)try{const d=await t.getVramState();a={total:d.total,used:d.used,free:d.free}}catch{}return{gpu:r?!1:t.gpu,gpuOffloading:!r&&t.supportsGpuOffloading,gpuDevices:i,vram:a,cpuCores:t.cpuMathCores}}async dispose(){if(!this.disposed){this.disposed=!0,this.inactivityTimer&&(clearTimeout(this.inactivityTimer),this.inactivityTimer=null);for(const e of this.embedContexts)await b("embedding context",()=>e.dispose());this.embedContexts=[];for(const e of this.rerankContexts)await b("rerank context",()=>e.dispose());this.rerankContexts=[],this.embedModel&&(await b("embedding model",()=>this.embedModel.dispose()),this.embedModel=null,this.embedModelPath=null),this.generateModel&&(await b("generation model",()=>this.generateModel.dispose()),this.generateModel=null),this.rerankModel&&(await b("rerank model",()=>this.rerankModel.dispose()),this.rerankModel=null),this.llama&&(await b("llama runtime",()=>this.llama.dispose()),this.llama=null),this.embedModelLoadPromise=null,this.embedContextsCreatePromise=null,this.generateModelLoadPromise=null,this.rerankModelLoadPromise=null,this.rerankContextsCreatePromise=null,this.llamaLoadPromise=null}}}class Qe{static{o(this,"LLMSessionManager")}llm;_activeSessionCount=0;_inFlightOperations=0;constructor(e){this.llm=e}get activeSessionCount(){return this._activeSessionCount}get inFlightOperations(){return this._inFlightOperations}canUnload(){return this._activeSessionCount===0&&this._inFlightOperations===0}acquire(){this._activeSessionCount++}release(){this._activeSessionCount=Math.max(0,this._activeSessionCount-1)}operationStart(){this._inFlightOperations++}operationEnd(){this._inFlightOperations=Math.max(0,this._inFlightOperations-1)}getLlamaCpp(){return this.llm}}class se extends Error{static{o(this,"SessionReleasedError")}constructor(e="LLM session has been released or aborted"){super(e),this.name="SessionReleasedError"}}class ae{static{o(this,"LLMSession")}manager;released=!1;abortController;maxDurationTimer=null;name;constructor(e,t={}){this.manager=e,this.name=t.name||"unnamed",this.abortController=new AbortController,t.signal&&(t.signal.aborted?this.abortController.abort(t.signal.reason):t.signal.addEventListener("abort",()=>{this.abortController.abort(t.signal.reason)},{once:!0}));const r=t.maxDuration??600*1e3;r>0&&(this.maxDurationTimer=setTimeout(()=>{this.abortController.abort(new Error(`Session "${this.name}" exceeded max duration of ${r}ms`))},r),this.maxDurationTimer.unref()),this.manager.acquire()}get isValid(){return!this.released&&!this.abortController.signal.aborted}get signal(){return this.abortController.signal}release(){this.released||(this.released=!0,this.maxDurationTimer&&(clearTimeout(this.maxDurationTimer),this.maxDurationTimer=null),this.abortController.abort(new Error("Session released")),this.manager.release())}async withOperation(e){if(!this.isValid)throw new se;this.manager.operationStart();try{if(this.abortController.signal.aborted)throw new se(this.abortController.signal.reason?.message||"Session aborted");return await e()}finally{this.manager.operationEnd()}}async embed(e,t){return this.withOperation(()=>this.manager.getLlamaCpp().embed(e,t))}async embedBatch(e,t){return this.withOperation(()=>this.manager.getLlamaCpp().embedBatch(e,t))}async expandQuery(e,t){return this.withOperation(()=>this.manager.getLlamaCpp().expandQuery(e,t))}async rerank(e,t,r){return this.withOperation(()=>this.manager.getLlamaCpp().rerank(e,t,r))}}const X=new WeakMap;function le(n){let e=X.get(n);return e||(e=new Qe(n),X.set(n,e)),e}o(le,"getManagerForLlm");let T=null;function qe(){const n=Xe();return(!T||T.getLlamaCpp()!==n)&&(T=le(n)),T}o(qe,"getSessionManager");async function lt(n,e){const t=qe(),r=new ae(t,e);try{return await n(r)}finally{r.release()}}o(lt,"withLLMSession");async function ct(n,e,t){const r=le(n),i=new ae(r,t);try{return await e(i)}finally{i.release()}}o(ct,"withLLMSessionForLlm");function ce(n){if(n){const e=X.get(n);return e?e.canUnload():!0}return T?T.canUnload():!0}o(ce,"canUnloadLLM");function dt(){return process.platform!=="darwin"||process.env.QMD_METAL_KEEP_RESIDENCY==="1"?!1:process.env.GGML_METAL_NO_RESIDENCY==="1"}o(dt,"isDarwinMetalMitigationActive");function ut(){}o(ut,"installDarwinExitGuard");let L=null;function Xe(){return L||(L=new x),L}o(Xe,"getDefaultLlamaCpp");function mt(n){n!==null&&void 0,L=n}o(mt,"setDefaultLlamaCpp");async function ht(){L&&(await L.dispose(),L=null)}o(ht,"disposeDefaultLlamaCpp");export{z as BASELINE_EMBED_CONTEXT_MB,nt as DEFAULT_EMBED_MODEL_URI,it as DEFAULT_GENERATE_MODEL_URI,st as DEFAULT_MODEL_CACHE_DIR,rt as DEFAULT_RERANK_MODEL_URI,Ge as EMBED_POOL_RERANK_RESERVE_MB,x as LlamaCpp,se as SessionReleasedError,ce as canUnloadLLM,ve as canWriteLlamaDir,Ue as computeGpuContextPoolSize,ht as disposeDefaultLlamaCpp,Be as estimateEmbedContextMB,et as formatDocForEmbedding,Je as formatQueryForEmbedding,Xe as getDefaultLlamaCpp,Re as inspectGgufFile,ut as installDarwinExitGuard,dt as isDarwinMetalMitigationActive,Z as isQwen3EmbeddingModel,_e as normalizeRerankMaxDocTokens,Se as parseExpansionLines,at as pullModels,R as resolveEmbedModel,J as resolveGenerateModel,Fe as resolveIdleTimeoutOverride,ie as resolveLlamaGpuMode,ot as resolveModels,Ne as resolveParallelismOverride,ee as resolveRerankModel,$ as resolveSafeParallelism,mt as setDefaultLlamaCpp,tt as setLlamaDirWritableForTest,Ke as setNodeLlamaCppModuleForTest,lt as withLLMSession,ct as withLLMSessionForLlm,K as withNativeStdoutRedirectedToStderr};
40
+ `});l=await this.generateModel.createContext({contextSize:this.expandContextSize}),h=l.getSequence();const{LlamaChatSession:u}=await v(),p=await new u({contextSequence:h}).prompt(d,{grammar:s,maxTokens:600,temperature:.7,topK:20,topP:.8,repeatPenalty:{lastTokens:64,presencePenalty:.5}}),f=Ae(e,p),g=i?f:f.filter(w=>w.type!=="lex");if(g.length>0)return g;const E=[{type:"hyde",text:`Information about ${e}`},{type:"lex",text:e},{type:"vec",text:e}];return i?E:E.filter(w=>w.type!=="lex")}catch(s){console.error("Structured query expansion failed:",s);const u=[{type:"vec",text:e}];return i&&u.unshift({type:"lex",text:e}),u}finally{l&&await oe(h,l)}}static RERANK_TEMPLATE_OVERHEAD=512;static RERANK_TARGET_DOCS_PER_CONTEXT=10;async rerank(e,t,r={}){if(this._ciMode)throw new Error("LLM operations are disabled in CI (set CI=true)");this.touchActivity();const i=await this.ensureRerankContexts();if(r.onModelReady?.(),i.length===0)return{results:t.map(m=>({...m,score:.5,index:0})),model:"fallback"};const a=await this.ensureRerankModel(),d=a.tokenize(e).length,l=x.RERANK_CONTEXT_SIZE-x.RERANK_TEMPLATE_OVERHEAD-d,h=Math.min(l,_e(r.maxDocTokens)??this.rerankMaxDocTokens??l),s=new Map,u=t.map(m=>{const M=s.get(m.text);if(M!==void 0)return M===m.text?m:{...m,text:M};const C=a.tokenize(m.text),F=C.length<=h?m.text:a.detokenize(C.slice(0,h));return s.set(m.text,F),F===m.text?m:{...m,text:F}}),c=new Map;u.forEach((m,M)=>{const C=c.get(m.text);C?C.push({file:m.file,index:M}):c.set(m.text,[{file:m.file,index:M}])});const p=Array.from(c.keys()),f=Math.max(1,Math.min(i.length,Math.ceil(p.length/x.RERANK_TARGET_DOCS_PER_CONTEXT))),g=i.slice(0,f),E=Math.ceil(p.length/g.length),w=Array.from({length:g.length},(m,M)=>p.slice(M*E,(M+1)*E)).filter(m=>m.length>0),de=(await Promise.all(w.map((m,M)=>g[M].rankAll(e,m)))).flat(),ue=p.map((m,M)=>({document:m,score:de[M]})).sort((m,M)=>M.score-m.score),K=[];for(const m of ue){const M=c.get(m.document)??[];for(const C of M)K.push({file:C.file,score:m.score,index:C.index})}return{results:K,model:this.rerankModelUri}}async getDeviceInfo(e={}){const t=await this.ensureLlama(e.allowBuild??!0),r=this.isCpuOffloadForced(),i=r?[]:await t.getGpuDeviceNames();let a;if(!r&&t.gpu)try{const d=await t.getVramState();a={total:d.total,used:d.used,free:d.free}}catch{}return{gpu:r?!1:t.gpu,gpuOffloading:!r&&t.supportsGpuOffloading,gpuDevices:i,vram:a,cpuCores:t.cpuMathCores}}async dispose(){if(!this.disposed){this.disposed=!0,this.inactivityTimer&&(clearTimeout(this.inactivityTimer),this.inactivityTimer=null);for(const e of this.embedContexts)await b("embedding context",()=>e.dispose());this.embedContexts=[];for(const e of this.rerankContexts)await b("rerank context",()=>e.dispose());this.rerankContexts=[],this.embedModel&&(await b("embedding model",()=>this.embedModel.dispose()),this.embedModel=null,this.embedModelPath=null),this.generateModel&&(await b("generation model",()=>this.generateModel.dispose()),this.generateModel=null),this.rerankModel&&(await b("rerank model",()=>this.rerankModel.dispose()),this.rerankModel=null),this.llama&&(await b("llama runtime",()=>this.llama.dispose()),this.llama=null),this.embedModelLoadPromise=null,this.embedContextsCreatePromise=null,this.generateModelLoadPromise=null,this.rerankModelLoadPromise=null,this.rerankContextsCreatePromise=null,this.llamaLoadPromise=null}}}class Xe{static{o(this,"LLMSessionManager")}llm;_activeSessionCount=0;_inFlightOperations=0;constructor(e){this.llm=e}get activeSessionCount(){return this._activeSessionCount}get inFlightOperations(){return this._inFlightOperations}canUnload(){return this._activeSessionCount===0&&this._inFlightOperations===0}acquire(){this._activeSessionCount++}release(){this._activeSessionCount=Math.max(0,this._activeSessionCount-1)}operationStart(){this._inFlightOperations++}operationEnd(){this._inFlightOperations=Math.max(0,this._inFlightOperations-1)}getLlamaCpp(){return this.llm}}class se extends Error{static{o(this,"SessionReleasedError")}constructor(e="LLM session has been released or aborted"){super(e),this.name="SessionReleasedError"}}class ae{static{o(this,"LLMSession")}manager;released=!1;abortController;maxDurationTimer=null;name;constructor(e,t={}){this.manager=e,this.name=t.name||"unnamed",this.abortController=new AbortController,t.signal&&(t.signal.aborted?this.abortController.abort(t.signal.reason):t.signal.addEventListener("abort",()=>{this.abortController.abort(t.signal.reason)},{once:!0}));const r=t.maxDuration??600*1e3;r>0&&(this.maxDurationTimer=setTimeout(()=>{this.abortController.abort(new Error(`Session "${this.name}" exceeded max duration of ${r}ms`))},r),this.maxDurationTimer.unref()),this.manager.acquire()}get isValid(){return!this.released&&!this.abortController.signal.aborted}get signal(){return this.abortController.signal}release(){this.released||(this.released=!0,this.maxDurationTimer&&(clearTimeout(this.maxDurationTimer),this.maxDurationTimer=null),this.abortController.abort(new Error("Session released")),this.manager.release())}async withOperation(e){if(!this.isValid)throw new se;this.manager.operationStart();try{if(this.abortController.signal.aborted)throw new se(this.abortController.signal.reason?.message||"Session aborted");return await e()}finally{this.manager.operationEnd()}}async embed(e,t){return this.withOperation(()=>this.manager.getLlamaCpp().embed(e,t))}async embedBatch(e,t){return this.withOperation(()=>this.manager.getLlamaCpp().embedBatch(e,t))}async expandQuery(e,t){return this.withOperation(()=>this.manager.getLlamaCpp().expandQuery(e,t))}async rerank(e,t,r){return this.withOperation(()=>this.manager.getLlamaCpp().rerank(e,t,r))}}const X=new WeakMap;function le(n){let e=X.get(n);return e||(e=new Xe(n),X.set(n,e)),e}o(le,"getManagerForLlm");let T=null;function Ke(){const n=We();return(!T||T.getLlamaCpp()!==n)&&(T=le(n)),T}o(Ke,"getSessionManager");async function dt(n,e){const t=Ke(),r=new ae(t,e);try{return await n(r)}finally{r.release()}}o(dt,"withLLMSession");async function ut(n,e,t){const r=le(n),i=new ae(r,t);try{return await e(i)}finally{i.release()}}o(ut,"withLLMSessionForLlm");function ce(n){if(n){const e=X.get(n);return e?e.canUnload():!0}return T?T.canUnload():!0}o(ce,"canUnloadLLM");function mt(){return process.platform!=="darwin"||process.env.QMD_METAL_KEEP_RESIDENCY==="1"?!1:process.env.GGML_METAL_NO_RESIDENCY==="1"}o(mt,"isDarwinMetalMitigationActive");function ht(){}o(ht,"installDarwinExitGuard");let L=null;function We(){return L||(L=new x),L}o(We,"getDefaultLlamaCpp");function ft(n){n!==null&&void 0,L=n}o(ft,"setDefaultLlamaCpp");async function pt(){L&&(await L.dispose(),L=null)}o(pt,"disposeDefaultLlamaCpp");export{z as BASELINE_EMBED_CONTEXT_MB,it as DEFAULT_EMBED_MODEL_URI,st as DEFAULT_GENERATE_MODEL_URI,lt as DEFAULT_MODEL_CACHE_DIR,De as DEFAULT_RERANK_MAX_DOC_TOKENS,ot as DEFAULT_RERANK_MODEL_URI,$e as EMBED_POOL_RERANK_RESERVE_MB,x as LlamaCpp,se as SessionReleasedError,ce as canUnloadLLM,ve as canWriteLlamaDir,Be as computeGpuContextPoolSize,pt as disposeDefaultLlamaCpp,ze as estimateEmbedContextMB,nt as formatDocForEmbedding,tt as formatQueryForEmbedding,We as getDefaultLlamaCpp,Fe as inspectGgufFile,ht as installDarwinExitGuard,mt as isDarwinMetalMitigationActive,Z as isQwen3EmbeddingModel,_e as normalizeRerankMaxDocTokens,Ae as parseExpansionLines,ct as pullModels,O as resolveEmbedModel,J as resolveGenerateModel,Ge as resolveIdleTimeoutOverride,ie as resolveLlamaGpuMode,at as resolveModels,Ue as resolveParallelismOverride,Se as resolveRerankMaxDocTokens,ee as resolveRerankModel,B as resolveSafeParallelism,ft as setDefaultLlamaCpp,rt as setLlamaDirWritableForTest,Ze as setNodeLlamaCppModuleForTest,dt as withLLMSession,ut as withLLMSessionForLlm,W as withNativeStdoutRedirectedToStderr};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ko-qmd",
3
- "version": "2.8.3-ko.10",
3
+ "version": "2.8.3-ko.11",
4
4
  "description": "Local hybrid search for Korean Markdown notes and Obsidian vaults — BM25 with Korean particle stripping, Hangul bigram FTS, Qwen3 vector search and LLM reranking, as CLI, MCP server and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -60,7 +60,7 @@
60
60
  "@modelcontextprotocol/server": "2.3.1",
61
61
  "better-sqlite3": "^13.0.3",
62
62
  "fast-glob": "3.3.3",
63
- "node-llama-cpp": "3.22.1",
63
+ "node-llama-cpp": "3.22.2",
64
64
  "picomatch": "4.0.7",
65
65
  "sqlite-vec": "0.1.9",
66
66
  "tree-sitter-go": "0.25.0",
@@ -69,7 +69,7 @@
69
69
  "tree-sitter-typescript": "0.23.2",
70
70
  "web-tree-sitter": "0.27.1",
71
71
  "yaml": "2.9.1",
72
- "zod": "4.6.6"
72
+ "zod": "4.6.7"
73
73
  },
74
74
  "optionalDependencies": {
75
75
  "sqlite-vec-darwin-arm64": "0.1.9",
@@ -79,9 +79,9 @@
79
79
  "sqlite-vec-windows-x64": "0.1.9"
80
80
  },
81
81
  "devDependencies": {
82
- "@oxlint/plugins": "1.87.0",
82
+ "@oxlint/plugins": "1.88.0",
83
83
  "@types/better-sqlite3": "7.6.13",
84
- "oxlint": "1.87.0",
84
+ "oxlint": "1.88.0",
85
85
  "tsx": "4.23.15",
86
86
  "vitest": "3.2.7"
87
87
  },
@@ -45,7 +45,16 @@ When the user triggers `/release <version>`:
45
45
  minor series rollup via `scripts/extract-changelog.sh <version>`.
46
46
  Ask the user to confirm before pushing.
47
47
 
48
- 7. **Push** — after explicit confirmation, run `git push origin main --tags`.
48
+ 7. **Push** — after explicit confirmation, push the branch, wait for its CI,
49
+ then push only the release tag:
50
+ ```
51
+ git push origin main
52
+ git push origin v<version>
53
+ ```
54
+ Never `--tags`: this fork fetches upstream qmd's tags, so `--tags` also
55
+ pushes upstream tags (`v0.9.0`, …) that the pre-push hook rejects against
56
+ `package.json`, and would leak them into origin. The hook also blocks the
57
+ tag until CI on the tagged commit has finished green.
49
58
 
50
59
  8. **Watch CI** — after the push, start a background dispatch to watch the
51
60
  publish workflow. Use `interactive_shell` in dispatch mode with:
@@ -10,7 +10,12 @@ if [[ -z "$REPO_ROOT" ]]; then
10
10
  exit 1
11
11
  fi
12
12
 
13
- HOOKS_DIR="$REPO_ROOT/.git/hooks"
13
+ GIT_COMMON_DIR=$(git -C "$REPO_ROOT" rev-parse --git-common-dir)
14
+ case "$GIT_COMMON_DIR" in
15
+ /*) : ;;
16
+ *) GIT_COMMON_DIR="$REPO_ROOT/$GIT_COMMON_DIR" ;;
17
+ esac
18
+ HOOKS_DIR="$GIT_COMMON_DIR/hooks"
14
19
  SOURCE="$REPO_ROOT/scripts/pre-push"
15
20
 
16
21
  if [[ ! -f "$SOURCE" ]]; then