diffwiki-qmd 0.4.0-rc.202607220502.c0ac143
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/dist/bin.js +412 -0
- package/package.json +31 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { existsSync, readdirSync } from "fs";
|
|
5
|
+
import { stat, readdir } from "fs/promises";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join as join2 } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
// src/protocol.ts
|
|
11
|
+
function toMessage(err) {
|
|
12
|
+
if (err instanceof Error) return err.message;
|
|
13
|
+
if (typeof err === "string") return err;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.stringify(err);
|
|
16
|
+
} catch {
|
|
17
|
+
return String(err);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function serve(handlers, io = {}) {
|
|
21
|
+
const input = io.input ?? process.stdin;
|
|
22
|
+
const output = io.output ?? process.stdout;
|
|
23
|
+
const write = (obj) => {
|
|
24
|
+
output.write(`${JSON.stringify(obj)}
|
|
25
|
+
`);
|
|
26
|
+
};
|
|
27
|
+
const log = (level, message) => {
|
|
28
|
+
write({ log: { level, message } });
|
|
29
|
+
};
|
|
30
|
+
const dispatch = async (line) => {
|
|
31
|
+
let req;
|
|
32
|
+
try {
|
|
33
|
+
req = JSON.parse(line);
|
|
34
|
+
} catch {
|
|
35
|
+
process.stderr.write(`diffwiki-qmd: skipping malformed line: ${line}
|
|
36
|
+
`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const handler = handlers[req.op];
|
|
40
|
+
if (!handler) {
|
|
41
|
+
write({ id: req.id, ok: false, error: { message: `unknown op: ${req.op}` } });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const result = await handler(req.params ?? {}, log);
|
|
46
|
+
write({ id: req.id, ok: true, result });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
write({ id: req.id, ok: false, error: { message: toMessage(err) } });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
let buffer = "";
|
|
53
|
+
let chain = Promise.resolve();
|
|
54
|
+
input.setEncoding("utf8");
|
|
55
|
+
input.on("data", (chunk) => {
|
|
56
|
+
buffer += chunk;
|
|
57
|
+
let nl = buffer.indexOf("\n");
|
|
58
|
+
while (nl !== -1) {
|
|
59
|
+
const line = buffer.slice(0, nl).trim();
|
|
60
|
+
buffer = buffer.slice(nl + 1);
|
|
61
|
+
if (line.length > 0) {
|
|
62
|
+
chain = chain.then(() => dispatch(line));
|
|
63
|
+
}
|
|
64
|
+
nl = buffer.indexOf("\n");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
input.on("end", () => {
|
|
68
|
+
const tail = buffer.trim();
|
|
69
|
+
if (tail.length > 0) chain = chain.then(() => dispatch(tail));
|
|
70
|
+
chain.then(resolve);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/qmd.ts
|
|
76
|
+
import { execFile } from "child_process";
|
|
77
|
+
import { join } from "path";
|
|
78
|
+
import { promisify } from "util";
|
|
79
|
+
var INDEX = "diffwiki";
|
|
80
|
+
var nodeExecFileAsync = promisify(execFile);
|
|
81
|
+
var execFileAsync = (file, args, options) => nodeExecFileAsync(file, args, options);
|
|
82
|
+
function qmdEnv(home) {
|
|
83
|
+
const base = join(home, "qmd");
|
|
84
|
+
return { ...process.env, XDG_CACHE_HOME: base, XDG_CONFIG_HOME: base };
|
|
85
|
+
}
|
|
86
|
+
function modelsDir(home) {
|
|
87
|
+
return join(home, "qmd", "qmd", "models");
|
|
88
|
+
}
|
|
89
|
+
async function qmdOnPath(exec, env) {
|
|
90
|
+
try {
|
|
91
|
+
await exec("qmd", ["--version"], { env });
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function runQmd(exec, args, env) {
|
|
98
|
+
const { stdout } = await exec("qmd", args, { env });
|
|
99
|
+
return stdout.trim();
|
|
100
|
+
}
|
|
101
|
+
async function runQmdJson(exec, args, env) {
|
|
102
|
+
const { stdout } = await exec("qmd", args, { env });
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(stdout);
|
|
105
|
+
} catch {
|
|
106
|
+
throw new Error(`qmd ${args.join(" ")} did not return valid JSON: ${stdout.slice(0, 200)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/bin.ts
|
|
111
|
+
var VERSION = "0.3.0";
|
|
112
|
+
var realStatMtime = async (filePath) => {
|
|
113
|
+
try {
|
|
114
|
+
const s = await stat(filePath);
|
|
115
|
+
return s.mtimeMs;
|
|
116
|
+
} catch {
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
async function newestMdMtime(dirs, statFn) {
|
|
121
|
+
let newest = 0;
|
|
122
|
+
async function walk(dir) {
|
|
123
|
+
let entries;
|
|
124
|
+
try {
|
|
125
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const full = join2(dir, entry.name);
|
|
131
|
+
if (entry.isDirectory()) {
|
|
132
|
+
await walk(full);
|
|
133
|
+
} else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
|
|
134
|
+
const mt = await statFn(full);
|
|
135
|
+
if (mt > newest) newest = mt;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const d of dirs) {
|
|
140
|
+
await walk(d);
|
|
141
|
+
}
|
|
142
|
+
return newest;
|
|
143
|
+
}
|
|
144
|
+
function qmdIndexPath(home) {
|
|
145
|
+
return join2(home, "qmd", "qmd", INDEX, "index.sqlite3");
|
|
146
|
+
}
|
|
147
|
+
function splitQmdPath(row) {
|
|
148
|
+
let raw = "";
|
|
149
|
+
if (typeof row.file === "string" && row.file.length > 0) {
|
|
150
|
+
raw = row.file.replace(/^qmd:\/\//, "");
|
|
151
|
+
} else if (typeof row.displayPath === "string") {
|
|
152
|
+
raw = row.displayPath;
|
|
153
|
+
}
|
|
154
|
+
raw = raw.replace(/^\/+/, "");
|
|
155
|
+
const slash = raw.indexOf("/");
|
|
156
|
+
if (slash === -1) return { collection: raw, relPath: "" };
|
|
157
|
+
return { collection: raw.slice(0, slash), relPath: raw.slice(slash + 1) };
|
|
158
|
+
}
|
|
159
|
+
function searchSubcommand(type) {
|
|
160
|
+
switch (type) {
|
|
161
|
+
case "semantic":
|
|
162
|
+
return "vsearch";
|
|
163
|
+
case "hybrid":
|
|
164
|
+
return "query";
|
|
165
|
+
case "keyword":
|
|
166
|
+
default:
|
|
167
|
+
return "search";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function isAlreadyExists(err) {
|
|
171
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
172
|
+
return /already exists/i.test(msg);
|
|
173
|
+
}
|
|
174
|
+
function asCollections(params) {
|
|
175
|
+
const raw = params.collections;
|
|
176
|
+
if (!Array.isArray(raw)) return [];
|
|
177
|
+
return raw.filter(
|
|
178
|
+
(c) => !!c && typeof c.name === "string" && typeof c.path === "string"
|
|
179
|
+
).map((c) => ({ name: c.name, path: c.path }));
|
|
180
|
+
}
|
|
181
|
+
async function addCollections(exec, env, collections, log) {
|
|
182
|
+
for (const c of collections) {
|
|
183
|
+
try {
|
|
184
|
+
await runQmd(exec, ["collection", "add", c.path, "--name", c.name, "--index", INDEX], env);
|
|
185
|
+
log("info", `added collection ${c.name} (${c.path})`);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (!isAlreadyExists(err)) throw err;
|
|
188
|
+
log("info", `collection ${c.name} already present`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function makeHandlers(deps) {
|
|
193
|
+
let home = deps.home ?? join2(homedir(), ".diffwiki");
|
|
194
|
+
const exec = deps.exec;
|
|
195
|
+
const statMtime = deps.statMtime ?? realStatMtime;
|
|
196
|
+
async function ensureFresh(collections, searchType, log) {
|
|
197
|
+
try {
|
|
198
|
+
const roots = collections.map((c) => c.path);
|
|
199
|
+
const contentMtime = await newestMdMtime(roots, statMtime);
|
|
200
|
+
if (contentMtime === 0) return;
|
|
201
|
+
const indexMtime = await statMtime(qmdIndexPath(home));
|
|
202
|
+
if (indexMtime >= contentMtime) return;
|
|
203
|
+
const env = qmdEnv(home);
|
|
204
|
+
log("info", "index stale \u2014 running qmd update");
|
|
205
|
+
await runQmd(exec, ["update", "--index", INDEX], env);
|
|
206
|
+
if (searchType === "semantic" || searchType === "hybrid") {
|
|
207
|
+
try {
|
|
208
|
+
const status = await runQmdJson(exec, ["status", "--index", INDEX, "--format", "json"], env);
|
|
209
|
+
if (typeof status.needsEmbedding === "number" && status.needsEmbedding > 0) {
|
|
210
|
+
log("info", `${status.needsEmbedding} doc(s) need embedding \u2014 running qmd embed`);
|
|
211
|
+
await runQmd(exec, ["embed", "--index", INDEX], env);
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
initialize(params) {
|
|
221
|
+
if (typeof params.home === "string" && params.home.length > 0) {
|
|
222
|
+
home = params.home;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
name: "diffwiki-qmd",
|
|
226
|
+
kind: "search",
|
|
227
|
+
version: VERSION,
|
|
228
|
+
capabilities: {
|
|
229
|
+
searchTypes: ["keyword", "semantic", "hybrid"],
|
|
230
|
+
collectionFilter: true,
|
|
231
|
+
hooks: true,
|
|
232
|
+
setup: true
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
async setup(params, log) {
|
|
237
|
+
const env = qmdEnv(home);
|
|
238
|
+
const steps = [];
|
|
239
|
+
const installed = await qmdOnPath(exec, env);
|
|
240
|
+
if (!installed) {
|
|
241
|
+
log("error", "qmd not found on PATH \u2014 install with: npm i -g @tobilu/qmd");
|
|
242
|
+
steps.push({ step: "verify-qmd", ok: false, message: "qmd not on PATH" });
|
|
243
|
+
return { ready: false, steps };
|
|
244
|
+
}
|
|
245
|
+
steps.push({ step: "verify-qmd", ok: true });
|
|
246
|
+
const collections = asCollections(params);
|
|
247
|
+
await addCollections(exec, env, collections, log);
|
|
248
|
+
steps.push({
|
|
249
|
+
step: "add-collections",
|
|
250
|
+
ok: true,
|
|
251
|
+
message: `${collections.length} collection(s)`
|
|
252
|
+
});
|
|
253
|
+
try {
|
|
254
|
+
await runQmd(exec, ["update", "--index", INDEX], env);
|
|
255
|
+
steps.push({ step: "index", ok: true });
|
|
256
|
+
} catch (err) {
|
|
257
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
258
|
+
steps.push({ step: "index", ok: false, message });
|
|
259
|
+
}
|
|
260
|
+
if (params.embed === true || params.prefetchModels === true) {
|
|
261
|
+
log("info", "embedding index (may download models on first run)");
|
|
262
|
+
try {
|
|
263
|
+
await runQmd(exec, ["embed", "--index", INDEX], env);
|
|
264
|
+
steps.push({ step: "embed", ok: true });
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
267
|
+
steps.push({ step: "embed", ok: false, message });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { ready: true, steps };
|
|
271
|
+
},
|
|
272
|
+
async index(params, log) {
|
|
273
|
+
const env = qmdEnv(home);
|
|
274
|
+
const collections = asCollections(params);
|
|
275
|
+
await addCollections(exec, env, collections, log);
|
|
276
|
+
await runQmd(exec, ["update", "--index", INDEX], env);
|
|
277
|
+
if (params.embed === true) {
|
|
278
|
+
log("info", "embedding index");
|
|
279
|
+
await runQmd(exec, ["embed", "--index", INDEX], env);
|
|
280
|
+
}
|
|
281
|
+
return { indexed: collections.length };
|
|
282
|
+
},
|
|
283
|
+
async notify(params, log) {
|
|
284
|
+
const env = qmdEnv(home);
|
|
285
|
+
const collection = params.collection;
|
|
286
|
+
if (collection && typeof collection.name === "string") {
|
|
287
|
+
try {
|
|
288
|
+
await runQmd(exec, ["update", "--index", INDEX, "-c", collection.name], env);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
291
|
+
log("warn", `notify update failed for ${collection.name}: ${message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { accepted: true };
|
|
295
|
+
},
|
|
296
|
+
async search(params, log) {
|
|
297
|
+
const env = qmdEnv(home);
|
|
298
|
+
const term = typeof params.term === "string" ? params.term : "";
|
|
299
|
+
const type = typeof params.type === "string" ? params.type : "keyword";
|
|
300
|
+
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20;
|
|
301
|
+
const sub = searchSubcommand(type);
|
|
302
|
+
const collections = asCollections(params);
|
|
303
|
+
await ensureFresh(collections, type, log);
|
|
304
|
+
const args = [sub, term, "--index", INDEX, "--format", "json", "-n", String(limit)];
|
|
305
|
+
if (typeof params.collection === "string" && params.collection.length > 0) {
|
|
306
|
+
args.push("-c", params.collection);
|
|
307
|
+
}
|
|
308
|
+
const rows = await runQmdJson(exec, args, env);
|
|
309
|
+
const hits = (Array.isArray(rows) ? rows : []).map((row) => {
|
|
310
|
+
const { collection, relPath } = splitQmdPath(row);
|
|
311
|
+
return {
|
|
312
|
+
collection,
|
|
313
|
+
relPath,
|
|
314
|
+
title: typeof row.title === "string" ? row.title : relPath,
|
|
315
|
+
snippet: row.bestChunk || void 0,
|
|
316
|
+
score: typeof row.score === "number" ? row.score : void 0,
|
|
317
|
+
docid: typeof row.docid === "string" ? row.docid : void 0
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
return { hits };
|
|
321
|
+
},
|
|
322
|
+
async health(params, log) {
|
|
323
|
+
const env = qmdEnv(home);
|
|
324
|
+
const diagnostics = [];
|
|
325
|
+
const toolInstalled = await qmdOnPath(exec, env);
|
|
326
|
+
if (!toolInstalled) {
|
|
327
|
+
log("error", "qmd not on PATH");
|
|
328
|
+
diagnostics.push({
|
|
329
|
+
level: "error",
|
|
330
|
+
message: "qmd not installed \u2014 run: npm i -g @tobilu/qmd"
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
ready: false,
|
|
334
|
+
readiness: {
|
|
335
|
+
toolInstalled: false,
|
|
336
|
+
modelsPresent: false,
|
|
337
|
+
indexBuilt: false,
|
|
338
|
+
docsIndexed: 0,
|
|
339
|
+
embeddingsPending: 0,
|
|
340
|
+
firstSearchCost: "model-download"
|
|
341
|
+
},
|
|
342
|
+
diagnostics
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
let modelsPresent = false;
|
|
346
|
+
try {
|
|
347
|
+
const dir = modelsDir(home);
|
|
348
|
+
modelsPresent = existsSync(dir) && readdirSync(dir).length > 0;
|
|
349
|
+
} catch {
|
|
350
|
+
modelsPresent = false;
|
|
351
|
+
}
|
|
352
|
+
let status = {};
|
|
353
|
+
try {
|
|
354
|
+
status = await runQmdJson(exec, ["status", "--index", INDEX, "--format", "json"], env);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
357
|
+
diagnostics.push({ level: "warn", message: `qmd status unavailable: ${message}` });
|
|
358
|
+
}
|
|
359
|
+
const docsIndexed = typeof status.totalDocuments === "number" ? status.totalDocuments : 0;
|
|
360
|
+
const embeddingsPending = typeof status.needsEmbedding === "number" ? status.needsEmbedding : 0;
|
|
361
|
+
const indexBuilt = docsIndexed > 0;
|
|
362
|
+
const firstSearchCost = indexBuilt ? "instant" : modelsPresent ? "model-load" : "model-download";
|
|
363
|
+
diagnostics.push({
|
|
364
|
+
level: "ok",
|
|
365
|
+
message: `qmd ready \u2014 ${docsIndexed} docs indexed, ${embeddingsPending} pending embeddings`
|
|
366
|
+
});
|
|
367
|
+
if (!modelsPresent) {
|
|
368
|
+
diagnostics.push({
|
|
369
|
+
level: "warn",
|
|
370
|
+
message: "semantic/hybrid search will download models (~2GB) on first use \u2014 run: diffwiki index --embed"
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
ready: indexBuilt || toolInstalled,
|
|
375
|
+
readiness: {
|
|
376
|
+
toolInstalled,
|
|
377
|
+
modelsPresent,
|
|
378
|
+
indexBuilt,
|
|
379
|
+
docsIndexed,
|
|
380
|
+
embeddingsPending,
|
|
381
|
+
firstSearchCost
|
|
382
|
+
},
|
|
383
|
+
diagnostics
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
shutdown() {
|
|
387
|
+
return {};
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
var isMain = (() => {
|
|
392
|
+
try {
|
|
393
|
+
return process.argv[1] === fileURLToPath(import.meta.url);
|
|
394
|
+
} catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
})();
|
|
398
|
+
if (isMain) {
|
|
399
|
+
serve(makeHandlers({ exec: execFileAsync })).then(() => process.exit(0)).catch((err) => {
|
|
400
|
+
process.stderr.write(`diffwiki-qmd: fatal: ${String(err)}
|
|
401
|
+
`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
export {
|
|
406
|
+
makeHandlers,
|
|
407
|
+
newestMdMtime,
|
|
408
|
+
qmdIndexPath,
|
|
409
|
+
realStatMtime,
|
|
410
|
+
searchSubcommand,
|
|
411
|
+
splitQmdPath
|
|
412
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "diffwiki-qmd",
|
|
3
|
+
"version": "0.4.0-rc.202607220502.c0ac143",
|
|
4
|
+
"description": "diffwiki search plugin driving the qmd CLI (keyword/semantic/hybrid)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"diffwiki-qmd": "./dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.0.0"
|
|
15
|
+
},
|
|
16
|
+
"diffwiki": {
|
|
17
|
+
"kind": "search"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"test": "vitest run --passWithNoTests",
|
|
28
|
+
"lint": "eslint src tests --no-error-on-unmatched-pattern && tsc --noEmit",
|
|
29
|
+
"lint:fix": "eslint src tests --no-error-on-unmatched-pattern --fix"
|
|
30
|
+
}
|
|
31
|
+
}
|