fathom-mcp 0.5.4 → 0.5.6
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 +140 -30
- package/src/cli.js +14 -6
- package/src/server-client.js +4 -0
- package/src/ws-connection.js +17 -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]"
|
|
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"
|
|
40
47
|
echo ""
|
|
41
|
-
echo "
|
|
42
|
-
echo " --detach Start but don't attach"
|
|
43
|
-
echo " --agent X Override agent: claude-code, codex, gemini, opencode"
|
|
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
|
|
156
|
+
|
|
157
|
+
# ── Resolve agent command ─────────────────────────────────────────────────────
|
|
118
158
|
|
|
119
|
-
|
|
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,99 @@ 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"
|
|
167
234
|
else
|
|
168
235
|
echo "Status: not running"
|
|
169
236
|
fi
|
|
170
237
|
}
|
|
171
238
|
|
|
172
239
|
do_kill() {
|
|
240
|
+
local killed=false
|
|
173
241
|
if session_exists; then
|
|
174
242
|
tmux kill-session -t "$SESSION"
|
|
175
243
|
rm -f "$PANE_FILE"
|
|
176
|
-
echo "Killed session: $SESSION"
|
|
177
|
-
|
|
178
|
-
|
|
244
|
+
echo "Killed tmux session: $SESSION"
|
|
245
|
+
killed=true
|
|
246
|
+
fi
|
|
247
|
+
if headless_is_running; then
|
|
248
|
+
local pid
|
|
249
|
+
pid=$(cat "$PID_FILE")
|
|
250
|
+
kill "$pid" 2>/dev/null || true
|
|
251
|
+
rm -f "$PID_FILE"
|
|
252
|
+
echo "Killed headless process: PID $pid"
|
|
253
|
+
killed=true
|
|
254
|
+
fi
|
|
255
|
+
if [[ "$killed" == false ]]; then
|
|
256
|
+
echo "No running session found for: $WORKSPACE"
|
|
179
257
|
fi
|
|
180
258
|
}
|
|
181
259
|
|
|
182
260
|
do_start() {
|
|
261
|
+
local agent_cmd
|
|
262
|
+
agent_cmd=$(resolve_agent_cmd)
|
|
263
|
+
|
|
264
|
+
if [[ "$USE_TMUX" == false ]]; then
|
|
265
|
+
# ── Headless mode — direct process, no tmux ──
|
|
266
|
+
if headless_is_running; then
|
|
267
|
+
echo "Headless agent already running (PID $(cat "$PID_FILE"))"
|
|
268
|
+
echo "Log: $LOG_FILE"
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
echo "Starting headless: $agent_cmd"
|
|
273
|
+
echo "Dir: $PROJECT_DIR"
|
|
274
|
+
echo "Logs: $LOG_FILE"
|
|
275
|
+
|
|
276
|
+
cd "$PROJECT_DIR"
|
|
277
|
+
unset CLAUDECODE 2>/dev/null || true
|
|
278
|
+
mkdir -p .fathom
|
|
279
|
+
|
|
280
|
+
# Run in background, log stdout/stderr
|
|
281
|
+
nohup $agent_cmd >> "$LOG_FILE" 2>&1 &
|
|
282
|
+
local pid=$!
|
|
283
|
+
echo "$pid" > "$PID_FILE"
|
|
284
|
+
echo "PID: $pid"
|
|
285
|
+
echo "Started. View logs: tail -f $LOG_FILE"
|
|
286
|
+
return 0
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# ── Interactive mode — tmux session ──
|
|
290
|
+
if ! command -v tmux &>/dev/null; then
|
|
291
|
+
echo "Error: tmux is not installed. Install it first:" >&2
|
|
292
|
+
echo " apt install tmux | brew install tmux | dnf install tmux" >&2
|
|
293
|
+
exit 1
|
|
294
|
+
fi
|
|
295
|
+
|
|
183
296
|
if session_exists; then
|
|
184
297
|
echo "Session already running: $SESSION"
|
|
185
298
|
save_pane_id
|
|
@@ -189,9 +302,6 @@ do_start() {
|
|
|
189
302
|
return 0
|
|
190
303
|
fi
|
|
191
304
|
|
|
192
|
-
local agent_cmd
|
|
193
|
-
agent_cmd=$(resolve_agent_cmd)
|
|
194
|
-
|
|
195
305
|
echo "Starting: $SESSION"
|
|
196
306
|
echo "Agent: $agent_cmd"
|
|
197
307
|
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/server-client.js
CHANGED
|
@@ -199,6 +199,8 @@ export function createClient(config) {
|
|
|
199
199
|
if (params.intervalMinutes != null) body.intervalMinutes = params.intervalMinutes;
|
|
200
200
|
if (params.singleFire != null) body.singleFire = params.singleFire;
|
|
201
201
|
if (params.contextSources != null) body.contextSources = params.contextSources;
|
|
202
|
+
if (params.schedule !== undefined) body.schedule = params.schedule;
|
|
203
|
+
if (params.conditions != null) body.conditions = params.conditions;
|
|
202
204
|
return request("POST", "/api/activation/ping/routines", {
|
|
203
205
|
params: { workspace: ws },
|
|
204
206
|
body,
|
|
@@ -218,6 +220,8 @@ export function createClient(config) {
|
|
|
218
220
|
if (params.intervalMinutes != null) body.intervalMinutes = params.intervalMinutes;
|
|
219
221
|
if (params.singleFire != null) body.singleFire = params.singleFire;
|
|
220
222
|
if (params.contextSources != null) body.contextSources = params.contextSources;
|
|
223
|
+
if (params.schedule !== undefined) body.schedule = params.schedule;
|
|
224
|
+
if (params.conditions != null) body.conditions = params.conditions;
|
|
221
225
|
return request("POST", `/api/activation/ping/routines/${encodeURIComponent(routineId)}`, {
|
|
222
226
|
params: { workspace: ws },
|
|
223
227
|
body,
|
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,22 @@ 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
|
+
// For headless SDK agents managed by fathom-server, injection goes through
|
|
158
|
+
// the server's stdin pipe (persistent_session.inject_headless). The WS push
|
|
159
|
+
// channel delivers the message to the server, which handles the actual write.
|
|
160
|
+
// Fall back to tmux injection if the agent happens to be in a tmux session
|
|
161
|
+
// (e.g., started with --claude-code-w-tmux override).
|
|
162
|
+
injectToTmux(text);
|
|
163
|
+
} else {
|
|
164
|
+
injectToTmux(text);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
152
167
|
|
|
153
168
|
function injectToTmux(text) {
|
|
154
169
|
if (!text) return;
|