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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fathom-mcp",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,46 +1,53 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # fathom-start.sh — Launch an agent in a correctly-named tmux session.
3
+ # fathom-start.sh — Launch an agent session.
4
4
  #
5
- # Reads .fathom.json for workspace name and agent, creates
6
- # {workspace}_fathom-session, saves pane ID for WebSocket push injection.
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
- # Usage:
9
- # fathom-start.sh Start agent, save pane ID, attach
10
- # fathom-start.sh --detach Start agent, save pane ID, don't attach
11
- # fathom-start.sh --agent X Override agent (claude-code|codex|gemini|opencode)
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 [--attach] [--detach] [--agent NAME] [--kill] [--status]"
42
+ echo "Usage: fathom-start.sh [FLAGS]"
40
43
  echo ""
41
- echo " (default) Start agent in tmux, save pane ID, attach"
42
- echo " --detach Start but don't attach"
43
- echo " --agent X Override agent: claude-code, codex, gemini, opencode"
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 command ─────────────────────────────────────────────────────
117
+ # ── Resolve agent type ────────────────────────────────────────────────────────
109
118
 
110
- resolve_agent_cmd() {
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
- case "$agent" in
157
+ # ── Resolve agent command ─────────────────────────────────────────────────────
158
+
159
+ resolve_agent_cmd() {
160
+ case "$AGENT" in
120
161
  claude-code)
121
- echo "claude --model opus --permission-mode bypassPermissions"
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 '$agent', falling back to claude" >&2
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
- else
178
- echo "Session not running: $SESSION"
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");
@@ -89,7 +89,7 @@ export function createWSConnection(config) {
89
89
 
90
90
  case "inject":
91
91
  case "ping_fire":
92
- injectToTmux(msg.text || "");
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
- // ── tmux injection ──────────────────────────────────────────────────────────
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;