fathom-mcp 0.5.13 → 0.5.16
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 +3 -2
- package/src/agents.js +36 -91
- package/src/cli.js +28 -37
- package/src/config.js +1 -1
- package/src/index.js +28 -1
- package/src/server-client.js +9 -0
- package/src/ws-connection.js +17 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fathom-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
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: {} };
|
|
@@ -65,40 +65,15 @@ export function removeAgent(name) {
|
|
|
65
65
|
export function isAgentRunning(name, entry) {
|
|
66
66
|
if (entry.ssh) return "ssh";
|
|
67
67
|
|
|
68
|
-
const
|
|
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 pids directory for any live processes
|
|
83
|
-
const pidsDir = path.join(entry.projectDir, ".fathom", "pids");
|
|
68
|
+
const session = `${name}_fathom-session`;
|
|
84
69
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!isNaN(pid)) {
|
|
90
|
-
process.kill(pid, 0); // signal 0 = existence check
|
|
91
|
-
return "running";
|
|
92
|
-
}
|
|
93
|
-
} catch {
|
|
94
|
-
// Dead process — clean up stale PID file
|
|
95
|
-
try { fs.unlinkSync(path.join(pidsDir, file)); } catch { /* */ }
|
|
96
|
-
}
|
|
97
|
-
}
|
|
70
|
+
execSync(`tmux has-session -t ${JSON.stringify(session)} 2>/dev/null`, {
|
|
71
|
+
stdio: "pipe",
|
|
72
|
+
});
|
|
73
|
+
return "running";
|
|
98
74
|
} catch {
|
|
99
|
-
|
|
75
|
+
return "stopped";
|
|
100
76
|
}
|
|
101
|
-
return "stopped";
|
|
102
77
|
}
|
|
103
78
|
|
|
104
79
|
// ── Start / Stop ────────────────────────────────────────────────────────────
|
|
@@ -106,29 +81,39 @@ export function isAgentRunning(name, entry) {
|
|
|
106
81
|
/**
|
|
107
82
|
* Default command per agent type + execution mode.
|
|
108
83
|
*/
|
|
109
|
-
export function defaultCommand(agentType
|
|
84
|
+
export function defaultCommand(agentType) {
|
|
110
85
|
const cmds = {
|
|
111
|
-
"claude-code":
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
116
|
-
"claude-sdk": {
|
|
117
|
-
tmux: "claude --model opus --permission-mode bypassPermissions",
|
|
118
|
-
headless:
|
|
119
|
-
"claude -p --permission-mode bypassPermissions --output-format stream-json",
|
|
120
|
-
},
|
|
121
|
-
codex: { tmux: "codex", headless: "codex" },
|
|
122
|
-
gemini: { tmux: "gemini", headless: "gemini" },
|
|
123
|
-
opencode: { tmux: "opencode", headless: "opencode" },
|
|
86
|
+
"claude-code": "claude --model opus --permission-mode bypassPermissions",
|
|
87
|
+
codex: "codex",
|
|
88
|
+
gemini: "gemini",
|
|
89
|
+
opencode: "opencode",
|
|
124
90
|
};
|
|
125
|
-
|
|
126
|
-
|
|
91
|
+
return cmds[agentType] || cmds["claude-code"];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a Claude Code project has any previous conversations to --continue from.
|
|
96
|
+
* Claude stores conversations as .jsonl files in ~/.claude/projects/<encoded-path>/.
|
|
97
|
+
*/
|
|
98
|
+
function hasPreviousConversation(projectDir) {
|
|
99
|
+
const encoded = projectDir.replace(/^\//, "").replace(/\//g, "-");
|
|
100
|
+
const convDir = path.join(process.env.HOME || "/tmp", ".claude", "projects", encoded);
|
|
101
|
+
try {
|
|
102
|
+
const files = fs.readdirSync(convDir);
|
|
103
|
+
return files.some((f) => f.endsWith(".jsonl"));
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
127
107
|
}
|
|
128
108
|
|
|
129
109
|
export function startAgent(name, entry) {
|
|
130
|
-
|
|
131
|
-
|
|
110
|
+
let command = entry.command || defaultCommand(entry.agentType || "claude-code");
|
|
111
|
+
|
|
112
|
+
// --continue crashes if there's no previous conversation. Strip it for new agents.
|
|
113
|
+
if (command.includes("--continue") && !hasPreviousConversation(entry.projectDir)) {
|
|
114
|
+
command = command.replace(/\s*--continue\b/g, "");
|
|
115
|
+
}
|
|
116
|
+
|
|
132
117
|
const session = `${name}_fathom-session`;
|
|
133
118
|
|
|
134
119
|
// Build env
|
|
@@ -144,10 +129,6 @@ export function startAgent(name, entry) {
|
|
|
144
129
|
return startAgentSSH(name, entry, command, env);
|
|
145
130
|
}
|
|
146
131
|
|
|
147
|
-
if (mode === "headless") {
|
|
148
|
-
return startAgentHeadless(name, entry, command, env);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
132
|
return startAgentTmux(name, entry, command, session, env);
|
|
152
133
|
}
|
|
153
134
|
|
|
@@ -199,12 +180,6 @@ function savePaneId(name, session) {
|
|
|
199
180
|
}
|
|
200
181
|
}
|
|
201
182
|
|
|
202
|
-
function startAgentHeadless(name, entry, command, env) {
|
|
203
|
-
// Headless agents are per-invocation — no persistent process to start.
|
|
204
|
-
// The server spawns fresh claude -p processes on demand.
|
|
205
|
-
return { ok: true, message: `Headless agent "${name}" ready (per-invocation mode)` };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
183
|
function startAgentSSH(name, entry, command, env) {
|
|
209
184
|
const { host, user, key } = entry.ssh;
|
|
210
185
|
const sshArgs = [];
|
|
@@ -248,33 +223,6 @@ export function stopAgent(name, entry) {
|
|
|
248
223
|
// No tmux session
|
|
249
224
|
}
|
|
250
225
|
|
|
251
|
-
// Kill headless processes (per-invocation pids directory)
|
|
252
|
-
const fathomDir = path.join(entry.projectDir, ".fathom");
|
|
253
|
-
const pidsDir = path.join(fathomDir, "pids");
|
|
254
|
-
try {
|
|
255
|
-
const files = fs.readdirSync(pidsDir);
|
|
256
|
-
for (const file of files) {
|
|
257
|
-
try {
|
|
258
|
-
const pid = parseInt(file, 10);
|
|
259
|
-
if (!isNaN(pid)) {
|
|
260
|
-
process.kill(pid);
|
|
261
|
-
messages.push(`Killed headless process: PID ${pid}`);
|
|
262
|
-
stopped = true;
|
|
263
|
-
}
|
|
264
|
-
} catch {
|
|
265
|
-
// Already dead
|
|
266
|
-
}
|
|
267
|
-
try { fs.unlinkSync(path.join(pidsDir, file)); } catch { /* */ }
|
|
268
|
-
}
|
|
269
|
-
} catch {
|
|
270
|
-
// pids dir doesn't exist
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Clean up legacy files
|
|
274
|
-
for (const f of ["agent.pid", "agent-keeper.pid", "agent.pipe"]) {
|
|
275
|
-
try { fs.unlinkSync(path.join(fathomDir, f)); } catch { /* */ }
|
|
276
|
-
}
|
|
277
|
-
|
|
278
226
|
if (!stopped) {
|
|
279
227
|
return { ok: false, message: `No running session found for: ${name}` };
|
|
280
228
|
}
|
|
@@ -289,13 +237,10 @@ export function stopAgent(name, entry) {
|
|
|
289
237
|
*/
|
|
290
238
|
export function buildEntryFromConfig(projectDir, fathomConfig) {
|
|
291
239
|
const agentType = fathomConfig.agents?.[0] || "claude-code";
|
|
292
|
-
const isHeadless = agentType === "claude-sdk";
|
|
293
|
-
const executionMode = isHeadless ? "headless" : "tmux";
|
|
294
240
|
return {
|
|
295
241
|
projectDir,
|
|
296
242
|
agentType,
|
|
297
|
-
|
|
298
|
-
command: defaultCommand(agentType, executionMode),
|
|
243
|
+
command: defaultCommand(agentType),
|
|
299
244
|
server: fathomConfig.server || "http://localhost:4243",
|
|
300
245
|
apiKey: fathomConfig.apiKey || "",
|
|
301
246
|
vault: fathomConfig.vault || "vault",
|
package/src/cli.js
CHANGED
|
@@ -144,14 +144,6 @@ function copyScripts(targetDir) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// --- Headless agent integration ----------------------------------------------
|
|
148
|
-
|
|
149
|
-
const HEADLESS_CMDS = {
|
|
150
|
-
"claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
151
|
-
"claude-sdk": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
152
|
-
"gemini": (prompt) => ["gemini", prompt],
|
|
153
|
-
};
|
|
154
|
-
|
|
155
147
|
function buildIntegrationPrompt(blob) {
|
|
156
148
|
return [
|
|
157
149
|
"The following instructions were generated by fathom-mcp init for this project.",
|
|
@@ -180,8 +172,14 @@ function detectFathomServer() {
|
|
|
180
172
|
}
|
|
181
173
|
}
|
|
182
174
|
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
// Agent commands for non-interactive prompt integration (init --non-interactive)
|
|
176
|
+
const AGENT_PROMPT_CMDS = {
|
|
177
|
+
"claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
178
|
+
"gemini": (prompt) => ["gemini", prompt],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
function runAgentPrompt(agentKey, prompt) {
|
|
182
|
+
const cmdBuilder = AGENT_PROMPT_CMDS[agentKey];
|
|
185
183
|
if (!cmdBuilder) return null;
|
|
186
184
|
const [cmd, ...args] = cmdBuilder(prompt);
|
|
187
185
|
try {
|
|
@@ -265,13 +263,6 @@ const AGENTS = {
|
|
|
265
263
|
hasHooks: true,
|
|
266
264
|
nextSteps: 'Add to CLAUDE.md: `ToolSearch query="+fathom" max_results=20`',
|
|
267
265
|
},
|
|
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",
|
|
274
|
-
},
|
|
275
266
|
"gemini": {
|
|
276
267
|
name: "Gemini CLI",
|
|
277
268
|
detect: (cwd) => fs.existsSync(path.join(cwd, ".gemini")),
|
|
@@ -358,7 +349,7 @@ async function runInit(flags = {}) {
|
|
|
358
349
|
const agent = AGENTS[key];
|
|
359
350
|
const isDetected = detected.includes(key);
|
|
360
351
|
const mark = isDetected ? "✓" : " ";
|
|
361
|
-
const markers = { "claude-code": ".claude/", "
|
|
352
|
+
const markers = { "claude-code": ".claude/", "gemini": ".gemini/" };
|
|
362
353
|
const hint = isDetected ? ` (${markers[key] || key} found)` : "";
|
|
363
354
|
console.log(` ${mark} ${agent.name}${hint}`);
|
|
364
355
|
}
|
|
@@ -458,7 +449,7 @@ async function runInit(flags = {}) {
|
|
|
458
449
|
: "vault";
|
|
459
450
|
|
|
460
451
|
// 9. Hooks — ask if any hook-supporting agent is selected (Claude Code, Claude SDK, Gemini CLI)
|
|
461
|
-
const hasClaude = selectedAgents.includes("claude-code")
|
|
452
|
+
const hasClaude = selectedAgents.includes("claude-code");
|
|
462
453
|
const hasGemini = selectedAgents.includes("gemini");
|
|
463
454
|
const hasHookAgent = hasClaude || hasGemini;
|
|
464
455
|
let enableRecallHook = false;
|
|
@@ -584,6 +575,11 @@ async function runInit(flags = {}) {
|
|
|
584
575
|
}
|
|
585
576
|
}
|
|
586
577
|
|
|
578
|
+
// Register in CLI agent registry (for ls/start/stop)
|
|
579
|
+
const entry = buildEntryFromConfig(cwd, configData);
|
|
580
|
+
registryAddAgent(workspace, entry);
|
|
581
|
+
console.log(` ✓ Registered agent "${workspace}" in CLI registry`);
|
|
582
|
+
|
|
587
583
|
// Context-aware next steps
|
|
588
584
|
console.log(`\n Done! Fathom MCP is configured for workspace "${workspace}".`);
|
|
589
585
|
console.log(` Vault mode: ${vaultMode}`);
|
|
@@ -628,12 +624,12 @@ async function runInit(flags = {}) {
|
|
|
628
624
|
|
|
629
625
|
if (instructionsBlob) {
|
|
630
626
|
const prompt = buildIntegrationPrompt(instructionsBlob);
|
|
631
|
-
const
|
|
627
|
+
const cmdBuilder = AGENT_PROMPT_CMDS[primaryAgent];
|
|
632
628
|
|
|
633
629
|
if (nonInteractive) {
|
|
634
|
-
if (
|
|
630
|
+
if (cmdBuilder) {
|
|
635
631
|
console.log(`\n Integrating instructions via ${AGENTS[primaryAgent].name}...`);
|
|
636
|
-
const result =
|
|
632
|
+
const result = runAgentPrompt(primaryAgent, prompt);
|
|
637
633
|
if (result !== null) {
|
|
638
634
|
console.log(result);
|
|
639
635
|
} else {
|
|
@@ -644,9 +640,9 @@ async function runInit(flags = {}) {
|
|
|
644
640
|
printInstructionsFallback(agentMdPath, selectedAgents);
|
|
645
641
|
}
|
|
646
642
|
} else {
|
|
647
|
-
if (
|
|
648
|
-
const [cmd, ...
|
|
649
|
-
const flagArgs =
|
|
643
|
+
if (cmdBuilder) {
|
|
644
|
+
const [cmd, ...sampleArgs] = cmdBuilder("<prompt>");
|
|
645
|
+
const flagArgs = sampleArgs.slice(0, -1).join(" ");
|
|
650
646
|
const displayCmd = `${cmd} ${flagArgs} <prompt>`;
|
|
651
647
|
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
652
648
|
console.log("\n" + "─".repeat(60));
|
|
@@ -659,7 +655,7 @@ async function runInit(flags = {}) {
|
|
|
659
655
|
|
|
660
656
|
if (integrate) {
|
|
661
657
|
console.log(`\n Running ${AGENTS[primaryAgent].name}...`);
|
|
662
|
-
const result =
|
|
658
|
+
const result = runAgentPrompt(primaryAgent, prompt);
|
|
663
659
|
if (result !== null) {
|
|
664
660
|
console.log(result);
|
|
665
661
|
} else {
|
|
@@ -679,7 +675,7 @@ async function runInit(flags = {}) {
|
|
|
679
675
|
}
|
|
680
676
|
|
|
681
677
|
function printInstructionsFallback(agentMdPath, selectedAgents) {
|
|
682
|
-
const hasNonClaude = selectedAgents.some((k) => k !== "claude-code"
|
|
678
|
+
const hasNonClaude = selectedAgents.some((k) => k !== "claude-code");
|
|
683
679
|
const docTarget = hasNonClaude
|
|
684
680
|
? "your CLAUDE.md, AGENTS.md, or equivalent"
|
|
685
681
|
: "your CLAUDE.md";
|
|
@@ -763,8 +759,8 @@ async function runUpdate() {
|
|
|
763
759
|
const sessionStartCmd = "bash ~/.config/fathom-mcp/scripts/fathom-sessionstart.sh";
|
|
764
760
|
const registeredHooks = [];
|
|
765
761
|
|
|
766
|
-
// Claude Code
|
|
767
|
-
const hasClaude = agents.includes("claude-code")
|
|
762
|
+
// Claude Code
|
|
763
|
+
const hasClaude = agents.includes("claude-code")
|
|
768
764
|
|| fs.existsSync(path.join(projectDir, ".claude"));
|
|
769
765
|
if (hasClaude) {
|
|
770
766
|
const settingsPath = path.join(projectDir, ".claude", "settings.local.json");
|
|
@@ -856,12 +852,11 @@ function runList() {
|
|
|
856
852
|
}
|
|
857
853
|
|
|
858
854
|
// Header
|
|
859
|
-
const cols = { name: 16, type: 13,
|
|
855
|
+
const cols = { name: 16, type: 13, status: 10 };
|
|
860
856
|
console.log(
|
|
861
857
|
"\n " +
|
|
862
858
|
"NAME".padEnd(cols.name) +
|
|
863
859
|
"TYPE".padEnd(cols.type) +
|
|
864
|
-
"MODE".padEnd(cols.mode) +
|
|
865
860
|
"STATUS".padEnd(cols.status) +
|
|
866
861
|
"DIR",
|
|
867
862
|
);
|
|
@@ -869,7 +864,6 @@ function runList() {
|
|
|
869
864
|
for (const name of names) {
|
|
870
865
|
const entry = agents[name];
|
|
871
866
|
const type = entry.agentType || "claude-code";
|
|
872
|
-
const mode = entry.executionMode || "tmux";
|
|
873
867
|
const status = entry.ssh ? "[ssh]" : isAgentRunning(name, entry);
|
|
874
868
|
const dir = entry.projectDir.replace(process.env.HOME, "~");
|
|
875
869
|
|
|
@@ -877,7 +871,6 @@ function runList() {
|
|
|
877
871
|
" " +
|
|
878
872
|
name.padEnd(cols.name) +
|
|
879
873
|
type.padEnd(cols.type) +
|
|
880
|
-
mode.padEnd(cols.mode) +
|
|
881
874
|
status.padEnd(cols.status) +
|
|
882
875
|
dir,
|
|
883
876
|
);
|
|
@@ -954,9 +947,8 @@ async function runAdd(argv) {
|
|
|
954
947
|
|
|
955
948
|
const name = await ask(rl, " Agent name", nameArg || defaultName);
|
|
956
949
|
const agentProjectDir = await ask(rl, " Project directory", defaults.projectDir);
|
|
957
|
-
const agentType = await ask(rl, " Agent type (claude-code|
|
|
958
|
-
const
|
|
959
|
-
const command = await ask(rl, " Command", defaultCommand(agentType, executionMode));
|
|
950
|
+
const agentType = await ask(rl, " Agent type (claude-code|gemini|manual)", defaults.agentType);
|
|
951
|
+
const command = await ask(rl, " Command", defaultCommand(agentType));
|
|
960
952
|
const server = await ask(rl, " Server URL", defaults.server);
|
|
961
953
|
const apiKey = await ask(rl, " API key", defaults.apiKey);
|
|
962
954
|
const vault = await ask(rl, " Vault subdirectory", defaults.vault || "vault");
|
|
@@ -968,7 +960,6 @@ async function runAdd(argv) {
|
|
|
968
960
|
const entry = {
|
|
969
961
|
projectDir: path.resolve(agentProjectDir),
|
|
970
962
|
agentType,
|
|
971
|
-
executionMode,
|
|
972
963
|
command,
|
|
973
964
|
server,
|
|
974
965
|
apiKey,
|
package/src/config.js
CHANGED
package/src/index.js
CHANGED
|
@@ -483,6 +483,26 @@ const telegramTools = [
|
|
|
483
483
|
},
|
|
484
484
|
];
|
|
485
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
|
+
|
|
486
506
|
// --- Server setup & dispatch -------------------------------------------------
|
|
487
507
|
|
|
488
508
|
const server = new Server(
|
|
@@ -501,7 +521,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
501
521
|
} catch {
|
|
502
522
|
// If settings unavailable, hide telegram tools
|
|
503
523
|
}
|
|
504
|
-
const allTools = [...tools, ...(showTelegram ? telegramTools : [])];
|
|
524
|
+
const allTools = [...tools, ...(showTelegram ? [...telegramTools, ...primaryAgentTools] : [])];
|
|
505
525
|
return { tools: allTools };
|
|
506
526
|
});
|
|
507
527
|
|
|
@@ -797,6 +817,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
797
817
|
}
|
|
798
818
|
break;
|
|
799
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
|
+
}
|
|
800
827
|
case "fathom_telegram_send_voice": {
|
|
801
828
|
const voiceContactArg = args.contact;
|
|
802
829
|
if (!voiceContactArg) { result = { error: "contact is required" }; break; }
|
package/src/server-client.js
CHANGED
|
@@ -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() {
|
|
@@ -361,6 +369,7 @@ export function createClient(config) {
|
|
|
361
369
|
telegramSendVoice,
|
|
362
370
|
telegramStatus,
|
|
363
371
|
speak,
|
|
372
|
+
injectKeys,
|
|
364
373
|
getSettings,
|
|
365
374
|
getApiKey,
|
|
366
375
|
rotateKey,
|
package/src/ws-connection.js
CHANGED
|
@@ -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
|
}
|
|
@@ -152,13 +164,6 @@ export function createWSConnection(config) {
|
|
|
152
164
|
|
|
153
165
|
function injectMessage(text) {
|
|
154
166
|
if (!text) return;
|
|
155
|
-
|
|
156
|
-
if (agent === "claude-sdk") {
|
|
157
|
-
// Headless agents are per-invocation — injection handled server-side.
|
|
158
|
-
// Nothing to do client-side.
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
167
|
injectToTmux(text);
|
|
163
168
|
}
|
|
164
169
|
|
|
@@ -177,8 +182,8 @@ export function createWSConnection(config) {
|
|
|
177
182
|
timeout: 5000,
|
|
178
183
|
stdio: "ignore",
|
|
179
184
|
});
|
|
180
|
-
} catch {
|
|
181
|
-
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`[ws] tmux inject failed for ${pane}: ${err.message}`);
|
|
182
187
|
}
|
|
183
188
|
}
|
|
184
189
|
|