fathom-mcp 0.6.3 → 2.0.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/src/cli.js DELETED
@@ -1,937 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * fathom-mcp CLI
5
- *
6
- * Usage:
7
- * npx fathom-mcp — Start MCP server (stdio, for .mcp.json)
8
- * npx fathom-mcp init — Interactive setup wizard
9
- * npx fathom-mcp status — Check server connection + workspace status
10
- * npx fathom-mcp update — Update hook scripts + version file
11
- * npx fathom-mcp list — List workspaces + status (from server)
12
- */
13
-
14
- import fs from "fs";
15
- import path from "path";
16
- import readline from "readline";
17
- import { execFileSync } from "child_process";
18
- import { fileURLToPath } from "url";
19
-
20
- import { findConfigFile, resolveConfig, writeConfig } from "./config.js";
21
- import { createClient } from "./server-client.js";
22
-
23
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
- const SCRIPTS_DIR = path.join(__dirname, "..", "scripts");
25
-
26
- // --- Helpers -----------------------------------------------------------------
27
-
28
- function ask(rl, question, defaultVal = "") {
29
- const suffix = defaultVal ? ` (${defaultVal})` : "";
30
- return new Promise((resolve) => {
31
- rl.question(`${question}${suffix}: `, (answer) => {
32
- resolve(answer.trim() || defaultVal);
33
- });
34
- });
35
- }
36
-
37
- function askYesNo(rl, question, defaultYes = true) {
38
- const hint = defaultYes ? "Y/n" : "y/N";
39
- return new Promise((resolve) => {
40
- rl.question(`${question} [${hint}]: `, (answer) => {
41
- const a = answer.trim().toLowerCase();
42
- if (!a) return resolve(defaultYes);
43
- resolve(a === "y" || a === "yes");
44
- });
45
- });
46
- }
47
-
48
- /**
49
- * Deep merge obj2 into obj1 (mutates obj1). Arrays are replaced, not merged.
50
- */
51
- function deepMerge(obj1, obj2) {
52
- for (const key of Object.keys(obj2)) {
53
- if (
54
- obj1[key] &&
55
- typeof obj1[key] === "object" &&
56
- !Array.isArray(obj1[key]) &&
57
- typeof obj2[key] === "object" &&
58
- !Array.isArray(obj2[key])
59
- ) {
60
- deepMerge(obj1[key], obj2[key]);
61
- } else {
62
- obj1[key] = obj2[key];
63
- }
64
- }
65
- return obj1;
66
- }
67
-
68
- function readJsonFile(filePath) {
69
- try {
70
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
71
- } catch {
72
- return null;
73
- }
74
- }
75
-
76
- function writeJsonFile(filePath, data) {
77
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
78
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
79
- }
80
-
81
- function appendToGitignore(dir, patterns) {
82
- const gitignorePath = path.join(dir, ".gitignore");
83
- let existing = "";
84
- try {
85
- existing = fs.readFileSync(gitignorePath, "utf-8");
86
- } catch { /* file doesn't exist */ }
87
-
88
- const missing = patterns.filter((p) => !existing.includes(p));
89
- if (missing.length > 0) {
90
- const suffix = existing.endsWith("\n") || !existing ? "" : "\n";
91
- fs.appendFileSync(gitignorePath, suffix + missing.join("\n") + "\n");
92
- }
93
- }
94
-
95
- /**
96
- * Idempotently register a hook in a settings object.
97
- * Works for both Claude Code and Gemini CLI (same JSON structure).
98
- * Returns true if a new hook was added, false if already present.
99
- */
100
- /**
101
- * MCP tool permissions upserted into .claude/settings.local.json during init.
102
- * Only fathom-vault and memento — the tools init itself provides.
103
- */
104
- const INIT_PERMISSIONS = [
105
- "mcp__fathom-vault__*",
106
- "mcp__memento__*",
107
- ];
108
-
109
- /**
110
- * Upsert permissions into settings.permissions.allow.
111
- * Returns true if any new entries were added.
112
- */
113
- function ensurePermissions(settings, perms = INIT_PERMISSIONS) {
114
- if (!settings.permissions) settings.permissions = {};
115
- if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
116
- const existing = new Set(settings.permissions.allow);
117
- let changed = false;
118
- for (const perm of perms) {
119
- if (!existing.has(perm)) {
120
- settings.permissions.allow.push(perm);
121
- changed = true;
122
- }
123
- }
124
- return changed;
125
- }
126
-
127
- function ensureHook(settings, eventName, command, timeout) {
128
- const existing = settings.hooks?.[eventName] || [];
129
- const alreadyRegistered = existing.some((entry) =>
130
- entry.hooks?.some((h) => h.command === command)
131
- );
132
- if (alreadyRegistered) return false;
133
- if (!settings.hooks) settings.hooks = {};
134
- settings.hooks[eventName] = [
135
- ...existing,
136
- { hooks: [{ type: "command", command, timeout }] },
137
- ];
138
- return true;
139
- }
140
-
141
- function copyScripts(targetDir) {
142
- fs.mkdirSync(targetDir, { recursive: true });
143
- try {
144
- const files = fs.readdirSync(SCRIPTS_DIR);
145
- for (const file of files) {
146
- const src = path.join(SCRIPTS_DIR, file);
147
- const dest = path.join(targetDir, file);
148
- fs.copyFileSync(src, dest);
149
- fs.chmodSync(dest, 0o755);
150
- }
151
- return files;
152
- } catch {
153
- return [];
154
- }
155
- }
156
-
157
- // ---------------------------------------------------------------------------
158
- // CLAUDE.md integration
159
- // ---------------------------------------------------------------------------
160
-
161
- const FATHOM_MD_MARKER_START = "<!-- BEGIN fathom-mcp -->";
162
- const FATHOM_MD_MARKER_END = "<!-- END fathom-mcp -->";
163
-
164
- function appendToClaudeMd(cwd, blob) {
165
- const mdPath = path.join(cwd, "CLAUDE.md");
166
- let content = "";
167
- try { content = fs.readFileSync(mdPath, "utf-8"); } catch { /* new file */ }
168
-
169
- const wrappedBlob = `${FATHOM_MD_MARKER_START}\n${blob}\n${FATHOM_MD_MARKER_END}`;
170
-
171
- const markerRe = new RegExp(
172
- `${FATHOM_MD_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${FATHOM_MD_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
173
- );
174
-
175
- if (markerRe.test(content)) {
176
- content = content.replace(markerRe, wrappedBlob);
177
- } else {
178
- content = content.trimEnd() + "\n\n" + wrappedBlob + "\n";
179
- }
180
-
181
- fs.writeFileSync(mdPath, content, "utf-8");
182
- return mdPath;
183
- }
184
-
185
- /**
186
- * Check if fathom-server is available on PATH.
187
- * Returns "installed" or "not-found".
188
- */
189
- function detectFathomServer() {
190
- try {
191
- execFileSync("which", ["fathom-server"], { stdio: "pipe" });
192
- return "installed";
193
- } catch {
194
- return "not-found";
195
- }
196
- }
197
-
198
- // --- CLI flag parsing --------------------------------------------------------
199
-
200
- function parseFlags(argv) {
201
- const flags = {
202
- nonInteractive: false,
203
- apiKey: null,
204
- server: null,
205
- workspace: null,
206
- agent: null,
207
- vaultMode: null,
208
- };
209
- for (let i = 0; i < argv.length; i++) {
210
- if (argv[i] === "-y" || argv[i] === "--yes") {
211
- flags.nonInteractive = true;
212
- } else if (argv[i] === "--api-key" && argv[i + 1]) {
213
- flags.apiKey = argv[i + 1];
214
- i++;
215
- } else if (argv[i] === "--server" && argv[i + 1]) {
216
- flags.server = argv[i + 1];
217
- i++;
218
- } else if (argv[i] === "--workspace" && argv[i + 1]) {
219
- flags.workspace = argv[i + 1];
220
- i++;
221
- } else if (argv[i] === "--agent" && argv[i + 1]) {
222
- flags.agent = argv[i + 1];
223
- i++;
224
- } else if (argv[i] === "--vault-mode" && argv[i + 1]) {
225
- flags.vaultMode = argv[i + 1];
226
- i++;
227
- }
228
- }
229
- // Check environment variables as fallback
230
- if (!flags.apiKey && process.env.FATHOM_API_KEY) {
231
- flags.apiKey = process.env.FATHOM_API_KEY;
232
- }
233
- return flags;
234
- }
235
-
236
- // --- Agent registry ----------------------------------------------------------
237
-
238
- const MCP_SERVER_ENTRY = {
239
- command: "npx",
240
- args: ["-y", "fathom-mcp"],
241
- };
242
-
243
- /**
244
- * Per-agent config writers. Each writes the appropriate MCP config file
245
- * for that agent, merging with existing config if present.
246
- */
247
-
248
- function writeMcpJson(cwd) {
249
- const filePath = path.join(cwd, ".mcp.json");
250
- const existing = readJsonFile(filePath) || {};
251
- deepMerge(existing, { mcpServers: { "fathom-vault": MCP_SERVER_ENTRY } });
252
- writeJsonFile(filePath, existing);
253
- return ".mcp.json";
254
- }
255
-
256
- function writeGeminiJson(cwd) {
257
- const dir = path.join(cwd, ".gemini");
258
- fs.mkdirSync(dir, { recursive: true });
259
- const filePath = path.join(dir, "settings.json");
260
- const existing = readJsonFile(filePath) || {};
261
- deepMerge(existing, { mcpServers: { "fathom-vault": MCP_SERVER_ENTRY } });
262
- writeJsonFile(filePath, existing);
263
- return ".gemini/settings.json";
264
- }
265
-
266
- const AGENTS = {
267
- "claude-code": {
268
- name: "Claude Code",
269
- detect: (cwd) => fs.existsSync(path.join(cwd, ".claude")),
270
- configWriter: writeMcpJson,
271
- hasHooks: true,
272
- nextSteps: 'Add to CLAUDE.md: `ToolSearch query="+fathom" max_results=20`',
273
- },
274
- "gemini": {
275
- name: "Gemini CLI",
276
- detect: (cwd) => fs.existsSync(path.join(cwd, ".gemini")),
277
- configWriter: writeGeminiJson,
278
- hasHooks: true,
279
- nextSteps: "Run `gemini` in this directory — fathom tools load automatically.",
280
- },
281
- "manual": {
282
- name: "I'll set up my agent myself",
283
- detect: () => false,
284
- configWriter: () => "(skipped — manual setup)",
285
- hasHooks: false,
286
- nextSteps: "Point your agent's MCP config at: npx -y fathom-mcp",
287
- },
288
- };
289
-
290
- // Exported for testing
291
- export { AGENTS, writeMcpJson, writeGeminiJson };
292
-
293
- // --- Init wizard -------------------------------------------------------------
294
-
295
- async function runInit(flags = {}) {
296
- const {
297
- nonInteractive = false,
298
- apiKey: flagApiKey = null,
299
- server: flagServer = null,
300
- workspace: flagWorkspace = null,
301
- agent: flagAgent = null,
302
- vaultMode: flagVaultMode = null,
303
- } = flags;
304
- const cwd = process.cwd();
305
-
306
- const rl = nonInteractive
307
- ? null
308
- : readline.createInterface({ input: process.stdin, output: process.stdout });
309
-
310
- console.log(`
311
- ▐▘ ▗ ▌
312
- ▜▘▀▌▜▘▛▌▛▌▛▛▌▄▖▛▛▌▛▘▛▌
313
- ▐ █▌▐▖▌▌▙▌▌▌▌ ▌▌▌▙▖▙▌
314
-
315
-
316
- hifathom.com · fathom@myrakrusemark.com
317
- `);
318
-
319
- // Non-interactive: require API key
320
- if (nonInteractive && !flagApiKey) {
321
- console.error(" Error: --api-key required in non-interactive mode.");
322
- process.exit(1);
323
- }
324
-
325
- // Check for existing config in *this* directory only (don't walk up —
326
- // a parent's .fathom.json belongs to a different workspace)
327
- const localConfigPath = path.join(cwd, ".fathom.json");
328
- if (fs.existsSync(localConfigPath)) {
329
- if (nonInteractive) {
330
- console.log(` Overwriting existing config at: ${localConfigPath}`);
331
- } else {
332
- console.log(` Found existing config at: ${localConfigPath}`);
333
- const proceed = await askYesNo(rl, " Overwrite?", false);
334
- if (!proceed) {
335
- console.log(" Aborted.");
336
- rl.close();
337
- process.exit(0);
338
- }
339
- }
340
- }
341
-
342
- // 1. Workspace name
343
- const defaultName = path.basename(cwd);
344
- const workspace = nonInteractive
345
- ? (flagWorkspace || defaultName)
346
- : await ask(rl, " Workspace name", defaultName);
347
-
348
- // 2. Agent selection — auto-detect and let user choose
349
- const agentKeys = Object.keys(AGENTS);
350
- const detected = agentKeys.filter((key) => AGENTS[key].detect(cwd));
351
-
352
- let selectedAgents;
353
- if (nonInteractive) {
354
- if (flagAgent) {
355
- // Validate --agent value
356
- if (!AGENTS[flagAgent]) {
357
- const valid = Object.keys(AGENTS).join(", ");
358
- console.error(` Error: unknown agent "${flagAgent}". Valid agents: ${valid}`);
359
- process.exit(1);
360
- }
361
- selectedAgents = [flagAgent];
362
- console.log(` Agent: ${AGENTS[flagAgent].name} (--agent flag)`);
363
- } else {
364
- // Auto-detect: use first detected agent, or default to claude-code
365
- selectedAgents = detected.length > 0 ? [detected[0]] : ["claude-code"];
366
- console.log(` Agent: ${AGENTS[selectedAgents[0]].name} (auto-detected)`);
367
- }
368
- } else {
369
- console.log("\n Detected agents:");
370
- for (const key of agentKeys) {
371
- const agent = AGENTS[key];
372
- const isDetected = detected.includes(key);
373
- const mark = isDetected ? "✓" : " ";
374
- const markers = { "claude-code": ".claude/", "gemini": ".gemini/" };
375
- const hint = isDetected ? ` (${markers[key] || key} found)` : "";
376
- console.log(` ${mark} ${agent.name}${hint}`);
377
- }
378
-
379
- console.log("\n Configure for which agents?");
380
- agentKeys.forEach((key, i) => {
381
- const agent = AGENTS[key];
382
- const mark = detected.includes(key) ? " ✓" : "";
383
- console.log(` ${i + 1}. ${agent.name}${mark}`);
384
- });
385
-
386
- const defaultSelection = detected.length > 0
387
- ? detected.map((key) => agentKeys.indexOf(key) + 1).join(",")
388
- : "1";
389
- const selectionStr = await ask(rl, "\n Enter numbers, comma-separated", defaultSelection);
390
-
391
- const selectedIndices = selectionStr
392
- .split(",")
393
- .map((s) => parseInt(s.trim(), 10))
394
- .filter((n) => n >= 1 && n <= agentKeys.length);
395
- selectedAgents = [...new Set(selectedIndices.map((i) => agentKeys[i - 1]))];
396
-
397
- if (selectedAgents.length === 0) {
398
- console.log(" No agents selected. Defaulting to Claude Code.");
399
- selectedAgents.push("claude-code");
400
- }
401
- }
402
-
403
- // 5. Server URL
404
- let serverUrl = nonInteractive
405
- ? (flagServer || "http://localhost:4243")
406
- : await ask(rl, "\n Fathom server URL", "http://localhost:4243");
407
-
408
- // 6. API key
409
- let apiKey = flagApiKey || (nonInteractive ? "" : await ask(rl, " API key (from dashboard or server first-run output)", ""));
410
-
411
- // 7. Server probe — check reachability early
412
- let regClient = createClient({ server: serverUrl, apiKey, workspace });
413
- let serverReachable = serverUrl ? await regClient.healthCheck() : false;
414
- const serverOnPath = detectFathomServer();
415
-
416
- // Retry loop for server connectivity (interactive mode)
417
- while (!serverReachable && !nonInteractive) {
418
- console.log(`\n ⚠ Fathom server not reachable at ${serverUrl}\n`);
419
- if (serverOnPath === "installed") {
420
- console.log(" Start it: fathom-server");
421
- } else {
422
- console.log(" Install & start it:");
423
- console.log(" pip install fathom-server && fathom-server");
424
- console.log(" # or: docker run -p 4243:4243 ghcr.io/myra/fathom-server");
425
- }
426
- console.log("\n Without the server, only \"local\" and \"none\" vault modes are available.");
427
- const retry = await askYesNo(rl, "\n Try a different server URL or API key?", true);
428
- if (!retry) break;
429
- serverUrl = await ask(rl, "\n Fathom server URL", serverUrl);
430
- apiKey = await ask(rl, " API key", apiKey);
431
- regClient = createClient({ server: serverUrl, apiKey, workspace });
432
- serverReachable = await regClient.healthCheck();
433
- if (serverReachable) {
434
- console.log(" ✓ Server connected!");
435
- }
436
- }
437
-
438
- // 8. Vault mode selection
439
- let vaultMode;
440
- const validVaultModes = ["synced", "local", "none"];
441
- if (nonInteractive) {
442
- if (!serverReachable && flagServer) {
443
- console.error(`\n Error: Server at ${serverUrl} is not reachable.`);
444
- console.error(" Fix the URL or start the server, then re-run init.");
445
- process.exit(1);
446
- }
447
- if (flagVaultMode) {
448
- if (!validVaultModes.includes(flagVaultMode)) {
449
- console.error(` Error: unknown vault mode "${flagVaultMode}". Valid modes: ${validVaultModes.join(", ")}`);
450
- process.exit(1);
451
- }
452
- if (flagVaultMode === "synced" && !serverReachable) {
453
- console.error(` Error: vault mode "${flagVaultMode}" requires a reachable server.`);
454
- process.exit(1);
455
- }
456
- vaultMode = flagVaultMode;
457
- console.log(` Vault mode: ${vaultMode} (--vault-mode flag)`);
458
- } else {
459
- vaultMode = serverReachable ? "synced" : "local";
460
- console.log(` Vault mode: ${vaultMode} (auto-selected)`);
461
- }
462
- } else {
463
- if (serverReachable) {
464
- console.log("\n Vault mode:");
465
- console.log(" 1. Synced (default) — local vault + server indexing");
466
- console.log(" 2. Local — vault on disk only, not visible to server");
467
- console.log(" 3. None — no vault, coordination features only");
468
- const modeChoice = await ask(rl, "\n Select mode", "1");
469
- const modeMap = { "1": "synced", "2": "local", "3": "none" };
470
- vaultMode = modeMap[modeChoice] || "synced";
471
- } else {
472
- console.log("\n Vault mode (server not available — hosted/synced require server):");
473
- console.log(" 1. Local (default) — vault on disk only");
474
- console.log(" 2. None — no vault, coordination features only");
475
- const modeChoice = await ask(rl, "\n Select mode", "1");
476
- vaultMode = modeChoice === "2" ? "none" : "local";
477
- }
478
- }
479
-
480
- // Only ask about vault subdirectory if mode needs a local dir
481
- const needsLocalVault = vaultMode === "synced" || vaultMode === "local";
482
- const vault = needsLocalVault
483
- ? (nonInteractive ? "vault" : await ask(rl, " Vault subdirectory", "vault"))
484
- : "vault";
485
-
486
- // 9. Hooks — ask if any hook-supporting agent is selected (Claude Code, Claude SDK, Gemini CLI)
487
- const hasClaude = selectedAgents.includes("claude-code");
488
- const hasGemini = selectedAgents.includes("gemini");
489
- const hasHookAgent = hasClaude || hasGemini;
490
- let enableRecallHook = false;
491
- let enablePrecompactHook = false;
492
- if (hasHookAgent) {
493
- if (nonInteractive) {
494
- enableRecallHook = true;
495
- enablePrecompactHook = true;
496
- } else {
497
- console.log();
498
- enableRecallHook = await askYesNo(rl, " Enable vault recall on every message?", true);
499
- enablePrecompactHook = await askYesNo(rl, " Enable PreCompact vault snapshot hook?", true);
500
- }
501
- }
502
-
503
- rl?.close();
504
-
505
- // --- Write files ---
506
-
507
- console.log("\n Creating files...\n");
508
-
509
- // .fathom.json
510
- const configData = {
511
- workspace,
512
- vaultMode,
513
- vault,
514
- server: serverUrl,
515
- apiKey,
516
- agents: selectedAgents,
517
- hooks: {
518
- "vault-recall": { enabled: enableRecallHook },
519
- "precompact-snapshot": { enabled: enablePrecompactHook },
520
- },
521
- };
522
- const configPath = writeConfig(cwd, configData);
523
- console.log(` ✓ ${path.relative(cwd, configPath)}`);
524
-
525
- // ~/.config/fathom-mcp/scripts/ (central, shared across all workspaces)
526
- const centralScriptsDir = path.join(process.env.HOME, ".config", "fathom-mcp", "scripts");
527
- const copiedScripts = copyScripts(centralScriptsDir);
528
- if (copiedScripts.length > 0) {
529
- console.log(` ✓ ~/.config/fathom-mcp/scripts/ (${copiedScripts.length} scripts)`);
530
- }
531
-
532
- // vault/ directory — only create for synced/local modes
533
- if (needsLocalVault) {
534
- const vaultDir = path.join(cwd, vault);
535
- if (!fs.existsSync(vaultDir)) {
536
- fs.mkdirSync(vaultDir, { recursive: true });
537
- console.log(` ✓ ${vault}/ (created)`);
538
- } else {
539
- console.log(` · ${vault}/ (already exists)`);
540
- }
541
- }
542
-
543
- // fathom-agents.md — boilerplate agent instructions (central)
544
- const agentMdSrc = path.join(__dirname, "..", "fathom-agents.md");
545
- const agentMdDest = path.join(process.env.HOME, ".config", "fathom-mcp", "fathom-agents.md");
546
- try {
547
- let template = fs.readFileSync(agentMdSrc, "utf-8");
548
- template = template
549
- .replace(/\{\{WORKSPACE_NAME\}\}/g, workspace)
550
- .replace(/\{\{VAULT_DIR\}\}/g, vault)
551
- .replace(/\{\{DESCRIPTION\}\}/g, `${workspace} workspace`);
552
- fs.mkdirSync(path.dirname(agentMdDest), { recursive: true });
553
- fs.writeFileSync(agentMdDest, template);
554
- console.log(" ✓ ~/.config/fathom-mcp/fathom-agents.md");
555
- } catch { /* template not found — skip silently */ }
556
-
557
- // Per-agent config files
558
- for (const agentKey of selectedAgents) {
559
- const agent = AGENTS[agentKey];
560
- const result = agent.configWriter(cwd);
561
- console.log(` ✓ ${result}`);
562
- }
563
-
564
- // Hook scripts (central location, shared across agents)
565
- const sessionStartCmd = "bash ~/.config/fathom-mcp/scripts/fathom-sessionstart.sh";
566
- const recallCmd = "bash ~/.config/fathom-mcp/scripts/fathom-recall.sh";
567
- const precompactCmd = "bash ~/.config/fathom-mcp/scripts/fathom-precompact.sh";
568
-
569
- // Claude Code hooks
570
- const lintCmd = "node ~/.config/fathom-mcp/scripts/vault-frontmatter-lint.js";
571
- if (hasClaude) {
572
- const settingsPath = path.join(cwd, ".claude", "settings.local.json");
573
- const settings = readJsonFile(settingsPath) || {};
574
- let changed = ensurePermissions(settings);
575
- changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
576
- if (enableRecallHook) changed = ensureHook(settings, "UserPromptSubmit", recallCmd, 10000) || changed;
577
- if (enablePrecompactHook) changed = ensureHook(settings, "PreCompact", precompactCmd, 30000) || changed;
578
- changed = ensureHook(settings, "PostToolUse", lintCmd, 5000) || changed;
579
- if (changed) {
580
- writeJsonFile(settingsPath, settings);
581
- console.log(" ✓ .claude/settings.local.json (permissions + hooks)");
582
- }
583
- }
584
-
585
- // Gemini CLI hooks
586
- if (hasGemini) {
587
- const settingsPath = path.join(cwd, ".gemini", "settings.json");
588
- const settings = readJsonFile(settingsPath) || {};
589
- let changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000);
590
- if (enableRecallHook) changed = ensureHook(settings, "BeforeAgent", recallCmd, 10000) || changed;
591
- if (enablePrecompactHook) changed = ensureHook(settings, "PreCompress", precompactCmd, 30000) || changed;
592
- if (changed) {
593
- writeJsonFile(settingsPath, settings);
594
- console.log(" ✓ .gemini/settings.json (hooks)");
595
- }
596
- }
597
-
598
- // .gitignore
599
- appendToGitignore(cwd, [".fathom.json"]);
600
- console.log(" ✓ .gitignore");
601
-
602
- // Register with server — use provision endpoint to get API key + create settings.json entry
603
- if (serverReachable) {
604
- const provResult = await regClient.provisionWorkspace(workspace, cwd, {
605
- vault,
606
- agents: selectedAgents,
607
- type: vaultMode,
608
- execution: "local",
609
- });
610
- if (provResult.ok) {
611
- console.log(` ✓ Registered workspace "${workspace}" with server`);
612
- // Use the server-provisioned API key (overwrites any manually entered one)
613
- if (provResult.api_key) {
614
- apiKey = provResult.api_key;
615
- configData.apiKey = apiKey;
616
- writeConfig(cwd, configData);
617
- console.log(` ✓ API key provisioned by server`);
618
- }
619
- } else if (provResult.error) {
620
- console.log(` · Server registration: ${provResult.error}`);
621
- // Fall back to old registration endpoint (server may be older version)
622
- const regResult = await regClient.registerWorkspace(workspace, cwd, {
623
- vault,
624
- agents: selectedAgents,
625
- type: vaultMode,
626
- });
627
- if (regResult.ok) {
628
- console.log(` ✓ Registered workspace "${workspace}" with server (legacy)`);
629
- } else if (regResult.error) {
630
- console.log(` · Server registration: ${regResult.error}`);
631
- }
632
- }
633
- }
634
-
635
- // Context-aware next steps
636
- console.log(`\n Done! Fathom MCP is configured for workspace "${workspace}".`);
637
- console.log(` Vault mode: ${vaultMode}`);
638
- console.log("\n Next steps:");
639
-
640
- if (!serverReachable) {
641
- if (serverOnPath === "installed") {
642
- console.log(" 1. Start the server: fathom-server");
643
- } else {
644
- console.log(" 1. Install & start the server: pip install fathom-server && fathom-server");
645
- }
646
- }
647
-
648
- const stepNum = serverReachable ? 1 : 2;
649
- switch (vaultMode) {
650
- case "synced":
651
- console.log(` ${stepNum}. Local vault indexed by server. Files in ./${vault}/ are the source of truth.`);
652
- break;
653
- case "local":
654
- console.log(` ${stepNum}. Local vault only. Server can't search or peek into it.`);
655
- break;
656
- case "none":
657
- console.log(` ${stepNum}. No vault configured. Rooms, messaging, and search still work.`);
658
- break;
659
- }
660
- for (const agentKey of selectedAgents) {
661
- const agent = AGENTS[agentKey];
662
- console.log(` · ${agent.name}: ${agent.nextSteps}`);
663
- }
664
-
665
- // Show non-interactive equivalent
666
- if (!nonInteractive) {
667
- const parts = ["npx fathom-mcp init -y"];
668
- if (apiKey) parts.push(`--api-key "${apiKey}"`);
669
- if (serverUrl && serverUrl !== "http://localhost:4243") parts.push(`--server ${serverUrl}`);
670
- parts.push(`--workspace ${workspace}`);
671
- parts.push(`--vault-mode ${vaultMode}`);
672
- parts.push(`--agent ${selectedAgents[0]}`);
673
- console.log(`\n Non-interactive equivalent:\n ${parts.join(" ")}\n`);
674
- }
675
-
676
- // Append agent instructions to CLAUDE.md
677
- let instructionsBlob = "";
678
- try {
679
- instructionsBlob = fs.readFileSync(agentMdDest, "utf-8");
680
- } catch { /* template wasn't created — skip */ }
681
-
682
- if (instructionsBlob) {
683
- if (nonInteractive) {
684
- const mdPath = appendToClaudeMd(cwd, instructionsBlob);
685
- console.log(`\n ✓ Instructions written to ${path.relative(cwd, mdPath)}\n`);
686
- } else {
687
- const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
688
- console.log("\n" + "─".repeat(60));
689
- const integrate = await askYesNo(
690
- rl2,
691
- "\n Append Fathom instructions to CLAUDE.md?",
692
- true,
693
- );
694
- rl2.close();
695
-
696
- if (integrate) {
697
- const mdPath = appendToClaudeMd(cwd, instructionsBlob);
698
- console.log(`\n ✓ Instructions written to ${path.relative(cwd, mdPath)}\n`);
699
- } else {
700
- printInstructionsFallback(agentMdDest, selectedAgents);
701
- }
702
- }
703
- } else {
704
- console.log("\n No agent instructions template found — skipping integration.\n");
705
- }
706
- }
707
-
708
- function printInstructionsFallback(agentMdPath, selectedAgents) {
709
- const hasNonClaude = selectedAgents.some((k) => k !== "claude-code");
710
- const docTarget = hasNonClaude
711
- ? "your CLAUDE.md, AGENTS.md, or equivalent"
712
- : "your CLAUDE.md";
713
-
714
- console.log(`
715
- Agent instructions saved to: ${path.relative(process.cwd(), agentMdPath)}
716
-
717
- Paste into ${docTarget}, or point your agent at the file
718
- and ask it to integrate the instructions.
719
- `);
720
- }
721
-
722
- // --- Status command ----------------------------------------------------------
723
-
724
- async function runStatus() {
725
- const config = resolveConfig();
726
- const client = createClient(config);
727
-
728
- console.log("\n Fathom MCP Status\n");
729
- console.log(` Config: ${config._configPath || "(not found — using defaults)"}`);
730
- console.log(` Workspace: ${config.workspace}`);
731
- console.log(` Vault mode: ${config.vaultMode}`);
732
- console.log(` Vault: ${config.vault}`);
733
- console.log(` Server: ${config.server}`);
734
- console.log(` API Key: ${config.apiKey ? config.apiKey.slice(0, 7) + "..." + config.apiKey.slice(-4) : "(not set)"}`);
735
- console.log(` Agents: ${config.agents.length > 0 ? config.agents.join(", ") : "(none)"}`);
736
-
737
- // Check vault directory
738
- const vaultExists = fs.existsSync(config.vault);
739
- console.log(`\n Vault dir: ${vaultExists ? "✓ exists" : "✗ not found"}`);
740
-
741
- // Check server
742
- const isUp = await client.healthCheck();
743
- console.log(` Server: ${isUp ? "✓ reachable" : "✗ not reachable"}`);
744
-
745
- if (isUp) {
746
- const wsResult = await client.listWorkspaces();
747
- if (wsResult.profiles) {
748
- const names = Object.keys(wsResult.profiles);
749
- console.log(` Workspaces: ${names.join(", ") || "(none)"}`);
750
- for (const [name, profile] of Object.entries(wsResult.profiles)) {
751
- if (profile.type === "human") {
752
- console.log(` ${name}: human`);
753
- } else {
754
- const agentLabel = profile.agents?.length > 0
755
- ? ` [${profile.agents.join(", ")}]`
756
- : "";
757
- const runStatus = profile.running ? "running" : "stopped";
758
- console.log(` ${name}: ${runStatus}${agentLabel}`);
759
- }
760
- }
761
- }
762
- }
763
-
764
- console.log();
765
- }
766
-
767
- // --- Update command ----------------------------------------------------------
768
-
769
- async function runUpdate() {
770
- const found = findConfigFile(process.cwd());
771
- if (!found) {
772
- console.error(" Error: No .fathom.json found. Run `npx fathom-mcp init` first.");
773
- process.exit(1);
774
- }
775
-
776
- const projectDir = found.dir;
777
-
778
- // Read package version from our own package.json
779
- const pkgJsonPath = path.join(__dirname, "..", "package.json");
780
- const pkg = readJsonFile(pkgJsonPath);
781
- const packageVersion = pkg?.version || "unknown";
782
-
783
- // Copy all scripts to central location
784
- const scriptsDir = path.join(process.env.HOME, ".config", "fathom-mcp", "scripts");
785
- const copiedScripts = copyScripts(scriptsDir);
786
-
787
- // Ensure SessionStart hook is registered for agents that support hooks
788
- // Detect by config agents field or directory presence (older configs may lack agents)
789
- const agents = found.config.agents || [];
790
- const sessionStartCmd = "bash ~/.config/fathom-mcp/scripts/fathom-sessionstart.sh";
791
- const registeredHooks = [];
792
-
793
- // Claude Code
794
- const hasClaude = agents.includes("claude-code")
795
- || fs.existsSync(path.join(projectDir, ".claude"));
796
- if (hasClaude) {
797
- const settingsPath = path.join(projectDir, ".claude", "settings.local.json");
798
- const settings = readJsonFile(settingsPath) || {};
799
- let changed = ensurePermissions(settings);
800
- changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
801
- if (changed) {
802
- writeJsonFile(settingsPath, settings);
803
- registeredHooks.push("Claude Code → .claude/settings.local.json");
804
- }
805
- }
806
-
807
- // Gemini CLI
808
- const hasGemini = agents.includes("gemini")
809
- || fs.existsSync(path.join(projectDir, ".gemini"));
810
- if (hasGemini) {
811
- const settingsPath = path.join(projectDir, ".gemini", "settings.json");
812
- const settings = readJsonFile(settingsPath) || {};
813
- if (ensureHook(settings, "SessionStart", sessionStartCmd, 10000)) {
814
- writeJsonFile(settingsPath, settings);
815
- registeredHooks.push("Gemini CLI → .gemini/settings.json");
816
- }
817
- }
818
-
819
- console.log(`\n ✓ Fathom hooks updated to v${packageVersion}\n`);
820
-
821
- if (copiedScripts.length > 0) {
822
- console.log(" Updated scripts in ~/.config/fathom-mcp/scripts/:");
823
- for (const script of copiedScripts) {
824
- console.log(` ${script}`);
825
- }
826
- }
827
-
828
- if (registeredHooks.length > 0) {
829
- console.log("\n Registered SessionStart hooks:");
830
- for (const hook of registeredHooks) {
831
- console.log(` ${hook}`);
832
- }
833
- }
834
-
835
- console.log("\n Restart your agent session to pick up changes.\n");
836
- }
837
-
838
- // --- List command (via server API) --------------------------------------------
839
-
840
- async function runList() {
841
- const config = resolveConfig();
842
- const client = createClient(config);
843
-
844
- const isUp = await client.healthCheck();
845
- if (!isUp) {
846
- console.error(`\n Error: Server not reachable at ${config.server}`);
847
- console.error(" Start the server or check --server URL.\n");
848
- process.exit(1);
849
- }
850
-
851
- const wsResult = await client.listWorkspaces();
852
- if (wsResult.error) {
853
- console.error(`\n Error: ${wsResult.error}\n`);
854
- process.exit(1);
855
- }
856
-
857
- const profiles = wsResult.profiles || {};
858
- const names = Object.keys(profiles);
859
-
860
- if (names.length === 0) {
861
- console.log("\n No workspaces registered on server.\n");
862
- return;
863
- }
864
-
865
- const cols = { name: 20, type: 10, status: 12 };
866
- console.log(
867
- "\n " +
868
- "WORKSPACE".padEnd(cols.name) +
869
- "TYPE".padEnd(cols.type) +
870
- "STATUS".padEnd(cols.status) +
871
- "PATH",
872
- );
873
-
874
- for (const name of names) {
875
- const p = profiles[name];
876
- const type = p.type || "local";
877
- let status;
878
- if (type === "human") {
879
- status = "human";
880
- } else if (p.process && p.connected) {
881
- status = "running";
882
- } else if (p.process || p.connected) {
883
- status = "partial";
884
- } else {
885
- status = "stopped";
886
- }
887
- const dir = (p.path || "").replace(process.env.HOME, "~");
888
-
889
- console.log(
890
- " " +
891
- name.padEnd(cols.name) +
892
- type.padEnd(cols.type) +
893
- status.padEnd(cols.status) +
894
- dir,
895
- );
896
- }
897
- console.log();
898
- }
899
-
900
- // --- Main --------------------------------------------------------------------
901
-
902
- // Guard: only run CLI when this module is the entry point (not when imported by tests)
903
- const isMain = process.argv[1] && (
904
- process.argv[1].endsWith("/cli.js") || process.argv[1].endsWith("fathom-mcp")
905
- );
906
-
907
- if (isMain) {
908
- const command = process.argv[2];
909
-
910
- const asyncHandler = (fn) => fn().catch((e) => {
911
- console.error(`Error: ${e.message}`);
912
- process.exit(1);
913
- });
914
-
915
- if (command === "init") {
916
- asyncHandler(() => runInit(parseFlags(process.argv.slice(3))));
917
- } else if (command === "status") {
918
- asyncHandler(runStatus);
919
- } else if (command === "update") {
920
- asyncHandler(runUpdate);
921
- } else if (command === "list" || command === "ls") {
922
- asyncHandler(runList);
923
- } else if (!command || command === "serve") {
924
- import("./index.js");
925
- } else {
926
- console.error(`Unknown command: ${command}`);
927
- console.error(`Usage: fathom-mcp [command]
928
-
929
- fathom-mcp Start MCP server (stdio)
930
- fathom-mcp serve Same as above
931
- fathom-mcp init [-y --api-key KEY --vault-mode MODE --agent AGENT] Interactive/non-interactive setup
932
- fathom-mcp status Check connection status
933
- fathom-mcp update Update hooks + version
934
- fathom-mcp list List workspaces + status (from server)`);
935
- process.exit(1);
936
- }
937
- }