fathom-mcp 0.5.20 → 0.6.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/package.json +2 -1
- package/scripts/vault-frontmatter-lint.js +65 -0
- package/src/cli.js +102 -236
- package/src/config.js +4 -24
- package/src/frontmatter.js +77 -0
- package/src/index.js +58 -387
- package/src/server-client.js +6 -45
- package/src/ws-connection.js +4 -6
- package/src/agents.js +0 -196
- package/src/vault-ops.js +0 -386
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fathom-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"src/",
|
|
12
12
|
"scripts/*.sh",
|
|
13
13
|
"scripts/*.py",
|
|
14
|
+
"scripts/*.js",
|
|
14
15
|
"fathom-agents.md",
|
|
15
16
|
"README.md",
|
|
16
17
|
"CHANGELOG.md",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vault frontmatter lint hook for Claude Code PostToolUse (Write/Edit).
|
|
5
|
+
*
|
|
6
|
+
* Reads the tool result from stdin (JSON), checks if the file is inside a
|
|
7
|
+
* vault/ directory and ends in .md. If so, validates frontmatter.
|
|
8
|
+
* Exits 0 on pass, 1 on failure (with error message on stderr).
|
|
9
|
+
*
|
|
10
|
+
* Environment: CLAUDE_TOOL_NAME, CLAUDE_FILE_PATH are set by Claude Code hooks.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { parseFrontmatter, validateFrontmatter } from "../src/frontmatter.js";
|
|
16
|
+
|
|
17
|
+
const toolName = process.env.CLAUDE_TOOL_NAME || "";
|
|
18
|
+
const filePath = process.env.CLAUDE_FILE_PATH || "";
|
|
19
|
+
|
|
20
|
+
// Only check Write and Edit tools
|
|
21
|
+
if (toolName !== "Write" && toolName !== "Edit") {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Only check .md files inside a vault/ directory
|
|
26
|
+
if (!filePath || !filePath.endsWith(".md")) {
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if the file is inside a vault/ directory
|
|
31
|
+
const parts = filePath.split(path.sep);
|
|
32
|
+
if (!parts.includes("vault")) {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Read the file and validate
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
39
|
+
|
|
40
|
+
// Only validate if file has frontmatter
|
|
41
|
+
if (!content.startsWith("---")) {
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { fm } = parseFrontmatter(content);
|
|
46
|
+
if (Object.keys(fm).length === 0) {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const errors = validateFrontmatter(fm);
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
process.stderr.write(`Vault frontmatter validation failed for ${path.basename(filePath)}:\n`);
|
|
53
|
+
for (const err of errors) {
|
|
54
|
+
process.stderr.write(` - ${err}\n`);
|
|
55
|
+
}
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// File read errors are non-fatal for the hook
|
|
60
|
+
if (e.code !== "ENOENT") {
|
|
61
|
+
process.stderr.write(`Frontmatter lint error: ${e.message}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
process.exit(0);
|
package/src/cli.js
CHANGED
|
@@ -8,13 +8,7 @@
|
|
|
8
8
|
* npx fathom-mcp init — Interactive setup wizard
|
|
9
9
|
* npx fathom-mcp status — Check server connection + workspace status
|
|
10
10
|
* npx fathom-mcp update — Update hook scripts + version file
|
|
11
|
-
* npx fathom-mcp list — List
|
|
12
|
-
* npx fathom-mcp start [name] — Start agent by name or legacy cwd-walk
|
|
13
|
-
* npx fathom-mcp stop <name> — Stop an agent
|
|
14
|
-
* npx fathom-mcp restart <name> — Restart an agent
|
|
15
|
-
* npx fathom-mcp add [name] — Add agent to registry (reads .fathom.json defaults)
|
|
16
|
-
* npx fathom-mcp remove <name>— Remove agent from registry
|
|
17
|
-
* npx fathom-mcp config <name>— Print agent config JSON
|
|
11
|
+
* npx fathom-mcp list — List workspaces + status (from server)
|
|
18
12
|
*/
|
|
19
13
|
|
|
20
14
|
import fs from "fs";
|
|
@@ -25,17 +19,6 @@ import { fileURLToPath } from "url";
|
|
|
25
19
|
|
|
26
20
|
import { findConfigFile, resolveConfig, writeConfig } from "./config.js";
|
|
27
21
|
import { createClient } from "./server-client.js";
|
|
28
|
-
import {
|
|
29
|
-
listAgents,
|
|
30
|
-
getAgent,
|
|
31
|
-
addAgent as registryAddAgent,
|
|
32
|
-
removeAgent as registryRemoveAgent,
|
|
33
|
-
isAgentRunning,
|
|
34
|
-
startAgent,
|
|
35
|
-
stopAgent,
|
|
36
|
-
defaultCommand,
|
|
37
|
-
buildEntryFromConfig,
|
|
38
|
-
} from "./agents.js";
|
|
39
22
|
|
|
40
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
41
24
|
const SCRIPTS_DIR = path.join(__dirname, "..", "scripts");
|
|
@@ -114,6 +97,33 @@ function appendToGitignore(dir, patterns) {
|
|
|
114
97
|
* Works for both Claude Code and Gemini CLI (same JSON structure).
|
|
115
98
|
* Returns true if a new hook was added, false if already present.
|
|
116
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
|
+
|
|
117
127
|
function ensureHook(settings, eventName, command, timeout) {
|
|
118
128
|
const existing = settings.hooks?.[eventName] || [];
|
|
119
129
|
const alreadyRegistered = existing.some((entry) =>
|
|
@@ -204,6 +214,7 @@ function parseFlags(argv) {
|
|
|
204
214
|
server: null,
|
|
205
215
|
workspace: null,
|
|
206
216
|
agent: null,
|
|
217
|
+
vaultMode: null,
|
|
207
218
|
};
|
|
208
219
|
for (let i = 0; i < argv.length; i++) {
|
|
209
220
|
if (argv[i] === "-y" || argv[i] === "--yes") {
|
|
@@ -220,6 +231,9 @@ function parseFlags(argv) {
|
|
|
220
231
|
} else if (argv[i] === "--agent" && argv[i + 1]) {
|
|
221
232
|
flags.agent = argv[i + 1];
|
|
222
233
|
i++;
|
|
234
|
+
} else if (argv[i] === "--vault-mode" && argv[i + 1]) {
|
|
235
|
+
flags.vaultMode = argv[i + 1];
|
|
236
|
+
i++;
|
|
223
237
|
}
|
|
224
238
|
}
|
|
225
239
|
// Check environment variables as fallback
|
|
@@ -295,6 +309,7 @@ async function runInit(flags = {}) {
|
|
|
295
309
|
server: flagServer = null,
|
|
296
310
|
workspace: flagWorkspace = null,
|
|
297
311
|
agent: flagAgent = null,
|
|
312
|
+
vaultMode: flagVaultMode = null,
|
|
298
313
|
} = flags;
|
|
299
314
|
const cwd = process.cwd();
|
|
300
315
|
|
|
@@ -346,7 +361,8 @@ async function runInit(flags = {}) {
|
|
|
346
361
|
|
|
347
362
|
let selectedAgents;
|
|
348
363
|
if (nonInteractive) {
|
|
349
|
-
|
|
364
|
+
// In non-interactive mode, always default to claude-code unless explicitly overridden
|
|
365
|
+
if (flagAgent && flagAgent !== "claude-code") {
|
|
350
366
|
if (!AGENTS[flagAgent]) {
|
|
351
367
|
const valid = Object.keys(AGENTS).join(", ");
|
|
352
368
|
console.error(` Error: unknown agent "${flagAgent}". Valid agents: ${valid}`);
|
|
@@ -355,8 +371,8 @@ async function runInit(flags = {}) {
|
|
|
355
371
|
selectedAgents = [flagAgent];
|
|
356
372
|
console.log(` Agent: ${AGENTS[flagAgent].name} (--agent flag)`);
|
|
357
373
|
} else {
|
|
358
|
-
selectedAgents =
|
|
359
|
-
console.log(` Agent:
|
|
374
|
+
selectedAgents = ["claude-code"];
|
|
375
|
+
console.log(` Agent: Claude Code`);
|
|
360
376
|
}
|
|
361
377
|
} else {
|
|
362
378
|
console.log("\n Detected agents:");
|
|
@@ -430,24 +446,37 @@ async function runInit(flags = {}) {
|
|
|
430
446
|
|
|
431
447
|
// 8. Vault mode selection
|
|
432
448
|
let vaultMode;
|
|
449
|
+
const validVaultModes = ["synced", "local", "none"];
|
|
433
450
|
if (nonInteractive) {
|
|
434
451
|
if (!serverReachable && flagServer) {
|
|
435
452
|
console.error(`\n Error: Server at ${serverUrl} is not reachable.`);
|
|
436
453
|
console.error(" Fix the URL or start the server, then re-run init.");
|
|
437
454
|
process.exit(1);
|
|
438
455
|
}
|
|
439
|
-
|
|
440
|
-
|
|
456
|
+
if (flagVaultMode) {
|
|
457
|
+
if (!validVaultModes.includes(flagVaultMode)) {
|
|
458
|
+
console.error(` Error: unknown vault mode "${flagVaultMode}". Valid modes: ${validVaultModes.join(", ")}`);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
if (flagVaultMode === "synced" && !serverReachable) {
|
|
462
|
+
console.error(` Error: vault mode "${flagVaultMode}" requires a reachable server.`);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
vaultMode = flagVaultMode;
|
|
466
|
+
console.log(` Vault mode: ${vaultMode} (--vault-mode flag)`);
|
|
467
|
+
} else {
|
|
468
|
+
vaultMode = serverReachable ? "synced" : "local";
|
|
469
|
+
console.log(` Vault mode: ${vaultMode} (auto-selected)`);
|
|
470
|
+
}
|
|
441
471
|
} else {
|
|
442
472
|
if (serverReachable) {
|
|
443
473
|
console.log("\n Vault mode:");
|
|
444
|
-
console.log(" 1.
|
|
445
|
-
console.log(" 2.
|
|
446
|
-
console.log(" 3.
|
|
447
|
-
console.log(" 4. None — no vault, coordination features only");
|
|
474
|
+
console.log(" 1. Synced (default) — local vault + server indexing");
|
|
475
|
+
console.log(" 2. Local — vault on disk only, not visible to server");
|
|
476
|
+
console.log(" 3. None — no vault, coordination features only");
|
|
448
477
|
const modeChoice = await ask(rl, "\n Select mode", "1");
|
|
449
|
-
const modeMap = { "1": "
|
|
450
|
-
vaultMode = modeMap[modeChoice] || "
|
|
478
|
+
const modeMap = { "1": "synced", "2": "local", "3": "none" };
|
|
479
|
+
vaultMode = modeMap[modeChoice] || "synced";
|
|
451
480
|
} else {
|
|
452
481
|
console.log("\n Vault mode (server not available — hosted/synced require server):");
|
|
453
482
|
console.log(" 1. Local (default) — vault on disk only");
|
|
@@ -547,15 +576,18 @@ async function runInit(flags = {}) {
|
|
|
547
576
|
const precompactCmd = "bash ~/.config/fathom-mcp/scripts/fathom-precompact.sh";
|
|
548
577
|
|
|
549
578
|
// Claude Code hooks
|
|
579
|
+
const lintCmd = "node ~/.config/fathom-mcp/scripts/vault-frontmatter-lint.js";
|
|
550
580
|
if (hasClaude) {
|
|
551
581
|
const settingsPath = path.join(cwd, ".claude", "settings.local.json");
|
|
552
582
|
const settings = readJsonFile(settingsPath) || {};
|
|
553
|
-
let changed =
|
|
583
|
+
let changed = ensurePermissions(settings);
|
|
584
|
+
changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
554
585
|
if (enableRecallHook) changed = ensureHook(settings, "UserPromptSubmit", recallCmd, 10000) || changed;
|
|
555
586
|
if (enablePrecompactHook) changed = ensureHook(settings, "PreCompact", precompactCmd, 30000) || changed;
|
|
587
|
+
changed = ensureHook(settings, "PostToolUse", lintCmd, 5000) || changed;
|
|
556
588
|
if (changed) {
|
|
557
589
|
writeJsonFile(settingsPath, settings);
|
|
558
|
-
console.log(" ✓ .claude/settings.local.json (hooks)");
|
|
590
|
+
console.log(" ✓ .claude/settings.local.json (permissions + hooks)");
|
|
559
591
|
}
|
|
560
592
|
}
|
|
561
593
|
|
|
@@ -609,11 +641,6 @@ async function runInit(flags = {}) {
|
|
|
609
641
|
}
|
|
610
642
|
}
|
|
611
643
|
|
|
612
|
-
// Register in CLI agent registry (for ls/start/stop)
|
|
613
|
-
const entry = buildEntryFromConfig(cwd, configData);
|
|
614
|
-
registryAddAgent(workspace, entry);
|
|
615
|
-
console.log(` ✓ Registered agent "${workspace}" in CLI registry`);
|
|
616
|
-
|
|
617
644
|
// Context-aware next steps
|
|
618
645
|
console.log(`\n Done! Fathom MCP is configured for workspace "${workspace}".`);
|
|
619
646
|
console.log(` Vault mode: ${vaultMode}`);
|
|
@@ -629,11 +656,8 @@ async function runInit(flags = {}) {
|
|
|
629
656
|
|
|
630
657
|
const stepNum = serverReachable ? 1 : 2;
|
|
631
658
|
switch (vaultMode) {
|
|
632
|
-
case "hosted":
|
|
633
|
-
console.log(` ${stepNum}. Your vault is stored on the server. Start writing!`);
|
|
634
|
-
break;
|
|
635
659
|
case "synced":
|
|
636
|
-
console.log(` ${stepNum}. Local vault
|
|
660
|
+
console.log(` ${stepNum}. Local vault indexed by server. Files in ./${vault}/ are the source of truth.`);
|
|
637
661
|
break;
|
|
638
662
|
case "local":
|
|
639
663
|
console.log(` ${stepNum}. Local vault only. Server can't search or peek into it.`);
|
|
@@ -653,6 +677,7 @@ async function runInit(flags = {}) {
|
|
|
653
677
|
if (apiKey) parts.push(`--api-key "${apiKey}"`);
|
|
654
678
|
if (serverUrl && serverUrl !== "http://localhost:4243") parts.push(`--server ${serverUrl}`);
|
|
655
679
|
parts.push(`--workspace ${workspace}`);
|
|
680
|
+
parts.push(`--vault-mode ${vaultMode}`);
|
|
656
681
|
parts.push(`--agent ${selectedAgents[0]}`);
|
|
657
682
|
console.log(`\n Non-interactive equivalent:\n ${parts.join(" ")}\n`);
|
|
658
683
|
}
|
|
@@ -809,7 +834,9 @@ async function runUpdate() {
|
|
|
809
834
|
if (hasClaude) {
|
|
810
835
|
const settingsPath = path.join(projectDir, ".claude", "settings.local.json");
|
|
811
836
|
const settings = readJsonFile(settingsPath) || {};
|
|
812
|
-
|
|
837
|
+
let changed = ensurePermissions(settings);
|
|
838
|
+
changed = ensureHook(settings, "SessionStart", sessionStartCmd, 10000) || changed;
|
|
839
|
+
if (changed) {
|
|
813
840
|
writeJsonFile(settingsPath, settings);
|
|
814
841
|
registeredHooks.push("Claude Code → .claude/settings.local.json");
|
|
815
842
|
}
|
|
@@ -846,70 +873,56 @@ async function runUpdate() {
|
|
|
846
873
|
console.log("\n Restart your agent session to pick up changes.\n");
|
|
847
874
|
}
|
|
848
875
|
|
|
849
|
-
// ---
|
|
850
|
-
|
|
851
|
-
function runStart(argv) {
|
|
852
|
-
// Check if first non-flag arg matches a registry entry
|
|
853
|
-
const firstArg = argv.find((a) => !a.startsWith("-"));
|
|
854
|
-
if (firstArg) {
|
|
855
|
-
const entry = getAgent(firstArg);
|
|
856
|
-
if (entry) {
|
|
857
|
-
const result = startAgent(firstArg, entry);
|
|
858
|
-
console.log(` ${result.message}`);
|
|
859
|
-
process.exit(result.ok ? 0 : 1);
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// Legacy fallback: delegate to fathom-start.sh
|
|
865
|
-
const found = findConfigFile(process.cwd());
|
|
866
|
-
const projectDir = found?.dir || process.cwd();
|
|
876
|
+
// --- List command (via server API) --------------------------------------------
|
|
867
877
|
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
const
|
|
878
|
+
async function runList() {
|
|
879
|
+
const config = resolveConfig();
|
|
880
|
+
const client = createClient(config);
|
|
871
881
|
|
|
872
|
-
|
|
873
|
-
|
|
882
|
+
const isUp = await client.healthCheck();
|
|
883
|
+
if (!isUp) {
|
|
884
|
+
console.error(`\n Error: Server not reachable at ${config.server}`);
|
|
885
|
+
console.error(" Start the server or check --server URL.\n");
|
|
874
886
|
process.exit(1);
|
|
875
887
|
}
|
|
876
888
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
});
|
|
882
|
-
} catch (e) {
|
|
883
|
-
process.exit(e.status || 1);
|
|
889
|
+
const wsResult = await client.listWorkspaces();
|
|
890
|
+
if (wsResult.error) {
|
|
891
|
+
console.error(`\n Error: ${wsResult.error}\n`);
|
|
892
|
+
process.exit(1);
|
|
884
893
|
}
|
|
885
|
-
}
|
|
886
894
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
function runList() {
|
|
890
|
-
const agents = listAgents();
|
|
891
|
-
const names = Object.keys(agents);
|
|
895
|
+
const profiles = wsResult.profiles || {};
|
|
896
|
+
const names = Object.keys(profiles);
|
|
892
897
|
|
|
893
898
|
if (names.length === 0) {
|
|
894
|
-
console.log("\n No
|
|
899
|
+
console.log("\n No workspaces registered on server.\n");
|
|
895
900
|
return;
|
|
896
901
|
}
|
|
897
902
|
|
|
898
|
-
|
|
899
|
-
const cols = { name: 16, type: 13, status: 10 };
|
|
903
|
+
const cols = { name: 20, type: 10, status: 12 };
|
|
900
904
|
console.log(
|
|
901
905
|
"\n " +
|
|
902
|
-
"
|
|
906
|
+
"WORKSPACE".padEnd(cols.name) +
|
|
903
907
|
"TYPE".padEnd(cols.type) +
|
|
904
908
|
"STATUS".padEnd(cols.status) +
|
|
905
|
-
"
|
|
909
|
+
"PATH",
|
|
906
910
|
);
|
|
907
911
|
|
|
908
912
|
for (const name of names) {
|
|
909
|
-
const
|
|
910
|
-
const type =
|
|
911
|
-
|
|
912
|
-
|
|
913
|
+
const p = profiles[name];
|
|
914
|
+
const type = p.type || "local";
|
|
915
|
+
let status;
|
|
916
|
+
if (type === "human") {
|
|
917
|
+
status = "human";
|
|
918
|
+
} else if (p.process && p.connected) {
|
|
919
|
+
status = "running";
|
|
920
|
+
} else if (p.process || p.connected) {
|
|
921
|
+
status = "partial";
|
|
922
|
+
} else {
|
|
923
|
+
status = "stopped";
|
|
924
|
+
}
|
|
925
|
+
const dir = (p.path || "").replace(process.env.HOME, "~");
|
|
913
926
|
|
|
914
927
|
console.log(
|
|
915
928
|
" " +
|
|
@@ -922,134 +935,6 @@ function runList() {
|
|
|
922
935
|
console.log();
|
|
923
936
|
}
|
|
924
937
|
|
|
925
|
-
// --- Stop command ------------------------------------------------------------
|
|
926
|
-
|
|
927
|
-
function runStop(name) {
|
|
928
|
-
if (!name) {
|
|
929
|
-
console.error(" Usage: fathom-mcp stop <name>");
|
|
930
|
-
process.exit(1);
|
|
931
|
-
}
|
|
932
|
-
const entry = getAgent(name);
|
|
933
|
-
if (!entry) {
|
|
934
|
-
console.error(` Error: No agent "${name}" in registry. Run \`fathom-mcp list\` to see agents.`);
|
|
935
|
-
process.exit(1);
|
|
936
|
-
}
|
|
937
|
-
const result = stopAgent(name, entry);
|
|
938
|
-
console.log(` ${result.message}`);
|
|
939
|
-
process.exit(result.ok ? 0 : 1);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// --- Restart command ---------------------------------------------------------
|
|
943
|
-
|
|
944
|
-
function runRestart(name) {
|
|
945
|
-
if (!name) {
|
|
946
|
-
console.error(" Usage: fathom-mcp restart <name>");
|
|
947
|
-
process.exit(1);
|
|
948
|
-
}
|
|
949
|
-
const entry = getAgent(name);
|
|
950
|
-
if (!entry) {
|
|
951
|
-
console.error(` Error: No agent "${name}" in registry. Run \`fathom-mcp list\` to see agents.`);
|
|
952
|
-
process.exit(1);
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
const stopResult = stopAgent(name, entry);
|
|
956
|
-
if (stopResult.ok) {
|
|
957
|
-
console.log(` ${stopResult.message}`);
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Brief pause between stop and start
|
|
961
|
-
try { execFileSync("sleep", ["1"], { stdio: "pipe" }); } catch { /* */ }
|
|
962
|
-
|
|
963
|
-
const startResult = startAgent(name, entry);
|
|
964
|
-
console.log(` ${startResult.message}`);
|
|
965
|
-
process.exit(startResult.ok ? 0 : 1);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// --- Add command -------------------------------------------------------------
|
|
969
|
-
|
|
970
|
-
async function runAdd(argv) {
|
|
971
|
-
const flags = parseFlags(argv);
|
|
972
|
-
const nameArg = argv.find((a) => !a.startsWith("-"));
|
|
973
|
-
const cwd = process.cwd();
|
|
974
|
-
|
|
975
|
-
// Try to read .fathom.json from cwd for defaults
|
|
976
|
-
const found = findConfigFile(cwd);
|
|
977
|
-
const fathomConfig = found?.config || {};
|
|
978
|
-
const projectDir = found?.dir || cwd;
|
|
979
|
-
|
|
980
|
-
const defaults = buildEntryFromConfig(projectDir, fathomConfig);
|
|
981
|
-
const defaultName = fathomConfig.workspace || path.basename(projectDir);
|
|
982
|
-
|
|
983
|
-
if (flags.nonInteractive) {
|
|
984
|
-
const name = nameArg || defaultName;
|
|
985
|
-
registryAddAgent(name, defaults);
|
|
986
|
-
console.log(` ✓ Added agent "${name}" to registry.`);
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
991
|
-
|
|
992
|
-
const name = await ask(rl, " Agent name", nameArg || defaultName);
|
|
993
|
-
const agentProjectDir = await ask(rl, " Project directory", defaults.projectDir);
|
|
994
|
-
const agentType = await ask(rl, " Agent type (claude-code|gemini|manual)", defaults.agentType);
|
|
995
|
-
const command = await ask(rl, " Command", defaultCommand(agentType));
|
|
996
|
-
const server = await ask(rl, " Server URL", defaults.server);
|
|
997
|
-
const apiKey = await ask(rl, " API key", defaults.apiKey);
|
|
998
|
-
const vault = await ask(rl, " Vault subdirectory", defaults.vault || "vault");
|
|
999
|
-
const vaultMode = await ask(rl, " Vault mode (hosted|synced|local|none)", defaults.vaultMode);
|
|
1000
|
-
const description = await ask(rl, " Description", defaults.description);
|
|
1001
|
-
|
|
1002
|
-
rl.close();
|
|
1003
|
-
|
|
1004
|
-
const entry = {
|
|
1005
|
-
projectDir: path.resolve(agentProjectDir),
|
|
1006
|
-
agentType,
|
|
1007
|
-
command,
|
|
1008
|
-
server,
|
|
1009
|
-
apiKey,
|
|
1010
|
-
vault,
|
|
1011
|
-
vaultMode,
|
|
1012
|
-
description,
|
|
1013
|
-
hooks: defaults.hooks || {},
|
|
1014
|
-
ssh: null,
|
|
1015
|
-
env: {},
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
registryAddAgent(name, entry);
|
|
1019
|
-
console.log(`\n ✓ Added agent "${name}" to registry.`);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// --- Remove command ----------------------------------------------------------
|
|
1023
|
-
|
|
1024
|
-
function runRemove(name) {
|
|
1025
|
-
if (!name) {
|
|
1026
|
-
console.error(" Usage: fathom-mcp remove <name>");
|
|
1027
|
-
process.exit(1);
|
|
1028
|
-
}
|
|
1029
|
-
const removed = registryRemoveAgent(name);
|
|
1030
|
-
if (removed) {
|
|
1031
|
-
console.log(` ✓ Removed agent "${name}" from registry.`);
|
|
1032
|
-
} else {
|
|
1033
|
-
console.error(` Error: No agent "${name}" in registry.`);
|
|
1034
|
-
process.exit(1);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// --- Config command ----------------------------------------------------------
|
|
1039
|
-
|
|
1040
|
-
function runConfigCmd(name) {
|
|
1041
|
-
if (!name) {
|
|
1042
|
-
console.error(" Usage: fathom-mcp config <name>");
|
|
1043
|
-
process.exit(1);
|
|
1044
|
-
}
|
|
1045
|
-
const entry = getAgent(name);
|
|
1046
|
-
if (!entry) {
|
|
1047
|
-
console.error(` Error: No agent "${name}" in registry.`);
|
|
1048
|
-
process.exit(1);
|
|
1049
|
-
}
|
|
1050
|
-
console.log(JSON.stringify({ [name]: entry }, null, 2));
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
938
|
// --- Main --------------------------------------------------------------------
|
|
1054
939
|
|
|
1055
940
|
// Guard: only run CLI when this module is the entry point (not when imported by tests)
|
|
@@ -1071,20 +956,8 @@ if (isMain) {
|
|
|
1071
956
|
asyncHandler(runStatus);
|
|
1072
957
|
} else if (command === "update") {
|
|
1073
958
|
asyncHandler(runUpdate);
|
|
1074
|
-
} else if (command === "start") {
|
|
1075
|
-
runStart(process.argv.slice(3));
|
|
1076
959
|
} else if (command === "list" || command === "ls") {
|
|
1077
|
-
runList
|
|
1078
|
-
} else if (command === "stop") {
|
|
1079
|
-
runStop(process.argv[3]);
|
|
1080
|
-
} else if (command === "restart") {
|
|
1081
|
-
runRestart(process.argv[3]);
|
|
1082
|
-
} else if (command === "add") {
|
|
1083
|
-
asyncHandler(() => runAdd(process.argv.slice(3)));
|
|
1084
|
-
} else if (command === "remove" || command === "rm") {
|
|
1085
|
-
runRemove(process.argv[3]);
|
|
1086
|
-
} else if (command === "config") {
|
|
1087
|
-
runConfigCmd(process.argv[3]);
|
|
960
|
+
asyncHandler(runList);
|
|
1088
961
|
} else if (!command || command === "serve") {
|
|
1089
962
|
import("./index.js");
|
|
1090
963
|
} else {
|
|
@@ -1093,17 +966,10 @@ if (isMain) {
|
|
|
1093
966
|
|
|
1094
967
|
fathom-mcp Start MCP server (stdio)
|
|
1095
968
|
fathom-mcp serve Same as above
|
|
1096
|
-
fathom-mcp init [-y --api-key KEY --agent AGENT] Interactive/non-interactive setup
|
|
969
|
+
fathom-mcp init [-y --api-key KEY --vault-mode MODE --agent AGENT] Interactive/non-interactive setup
|
|
1097
970
|
fathom-mcp status Check connection status
|
|
1098
971
|
fathom-mcp update Update hooks + version
|
|
1099
|
-
|
|
1100
|
-
fathom-mcp list List all agents + status
|
|
1101
|
-
fathom-mcp start [name] Start agent (by name or legacy cwd)
|
|
1102
|
-
fathom-mcp stop <name> Stop agent
|
|
1103
|
-
fathom-mcp restart <name> Stop + start agent
|
|
1104
|
-
fathom-mcp add [name] Add agent to registry
|
|
1105
|
-
fathom-mcp remove <name> Remove from registry
|
|
1106
|
-
fathom-mcp config <name> Print agent config JSON`);
|
|
972
|
+
fathom-mcp list List workspaces + status (from server)`);
|
|
1107
973
|
process.exit(1);
|
|
1108
974
|
}
|
|
1109
975
|
}
|
package/src/config.js
CHANGED
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
* Precedence (highest wins):
|
|
5
5
|
* 1. Environment variables (FATHOM_SERVER_URL, FATHOM_API_KEY, FATHOM_WORKSPACE, FATHOM_VAULT_DIR)
|
|
6
6
|
* 2. .fathom.json (walked up from cwd to filesystem root)
|
|
7
|
-
* 3.
|
|
8
|
-
* 4. Built-in defaults
|
|
7
|
+
* 3. Built-in defaults
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import fs from "fs";
|
|
12
11
|
import path from "path";
|
|
13
|
-
import { findAgentByDir } from "./agents.js";
|
|
14
12
|
|
|
15
13
|
const CONFIG_FILENAME = ".fathom.json";
|
|
16
14
|
|
|
17
|
-
const VALID_VAULT_MODES = new Set(["
|
|
15
|
+
const VALID_VAULT_MODES = new Set(["synced", "local", "none"]);
|
|
18
16
|
|
|
19
17
|
const DEFAULTS = {
|
|
20
18
|
workspace: "",
|
|
@@ -76,31 +74,13 @@ function applyConfig(result, config) {
|
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
/**
|
|
79
|
-
* Resolve final config by merging: defaults →
|
|
77
|
+
* Resolve final config by merging: defaults → .fathom.json → env vars.
|
|
80
78
|
*/
|
|
81
79
|
export function resolveConfig(startDir = process.cwd()) {
|
|
82
80
|
const result = { ...DEFAULTS, hooks: { ...DEFAULTS.hooks } };
|
|
83
81
|
let projectDir = startDir;
|
|
84
82
|
|
|
85
|
-
// Layer
|
|
86
|
-
let registryMatch = null;
|
|
87
|
-
try {
|
|
88
|
-
registryMatch = findAgentByDir(startDir);
|
|
89
|
-
} catch {
|
|
90
|
-
// Registry not available — skip
|
|
91
|
-
}
|
|
92
|
-
if (registryMatch) {
|
|
93
|
-
const { name, entry } = registryMatch;
|
|
94
|
-
projectDir = entry.projectDir;
|
|
95
|
-
result.workspace = name;
|
|
96
|
-
applyConfig(result, entry);
|
|
97
|
-
if (entry.agentType) {
|
|
98
|
-
result.agents = [entry.agentType];
|
|
99
|
-
}
|
|
100
|
-
result._registryName = name;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Layer 2: .fathom.json (overrides registry)
|
|
83
|
+
// Layer 2: .fathom.json
|
|
104
84
|
const found = findConfigFile(startDir);
|
|
105
85
|
if (found) {
|
|
106
86
|
projectDir = found.dir;
|