fathom-mcp 0.5.5 → 0.5.7
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 +168 -30
- package/src/cli.js +14 -6
- package/src/ws-connection.js +37 -2
package/package.json
CHANGED
package/scripts/fathom-start.sh
CHANGED
|
@@ -1,46 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# fathom-start.sh — Launch an agent
|
|
3
|
+
# fathom-start.sh — Launch an agent session.
|
|
4
4
|
#
|
|
5
|
-
# Reads .fathom.json for workspace name and agent
|
|
6
|
-
#
|
|
5
|
+
# Reads .fathom.json for workspace name and agent. For interactive agents
|
|
6
|
+
# (claude-code, codex, gemini, opencode), wraps in a tmux session. For
|
|
7
|
+
# headless agents (claude-sdk), spawns a direct process with PID tracking.
|
|
7
8
|
#
|
|
8
|
-
#
|
|
9
|
-
# fathom-start.sh
|
|
10
|
-
# fathom-start.sh --
|
|
11
|
-
#
|
|
9
|
+
# Claude agents require an explicit mode flag:
|
|
10
|
+
# fathom-start.sh --claude-code-w-tmux Interactive Claude Code in tmux
|
|
11
|
+
# fathom-start.sh --claude-sdk Headless Claude SDK, no tmux
|
|
12
|
+
#
|
|
13
|
+
# Other usage:
|
|
14
|
+
# fathom-start.sh Start agent (non-claude agents auto-detect)
|
|
15
|
+
# fathom-start.sh --detach Start agent, don't attach to tmux
|
|
16
|
+
# fathom-start.sh --agent X Override agent
|
|
12
17
|
# fathom-start.sh --kill Kill existing session
|
|
13
18
|
# fathom-start.sh --status Show session status
|
|
14
19
|
|
|
15
20
|
set -euo pipefail
|
|
16
21
|
|
|
17
|
-
if ! command -v tmux &>/dev/null; then
|
|
18
|
-
echo "Error: tmux is not installed. Install it first:" >&2
|
|
19
|
-
echo " apt install tmux | brew install tmux | dnf install tmux" >&2
|
|
20
|
-
exit 1
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
22
|
# ── Defaults ──────────────────────────────────────────────────────────────────
|
|
24
23
|
|
|
25
24
|
ATTACH=true
|
|
26
25
|
AGENT_OVERRIDE=""
|
|
27
26
|
ACTION="start"
|
|
27
|
+
MODE_FLAG=""
|
|
28
|
+
USE_TMUX=true
|
|
28
29
|
|
|
29
30
|
# ── Parse flags ───────────────────────────────────────────────────────────────
|
|
30
31
|
|
|
31
32
|
while [[ $# -gt 0 ]]; do
|
|
32
33
|
case "$1" in
|
|
34
|
+
--claude-code-w-tmux) MODE_FLAG="claude-code-w-tmux"; shift ;;
|
|
35
|
+
--claude-sdk) MODE_FLAG="claude-sdk"; shift ;;
|
|
33
36
|
--detach) ATTACH=false; shift ;;
|
|
34
37
|
--attach) ATTACH=true; shift ;;
|
|
35
38
|
--agent) AGENT_OVERRIDE="$2"; shift 2 ;;
|
|
36
39
|
--kill) ACTION="kill"; shift ;;
|
|
37
40
|
--status) ACTION="status"; shift ;;
|
|
38
41
|
-h|--help)
|
|
39
|
-
echo "Usage: fathom-start.sh [
|
|
42
|
+
echo "Usage: fathom-start.sh [FLAGS]"
|
|
40
43
|
echo ""
|
|
41
|
-
echo "
|
|
42
|
-
echo " --
|
|
43
|
-
echo " --
|
|
44
|
+
echo "Mode flags (required for Claude agents):"
|
|
45
|
+
echo " --claude-code-w-tmux Interactive Claude Code in tmux"
|
|
46
|
+
echo " --claude-sdk Headless Claude SDK, no tmux"
|
|
47
|
+
echo ""
|
|
48
|
+
echo "Other flags:"
|
|
49
|
+
echo " --detach Start but don't attach to tmux"
|
|
50
|
+
echo " --agent X Override agent: claude-code, claude-sdk, codex, gemini, opencode"
|
|
44
51
|
echo " --kill Kill existing session"
|
|
45
52
|
echo " --status Show if session is running"
|
|
46
53
|
exit 0
|
|
@@ -104,10 +111,12 @@ fi
|
|
|
104
111
|
SESSION="${WORKSPACE}_fathom-session"
|
|
105
112
|
PANE_DIR="$HOME/.config/fathom"
|
|
106
113
|
PANE_FILE="$PANE_DIR/${WORKSPACE}-pane-id"
|
|
114
|
+
PID_FILE="$PROJECT_DIR/.fathom/agent.pid"
|
|
115
|
+
LOG_FILE="$PROJECT_DIR/.fathom/agent.log"
|
|
107
116
|
|
|
108
|
-
# ── Resolve agent
|
|
117
|
+
# ── Resolve agent type ────────────────────────────────────────────────────────
|
|
109
118
|
|
|
110
|
-
|
|
119
|
+
resolve_agent() {
|
|
111
120
|
local agent="${AGENT_OVERRIDE:-}"
|
|
112
121
|
if [[ -z "$agent" ]]; then
|
|
113
122
|
agent=$(read_json_array_first "$CONFIG_FILE" "agents")
|
|
@@ -115,10 +124,53 @@ resolve_agent_cmd() {
|
|
|
115
124
|
if [[ -z "$agent" ]]; then
|
|
116
125
|
agent="claude-code"
|
|
117
126
|
fi
|
|
127
|
+
echo "$agent"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
AGENT=$(resolve_agent)
|
|
131
|
+
|
|
132
|
+
# ── Determine execution mode (tmux vs headless) ──────────────────────────────
|
|
133
|
+
# Must happen in the main shell, not inside $(...) subshell.
|
|
134
|
+
|
|
135
|
+
case "$AGENT" in
|
|
136
|
+
claude-code)
|
|
137
|
+
if [[ "$MODE_FLAG" == "claude-sdk" ]]; then
|
|
138
|
+
USE_TMUX=false
|
|
139
|
+
elif [[ -z "$MODE_FLAG" ]]; then
|
|
140
|
+
echo "Error: Claude Code requires an explicit mode flag:" >&2
|
|
141
|
+
echo " fathom-start.sh --claude-code-w-tmux Interactive Claude in tmux" >&2
|
|
142
|
+
echo " fathom-start.sh --claude-sdk Headless Claude SDK" >&2
|
|
143
|
+
echo "" >&2
|
|
144
|
+
echo "For local agents managed by fathom-server, use the dashboard restart button instead." >&2
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
;;
|
|
148
|
+
claude-sdk)
|
|
149
|
+
if [[ "$MODE_FLAG" == "claude-code-w-tmux" ]]; then
|
|
150
|
+
USE_TMUX=true
|
|
151
|
+
else
|
|
152
|
+
USE_TMUX=false
|
|
153
|
+
fi
|
|
154
|
+
;;
|
|
155
|
+
esac
|
|
118
156
|
|
|
119
|
-
|
|
157
|
+
# ── Resolve agent command ─────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
resolve_agent_cmd() {
|
|
160
|
+
case "$AGENT" in
|
|
120
161
|
claude-code)
|
|
121
|
-
|
|
162
|
+
if [[ "$USE_TMUX" == false ]]; then
|
|
163
|
+
echo "claude -p --permission-mode bypassPermissions --no-user-prompt --output-format stream-json"
|
|
164
|
+
else
|
|
165
|
+
echo "claude --model opus --permission-mode bypassPermissions"
|
|
166
|
+
fi
|
|
167
|
+
;;
|
|
168
|
+
claude-sdk)
|
|
169
|
+
if [[ "$USE_TMUX" == true ]]; then
|
|
170
|
+
echo "claude --model opus --permission-mode bypassPermissions"
|
|
171
|
+
else
|
|
172
|
+
echo "claude -p --permission-mode bypassPermissions --no-user-prompt --output-format stream-json"
|
|
173
|
+
fi
|
|
122
174
|
;;
|
|
123
175
|
codex)
|
|
124
176
|
echo "codex"
|
|
@@ -130,7 +182,7 @@ resolve_agent_cmd() {
|
|
|
130
182
|
echo "opencode"
|
|
131
183
|
;;
|
|
132
184
|
*)
|
|
133
|
-
echo "Warning: Unknown agent '$
|
|
185
|
+
echo "Warning: Unknown agent '$AGENT', falling back to claude" >&2
|
|
134
186
|
echo "claude"
|
|
135
187
|
;;
|
|
136
188
|
esac
|
|
@@ -148,38 +200,127 @@ save_pane_id() {
|
|
|
148
200
|
fi
|
|
149
201
|
}
|
|
150
202
|
|
|
151
|
-
# ── Session check
|
|
203
|
+
# ── Session/process check ────────────────────────────────────────────────────
|
|
152
204
|
|
|
153
205
|
session_exists() {
|
|
154
206
|
tmux has-session -t "$SESSION" 2>/dev/null
|
|
155
207
|
}
|
|
156
208
|
|
|
209
|
+
headless_is_running() {
|
|
210
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
211
|
+
local pid
|
|
212
|
+
pid=$(cat "$PID_FILE" 2>/dev/null)
|
|
213
|
+
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
214
|
+
return 0
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
return 1
|
|
218
|
+
}
|
|
219
|
+
|
|
157
220
|
# ── Actions ───────────────────────────────────────────────────────────────────
|
|
158
221
|
|
|
159
222
|
do_status() {
|
|
160
223
|
echo "Workspace: $WORKSPACE"
|
|
161
224
|
echo "Session: $SESSION"
|
|
162
225
|
if session_exists; then
|
|
163
|
-
echo "Status: running"
|
|
226
|
+
echo "Status: running (tmux)"
|
|
164
227
|
if [[ -f "$PANE_FILE" ]]; then
|
|
165
228
|
echo "Pane ID: $(cat "$PANE_FILE")"
|
|
166
229
|
fi
|
|
230
|
+
elif headless_is_running; then
|
|
231
|
+
echo "Status: running (headless)"
|
|
232
|
+
echo "PID: $(cat "$PID_FILE")"
|
|
233
|
+
echo "Log: $LOG_FILE"
|
|
234
|
+
if [[ -p "$PROJECT_DIR/.fathom/agent.pipe" ]]; then
|
|
235
|
+
echo "Pipe: $PROJECT_DIR/.fathom/agent.pipe"
|
|
236
|
+
fi
|
|
167
237
|
else
|
|
168
238
|
echo "Status: not running"
|
|
169
239
|
fi
|
|
170
240
|
}
|
|
171
241
|
|
|
172
242
|
do_kill() {
|
|
243
|
+
local killed=false
|
|
173
244
|
if session_exists; then
|
|
174
245
|
tmux kill-session -t "$SESSION"
|
|
175
246
|
rm -f "$PANE_FILE"
|
|
176
|
-
echo "Killed session: $SESSION"
|
|
177
|
-
|
|
178
|
-
|
|
247
|
+
echo "Killed tmux session: $SESSION"
|
|
248
|
+
killed=true
|
|
249
|
+
fi
|
|
250
|
+
if headless_is_running; then
|
|
251
|
+
local pid
|
|
252
|
+
pid=$(cat "$PID_FILE")
|
|
253
|
+
kill "$pid" 2>/dev/null || true
|
|
254
|
+
rm -f "$PID_FILE"
|
|
255
|
+
echo "Killed headless process: PID $pid"
|
|
256
|
+
killed=true
|
|
257
|
+
fi
|
|
258
|
+
# Clean up keeper process and FIFO
|
|
259
|
+
local keeper_pid_file="$PROJECT_DIR/.fathom/agent-keeper.pid"
|
|
260
|
+
if [[ -f "$keeper_pid_file" ]]; then
|
|
261
|
+
local keeper_pid
|
|
262
|
+
keeper_pid=$(cat "$keeper_pid_file")
|
|
263
|
+
kill "$keeper_pid" 2>/dev/null || true
|
|
264
|
+
rm -f "$keeper_pid_file"
|
|
265
|
+
fi
|
|
266
|
+
rm -f "$PROJECT_DIR/.fathom/agent.pipe"
|
|
267
|
+
if [[ "$killed" == false ]]; then
|
|
268
|
+
echo "No running session found for: $WORKSPACE"
|
|
179
269
|
fi
|
|
180
270
|
}
|
|
181
271
|
|
|
182
272
|
do_start() {
|
|
273
|
+
local agent_cmd
|
|
274
|
+
agent_cmd=$(resolve_agent_cmd)
|
|
275
|
+
|
|
276
|
+
if [[ "$USE_TMUX" == false ]]; then
|
|
277
|
+
# ── Headless mode — FIFO-based stdin ──
|
|
278
|
+
if headless_is_running; then
|
|
279
|
+
echo "Headless agent already running (PID $(cat "$PID_FILE"))"
|
|
280
|
+
echo "Log: $LOG_FILE"
|
|
281
|
+
return 0
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
echo "Starting headless: $agent_cmd"
|
|
285
|
+
echo "Dir: $PROJECT_DIR"
|
|
286
|
+
echo "Logs: $LOG_FILE"
|
|
287
|
+
|
|
288
|
+
cd "$PROJECT_DIR"
|
|
289
|
+
unset CLAUDECODE 2>/dev/null || true
|
|
290
|
+
mkdir -p .fathom
|
|
291
|
+
|
|
292
|
+
local pipe_file="$PROJECT_DIR/.fathom/agent.pipe"
|
|
293
|
+
|
|
294
|
+
# Clean up stale pipe
|
|
295
|
+
rm -f "$pipe_file"
|
|
296
|
+
mkfifo "$pipe_file"
|
|
297
|
+
|
|
298
|
+
# Keep a writer FD open so the pipe never sends EOF to the reader.
|
|
299
|
+
# Without this, the agent reads EOF and exits when no writer is connected.
|
|
300
|
+
sleep infinity > "$pipe_file" &
|
|
301
|
+
local keeper_pid=$!
|
|
302
|
+
|
|
303
|
+
# Start agent reading from the FIFO, logging stdout/stderr
|
|
304
|
+
$agent_cmd < "$pipe_file" >> "$LOG_FILE" 2>&1 &
|
|
305
|
+
local pid=$!
|
|
306
|
+
|
|
307
|
+
echo "$pid" > "$PID_FILE"
|
|
308
|
+
echo "$keeper_pid" > "$PROJECT_DIR/.fathom/agent-keeper.pid"
|
|
309
|
+
|
|
310
|
+
echo "PID: $pid"
|
|
311
|
+
echo "Pipe: $pipe_file"
|
|
312
|
+
echo "Started. View logs: tail -f $LOG_FILE"
|
|
313
|
+
echo "Inject: echo 'your message' > $pipe_file"
|
|
314
|
+
return 0
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# ── Interactive mode — tmux session ──
|
|
318
|
+
if ! command -v tmux &>/dev/null; then
|
|
319
|
+
echo "Error: tmux is not installed. Install it first:" >&2
|
|
320
|
+
echo " apt install tmux | brew install tmux | dnf install tmux" >&2
|
|
321
|
+
exit 1
|
|
322
|
+
fi
|
|
323
|
+
|
|
183
324
|
if session_exists; then
|
|
184
325
|
echo "Session already running: $SESSION"
|
|
185
326
|
save_pane_id
|
|
@@ -189,9 +330,6 @@ do_start() {
|
|
|
189
330
|
return 0
|
|
190
331
|
fi
|
|
191
332
|
|
|
192
|
-
local agent_cmd
|
|
193
|
-
agent_cmd=$(resolve_agent_cmd)
|
|
194
|
-
|
|
195
333
|
echo "Starting: $SESSION"
|
|
196
334
|
echo "Agent: $agent_cmd"
|
|
197
335
|
echo "Dir: $PROJECT_DIR"
|
package/src/cli.js
CHANGED
|
@@ -130,6 +130,7 @@ function copyScripts(targetDir) {
|
|
|
130
130
|
|
|
131
131
|
const HEADLESS_CMDS = {
|
|
132
132
|
"claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
133
|
+
"claude-sdk": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
133
134
|
"codex": (prompt) => ["codex", "exec", prompt],
|
|
134
135
|
"gemini": (prompt) => ["gemini", prompt],
|
|
135
136
|
"opencode": (prompt) => ["opencode", "run", prompt],
|
|
@@ -306,6 +307,13 @@ const AGENTS = {
|
|
|
306
307
|
hasHooks: false,
|
|
307
308
|
nextSteps: "Run `opencode` in this directory — fathom tools load automatically.",
|
|
308
309
|
},
|
|
310
|
+
"claude-sdk": {
|
|
311
|
+
name: "Claude SDK (headless)",
|
|
312
|
+
detect: (cwd) => fs.existsSync(path.join(cwd, ".claude")),
|
|
313
|
+
configWriter: writeMcpJson,
|
|
314
|
+
hasHooks: true,
|
|
315
|
+
nextSteps: "Headless Claude Code — no TUI, structured I/O. Start with: npx fathom-mcp start",
|
|
316
|
+
},
|
|
309
317
|
};
|
|
310
318
|
|
|
311
319
|
// Exported for testing
|
|
@@ -381,7 +389,7 @@ async function runInit(flags = {}) {
|
|
|
381
389
|
const agent = AGENTS[key];
|
|
382
390
|
const isDetected = detected.includes(key);
|
|
383
391
|
const mark = isDetected ? "✓" : " ";
|
|
384
|
-
const markers = { "claude-code": ".claude/", "codex": ".codex/", "gemini": ".gemini/", "opencode": "opencode.json" };
|
|
392
|
+
const markers = { "claude-code": ".claude/", "codex": ".codex/", "gemini": ".gemini/", "opencode": "opencode.json", "claude-sdk": ".claude/" };
|
|
385
393
|
const hint = isDetected ? ` (${markers[key] || key} found)` : "";
|
|
386
394
|
console.log(` ${mark} ${agent.name}${hint}`);
|
|
387
395
|
}
|
|
@@ -480,8 +488,8 @@ async function runInit(flags = {}) {
|
|
|
480
488
|
? (nonInteractive ? "vault" : await ask(rl, " Vault subdirectory", "vault"))
|
|
481
489
|
: "vault";
|
|
482
490
|
|
|
483
|
-
// 9. Hooks — ask if any hook-supporting agent is selected (Claude Code, Gemini CLI)
|
|
484
|
-
const hasClaude = selectedAgents.includes("claude-code");
|
|
491
|
+
// 9. Hooks — ask if any hook-supporting agent is selected (Claude Code, Claude SDK, Gemini CLI)
|
|
492
|
+
const hasClaude = selectedAgents.includes("claude-code") || selectedAgents.includes("claude-sdk");
|
|
485
493
|
const hasGemini = selectedAgents.includes("gemini");
|
|
486
494
|
const hasHookAgent = hasClaude || hasGemini;
|
|
487
495
|
let enableRecallHook = false;
|
|
@@ -712,7 +720,7 @@ async function runInit(flags = {}) {
|
|
|
712
720
|
}
|
|
713
721
|
|
|
714
722
|
function printInstructionsFallback(agentMdPath, selectedAgents) {
|
|
715
|
-
const hasNonClaude = selectedAgents.some((k) => k !== "claude-code");
|
|
723
|
+
const hasNonClaude = selectedAgents.some((k) => k !== "claude-code" && k !== "claude-sdk");
|
|
716
724
|
const docTarget = hasNonClaude
|
|
717
725
|
? "your CLAUDE.md, AGENTS.md, or equivalent"
|
|
718
726
|
: "your CLAUDE.md";
|
|
@@ -801,8 +809,8 @@ async function runUpdate() {
|
|
|
801
809
|
const sessionStartCmd = "bash .fathom/scripts/fathom-sessionstart.sh";
|
|
802
810
|
const registeredHooks = [];
|
|
803
811
|
|
|
804
|
-
// Claude Code
|
|
805
|
-
const hasClaude = agents.includes("claude-code")
|
|
812
|
+
// Claude Code / Claude SDK
|
|
813
|
+
const hasClaude = agents.includes("claude-code") || agents.includes("claude-sdk")
|
|
806
814
|
|| fs.existsSync(path.join(projectDir, ".claude"));
|
|
807
815
|
if (hasClaude) {
|
|
808
816
|
const settingsPath = path.join(projectDir, ".claude", "settings.local.json");
|
package/src/ws-connection.js
CHANGED
|
@@ -89,7 +89,7 @@ export function createWSConnection(config) {
|
|
|
89
89
|
|
|
90
90
|
case "inject":
|
|
91
91
|
case "ping_fire":
|
|
92
|
-
|
|
92
|
+
injectMessage(msg.text || "");
|
|
93
93
|
break;
|
|
94
94
|
|
|
95
95
|
case "image":
|
|
@@ -148,7 +148,42 @@ export function createWSConnection(config) {
|
|
|
148
148
|
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
// ──
|
|
151
|
+
// ── Message injection ───────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
function injectMessage(text) {
|
|
154
|
+
if (!text) return;
|
|
155
|
+
|
|
156
|
+
if (agent === "claude-sdk") {
|
|
157
|
+
// Headless SDK agents use a FIFO pipe for stdin injection.
|
|
158
|
+
// Server-managed (local): server handles via persistent_session._inject_headless()
|
|
159
|
+
// Standalone (remote): write to .fathom/agent.pipe FIFO
|
|
160
|
+
injectToFifo(text);
|
|
161
|
+
} else {
|
|
162
|
+
injectToTmux(text);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function injectToFifo(text) {
|
|
167
|
+
if (!text) return;
|
|
168
|
+
|
|
169
|
+
const projectDir = config._projectDir || process.cwd();
|
|
170
|
+
const pipePath = path.join(projectDir, ".fathom", "agent.pipe");
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const stat = fs.statSync(pipePath);
|
|
174
|
+
if (stat.isFIFO()) {
|
|
175
|
+
// Write to FIFO — opens, writes, closes in one shot.
|
|
176
|
+
// The newline terminates the message for claude -p.
|
|
177
|
+
fs.appendFileSync(pipePath, text + "\n");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// FIFO not found — fall back to tmux
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fallback: try tmux injection (e.g., started with --claude-code-w-tmux override)
|
|
185
|
+
injectToTmux(text);
|
|
186
|
+
}
|
|
152
187
|
|
|
153
188
|
function injectToTmux(text) {
|
|
154
189
|
if (!text) return;
|