fathom-mcp 0.5.19 → 0.5.20
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/src/agents.js +46 -159
- package/src/index.js +131 -15
- package/src/server-client.js +0 -9
- package/src/ws-connection.js +5 -49
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for all agent definitions. The CLI uses this
|
|
5
5
|
* for list/start/stop/restart/add/remove/config commands.
|
|
6
|
+
*
|
|
7
|
+
* All agents use stream-json transport — lifecycle delegated to fathom-server.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import fs from "fs";
|
|
9
11
|
import path from "path";
|
|
10
|
-
import { execSync
|
|
12
|
+
import { execSync } from "child_process";
|
|
11
13
|
|
|
12
14
|
const CONFIG_DIR = process.env.FATHOM_CONFIG_DIR || path.join(process.env.HOME || "/tmp", ".config", "fathom-mcp");
|
|
13
15
|
const AGENTS_FILE = path.join(CONFIG_DIR, "agents.json");
|
|
@@ -65,12 +67,17 @@ export function removeAgent(name) {
|
|
|
65
67
|
export function isAgentRunning(name, entry) {
|
|
66
68
|
if (entry.ssh) return "ssh";
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
// All agents: check via server API
|
|
69
71
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
const serverUrl = entry.server || "http://localhost:4243";
|
|
73
|
+
const apiKey = entry.apiKey || "";
|
|
74
|
+
const url = `${serverUrl}/api/activation/session?workspace=${encodeURIComponent(name)}`;
|
|
75
|
+
const resp = execSync(`curl -sf -H "Authorization: Bearer ${apiKey}" "${url}"`, {
|
|
76
|
+
encoding: "utf-8",
|
|
77
|
+
timeout: 5000,
|
|
72
78
|
});
|
|
73
|
-
|
|
79
|
+
const data = JSON.parse(resp);
|
|
80
|
+
return data.running ? "running" : "stopped";
|
|
74
81
|
} catch {
|
|
75
82
|
return "stopped";
|
|
76
83
|
}
|
|
@@ -78,174 +85,54 @@ export function isAgentRunning(name, entry) {
|
|
|
78
85
|
|
|
79
86
|
// ── Start / Stop ────────────────────────────────────────────────────────────
|
|
80
87
|
|
|
81
|
-
/**
|
|
82
|
-
* Default command per agent type + execution mode.
|
|
83
|
-
*/
|
|
84
|
-
export function defaultCommand(agentType) {
|
|
85
|
-
const cmds = {
|
|
86
|
-
"claude-code": "claude --continue",
|
|
87
|
-
codex: "codex",
|
|
88
|
-
gemini: "gemini",
|
|
89
|
-
opencode: "opencode",
|
|
90
|
-
};
|
|
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
|
-
* The encoded directory name uses the *resolved* path that Claude sees at runtime,
|
|
99
|
-
* which may differ from agents.json projectDir (e.g., container bind-mounts).
|
|
100
|
-
* We resolve the real path and also check the literal path as a fallback.
|
|
101
|
-
*/
|
|
102
|
-
function hasPreviousConversation(projectDir) {
|
|
103
|
-
const claudeProjectsDir = path.join(process.env.HOME || "/tmp", ".claude", "projects");
|
|
104
|
-
|
|
105
|
-
// Try both the literal path and the real (resolved symlink/bind-mount) path
|
|
106
|
-
const candidates = new Set([projectDir]);
|
|
107
|
-
try {
|
|
108
|
-
candidates.add(fs.realpathSync(projectDir));
|
|
109
|
-
} catch {
|
|
110
|
-
// projectDir might not exist on this machine (e.g., host path checked from container)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
for (const dir of candidates) {
|
|
114
|
-
const encoded = dir.replace(/\//g, "-");
|
|
115
|
-
const convDir = path.join(claudeProjectsDir, encoded);
|
|
116
|
-
try {
|
|
117
|
-
const files = fs.readdirSync(convDir);
|
|
118
|
-
if (files.some((f) => f.endsWith(".jsonl"))) return true;
|
|
119
|
-
} catch {
|
|
120
|
-
// continue to next candidate
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
88
|
export function startAgent(name, entry) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (command.includes("--continue") && !hasPreviousConversation(entry.projectDir)) {
|
|
132
|
-
command = command.replace(/\s*--continue\b/g, "");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const session = `${name}_fathom-session`;
|
|
136
|
-
|
|
137
|
-
// Build env
|
|
138
|
-
const env = {
|
|
139
|
-
...process.env,
|
|
140
|
-
PATH: `${process.env.HOME}/.local/bin:${process.env.HOME}/.claude/local/bin:${process.env.PATH}`,
|
|
141
|
-
...(entry.env || {}),
|
|
142
|
-
};
|
|
143
|
-
// Remove CLAUDECODE to avoid nested session detection
|
|
144
|
-
delete env.CLAUDECODE;
|
|
145
|
-
|
|
146
|
-
if (entry.ssh) {
|
|
147
|
-
return startAgentSSH(name, entry, command, env);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return startAgentTmux(name, entry, command, session, env);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function startAgentTmux(name, entry, command, session, env) {
|
|
154
|
-
// Check if already running
|
|
89
|
+
// All agents: delegate lifecycle to fathom-server
|
|
90
|
+
const serverUrl = entry.server || "http://localhost:4243";
|
|
91
|
+
const apiKey = entry.apiKey || "";
|
|
155
92
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
93
|
+
const url = `${serverUrl}/api/activation/session/start?workspace=${encodeURIComponent(name)}`;
|
|
94
|
+
const resp = execSync(`curl -sf -X POST -H "Authorization: Bearer ${apiKey}" "${url}"`, {
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
timeout: 30000,
|
|
158
97
|
});
|
|
159
|
-
|
|
160
|
-
savePaneId(name, session);
|
|
161
|
-
return { ok: true, message: `Session already running: ${session}`, alreadyRunning: true };
|
|
162
|
-
} catch {
|
|
163
|
-
// Not running — continue
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
execSync(
|
|
168
|
-
`tmux new-session -d -s ${JSON.stringify(session)} -c ${JSON.stringify(entry.projectDir)} ${command}`,
|
|
169
|
-
{ stdio: "pipe", env },
|
|
170
|
-
);
|
|
98
|
+
return { ok: true, message: `Delegated to server subprocess manager: ${resp.trim()}` };
|
|
171
99
|
} catch (e) {
|
|
172
|
-
return { ok: false, message: `
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Brief wait for session to stabilize, then save pane ID
|
|
176
|
-
try {
|
|
177
|
-
execSync("sleep 1", { stdio: "pipe" });
|
|
178
|
-
} catch {
|
|
179
|
-
// ignore
|
|
180
|
-
}
|
|
181
|
-
savePaneId(name, session);
|
|
182
|
-
|
|
183
|
-
return { ok: true, message: `Started: ${session}` };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function savePaneId(name, session) {
|
|
187
|
-
try {
|
|
188
|
-
const paneId = execSync(
|
|
189
|
-
`tmux list-panes -t ${JSON.stringify(session)} -F '#{pane_id}' 2>/dev/null | head -1`,
|
|
190
|
-
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
|
|
191
|
-
).trim();
|
|
192
|
-
if (paneId) {
|
|
193
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
194
|
-
fs.writeFileSync(path.join(CONFIG_DIR, `${name}-pane-id`), paneId + "\n");
|
|
195
|
-
}
|
|
196
|
-
} catch {
|
|
197
|
-
// Non-critical
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function startAgentSSH(name, entry, command, env) {
|
|
202
|
-
const { host, user, key } = entry.ssh;
|
|
203
|
-
const sshArgs = [];
|
|
204
|
-
if (key) sshArgs.push("-i", key);
|
|
205
|
-
const target = user ? `${user}@${host}` : host;
|
|
206
|
-
const remoteCmd = `cd ${JSON.stringify(entry.projectDir)} && ${command}`;
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
execFileSync("ssh", [...sshArgs, target, remoteCmd], {
|
|
210
|
-
stdio: "inherit",
|
|
211
|
-
env,
|
|
212
|
-
});
|
|
213
|
-
return { ok: true, message: `SSH agent started on ${target}` };
|
|
214
|
-
} catch (e) {
|
|
215
|
-
return { ok: false, message: `SSH start failed: ${e.message}` };
|
|
100
|
+
return { ok: false, message: `Server delegation failed: ${e.message}` };
|
|
216
101
|
}
|
|
217
102
|
}
|
|
218
103
|
|
|
219
104
|
export function stopAgent(name, entry) {
|
|
220
|
-
const session = `${name}_fathom-session`;
|
|
221
|
-
const messages = [];
|
|
222
|
-
let stopped = false;
|
|
223
|
-
|
|
224
105
|
if (entry.ssh) {
|
|
225
106
|
return { ok: false, message: "Cannot stop SSH agents remotely — connect to the host directly." };
|
|
226
107
|
}
|
|
227
108
|
|
|
228
|
-
//
|
|
109
|
+
// All agents: delegate to server
|
|
229
110
|
try {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
} catch {
|
|
241
|
-
// No tmux session
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (!stopped) {
|
|
245
|
-
return { ok: false, message: `No running session found for: ${name}` };
|
|
111
|
+
const serverUrl = entry.server || "http://localhost:4243";
|
|
112
|
+
const apiKey = entry.apiKey || "";
|
|
113
|
+
const url = `${serverUrl}/api/activation/session/stop?workspace=${encodeURIComponent(name)}`;
|
|
114
|
+
const resp = execSync(`curl -sf -X POST -H "Authorization: Bearer ${apiKey}" "${url}"`, {
|
|
115
|
+
encoding: "utf-8",
|
|
116
|
+
timeout: 15000,
|
|
117
|
+
});
|
|
118
|
+
return { ok: true, message: `Server stopped subprocess: ${resp.trim()}` };
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return { ok: false, message: `Server stop failed: ${e.message}` };
|
|
246
121
|
}
|
|
122
|
+
}
|
|
247
123
|
|
|
248
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Default command per agent type (vestigial — used by CLI `add` flow).
|
|
126
|
+
* Stream-json agents don't use this at runtime; the server manages lifecycle.
|
|
127
|
+
*/
|
|
128
|
+
export function defaultCommand(agentType) {
|
|
129
|
+
const cmds = {
|
|
130
|
+
"claude-code": "claude",
|
|
131
|
+
codex: "codex",
|
|
132
|
+
gemini: "gemini",
|
|
133
|
+
opencode: "opencode",
|
|
134
|
+
};
|
|
135
|
+
return cmds[agentType] || cmds["claude-code"];
|
|
249
136
|
}
|
|
250
137
|
|
|
251
138
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -258,7 +145,7 @@ export function buildEntryFromConfig(projectDir, fathomConfig) {
|
|
|
258
145
|
return {
|
|
259
146
|
projectDir,
|
|
260
147
|
agentType,
|
|
261
|
-
command:
|
|
148
|
+
command: "",
|
|
262
149
|
server: fathomConfig.server || "http://localhost:4243",
|
|
263
150
|
apiKey: fathomConfig.apiKey || "",
|
|
264
151
|
vault: fathomConfig.vault || "vault",
|
package/src/index.js
CHANGED
|
@@ -483,26 +483,143 @@ const telegramTools = [
|
|
|
483
483
|
},
|
|
484
484
|
];
|
|
485
485
|
|
|
486
|
-
// ---
|
|
486
|
+
// --- Policy evaluation tool (permission-prompt-tool for stream-json) ---------
|
|
487
487
|
|
|
488
|
-
const
|
|
488
|
+
const policyTools = [
|
|
489
489
|
{
|
|
490
|
-
name: "
|
|
490
|
+
name: "policy_evaluate",
|
|
491
491
|
description:
|
|
492
|
-
"
|
|
493
|
-
"
|
|
494
|
-
"
|
|
492
|
+
"Evaluate a permission request for a tool call. Called automatically by Claude " +
|
|
493
|
+
"via --permission-prompt-tool when a tool is not in the static allow list. " +
|
|
494
|
+
"Returns {behavior: 'allow'} or {behavior: 'deny', message: '...'}.",
|
|
495
495
|
inputSchema: {
|
|
496
496
|
type: "object",
|
|
497
497
|
properties: {
|
|
498
|
-
|
|
499
|
-
|
|
498
|
+
tool_name: { type: "string", description: "The tool being requested (e.g. 'Bash', 'Edit')" },
|
|
499
|
+
input: { type: "object", description: "The tool's input arguments" },
|
|
500
500
|
},
|
|
501
|
-
required: ["
|
|
501
|
+
required: ["tool_name", "input"],
|
|
502
502
|
},
|
|
503
503
|
},
|
|
504
504
|
];
|
|
505
505
|
|
|
506
|
+
// --- Primary-agent-only tools (reserved for future use) ----------------------
|
|
507
|
+
|
|
508
|
+
const primaryAgentTools = [];
|
|
509
|
+
|
|
510
|
+
// --- Policy evaluation -------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Evaluate a permission request from Claude's --permission-prompt-tool.
|
|
514
|
+
*
|
|
515
|
+
* Decision logic:
|
|
516
|
+
* 1. Allow list: auto-approve safe tools/commands
|
|
517
|
+
* 2. Hard deny: auto-reject dangerous operations
|
|
518
|
+
* 3. Ambiguous: escalate to dashboard for human approval
|
|
519
|
+
*/
|
|
520
|
+
async function evaluatePermission(toolName, input) {
|
|
521
|
+
const projectDir = config.vault ? path.dirname(config.vault) : process.cwd();
|
|
522
|
+
|
|
523
|
+
// --- Allow list (auto-approve) ---
|
|
524
|
+
const alwaysAllowed = [
|
|
525
|
+
"mcp__fathom-vault__", "mcp__fathom__", "mcp__memento__",
|
|
526
|
+
"Read", "Glob", "Grep", "Agent",
|
|
527
|
+
];
|
|
528
|
+
for (const prefix of alwaysAllowed) {
|
|
529
|
+
if (toolName === prefix || toolName.startsWith(prefix)) {
|
|
530
|
+
return { behavior: "allow" };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Edit/Write — allow if path within workspace
|
|
535
|
+
if (toolName === "Edit" || toolName === "Write") {
|
|
536
|
+
const filePath = input.file_path || input.path || "";
|
|
537
|
+
if (filePath.startsWith(projectDir)) {
|
|
538
|
+
return { behavior: "allow" };
|
|
539
|
+
}
|
|
540
|
+
// Paths in home .claude dir are ok
|
|
541
|
+
const homeDir = process.env.HOME || "/tmp";
|
|
542
|
+
if (filePath.startsWith(path.join(homeDir, ".claude"))) {
|
|
543
|
+
return { behavior: "allow" };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Bash — check against safe command patterns
|
|
548
|
+
if (toolName === "Bash") {
|
|
549
|
+
const cmd = (input.command || "").trim();
|
|
550
|
+
const safePatterns = [
|
|
551
|
+
/^(npm|npx|node|bun|deno|pnpm|yarn)\s/,
|
|
552
|
+
/^git\s+(status|log|diff|branch|show|remote|fetch|stash)/,
|
|
553
|
+
/^(ls|pwd|which|whoami|date|uname|cat|head|tail|wc)\b/,
|
|
554
|
+
/^(cd|echo|printf)\s/,
|
|
555
|
+
/^(curl|wget)\s/,
|
|
556
|
+
/^(python|python3|pip)\s/,
|
|
557
|
+
/^(cargo|rustc|go)\s/,
|
|
558
|
+
/^(make|cmake)\b/,
|
|
559
|
+
/^(docker|podman)\s+(ps|images|logs|inspect|compose\s+(ps|logs))/,
|
|
560
|
+
/^(tmux|screen)\s+(list|ls|has-session)/,
|
|
561
|
+
/^mkdir\s/,
|
|
562
|
+
/^cp\s/,
|
|
563
|
+
/^mv\s/,
|
|
564
|
+
/^touch\s/,
|
|
565
|
+
/^chmod\s/,
|
|
566
|
+
/^test\s/,
|
|
567
|
+
/^\[/,
|
|
568
|
+
];
|
|
569
|
+
if (safePatterns.some(p => p.test(cmd))) {
|
|
570
|
+
return { behavior: "allow" };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// --- Hard deny list ---
|
|
575
|
+
if (toolName === "Bash") {
|
|
576
|
+
const cmd = (input.command || "").trim();
|
|
577
|
+
const denyPatterns = [
|
|
578
|
+
/\brm\s+-rf\s+[\/~]/, // rm -rf broad paths
|
|
579
|
+
/\bsudo\b/, // privilege escalation
|
|
580
|
+
/\bsu\s/, // switch user
|
|
581
|
+
/\bgit\s+push\s+.*--force\s.*(main|master)/, // force push to main
|
|
582
|
+
/\b(\.ssh|\.gnupg)\b/, // sensitive directories
|
|
583
|
+
];
|
|
584
|
+
for (const p of denyPatterns) {
|
|
585
|
+
if (p.test(cmd)) {
|
|
586
|
+
return { behavior: "deny", message: `Blocked by policy: ${cmd.slice(0, 100)}` };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// --- Ambiguous: escalate to dashboard ---
|
|
592
|
+
try {
|
|
593
|
+
const serverUrl = config.server || "http://localhost:4243";
|
|
594
|
+
const apiKey = config.apiKey || "";
|
|
595
|
+
const resp = await fetch(`${serverUrl}/api/permissions/request`, {
|
|
596
|
+
method: "POST",
|
|
597
|
+
headers: {
|
|
598
|
+
"Content-Type": "application/json",
|
|
599
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
600
|
+
},
|
|
601
|
+
body: JSON.stringify({
|
|
602
|
+
workspace: config.workspace,
|
|
603
|
+
tool_name: toolName,
|
|
604
|
+
tool_input: input,
|
|
605
|
+
}),
|
|
606
|
+
});
|
|
607
|
+
if (resp.ok) {
|
|
608
|
+
const data = await resp.json();
|
|
609
|
+
if (data.allow) {
|
|
610
|
+
return { behavior: "allow" };
|
|
611
|
+
}
|
|
612
|
+
return { behavior: "deny", message: data.reason || "Denied by human" };
|
|
613
|
+
}
|
|
614
|
+
} catch (err) {
|
|
615
|
+
console.error(`[policy] Dashboard escalation failed: ${err.message}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Default deny if escalation fails
|
|
619
|
+
return { behavior: "deny", message: "Could not reach dashboard for approval" };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
|
|
506
623
|
// --- Server setup & dispatch -------------------------------------------------
|
|
507
624
|
|
|
508
625
|
const server = new Server(
|
|
@@ -521,7 +638,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
521
638
|
} catch {
|
|
522
639
|
// If settings unavailable, hide telegram tools
|
|
523
640
|
}
|
|
524
|
-
const allTools = [...tools, ...(showTelegram ? [...telegramTools, ...primaryAgentTools] : [])];
|
|
641
|
+
const allTools = [...tools, ...policyTools, ...(showTelegram ? [...telegramTools, ...primaryAgentTools] : [])];
|
|
525
642
|
return { tools: allTools };
|
|
526
643
|
});
|
|
527
644
|
|
|
@@ -817,13 +934,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
817
934
|
}
|
|
818
935
|
break;
|
|
819
936
|
}
|
|
820
|
-
// ---
|
|
821
|
-
case "
|
|
822
|
-
|
|
823
|
-
if (!args.keys) { result = { error: "keys is required" }; break; }
|
|
824
|
-
result = await client.injectKeys(args.workspace, args.keys);
|
|
937
|
+
// --- Policy evaluation (permission-prompt-tool for stream-json agents) ---
|
|
938
|
+
case "policy_evaluate": {
|
|
939
|
+
result = await evaluatePermission(args.tool_name, args.input || {});
|
|
825
940
|
break;
|
|
826
941
|
}
|
|
942
|
+
|
|
827
943
|
case "fathom_telegram_send_voice": {
|
|
828
944
|
const voiceContactArg = args.contact;
|
|
829
945
|
if (!voiceContactArg) { result = { error: "contact is required" }; break; }
|
package/src/server-client.js
CHANGED
|
@@ -309,14 +309,6 @@ export function createClient(config) {
|
|
|
309
309
|
});
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
// --- Session injection (policy gate) ----------------------------------------
|
|
313
|
-
|
|
314
|
-
async function injectKeys(targetWorkspace, keys) {
|
|
315
|
-
return request("POST", `/api/session/${encodeURIComponent(targetWorkspace)}/inject`, {
|
|
316
|
-
body: { keys },
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
312
|
// --- Settings --------------------------------------------------------------
|
|
321
313
|
|
|
322
314
|
async function getSettings() {
|
|
@@ -381,7 +373,6 @@ export function createClient(config) {
|
|
|
381
373
|
telegramSendVoice,
|
|
382
374
|
telegramStatus,
|
|
383
375
|
speak,
|
|
384
|
-
injectKeys,
|
|
385
376
|
getSettings,
|
|
386
377
|
getApiKey,
|
|
387
378
|
rotateKey,
|
package/src/ws-connection.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* WebSocket push channel — receives server-pushed messages and handles them locally.
|
|
3
3
|
*
|
|
4
4
|
* Connects to fathom-server's /ws/agent/{workspace} endpoint. Receives:
|
|
5
|
-
* - inject / ping_fire → tmux send-keys into local pane
|
|
6
5
|
* - image → cache base64 data to .fathom/telegram-cache/
|
|
7
6
|
* - ping → respond with pong
|
|
8
7
|
*
|
|
8
|
+
* Stream-json agents handle inject/ping_fire via subprocess stdin — the server
|
|
9
|
+
* writes directly, so no tmux injection is needed here.
|
|
10
|
+
*
|
|
9
11
|
* Auto-reconnects with exponential backoff (1s → 60s cap).
|
|
10
|
-
* HTTP heartbeat still runs separately for backwards compat with old servers.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import { execSync } from "child_process";
|
|
14
14
|
import fs from "fs";
|
|
15
15
|
import os from "os";
|
|
16
16
|
import path from "path";
|
|
@@ -96,8 +96,8 @@ export function createWSConnection(config) {
|
|
|
96
96
|
|
|
97
97
|
case "inject":
|
|
98
98
|
case "ping_fire":
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// Stream-json agents handle injection via subprocess stdin on the server side
|
|
100
|
+
console.error(`[ws] received ${msg.type} (${(msg.text || "").length} chars) — handled by server subprocess`);
|
|
101
101
|
break;
|
|
102
102
|
|
|
103
103
|
case "image":
|
|
@@ -160,50 +160,6 @@ export function createWSConnection(config) {
|
|
|
160
160
|
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// ── Message injection ───────────────────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
function injectMessage(text) {
|
|
166
|
-
if (!text) return;
|
|
167
|
-
injectToTmux(text);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function injectToTmux(text) {
|
|
171
|
-
if (!text) return;
|
|
172
|
-
const pane = resolvePaneTarget();
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
// Send the text literally, pause for tmux to render, then press Enter
|
|
176
|
-
execSync(`tmux send-keys -t ${shellEscape(pane)} -l ${shellEscape(text)}`, {
|
|
177
|
-
timeout: 5000,
|
|
178
|
-
stdio: "ignore",
|
|
179
|
-
});
|
|
180
|
-
execSync("sleep 2", { timeout: 5000 });
|
|
181
|
-
execSync(`tmux send-keys -t ${shellEscape(pane)} Enter`, {
|
|
182
|
-
timeout: 5000,
|
|
183
|
-
stdio: "ignore",
|
|
184
|
-
});
|
|
185
|
-
} catch (err) {
|
|
186
|
-
console.error(`[ws] tmux inject failed for ${pane}: ${err.message}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function resolvePaneTarget() {
|
|
191
|
-
// Check for explicit pane ID file
|
|
192
|
-
const paneIdFile = path.join(os.homedir(), ".config", "fathom", `${workspace}-pane-id`);
|
|
193
|
-
try {
|
|
194
|
-
const paneId = fs.readFileSync(paneIdFile, "utf-8").trim();
|
|
195
|
-
if (paneId) return paneId;
|
|
196
|
-
} catch {
|
|
197
|
-
// Fall through to default
|
|
198
|
-
}
|
|
199
|
-
return `${workspace}_fathom-session`;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function shellEscape(s) {
|
|
203
|
-
// Escape for shell — wrap in single quotes, escape internal single quotes
|
|
204
|
-
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
205
|
-
}
|
|
206
|
-
|
|
207
163
|
// ── Image cache ─────────────────────────────────────────────────────────────
|
|
208
164
|
|
|
209
165
|
function cacheImage(msg) {
|