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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fathom-mcp",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
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]"
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 " (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"
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
156
+
157
+ # ── Resolve agent command ─────────────────────────────────────────────────────
118
158
 
119
- case "$agent" in
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,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
- else
178
- echo "Session not running: $SESSION"
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");
@@ -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,
@@ -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,22 @@ 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
+ // 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;