ko-qmd 2.8.3-ko.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +1937 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +184 -0
  4. package/README.md +292 -0
  5. package/bin/qmd +192 -0
  6. package/dist/ast.d.ts +65 -0
  7. package/dist/ast.js +51 -0
  8. package/dist/bench/bench.d.ts +41 -0
  9. package/dist/bench/bench.js +12 -0
  10. package/dist/bench/score.d.ts +33 -0
  11. package/dist/bench/score.js +1 -0
  12. package/dist/bench/types.d.ts +101 -0
  13. package/dist/bench/types.js +1 -0
  14. package/dist/cli/build-info.json +4 -0
  15. package/dist/cli/embed-lock.d.ts +24 -0
  16. package/dist/cli/embed-lock.js +2 -0
  17. package/dist/cli/formatter.d.ts +120 -0
  18. package/dist/cli/formatter.js +60 -0
  19. package/dist/cli/mcp-pid.d.ts +33 -0
  20. package/dist/cli/mcp-pid.js +1 -0
  21. package/dist/cli/qmd.d.ts +101 -0
  22. package/dist/cli/qmd.js +165 -0
  23. package/dist/cli/version.d.ts +42 -0
  24. package/dist/cli/version.js +1 -0
  25. package/dist/collections.d.ts +162 -0
  26. package/dist/collections.js +1 -0
  27. package/dist/db.d.ts +63 -0
  28. package/dist/db.js +2 -0
  29. package/dist/filters.d.ts +46 -0
  30. package/dist/filters.js +1 -0
  31. package/dist/grep.d.ts +85 -0
  32. package/dist/grep.js +9 -0
  33. package/dist/hangul.d.ts +102 -0
  34. package/dist/hangul.js +1 -0
  35. package/dist/index.d.ts +270 -0
  36. package/dist/index.js +1 -0
  37. package/dist/llm.d.ts +584 -0
  38. package/dist/llm.js +40 -0
  39. package/dist/maintenance.d.ts +33 -0
  40. package/dist/maintenance.js +1 -0
  41. package/dist/mcp/daemon-health.d.ts +17 -0
  42. package/dist/mcp/daemon-health.js +1 -0
  43. package/dist/mcp/origin-guard.d.ts +67 -0
  44. package/dist/mcp/origin-guard.js +1 -0
  45. package/dist/mcp/server.d.ts +131 -0
  46. package/dist/mcp/server.js +35 -0
  47. package/dist/memory.d.ts +74 -0
  48. package/dist/memory.js +13 -0
  49. package/dist/metadata-filter.d.ts +74 -0
  50. package/dist/metadata-filter.js +1 -0
  51. package/dist/metadata-store.d.ts +45 -0
  52. package/dist/metadata-store.js +68 -0
  53. package/dist/metadata.d.ts +61 -0
  54. package/dist/metadata.js +1 -0
  55. package/dist/paths.d.ts +1 -0
  56. package/dist/paths.js +1 -0
  57. package/dist/query-log.d.ts +156 -0
  58. package/dist/query-log.js +2 -0
  59. package/dist/refresh.d.ts +60 -0
  60. package/dist/refresh.js +1 -0
  61. package/dist/rerank-input.d.ts +46 -0
  62. package/dist/rerank-input.js +5 -0
  63. package/dist/store.d.ts +1267 -0
  64. package/dist/store.js +421 -0
  65. package/dist/trust.d.ts +152 -0
  66. package/dist/trust.js +2 -0
  67. package/package.json +124 -0
  68. package/scripts/build.mjs +130 -0
  69. package/scripts/check-package-grammars.mjs +29 -0
  70. package/scripts/package-smoke.mjs +65 -0
  71. package/scripts/test-all.mjs +42 -0
  72. package/skills/qmd/SKILL.md +333 -0
  73. package/skills/qmd/references/mcp-setup.md +122 -0
  74. package/skills/release/SKILL.md +141 -0
  75. package/skills/release/scripts/install-hooks.sh +38 -0
  76. package/skills/release/scripts/release-context.sh +129 -0
