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