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.
- package/CHANGELOG.md +1937 -0
- package/LICENSE +21 -0
- package/README.en.md +184 -0
- package/README.md +292 -0
- package/bin/qmd +192 -0
- package/dist/ast.d.ts +65 -0
- package/dist/ast.js +51 -0
- package/dist/bench/bench.d.ts +41 -0
- package/dist/bench/bench.js +12 -0
- package/dist/bench/score.d.ts +33 -0
- package/dist/bench/score.js +1 -0
- package/dist/bench/types.d.ts +101 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/build-info.json +4 -0
- package/dist/cli/embed-lock.d.ts +24 -0
- package/dist/cli/embed-lock.js +2 -0
- package/dist/cli/formatter.d.ts +120 -0
- package/dist/cli/formatter.js +60 -0
- package/dist/cli/mcp-pid.d.ts +33 -0
- package/dist/cli/mcp-pid.js +1 -0
- package/dist/cli/qmd.d.ts +101 -0
- package/dist/cli/qmd.js +165 -0
- package/dist/cli/version.d.ts +42 -0
- package/dist/cli/version.js +1 -0
- package/dist/collections.d.ts +162 -0
- package/dist/collections.js +1 -0
- package/dist/db.d.ts +63 -0
- package/dist/db.js +2 -0
- package/dist/filters.d.ts +46 -0
- package/dist/filters.js +1 -0
- package/dist/grep.d.ts +85 -0
- package/dist/grep.js +9 -0
- package/dist/hangul.d.ts +102 -0
- package/dist/hangul.js +1 -0
- package/dist/index.d.ts +270 -0
- package/dist/index.js +1 -0
- package/dist/llm.d.ts +584 -0
- package/dist/llm.js +40 -0
- package/dist/maintenance.d.ts +33 -0
- package/dist/maintenance.js +1 -0
- package/dist/mcp/daemon-health.d.ts +17 -0
- package/dist/mcp/daemon-health.js +1 -0
- package/dist/mcp/origin-guard.d.ts +67 -0
- package/dist/mcp/origin-guard.js +1 -0
- package/dist/mcp/server.d.ts +131 -0
- package/dist/mcp/server.js +35 -0
- package/dist/memory.d.ts +74 -0
- package/dist/memory.js +13 -0
- package/dist/metadata-filter.d.ts +74 -0
- package/dist/metadata-filter.js +1 -0
- package/dist/metadata-store.d.ts +45 -0
- package/dist/metadata-store.js +68 -0
- package/dist/metadata.d.ts +61 -0
- package/dist/metadata.js +1 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +1 -0
- package/dist/query-log.d.ts +156 -0
- package/dist/query-log.js +2 -0
- package/dist/refresh.d.ts +60 -0
- package/dist/refresh.js +1 -0
- package/dist/rerank-input.d.ts +46 -0
- package/dist/rerank-input.js +5 -0
- package/dist/store.d.ts +1267 -0
- package/dist/store.js +421 -0
- package/dist/trust.d.ts +152 -0
- package/dist/trust.js +2 -0
- package/package.json +124 -0
- package/scripts/build.mjs +130 -0
- package/scripts/check-package-grammars.mjs +29 -0
- package/scripts/package-smoke.mjs +65 -0
- package/scripts/test-all.mjs +42 -0
- package/skills/qmd/SKILL.md +333 -0
- package/skills/qmd/references/mcp-setup.md +122 -0
- package/skills/release/SKILL.md +141 -0
- package/skills/release/scripts/install-hooks.sh +38 -0
- package/skills/release/scripts/release-context.sh +129 -0
package/dist/trust.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trust.ts — approval gate for project-local `.qmd` config.
|
|
3
|
+
*
|
|
4
|
+
* A project-local `.qmd/index.yml` arrives with a `git clone`, and
|
|
5
|
+
* `findLocalConfigPath` adopts it automatically for any command run inside the
|
|
6
|
+
* tree. Three fields in that file can reach outside the project:
|
|
7
|
+
*
|
|
8
|
+
* - `update:` — a shell command run by `qmd update` (#886)
|
|
9
|
+
* - `collections.*.path` — any directory the process can read (#889)
|
|
10
|
+
* - `models.embed` / `models.rerank` / `models.generate` — any `hf:` repo or
|
|
11
|
+
* local GGUF path (#889)
|
|
12
|
+
*
|
|
13
|
+
* Global `~/.config/qmd` is never gated. In-project collection paths and the
|
|
14
|
+
* built-in default model URIs are also allowed without approval: those are
|
|
15
|
+
* what a local config is for. Approvals are per config file and per gated
|
|
16
|
+
* set, recorded in `<config dir>/trusted.json`. Editing a hook, pointing a
|
|
17
|
+
* collection outside the project, or changing a custom model URI changes the
|
|
18
|
+
* digest and re-arms the gate.
|
|
19
|
+
*/
|
|
20
|
+
/** A collection's pre-update hook, as it will be executed. */
|
|
21
|
+
export type UpdateHook = {
|
|
22
|
+
collection: string;
|
|
23
|
+
command: string;
|
|
24
|
+
};
|
|
25
|
+
export type CollectionPath = {
|
|
26
|
+
collection: string;
|
|
27
|
+
path: string;
|
|
28
|
+
};
|
|
29
|
+
export type ModelSlot = "embed" | "rerank" | "generate";
|
|
30
|
+
export type ModelsSnapshot = {
|
|
31
|
+
embed?: string;
|
|
32
|
+
rerank?: string;
|
|
33
|
+
generate?: string;
|
|
34
|
+
};
|
|
35
|
+
export type SensitiveSnapshot = {
|
|
36
|
+
hooks: UpdateHook[];
|
|
37
|
+
paths: CollectionPath[];
|
|
38
|
+
models: ModelsSnapshot;
|
|
39
|
+
};
|
|
40
|
+
export type GatedItems = {
|
|
41
|
+
hooks: UpdateHook[];
|
|
42
|
+
paths: CollectionPath[];
|
|
43
|
+
models: Array<{
|
|
44
|
+
slot: ModelSlot;
|
|
45
|
+
uri: string;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
export type BuiltinModels = Required<ModelsSnapshot>;
|
|
49
|
+
export type TrustRecord = {
|
|
50
|
+
/** Digest of the gated set that was approved. */
|
|
51
|
+
hooks: string;
|
|
52
|
+
/** ISO timestamp of the approval. */
|
|
53
|
+
trustedAt: string;
|
|
54
|
+
};
|
|
55
|
+
export type TrustStore = Record<string, TrustRecord>;
|
|
56
|
+
/** Path of the trust database. Lives beside the global config. */
|
|
57
|
+
export declare function getTrustFilePath(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Whether a config path is project-local — i.e. discovered by walking up from
|
|
60
|
+
* the working directory rather than written by the user in their config dir.
|
|
61
|
+
*
|
|
62
|
+
* `findLocalConfigPath` and `qmd init` both use `.qmd/index.y{a,}ml`, and the
|
|
63
|
+
* global config lives in a directory named `qmd`, so the parent directory name
|
|
64
|
+
* separates the two cases without extra bookkeeping.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isLocalConfigPath(configPath: string): boolean;
|
|
67
|
+
/** Directory that contains the `.qmd` folder for a project-local config. */
|
|
68
|
+
export declare function projectRootFromConfig(configPath: string): string;
|
|
69
|
+
/** Expand a leading `~` to the user home directory. */
|
|
70
|
+
export declare function expandUserPath(raw: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a collection `path` from a project-local config: `~` expansion,
|
|
73
|
+
* then relative paths against the project root (the directory that contains
|
|
74
|
+
* `.qmd`), not against the process cwd.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveConfigCollectionPath(rawPath: string, configPath: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* True if `rawPath` from a project-local config stays inside that project
|
|
79
|
+
* (the directory containing `.qmd`), after `~` expansion, relative
|
|
80
|
+
* resolution, and symlink realpath.
|
|
81
|
+
*/
|
|
82
|
+
export declare function isCollectionPathInsideProject(configPath: string, rawPath: string): boolean;
|
|
83
|
+
export declare function gatedModels(models: ModelsSnapshot, builtins: BuiltinModels): Array<{
|
|
84
|
+
slot: ModelSlot;
|
|
85
|
+
uri: string;
|
|
86
|
+
}>;
|
|
87
|
+
export declare function gatedItems(configPath: string, snapshot: SensitiveSnapshot, builtins: BuiltinModels): GatedItems;
|
|
88
|
+
export declare function hasGatedItems(gated: GatedItems): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Stable digest over a hook set. Order-independent so that reordering
|
|
91
|
+
* collections in the YAML does not invalidate an approval, while any change to
|
|
92
|
+
* a command — or a new collection gaining one — does.
|
|
93
|
+
*/
|
|
94
|
+
export declare function hookDigest(hooks: UpdateHook[]): string;
|
|
95
|
+
/**
|
|
96
|
+
* Digest of the gated surface of a project-local config: hooks, resolved
|
|
97
|
+
* collection paths, and non-default model URIs. Missing model keys and the
|
|
98
|
+
* built-in default URIs are equivalent so that `qmd init` filling defaults
|
|
99
|
+
* into the YAML does not invalidate an approval.
|
|
100
|
+
*/
|
|
101
|
+
export declare function sensitiveDigest(snapshot: SensitiveSnapshot, configPath: string, builtins: BuiltinModels): string;
|
|
102
|
+
export declare function loadTrustStore(): TrustStore;
|
|
103
|
+
export declare function isTrusted(configPath: string, digest: string): boolean;
|
|
104
|
+
export declare function recordTrust(configPath: string, digest: string): void;
|
|
105
|
+
/** Drop the record for a config path. Returns false if there was none. */
|
|
106
|
+
export declare function revokeTrust(configPath: string): boolean;
|
|
107
|
+
export declare function listTrusted(): Array<{
|
|
108
|
+
path: string;
|
|
109
|
+
} & TrustRecord>;
|
|
110
|
+
export type HookGateDecision = {
|
|
111
|
+
action: "run";
|
|
112
|
+
digest: string;
|
|
113
|
+
} | {
|
|
114
|
+
action: "prompt";
|
|
115
|
+
digest: string;
|
|
116
|
+
} | {
|
|
117
|
+
action: "skip";
|
|
118
|
+
digest: string;
|
|
119
|
+
};
|
|
120
|
+
export type LocalConfigGateDecision = HookGateDecision;
|
|
121
|
+
/** True when the process has opted in to trusting project-local config unattended. */
|
|
122
|
+
export declare function isLocalConfigTrustOptedIn(env?: NodeJS.ProcessEnv): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Decide what to do with a config's `update:` hooks.
|
|
125
|
+
*
|
|
126
|
+
* Non-interactive callers — agents, CI, the MCP server — get `skip` rather than
|
|
127
|
+
* a hard failure: indexing is what they asked for, and failing the whole
|
|
128
|
+
* command would only push people toward a blanket opt-out.
|
|
129
|
+
*/
|
|
130
|
+
export declare function decideHookGate(options: {
|
|
131
|
+
configPath: string;
|
|
132
|
+
hooks: UpdateHook[];
|
|
133
|
+
isInteractive: boolean;
|
|
134
|
+
env?: NodeJS.ProcessEnv;
|
|
135
|
+
trustedCheck?: (configPath: string, digest: string) => boolean;
|
|
136
|
+
}): HookGateDecision;
|
|
137
|
+
/**
|
|
138
|
+
* Decide whether a project-local config may use its gated fields (hooks,
|
|
139
|
+
* out-of-project collection paths, non-default model URIs).
|
|
140
|
+
*
|
|
141
|
+
* In-project paths and built-in default models do not need a decision.
|
|
142
|
+
* Non-interactive callers get `skip`: in-project indexing still proceeds,
|
|
143
|
+
* hooks/outside paths/custom models do not.
|
|
144
|
+
*/
|
|
145
|
+
export declare function decideLocalConfigGate(options: {
|
|
146
|
+
configPath: string;
|
|
147
|
+
snapshot: SensitiveSnapshot;
|
|
148
|
+
builtins: BuiltinModels;
|
|
149
|
+
isInteractive: boolean;
|
|
150
|
+
env?: NodeJS.ProcessEnv;
|
|
151
|
+
trustedCheck?: (configPath: string, digest: string) => boolean;
|
|
152
|
+
}): LocalConfigGateDecision;
|
package/dist/trust.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var D=Object.defineProperty;var n=(t,e)=>D(t,"name",{value:e,configurable:!0});import{createHash as p}from"crypto";import{existsSync as m,mkdirSync as I,readFileSync as P,realpathSync as b,writeFileSync as A}from"fs";import{basename as L,dirname as c,isAbsolute as g,join as x,relative as F,resolve as u}from"path";import{getConfigDir as U}from"./collections.js";import{qmdHomedir as S}from"./paths.js";function k(){return x(U(),"trusted.json")}n(k,"getTrustFilePath");function T(t){return!t||t==="<inline>"?!1:L(c(u(t)))===".qmd"}n(T,"isLocalConfigPath");function d(t){return c(c(u(t)))}n(d,"projectRootFromConfig");function j(t){return t==="~"?S():t.startsWith("~/")||t.startsWith("~\\")?x(S(),t.slice(2)):t}n(j,"expandUserPath");function y(t,e){const r=j(String(t??"").trim());return r?g(r)?u(r):u(d(e),r):d(e)}n(y,"resolveConfigCollectionPath");function v(t){try{return b(t)}catch{return u(t)}}n(v,"realOrResolve");function N(t,e){const r=v(d(t)),o=v(y(e,t)),i=F(r,o);return i===""?!0:g(i)?!1:!i.split(/[/\\]/).includes("..")}n(N,"isCollectionPathInsideProject");function R(t,e){const r=[];for(const o of["embed","rerank","generate"]){const i=t[o];i&&i!==e[o]&&r.push({slot:o,uri:i})}return r}n(R,"gatedModels");function O(t,e,r){return{hooks:e.hooks,paths:e.paths.filter(o=>!N(t,o.path)),models:R(e.models,r)}}n(O,"gatedItems");function G(t){return t.hooks.length>0||t.paths.length>0||t.models.length>0}n(G,"hasGatedItems");function a(t,e){const r=t[0]??"",o=e[0]??"";return r<o?-1:r>o?1:0}n(a,"byFirst");function H(t){const e=JSON.stringify(t.map(r=>[r.collection,r.command]).sort(a));return p("sha256").update(e).digest("hex")}n(H,"hookDigest");function M(t,e,r){const o=O(e,t,r),i=JSON.stringify({hooks:o.hooks.map(s=>[s.collection,s.command]).sort(a),paths:o.paths.map(s=>[s.collection,y(s.path,e)]).sort(a),models:o.models.map(s=>[s.slot,s.uri]).sort(a)});return p("sha256").update(i).digest("hex")}n(M,"sensitiveDigest");function f(){const t=k();if(!m(t))return{};try{const e=JSON.parse(P(t,"utf-8"));return!e||typeof e!="object"||Array.isArray(e)?{}:e}catch{return{}}}n(f,"loadTrustStore");function C(t){const e=k(),r=c(e);m(r)||I(r,{recursive:!0}),A(e,`${JSON.stringify(t,null,2)}
|
|
2
|
+
`,"utf-8")}n(C,"saveTrustStore");function h(t){return u(t)}n(h,"trustKey");function _(t,e){return f()[h(t)]?.hooks===e}n(_,"isTrusted");function z(t,e){const r=f();r[h(t)]={hooks:e,trustedAt:new Date().toISOString()},C(r)}n(z,"recordTrust");function B(t){const e=f(),r=h(t);return r in e?(delete e[r],C(e),!0):!1}n(B,"revokeTrust");function V(){return Object.entries(f()).map(([t,e])=>({path:t,...e}))}n(V,"listTrusted");function l(t){return t?!["0","false","off","no","none"].includes(t.trim().toLowerCase()):!1}n(l,"isTruthyEnv");function J(t=process.env){return l(t.QMD_TRUST_LOCAL_CONFIG)||l(t.QMD_TRUST_UPDATE_HOOKS)}n(J,"isLocalConfigTrustOptedIn");function X(t){const e=t.env??process.env,r=H(t.hooks);return t.hooks.length===0?{action:"run",digest:r}:l(e.QMD_TRUST_UPDATE_HOOKS)||l(e.QMD_TRUST_LOCAL_CONFIG)?{action:"run",digest:r}:T(t.configPath)?(t.trustedCheck??_)(t.configPath,r)?{action:"run",digest:r}:{action:t.isInteractive?"prompt":"skip",digest:r}:{action:"run",digest:r}}n(X,"decideHookGate");function Y(t){const e=t.env??process.env,r=M(t.snapshot,t.configPath,t.builtins),o=O(t.configPath,t.snapshot,t.builtins);return G(o)?J(e)?{action:"run",digest:r}:T(t.configPath)?(t.trustedCheck??_)(t.configPath,r)?{action:"run",digest:r}:{action:t.isInteractive?"prompt":"skip",digest:r}:{action:"run",digest:r}:{action:"run",digest:r}}n(Y,"decideLocalConfigGate");export{X as decideHookGate,Y as decideLocalConfigGate,j as expandUserPath,O as gatedItems,R as gatedModels,k as getTrustFilePath,G as hasGatedItems,H as hookDigest,N as isCollectionPathInsideProject,T as isLocalConfigPath,J as isLocalConfigTrustOptedIn,_ as isTrusted,V as listTrusted,f as loadTrustStore,d as projectRootFromConfig,z as recordTrust,y as resolveConfigCollectionPath,B as revokeTrust,M as sensitiveDigest};
|
package/package.json
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ko-qmd",
|
|
3
|
+
"version": "2.8.3-ko.10",
|
|
4
|
+
"description": "Local hybrid search for Korean Markdown notes and Obsidian vaults — BM25 with Korean particle stripping, Hangul bigram FTS, Qwen3 vector search and LLM reranking, as CLI, MCP server and SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"qmd": "bin/qmd"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"dist/",
|
|
20
|
+
"skills/",
|
|
21
|
+
"scripts/build.mjs",
|
|
22
|
+
"scripts/check-package-grammars.mjs",
|
|
23
|
+
"scripts/package-smoke.mjs",
|
|
24
|
+
"scripts/test-all.mjs",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"prepare": "node scripts/install-hooks.mjs && node scripts/build.mjs",
|
|
30
|
+
"build": "node scripts/build.mjs",
|
|
31
|
+
"lint": "oxlint",
|
|
32
|
+
"test": "node scripts/test-all.mjs",
|
|
33
|
+
"test:types": "node ./node_modules/typescript/bin/tsc -p tsconfig.build.json --noEmit",
|
|
34
|
+
"test:node": "node ./node_modules/vitest/vitest.mjs run --reporter=verbose --testTimeout 60000",
|
|
35
|
+
"test:bun": "bun test --timeout 60000 --preload ./src/test-preload.ts",
|
|
36
|
+
"test:unit": "CI=true node ./node_modules/vitest/vitest.mjs run --reporter=verbose --testTimeout 60000 test/ && CI=true bun test --timeout 60000 --preload ./src/test-preload.ts test/",
|
|
37
|
+
"test:package": "node scripts/package-smoke.mjs",
|
|
38
|
+
"qmd": "tsx src/cli/qmd.ts",
|
|
39
|
+
"index": "tsx src/cli/qmd.ts index",
|
|
40
|
+
"vector": "tsx src/cli/qmd.ts vector",
|
|
41
|
+
"search": "tsx src/cli/qmd.ts search",
|
|
42
|
+
"vsearch": "tsx src/cli/qmd.ts vsearch",
|
|
43
|
+
"rerank": "tsx src/cli/qmd.ts rerank",
|
|
44
|
+
"inspector": "npx @modelcontextprotocol/inspector tsx src/cli/qmd.ts mcp",
|
|
45
|
+
"release": "./scripts/release.sh",
|
|
46
|
+
"smoke:package-grammars": "node scripts/check-package-grammars.mjs"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/tak-bro/ko-qmd.git"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/tak-bro/ko-qmd#readme",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/tak-bro/ko-qmd/issues"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@modelcontextprotocol/server": "2.3.1",
|
|
61
|
+
"better-sqlite3": "^13.0.3",
|
|
62
|
+
"fast-glob": "3.3.3",
|
|
63
|
+
"node-llama-cpp": "3.22.1",
|
|
64
|
+
"picomatch": "4.0.7",
|
|
65
|
+
"sqlite-vec": "0.1.9",
|
|
66
|
+
"tree-sitter-go": "0.25.0",
|
|
67
|
+
"tree-sitter-python": "0.25.0",
|
|
68
|
+
"tree-sitter-rust": "0.24.0",
|
|
69
|
+
"tree-sitter-typescript": "0.23.2",
|
|
70
|
+
"web-tree-sitter": "0.27.1",
|
|
71
|
+
"yaml": "2.9.1",
|
|
72
|
+
"zod": "4.6.6"
|
|
73
|
+
},
|
|
74
|
+
"optionalDependencies": {
|
|
75
|
+
"sqlite-vec-darwin-arm64": "0.1.9",
|
|
76
|
+
"sqlite-vec-darwin-x64": "0.1.9",
|
|
77
|
+
"sqlite-vec-linux-arm64": "0.1.9",
|
|
78
|
+
"sqlite-vec-linux-x64": "0.1.9",
|
|
79
|
+
"sqlite-vec-windows-x64": "0.1.9"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@oxlint/plugins": "1.87.0",
|
|
83
|
+
"@types/better-sqlite3": "7.6.13",
|
|
84
|
+
"oxlint": "1.87.0",
|
|
85
|
+
"tsx": "4.23.15",
|
|
86
|
+
"vitest": "3.2.7"
|
|
87
|
+
},
|
|
88
|
+
"pnpm": {
|
|
89
|
+
"onlyBuiltDependencies": []
|
|
90
|
+
},
|
|
91
|
+
"peerDependencies": {
|
|
92
|
+
"typescript": "^5.9.3"
|
|
93
|
+
},
|
|
94
|
+
"engines": {
|
|
95
|
+
"node": ">=22.0.0"
|
|
96
|
+
},
|
|
97
|
+
"keywords": [
|
|
98
|
+
"markdown",
|
|
99
|
+
"search",
|
|
100
|
+
"fts",
|
|
101
|
+
"full-text-search",
|
|
102
|
+
"vector",
|
|
103
|
+
"semantic-search",
|
|
104
|
+
"sqlite",
|
|
105
|
+
"bm25",
|
|
106
|
+
"embeddings",
|
|
107
|
+
"rag",
|
|
108
|
+
"mcp",
|
|
109
|
+
"reranking",
|
|
110
|
+
"knowledge-base",
|
|
111
|
+
"local-ai",
|
|
112
|
+
"llm",
|
|
113
|
+
"korean",
|
|
114
|
+
"hangul",
|
|
115
|
+
"korean-nlp",
|
|
116
|
+
"obsidian",
|
|
117
|
+
"hybrid-search",
|
|
118
|
+
"mcp-server",
|
|
119
|
+
"claude-code",
|
|
120
|
+
"qwen3"
|
|
121
|
+
],
|
|
122
|
+
"author": "Tobi Lutke <tobi@lutke.com>",
|
|
123
|
+
"license": "MIT"
|
|
124
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = join(fileURLToPath(new URL("..", import.meta.url)));
|
|
8
|
+
|
|
9
|
+
function run(command, args, options = {}) {
|
|
10
|
+
const result = spawnSync(command, args, {
|
|
11
|
+
cwd: root,
|
|
12
|
+
stdio: "inherit",
|
|
13
|
+
// Never shell:true — on Windows, cmd.exe splits unquoted process.execPath
|
|
14
|
+
// at spaces (C:\Program Files\nodejs\node.exe) and the build can exit 0
|
|
15
|
+
// with no dist/. spawnSync can spawn the binary + args array directly. (#681)
|
|
16
|
+
shell: false,
|
|
17
|
+
...options,
|
|
18
|
+
});
|
|
19
|
+
if (result.error || result.status !== 0) {
|
|
20
|
+
if (result.error) {
|
|
21
|
+
console.error(`build: failed to spawn ${command}: ${result.error.message}`);
|
|
22
|
+
}
|
|
23
|
+
process.exit(result.status ?? 1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
run(process.execPath, [join(root, "node_modules", "typescript", "bin", "tsc"), "-p", "tsconfig.build.json"]);
|
|
28
|
+
|
|
29
|
+
// Strip comments from emitted JS only (dist/**/*.js, never *.d.ts — the
|
|
30
|
+
// declarations carry the SDK's API docs). Uses the TypeScript compiler API
|
|
31
|
+
// with the build's own target/module so emit is identical minus trivia.
|
|
32
|
+
{
|
|
33
|
+
const { createRequire } = await import("node:module");
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const ts = require(join(root, "node_modules", "typescript"));
|
|
36
|
+
const { readdirSync } = await import("node:fs");
|
|
37
|
+
const jsFiles = [];
|
|
38
|
+
const walk = (dir) => {
|
|
39
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
40
|
+
const p = join(dir, e.name);
|
|
41
|
+
if (e.isDirectory()) walk(p);
|
|
42
|
+
else if (e.name.endsWith(".js")) jsFiles.push(p);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
walk(join(root, "dist"));
|
|
46
|
+
for (const f of jsFiles) {
|
|
47
|
+
const src = readFileSync(f, "utf8");
|
|
48
|
+
const out = ts.transpileModule(src, {
|
|
49
|
+
// Forced ESNext: single-file transpile cannot do nodenext package-scope
|
|
50
|
+
// detection (it would emit CJS); the full-program build emits ESM here.
|
|
51
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext, removeComments: true },
|
|
52
|
+
}).outputText;
|
|
53
|
+
writeFileSync(f, out);
|
|
54
|
+
}
|
|
55
|
+
console.log(`build: stripped comments from ${jsFiles.length} JS files`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Minify emitted JS (dist/**/*.js only). esbuild comes from the dev
|
|
59
|
+
// toolchain (tsx dependency) through its JS API: node_modules/.bin holds a
|
|
60
|
+
// native binary on POSIX but only .cmd/.ps1 shims on Windows, which
|
|
61
|
+
// spawnSync cannot run without a shell (#681). It is resolved from tsx's real
|
|
62
|
+
// path because pnpm does not hoist it to node_modules/esbuild. keepNames keeps
|
|
63
|
+
// top-level/function names readable in stack traces.
|
|
64
|
+
{
|
|
65
|
+
const { createRequire } = await import("node:module");
|
|
66
|
+
const esbuild = createRequire(realpathSync(join(root, "node_modules", "tsx", "package.json")))("esbuild");
|
|
67
|
+
const { readdirSync } = await import("node:fs");
|
|
68
|
+
const jsFiles = [];
|
|
69
|
+
const walk = (dir) => {
|
|
70
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
71
|
+
const p = join(dir, e.name);
|
|
72
|
+
if (e.isDirectory()) walk(p);
|
|
73
|
+
else if (e.name.endsWith(".js")) jsFiles.push(p);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
walk(join(root, "dist"));
|
|
77
|
+
for (const f of jsFiles) {
|
|
78
|
+
esbuild.buildSync({
|
|
79
|
+
entryPoints: [f], outfile: `${f}.min-tmp`, minify: true, keepNames: true,
|
|
80
|
+
format: "esm", target: "esnext", legalComments: "none", logLevel: "warning",
|
|
81
|
+
});
|
|
82
|
+
renameSync(`${f}.min-tmp`, f);
|
|
83
|
+
}
|
|
84
|
+
console.log(`build: minified ${jsFiles.length} JS files`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cliPath = join(root, "dist", "cli", "qmd.js");
|
|
88
|
+
const tmpPath = `${cliPath}.tmp`;
|
|
89
|
+
const built = readFileSync(cliPath, "utf8");
|
|
90
|
+
const withoutExistingShebang = built.startsWith("#!") ? built.slice(built.indexOf("\n") + 1) : built;
|
|
91
|
+
writeFileSync(tmpPath, `#!/usr/bin/env node\n${withoutExistingShebang}`);
|
|
92
|
+
renameSync(tmpPath, cliPath);
|
|
93
|
+
chmodSync(cliPath, 0o755);
|
|
94
|
+
|
|
95
|
+
// Stamp the commit this build came from, for `qmd --version`.
|
|
96
|
+
//
|
|
97
|
+
// It has to happen here: a published tarball has no git history of its own, so
|
|
98
|
+
// discovering the commit at runtime finds whatever repository the install
|
|
99
|
+
// happens to sit inside (a global install under /opt/homebrew reported
|
|
100
|
+
// Homebrew's HEAD as qmd's). See src/cli/version.ts.
|
|
101
|
+
//
|
|
102
|
+
// Uses spawnSync directly rather than run() above, because run() exits the
|
|
103
|
+
// build on a non-zero status: no git, no repo, or a source tarball must all
|
|
104
|
+
// degrade to an unstamped build, not a failed one.
|
|
105
|
+
function git(args) {
|
|
106
|
+
const result = spawnSync("git", args, { cwd: root, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
107
|
+
if (result.status !== 0 || typeof result.stdout !== "string") return null;
|
|
108
|
+
return result.stdout.trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildCommit() {
|
|
112
|
+
const top = git(["rev-parse", "--show-toplevel"]);
|
|
113
|
+
// Only trust a repository that *is* this package — same rule the runtime
|
|
114
|
+
// fallback applies, so building from inside an unrelated checkout can't
|
|
115
|
+
// stamp that checkout's commit.
|
|
116
|
+
if (top === null || realpathSync(top) !== realpathSync(root)) return "";
|
|
117
|
+
|
|
118
|
+
const commit = git(["rev-parse", "--short", "HEAD"]);
|
|
119
|
+
if (!commit) return "";
|
|
120
|
+
|
|
121
|
+
// A build from a dirty tree is not the commit it claims to be; say so, so
|
|
122
|
+
// "did my install actually take?" has an answer.
|
|
123
|
+
const status = git(["status", "--porcelain"]);
|
|
124
|
+
return status ? `${commit}-dirty` : commit;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(root, "dist", "cli", "build-info.json"),
|
|
129
|
+
`${JSON.stringify({ commit: buildCommit(), builtAt: new Date().toISOString() }, null, 2)}\n`,
|
|
130
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
const grammars = [
|
|
7
|
+
"tree-sitter-typescript/tree-sitter-typescript.wasm",
|
|
8
|
+
"tree-sitter-typescript/tree-sitter-tsx.wasm",
|
|
9
|
+
"tree-sitter-python/tree-sitter-python.wasm",
|
|
10
|
+
"tree-sitter-go/tree-sitter-go.wasm",
|
|
11
|
+
"tree-sitter-rust/tree-sitter-rust.wasm",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
let ok = true;
|
|
15
|
+
for (const grammar of grammars) {
|
|
16
|
+
try {
|
|
17
|
+
const resolved = require.resolve(grammar);
|
|
18
|
+
console.log(`ok ${grammar} -> ${resolved}`);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
ok = false;
|
|
21
|
+
console.error(`missing ${grammar}`);
|
|
22
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!ok) {
|
|
27
|
+
console.error("\nAST grammar package smoke check failed. Run `bun install` locally or repair a broken global install with the matching `bun add tree-sitter-...@<version>` command shown by `qmd status`.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
9
|
+
|
|
10
|
+
function run(label, command, args, options = {}) {
|
|
11
|
+
console.log(`==> ${label}`);
|
|
12
|
+
const { quiet, ...spawnOptions } = options;
|
|
13
|
+
const result = spawnSync(command, args, {
|
|
14
|
+
cwd: root,
|
|
15
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
16
|
+
shell: process.platform === "win32",
|
|
17
|
+
...spawnOptions,
|
|
18
|
+
});
|
|
19
|
+
if (result.status !== 0) {
|
|
20
|
+
console.error(`Package smoke failed: ${label}`);
|
|
21
|
+
if (quiet) {
|
|
22
|
+
if (result.stdout) process.stderr.write(result.stdout);
|
|
23
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
24
|
+
}
|
|
25
|
+
process.exit(result.status ?? 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertPath(path, label = path) {
|
|
30
|
+
const full = join(root, path);
|
|
31
|
+
if (!existsSync(full)) {
|
|
32
|
+
console.error(`Package smoke failed: missing ${label} (${path})`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return full;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
run("build compiled package", process.execPath, ["scripts/build.mjs"]);
|
|
39
|
+
run("AST grammar runtime packages", process.execPath, ["scripts/check-package-grammars.mjs"]);
|
|
40
|
+
|
|
41
|
+
for (const entry of pkg.files ?? []) {
|
|
42
|
+
assertPath(entry.replace(/\/$/, ""), `package.json files[] entry ${entry}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const [name, binPath] of Object.entries(pkg.bin ?? {})) {
|
|
46
|
+
const full = assertPath(binPath, `bin ${name}`);
|
|
47
|
+
const mode = statSync(full).mode;
|
|
48
|
+
if ((mode & 0o111) === 0) {
|
|
49
|
+
console.error(`Package smoke failed: bin ${name} is not executable (${binPath})`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
assertPath("dist/index.js", "compiled main export");
|
|
55
|
+
assertPath("dist/index.d.ts", "compiled type export");
|
|
56
|
+
assertPath("dist/cli/qmd.js", "compiled CLI");
|
|
57
|
+
|
|
58
|
+
run("compiled CLI under Node", process.execPath, ["dist/cli/qmd.js", "--help"], { quiet: true });
|
|
59
|
+
run("package wrapper", "sh", ["bin/qmd", "--help"], { quiet: true });
|
|
60
|
+
|
|
61
|
+
if (process.env.QMD_SKIP_BUN_SMOKE === "1") {
|
|
62
|
+
console.log("==> compiled CLI under Bun (skipped by QMD_SKIP_BUN_SMOKE=1)");
|
|
63
|
+
} else {
|
|
64
|
+
run("compiled CLI under Bun", "bun", ["dist/cli/qmd.js", "--help"], { quiet: true });
|
|
65
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
7
|
+
|
|
8
|
+
// Mirror bin/qmd's darwin Metal residency mitigation for test subprocesses.
|
|
9
|
+
// libggml-metal asserts on a non-empty residency set during its static
|
|
10
|
+
// destructor (ggml-org/llama.cpp#22593, fix open as #22595) and dumps a
|
|
11
|
+
// multi-kB backtrace at process exit even when tests pass. The env var must
|
|
12
|
+
// be set BEFORE the subprocess starts because libggml-metal reads it via
|
|
13
|
+
// libc getenv at module-load time. Opt out with QMD_METAL_KEEP_RESIDENCY=1.
|
|
14
|
+
const darwinMetalEnv =
|
|
15
|
+
process.platform === "darwin" && process.env.QMD_METAL_KEEP_RESIDENCY !== "1"
|
|
16
|
+
? { GGML_METAL_NO_RESIDENCY: "1" }
|
|
17
|
+
: {};
|
|
18
|
+
|
|
19
|
+
function run(label, command, args, options = {}) {
|
|
20
|
+
console.log(`==> ${label}`);
|
|
21
|
+
const { env: extraEnv, ...spawnOptions } = options;
|
|
22
|
+
const result = spawnSync(command, args, {
|
|
23
|
+
cwd: root,
|
|
24
|
+
stdio: "inherit",
|
|
25
|
+
shell: process.platform === "win32",
|
|
26
|
+
env: { ...process.env, ...darwinMetalEnv, ...(extraEnv ?? {}) },
|
|
27
|
+
...spawnOptions,
|
|
28
|
+
});
|
|
29
|
+
if (result.status !== 0) {
|
|
30
|
+
console.error(`Test task failed: ${label}`);
|
|
31
|
+
process.exit(result.status ?? 1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
run("TypeScript build typecheck", process.execPath, [join(root, "node_modules", "typescript", "bin", "tsc"), "-p", "tsconfig.build.json", "--noEmit"]);
|
|
36
|
+
run("Vitest suite under Node", process.execPath, [join(root, "node_modules", "vitest", "vitest.mjs"), "run", "--reporter=verbose", "--testTimeout", "60000", "test/"], { env: { CI: "true" } });
|
|
37
|
+
// RuleTester-based custom oxlint rule tests run under plain node only:
|
|
38
|
+
// vitest reports "No test suite found" and bun's runner rejects RuleTester.
|
|
39
|
+
// The quoted glob is expanded by node's own test runner, not the shell.
|
|
40
|
+
run("oxlint rule tests (node)", process.execPath, ["--import", "tsx", "--test", "tools/oxlint/**/*.test.ts"]);
|
|
41
|
+
run("Bun test suite", "bun", ["test", "--timeout", "60000", "--preload", "./src/test-preload.ts", "test/"], { env: { CI: "true" } });
|
|
42
|
+
run("Package smoke", process.execPath, ["scripts/package-smoke.mjs"]);
|