package/dist/llm.js ADDED
@@ -0,0 +1,40 @@
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}).
6
+ Model: ${e}
7
+ Path: ${n}
8
+
9
+ The file has been left in place. If this repeats, check open file limits, permissions, and whether the volume holding the model cache is still mounted.`);let r=!0;try{A(n)}catch{r=!1}throw t.kind==="html"?new Error(`Downloaded model file is an HTML page, not a GGUF model (${_(t.sizeBytes??0)}).
10
+ Something is intercepting the download from huggingface.co (a proxy, firewall, or captive portal).
11
+
12
+ Model: ${e}
13
+ Path: ${n}
14
+
15
+ To fix this, either:
16
+ 1. Try a HuggingFace mirror: HF_ENDPOINT=https://hf-mirror.com qmd embed
17
+ 2. Download the model manually and set the env var, e.g.:
18
+ QMD_EMBED_MODEL=/path/to/model.gguf qmd embed
19
+
20
+ Note: 'qmd search' works without any model downloads.`):new Error(`Model file is not valid GGUF (expected magic "GGUF", got "${t.magic??"unknown"}", file is ${_(t.sizeBytes??0)}).
21
+ Model: ${e}
22
+ Path: ${n}
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.
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
+ `)}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.
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
+ `),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
+ `),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:`
36
+ root ::= line+
37
+ line ::= type ": " content "\\n"
38
+ type ::= "lex" | "vec" | "hyde"
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};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Maintenance - Database cleanup operations for QMD.
3
+ *
4
+ * Wraps low-level store operations that the CLI needs for housekeeping.
5
+ * Takes an internal Store in the constructor — allowed to access DB directly.
6
+ */
7
+ import type { Store } from "./store.js";
8
+ import { type CleanupStats } from "./store.js";
9
+ export declare class Maintenance {
10
+ private store;
11
+ constructor(store: Store);
12
+ /** Run VACUUM on the SQLite database to reclaim space */
13
+ vacuum(): void;
14
+ /** Remove content rows that are no longer referenced by any document */
15
+ cleanupOrphanedContent(): number;
16
+ /** Remove vector embeddings for content that no longer exists */
17
+ cleanupOrphanedVectors(): number;
18
+ /** Clear the LLM response cache (query expansion, reranking) */
19
+ clearLLMCache(): number;
20
+ /** Delete documents marked as inactive (removed from filesystem) */
21
+ deleteInactiveDocs(): number;
22
+ /** Clear all vector embeddings (forces re-embedding) */
23
+ clearEmbeddings(): void;
24
+ /** Compact FTS5 so deleted rows leave documents_fts_data (#550). */
25
+ optimizeFts(): void;
26
+ /** Preview what {@link run} would remove, without writing. */
27
+ preview(): CleanupStats;
28
+ /**
29
+ * Full cleanup: cache, orphaned vectors, inactive docs, orphaned content,
30
+ * FTS optimize, vacuum. Same sequence as `qmd cleanup`.
31
+ */
32
+ run(): CleanupStats;
33
+ }
@@ -0,0 +1 @@
1
+ var s=Object.defineProperty;var r=(t,e)=>s(t,"name",{value:e,configurable:!0});import{vacuumDatabase as n,cleanupOrphanedContent as a,cleanupOrphanedVectors as o,deleteLLMCache as c,deleteInactiveDocuments as i,clearAllEmbeddings as u,optimizeDocumentsFts as d,previewCleanup as h,runCleanup as p}from"./store.js";class m{static{r(this,"Maintenance")}store;constructor(e){this.store=e}vacuum(){n(this.store.db)}cleanupOrphanedContent(){return a(this.store.db)}cleanupOrphanedVectors(){return o(this.store.db)}clearLLMCache(){return c(this.store.db)}deleteInactiveDocs(){return i(this.store.db)}clearEmbeddings(){u(this.store.db)}optimizeFts(){d(this.store.db)}preview(){return h(this.store.db)}run(){return p(this.store.db)}}export{m as Maintenance};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ko-qmd: periodic health line for the HTTP daemon's log.
3
+ *
4
+ * One daemon ran ten days and died with "Reached heap limit" after its heap
5
+ * crept to 3.8 GB under nothing but idle discover/cancelled traffic. A line an
6
+ * hour of heap, rss and open `subscriptions/listen` streams gives the slope of
7
+ * that growth and whether it tracks the listen count.
8
+ */
9
+ export type DaemonRequestCounts = {
10
+ /** `subscriptions/listen` requests whose handler is still awaiting the response body — not open sockets */
11
+ listens: number;
12
+ /** the same for every /mcp request, listens included */
13
+ inflight: number;
14
+ };
15
+ export declare function formatDaemonHealthLine(memory: Pick<NodeJS.MemoryUsage, "heapUsed" | "heapTotal" | "rss">, counts: DaemonRequestCounts, uptimeMs: number): string;
16
+ /** QMD_HEALTH_LOG_INTERVAL_MS: 0 = off, an integer in [1000, 2^31-1] = that many ms, anything else = one hour. */
17
+ export declare function resolveHealthLogIntervalMs(envValue?: string | undefined): number;
@@ -0,0 +1 @@
1
+ var r=Object.defineProperty;var _=(t,e)=>r(t,"name",{value:e,configurable:!0});const T=1048576,h=36e5;function A(t,e,s){const n=_(o=>`${Math.round(o/1048576)}MB`,"mb");return`health heap_used=${n(t.heapUsed)} heap_total=${n(t.heapTotal)} rss=${n(t.rss)} listens=${e.listens} mcp_inflight=${e.inflight} uptime=${(s/36e5).toFixed(1)}h`}_(A,"formatDaemonHealthLine");const M=2**31-1,i=1e3;function E(t=process.env.QMD_HEALTH_LOG_INTERVAL_MS){if(t===void 0||!/^\d+$/.test(t))return 36e5;const e=Number(t);return e!==0&&e<i?36e5:e<=M?e:36e5}_(E,"resolveHealthLogIntervalMs");export{A as formatDaemonHealthLine,E as resolveHealthLogIntervalMs};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * origin-guard.ts — DNS-rebinding protection for the HTTP MCP transport.
3
+ *
4
+ * Binding to localhost keeps *remote* clients out, but it does not keep a web
5
+ * page out. A page served from `attacker.example` can re-point that hostname at
6
+ * `127.0.0.1` after load (DNS rebinding); the browser then treats
7
+ * `http://attacker.example:8181` as same-origin with the page and hands the
8
+ * response body to attacker JavaScript. The only server-side signal that
9
+ * distinguishes such a request from a legitimate local client is the `Origin`
10
+ * and `Host` headers, so the MCP spec requires local HTTP servers to validate
11
+ * them.
12
+ *
13
+ * The predicates here are pure so they can be unit-tested without a socket;
14
+ * `startMcpHttpServer` applies them to every request before routing.
15
+ */
16
+ /**
17
+ * Hostnames that resolve to this machine's loopback interface.
18
+ *
19
+ * `0.0.0.0` is deliberately excluded: browsers happily route it to loopback on
20
+ * Linux and macOS, which makes it a rebinding-free path to local servers, so an
21
+ * `Origin: http://0.0.0.0:8181` is treated as untrusted like any other.
22
+ */
23
+ export declare function isLoopbackHostname(hostname: string): boolean;
24
+ export type OriginGuard = {
25
+ /** All checks disabled (`QMD_ALLOWED_ORIGINS=*`). */
26
+ disabled: boolean;
27
+ /** Extra origins accepted beyond the loopback defaults, normalized. */
28
+ allowedOrigins: string[];
29
+ /** Extra `Host` values accepted beyond the loopback defaults, lowercased. */
30
+ allowedHosts: string[];
31
+ /** Whether the `Host` header is checked at all — see `resolveOriginGuard`. */
32
+ enforceHost: boolean;
33
+ };
34
+ /**
35
+ * Build the guard for a server bound to `host`.
36
+ *
37
+ * `Host` validation is enforced whenever the bind address names a concrete
38
+ * interface, since the legitimate `Host` header is then known (loopback, or the
39
+ * bind address itself). For a wildcard bind — `--host 0.0.0.0`, e.g. Docker —
40
+ * any of the container's names may legitimately appear, so the check is only
41
+ * enforced when the operator supplies an explicit allowlist. `Origin`
42
+ * validation always applies; browsers are the threat, and they always send it.
43
+ */
44
+ export declare function resolveOriginGuard(options: {
45
+ host: string;
46
+ allowedOrigins?: string[];
47
+ allowedHosts?: string[];
48
+ env?: NodeJS.ProcessEnv;
49
+ }): OriginGuard;
50
+ export type GuardVerdict = {
51
+ ok: true;
52
+ } | {
53
+ ok: false;
54
+ reason: string;
55
+ };
56
+ /**
57
+ * Validate a request's `Origin` and `Host` headers against the guard.
58
+ *
59
+ * A missing `Origin` is allowed: non-browser clients (curl, the MCP SDK's HTTP
60
+ * client, editors) omit it, and browsers cannot. The header is the signal that
61
+ * a request came from page JavaScript, so its *absence* is not suspicious while
62
+ * a foreign *value* is conclusive.
63
+ */
64
+ export declare function checkRequestOrigin(headers: {
65
+ origin?: string | undefined;
66
+ host?: string | undefined;
67
+ }, guard: OriginGuard): GuardVerdict;
@@ -0,0 +1 @@
1
+ var w=Object.defineProperty;var r=(t,e)=>w(t,"name",{value:e,configurable:!0});const h=new Set(["","0.0.0.0","::","[::]","*"]);function c(t){const e=t.trim().toLowerCase().replace(/^\[|\]$/g,"");if(e==="localhost"||e.endsWith(".localhost")||e==="::1"||e==="0:0:0:0:0:0:0:1")return!0;const o=e.startsWith("::ffff:")?e.slice(7):e;return/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(o)}r(c,"isLoopbackHostname");function m(t){try{const e=new URL(t.trim());return e.protocol!=="http:"&&e.protocol!=="https:"?void 0:e.hostname}catch{return}}r(m,"originHostname");function u(t){try{return new URL(t.trim()).origin.toLowerCase()}catch{return}}r(u,"normalizeOrigin");function H(t){try{return new URL(`http://${t.trim()}`).hostname}catch{return}}r(H,"hostHeaderHostname");function f(t){return t?t.split(",").map(e=>e.trim()).filter(e=>e.length>0):[]}r(f,"splitList");function p(t){const e=t.env??process.env,o=t.allowedOrigins??f(e.QMD_ALLOWED_ORIGINS),s=t.allowedHosts??f(e.QMD_ALLOWED_HOSTS);if(o.includes("*"))return{disabled:!0,allowedOrigins:[],allowedHosts:[],enforceHost:!1};const i=o.map(a=>u(a)).filter(a=>a!==void 0),n=s.map(a=>a.toLowerCase()),l=t.host.trim().toLowerCase(),d=h.has(l);return!d&&!c(l)&&n.push(l),{disabled:!1,allowedOrigins:i,allowedHosts:n,enforceHost:!d||n.length>0}}r(p,"resolveOriginGuard");function O(t,e){if(e.disabled)return{ok:!0};const o=t.origin?.trim();if(o){const i=u(o),n=m(o);if(!(n!==void 0&&c(n)||i!==void 0&&e.allowedOrigins.includes(i)))return{ok:!1,reason:`Origin not allowed: ${o}`}}const s=t.host?.trim();if(e.enforceHost&&s){const i=s.toLowerCase(),n=H(s);if(!(n!==void 0&&c(n)||e.allowedHosts.includes(i)||n!==void 0&&e.allowedHosts.includes(n.toLowerCase())))return{ok:!1,reason:`Host not allowed: ${s}`}}return{ok:!0}}r(O,"checkRequestOrigin");export{O as checkRequestOrigin,c as isLoopbackHostname,p as resolveOriginGuard};
@@ -0,0 +1,131 @@
1
+ /**
2
+ * QMD MCP Server - Model Context Protocol server for QMD
3
+ *
4
+ * Exposes QMD search and document retrieval as MCP tools and resources.
5
+ * Documents are accessible via qmd:// URIs.
6
+ *
7
+ * Speaks MCP spec 2026-07-28 (stateless, no initialize handshake) and dual-speaks
8
+ * 2025-era clients via the official SDK entries (`serveStdio` / `createMcpHandler`).
9
+ */
10
+ export type StatusCollection = {
11
+ name: string;
12
+ path: string | null;
13
+ pattern: string | null;
14
+ documents: number;
15
+ lastUpdated: string;
16
+ };
17
+ /**
18
+ * Render one collection line for the status text summary.
19
+ * path is null when the store_collections row is gone but documents remain
20
+ * active (e.g. after removeCollection); render a human token instead of the
21
+ * literal "null". structuredContent keeps the null unchanged.
22
+ */
23
+ export declare function formatStatusCollectionLine(col: Pick<StatusCollection, "name" | "path" | "documents">): string;
24
+ export declare function resolveDeadlineMs(envValue: string | undefined, fallback: number): number;
25
+ export type McpStartupOptions = {
26
+ dbPath?: string;
27
+ };
28
+ /**
29
+ * Counts running request handlers so shutdown can wait for them to settle
30
+ * before tearing down their llm/store dependencies. The SDK aborts in-flight
31
+ * request controllers on close, but qmd's handlers finish their current
32
+ * store/llm work rather than observing the signal mid-operation.
33
+ */
34
+ export type InflightGate = {
35
+ /** Wraps a handler so the gate counts it while it runs. */
36
+ track<T extends (...args: never[]) => unknown>(fn: T): T;
37
+ /** Resolves once no tracked handler runs, or after timeoutMs. Returns whether idle was reached. */
38
+ waitForIdle(timeoutMs: number): Promise<boolean>;
39
+ };
40
+ export declare function createInflightGate(): InflightGate;
41
+ /** Minimal stdin surface consumed by registerStdioEofShutdown, injectable for tests. */
42
+ export type StdioShutdownStdin = {
43
+ once(event: "end" | "close", listener: () => void): unknown;
44
+ off(event: "end" | "close", listener: () => void): unknown;
45
+ readableEnded?: boolean;
46
+ destroyed?: boolean;
47
+ };
48
+ export type StdioShutdownOptions = {
49
+ /** Closes the MCP server and its transport. */
50
+ closeServer: () => Promise<void>;
51
+ /** Closes the SQLite store (owns disposing the per-store llama.cpp instance). */
52
+ closeStore: () => void | Promise<void>;
53
+ /**
54
+ * Optional extra llama.cpp teardown, run before closeStore. The MCP store
55
+ * disposes its own per-store LlamaCpp inside closeStore, so this is left
56
+ * unset there; it exists for callers that own a separate instance. If
57
+ * omitted, the step is skipped (do NOT default it to the global
58
+ * disposeDefaultLlamaCpp — that would tear down an unrelated instance in an
59
+ * embedded process).
60
+ */
61
+ disposeLlm?: () => Promise<void>;
62
+ /** Waits for in-flight handlers to settle (see InflightGate.waitForIdle). */
63
+ waitForIdle?: (timeoutMs: number) => Promise<boolean>;
64
+ /** Deadline for the in-flight wait. Defaults to 5000 ms. */
65
+ idleTimeoutMs?: number;
66
+ /** Defaults to process.stdin. */
67
+ stdin?: StdioShutdownStdin;
68
+ /** Defaults to assigning process.exitCode. */
69
+ setExitCode?: (code: number) => void;
70
+ /** Defaults to reading process.exitCode. */
71
+ getExitCode?: () => number | undefined;
72
+ /** Defaults to process.stderr. */
73
+ stderr?: {
74
+ write(chunk: string): unknown;
75
+ on?(event: "error", listener: (err: unknown) => void): unknown;
76
+ };
77
+ };
78
+ /**
79
+ * Shut the stdio MCP server down when stdin reaches EOF (#751).
80
+ *
81
+ * The SDK's StdioServerTransport subscribes to stdin "data"/"error" only and
82
+ * never notices "end"/"close". When the parent MCP client dies, nothing tears
83
+ * the process down: the warm llama.cpp model's native handles keep the event
84
+ * loop alive, so the server reparents to PID 1, leaks RAM, and keeps the
85
+ * SQLite index open. stdin EOF means the client is gone, so this treats it as
86
+ * a disconnect: no new requests are accepted and nobody is left to read a
87
+ * response — but handlers that are already running get a bounded window to
88
+ * settle (waitForIdle) before their llm/store dependencies are torn down.
89
+ *
90
+ * Teardown order matters. Close the transport first so no further requests
91
+ * are dispatched, wait for in-flight handlers, then close the store last —
92
+ * which disposes the store's own llama.cpp instance and then the database, so
93
+ * the dispose path cannot hit an already-closed DB. (disposeLlm is an optional
94
+ * extra step for callers that own a separate instance; the MCP store does
95
+ * not.) Failures are logged best-effort (the parent's death may have closed
96
+ * stderr too) and do not stop the remaining steps. The function sets process.exitCode
97
+ * instead of calling process.exit() so `beforeExit` still fires and
98
+ * node-llama-cpp's auto-dispose runs before libc's static destructors —
99
+ * process.exit() during native-addon unload has caused exit-time crashes
100
+ * before (#59, #129; same rationale as finishSuccessfulCliCommand in the CLI).
101
+ *
102
+ * Returns the idempotent shutdown function: every invocation (manual, "end",
103
+ * "close", or already-ended stdin) shares one promise, and the promise never
104
+ * rejects.
105
+ */
106
+ export declare function registerStdioEofShutdown(options: StdioShutdownOptions): () => Promise<void>;
107
+ export declare function startMcpServer(options?: McpStartupOptions): Promise<void>;
108
+ export type HttpServerHandle = {
109
+ httpServer: import("http").Server;
110
+ port: number;
111
+ stop: () => Promise<void>;
112
+ };
113
+ /**
114
+ * Start MCP server over Streamable HTTP (JSON responses by default).
115
+ * Binds to `options.host` (default "localhost", overridable via the QMD_HOST
116
+ * env var) — set "0.0.0.0" to accept connections from other hosts, e.g. a
117
+ * container liveness probe. Returns a handle for shutdown and port discovery.
118
+ *
119
+ * HTTP is sessionless (MCP 2026-07-28): there is no `Mcp-Session-Id`, no
120
+ * initialize handshake, and no idle-session TTL. 2025-era clients are still
121
+ * served per-request via the SDK's stateless legacy fallback (initialize
122
+ * works as a standalone call; subsequent 2025 methods need a modern envelope
123
+ * or a stdio connection). The previous session reaper (#816) is gone because
124
+ * there are no sessions to reap.
125
+ */
126
+ export declare function startMcpHttpServer(port: number, options?: ({
127
+ quiet?: boolean;
128
+ host?: string;
129
+ allowedOrigins?: string[];
130
+ allowedHosts?: string[];
131
+ } & McpStartupOptions)): Promise<HttpServerHandle>;
@@ -0,0 +1,35 @@
1
+ var qe=Object.defineProperty;var c=(e,r)=>qe(e,"name",{value:r,configurable:!0});import{createServer as Ne}from"node:http";import{readFileSync as je}from"node:fs";import{join as Y,dirname as Oe,sep as Ie}from"node:path";import{fileURLToPath as ae}from"url";import{createMcpHandler as Pe,McpServer as He,ResourceTemplate as _e}from"@modelcontextprotocol/server";import{serveStdio as Le}from"@modelcontextprotocol/server/stdio";import{z as o}from"zod";import{existsSync as ce}from"fs";import{createStore as le,extractSnippet as X,addLineNumbers as Q,getDefaultDbPath as de,DEFAULT_MULTI_GET_MAX_BYTES as ue,parseMetadataFilter as Fe}from"../index.js";import{getConfigPath as R}from"../collections.js";import{enableProductionMode as pe}from"../store.js";import{checkRequestOrigin as Ae,resolveOriginGuard as We}from"./origin-guard.js";import{entryFromMcp as Be,entryFromRest as Ge,isoWithOffset as Je,logQuery as he,queryLogStatus as Qe}from"../query-log.js";import{formatDaemonHealthLine as Ue,resolveHealthLogIntervalMs as Ke}from"./daemon-health.js";import{createStoreIndexRefresher as ze}from"../refresh.js";import{buildDocumentFilter as Z,describeFilter as ee}from"../filters.js";import{countFilteredDocuments as Ve,getHashesNeedingEmbedding as Ye,DEFAULT_EMBED_MODEL as Xe}from"../store.js";import{normalizeRerankMaxDocTokens as Re}from"../llm.js";import{compileGrepPattern as Ze,grepDocuments as et}from"../grep.js";import{writeFile as tt,appendFile as nt,readFile as rt,lstat as ot,realpath as me,unlink as it}from"node:fs/promises";import{MEMORY_COLLECTION as A,validateMemoryStoreInput as fe,validateCorrection as st,parseSupersededBy as at,buildMemoryFilename as ct,renderMemoryDoc as ye,renderSupersededFooter as lt,resolveInCollection as dt,buildStoreReceipt as ge,VECTORS_STALE_NOTICE as ut}from"../memory.js";import{extractDocumentMetadata as pt}from"../metadata.js";const U=c(e=>Math.round(e*100)/100,"roundScore"),te=c(e=>e?{...e.vectorScores.length>0&&{vec_score:U(Math.max(...e.vectorScores))},...e.ftsScores.length>0&&{fts_score:U(Math.max(...e.ftsScores))}}:{},"rawBackendScores");function ne(e){if(e===void 0)return{};try{return{filter:Fe(e)}}catch(r){return{error:r instanceof Error?r.message:String(r)}}}c(ne,"validateFilterArgument");function ht(e){return` - ${e.name}: ${e.path??"(removed)"} (${e.documents} docs)`}c(ht,"formatStatusCollectionLine");function re(e){return e.split("/").map(r=>encodeURIComponent(r)).join("/")}c(re,"encodeQmdPath");function we(e,r){if(e.length===0)return`No results found for "${r}"`;const l=[`Found ${e.length} result${e.length===1?"":"s"} for "${r}":
2
+ `];for(const t of e)l.push(`${t.docid} ${Math.round(t.score*100)}% ${t.file} - ${t.title}`);return l.join(`
3
+ `)}c(we,"formatSearchSummary");function mt(){try{const e=Y(Oe(ae(import.meta.url)),"../../package.json");return JSON.parse(je(e,"utf-8")).version??"unknown"}catch{return"unknown"}}c(mt,"getPackageVersion");const be=new WeakMap,K=new Map;function ft(e,r){const t=(K.get(e)??Promise.resolve()).then(r,r),x=t.catch(()=>{});return K.set(e,x),x.then(()=>{K.get(e)===x&&K.delete(e)}),t}c(ft,"withFileLock");function xe(){const e={},r=[];return{stages:e,timeouts:r,hooks:{onDeadline:c((l,t)=>{t!==void 0&&(e[l]=t),r.push(l)},"onDeadline"),onModelLoad:c((l,t)=>{e.load=(e.load??0)+t},"onModelLoad"),onExpand:c((l,t,x)=>{t.length>0&&(e.expand=x)},"onExpand"),onFtsDone:c(l=>{e.fts=l},"onFtsDone"),onEmbedDone:c(l=>{e.embed=l},"onEmbedDone"),onVecDone:c(l=>{e.vec=l},"onVecDone"),onChunkDone:c(l=>{e.chunk=l},"onChunkDone"),onRerankDone:c(l=>{e.rerank=l},"onRerankDone")}}}c(xe,"stageTimer");const yt=5e3,gt=1e4,wt=2**31-1;function Se(e,r){if(e===void 0||!/^\d+$/.test(e))return r;const l=Number(e);return l<=wt?l:r}c(Se,"resolveDeadlineMs");function oe(){return{expandDeadlineMs:Se(process.env.QMD_EXPAND_DEADLINE_MS,yt),rerankDeadlineMs:Se(process.env.QMD_RERANK_DEADLINE_MS,gt)}}c(oe,"searchDeadlines");function ve(e){return e.length>0?{timeouts:e}:{}}c(ve,"timeoutField");function $e(e){return e.length===0?"":`
4
+
5
+ Note: ${e.map(l=>l==="rerank"?"the reranker timed out, so results are in retrieval order and scores are 1/rank":"query expansion timed out, so only the original query was searched").join("; ")}.`}c($e,"describeTimeouts");async function z(e,r){const l=Ee(e);try{const t=r.length>0?r:(await e.listCollections()).map(x=>x.name);await l.refreshIfStale(t)}catch(t){console.error(`qmd: index refresh failed: ${t instanceof Error?t.message:String(t)}`)}}c(z,"refreshBeforeQuery");async function ke(e,r){try{await Ee(e).refreshNow([r])}catch(l){console.error(`qmd: index refresh failed: ${l instanceof Error?l.message:String(l)}`)}}c(ke,"refreshAfterWrite");function Ee(e){let r=be.get(e);return r||(r=ze(e,{configPath:R(),onError:c((l,t)=>{console.error(`qmd: index refresh for '${l}' failed: ${t instanceof Error?t.message:String(t)}`)},"onError")}),be.set(e,r)),r}c(Ee,"refresherFor");function bt(e,r,l){const{kept:t,total:x}=Ve(e.internal.db,r,l);return t===0?`No results \u2014 ${ee(r)} matched 0 of ${x} documents. Widen or drop the filter.`:`No results in the ${t} of ${x} documents matching ${ee(r)}.`}c(bt,"describeEmptyFilteredSearch");async function xt(e){const r=await e.getStatus(),l=await e.getGlobalContext(),t=[];if(t.push(`QMD is your local search engine over ${r.totalDocuments} markdown documents.`),l&&t.push(`Context: ${l}`),r.collections.length>0){t.push("");const x=r.collections.map(O=>O.name).join(", ");t.push(`Collections (scope with \`collections\` parameter): ${x}`),t.push("Call the `status` tool for collection descriptions, paths, and per-collection doc counts.")}return r.hasVectorIndex?r.needsEmbedding>0&&(t.push(""),t.push(`Note: ${r.needsEmbedding} documents need embedding. Run \`qmd embed\` to update.`)):(t.push(""),t.push("Note: No vector embeddings yet. Run `qmd embed` to enable semantic search (vec/hyde).")),t.push(""),t.push("Search: Use `query` with sub-queries (lex/vec/hyde):"),t.push(" - type:'lex' \u2014 BM25 keyword search (exact terms, fast)"),t.push(" - type:'vec' \u2014 semantic vector search (meaning-based)"),t.push(" - type:'hyde' \u2014 hypothetical document (write what the answer looks like)"),t.push(""),t.push(" Always provide `intent` on every search call to disambiguate and improve snippets."),t.push(""),t.push("Examples:"),t.push(" Quick keyword lookup: [{type:'lex', query:'error handling'}]"),t.push(" Semantic search: [{type:'vec', query:'how to handle errors gracefully'}]"),t.push(" Best results: [{type:'lex', query:'error'}, {type:'vec', query:'error handling best practices'}]"),t.push(" With intent: searches=[{type:'lex', query:'performance'}], intent='web page load times'"),t.push(""),t.push(" Narrow a search with `path` (globs against collection/path, `!` excludes), `since` and `until`"),t.push(" (a span like '7d' or a date). They cut the corpus before ranking, so a narrow filter still"),t.push(" returns its own best matches."),t.push(""),t.push("Exact matching: use `grep` when `query` should have found something and did not."),t.push(" It matches document bodies by string or regex \u2014 no ranking \u2014 and returns every matching"),t.push(" line with a line number that `get` takes: `file: 'notes/x.md:120:40'`. Reach for it when a"),t.push(" term is an identifier, an error string or a config key, or when tokenization may never have"),t.push(" produced the word the document contains (common with Korean text)."),t.push(""),t.push("Retrieval:"),t.push(" - `get` \u2014 single document by path or docid (#abc123). Supports a line-range suffix: `file.md:100` (from line 100) or `file.md:100:40` (40 lines from line 100)."),t.push(" - `multi_get` \u2014 batch retrieve by glob (`journals/2025-05*.md`), comma-separated list, or docids (#abc123)."),t.push(""),t.push("Tips:"),t.push(" - File paths in results are relative to their collection."),t.push(" - Use `minScore: 0.5` to filter low-confidence results."),t.push(" - Results include a `context` field describing the content type."),t.join(`
6
+ `)}c(xt,"buildInstructions");async function Me(e,r,l){const t=r?.track??(n=>n),x=new He({name:"qmd",version:mt()},{instructions:await xt(e),cacheHints:{"tools/list":{ttlMs:6e4,cacheScope:"private"},"server/discover":{ttlMs:6e4,cacheScope:"private"},"resources/read":{ttlMs:0,cacheScope:"private"}}}),O=await e.getDefaultCollectionNames();x.registerResource("document",new _e("qmd://{+path}",{list:void 0}),{title:"QMD Document",description:"A markdown document from your QMD knowledge base. Use search tools to discover documents.",mimeType:"text/markdown"},t(async(n,{path:s})=>{const f=Array.isArray(s)?s.join("/"):s||"",S=decodeURIComponent(f),u=await e.get(S,{includeBody:!0});if("error"in u){const E=u.error==="excluded_by_ignore"?`Document excluded by ignore rule: ${S}
7
+ Collection: ${u.collection}
8
+ Matched path: ${u.path}
9
+ Ignore rule: ${u.rule}`:`Document not found: ${S}`;return{contents:[{uri:n.href,text:E}]}}let b=Q(u.body||"");return u.context&&(b=`<!-- Context: ${u.context} -->
10
+
11
+ `+b),{contents:[{uri:n.href,name:u.displayPath,title:u.title||u.displayPath,mimeType:"text/markdown",text:b}]}}));const H=o.object({type:o.enum(["lex","vec","hyde"]).describe('lex = BM25 keywords (supports "phrase" and -negation); vec = semantic question; hyde = hypothetical answer passage'),query:o.string().describe('The query text. For lex: use keywords, "quoted phrases", and -negation. For vec: natural language question. For hyde: 50-100 word answer passage.')});x.registerTool("query",{title:"Query",description:'Search the knowledge base using a query document \u2014 one or more typed sub-queries combined for best recall.\n\nEach result includes a `line` field with the absolute 1-indexed line of the best match in the source markdown. To read more context around a hit, call `get(file, fromLine = max(1, line - 20), maxLines = 80, lineNumbers = true)`.\n\n## Query Types\n\n**lex** \u2014 BM25 keyword search. Fast, exact, no LLM needed.\nFull lex syntax:\n- `term` \u2014 prefix match ("perf" matches "performance")\n- `"exact phrase"` \u2014 phrase must appear verbatim\n- `-term` or `-"phrase"` \u2014 exclude documents containing this\n\nGood lex examples:\n- `"connection pool" timeout -redis`\n- `"machine learning" -sports -athlete`\n- `handleError async typescript`\n\n**vec** \u2014 Semantic vector search. Write a natural language question. Finds documents by meaning, not exact words.\n- `how does the rate limiter handle burst traffic?`\n- `what is the tradeoff between consistency and availability?`\n\n**hyde** \u2014 Hypothetical document. Write 50-100 words that look like the answer. Often the most powerful for nuanced topics.\n- `The rate limiter uses a token bucket algorithm. When a client exceeds 100 req/min, subsequent requests return 429 until the window resets.`\n\n## Strategy\n\nCombine types for best results. First sub-query gets 2\xD7 weight \u2014 put your strongest signal first.\n\n| Goal | Approach |\n|------|----------|\n| General search (recommended) | Pass `query` \u2014 auto-expanded into typed variants (not when it has Hangul), fused, reranked |\n| Know exact term/name | `lex` only |\n| Concept search | `vec` only |\n| Best recall | `lex` + `vec` |\n| Complex/nuanced | `lex` + `vec` + `hyde` |\n| Unknown vocabulary | Pass `query` with natural language so the server auto-expands it \u2014 a Korean query is not expanded, so pass `searches` with your own `lex`/`vec` variants |\n\n## Examples\n\nSimple lookup:\n```json\n[{ "type": "lex", "query": "CAP theorem" }]\n```\n\nBest recall on a technical topic:\n```json\n[\n { "type": "lex", "query": "\\"connection pool\\" timeout -redis" },\n { "type": "vec", "query": "why do database connections time out under load" },\n { "type": "hyde", "query": "Connection pool exhaustion occurs when all connections are in use and new requests must wait. This typically happens under high concurrency when queries run longer than expected." }\n]\n```\n\nIntent-aware lex (C++ performance, not sports):\n```json\n[\n { "type": "lex", "query": "\\"C++ performance\\" optimization -sports -athlete" },\n { "type": "vec", "query": "how to optimize C++ program performance" }\n]\n```',annotations:{readOnlyHint:!0,openWorldHint:!1},inputSchema:o.object({query:o.string().optional().describe("Plain-text query, auto-expanded by the SDK into lex/vec/hyde variants (a query with any Hangul is not expanded), fused via RRF and reranked. Recommended default for most searches. Mutually exclusive with 'searches'."),searches:o.array(H).max(10).optional().describe("Typed sub-queries to execute (lex/vec/hyde). First gets 2x weight (a lex line that matches only some of its words counts half). Use for precise control over retrieval strategy. Mutually exclusive with 'query'."),limit:o.number().optional().default(10).describe("Max results (default: 10)"),minScore:o.number().optional().default(0).describe("Min relevance 0-1 (default: 0)"),candidateLimit:o.number().optional().describe("Maximum candidates to rerank (default: 40, lower = faster but may miss results)"),collections:o.array(o.string()).optional().describe("Filter to collections (OR match)"),path:o.array(o.string()).optional().describe("Only search documents whose 'collection/path' matches one of these globs \u2014 the same path strings results report. Prefix a glob with '!' to exclude it; an exclude beats an include. A bare directory means everything under it. Example: ['journals/**', '!journals/2024/**']"),since:o.string().optional().describe("Only documents modified at or after this point: a span back from now ('30m', '3h', '7d', '2w', '6mo', '1y') or a date ('2026-09-01', or a full ISO timestamp)."),until:o.string().optional().describe("Only documents modified at or before this point. Same formats as 'since'."),filter:o.record(o.string(),o.unknown()).optional().describe(`Metadata filter (recursive JSON AST). Every returned result satisfies it. Nodes are operator-discriminated: logical groups {operator:'and'|'or', operands:[...]}, negation {operator:'not', operand:{...}}, and conditions {key, operator, value} with operators eq/ne/gt/gte/lt/lte (comparison), in/nin/all (membership), exists (presence). Values are typed exactly (no coercion); missing keys do not match ne/nin. Example: {"operator":"and","operands":[{"key":"topics","operator":"all","value":["typescript"]},{"key":"status","operator":"ne","value":"draft"}]}`),intent:o.string().optional().describe("Background context to disambiguate the query. Example: query='performance', intent='web page load times and Core Web Vitals'. Does not search on its own."),rerank:o.boolean().optional().default(!0).describe("Rerank results using LLM (default: true). Set to false for faster results on CPU-only machines; `score` is then the 1/rank fusion position, and each result adds `vec_score` (cosine similarity) and `fts_score` (normalized BM25) for the backends that returned it.")})},t(async({query:n,searches:s,limit:f,minScore:S,candidateLimit:u,collections:b,path:E,since:g,until:d,filter:D,intent:k,rerank:a})=>{const i=Date.now();if(!Number.isInteger(f)||f<1)return{content:[{type:"text",text:"Error: limit must be a positive integer"}],isError:!0};if(!n&&(!s||s.length===0))return{content:[{type:"text",text:"Error: provide either 'query' (plain text) or 'searches' (typed sub-queries)"}],isError:!0};if(n&&s&&s.length>0)return{content:[{type:"text",text:"Error: 'query' and 'searches' are mutually exclusive; provide only one"}],isError:!0};const v=ne(D);if(v.error)return{content:[{type:"text",text:`Error: ${v.error}`}],isError:!0};let y;try{y=Z({path:E,since:g,until:d})}catch(h){return{content:[{type:"text",text:`Error: ${h instanceof Error?h.message:String(h)}`}],isError:!0}}const p=b??O;if(b!==void 0){const h=(await e.listCollections()).map(_=>_.name),P=new Set(h),L=b.filter(_=>!P.has(_));if(L.length>0){const _=L.length===1?"collection":"collections",V=h.length>0?h.join(", "):"(none)";return{content:[{type:"text",text:`Error: unknown ${_}: ${L.join(", ")}. Known collections: ${V}`}],isError:!0}}}const T=n?{query:n}:{queries:(s??[]).map(h=>({type:h.type,query:h.query}))},C=xe(),q=Date.now();await z(e,p),C.stages.refresh=Date.now()-q;const N=await e.search({...T,collections:p.length>0?p:void 0,filter:v.filter,limit:f,minScore:S,candidateLimit:u,rerank:a,explain:a===!1,intent:k,scope:y,...oe(),hooks:C.hooks}),m=Date.now(),W=n||s?.find(h=>h.type==="lex")?.query||s?.find(h=>h.type==="vec")?.query||s?.[0]?.query||"",I=N.map(h=>{const{line:P,snippet:L}=X(h.body,W,300,h.bestChunkPos,h.bestChunk.length,k);return{docid:`#${h.docid}`,file:h.displayPath,title:h.title,score:U(h.score),...te(h.explain),context:h.context,...Object.keys(h.metadata).length>0?{metadata:h.metadata}:{},line:P,snippet:Q(L,P)}});C.stages.serialize=Date.now()-m,l&&he(Be({headers:l,searches:n?[{type:"auto",query:n}]:(s??[]).map(h=>({type:h.type,query:h.query})),collections:p,limit:f,rerank:a,results:I,ms:Date.now()-i,stages:C.stages,timeouts:C.timeouts}),e);let w;if(I.length===0&&y)try{w=bt(e,y,p)}catch(h){console.error(`qmd: empty-result description failed: ${h instanceof Error?h.message:String(h)}`),w=we(I,W)+$e(C.timeouts)}else w=we(I,W)+$e(C.timeouts);return{content:[{type:"text",text:w}],structuredContent:{results:I,...ve(C.timeouts)}}}));async function G(n){const s=await n.listCollections(),f=s.find(S=>S.name===A);if(!f||!f.pwd){const S=s.map(u=>u.name).join(", ")||"(none)";return{error:`unknown collection: ${A}. Known collections: ${S}. Register a mem collection first.`}}return{dir:f.pwd}}c(G,"resolveMemDir");const M=c(n=>({content:[{type:"text",text:n}],isError:!0}),"memError");async function F(n,s,f){for(let S=0;;S++){const u=ct(new Date,s);try{return await tt(Y(n,u),f,{encoding:"utf-8",flag:"wx"}),u}catch(b){if(b.code!=="EEXIST"||S>=4)throw b}}}c(F,"writeNewMemory");function j(){try{const n=e.internal.llm?.embedModelName??Xe;return Ye(e.internal.db,A,n)>0}catch{return null}}return c(j,"memVectorsStale"),x.registerTool("memory_store",{title:"Store Memory",description:"Store one claim (fact | opinion | decision) with provenance (actor, source) into the shared mem collection. Facts and decisions require source. Never stores secrets. Returns vectors_stale:true until `qmd embed -c mem` runs.",inputSchema:{text:o.string().describe("The claim, one idea per call."),actor:o.string().describe("Who says it: user | agent:<harness|lane>."),type:o.enum(["fact","opinion","decision"]).describe("Opinions need no source; facts and decisions do."),source:o.string().nullable().optional().describe("Where it came from: file:line, URL, or session ref. Required for fact/decision."),project:o.string().nullable().optional().describe("Project tag for narrowing recall."),confidence:o.number().min(0).max(1).nullable().optional().describe("0..1 when known.")}},t(async({text:n,actor:s,type:f,source:S,project:u,confidence:b})=>{const E={text:n,actor:s,type:f,source:S??void 0,project:u??void 0,confidence:b??void 0},g=fe(E);if(!g.ok)return M(`Error [${g.rule}]: ${g.error}`);const d=await G(e);if("error"in d)return M(`Error: ${d.error}`);const D=await F(d.dir,n,ye(E));await ke(e,A);const k=ge(D);return{content:[{type:"text",text:JSON.stringify(k)}],structuredContent:k}})),x.registerTool("memory_recall",{title:"Recall Memory",description:"Recall shared memory across the mem + kb collections (existing docs need no migration), with actor/project narrowing. Hits carry qmd.metadata provenance (actor/type/source) when the doc has it, and superseded_by when a newer correction exists. kb-only when the mem collection is absent.",inputSchema:{query:o.string().optional().describe("Plain-text query, auto-expanded unless it has Hangul."),searches:o.array(o.object({type:o.enum(["lex","vec","hyde"]),query:o.string()})).optional().describe("Typed sub-queries; mutually exclusive with 'query'."),actor:o.string().nullable().optional().describe("Narrow to who said it."),project:o.string().nullable().optional().describe("Narrow to a project tag."),limit:o.number().int().max(50).default(5).describe("Max hits, 1..50."),collections:o.array(o.string()).optional().describe("Override search scope; default [mem, kb] \u2229 known."),filter:o.any().optional().describe("Extra metadata filter (JSON AST), AND-combined with actor/project."),rerank:o.boolean().optional().describe("true = rerank with the LLM (slower); omitted/false = fused RRF fast path.")}},t(async({query:n,searches:s,actor:f,project:S,limit:u,collections:b,filter:E,rerank:g})=>{if(!Number.isInteger(u)||u<1)return M("Error: limit must be a positive integer");if(!n&&(!s||s.length===0))return M("Error: provide either 'query' or 'searches'");if(n&&s&&s.length>0)return M("Error: 'query' and 'searches' are mutually exclusive; provide only one");const d=ne(E);if(d.error)return M(`Error: ${d.error}`);const D=[];f!=null&&D.push({key:"actor",operator:"eq",value:f}),S!=null&&D.push({key:"project",operator:"eq",value:S});const k=[...D];d.filter!==void 0&&k.push(d.filter);const a=k.length===0?void 0:k.length===1?k[0]:{operator:"and",operands:k},i=(await e.listCollections()).map(w=>w.name),v=new Set(i);let y;const p=[];if(b!==void 0){const w=b.filter(h=>!v.has(h));if(w.length>0)return M(`Error: unknown collection${w.length===1?"":"s"}: ${w.join(", ")}. Known collections: ${i.join(", ")||"(none)"}`);y=b}else if(y=[A,"kb"].filter(w=>v.has(w)),v.has(A)||p.push("mem collection not configured \u2014 kb only"),y.length===0)return M(`Error: no searchable collections. Known collections: ${i.join(", ")||"(none)"}`);await z(e,y);const T=g!==!0,C=n?{query:n}:{queries:(s??[]).map(w=>({type:w.type,query:w.query}))},q=await e.search({...C,collections:y,filter:a,limit:u,rerank:!T,explain:T,...oe()}),N=n||s?.find(w=>w.type==="lex")?.query||s?.find(w=>w.type==="vec")?.query||s?.[0]?.query||"",m=q.map(w=>{const{line:h,snippet:P}=X(w.body,N,300,w.bestChunkPos,w.bestChunk.length),L=w.displayPath.startsWith(`${A}/`)?at(w.body):void 0;return{file:w.displayPath,title:w.title,score:U(w.score),...te(w.explain),...Object.keys(w.metadata).length>0?{metadata:w.metadata}:{},...L!==void 0?{superseded_by:`${A}/${L}`}:{},line:h,snippet:P}}),W=y.includes(A)?j():!1,I={ok:!0,collections_searched:y,...p.length>0?{notices:p}:{},vectors_stale:W,...W!==!1?{vectors_stale_note:ut}:{},results:m};return{content:[{type:"text",text:JSON.stringify(I,null,2)}],structuredContent:I}})),x.registerTool("memory_correct",{title:"Correct Memory",description:"Append-only correction: writes the new claim as a new mem file carrying supersedes, and appends a superseded_by footer to the old file. History is never erased. Only mem files can be corrected.",inputSchema:{file:o.string().describe("Mem filename to correct (e.g. 20261014-210000-abcdef12.md)."),text:o.string().describe("The corrected claim."),reason:o.string().describe("Why the old claim was wrong."),actor:o.string().describe("Who corrects it: user | agent:<harness|lane>."),type:o.enum(["fact","opinion","decision"]).nullable().optional().describe("Defaults to the old claim's type."),source:o.string().nullable().optional().describe("Evidence for the correction \u2014 never inherited. Required when the type is fact/decision."),project:o.string().nullable().optional().describe("Defaults to the old claim's project.")}},t(async({file:n,text:s,reason:f,actor:S,type:u,source:b,project:E})=>{const g=await G(e);if("error"in g)return M(`Error: ${g.error}`);const d=dt(g.dir,n);return d===null?M("Error: file must be a tool-written mem filename (YYYYMMDD-HHMMSS-xxxx-xxxxxxxx.md)"):ft(d,async()=>{let D;try{if(!(await ot(d)).isFile())return M(`Error: not a regular mem file: ${n}`);const[C,q]=await Promise.all([me(g.dir),me(d)]);if(!q.startsWith(C+Ie))return M(`Error: ${n} resolves outside the mem collection`);D=await rt(d,"utf-8")}catch{return M(`Error: no such mem file: ${n}`)}const k=st(D,f);if(!k.ok)return M(`Error [${k.rule}]: ${k.error}`);const a=pt(D,n).metadata,i={text:s,actor:S,type:u??(a.type==="fact"||a.type==="opinion"||a.type==="decision"?a.type:"opinion"),source:b??void 0,project:E??(typeof a.project=="string"?a.project:void 0)},v=fe(i);if(!v.ok)return M(`Error [${v.rule}]: ${v.error}`);const y=await F(g.dir,s,ye(i,{supersedes:n}));try{await nt(d,lt(y,f),"utf-8")}catch(T){return await it(Y(g.dir,y)).catch(()=>{}),M(`Error: could not mark ${n} superseded (${T instanceof Error?T.message:String(T)}); nothing written`)}await ke(e,A);const p=ge(y,n);return{content:[{type:"text",text:JSON.stringify(p)}],structuredContent:p}})})),x.registerTool("get",{title:"Get Document",description:"Retrieve the full content of a document by its file path or docid. Use paths or docids (#abc123) from search results. Suggests similar files if not found.",annotations:{readOnlyHint:!0,openWorldHint:!1},inputSchema:o.object({file:o.string().describe("File path or docid from search results. Supports a line-range suffix: 'pages/meeting.md:100' starts at line 100; 'pages/meeting.md:100:40' (or '#abc123:100:40') reads 40 lines from line 100."),fromLine:o.number().optional().describe("Start from this line number (1-indexed)"),maxLines:o.number().optional().describe("Maximum number of lines to return"),lineNumbers:o.boolean().optional().default(!0).describe("Add line numbers to output (format: 'N: content'). On by default; set false for raw content.")})},t(async({file:n,fromLine:s,maxLines:f,lineNumbers:S})=>{let u=s,b=f,E=n;const g=E.match(/:(\d+):(\d+)$/);if(g)u===void 0&&(u=parseInt(g[1],10)),b===void 0&&(b=parseInt(g[2],10)),E=E.slice(0,-g[0].length);else{const a=E.match(/:(\d+)$/);a&&a[1]&&u===void 0&&(u=parseInt(a[1],10),E=E.slice(0,-a[0].length))}u!==void 0&&(u=Math.max(1,u));const d=await e.get(E,{includeBody:!1});if("error"in d){let a=d.error==="excluded_by_ignore"?`Document excluded by ignore rule: ${n}
12
+ Collection: ${d.collection}
13
+ Matched path: ${d.path}
14
+ Ignore rule: ${d.rule}`:`Document not found: ${n}`;return d.error==="not_found"&&d.similarFiles.length>0&&(a+=`
15
+
16
+ Did you mean one of these?
17
+ ${d.similarFiles.map(i=>` - ${i}`).join(`
18
+ `)}`),{content:[{type:"text",text:a}],isError:!0}}let k=await e.getDocumentBody(d.filepath,{fromLine:u,maxLines:b})??"";return S&&(k=Q(k,u||1)),d.context&&(k=`<!-- Context: ${d.context} -->
19
+
20
+ `+k),{content:[{type:"resource",resource:{uri:`qmd://${re(d.displayPath)}`,name:d.displayPath,title:d.title,mimeType:"text/markdown",text:k}}]}})),x.registerTool("multi_get",{title:"Multi-Get Documents",description:"Retrieve multiple documents by glob pattern (e.g., 'journals/2025-05*.md'), comma-separated list, or docids. Skips files larger than maxBytes.",annotations:{readOnlyHint:!0,openWorldHint:!1},inputSchema:o.object({pattern:o.string().describe("Glob pattern, docid, or comma-separated list of file paths/docids"),maxLines:o.number().optional().describe("Maximum lines per file"),maxBytes:o.number().optional().default(ue).describe("Skip files larger than this (default: 65536 = 64KB)"),lineNumbers:o.boolean().optional().default(!0).describe("Add line numbers to output (format: 'N: content'). On by default; set false for raw content.")})},t(async({pattern:n,maxLines:s,maxBytes:f,lineNumbers:S})=>{if(s!==void 0&&(!Number.isInteger(s)||s<1))return{content:[{type:"text",text:"maxLines must be a positive integer"}],isError:!0};if(f!==void 0&&(!Number.isInteger(f)||f<1))return{content:[{type:"text",text:"maxBytes must be a positive integer"}],isError:!0};const{docs:u,errors:b}=await e.multiGet(n,{includeBody:!0,maxBytes:f||ue});if(u.length===0)return b.length===0?{content:[{type:"text",text:`No files matched pattern: ${n}`}],isError:!0}:{content:[{type:"text",text:`Errors:
21
+ ${b.join(`
22
+ `)}`}],isError:!0};const E=[];b.length>0&&E.push({type:"text",text:`Errors:
23
+ ${b.join(`
24
+ `)}`});for(const g of u){if(g.skipped){E.push({type:"text",text:`[SKIPPED: ${g.doc.displayPath} - ${g.skipReason}. Use 'get' with file="${g.doc.displayPath}" to retrieve.]`});continue}let d=g.doc.body||"";if(s!==void 0){const D=d.split(`
25
+ `);d=D.slice(0,s).join(`
26
+ `),D.length>s&&(d+=`
27
+
28
+ [... truncated ${D.length-s} more lines]`)}S&&(d=Q(d)),g.doc.context&&(d=`<!-- Context: ${g.doc.context} -->
29
+
30
+ `+d),E.push({type:"resource",resource:{uri:`qmd://${re(g.doc.displayPath)}`,name:g.doc.displayPath,title:g.doc.title,mimeType:"text/markdown",text:d}})}return{content:E}})),x.registerTool("grep",{title:"Grep Documents",description:'Exact string and regular-expression matching over indexed document bodies. No ranking, no LLM \u2014 every line that matches, grouped by file, with line numbers.\n\nUse this when `query` should have found something and did not. Ranked search answers "what is this about"; grep answers "where does this string appear", and a term that tokenization never produced is invisible to the first and obvious to the second. It is also the way to find an identifier, an error string, a URL or a config key verbatim.\n\nLine numbers address `get`: a match on line 120 of `notes/x.md` is readable with `file: "notes/x.md:120:40"`.\n\nSmart case: a pattern containing an uppercase letter matches case-sensitively, otherwise either case. Hangul has no case and is unaffected. Set `fixedString` to search for a literal string containing regex characters.',annotations:{readOnlyHint:!0,openWorldHint:!1},inputSchema:o.object({pattern:o.string().describe("Regular expression, or a literal string when 'fixedString' is set. JavaScript regex syntax."),collections:o.array(o.string()).optional().describe("Filter to collections (OR match)"),path:o.array(o.string()).optional().describe("Only scan documents whose 'collection/path' matches one of these globs; '!' excludes."),since:o.string().optional().describe("Only documents modified at or after this span ('7d') or date."),until:o.string().optional().describe("Only documents modified at or before this span or date."),limit:o.number().optional().default(20).describe("Max files to return (default: 20)"),maxLinesPerFile:o.number().optional().default(20).describe("Max matching lines reported per file (default: 20)"),caseSensitive:o.boolean().optional().describe("Override smart case."),fixedString:o.boolean().optional().describe("Treat the pattern as a literal string, not a regex.")})},t(async({pattern:n,collections:s,path:f,since:S,until:u,limit:b,maxLinesPerFile:E,caseSensitive:g,fixedString:d})=>{let D,k;try{D=Ze(n,{caseSensitive:g,fixedString:d}),k=Z({path:f,since:S,until:u})}catch(p){return{content:[{type:"text",text:`Error: ${p instanceof Error?p.message:String(p)}`}],isError:!0}}const a=s??O;if(s!==void 0){const p=(await e.listCollections()).map(q=>q.name),T=new Set(p),C=s.filter(q=>!T.has(q));if(C.length>0){const q=C.length===1?"collection":"collections",N=p.length>0?p.join(", "):"(none)";return{content:[{type:"text",text:`Error: unknown ${q}: ${C.join(", ")}. Known collections: ${N}`}],isError:!0}}}await z(e,a);const i=et(e.internal.db,D,{collections:a.length>0?a:void 0,filter:k,limit:b,maxLinesPerFile:E}),v=i.files.map(p=>({docid:`#${p.docid}`,file:p.displayPath,title:p.title,matchCount:p.matchCount,lines:p.lines.map(T=>({line:T.line,text:T.text}))}));return{content:[{type:"text",text:v.length===0?`No lines match /${n}/ in ${i.scanned} document${i.scanned===1?"":"s"}${k?` matching ${ee(k)}`:""}.`:[`${v.length} file${v.length===1?"":"s"} match /${n}/${i.truncated?` (stopped at the limit of ${b})`:""}:`,...v.flatMap(p=>[`${p.file} ${p.docid}${p.matchCount>p.lines.length?` (+${p.matchCount-p.lines.length} more)`:""}`,...p.lines.map(T=>` ${T.line}: ${T.text.length>300?`${T.text.slice(0,300)}\u2026`:T.text}`)])].join(`
31
+ `)}],structuredContent:{files:v,scanned:i.scanned,truncated:i.truncated}}})),x.registerTool("status",{title:"Index Status",description:"Show the status of the QMD index: collections, document counts, and health information.",annotations:{readOnlyHint:!0,openWorldHint:!1},inputSchema:o.object({})},t(async()=>{const n=await e.getStatus(),s=["QMD Index Status:",` Total documents: ${n.totalDocuments}`,` Needs embedding: ${n.needsEmbedding}`,` Vector index: ${n.hasVectorIndex?"yes":"no"}`,` Collections: ${n.collections.length}`];for(const S of n.collections)s.push(ht(S));const f=Qe();return s.push(` Query log (REST + MCP over HTTP): ${f.enabled?f.path:"off"} (last write: ${f.lastWrite??"none"}${f.lastError?`, last error: ${f.lastError}`:""})`),{content:[{type:"text",text:s.join(`
32
+ `)}],structuredContent:{...n,queryLog:f}}})),x}c(Me,"createMcpServer");function St(){let e=0;const r=[];return{track(l){return c(async(...x)=>{e+=1;try{return await l(...x)}finally{if(e-=1,e===0)for(;r.length>0;)r.shift()()}},"wrapped")},waitForIdle(l){return e===0?Promise.resolve(!0):new Promise(t=>{const x=c(()=>{clearTimeout(O),t(!0)},"onIdle"),O=setTimeout(()=>{const H=r.indexOf(x);H>=0&&r.splice(H,1),t(!1)},l);O.unref?.(),r.push(x)})}}}c(St,"createInflightGate");function vt(e){const r=e.stdin??process.stdin,l=e.stderr??process.stderr,t=e.setExitCode??(j=>{process.exitCode=j}),x=e.getExitCode??(()=>typeof process.exitCode=="number"?process.exitCode:void 0);let O=null;l.on?.("error",()=>{});const H=c(j=>{try{l.write(j)}catch{}},"safeWrite"),G=c(async()=>{try{r.off("end",F),r.off("close",F)}catch{}H(`Shutting down (stdin closed)...
33
+ `);let j=!1;const n=c(async(s,f)=>{try{await f()}catch(S){j=!0,H(`QMD Warning: ${s} failed during stdio shutdown (${S instanceof Error?S.message:String(S)}); continuing shutdown.
34
+ `)}},"step");await n("server.close()",e.closeServer),e.waitForIdle&&await n("in-flight drain",async()=>{await e.waitForIdle(e.idleTimeoutMs??5e3)||H(`QMD Warning: in-flight request did not settle before the shutdown deadline; continuing shutdown.
35
+ `)}),e.disposeLlm&&await n("llama disposal",e.disposeLlm),await n("store.close()",e.closeStore);try{const s=x();j?t(1):(s===void 0||s===0)&&t(0)}catch{}},"performShutdown"),M=c(()=>O??=G(),"shutdown"),F=c(()=>{M().catch(()=>{})},"onStdinEof");return r.once("end",F),r.once("close",F),(r.readableEnded||r.destroyed)&&F(),M}c(vt,"registerStdioEofShutdown");async function $t(e={}){pe();const r=R(),l=await le({dbPath:e.dbPath??de(),...ce(r)?{configPath:r}:{}}),t=St(),x=Le(()=>Me(l,t)),O=vt({closeServer:c(()=>x.close(),"closeServer"),waitForIdle:c(H=>t.waitForIdle(H),"waitForIdle"),closeStore:c(()=>l.close(),"closeStore")});process.on("SIGTERM",async()=>{console.error("Shutting down (SIGTERM)..."),await O(),process.exit(0)}),process.on("SIGINT",async()=>{console.error("Shutting down (SIGINT)..."),await O(),process.exit(0)})}c($t,"startMcpServer");async function Kt(e,r={}){pe();const l=R(),t=await le({dbPath:r.dbPath??de(),...ce(l)?{configPath:l}:{}}),x=await t.getDefaultCollectionNames(),O=Pe(a=>Me(t,void 0,a.requestInfo?.headers),{responseMode:"json"}),H=Date.now(),G=r?.quiet??!1;function M(){return Je(new Date)}c(M,"ts");function F(a){const i=typeof a.method=="string"?a.method:"unknown";if(i==="tools/call"){const v=a.params?.name??"?",y=a.params?.arguments;if(y?.query){const p=String(y.query).slice(0,80);return`tools/call ${v} "${p}"`}return y?.file?`tools/call ${v} ${y.file}`:y?.path?`tools/call ${v} ${y.path}`:y?.pattern?`tools/call ${v} ${y.pattern}`:`tools/call ${v}`}return i}c(F,"describeRequest");function j(a){G||console.error(a)}c(j,"log");const n={listens:0,inflight:0},s=Ke(),f=s>0?setInterval(()=>j(`${M()} ${Ue(process.memoryUsage(),n,Date.now()-H)}`),s).unref():null;function S(a){const i=new Headers;for(const[v,y]of Object.entries(a.headers))if(typeof y=="string")i.set(v,y);else if(Array.isArray(y))for(const p of y)i.append(v,p);return i}c(S,"nodeHeadersToWeb");async function u(a){const i=[];for await(const v of a)i.push(v);return Buffer.concat(i).toString()}c(u,"collectBody");const b=r.host??process.env.QMD_HOST??"localhost",E=We({host:b,...r.allowedOrigins?{allowedOrigins:r.allowedOrigins}:{},...r.allowedHosts?{allowedHosts:r.allowedHosts}:{}}),g=Ne(async(a,i)=>{const v=Date.now(),y=(a.url||"/").split("?")[0];try{const p=a.headers.origin,T=a.headers.host,C=Ae({origin:typeof p=="string"?p:void 0,host:typeof T=="string"?T:void 0},E);if(!C.ok){i.writeHead(403,{"Content-Type":"application/json"}),i.end(JSON.stringify({jsonrpc:"2.0",error:{code:-32003,message:`Forbidden: ${C.reason}`},id:null})),j(`${M()} ${a.method} ${y} 403 \u2014 ${C.reason}`);return}if(y==="/health"&&a.method==="GET"){const q=JSON.stringify({status:"ok",uptime:Math.floor((Date.now()-H)/1e3),listens:n.listens,mcp_inflight:n.inflight});i.writeHead(200,{"Content-Type":"application/json"}),i.end(q),j(`${M()} GET /health (${Date.now()-v}ms)`);return}if((y==="/query"||y==="/search")&&a.method==="POST"){const q=await u(a);let N;try{N=JSON.parse(q)}catch{i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Invalid JSON body"}));return}if(typeof N!="object"||N===null||Array.isArray(N)){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"JSON body must be an object"}));return}const m=N;if(!m.searches||!Array.isArray(m.searches)){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Missing required field: searches (array)"}));return}if(m.searches.length===0){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Invalid field: searches (must be a non-empty array)"}));return}if(m.searches.length>10){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Invalid field: searches (maximum 10 items)"}));return}const W=new Set(["lex","vec","hyde"]);for(let $=0;$<m.searches.length;$++){const B=m.searches[$];if(typeof B!="object"||B===null||Array.isArray(B)){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:`Invalid field: searches[${$}] (must be an object)`}));return}const J=B;if(typeof J.type!="string"||!W.has(J.type)){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:`Invalid field: searches[${$}].type (must be one of: lex, vec, hyde)`}));return}if(typeof J.query!="string"||J.query.trim()===""){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:`Invalid field: searches[${$}].query (must be a non-empty string)`}));return}}const I=m.searches,w=I.map($=>({type:$.type,query:$.query}));let h;if(m.filter!==void 0){if(typeof m.filter!="object"||m.filter===null||Array.isArray(m.filter)){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Invalid field: filter (must be an object)"}));return}const $=ne(m.filter);if($.error){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:$.error}));return}h=$.filter}const P=Array.isArray(m.collections)?m.collections.map(String):x;let L;try{L=Z({path:Array.isArray(m.path)?m.path.map(String):void 0,since:typeof m.since=="string"?m.since:void 0,until:typeof m.until=="string"?m.until:void 0})}catch($){i.writeHead(400,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:$ instanceof Error?$.message:String($)}));return}const _=xe(),V=Date.now();await z(t,P),_.stages.refresh=Date.now()-V;const ie=typeof m.rerank=="boolean"?m.rerank:void 0,De=await t.search({queries:w,collections:P.length>0?P:void 0,filter:h,limit:typeof m.limit=="number"?m.limit:10,minScore:typeof m.minScore=="number"?m.minScore:0,candidateLimit:typeof m.candidateLimit=="number"?m.candidateLimit:void 0,rerankMaxDocTokens:Re(m.rerankMaxDocTokens),intent:typeof m.intent=="string"?m.intent:void 0,rerank:ie,explain:ie===!1,scope:L,...oe(),hooks:_.hooks}),Te=Date.now(),Ce=I.find($=>$.type==="lex")?.query||I.find($=>$.type==="vec")?.query||I[0]?.query||"",se=De.map($=>{const{line:B,snippet:J}=X($.body,String(Ce),300,$.bestChunkPos,$.bestChunk.length,typeof m.intent=="string"?m.intent:void 0);return{docid:`#${$.docid}`,file:`qmd://${re($.displayPath)}`,title:$.title,score:U($.score),...te($.explain),context:$.context,...Object.keys($.metadata).length>0?{metadata:$.metadata}:{},line:B,snippet:Q(J,B)}});i.writeHead(200,{"Content-Type":"application/json"}),i.end(JSON.stringify({results:se,...ve(_.timeouts)})),_.stages.serialize=Date.now()-Te,he(Ge({path:y,headers:a.headers,searches:w,params:m,collections:P,results:se,ms:Date.now()-v,stages:_.stages,timeouts:_.timeouts}),t),j(`${M()} POST ${y} ${m.searches.length} queries (${Date.now()-v}ms)`);return}if(y==="/mcp"){const q=a.method!=="GET"&&a.method!=="HEAD"?await u(a):void 0;let N;if(q)try{N=JSON.parse(q)}catch{N=void 0}const m=N&&typeof N=="object"&&N!==null?F(N):a.method||"GET",I=`http://${typeof a.headers.host=="string"?a.headers.host:`localhost:${e}`}${y}`,w=new Request(I,{method:a.method||"GET",headers:S(a),...q!==void 0?{body:q}:{}}),h=m==="subscriptions/listen";n.inflight++,h&&n.listens++;try{const P=await O.fetch(w,N!==void 0?{parsedBody:N}:void 0);i.writeHead(P.status,Object.fromEntries(P.headers)),i.end(Buffer.from(await P.arrayBuffer()))}finally{n.inflight--,h&&n.listens--}j(`${M()} ${a.method} /mcp ${m} (${Date.now()-v}ms)`);return}i.writeHead(404,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Not Found"}))}catch(p){console.error("HTTP handler error:",p),i.headersSent?i.end():(i.writeHead(500,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Internal Server Error"})))}});await new Promise((a,i)=>{g.on("error",i),g.listen(e,b,()=>a())});const d=g.address().port;let D=!1;const k=c(async()=>{D||(D=!0,f&&clearInterval(f),await O.close(),g.close(),await t.close())},"stop");return process.on("SIGTERM",async()=>{console.error("Shutting down (SIGTERM)..."),await k(),process.exit(0)}),process.on("SIGINT",async()=>{console.error("Shutting down (SIGINT)..."),await k(),process.exit(0)}),j(`QMD MCP server listening on http://${b}:${d}/mcp`),E.disabled?j("Warning: QMD_ALLOWED_ORIGINS=* \u2014 DNS-rebinding protection is off. Only do this behind your own authenticating proxy."):E.enforceHost||j(`Warning: bound to ${b} with no QMD_ALLOWED_HOSTS \u2014 Host validation is off and the index is readable by anyone who can reach this port.`),{httpServer:g,port:d,stop:k}}c(Kt,"startMcpHttpServer"),(ae(import.meta.url)===process.argv[1]||process.argv[1]?.endsWith("/server.ts")||process.argv[1]?.endsWith("/server.js"))&&$t().catch(console.error);export{St as createInflightGate,ht as formatStatusCollectionLine,vt as registerStdioEofShutdown,Se as resolveDeadlineMs,Kt as startMcpHttpServer,$t as startMcpServer};
@@ -0,0 +1,74 @@
1
+ /**
2
+ * memory.ts — shared second-brain write/read core (slice 03).
3
+ *
4
+ * Thin and pure by design: validation, filename derivation, document
5
+ * rendering, and collection jailing live here so both the MCP tools
6
+ * (`src/mcp/server.ts`) and the write eval (`recall-write.sh` rule mirror)
7
+ * share one rule set. File I/O and search stay with the callers.
8
+ *
9
+ * Storage contract: one claim per file under the `mem` collection,
10
+ * machine fields in `qmd.metadata` (see knowledge-base CLAUDE.md
11
+ * § provenance), human claim in the body. Corrections never edit —
12
+ * a new file carries `supersedes`, the old file keeps a footer.
13
+ */
14
+ export declare const MEMORY_COLLECTION = "mem";
15
+ export declare const MEMORY_MAX_CHARS = 8000;
16
+ export declare const MEMORY_MAX_SOURCE_CHARS = 500;
17
+ export declare const MEMORY_MAX_ACTOR_CHARS = 80;
18
+ export declare const MEMORY_MAX_REASON_CHARS = 500;
19
+ /** Tool-derived names only; the optional rand4 segment predates collision hardening. */
20
+ export declare const MEMORY_FILENAME_RE: RegExp;
21
+ export type MemoryType = "fact" | "opinion" | "decision";
22
+ export interface MemoryStoreInput {
23
+ text: string;
24
+ actor: string;
25
+ type: MemoryType;
26
+ source?: string;
27
+ project?: string;
28
+ confidence?: number;
29
+ }
30
+ export type MemoryValidation = {
31
+ ok: true;
32
+ } | {
33
+ ok: false;
34
+ error: string;
35
+ rule: string;
36
+ };
37
+ /**
38
+ * Claim-lint, rule ids R1..R6 (scored by scripts/eval/recall-write.sh).
39
+ * Rejects caller-fixable input; never throws on unknown shapes.
40
+ */
41
+ export declare function validateMemoryStoreInput(input: unknown): MemoryValidation;
42
+ /**
43
+ * Correction preconditions (R6 supersedes chain, plus reason hygiene).
44
+ * The old doc must not already be superseded — correct the newest link instead,
45
+ * so the chain stays linear. The reason lands in the old file, so it is one line.
46
+ */
47
+ export declare function validateCorrection(oldBody: string, reason: unknown): MemoryValidation;
48
+ /** The newer file a mem doc points to via its footer, or undefined when it is current. */
49
+ export declare function parseSupersededBy(body: string): string | undefined;
50
+ /** `YYYYMMDD-HHMMSS-<rand4>-<hash8>.md`, derived — callers never take a filename.
51
+ * The random segment keeps same-second/same-text writes from colliding. */
52
+ export declare function buildMemoryFilename(now: Date, text: string): string;
53
+ /** Full markdown: YAML frontmatter (qmd.metadata) + `# title` + claim body. */
54
+ export declare function renderMemoryDoc(input: MemoryStoreInput, opts?: {
55
+ supersedes?: string;
56
+ }): string;
57
+ /** Append-only correction marker for the superseded file. History stays. */
58
+ export declare function renderSupersededFooter(newFile: string, reason: string): string;
59
+ /**
60
+ * Jail a caller-supplied ref inside the mem dir. Tool-derived filenames only
61
+ * (MEMORY_FILENAME_RE) — no subdirs, traversal, README, or hand-named files.
62
+ * Returns the absolute path or null (reject). Symlinks are the caller's
63
+ * check: this stays pure.
64
+ */
65
+ export declare function resolveInCollection(memDir: string, ref: string): string | null;
66
+ export declare const VECTORS_STALE_NOTICE = "lex-searchable now; vectors need `qmd embed -c mem`";
67
+ /** Store/correct success receipt — vectors_stale is always true at write time. */
68
+ export declare function buildStoreReceipt(filename: string, supersedes?: string): {
69
+ ok: true;
70
+ file: string;
71
+ vectors_stale: true;
72
+ notice: string;
73
+ supersedes?: string;
74
+ };
package/dist/memory.js ADDED
@@ -0,0 +1,13 @@
1
+ var y=Object.defineProperty;var o=(e,r)=>y(e,"name",{value:r,configurable:!0});import{join as A}from"node:path";import h from"yaml";const f="mem",R=8e3,g=500,d=80,k=500,E=/^\d{8}-\d{6}-(?:[0-9a-f]{4}-)?[0-9a-f]{8}\.md$/,p="> superseded_by:",S=/^> superseded_by: (\S+\.md) — /m,$=[{pattern:/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/,label:"private key"},{pattern:/\bsk-(?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{16,}/,label:"api key"},{pattern:/\bgh[pousr]_[A-Za-z0-9]{8,}\b/,label:"github token"},{pattern:/\bgithub_pat_[A-Za-z0-9_]{8,}/,label:"github token"},{pattern:/\bAKIA[0-9A-Z]{16}\b/,label:"aws key"},{pattern:/\bAIza[0-9A-Za-z_-]{35}/,label:"google api key"},{pattern:/\bxox[baprs]-[A-Za-z0-9-]{8,}\b/,label:"slack token"},{pattern:/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/,label:"jwt"},{pattern:/\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token)\s*[:=]\s*['"]?(?!\$|process\.env)(?=[^\s'"]*\d)(?=[^\s'"]*[A-Za-z])[^\s'"]{8,}/i,label:"credential assignment"}],M=/^[a-z][a-z0-9+.-]*:\/\/\S|\/|[\w-]{2,}\.[A-Za-z][A-Za-z0-9]{0,7}\b|\bsession:\S|#\d+/i;function _(e){const r=e.filter(t=>typeof t=="string");for(const{pattern:t,label:n}of $)if(r.some(a=>t.test(a)))return n}o(_,"findSecret");function x(e){return e.split(`
2
+ `).some(r=>r.trimStart().startsWith(p))}o(x,"hasReservedMarker");function j(e){if(typeof e!="object"||e===null)return{ok:!1,error:"input must be an object",rule:"R1"};const r=e,t=o(b=>b===null?void 0:b,"nonNull"),{text:n,actor:a,type:i}=r,s=t(r.source),u=t(r.project),l=t(r.confidence);if(typeof n!="string"||n.trim().length===0)return{ok:!1,error:"text is required",rule:"R1"};if(n.length>R)return{ok:!1,error:`text exceeds ${R} chars`,rule:"R3"};if(typeof a!="string"||a.trim().length===0)return{ok:!1,error:"actor is required (user | agent:<name>)",rule:"R1"};if(a.length>d||/[/\\\u0000-\u001f\u007f]/.test(a))return{ok:!1,error:"actor must be a flat name without path separators",rule:"R3"};if(i!=="fact"&&i!=="opinion"&&i!=="decision")return{ok:!1,error:"type must be fact | opinion | decision",rule:"R1"};if((i==="fact"||i==="decision")&&(typeof s!="string"||s.trim().length===0))return{ok:!1,error:`${i} requires source`,rule:"R2"};if(s!==void 0&&(typeof s!="string"||s.length>g||/[\u0000-\u001f\u007f]/.test(s)))return{ok:!1,error:`source must be a single line \u2264 ${g} chars`,rule:"R3"};if((i==="fact"||i==="decision")&&!M.test(s))return{ok:!1,error:"source must be locatable: file:line, path, URL, session:<id>, or #<id>",rule:"R2"};if(u!==void 0&&typeof u!="string")return{ok:!1,error:"project must be a string",rule:"R1"};if(typeof u=="string"&&(u.length>d||/[\u0000-\u001f\u007f]/.test(u)))return{ok:!1,error:`project must be a single line \u2264 ${d} chars`,rule:"R3"};if(l!==void 0&&(typeof l!="number"||!Number.isFinite(l)||l<0||l>1))return{ok:!1,error:"confidence must be a number in 0..1",rule:"R5"};const m=_([n,a,s,u]);return m!==void 0?{ok:!1,error:`rejected: looks like a ${m} \u2014 never store secrets`,rule:"R4"}:x(n)?{ok:!1,error:`text must not contain a '${p}' line \u2014 only memory_correct writes it`,rule:"R6"}:{ok:!0}}o(j,"validateMemoryStoreInput");function U(e,r){if(typeof r!="string"||r.trim().length===0)return{ok:!1,error:"reason is required",rule:"R1"};if(r.length>k||/[\u0000-\u001f\u007f]/.test(r))return{ok:!1,error:`reason must be a single line \u2264 ${k} chars`,rule:"R3"};const t=_([r]);if(t!==void 0)return{ok:!1,error:`rejected: reason looks like a ${t} \u2014 never store secrets`,rule:"R4"};const n=C(e);return n!==void 0?{ok:!1,error:`already superseded by ${f}/${n} \u2014 correct that file instead`,rule:"R6"}:{ok:!0}}o(U,"validateCorrection");function C(e){return S.exec(e)?.[1]}o(C,"parseSupersededBy");function c(e){return String(e).padStart(2,"0")}o(c,"pad");function I(e,r){const t=`${e.getUTCFullYear()}${c(e.getUTCMonth()+1)}${c(e.getUTCDate())}-${c(e.getUTCHours())}${c(e.getUTCMinutes())}${c(e.getUTCSeconds())}`;let n=0;for(let s=0;s<r.length;s++)n=Math.imul(n,31)+r.charCodeAt(s)|0;const a=(n>>>0).toString(16).padStart(8,"0"),i=Math.floor(Math.random()*65535).toString(16).padStart(4,"0");return`${t}-${i}-${a}.md`}o(I,"buildMemoryFilename");function v(e,r){const t={actor:e.actor,type:e.type};e.project!==void 0&&(t.project=e.project),e.source!==void 0&&(t.source=e.source),e.confidence!==void 0&&(t.confidence=e.confidence),r?.supersedes!==void 0&&(t.supersedes=r.supersedes);const n=h.stringify({qmd:{metadata:t}}).trimEnd(),a=e.text.split(`
3
+ `,1)[0]?.slice(0,60)??"memory";return`---
4
+ ${n}
5
+ ---
6
+
7
+ # ${a}
8
+
9
+ ${e.text}
10
+ `}o(v,"renderMemoryDoc");function N(e,r){return`
11
+
12
+ ${p} ${e} \u2014 ${r}
13
+ `}o(N,"renderSupersededFooter");function Y(e,r){return typeof r!="string"||!E.test(r)?null:A(e,r)}o(Y,"resolveInCollection");const O="lex-searchable now; vectors need `qmd embed -c mem`";function L(e,r){return{ok:!0,file:`${f}/${e}`,vectors_stale:!0,notice:O,...r!==void 0?{supersedes:`${f}/${r}`}:{}}}o(L,"buildStoreReceipt");export{f as MEMORY_COLLECTION,E as MEMORY_FILENAME_RE,d as MEMORY_MAX_ACTOR_CHARS,R as MEMORY_MAX_CHARS,k as MEMORY_MAX_REASON_CHARS,g as MEMORY_MAX_SOURCE_CHARS,O as VECTORS_STALE_NOTICE,I as buildMemoryFilename,L as buildStoreReceipt,C as parseSupersededBy,v as renderMemoryDoc,N as renderSupersededFooter,Y as resolveInCollection,U as validateCorrection,j as validateMemoryStoreInput};