fathom-mcp 0.5.12 → 0.5.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fathom-mcp",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "main": "src/index.js",
10
10
  "files": [
11
11
  "src/",
12
- "scripts/",
12
+ "scripts/*.sh",
13
+ "scripts/*.py",
13
14
  "fathom-agents.md",
14
15
  "README.md",
15
16
  "CHANGELOG.md",
package/src/agents.js CHANGED
@@ -9,7 +9,7 @@ import fs from "fs";
9
9
  import path from "path";
10
10
  import { execSync, execFileSync } from "child_process";
11
11
 
12
- const CONFIG_DIR = path.join(process.env.HOME || "/tmp", ".config", "fathom-mcp");
12
+ const CONFIG_DIR = process.env.FATHOM_CONFIG_DIR || path.join(process.env.HOME || "/tmp", ".config", "fathom-mcp");
13
13
  const AGENTS_FILE = path.join(CONFIG_DIR, "agents.json");
14
14
 
15
15
  const EMPTY_CONFIG = { version: 1, agents: {} };
package/src/cli.js CHANGED
@@ -149,9 +149,7 @@ function copyScripts(targetDir) {
149
149
  const HEADLESS_CMDS = {
150
150
  "claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
151
151
  "claude-sdk": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
152
- "codex": (prompt) => ["codex", "exec", prompt],
153
152
  "gemini": (prompt) => ["gemini", prompt],
154
- "opencode": (prompt) => ["opencode", "run", prompt],
155
153
  };
156
154
 
157
155
  function buildIntegrationPrompt(blob) {
@@ -249,27 +247,6 @@ function writeMcpJson(cwd) {
249
247
  return ".mcp.json";
250
248
  }
251
249
 
252
- function writeCodexToml(cwd) {
253
- const dir = path.join(cwd, ".codex");
254
- fs.mkdirSync(dir, { recursive: true });
255
- const filePath = path.join(dir, "config.toml");
256
-
257
- let content = "";
258
- try {
259
- content = fs.readFileSync(filePath, "utf-8");
260
- } catch { /* file doesn't exist */ }
261
-
262
- // Check if fathom-vault section already exists
263
- if (/\[mcp_servers\.fathom-vault\]/.test(content)) {
264
- return ".codex/config.toml (already configured)";
265
- }
266
-
267
- const section = `\n[mcp_servers.fathom-vault]\ncommand = "npx"\nargs = ["-y", "fathom-mcp"]\n`;
268
- const separator = content && !content.endsWith("\n") ? "\n" : "";
269
- fs.writeFileSync(filePath, content + separator + section);
270
- return ".codex/config.toml";
271
- }
272
-
273
250
  function writeGeminiJson(cwd) {
274
251
  const dir = path.join(cwd, ".gemini");
275
252
  fs.mkdirSync(dir, { recursive: true });
@@ -280,22 +257,6 @@ function writeGeminiJson(cwd) {
280
257
  return ".gemini/settings.json";
281
258
  }
282
259
 
283
- function writeOpencodeJson(cwd) {
284
- const filePath = path.join(cwd, "opencode.json");
285
- const existing = readJsonFile(filePath) || {};
286
- deepMerge(existing, {
287
- mcp: {
288
- "fathom-vault": {
289
- type: "local",
290
- command: ["npx", "-y", "fathom-mcp"],
291
- enabled: true,
292
- },
293
- },
294
- });
295
- writeJsonFile(filePath, existing);
296
- return "opencode.json";
297
- }
298
-
299
260
  const AGENTS = {
300
261
  "claude-code": {
301
262
  name: "Claude Code",
@@ -304,38 +265,31 @@ const AGENTS = {
304
265
  hasHooks: true,
305
266
  nextSteps: 'Add to CLAUDE.md: `ToolSearch query="+fathom" max_results=20`',
306
267
  },
307
- "codex": {
308
- name: "OpenAI Codex",
309
- detect: (cwd) => fs.existsSync(path.join(cwd, ".codex")),
310
- configWriter: writeCodexToml,
311
- hasHooks: false,
312
- nextSteps: "Run `codex` in this directory fathom tools load automatically.",
268
+ "claude-sdk": {
269
+ name: "Claude SDK (headless)",
270
+ detect: (cwd) => fs.existsSync(path.join(cwd, ".claude")),
271
+ configWriter: writeMcpJson,
272
+ hasHooks: true,
273
+ nextSteps: "Headless Claude Code no TUI, structured I/O. Start with: npx fathom-mcp start",
313
274
  },
314
275
  "gemini": {
315
276
  name: "Gemini CLI",
316
277
  detect: (cwd) => fs.existsSync(path.join(cwd, ".gemini")),
317
278
  configWriter: writeGeminiJson,
318
- hasHooks: false,
279
+ hasHooks: true,
319
280
  nextSteps: "Run `gemini` in this directory — fathom tools load automatically.",
320
281
  },
321
- "opencode": {
322
- name: "OpenCode",
323
- detect: (cwd) => fs.existsSync(path.join(cwd, "opencode.json")),
324
- configWriter: writeOpencodeJson,
282
+ "manual": {
283
+ name: "I'll set up my agent myself",
284
+ detect: () => false,
285
+ configWriter: () => "(skipped — manual setup)",
325
286
  hasHooks: false,
326
- nextSteps: "Run `opencode` in this directory fathom tools load automatically.",
327
- },
328
- "claude-sdk": {
329
- name: "Claude SDK (headless)",
330
- detect: (cwd) => fs.existsSync(path.join(cwd, ".claude")),
331
- configWriter: writeMcpJson,
332
- hasHooks: true,
333
- nextSteps: "Headless Claude Code — no TUI, structured I/O. Start with: npx fathom-mcp start",
287
+ nextSteps: "Point your agent's MCP config at: npx -y fathom-mcp",
334
288
  },
335
289
  };
336
290
 
337
291
  // Exported for testing
338
- export { AGENTS, writeMcpJson, writeCodexToml, writeGeminiJson, writeOpencodeJson };
292
+ export { AGENTS, writeMcpJson, writeGeminiJson };
339
293
 
340
294
  // --- Init wizard -------------------------------------------------------------
341
295
 
@@ -404,7 +358,7 @@ async function runInit(flags = {}) {
404
358
  const agent = AGENTS[key];
405
359
  const isDetected = detected.includes(key);
406
360
  const mark = isDetected ? "✓" : " ";
407
- const markers = { "claude-code": ".claude/", "codex": ".codex/", "gemini": ".gemini/", "opencode": "opencode.json", "claude-sdk": ".claude/" };
361
+ const markers = { "claude-code": ".claude/", "claude-sdk": ".claude/", "gemini": ".gemini/" };
408
362
  const hint = isDetected ? ` (${markers[key] || key} found)` : "";
409
363
  console.log(` ${mark} ${agent.name}${hint}`);
410
364
  }
@@ -630,6 +584,11 @@ async function runInit(flags = {}) {
630
584
  }
631
585
  }
632
586
 
587
+ // Register in CLI agent registry (for ls/start/stop)
588
+ const entry = buildEntryFromConfig(cwd, configData);
589
+ registryAddAgent(workspace, entry);
590
+ console.log(` ✓ Registered agent "${workspace}" in CLI registry`);
591
+
633
592
  // Context-aware next steps
634
593
  console.log(`\n Done! Fathom MCP is configured for workspace "${workspace}".`);
635
594
  console.log(` Vault mode: ${vaultMode}`);
@@ -1000,7 +959,7 @@ async function runAdd(argv) {
1000
959
 
1001
960
  const name = await ask(rl, " Agent name", nameArg || defaultName);
1002
961
  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);
962
+ const agentType = await ask(rl, " Agent type (claude-code|claude-sdk|gemini|manual)", defaults.agentType);
1004
963
  const executionMode = await ask(rl, " Execution mode (tmux|headless)", defaults.executionMode);
1005
964
  const command = await ask(rl, " Command", defaultCommand(agentType, executionMode));
1006
965
  const server = await ask(rl, " Server URL", defaults.server);
package/src/config.js CHANGED
@@ -20,7 +20,7 @@ const DEFAULTS = {
20
20
  workspace: "",
21
21
  vault: "vault",
22
22
  vaultMode: "local", // hosted | synced | local | none
23
- server: "http://localhost:4243",
23
+ server: "http://127.0.0.1:4243",
24
24
  apiKey: "",
25
25
  description: "",
26
26
  agents: [],
package/src/index.js CHANGED
@@ -346,6 +346,18 @@ const tools = [
346
346
  required: [],
347
347
  },
348
348
  },
349
+ {
350
+ name: "fathom_key_rotate",
351
+ description:
352
+ "Rotate this agent's API key. Revokes the current per-agent key and issues a new one. " +
353
+ "Updates the local agents.json config and reconnects the WebSocket. " +
354
+ "Only works with per-agent keys — admin keys use the dashboard.",
355
+ inputSchema: {
356
+ type: "object",
357
+ properties: {},
358
+ required: [],
359
+ },
360
+ },
349
361
  ];
350
362
 
351
363
  // --- Vault routing by mode ---------------------------------------------------
@@ -471,6 +483,26 @@ const telegramTools = [
471
483
  },
472
484
  ];
473
485
 
486
+ // --- Primary-agent-only tools (policy gate) ----------------------------------
487
+
488
+ const primaryAgentTools = [
489
+ {
490
+ name: "fathom_session_inject",
491
+ description:
492
+ "Inject a keystroke into a workspace's tmux session. Primary agent only. " +
493
+ "Used by the policy gate to respond to permission prompts. " +
494
+ "Keys must be a single digit 1-9 (to select a numbered option) or a named key (Enter, Escape).",
495
+ inputSchema: {
496
+ type: "object",
497
+ properties: {
498
+ workspace: { type: "string", description: "Target workspace name (e.g. 'navier-stokes')" },
499
+ keys: { type: "string", description: "Keys to send — single digit 1-9 or named key (Enter, Escape)" },
500
+ },
501
+ required: ["workspace", "keys"],
502
+ },
503
+ },
504
+ ];
505
+
474
506
  // --- Server setup & dispatch -------------------------------------------------
475
507
 
476
508
  const server = new Server(
@@ -489,7 +521,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
489
521
  } catch {
490
522
  // If settings unavailable, hide telegram tools
491
523
  }
492
- const allTools = [...tools, ...(showTelegram ? telegramTools : [])];
524
+ const allTools = [...tools, ...(showTelegram ? [...telegramTools, ...primaryAgentTools] : [])];
493
525
  return { tools: allTools };
494
526
  });
495
527
 
@@ -657,6 +689,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
657
689
  break;
658
690
  }
659
691
 
692
+ // --- Key rotation ---
693
+ case "fathom_key_rotate": {
694
+ const rotateResult = await client.rotateKey();
695
+ if (rotateResult.error) {
696
+ result = { error: `Key rotation failed: ${rotateResult.error}` };
697
+ break;
698
+ }
699
+
700
+ // Update agents.json with new key
701
+ try {
702
+ const { loadAgentsConfig, saveAgentsConfig } = await import("./agents.js");
703
+ const agentsConf = loadAgentsConfig();
704
+ if (agentsConf.agents[config.workspace]) {
705
+ agentsConf.agents[config.workspace].apiKey = rotateResult.api_key;
706
+ saveAgentsConfig(agentsConf);
707
+ }
708
+ } catch {
709
+ // agents.json update failed — still return the new key
710
+ }
711
+
712
+ // Reconnect WebSocket with new key
713
+ if (wsConn) {
714
+ wsConn.close();
715
+ config.apiKey = rotateResult.api_key;
716
+ wsConn = createWSConnection(config);
717
+ }
718
+
719
+ result = {
720
+ ok: true,
721
+ workspace: rotateResult.workspace,
722
+ message: "Key rotated successfully. agents.json updated, WebSocket reconnected.",
723
+ };
724
+ break;
725
+ }
726
+
660
727
  // --- Telegram ---
661
728
  case "fathom_telegram_contacts":
662
729
  result = await client.telegramContacts(config.workspace);
@@ -750,6 +817,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
750
817
  }
