fathom-mcp 0.5.8 → 0.5.9
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 +1 -1
- package/scripts/fathom-start.sh +2 -0
- package/src/agents.js +394 -0
- package/src/cli.js +260 -72
- package/src/config.js +43 -19
- package/src/index.js +0 -1
package/package.json
CHANGED
package/scripts/fathom-start.sh
CHANGED
|
@@ -287,6 +287,8 @@ do_start() {
|
|
|
287
287
|
|
|
288
288
|
cd "$PROJECT_DIR"
|
|
289
289
|
unset CLAUDECODE 2>/dev/null || true
|
|
290
|
+
# Ensure common binary locations are on PATH for backgrounded processes
|
|
291
|
+
export PATH="$HOME/.local/bin:$HOME/.claude/local/bin:$PATH"
|
|
290
292
|
mkdir -p .fathom
|
|
291
293
|
|
|
292
294
|
local pipe_file="$PROJECT_DIR/.fathom/agent.pipe"
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central agent registry — ~/.config/fathom/agents.json
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all agent definitions. The CLI uses this
|
|
5
|
+
* for list/start/stop/restart/add/remove/config commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { execSync, execFileSync, spawn } from "child_process";
|
|
11
|
+
|
|
12
|
+
const CONFIG_DIR = path.join(process.env.HOME || "/tmp", ".config", "fathom");
|
|
13
|
+
const AGENTS_FILE = path.join(CONFIG_DIR, "agents.json");
|
|
14
|
+
|
|
15
|
+
const EMPTY_CONFIG = { version: 1, agents: {} };
|
|
16
|
+
|
|
17
|
+
// ── Config I/O ──────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function loadAgentsConfig() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = fs.readFileSync(AGENTS_FILE, "utf-8");
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (!parsed.agents || typeof parsed.agents !== "object") {
|
|
24
|
+
return { ...EMPTY_CONFIG };
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
} catch {
|
|
28
|
+
return { ...EMPTY_CONFIG };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveAgentsConfig(config) {
|
|
33
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
fs.writeFileSync(AGENTS_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── CRUD ────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export function getAgent(name) {
|
|
40
|
+
const config = loadAgentsConfig();
|
|
41
|
+
return config.agents[name] || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function listAgents() {
|
|
45
|
+
const config = loadAgentsConfig();
|
|
46
|
+
return config.agents;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function addAgent(name, entry) {
|
|
50
|
+
const config = loadAgentsConfig();
|
|
51
|
+
config.agents[name] = entry;
|
|
52
|
+
saveAgentsConfig(config);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function removeAgent(name) {
|
|
56
|
+
const config = loadAgentsConfig();
|
|
57
|
+
if (!config.agents[name]) return false;
|
|
58
|
+
delete config.agents[name];
|
|
59
|
+
saveAgentsConfig(config);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Status ──────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export function isAgentRunning(name, entry) {
|
|
66
|
+
if (entry.ssh) return "ssh";
|
|
67
|
+
|
|
68
|
+
const mode = entry.executionMode || "tmux";
|
|
69
|
+
|
|
70
|
+
if (mode === "tmux") {
|
|
71
|
+
const session = `${name}_fathom-session`;
|
|
72
|
+
try {
|
|
73
|
+
execSync(`tmux has-session -t ${JSON.stringify(session)} 2>/dev/null`, {
|
|
74
|
+
stdio: "pipe",
|
|
75
|
+
});
|
|
76
|
+
return "running";
|
|
77
|
+
} catch {
|
|
78
|
+
return "stopped";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// headless — check PID file
|
|
83
|
+
const pidFile = path.join(entry.projectDir, ".fathom", "agent.pid");
|
|
84
|
+
try {
|
|
85
|
+
const pid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
86
|
+
if (pid) {
|
|
87
|
+
process.kill(parseInt(pid, 10), 0); // signal 0 = existence check
|
|
88
|
+
return "running";
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// PID file missing or process gone
|
|
92
|
+
}
|
|
93
|
+
return "stopped";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Start / Stop ────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Default command per agent type + execution mode.
|
|
100
|
+
*/
|
|
101
|
+
export function defaultCommand(agentType, executionMode) {
|
|
102
|
+
const cmds = {
|
|
103
|
+
"claude-code": {
|
|
104
|
+
tmux: "claude --model opus --permission-mode bypassPermissions",
|
|
105
|
+
headless:
|
|
106
|
+
"claude -p --permission-mode bypassPermissions --output-format stream-json",
|
|
107
|
+
},
|
|
108
|
+
"claude-sdk": {
|
|
109
|
+
tmux: "claude --model opus --permission-mode bypassPermissions",
|
|
110
|
+
headless:
|
|
111
|
+
"claude -p --permission-mode bypassPermissions --output-format stream-json",
|
|
112
|
+
},
|
|
113
|
+
codex: { tmux: "codex", headless: "codex" },
|
|
114
|
+
gemini: { tmux: "gemini", headless: "gemini" },
|
|
115
|
+
opencode: { tmux: "opencode", headless: "opencode" },
|
|
116
|
+
};
|
|
117
|
+
const forType = cmds[agentType] || cmds["claude-code"];
|
|
118
|
+
return forType[executionMode] || forType.tmux;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function startAgent(name, entry) {
|
|
122
|
+
const mode = entry.executionMode || "tmux";
|
|
123
|
+
const command = entry.command || defaultCommand(entry.agentType || "claude-code", mode);
|
|
124
|
+
const session = `${name}_fathom-session`;
|
|
125
|
+
|
|
126
|
+
// Build env
|
|
127
|
+
const env = {
|
|
128
|
+
...process.env,
|
|
129
|
+
PATH: `${process.env.HOME}/.local/bin:${process.env.HOME}/.claude/local/bin:${process.env.PATH}`,
|
|
130
|
+
...(entry.env || {}),
|
|
131
|
+
};
|
|
132
|
+
// Remove CLAUDECODE to avoid nested session detection
|
|
133
|
+
delete env.CLAUDECODE;
|
|
134
|
+
|
|
135
|
+
if (entry.ssh) {
|
|
136
|
+
return startAgentSSH(name, entry, command, env);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (mode === "headless") {
|
|
140
|
+
return startAgentHeadless(name, entry, command, env);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return startAgentTmux(name, entry, command, session, env);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function startAgentTmux(name, entry, command, session, env) {
|
|
147
|
+
// Check if already running
|
|
148
|
+
try {
|
|
149
|
+
execSync(`tmux has-session -t ${JSON.stringify(session)} 2>/dev/null`, {
|
|
150
|
+
stdio: "pipe",
|
|
151
|
+
});
|
|
152
|
+
// Save pane ID in case it wasn't saved
|
|
153
|
+
savePaneId(name, session);
|
|
154
|
+
return { ok: true, message: `Session already running: ${session}`, alreadyRunning: true };
|
|
155
|
+
} catch {
|
|
156
|
+
// Not running — continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
execSync(
|
|
161
|
+
`tmux new-session -d -s ${JSON.stringify(session)} -c ${JSON.stringify(entry.projectDir)} ${command}`,
|
|
162
|
+
{ stdio: "pipe", env },
|
|
163
|
+
);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return { ok: false, message: `Failed to start tmux session: ${e.message}` };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Brief wait for session to stabilize, then save pane ID
|
|
169
|
+
try {
|
|
170
|
+
execSync("sleep 1", { stdio: "pipe" });
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
savePaneId(name, session);
|
|
175
|
+
|
|
176
|
+
return { ok: true, message: `Started: ${session}` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function savePaneId(name, session) {
|
|
180
|
+
try {
|
|
181
|
+
const paneId = execSync(
|
|
182
|
+
`tmux list-panes -t ${JSON.stringify(session)} -F '#{pane_id}' 2>/dev/null | head -1`,
|
|
183
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
|
|
184
|
+
).trim();
|
|
185
|
+
if (paneId) {
|
|
186
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
187
|
+
fs.writeFileSync(path.join(CONFIG_DIR, `${name}-pane-id`), paneId + "\n");
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// Non-critical
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function startAgentHeadless(name, entry, command, env) {
|
|
195
|
+
const fathomDir = path.join(entry.projectDir, ".fathom");
|
|
196
|
+
const pidFile = path.join(fathomDir, "agent.pid");
|
|
197
|
+
const keeperPidFile = path.join(fathomDir, "agent-keeper.pid");
|
|
198
|
+
const pipeFile = path.join(fathomDir, "agent.pipe");
|
|
199
|
+
const logFile = path.join(fathomDir, "agent.log");
|
|
200
|
+
|
|
201
|
+
// Check if already running
|
|
202
|
+
try {
|
|
203
|
+
const pid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
204
|
+
if (pid) {
|
|
205
|
+
process.kill(parseInt(pid, 10), 0);
|
|
206
|
+
return { ok: true, message: `Headless agent already running (PID ${pid})`, alreadyRunning: true };
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
// Not running
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fs.mkdirSync(fathomDir, { recursive: true });
|
|
213
|
+
|
|
214
|
+
// Clean up stale pipe
|
|
215
|
+
try {
|
|
216
|
+
fs.unlinkSync(pipeFile);
|
|
217
|
+
} catch {
|
|
218
|
+
// ignore
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create FIFO, start keeper, start agent
|
|
222
|
+
try {
|
|
223
|
+
execSync(`mkfifo ${JSON.stringify(pipeFile)}`, { stdio: "pipe" });
|
|
224
|
+
|
|
225
|
+
// Keeper: holds the pipe open so agent doesn't get EOF
|
|
226
|
+
const keeper = spawn("sleep", ["infinity"], {
|
|
227
|
+
stdio: ["ignore", fs.openSync(pipeFile, "w"), "ignore"],
|
|
228
|
+
detached: true,
|
|
229
|
+
env,
|
|
230
|
+
});
|
|
231
|
+
keeper.unref();
|
|
232
|
+
fs.writeFileSync(keeperPidFile, String(keeper.pid) + "\n");
|
|
233
|
+
|
|
234
|
+
// Agent: reads from pipe, logs to file
|
|
235
|
+
const logFd = fs.openSync(logFile, "a");
|
|
236
|
+
const pipeFd = fs.openSync(pipeFile, "r");
|
|
237
|
+
const agent = spawn("bash", ["-c", command], {
|
|
238
|
+
cwd: entry.projectDir,
|
|
239
|
+
stdio: [pipeFd, logFd, logFd],
|
|
240
|
+
detached: true,
|
|
241
|
+
env,
|
|
242
|
+
});
|
|
243
|
+
agent.unref();
|
|
244
|
+
fs.closeSync(pipeFd);
|
|
245
|
+
fs.closeSync(logFd);
|
|
246
|
+
|
|
247
|
+
fs.writeFileSync(pidFile, String(agent.pid) + "\n");
|
|
248
|
+
|
|
249
|
+
return { ok: true, message: `Started headless (PID ${agent.pid}). Log: ${logFile}` };
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return { ok: false, message: `Failed to start headless agent: ${e.message}` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function startAgentSSH(name, entry, command, env) {
|
|
256
|
+
const { host, user, key } = entry.ssh;
|
|
257
|
+
const sshArgs = [];
|
|
258
|
+
if (key) sshArgs.push("-i", key);
|
|
259
|
+
const target = user ? `${user}@${host}` : host;
|
|
260
|
+
const remoteCmd = `cd ${JSON.stringify(entry.projectDir)} && ${command}`;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
execFileSync("ssh", [...sshArgs, target, remoteCmd], {
|
|
264
|
+
stdio: "inherit",
|
|
265
|
+
env,
|
|
266
|
+
});
|
|
267
|
+
return { ok: true, message: `SSH agent started on ${target}` };
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return { ok: false, message: `SSH start failed: ${e.message}` };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function stopAgent(name, entry) {
|
|
274
|
+
const session = `${name}_fathom-session`;
|
|
275
|
+
const messages = [];
|
|
276
|
+
let stopped = false;
|
|
277
|
+
|
|
278
|
+
if (entry.ssh) {
|
|
279
|
+
return { ok: false, message: "Cannot stop SSH agents remotely — connect to the host directly." };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Kill tmux session
|
|
283
|
+
try {
|
|
284
|
+
execSync(`tmux has-session -t ${JSON.stringify(session)} 2>/dev/null`, { stdio: "pipe" });
|
|
285
|
+
execSync(`tmux kill-session -t ${JSON.stringify(session)}`, { stdio: "pipe" });
|
|
286
|
+
// Remove pane ID file
|
|
287
|
+
try {
|
|
288
|
+
fs.unlinkSync(path.join(CONFIG_DIR, `${name}-pane-id`));
|
|
289
|
+
} catch {
|
|
290
|
+
// ignore
|
|
291
|
+
}
|
|
292
|
+
messages.push(`Killed tmux session: ${session}`);
|
|
293
|
+
stopped = true;
|
|
294
|
+
} catch {
|
|
295
|
+
// No tmux session
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Kill headless process
|
|
299
|
+
const fathomDir = path.join(entry.projectDir, ".fathom");
|
|
300
|
+
const pidFile = path.join(fathomDir, "agent.pid");
|
|
301
|
+
const keeperPidFile = path.join(fathomDir, "agent-keeper.pid");
|
|
302
|
+
const pipeFile = path.join(fathomDir, "agent.pipe");
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const pid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
306
|
+
if (pid) {
|
|
307
|
+
process.kill(parseInt(pid, 10));
|
|
308
|
+
messages.push(`Killed headless process: PID ${pid}`);
|
|
309
|
+
stopped = true;
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// Not running or file missing
|
|
313
|
+
}
|
|
314
|
+
try { fs.unlinkSync(pidFile); } catch { /* */ }
|
|
315
|
+
|
|
316
|
+
// Keeper cleanup
|
|
317
|
+
try {
|
|
318
|
+
const keeperPid = fs.readFileSync(keeperPidFile, "utf-8").trim();
|
|
319
|
+
if (keeperPid) process.kill(parseInt(keeperPid, 10));
|
|
320
|
+
} catch {
|
|
321
|
+
// ignore
|
|
322
|
+
}
|
|
323
|
+
try { fs.unlinkSync(keeperPidFile); } catch { /* */ }
|
|
324
|
+
try { fs.unlinkSync(pipeFile); } catch { /* */ }
|
|
325
|
+
|
|
326
|
+
if (!stopped) {
|
|
327
|
+
return { ok: false, message: `No running session found for: ${name}` };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { ok: true, message: messages.join("\n") };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Build a default agent entry from a .fathom.json config in a directory.
|
|
337
|
+
*/
|
|
338
|
+
export function buildEntryFromConfig(projectDir, fathomConfig) {
|
|
339
|
+
const agentType = fathomConfig.agents?.[0] || "claude-code";
|
|
340
|
+
const isHeadless = agentType === "claude-sdk";
|
|
341
|
+
const executionMode = isHeadless ? "headless" : "tmux";
|
|
342
|
+
return {
|
|
343
|
+
projectDir,
|
|
344
|
+
agentType,
|
|
345
|
+
executionMode,
|
|
346
|
+
command: defaultCommand(agentType, executionMode),
|
|
347
|
+
server: fathomConfig.server || "http://localhost:4243",
|
|
348
|
+
apiKey: fathomConfig.apiKey || "",
|
|
349
|
+
vault: fathomConfig.vault || "vault",
|
|
350
|
+
vaultMode: fathomConfig.vaultMode || "local",
|
|
351
|
+
description: fathomConfig.description || "",
|
|
352
|
+
hooks: fathomConfig.hooks || {},
|
|
353
|
+
ssh: null,
|
|
354
|
+
env: {},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Look up an agent entry by workspace name from the central registry.
|
|
360
|
+
* Returns null if no match found.
|
|
361
|
+
*/
|
|
362
|
+
export function findAgentByWorkspace(workspace) {
|
|
363
|
+
const config = loadAgentsConfig();
|
|
364
|
+
// Direct name match first
|
|
365
|
+
if (config.agents[workspace]) {
|
|
366
|
+
return { name: workspace, entry: config.agents[workspace] };
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Look up an agent entry by projectDir from the central registry.
|
|
373
|
+
* Walks up from startDir matching against registered projectDirs.
|
|
374
|
+
* Returns { name, entry } or null.
|
|
375
|
+
*/
|
|
376
|
+
export function findAgentByDir(startDir) {
|
|
377
|
+
const config = loadAgentsConfig();
|
|
378
|
+
let dir = path.resolve(startDir);
|
|
379
|
+
const root = path.parse(dir).root;
|
|
380
|
+
|
|
381
|
+
while (true) {
|
|
382
|
+
for (const [name, entry] of Object.entries(config.agents)) {
|
|
383
|
+
if (entry.projectDir === dir) {
|
|
384
|
+
return { name, entry };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const parent = path.dirname(dir);
|
|
388
|
+
if (parent === dir || dir === root) break;
|
|
389
|
+
dir = parent;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export { AGENTS_FILE, CONFIG_DIR };
|
package/src/cli.js
CHANGED
|
@@ -4,10 +4,17 @@
|
|
|
4
4
|
* fathom-mcp CLI
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx fathom-mcp
|
|
8
|
-
* npx fathom-mcp init
|
|
9
|
-
* npx fathom-mcp status
|
|
10
|
-
* npx fathom-mcp update
|
|
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 all agents + running status
|
|
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
18
|
*/
|
|
12
19
|
|
|
13
20
|
import fs from "fs";
|
|
@@ -18,6 +25,17 @@ import { fileURLToPath } from "url";
|
|
|
18
25
|
|
|
19
26
|
import { findConfigFile, resolveConfig, writeConfig } from "./config.js";
|
|
20
27
|
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";
|
|
21
39
|
|
|
22
40
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
41
|
const SCRIPTS_DIR = path.join(__dirname, "..", "scripts");
|
|
@@ -372,10 +390,7 @@ async function runInit(flags = {}) {
|
|
|
372
390
|
? (flagWorkspace || defaultName)
|
|
373
391
|
: await ask(rl, " Workspace name", defaultName);
|
|
374
392
|
|
|
375
|
-
// 2.
|
|
376
|
-
const description = nonInteractive ? "" : await ask(rl, " Workspace description (optional)", "");
|
|
377
|
-
|
|
378
|
-
// 4. Agent selection — auto-detect and let user choose
|
|
393
|
+
// 2. Agent selection — auto-detect and let user choose
|
|
379
394
|
const agentKeys = Object.keys(AGENTS);
|
|
380
395
|
const detected = agentKeys.filter((key) => AGENTS[key].detect(cwd));
|
|
381
396
|
|
|
@@ -518,7 +533,6 @@ async function runInit(flags = {}) {
|
|
|
518
533
|
vault,
|
|
519
534
|
server: serverUrl,
|
|
520
535
|
apiKey,
|
|
521
|
-
description,
|
|
522
536
|
agents: selectedAgents,
|
|
523
537
|
hooks: {
|
|
524
538
|
"vault-recall": { enabled: enableRecallHook },
|
|
@@ -528,21 +542,13 @@ async function runInit(flags = {}) {
|
|
|
528
542
|
const configPath = writeConfig(cwd, configData);
|
|
529
543
|
console.log(` ✓ ${path.relative(cwd, configPath)}`);
|
|
530
544
|
|
|
531
|
-
//
|
|
532
|
-
const
|
|
533
|
-
const copiedScripts = copyScripts(
|
|
545
|
+
// ~/.config/fathom/scripts/ (central, shared across all workspaces)
|
|
546
|
+
const centralScriptsDir = path.join(process.env.HOME, ".config", "fathom", "scripts");
|
|
547
|
+
const copiedScripts = copyScripts(centralScriptsDir);
|
|
534
548
|
if (copiedScripts.length > 0) {
|
|
535
|
-
console.log(` ✓
|
|
549
|
+
console.log(` ✓ ~/.config/fathom/scripts/ (${copiedScripts.length} scripts)`);
|
|
536
550
|
}
|
|
537
551
|
|
|
538
|
-
// .fathom/version
|
|
539
|
-
const pkgJsonPath = path.join(__dirname, "..", "package.json");
|
|
540
|
-
const pkg = readJsonFile(pkgJsonPath);
|
|
541
|
-
const packageVersion = pkg?.version || "unknown";
|
|
542
|
-
const fathomDir = path.join(cwd, ".fathom");
|
|
543
|
-
fs.writeFileSync(path.join(fathomDir, "version"), packageVersion + "\n");
|
|
544
|
-
console.log(` ✓ .fathom/version (${packageVersion})`);
|
|
545
|
-
|
|
546
552
|
// vault/ directory — only create for synced/local modes
|
|
547
553
|
if (needsLocalVault) {
|
|
548
554
|
const vaultDir = path.join(cwd, vault);
|
|
@@ -554,18 +560,18 @@ async function runInit(flags = {}) {
|
|
|
554
560
|
}
|
|
555
561
|
}
|
|
556
562
|
|
|
557
|
-
// fathom-agents.md — boilerplate agent instructions
|
|
563
|
+
// fathom-agents.md — boilerplate agent instructions (central)
|
|
558
564
|
const agentMdSrc = path.join(__dirname, "..", "fathom-agents.md");
|
|
559
|
-
const agentMdDest = path.join(
|
|
565
|
+
const agentMdDest = path.join(process.env.HOME, ".config", "fathom", "fathom-agents.md");
|
|
560
566
|
try {
|
|
561
567
|
let template = fs.readFileSync(agentMdSrc, "utf-8");
|
|
562
568
|
template = template
|
|
563
569
|
.replace(/\{\{WORKSPACE_NAME\}\}/g, workspace)
|
|
564
570
|
.replace(/\{\{VAULT_DIR\}\}/g, vault)
|
|
565
|
-
.replace(/\{\{DESCRIPTION\}\}/g,
|
|
571
|
+
.replace(/\{\{DESCRIPTION\}\}/g, `${workspace} workspace`);
|
|
566
572
|
fs.mkdirSync(path.dirname(agentMdDest), { recursive: true });
|
|
567
573
|
fs.writeFileSync(agentMdDest, template);
|
|
568
|
-
console.log(" ✓
|
|
574
|
+
console.log(" ✓ ~/.config/fathom/fathom-agents.md");
|
|
569
575
|
} catch { /* template not found — skip silently */ }
|
|
570
576
|
|
|
571
577
|
// Per-agent config files
|
|
@@ -575,10 +581,10 @@ async function runInit(flags = {}) {
|
|
|
575
581
|
console.log(` ✓ ${result}`);
|
|
576
582
|
}
|
|
577
583
|
|
|
578
|
-
// Hook scripts (shared across agents)
|
|
579
|
-
const sessionStartCmd = "bash
|
|
580
|
-
const recallCmd = "bash
|
|
581
|
-
const precompactCmd = "bash
|
|
584
|
+
// Hook scripts (central location, shared across agents)
|
|
585
|
+
const sessionStartCmd = "bash ~/.config/fathom/scripts/fathom-sessionstart.sh";
|
|
586
|
+
const recallCmd = "bash ~/.config/fathom/scripts/fathom-recall.sh";
|
|
587
|
+
const precompactCmd = "bash ~/.config/fathom/scripts/fathom-precompact.sh";
|
|
582
588
|
|
|
583
589
|
// Claude Code hooks
|
|
584
590
|
if (hasClaude) {
|
|
@@ -607,14 +613,13 @@ async function runInit(flags = {}) {
|
|
|
607
613
|
}
|
|
608
614
|
|
|
609
615
|
// .gitignore
|
|
610
|
-
appendToGitignore(cwd, [".fathom.json"
|
|
616
|
+
appendToGitignore(cwd, [".fathom.json"]);
|
|
611
617
|
console.log(" ✓ .gitignore");
|
|
612
618
|
|
|
613
619
|
// Register with server
|
|
614
620
|
if (serverReachable) {
|
|
615
621
|
const regResult = await regClient.registerWorkspace(workspace, cwd, {
|
|
616
622
|
vault,
|
|
617
|
-
description,
|
|
618
623
|
agents: selectedAgents,
|
|
619
624
|
type: selectedAgents[0] || "local",
|
|
620
625
|
});
|
|
@@ -659,7 +664,7 @@ async function runInit(flags = {}) {
|
|
|
659
664
|
}
|
|
660
665
|
|
|
661
666
|
// Auto-integrate agent instructions
|
|
662
|
-
const agentMdPath =
|
|
667
|
+
const agentMdPath = agentMdDest;
|
|
663
668
|
let instructionsBlob = "";
|
|
664
669
|
try {
|
|
665
670
|
instructionsBlob = fs.readFileSync(agentMdPath, "utf-8");
|
|
@@ -788,25 +793,20 @@ async function runUpdate() {
|
|
|
788
793
|
}
|
|
789
794
|
|
|
790
795
|
const projectDir = found.dir;
|
|
791
|
-
const fathomDir = path.join(projectDir, ".fathom");
|
|
792
796
|
|
|
793
797
|
// Read package version from our own package.json
|
|
794
798
|
const pkgJsonPath = path.join(__dirname, "..", "package.json");
|
|
795
799
|
const pkg = readJsonFile(pkgJsonPath);
|
|
796
800
|
const packageVersion = pkg?.version || "unknown";
|
|
797
801
|
|
|
798
|
-
// Copy all scripts
|
|
799
|
-
const scriptsDir = path.join(
|
|
802
|
+
// Copy all scripts to central location
|
|
803
|
+
const scriptsDir = path.join(process.env.HOME, ".config", "fathom", "scripts");
|
|
800
804
|
const copiedScripts = copyScripts(scriptsDir);
|
|
801
805
|
|
|
802
|
-
// Write version file
|
|
803
|
-
fs.mkdirSync(fathomDir, { recursive: true });
|
|
804
|
-
fs.writeFileSync(path.join(fathomDir, "version"), packageVersion + "\n");
|
|
805
|
-
|
|
806
806
|
// Ensure SessionStart hook is registered for agents that support hooks
|
|
807
807
|
// Detect by config agents field or directory presence (older configs may lack agents)
|
|
808
808
|
const agents = found.config.agents || [];
|
|
809
|
-
const sessionStartCmd = "bash
|
|
809
|
+
const sessionStartCmd = "bash ~/.config/fathom/scripts/fathom-sessionstart.sh";
|
|
810
810
|
const registeredHooks = [];
|
|
811
811
|
|
|
812
812
|
// Claude Code / Claude SDK
|
|
@@ -836,7 +836,7 @@ async function runUpdate() {
|
|
|
836
836
|
console.log(`\n ✓ Fathom hooks updated to v${packageVersion}\n`);
|
|
837
837
|
|
|
838
838
|
if (copiedScripts.length > 0) {
|
|
839
|
-
console.log(" Updated scripts
|
|
839
|
+
console.log(" Updated scripts in ~/.config/fathom/scripts/:");
|
|
840
840
|
for (const script of copiedScripts) {
|
|
841
841
|
console.log(` ${script}`);
|
|
842
842
|
}
|
|
@@ -849,39 +849,218 @@ async function runUpdate() {
|
|
|
849
849
|
}
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
-
console.log(
|
|
853
|
-
console.log(" Restart your agent session to pick up changes.\n");
|
|
852
|
+
console.log("\n Restart your agent session to pick up changes.\n");
|
|
854
853
|
}
|
|
855
854
|
|
|
856
855
|
// --- Start command -----------------------------------------------------------
|
|
857
856
|
|
|
858
857
|
function runStart(argv) {
|
|
859
|
-
//
|
|
858
|
+
// Check if first non-flag arg matches a registry entry
|
|
859
|
+
const firstArg = argv.find((a) => !a.startsWith("-"));
|
|
860
|
+
if (firstArg) {
|
|
861
|
+
const entry = getAgent(firstArg);
|
|
862
|
+
if (entry) {
|
|
863
|
+
const result = startAgent(firstArg, entry);
|
|
864
|
+
console.log(` ${result.message}`);
|
|
865
|
+
process.exit(result.ok ? 0 : 1);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Legacy fallback: delegate to fathom-start.sh
|
|
860
871
|
const found = findConfigFile(process.cwd());
|
|
861
872
|
const projectDir = found?.dir || process.cwd();
|
|
862
873
|
|
|
863
|
-
|
|
864
|
-
const localScript = path.join(projectDir, ".fathom", "scripts", "fathom-start.sh");
|
|
874
|
+
const centralScript = path.join(process.env.HOME, ".config", "fathom", "scripts", "fathom-start.sh");
|
|
865
875
|
const packageScript = path.join(SCRIPTS_DIR, "fathom-start.sh");
|
|
866
|
-
const script = fs.existsSync(
|
|
876
|
+
const script = fs.existsSync(centralScript) ? centralScript : packageScript;
|
|
867
877
|
|
|
868
878
|
if (!fs.existsSync(script)) {
|
|
869
879
|
console.error(" Error: fathom-start.sh not found. Run `npx fathom-mcp update` first.");
|
|
870
880
|
process.exit(1);
|
|
871
881
|
}
|
|
872
882
|
|
|
873
|
-
// Pass remaining args through to the shell script
|
|
874
883
|
try {
|
|
875
884
|
execFileSync("bash", [script, ...argv], {
|
|
876
885
|
cwd: projectDir,
|
|
877
886
|
stdio: "inherit",
|
|
878
887
|
});
|
|
879
888
|
} catch (e) {
|
|
880
|
-
// Script already printed its own errors; just propagate exit code
|
|
881
889
|
process.exit(e.status || 1);
|
|
882
890
|
}
|
|
883
891
|
}
|
|
884
892
|
|
|
893
|
+
// --- List command -------------------------------------------------------------
|
|
894
|
+
|
|
895
|
+
function runList() {
|
|
896
|
+
const agents = listAgents();
|
|
897
|
+
const names = Object.keys(agents);
|
|
898
|
+
|
|
899
|
+
if (names.length === 0) {
|
|
900
|
+
console.log("\n No agents registered. Run `fathom-mcp add` to register one.\n");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Header
|
|
905
|
+
const cols = { name: 16, type: 13, mode: 11, status: 10 };
|
|
906
|
+
console.log(
|
|
907
|
+
"\n " +
|
|
908
|
+
"NAME".padEnd(cols.name) +
|
|
909
|
+
"TYPE".padEnd(cols.type) +
|
|
910
|
+
"MODE".padEnd(cols.mode) +
|
|
911
|
+
"STATUS".padEnd(cols.status) +
|
|
912
|
+
"DIR",
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
for (const name of names) {
|
|
916
|
+
const entry = agents[name];
|
|
917
|
+
const type = entry.agentType || "claude-code";
|
|
918
|
+
const mode = entry.executionMode || "tmux";
|
|
919
|
+
const status = entry.ssh ? "[ssh]" : isAgentRunning(name, entry);
|
|
920
|
+
const dir = entry.projectDir.replace(process.env.HOME, "~");
|
|
921
|
+
|
|
922
|
+
console.log(
|
|
923
|
+
" " +
|
|
924
|
+
name.padEnd(cols.name) +
|
|
925
|
+
type.padEnd(cols.type) +
|
|
926
|
+
mode.padEnd(cols.mode) +
|
|
927
|
+
status.padEnd(cols.status) +
|
|
928
|
+
dir,
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
console.log();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// --- Stop command ------------------------------------------------------------
|
|
935
|
+
|
|
936
|
+
function runStop(name) {
|
|
937
|
+
if (!name) {
|
|
938
|
+
console.error(" Usage: fathom-mcp stop <name>");
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
const entry = getAgent(name);
|
|
942
|
+
if (!entry) {
|
|
943
|
+
console.error(` Error: No agent "${name}" in registry. Run \`fathom-mcp list\` to see agents.`);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
const result = stopAgent(name, entry);
|
|
947
|
+
console.log(` ${result.message}`);
|
|
948
|
+
process.exit(result.ok ? 0 : 1);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// --- Restart command ---------------------------------------------------------
|
|
952
|
+
|
|
953
|
+
function runRestart(name) {
|
|
954
|
+
if (!name) {
|
|
955
|
+
console.error(" Usage: fathom-mcp restart <name>");
|
|
956
|
+
process.exit(1);
|
|
957
|
+
}
|
|
958
|
+
const entry = getAgent(name);
|
|
959
|
+
if (!entry) {
|
|
960
|
+
console.error(` Error: No agent "${name}" in registry. Run \`fathom-mcp list\` to see agents.`);
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const stopResult = stopAgent(name, entry);
|
|
965
|
+
if (stopResult.ok) {
|
|
966
|
+
console.log(` ${stopResult.message}`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Brief pause between stop and start
|
|
970
|
+
try { execFileSync("sleep", ["1"], { stdio: "pipe" }); } catch { /* */ }
|
|
971
|
+
|
|
972
|
+
const startResult = startAgent(name, entry);
|
|
973
|
+
console.log(` ${startResult.message}`);
|
|
974
|
+
process.exit(startResult.ok ? 0 : 1);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// --- Add command -------------------------------------------------------------
|
|
978
|
+
|
|
979
|
+
async function runAdd(argv) {
|
|
980
|
+
const flags = parseFlags(argv);
|
|
981
|
+
const nameArg = argv.find((a) => !a.startsWith("-"));
|
|
982
|
+
const cwd = process.cwd();
|
|
983
|
+
|
|
984
|
+
// Try to read .fathom.json from cwd for defaults
|
|
985
|
+
const found = findConfigFile(cwd);
|
|
986
|
+
const fathomConfig = found?.config || {};
|
|
987
|
+
const projectDir = found?.dir || cwd;
|
|
988
|
+
|
|
989
|
+
const defaults = buildEntryFromConfig(projectDir, fathomConfig);
|
|
990
|
+
const defaultName = fathomConfig.workspace || path.basename(projectDir);
|
|
991
|
+
|
|
992
|
+
if (flags.nonInteractive) {
|
|
993
|
+
const name = nameArg || defaultName;
|
|
994
|
+
registryAddAgent(name, defaults);
|
|
995
|
+
console.log(` ✓ Added agent "${name}" to registry.`);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1000
|
+
|
|
1001
|
+
const name = await ask(rl, " Agent name", nameArg || defaultName);
|
|
1002
|
+
const agentProjectDir = await ask(rl, " Project directory", defaults.projectDir);
|
|
1003
|
+
const agentType = await ask(rl, " Agent type (claude-code|claude-sdk|codex|gemini|opencode)", defaults.agentType);
|
|
1004
|
+
const executionMode = await ask(rl, " Execution mode (tmux|headless)", defaults.executionMode);
|
|
1005
|
+
const command = await ask(rl, " Command", defaultCommand(agentType, executionMode));
|
|
1006
|
+
const server = await ask(rl, " Server URL", defaults.server);
|
|
1007
|
+
const apiKey = await ask(rl, " API key", defaults.apiKey);
|
|
1008
|
+
const vault = await ask(rl, " Vault subdirectory", defaults.vault || "vault");
|
|
1009
|
+
const vaultMode = await ask(rl, " Vault mode (hosted|synced|local|none)", defaults.vaultMode);
|
|
1010
|
+
const description = await ask(rl, " Description", defaults.description);
|
|
1011
|
+
|
|
1012
|
+
rl.close();
|
|
1013
|
+
|
|
1014
|
+
const entry = {
|
|
1015
|
+
projectDir: path.resolve(agentProjectDir),
|
|
1016
|
+
agentType,
|
|
1017
|
+
executionMode,
|
|
1018
|
+
command,
|
|
1019
|
+
server,
|
|
1020
|
+
apiKey,
|
|
1021
|
+
vault,
|
|
1022
|
+
vaultMode,
|
|
1023
|
+
description,
|
|
1024
|
+
hooks: defaults.hooks || {},
|
|
1025
|
+
ssh: null,
|
|
1026
|
+
env: {},
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
registryAddAgent(name, entry);
|
|
1030
|
+
console.log(`\n ✓ Added agent "${name}" to registry.`);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// --- Remove command ----------------------------------------------------------
|
|
1034
|
+
|
|
1035
|
+
function runRemove(name) {
|
|
1036
|
+
if (!name) {
|
|
1037
|
+
console.error(" Usage: fathom-mcp remove <name>");
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
const removed = registryRemoveAgent(name);
|
|
1041
|
+
if (removed) {
|
|
1042
|
+
console.log(` ✓ Removed agent "${name}" from registry.`);
|
|
1043
|
+
} else {
|
|
1044
|
+
console.error(` Error: No agent "${name}" in registry.`);
|
|
1045
|
+
process.exit(1);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// --- Config command ----------------------------------------------------------
|
|
1050
|
+
|
|
1051
|
+
function runConfigCmd(name) {
|
|
1052
|
+
if (!name) {
|
|
1053
|
+
console.error(" Usage: fathom-mcp config <name>");
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
const entry = getAgent(name);
|
|
1057
|
+
if (!entry) {
|
|
1058
|
+
console.error(` Error: No agent "${name}" in registry.`);
|
|
1059
|
+
process.exit(1);
|
|
1060
|
+
}
|
|
1061
|
+
console.log(JSON.stringify({ [name]: entry }, null, 2));
|
|
1062
|
+
}
|
|
1063
|
+
|
|
885
1064
|
// --- Main --------------------------------------------------------------------
|
|
886
1065
|
|
|
887
1066
|
// Guard: only run CLI when this module is the entry point (not when imported by tests)
|
|
@@ -892,41 +1071,50 @@ const isMain = process.argv[1] && (
|
|
|
892
1071
|
if (isMain) {
|
|
893
1072
|
const command = process.argv[2];
|
|
894
1073
|
|
|
1074
|
+
const asyncHandler = (fn) => fn().catch((e) => {
|
|
1075
|
+
console.error(`Error: ${e.message}`);
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
});
|
|
1078
|
+
|
|
895
1079
|
if (command === "init") {
|
|
896
|
-
|
|
897
|
-
runInit(flags).catch((e) => {
|
|
898
|
-
console.error(`Error: ${e.message}`);
|
|
899
|
-
process.exit(1);
|
|
900
|
-
});
|
|
1080
|
+
asyncHandler(() => runInit(parseFlags(process.argv.slice(3))));
|
|
901
1081
|
} else if (command === "status") {
|
|
902
|
-
runStatus
|
|
903
|
-
console.error(`Error: ${e.message}`);
|
|
904
|
-
process.exit(1);
|
|
905
|
-
});
|
|
1082
|
+
asyncHandler(runStatus);
|
|
906
1083
|
} else if (command === "update") {
|
|
907
|
-
runUpdate
|
|
908
|
-
console.error(`Error: ${e.message}`);
|
|
909
|
-
process.exit(1);
|
|
910
|
-
});
|
|
1084
|
+
asyncHandler(runUpdate);
|
|
911
1085
|
} else if (command === "start") {
|
|
912
1086
|
runStart(process.argv.slice(3));
|
|
1087
|
+
} else if (command === "list" || command === "ls") {
|
|
1088
|
+
runList();
|
|
1089
|
+
} else if (command === "stop") {
|
|
1090
|
+
runStop(process.argv[3]);
|
|
1091
|
+
} else if (command === "restart") {
|
|
1092
|
+
runRestart(process.argv[3]);
|
|
1093
|
+
} else if (command === "add") {
|
|
1094
|
+
asyncHandler(() => runAdd(process.argv.slice(3)));
|
|
1095
|
+
} else if (command === "remove" || command === "rm") {
|
|
1096
|
+
runRemove(process.argv[3]);
|
|
1097
|
+
} else if (command === "config") {
|
|
1098
|
+
runConfigCmd(process.argv[3]);
|
|
913
1099
|
} else if (!command || command === "serve") {
|
|
914
|
-
// Default: start MCP server
|
|
915
1100
|
import("./index.js");
|
|
916
1101
|
} else {
|
|
917
1102
|
console.error(`Unknown command: ${command}`);
|
|
918
|
-
console.error(`Usage: fathom-mcp [
|
|
1103
|
+
console.error(`Usage: fathom-mcp [command]
|
|
919
1104
|
|
|
920
|
-
fathom-mcp
|
|
921
|
-
fathom-mcp
|
|
922
|
-
fathom-mcp init -y --api-key KEY
|
|
923
|
-
fathom-mcp init -y --api-key KEY --workspace NAME Custom workspace name
|
|
1105
|
+
fathom-mcp Start MCP server (stdio)
|
|
1106
|
+
fathom-mcp serve Same as above
|
|
1107
|
+
fathom-mcp init [-y --api-key KEY] Interactive/non-interactive setup
|
|
924
1108
|
fathom-mcp status Check connection status
|
|
925
1109
|
fathom-mcp update Update hooks + version
|
|
926
|
-
|
|
927
|
-
fathom-mcp
|
|
928
|
-
fathom-mcp start
|
|
929
|
-
fathom-mcp
|
|
1110
|
+
|
|
1111
|
+
fathom-mcp list List all agents + status
|
|
1112
|
+
fathom-mcp start [name] Start agent (by name or legacy cwd)
|
|
1113
|
+
fathom-mcp stop <name> Stop agent
|
|
1114
|
+
fathom-mcp restart <name> Stop + start agent
|
|
1115
|
+
fathom-mcp add [name] Add agent to registry
|
|
1116
|
+
fathom-mcp remove <name> Remove from registry
|
|
1117
|
+
fathom-mcp config <name> Print agent config JSON`);
|
|
930
1118
|
process.exit(1);
|
|
931
1119
|
}
|
|
932
1120
|
}
|
package/src/config.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
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.
|
|
7
|
+
* 3. Central registry (~/.config/fathom/agents.json, matched by projectDir)
|
|
8
|
+
* 4. Built-in defaults
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import fs from "fs";
|
|
11
12
|
import path from "path";
|
|
13
|
+
import { findAgentByDir } from "./agents.js";
|
|
12
14
|
|
|
13
15
|
const CONFIG_FILENAME = ".fathom.json";
|
|
14
16
|
|
|
@@ -55,31 +57,54 @@ export function findConfigFile(startDir = process.cwd()) {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
60
|
+
* Apply fields from a source config object onto a result object.
|
|
61
|
+
*/
|
|
62
|
+
function applyConfig(result, config) {
|
|
63
|
+
if (config.workspace) result.workspace = config.workspace;
|
|
64
|
+
if (config.vault) result.vault = config.vault;
|
|
65
|
+
if (config.vaultMode && VALID_VAULT_MODES.has(config.vaultMode)) {
|
|
66
|
+
result.vaultMode = config.vaultMode;
|
|
67
|
+
}
|
|
68
|
+
if (config.server) result.server = config.server;
|
|
69
|
+
if (config.apiKey) result.apiKey = config.apiKey;
|
|
70
|
+
if (config.agents && Array.isArray(config.agents)) {
|
|
71
|
+
result.agents = config.agents;
|
|
72
|
+
}
|
|
73
|
+
if (config.hooks) {
|
|
74
|
+
result.hooks = { ...result.hooks, ...config.hooks };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve final config by merging: defaults → registry → .fathom.json → env vars.
|
|
59
80
|
*/
|
|
60
81
|
export function resolveConfig(startDir = process.cwd()) {
|
|
61
82
|
const result = { ...DEFAULTS, hooks: { ...DEFAULTS.hooks } };
|
|
62
83
|
let projectDir = startDir;
|
|
63
84
|
|
|
64
|
-
// Layer
|
|
85
|
+
// Layer 3: Central registry (matched by projectDir walk-up)
|
|
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)
|
|
65
104
|
const found = findConfigFile(startDir);
|
|
66
105
|
if (found) {
|
|
67
106
|
projectDir = found.dir;
|
|
68
|
-
|
|
69
|
-
if (config.workspace) result.workspace = config.workspace;
|
|
70
|
-
if (config.vault) result.vault = config.vault;
|
|
71
|
-
if (config.vaultMode && VALID_VAULT_MODES.has(config.vaultMode)) {
|
|
72
|
-
result.vaultMode = config.vaultMode;
|
|
73
|
-
}
|
|
74
|
-
if (config.server) result.server = config.server;
|
|
75
|
-
if (config.apiKey) result.apiKey = config.apiKey;
|
|
76
|
-
if (config.description) result.description = config.description;
|
|
77
|
-
if (config.agents && Array.isArray(config.agents)) {
|
|
78
|
-
result.agents = config.agents;
|
|
79
|
-
}
|
|
80
|
-
if (config.hooks) {
|
|
81
|
-
result.hooks = { ...result.hooks, ...config.hooks };
|
|
82
|
-
}
|
|
107
|
+
applyConfig(result, found.config);
|
|
83
108
|
}
|
|
84
109
|
|
|
85
110
|
// Layer 1: Environment variables (highest priority)
|
|
@@ -124,7 +149,6 @@ export function writeConfig(dir, config) {
|
|
|
124
149
|
vault: config.vault || "vault",
|
|
125
150
|
server: config.server || DEFAULTS.server,
|
|
126
151
|
apiKey: config.apiKey || "",
|
|
127
|
-
description: config.description || "",
|
|
128
152
|
agents: config.agents || [],
|
|
129
153
|
hooks: config.hooks || DEFAULTS.hooks,
|
|
130
154
|
};
|
package/src/index.js
CHANGED
|
@@ -947,7 +947,6 @@ async function main() {
|
|
|
947
947
|
if (config.server && config.workspace) {
|
|
948
948
|
client.registerWorkspace(config.workspace, config._projectDir, {
|
|
949
949
|
vault: config._rawVault,
|
|
950
|
-
description: config.description,
|
|
951
950
|
agents: config.agents,
|
|
952
951
|
type: config.vaultMode,
|
|
953
952
|
}).catch(() => {});
|