docmind-mcp 0.3.0
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/.env.example +12 -0
- package/README.md +109 -0
- package/dist/cli.js +943 -0
- package/dist/index.js +24969 -0
- package/package.json +47 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
function defaultConfig(defaultRoot = "") {
|
|
17
|
+
return {
|
|
18
|
+
defaultRoot,
|
|
19
|
+
sirchmunkUrl: "http://127.0.0.1:8584",
|
|
20
|
+
researchTimeoutSec: 120,
|
|
21
|
+
searchTimeoutSec: 30
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function loadConfig() {
|
|
25
|
+
for (const file of [CONFIG_FILE, LEGACY_CONFIG_FILE]) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await readFile(file, "utf-8");
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
return { ...defaultConfig(), ...parsed };
|
|
30
|
+
} catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return defaultConfig();
|
|
35
|
+
}
|
|
36
|
+
async function saveConfig(config) {
|
|
37
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
38
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
39
|
+
return CONFIG_FILE;
|
|
40
|
+
}
|
|
41
|
+
function resolveRoot(config, rootArg) {
|
|
42
|
+
const root = rootArg ?? process.env.DOCMIND_ROOT ?? process.env.DIGMUNK_ROOT ?? config.defaultRoot;
|
|
43
|
+
if (!root) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"No knowledge root configured. Run `docmind init`, or pass a `root` argument / DOCMIND_ROOT env."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return resolve(root);
|
|
49
|
+
}
|
|
50
|
+
var CONFIG_DIR, CONFIG_FILE, LEGACY_CONFIG_FILE;
|
|
51
|
+
var init_config = __esm({
|
|
52
|
+
"src/config.ts"() {
|
|
53
|
+
"use strict";
|
|
54
|
+
CONFIG_DIR = join(homedir(), ".docmind");
|
|
55
|
+
CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
56
|
+
LEGACY_CONFIG_FILE = join(homedir(), ".digmunk", "config.json");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// src/installer.ts
|
|
61
|
+
var installer_exports = {};
|
|
62
|
+
__export(installer_exports, {
|
|
63
|
+
runInstall: () => runInstall
|
|
64
|
+
});
|
|
65
|
+
import { readFile as readFile2, writeFile as writeFile2, copyFile as copyFile2, access } from "node:fs/promises";
|
|
66
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
67
|
+
import { homedir as homedir4 } from "node:os";
|
|
68
|
+
import { join as join4, dirname } from "node:path";
|
|
69
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
70
|
+
function mcpServerEntry(root) {
|
|
71
|
+
return { command: ["docmind-mcp"], env: { DOCMIND_ROOT: root } };
|
|
72
|
+
}
|
|
73
|
+
async function which(cmd) {
|
|
74
|
+
return new Promise((resolve3) => {
|
|
75
|
+
execFile2("which", [cmd], (err) => resolve3(!err));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function installOpencode(root) {
|
|
79
|
+
const candidates = [
|
|
80
|
+
join4(homedir4(), ".config", "opencode", "opencode.json"),
|
|
81
|
+
join4(homedir4(), ".config", "opencode", "opencode.jsonc")
|
|
82
|
+
];
|
|
83
|
+
const configPath = candidates.find((p) => existsSync3(p));
|
|
84
|
+
if (!configPath) {
|
|
85
|
+
return {
|
|
86
|
+
target: "opencode",
|
|
87
|
+
ok: false,
|
|
88
|
+
detail: "global config not found",
|
|
89
|
+
manualHint: `Add to ~/.config/opencode/opencode.json:
|
|
90
|
+
${JSON.stringify({ mcp: { docmind: { type: "local", ...mcpServerEntry(root) } } }, null, 2)}`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const raw = await readFile2(configPath, "utf-8");
|
|
94
|
+
const jsonText = configPath.endsWith(".jsonc") ? raw.replace(/\/\/.*$/gm, "") : raw;
|
|
95
|
+
const config = JSON.parse(jsonText);
|
|
96
|
+
config.mcp = config.mcp ?? {};
|
|
97
|
+
config.mcp.docmind = { type: "local", ...mcpServerEntry(root) };
|
|
98
|
+
await copyFile2(configPath, `${configPath}.bak`);
|
|
99
|
+
await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
100
|
+
return { target: "opencode", ok: true, detail: `registered in ${configPath} (backup: .bak)` };
|
|
101
|
+
}
|
|
102
|
+
async function installClaudeCode(root) {
|
|
103
|
+
if (await which("claude")) {
|
|
104
|
+
const result = await new Promise((resolve3) => {
|
|
105
|
+
execFile2(
|
|
106
|
+
"claude",
|
|
107
|
+
["mcp", "add", "--scope", "user", "docmind", "--", "docmind-mcp"],
|
|
108
|
+
{ env: { ...process.env, DOCMIND_ROOT: root }, timeout: 3e4 },
|
|
109
|
+
(err, stdout2, stderr) => {
|
|
110
|
+
if (err) resolve3({ ok: false, detail: `claude mcp add failed: ${(stderr || err.message).slice(0, 200)}` });
|
|
111
|
+
else resolve3({ ok: true, detail: `claude mcp add succeeded: ${(stdout2 || "done").trim().slice(0, 120)}` });
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
if (result.ok) return { target: "claude-code", ...result };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
target: "claude-code",
|
|
119
|
+
ok: false,
|
|
120
|
+
detail: "claude CLI unavailable",
|
|
121
|
+
manualHint: `Add to ~/.claude.json mcpServers:
|
|
122
|
+
"docmind": {"command": "docmind-mcp", "env": {"DOCMIND_ROOT": "${root}"}}`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function installCodex(root) {
|
|
126
|
+
const configPath = join4(homedir4(), ".codex", "config.toml");
|
|
127
|
+
const section = `
|
|
128
|
+
[mcp_servers.docmind]
|
|
129
|
+
command = "docmind-mcp"
|
|
130
|
+
env = { "DOCMIND_ROOT" = "${root}" }
|
|
131
|
+
`;
|
|
132
|
+
if (existsSync3(configPath)) {
|
|
133
|
+
const raw = await readFile2(configPath, "utf-8");
|
|
134
|
+
if (raw.includes("[mcp_servers.docmind]")) {
|
|
135
|
+
return { target: "codex", ok: true, detail: "already registered in ~/.codex/config.toml" };
|
|
136
|
+
}
|
|
137
|
+
await copyFile2(configPath, `${configPath}.bak`);
|
|
138
|
+
await writeFile2(configPath, raw.replace(/\n*$/, "\n") + section, "utf-8");
|
|
139
|
+
return { target: "codex", ok: true, detail: `registered in ${configPath} (backup: .bak)` };
|
|
140
|
+
}
|
|
141
|
+
await import("node:fs/promises").then((m) => m.mkdir(dirname(configPath), { recursive: true }));
|
|
142
|
+
await writeFile2(configPath, section.trimStart(), "utf-8");
|
|
143
|
+
return { target: "codex", ok: true, detail: `created ${configPath}` };
|
|
144
|
+
}
|
|
145
|
+
function genericInstructions(root) {
|
|
146
|
+
const entry = mcpServerEntry(root);
|
|
147
|
+
return {
|
|
148
|
+
target: "generic",
|
|
149
|
+
ok: true,
|
|
150
|
+
detail: `Register this MCP stdio server in your agent config:
|
|
151
|
+
${JSON.stringify({ docmind: entry }, null, 2)}`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function runInstall(target, rootOverride) {
|
|
155
|
+
const config = await loadConfig();
|
|
156
|
+
const root = rootOverride ?? config.defaultRoot;
|
|
157
|
+
if (!root) {
|
|
158
|
+
return [{ target: target ?? "all", ok: false, detail: "no knowledge root configured \u2014 run `docmind init` first" }];
|
|
159
|
+
}
|
|
160
|
+
const targets = target ? [target] : ["opencode", "claude-code", "codex"];
|
|
161
|
+
const results = [];
|
|
162
|
+
for (const t of targets) {
|
|
163
|
+
switch (t) {
|
|
164
|
+
case "opencode":
|
|
165
|
+
results.push(await installOpencode(root));
|
|
166
|
+
break;
|
|
167
|
+
case "claude-code":
|
|
168
|
+
results.push(await installClaudeCode(root));
|
|
169
|
+
break;
|
|
170
|
+
case "codex":
|
|
171
|
+
results.push(await installCodex(root));
|
|
172
|
+
break;
|
|
173
|
+
case "generic":
|
|
174
|
+
case "manual":
|
|
175
|
+
results.push(genericInstructions(root));
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
results.push({
|
|
179
|
+
target: t,
|
|
180
|
+
ok: false,
|
|
181
|
+
detail: `unknown target "${t}" (supported: opencode, claude-code, codex, generic)`
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return results;
|
|
186
|
+
}
|
|
187
|
+
var init_installer = __esm({
|
|
188
|
+
"src/installer.ts"() {
|
|
189
|
+
"use strict";
|
|
190
|
+
init_config();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// src/lock.ts
|
|
195
|
+
function serialized(fn) {
|
|
196
|
+
const next = chain.then(fn, fn);
|
|
197
|
+
chain = next.catch(() => void 0);
|
|
198
|
+
return next;
|
|
199
|
+
}
|
|
200
|
+
var chain;
|
|
201
|
+
var init_lock = __esm({
|
|
202
|
+
"src/lock.ts"() {
|
|
203
|
+
"use strict";
|
|
204
|
+
chain = Promise.resolve();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// src/zg.ts
|
|
209
|
+
var zg_exports = {};
|
|
210
|
+
__export(zg_exports, {
|
|
211
|
+
parseZgOutput: () => parseZgOutput,
|
|
212
|
+
readHitFile: () => readHitFile,
|
|
213
|
+
zgIndex: () => zgIndex,
|
|
214
|
+
zgRg: () => zgRg,
|
|
215
|
+
zgSearch: () => zgSearch,
|
|
216
|
+
zgStatus: () => zgStatus
|
|
217
|
+
});
|
|
218
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
219
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
220
|
+
function runZg(root, args, timeoutSec) {
|
|
221
|
+
return new Promise((resolve3, reject) => {
|
|
222
|
+
const child = spawn2("zg", [...args, "--no-color"], {
|
|
223
|
+
cwd: root,
|
|
224
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
225
|
+
});
|
|
226
|
+
let stdout2 = "";
|
|
227
|
+
let stderr = "";
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
child.kill("SIGKILL");
|
|
230
|
+
reject(new Error(`zg timed out after ${timeoutSec}s`));
|
|
231
|
+
}, timeoutSec * 1e3);
|
|
232
|
+
child.stdout.on("data", (d) => stdout2 += d);
|
|
233
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
234
|
+
child.on("error", (e) => {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
reject(new Error(`zg spawn failed: ${e.message}. Is zvec-grep installed?`));
|
|
237
|
+
});
|
|
238
|
+
child.on("close", (code) => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
resolve3({ code: code ?? 1, stdout: stdout2, stderr });
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function parseZgOutput(output, query) {
|
|
245
|
+
const hits = [];
|
|
246
|
+
const groups = [];
|
|
247
|
+
let current = null;
|
|
248
|
+
for (const rawLine of output.split("\n")) {
|
|
249
|
+
const line = rawLine.replace(/\r$/, "");
|
|
250
|
+
const group = line.match(/^Q\d+\s+\[(\w+)\]:\s(.*)$/);
|
|
251
|
+
if (group) {
|
|
252
|
+
groups.push(group[2]);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const total = line.match(/^hits:\s(\d+)$/);
|
|
256
|
+
if (total) continue;
|
|
257
|
+
if (line.startsWith("query groups") || line.trim() === "") continue;
|
|
258
|
+
const header = line.match(HIT_HEADER);
|
|
259
|
+
if (header) {
|
|
260
|
+
current = {
|
|
261
|
+
rank: Number(header[1]),
|
|
262
|
+
matchedBy: header[2],
|
|
263
|
+
file: header[3],
|
|
264
|
+
lineStart: Number(header[4]),
|
|
265
|
+
lineEnd: Number(header[5]),
|
|
266
|
+
snippets: []
|
|
267
|
+
};
|
|
268
|
+
hits.push(current);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!current) continue;
|
|
272
|
+
const meta = line.match(META_LINE);
|
|
273
|
+
if (meta) {
|
|
274
|
+
if (meta[1] === "heading") current.heading = meta[2];
|
|
275
|
+
if (meta[1] === "scope") current.scope = meta[2];
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const content = line.match(CONTENT_LINE);
|
|
279
|
+
if (content) {
|
|
280
|
+
current.snippets.push({ line: Number(content[1]), text: content[2] });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
hits.sort((a, b) => a.rank - b.rank);
|
|
284
|
+
return { query, groups, hits, raw: output };
|
|
285
|
+
}
|
|
286
|
+
async function zgSearch(root, query, opts = {}) {
|
|
287
|
+
return serialized(async () => {
|
|
288
|
+
const { limit, timeoutSec = 30 } = opts;
|
|
289
|
+
const args = ["query", query];
|
|
290
|
+
if (limit) args.push("--limit", String(limit));
|
|
291
|
+
const { code, stdout: stdout2, stderr } = await runZg(root, args, timeoutSec);
|
|
292
|
+
if (code !== 0 && !stdout2.trim()) {
|
|
293
|
+
throw new Error(`zg query failed (exit ${code}): ${stderr.trim() || "no output"}`);
|
|
294
|
+
}
|
|
295
|
+
return parseZgOutput(stdout2, query);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async function zgRg(root, pattern, opts = {}) {
|
|
299
|
+
return serialized(async () => {
|
|
300
|
+
const { timeoutSec = 30 } = opts;
|
|
301
|
+
const { code, stdout: stdout2, stderr } = await runZg(
|
|
302
|
+
root,
|
|
303
|
+
["query", "--rg", "-F", "--", pattern, "."],
|
|
304
|
+
timeoutSec
|
|
305
|
+
);
|
|
306
|
+
if (code !== 0 && !stdout2.trim()) {
|
|
307
|
+
throw new Error(`zg rg failed (exit ${code}): ${stderr.trim() || "no output"}`);
|
|
308
|
+
}
|
|
309
|
+
return stdout2;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
async function zgIndex(root, timeoutSec = 120) {
|
|
313
|
+
return serialized(async () => {
|
|
314
|
+
const { code, stdout: stdout2, stderr } = await runZg(root, ["index", "--hidden"], timeoutSec);
|
|
315
|
+
if (code !== 0) throw new Error(`zg index failed (exit ${code}): ${stderr.trim()}`);
|
|
316
|
+
return stdout2.trim();
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async function zgStatus(root) {
|
|
320
|
+
const { stdout: stdout2, stderr } = await runZg(root, ["status", "--check-ready"], 15).catch((e) => ({
|
|
321
|
+
code: 1,
|
|
322
|
+
stdout: "",
|
|
323
|
+
stderr: String(e.message)
|
|
324
|
+
}));
|
|
325
|
+
return { ready: /is ready/.test(stdout2), detail: (stdout2 + stderr).trim() };
|
|
326
|
+
}
|
|
327
|
+
async function readHitFile(root, hit) {
|
|
328
|
+
return readFile3(`${root}/${hit.file}`, "utf-8");
|
|
329
|
+
}
|
|
330
|
+
var HIT_HEADER, META_LINE, CONTENT_LINE;
|
|
331
|
+
var init_zg = __esm({
|
|
332
|
+
"src/zg.ts"() {
|
|
333
|
+
"use strict";
|
|
334
|
+
init_lock();
|
|
335
|
+
HIT_HEADER = /^#(\d+)\s+matchedBy=(\S+)\s+(.+?):(\d+)-(\d+)\s*$/;
|
|
336
|
+
META_LINE = /^(heading|heading_level|scope):\s(.*)$/;
|
|
337
|
+
CONTENT_LINE = /^(\d+)\t(.*)$/;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// src/wizard.ts
|
|
342
|
+
init_config();
|
|
343
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
344
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
345
|
+
import { resolve as resolve2 } from "node:path";
|
|
346
|
+
import { homedir as homedir5 } from "node:os";
|
|
347
|
+
import { join as join5 } from "node:path";
|
|
348
|
+
import { stdin, stdout } from "node:process";
|
|
349
|
+
|
|
350
|
+
// src/daemon.ts
|
|
351
|
+
import { spawn } from "node:child_process";
|
|
352
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
353
|
+
import { homedir as homedir2 } from "node:os";
|
|
354
|
+
import { join as join2 } from "node:path";
|
|
355
|
+
var PID_FILE = join2(homedir2(), ".docmind", "sirchmunk.pid");
|
|
356
|
+
async function fetchStatus(url, timeoutMs = 4e3) {
|
|
357
|
+
try {
|
|
358
|
+
const res = await fetch(`${url}/api/v1/search/status`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
359
|
+
if (!res.ok) return { reachable: false, body: null };
|
|
360
|
+
return { reachable: true, body: await res.json() };
|
|
361
|
+
} catch {
|
|
362
|
+
return { reachable: false, body: null };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function readManagedPid() {
|
|
366
|
+
try {
|
|
367
|
+
const pid = Number(readFileSync(PID_FILE, "utf-8").trim());
|
|
368
|
+
process.kill(pid, 0);
|
|
369
|
+
return pid;
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function serveState(url) {
|
|
375
|
+
const { reachable, body } = await fetchStatus(url);
|
|
376
|
+
const data = body?.data ?? {};
|
|
377
|
+
return {
|
|
378
|
+
reachable,
|
|
379
|
+
llmConfigured: Boolean(data.llm_configured),
|
|
380
|
+
model: data.llm_model ?? null,
|
|
381
|
+
status: data.status ?? (reachable ? "unknown" : "unreachable"),
|
|
382
|
+
pid: readManagedPid() ?? void 0,
|
|
383
|
+
managed: readManagedPid() !== null
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async function serveUp(url, waitTimeoutSec = 120) {
|
|
387
|
+
const existing = await serveState(url);
|
|
388
|
+
if (existing.reachable) return { started: false, state: existing };
|
|
389
|
+
const dir = join2(homedir2(), ".docmind");
|
|
390
|
+
writeFileSync(PID_FILE, "", "utf-8");
|
|
391
|
+
const logFile = join2(dir, "sirchmunk-serve.log");
|
|
392
|
+
const out = existsSync(logFile) ? "a" : "w";
|
|
393
|
+
const { openSync } = await import("node:fs");
|
|
394
|
+
const logFd = openSync(logFile, out);
|
|
395
|
+
const child = spawn("sirchmunk", ["serve"], {
|
|
396
|
+
detached: true,
|
|
397
|
+
stdio: ["ignore", logFd, logFd],
|
|
398
|
+
env: { ...process.env }
|
|
399
|
+
});
|
|
400
|
+
child.unref();
|
|
401
|
+
writeFileSync(PID_FILE, String(child.pid), "utf-8");
|
|
402
|
+
const deadline = Date.now() + waitTimeoutSec * 1e3;
|
|
403
|
+
while (Date.now() < deadline) {
|
|
404
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
405
|
+
const state = await serveState(url);
|
|
406
|
+
if (state.reachable) return { started: true, state };
|
|
407
|
+
}
|
|
408
|
+
return { started: true, state: await serveState(url) };
|
|
409
|
+
}
|
|
410
|
+
async function serveDown(url) {
|
|
411
|
+
const pid = readManagedPid();
|
|
412
|
+
if (pid === null) {
|
|
413
|
+
const state = await serveState(url);
|
|
414
|
+
return {
|
|
415
|
+
stopped: false,
|
|
416
|
+
detail: state.reachable ? `sirchmunk serve is running but was not started by docmind (kill its process manually: pid unknown).` : "sirchmunk serve is not running."
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
process.kill(pid, "SIGTERM");
|
|
421
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
422
|
+
try {
|
|
423
|
+
process.kill(pid, 0);
|
|
424
|
+
process.kill(pid, "SIGKILL");
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
} catch (e) {
|
|
428
|
+
return { stopped: false, detail: `failed to stop pid ${pid}: ${e.message}` };
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
unlinkSync(PID_FILE);
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
return { stopped: true, detail: `stopped sirchmunk serve (pid ${pid}).` };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/deps.ts
|
|
438
|
+
import { execFile } from "node:child_process";
|
|
439
|
+
import { createWriteStream, existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
440
|
+
import { chmod as chmodAsync, copyFile, mkdir as mkdirAsync } from "node:fs/promises";
|
|
441
|
+
import { homedir as homedir3, platform, arch } from "node:os";
|
|
442
|
+
import { join as join3 } from "node:path";
|
|
443
|
+
import { pipeline } from "node:stream/promises";
|
|
444
|
+
function run(cmd, args, timeoutMs = 12e4) {
|
|
445
|
+
return new Promise((resolve3, reject) => {
|
|
446
|
+
execFile(cmd, args, { timeout: timeoutMs, encoding: "utf-8" }, (err, stdout2, stderr) => {
|
|
447
|
+
if (err && typeof err.code !== "number") return reject(err);
|
|
448
|
+
resolve3({ code: err?.code ?? 0, stdout: stdout2, stderr });
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async function checkZg() {
|
|
453
|
+
try {
|
|
454
|
+
const { stdout: stdout2 } = await run("zg", ["--version"]);
|
|
455
|
+
return { name: "zvec-grep (zg)", ok: true, detail: stdout2.trim() };
|
|
456
|
+
} catch {
|
|
457
|
+
return {
|
|
458
|
+
name: "zvec-grep (zg)",
|
|
459
|
+
ok: false,
|
|
460
|
+
detail: "not found",
|
|
461
|
+
fixHint: "npm install -g @zvec/zvec-grep"
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function checkRipgrep() {
|
|
466
|
+
try {
|
|
467
|
+
const { stdout: stdout2 } = await run("rg", ["--version"]);
|
|
468
|
+
return { name: "ripgrep (rg)", ok: true, detail: stdout2.split("\n")[0].trim() };
|
|
469
|
+
} catch {
|
|
470
|
+
return { name: "ripgrep (rg)", ok: false, detail: "not found", fixHint: "apt install ripgrep / brew install ripgrep" };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function checkRga() {
|
|
474
|
+
try {
|
|
475
|
+
const { stdout: stdout2 } = await run("rga", ["--version"]);
|
|
476
|
+
return { name: "ripgrep-all (rga)", ok: true, detail: stdout2.split("\n")[0].trim() };
|
|
477
|
+
} catch {
|
|
478
|
+
return {
|
|
479
|
+
name: "ripgrep-all (rga)",
|
|
480
|
+
ok: false,
|
|
481
|
+
detail: "not found (sirchmunk auto-install hits GitHub rate limits)",
|
|
482
|
+
fixHint: "docmind init will install it from a pinned release URL"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function checkSirchmunk() {
|
|
487
|
+
try {
|
|
488
|
+
const { stdout: stdout2 } = await run("sirchmunk", ["version"]);
|
|
489
|
+
return { name: "sirchmunk", ok: true, detail: stdout2.trim() };
|
|
490
|
+
} catch {
|
|
491
|
+
return {
|
|
492
|
+
name: "sirchmunk",
|
|
493
|
+
ok: false,
|
|
494
|
+
detail: "not found",
|
|
495
|
+
fixHint: "pip install torch (CPU) first, then pip install 'sirchmunk[mcp]'"
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function checkTorch() {
|
|
500
|
+
try {
|
|
501
|
+
const { stdout: stdout2 } = await run("python3", ["-c", "import torch; print(torch.__version__)"]);
|
|
502
|
+
const v = stdout2.trim();
|
|
503
|
+
return { name: "pytorch", ok: true, detail: v.includes("cpu") ? `${v} (CPU)` : v };
|
|
504
|
+
} catch {
|
|
505
|
+
return { name: "pytorch", ok: false, detail: "not installed", fixHint: "pip install torch --index-url https://download.pytorch.org/whl/cpu" };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function checkSirchmunkEnv() {
|
|
509
|
+
const envFile = join3(homedir3(), ".sirchmunk", ".env");
|
|
510
|
+
if (!existsSync2(envFile)) {
|
|
511
|
+
return { name: "sirchmunk LLM config", ok: false, detail: "~/.sirchmunk/.env missing", fixHint: "docmind init will create it" };
|
|
512
|
+
}
|
|
513
|
+
const content = await import("node:fs/promises").then((m) => m.readFile(envFile, "utf-8"));
|
|
514
|
+
const hasKey = /LLM_API_KEY\s*=\s*\S+/.test(content);
|
|
515
|
+
const model = content.match(/LLM_MODEL_NAME\s*=\s*(\S+)/)?.[1];
|
|
516
|
+
const baseUrl = content.match(/LLM_BASE_URL\s*=\s*(\S+)/)?.[1];
|
|
517
|
+
if (baseUrl && !/^https?:\/\/.+/.test(baseUrl)) {
|
|
518
|
+
return {
|
|
519
|
+
name: "sirchmunk LLM config",
|
|
520
|
+
ok: false,
|
|
521
|
+
detail: `LLM_BASE_URL is invalid ("${baseUrl}") \u2014 research will fail with connection errors`,
|
|
522
|
+
fixHint: `edit ~/.sirchmunk/.env, set LLM_BASE_URL=https://<provider>/v1, then re-run \`docmind up\``
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
name: "sirchmunk LLM config",
|
|
527
|
+
ok: hasKey,
|
|
528
|
+
detail: hasKey ? `key present${model ? `, model: ${model}` : ""}` : "LLM_API_KEY not set in ~/.sirchmunk/.env"
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async function installZg() {
|
|
532
|
+
const { code, stderr } = await run("npm", ["install", "-g", "@zvec/zvec-grep"], 6e5);
|
|
533
|
+
if (code !== 0) throw new Error(`npm install -g @zvec/zvec-grep failed: ${stderr.slice(0, 300)}`);
|
|
534
|
+
}
|
|
535
|
+
async function installTorchCpu() {
|
|
536
|
+
const { code, stderr } = await run(
|
|
537
|
+
"pip",
|
|
538
|
+
["install", "--break-system-packages", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
|
|
539
|
+
18e5
|
|
540
|
+
);
|
|
541
|
+
if (code !== 0) throw new Error(`CPU torch install failed: ${stderr.slice(0, 300)}`);
|
|
542
|
+
}
|
|
543
|
+
async function installSirchmunk() {
|
|
544
|
+
const { code, stderr } = await run("pip", ["install", "--break-system-packages", "sirchmunk[mcp]"], 12e5);
|
|
545
|
+
if (code !== 0) throw new Error(`pip install sirchmunk[mcp] failed: ${stderr.slice(0, 300)}`);
|
|
546
|
+
}
|
|
547
|
+
var RGA_VERSION = "v1.0.0-alpha.5";
|
|
548
|
+
var RGA_ASSET = `ripgrep_all-${RGA_VERSION}-x86_64-unknown-linux-musl.tar.gz`;
|
|
549
|
+
var RGA_URL = `https://github.com/phiresky/ripgrep-all/releases/download/${RGA_VERSION}/${RGA_ASSET}`;
|
|
550
|
+
async function installRga() {
|
|
551
|
+
if (platform() !== "linux" || arch() !== "x64") {
|
|
552
|
+
throw new Error(`Automatic rga install supports linux/x64 only. On this platform, install ripgrep-all manually.`);
|
|
553
|
+
}
|
|
554
|
+
const tmpDir = "/tmp/opencode/docmind-rga";
|
|
555
|
+
const tarball = join3(tmpDir, RGA_ASSET);
|
|
556
|
+
await mkdirAsync(tmpDir, { recursive: true });
|
|
557
|
+
const res = await fetch(RGA_URL, { redirect: "follow" });
|
|
558
|
+
if (!res.ok || !res.body) {
|
|
559
|
+
throw new Error(`rga download failed (HTTP ${res.status}) from ${RGA_URL}`);
|
|
560
|
+
}
|
|
561
|
+
await pipeline(res.body, createWriteStream(tarball));
|
|
562
|
+
const extractDir = join3(tmpDir, "out");
|
|
563
|
+
await mkdirAsync(extractDir, { recursive: true });
|
|
564
|
+
await new Promise((resolve3, reject) => {
|
|
565
|
+
execFile("tar", ["xzf", tarball, "-C", extractDir], (err) => err ? reject(err) : resolve3());
|
|
566
|
+
});
|
|
567
|
+
const binDir = "/usr/local/bin";
|
|
568
|
+
for (const bin of ["rga", "rga-preproc"]) {
|
|
569
|
+
const src = join3(extractDir, `ripgrep_all-${RGA_VERSION}-x86_64-unknown-linux-musl`, bin);
|
|
570
|
+
const dest = join3(binDir, bin);
|
|
571
|
+
await copyFile(src, dest);
|
|
572
|
+
await chmodAsync(dest, 493);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function writeSirchmunkEnv(llm) {
|
|
576
|
+
if (!/^https?:\/\/.+/.test(llm.baseUrl)) {
|
|
577
|
+
throw new Error(`Invalid LLM base URL "${llm.baseUrl}" \u2014 expected an http(s) URL like https://api.example.com/v1`);
|
|
578
|
+
}
|
|
579
|
+
const dir = join3(homedir3(), ".sirchmunk");
|
|
580
|
+
mkdirSync(dir, { recursive: true });
|
|
581
|
+
const envFile = join3(dir, ".env");
|
|
582
|
+
const content = [
|
|
583
|
+
"# Written by `docmind init`",
|
|
584
|
+
`LLM_API_KEY=${llm.key}`,
|
|
585
|
+
`LLM_BASE_URL=${llm.baseUrl}`,
|
|
586
|
+
`LLM_MODEL_NAME=${llm.model}`,
|
|
587
|
+
""
|
|
588
|
+
].join("\n");
|
|
589
|
+
writeFileSync2(envFile, content, "utf-8");
|
|
590
|
+
return envFile;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/wizard.ts
|
|
594
|
+
var LineReader = class {
|
|
595
|
+
buf = "";
|
|
596
|
+
lines = [];
|
|
597
|
+
eof = false;
|
|
598
|
+
waiter = null;
|
|
599
|
+
constructor() {
|
|
600
|
+
stdin.setEncoding("utf-8");
|
|
601
|
+
stdin.on("data", (chunk) => {
|
|
602
|
+
this.buf += chunk;
|
|
603
|
+
let idx;
|
|
604
|
+
while ((idx = this.buf.indexOf("\n")) >= 0) {
|
|
605
|
+
const line = this.buf.slice(0, idx).replace(/\r$/, "");
|
|
606
|
+
this.buf = this.buf.slice(idx + 1);
|
|
607
|
+
if (this.waiter) {
|
|
608
|
+
const w = this.waiter;
|
|
609
|
+
this.waiter = null;
|
|
610
|
+
w(line);
|
|
611
|
+
} else {
|
|
612
|
+
this.lines.push(line);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
stdin.on("end", () => {
|
|
617
|
+
this.eof = true;
|
|
618
|
+
if (this.waiter) {
|
|
619
|
+
const w = this.waiter;
|
|
620
|
+
this.waiter = null;
|
|
621
|
+
w(null);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
stdin.on("error", () => {
|
|
625
|
+
this.eof = true;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
next(fallback) {
|
|
629
|
+
if (this.lines.length > 0) return Promise.resolve(this.lines.shift());
|
|
630
|
+
if (this.eof) return Promise.resolve(fallback);
|
|
631
|
+
return new Promise((resolve3) => {
|
|
632
|
+
this.waiter = (line) => resolve3(line ?? fallback);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
function parseLlmText(text) {
|
|
637
|
+
const out = {};
|
|
638
|
+
const keyMatch = text.match(/(?:api[_-]?key|key)\s*[=::]\s*([A-Za-z0-9_\-]{16,})/i) ?? text.match(/\b(sk-[A-Za-z0-9_\-]{16,})\b/);
|
|
639
|
+
if (keyMatch) out.key = keyMatch[1];
|
|
640
|
+
const urlMatch = text.match(/https?:\/\/[^\s,,;;'"()()]+/i);
|
|
641
|
+
if (urlMatch) {
|
|
642
|
+
out.baseUrl = urlMatch[0].replace(/[.。,,]+$/, "");
|
|
643
|
+
if (!/\/v\d+$/.test(out.baseUrl)) out.baseUrl = `${out.baseUrl.replace(/\/+$/, "")}/v1`;
|
|
644
|
+
}
|
|
645
|
+
const modelMatch = text.match(/model(?:\s*name)?(?:\s*\([^)]*\))?\s*[=::]\s*([\w.\-/]+)/i) ?? text.match(/(?:模型名(?:称)?|模型)\s*[=::((]\s*([\w.\-/]+)/);
|
|
646
|
+
if (modelMatch) out.model = modelMatch[1];
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
async function ask(reader, question, fallback) {
|
|
650
|
+
const suffix = fallback ? ` [${fallback}]` : "";
|
|
651
|
+
stdout.write(`${question}${suffix}: `);
|
|
652
|
+
const answer = (await reader.next(fallback ?? "")).trim();
|
|
653
|
+
return answer || fallback || "";
|
|
654
|
+
}
|
|
655
|
+
async function confirm(reader, question, defaultYes = true) {
|
|
656
|
+
stdout.write(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"}: `);
|
|
657
|
+
const answer = (await reader.next(defaultYes ? "y" : "n")).trim().toLowerCase();
|
|
658
|
+
if (!answer) return defaultYes;
|
|
659
|
+
return answer === "y" || answer === "yes";
|
|
660
|
+
}
|
|
661
|
+
async function step(name, fn) {
|
|
662
|
+
stdout.write(` installing ${name} ... `);
|
|
663
|
+
try {
|
|
664
|
+
await fn();
|
|
665
|
+
stdout.write("done\n");
|
|
666
|
+
} catch (e) {
|
|
667
|
+
stdout.write("FAILED\n");
|
|
668
|
+
throw new Error(`${name}: ${e.message}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function runInit() {
|
|
672
|
+
const reader = new LineReader();
|
|
673
|
+
try {
|
|
674
|
+
console.log("docmind init \u2014 set up your local Deep Research knowledge base\n");
|
|
675
|
+
console.log("Step 1/4 Checking dependencies...");
|
|
676
|
+
const checks = await Promise.all([checkZg(), checkRipgrep(), checkRga(), checkTorch(), checkSirchmunk()]);
|
|
677
|
+
const missing = [];
|
|
678
|
+
for (const c of checks) {
|
|
679
|
+
console.log(` ${c.ok ? "ok " : "MISS"} ${c.name}: ${c.detail}`);
|
|
680
|
+
if (!c.ok) missing.push(c);
|
|
681
|
+
}
|
|
682
|
+
if (missing.length > 0) {
|
|
683
|
+
const proceed = await confirm(reader, `Install ${missing.length} missing component(s) now?`, true);
|
|
684
|
+
if (!proceed) {
|
|
685
|
+
console.log("\nInstall manually later, then re-run `docmind init`:");
|
|
686
|
+
for (const c of missing) console.log(` - ${c.name}: ${c.fixHint}`);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const wantRg = checks.find((c) => c.name.includes("ripgrep (rg)"));
|
|
690
|
+
if (!wantRg.ok) {
|
|
691
|
+
console.log("\nripgrep is missing. Install it with your system package manager first:");
|
|
692
|
+
console.log(" apt install ripgrep / brew install ripgrep\n");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (checks.find((c) => c.name === "zvec-grep (zg)")?.ok === false) {
|
|
696
|
+
await step("zvec-grep (npm, ~1 min)", installZg);
|
|
697
|
+
}
|
|
698
|
+
if (checks.find((c) => c.name === "pytorch")?.ok === false) {
|
|
699
|
+
await step("pytorch CPU (~200MB download)", installTorchCpu);
|
|
700
|
+
}
|
|
701
|
+
if (checks.find((c) => c.name === "ripgrep-all (rga)")?.ok === false) {
|
|
702
|
+
await step("ripgrep-all (pinned release, ~6MB)", installRga);
|
|
703
|
+
}
|
|
704
|
+
if (checks.find((c) => c.name === "sirchmunk")?.ok === false) {
|
|
705
|
+
await step("sirchmunk[mcp] (pip)", installSirchmunk);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
console.log("");
|
|
709
|
+
console.log("Step 2/4 Knowledge base folder");
|
|
710
|
+
const suggestedRoot = join5(homedir5(), "docmind-docs");
|
|
711
|
+
const rootInput = await ask(reader, "Folder to use as your knowledge base", suggestedRoot);
|
|
712
|
+
const root = resolve2(rootInput);
|
|
713
|
+
await mkdir2(root, { recursive: true });
|
|
714
|
+
console.log(` knowledge base: ${root}`);
|
|
715
|
+
console.log("\nStep 3/4 LLM for research mode");
|
|
716
|
+
console.log(" Paste your key/url/model in any format, e.g.:");
|
|
717
|
+
console.log(' "key=sk-xxx Base url=https://api.example.com/v1 Model name=your-model"');
|
|
718
|
+
const llmText = await ask(reader, "LLM credentials (or press Enter to skip research mode)");
|
|
719
|
+
const parsed = parseLlmText(llmText);
|
|
720
|
+
let llm = null;
|
|
721
|
+
if (parsed.key) {
|
|
722
|
+
const key = parsed.key;
|
|
723
|
+
const baseUrl = await ask(reader, "LLM base URL", parsed.baseUrl ?? "https://api.openai.com/v1");
|
|
724
|
+
const model = await ask(reader, "LLM model name", parsed.model ?? "gpt-4o-mini");
|
|
725
|
+
llm = { key, baseUrl, model };
|
|
726
|
+
const envFile = writeSirchmunkEnv(llm);
|
|
727
|
+
console.log(` written: ${envFile} (model: ${model})`);
|
|
728
|
+
} else {
|
|
729
|
+
console.log(" skipped \u2014 research mode will degrade to fast search until configured.");
|
|
730
|
+
console.log(" Run `docmind init` again anytime to add it.");
|
|
731
|
+
}
|
|
732
|
+
console.log("\nStep 4/4 Starting the research engine");
|
|
733
|
+
const { started, state } = await serveUp("http://127.0.0.1:8584");
|
|
734
|
+
console.log(` ${started ? "started" : "already running"} sirchmunk serve \u2014 status: ${state.status}, llm: ${state.llmConfigured ? state.model : "not configured"}`);
|
|
735
|
+
if (!state.reachable) {
|
|
736
|
+
console.log(" note: sirchmunk may still be loading models in the background. Check `docmind doctor` in a minute.");
|
|
737
|
+
}
|
|
738
|
+
await saveConfig({ ...defaultConfig(root), sirchmunkUrl: "http://127.0.0.1:8584" });
|
|
739
|
+
console.log(` config written: ~/.docmind/config.json`);
|
|
740
|
+
console.log("\nNext: connect your agent \u2014");
|
|
741
|
+
console.log(" docmind install (auto-detect installed agents)");
|
|
742
|
+
console.log(" docmind install --target opencode / claude-code / codex");
|
|
743
|
+
console.log(" docmind doctor (verify everything)");
|
|
744
|
+
const doInstall = await confirm(reader, "\nRun agent integration now?", true);
|
|
745
|
+
if (doInstall) {
|
|
746
|
+
const target = await ask(reader, "Agent (opencode / claude-code / codex / generic)", "auto");
|
|
747
|
+
const { runInstall: runInstall2 } = await Promise.resolve().then(() => (init_installer(), installer_exports));
|
|
748
|
+
await runInstall2(target === "auto" ? void 0 : target, root);
|
|
749
|
+
}
|
|
750
|
+
} finally {
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function ensureIndexed(root) {
|
|
754
|
+
if (!existsSync4(join5(root, ".zvec-grep"))) {
|
|
755
|
+
console.log(`Building initial index for ${root} ...`);
|
|
756
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
757
|
+
await new Promise((res, rej) => {
|
|
758
|
+
execFile3("zg", ["index", "--hidden"], { cwd: root, timeout: 6e5 }, (err) => err ? rej(err) : res());
|
|
759
|
+
});
|
|
760
|
+
console.log("Index ready.");
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/doctor.ts
|
|
765
|
+
init_config();
|
|
766
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
767
|
+
import { join as join6 } from "node:path";
|
|
768
|
+
async function zgIndexStatus(root) {
|
|
769
|
+
if (!existsSync5(join6(root, ".zvec-grep"))) {
|
|
770
|
+
return { name: "knowledge index", ok: false, detail: `no index in ${root} (built on first write/search attempt)` };
|
|
771
|
+
}
|
|
772
|
+
return { name: "knowledge index", ok: true, detail: `indexed (${join6(root, ".zvec-grep")})` };
|
|
773
|
+
}
|
|
774
|
+
async function rootCheck() {
|
|
775
|
+
const config = await loadConfig();
|
|
776
|
+
const root = process.env.DOCMIND_ROOT ?? config.defaultRoot;
|
|
777
|
+
if (!root) {
|
|
778
|
+
return {
|
|
779
|
+
check: { name: "knowledge root", ok: false, detail: "not configured (run `docmind init`)" },
|
|
780
|
+
root: null
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const exists = existsSync5(root);
|
|
784
|
+
return {
|
|
785
|
+
check: { name: "knowledge root", ok: exists, detail: exists ? root : `${root} (missing)` },
|
|
786
|
+
root
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async function runDoctor() {
|
|
790
|
+
const lines = [];
|
|
791
|
+
let allOk = true;
|
|
792
|
+
const { check: rootCheckResult, root } = await rootCheck();
|
|
793
|
+
const checks = [
|
|
794
|
+
rootCheckResult,
|
|
795
|
+
await checkZg(),
|
|
796
|
+
await checkRipgrep(),
|
|
797
|
+
await checkRga(),
|
|
798
|
+
await checkTorch().then((c) => ({ ...c, name: "pytorch (optional, speeds first research)" })),
|
|
799
|
+
await checkSirchmunk(),
|
|
800
|
+
await checkSirchmunkEnv()
|
|
801
|
+
];
|
|
802
|
+
if (root) checks.push(await zgIndexStatus(root));
|
|
803
|
+
const config = await loadConfig();
|
|
804
|
+
const state = await serveState(config.sirchmunkUrl);
|
|
805
|
+
checks.push({
|
|
806
|
+
name: "sirchmunk serve",
|
|
807
|
+
ok: state.reachable,
|
|
808
|
+
detail: state.reachable ? `status: ${state.status}, llm: ${state.llmConfigured ? state.model : "NOT configured"}, url: ${config.sirchmunkUrl}${state.managed ? `, managed pid ${state.pid}` : " (external process)"}` : `unreachable at ${config.sirchmunkUrl} \u2014 run \`docmind up\``
|
|
809
|
+
});
|
|
810
|
+
if (state.reachable && !state.llmConfigured) {
|
|
811
|
+
checks.push({
|
|
812
|
+
name: "research mode",
|
|
813
|
+
ok: false,
|
|
814
|
+
detail: "no LLM key \u2014 research will degrade to fast search. Re-run `docmind init`."
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
for (const c of checks) {
|
|
818
|
+
if (!c.ok) allOk = false;
|
|
819
|
+
lines.push(`${c.ok ? "[ok]" : "[!!]"} ${c.name}: ${c.detail}`);
|
|
820
|
+
if (!c.ok && c.fixHint) lines.push(` fix: ${c.fixHint}`);
|
|
821
|
+
}
|
|
822
|
+
lines.push("");
|
|
823
|
+
lines.push(
|
|
824
|
+
allOk ? "All systems operational. Your agent can use docmind's search / research / remember tools." : "Some components need attention (see [!!] items above)."
|
|
825
|
+
);
|
|
826
|
+
return { ok: allOk, lines };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/cli.ts
|
|
830
|
+
init_installer();
|
|
831
|
+
init_config();
|
|
832
|
+
init_zg();
|
|
833
|
+
var HELP = `docmind \u2014 local Deep Research knowledge base for coding agents
|
|
834
|
+
|
|
835
|
+
Usage:
|
|
836
|
+
docmind init Interactive setup: deps, LLM key, service, agent config
|
|
837
|
+
docmind up Start the sirchmunk research engine in the background
|
|
838
|
+
docmind down Stop the docmind-managed sirchmunk serve
|
|
839
|
+
docmind status Show whether the research engine is reachable
|
|
840
|
+
docmind doctor Full health check of the whole chain
|
|
841
|
+
docmind install [--target T] Register the MCP server in an agent
|
|
842
|
+
targets: opencode, claude-code, codex, generic
|
|
843
|
+
docmind search <query> [--root D] Quick terminal search (free, no LLM)
|
|
844
|
+
docmind index [--root D] (Re)build the search index for a knowledge folder
|
|
845
|
+
docmind-mcp MCP stdio server (this is what agents run)
|
|
846
|
+
|
|
847
|
+
Config lives in ~/.docmind/config.json; LLM key in ~/.sirchmunk/.env.
|
|
848
|
+
`;
|
|
849
|
+
function rootFlag(args) {
|
|
850
|
+
const i = args.indexOf("--root");
|
|
851
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
852
|
+
}
|
|
853
|
+
async function main() {
|
|
854
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
855
|
+
switch (cmd) {
|
|
856
|
+
case "init":
|
|
857
|
+
await runInit();
|
|
858
|
+
break;
|
|
859
|
+
case "up": {
|
|
860
|
+
const config = await loadConfig();
|
|
861
|
+
const { started, state } = await serveUp(config.sirchmunkUrl);
|
|
862
|
+
console.log(
|
|
863
|
+
started ? `sirchmunk serve started (pid ${state.pid}). status: ${state.status}, llm: ${state.llmConfigured ? state.model : "not configured"}` : `already running \u2014 status: ${state.status}, llm: ${state.llmConfigured ? state.model : "not configured"}`
|
|
864
|
+
);
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
case "down": {
|
|
868
|
+
const config = await loadConfig();
|
|
869
|
+
const { stopped, detail } = await serveDown(config.sirchmunkUrl);
|
|
870
|
+
console.log(stopped ? detail : `note: ${detail}`);
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
case "status": {
|
|
874
|
+
const config = await loadConfig();
|
|
875
|
+
const state = await serveState(config.sirchmunkUrl);
|
|
876
|
+
console.log(
|
|
877
|
+
state.reachable ? `sirchmunk: ${state.status} | llm: ${state.llmConfigured ? state.model : "not configured"} | ${config.sirchmunkUrl}${state.managed ? ` | managed pid ${state.pid}` : ""}` : `sirchmunk unreachable at ${config.sirchmunkUrl} \u2014 run \`docmind up\``
|
|
878
|
+
);
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case "doctor": {
|
|
882
|
+
const report = await runDoctor();
|
|
883
|
+
console.log(report.lines.join("\n"));
|
|
884
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
case "install": {
|
|
888
|
+
const targetIdx = args.indexOf("--target");
|
|
889
|
+
const target = targetIdx >= 0 ? args[targetIdx + 1] : void 0;
|
|
890
|
+
const results = await runInstall(target);
|
|
891
|
+
for (const r of results) {
|
|
892
|
+
console.log(`[${r.ok ? "ok" : "!!"}] ${r.target}: ${r.detail}`);
|
|
893
|
+
if (r.manualHint) console.log(` manual setup:
|
|
894
|
+
${r.manualHint}`);
|
|
895
|
+
}
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
case "search": {
|
|
899
|
+
const query = args.find((a) => !a.startsWith("--"));
|
|
900
|
+
if (!query) {
|
|
901
|
+
console.error("Usage: docmind search <query> [--root D]");
|
|
902
|
+
process.exitCode = 1;
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
const config = await loadConfig();
|
|
906
|
+
const root = resolveRoot(config, rootFlag(args));
|
|
907
|
+
await ensureIndexed(root);
|
|
908
|
+
const result = await zgSearch(root, query);
|
|
909
|
+
if (result.hits.length === 0) {
|
|
910
|
+
console.log(`No hits for "${query}".`);
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
for (const hit of result.hits) {
|
|
914
|
+
console.log(`#${hit.rank} ${hit.file}:${hit.lineStart}-${hit.lineEnd} [${hit.matchedBy}]${hit.heading ? ` \u2014 ${hit.heading}` : ""}`);
|
|
915
|
+
for (const s of hit.snippets.slice(0, 2)) console.log(` L${s.line}: ${s.text.trim().slice(0, 160)}`);
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
case "index": {
|
|
920
|
+
const config = await loadConfig();
|
|
921
|
+
const root = resolveRoot(config, rootFlag(args));
|
|
922
|
+
await ensureIndexed(root);
|
|
923
|
+
const { zgIndex: zgIndex2 } = await Promise.resolve().then(() => (init_zg(), zg_exports));
|
|
924
|
+
console.log(await zgIndex2(root));
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
case "help":
|
|
928
|
+
case "--help":
|
|
929
|
+
case "-h":
|
|
930
|
+
case void 0:
|
|
931
|
+
console.log(HELP);
|
|
932
|
+
break;
|
|
933
|
+
default:
|
|
934
|
+
console.error(`Unknown command: ${cmd}
|
|
935
|
+
`);
|
|
936
|
+
console.log(HELP);
|
|
937
|
+
process.exitCode = 1;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
main().catch((e) => {
|
|
941
|
+
console.error(`docmind: ${e.message}`);
|
|
942
|
+
process.exitCode = 1;
|
|
943
|
+
});
|