751
818
  break;
752
819
  }
820
+ // --- Session injection (policy gate) ---
821
+ case "fathom_session_inject": {
822
+ if (!args.workspace) { result = { error: "workspace is required" }; break; }
823
+ if (!args.keys) { result = { error: "keys is required" }; break; }
824
+ result = await client.injectKeys(args.workspace, args.keys);
825
+ break;
826
+ }
753
827
  case "fathom_telegram_send_voice": {
754
828
  const voiceContactArg = args.contact;
755
829
  if (!voiceContactArg) { result = { error: "contact is required" }; break; }
@@ -298,6 +298,14 @@ export function createClient(config) {
298
298
  });
299
299
  }
300
300
 
301
+ // --- Session injection (policy gate) ----------------------------------------
302
+
303
+ async function injectKeys(targetWorkspace, keys) {
304
+ return request("POST", `/api/session/${encodeURIComponent(targetWorkspace)}/inject`, {
305
+ body: { keys },
306
+ });
307
+ }
308
+
301
309
  // --- Settings --------------------------------------------------------------
302
310
 
303
311
  async function getSettings() {
@@ -310,6 +318,10 @@ export function createClient(config) {
310
318
  return request("GET", "/api/auth/key");
311
319
  }
312
320
 
321
+ async function rotateKey() {
322
+ return request("POST", "/api/auth/keys/rotate");
323
+ }
324
+
313
325
  async function healthCheck() {
314
326
  try {
315
327
  const resp = await fetch(`${baseUrl}/api/auth/status`, {
@@ -357,8 +369,10 @@ export function createClient(config) {
357
369
  telegramSendVoice,
358
370
  telegramStatus,
359
371
  speak,
372
+ injectKeys,
360
373
  getSettings,
361
374
  getApiKey,
375
+ rotateKey,
362
376
  healthCheck,
363
377
  };
364
378
  }
@@ -54,14 +54,20 @@ export function createWSConnection(config) {
54
54
  function connect() {
55
55
  if (closed) return;
56
56
 
57
+ // Redact token from URL for logging
58
+ const redactedUrl = wsUrl.replace(/token=[^&]+/, "token=***");
59
+ console.error(`[ws] connecting to ${redactedUrl}`);
60
+
57
61
  try {
58
62
  ws = new WebSocket(wsUrl);
59
- } catch {
63
+ } catch (err) {
64
+ console.error(`[ws] connection constructor failed: ${err.message}`);
60
65
  scheduleReconnect();
61
66
  return;
62
67
  }
63
68
 
64
69
  ws.on("open", () => {
70
+ console.error(`[ws] connected — sending hello (agent=${agent}, vault_mode=${vaultMode})`);
65
71
  reconnectDelay = INITIAL_RECONNECT_MS;
66
72
 
67
73
  // Send hello handshake
@@ -85,10 +91,12 @@ export function createWSConnection(config) {
85
91
 
86
92
  switch (msg.type) {
87
93
  case "welcome":
94
+ console.error(`[ws] welcome received — connection established for workspace=${workspace}`);
88
95
  break;
89
96
 
90
97
  case "inject":
91
98
  case "ping_fire":
99
+ console.error(`[ws] received ${msg.type} (${(msg.text || "").length} chars)`);
92
100
  injectMessage(msg.text || "");
93
101
  break;
94
102
 
@@ -101,18 +109,21 @@ export function createWSConnection(config) {
101
109
  break;
102
110
 
103
111
  case "error":
112
+ console.error(`[ws] server error: ${msg.message || JSON.stringify(msg)}`);
104
113
  // Server rejected us — don't reconnect immediately
105
114
  reconnectDelay = MAX_RECONNECT_MS;
106
115
  break;
107
116
  }
108
117
  });
109
118
 
110
- ws.on("close", () => {
119
+ ws.on("close", (code, reason) => {
120
+ console.error(`[ws] closed (code=${code}, reason=${reason || "none"})`);
111
121
  stopKeepalive();
112
122
  if (!closed) scheduleReconnect();
113
123
  });
114
124
 
115
- ws.on("error", () => {
125
+ ws.on("error", (err) => {
126
+ console.error(`[ws] error: ${err.message}`);
116
127
  // Error always followed by close event — reconnect handled there
117
128
  stopKeepalive();
118
129
  });
@@ -144,6 +155,7 @@ export function createWSConnection(config) {
144
155
 
145
156
  function scheduleReconnect() {
146
157
  if (closed) return;
158
+ console.error(`[ws] reconnecting in ${reconnectDelay}ms`);
147
159
  setTimeout(connect, reconnectDelay);
148
160
  reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
149
161
  }
@@ -177,8 +189,8 @@ export function createWSConnection(config) {
177
189
  timeout: 5000,
178
190
  stdio: "ignore",
179
191
  });
180
- } catch {
181
- // tmux not available or pane not found — non-fatal
192
+ } catch (err) {
193
+ console.error(`[ws] tmux inject failed for ${pane}: ${err.message}`);
182
194
  }
183
195
  }
184
196