fathom-mcp 0.5.13 → 0.5.16

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.13",
3
+ "version": "0.5.16",
4
4
  "description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "main": "src/index.js",
10
10
  "files": [
11
11
  "src/",
12
- "scripts/",
12
+ "scripts/*.sh",
13
+ "scripts/*.py",
13
14
  "fathom-agents.md",
14
15
  "README.md",
15
16
  "CHANGELOG.md",
package/src/agents.js CHANGED
@@ -9,7 +9,7 @@ import fs from "fs";
9
9
  import path from "path";
10
10
  import { execSync, execFileSync } from "child_process";
11
11
 
12
- const CONFIG_DIR = path.join(process.env.HOME || "/tmp", ".config", "fathom-mcp");
12
+ const CONFIG_DIR = process.env.FATHOM_CONFIG_DIR || path.join(process.env.HOME || "/tmp", ".config", "fathom-mcp");
13
13
  const AGENTS_FILE = path.join(CONFIG_DIR, "agents.json");
14
14
 
15
15
  const EMPTY_CONFIG = { version: 1, agents: {} };
@@ -65,40 +65,15 @@ export function removeAgent(name) {
65
65
  export function isAgentRunning(name, entry) {
66
66
  if (entry.ssh) return "ssh";
67
67
 
68
- const mode = entry.executionMode || "tmux";
69
-
70
- if (mode === "tmux") {
71
- const session = `${name}_fathom-session`;
72
- try {
73
- execSync(`tmux has-session -t ${JSON.stringify(session)} 2>/dev/null`, {
74
- stdio: "pipe",
75
- });
76
- return "running";
77
- } catch {
78
- return "stopped";
79
- }
80
- }
81
-
82
- // headless — check pids directory for any live processes
83
- const pidsDir = path.join(entry.projectDir, ".fathom", "pids");
68
+ const session = `${name}_fathom-session`;
84
69
  try {
85
- const files = fs.readdirSync(pidsDir);
86
- for (const file of files) {
87
- try {
88
- const pid = parseInt(file, 10);
89
- if (!isNaN(pid)) {
90
- process.kill(pid, 0); // signal 0 = existence check
91
- return "running";
92
- }
93
- } catch {
94
- // Dead process — clean up stale PID file
95
- try { fs.unlinkSync(path.join(pidsDir, file)); } catch { /* */ }
96
- }
97
- }
70
+ execSync(`tmux has-session -t ${JSON.stringify(session)} 2>/dev/null`, {
71
+ stdio: "pipe",
72
+ });
73
+ return "running";
98
74
  } catch {
99
- // pids dir doesn't exist
75
+ return "stopped";
100
76
  }
101
- return "stopped";
102
77
  }
103
78
 
104
79
  // ── Start / Stop ────────────────────────────────────────────────────────────
@@ -106,29 +81,39 @@ export function isAgentRunning(name, entry) {
106
81
  /**
107
82
  * Default command per agent type + execution mode.
108
83
  */
109
- export function defaultCommand(agentType, executionMode) {
84
+ export function defaultCommand(agentType) {
110
85
  const cmds = {
111
- "claude-code": {
112
- tmux: "claude --model opus --permission-mode bypassPermissions",
113
- headless:
114
- "claude -p --permission-mode bypassPermissions --output-format stream-json",
115
- },
116
- "claude-sdk": {
117
- tmux: "claude --model opus --permission-mode bypassPermissions",
118
- headless:
119
- "claude -p --permission-mode bypassPermissions --output-format stream-json",
120
- },
121
- codex: { tmux: "codex", headless: "codex" },
122
- gemini: { tmux: "gemini", headless: "gemini" },
123
- opencode: { tmux: "opencode", headless: "opencode" },
86
+ "claude-code": "claude --model opus --permission-mode bypassPermissions",
87
+ codex: "codex",
88
+ gemini: "gemini",
89
+ opencode: "opencode",
124
90
  };
125
- const forType = cmds[agentType] || cmds["claude-code"];
126
- return forType[executionMode] || forType.tmux;
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
+ function hasPreviousConversation(projectDir) {
99
+ const encoded = projectDir.replace(/^\//, "").replace(/\//g, "-");
100
+ const convDir = path.join(process.env.HOME || "/tmp", ".claude", "projects", encoded);
101
+ try {
102
+ const files = fs.readdirSync(convDir);
103
+ return files.some((f) => f.endsWith(".jsonl"));
104
+ } catch {
105
+ return false;
106
+ }
127
107
  }
128
108
 
129
109
  export function startAgent(name, entry) {
130
- const mode = entry.executionMode || "tmux";
131
- const command = entry.command || defaultCommand(entry.agentType || "claude-code", mode);
110
+ let command = entry.command || defaultCommand(entry.agentType || "claude-code");
111
+
112
+ // --continue crashes if there's no previous conversation. Strip it for new agents.
113
+ if (command.includes("--continue") && !hasPreviousConversation(entry.projectDir)) {
114
+ command = command.replace(/\s*--continue\b/g, "");
115
+ }
116
+
132
117
  const session = `${name}_fathom-session`;
133
118
 
134
119
  // Build env
@@ -144,10 +129,6 @@ export function startAgent(name, entry) {
144
129
  return startAgentSSH(name, entry, command, env);
145
130
  }
146
131
 
147
- if (mode === "headless") {
148
- return startAgentHeadless(name, entry, command, env);
149
- }
150
-
151
132
  return startAgentTmux(name, entry, command, session, env);
152
133
  }
153
134
 
@@ -199,12 +180,6 @@ function savePaneId(name, session) {
199
180
  }
200
181
  }
201
182
 
202
- function startAgentHeadless(name, entry, command, env) {
203
- // Headless agents are per-invocation — no persistent process to start.
204
- // The server spawns fresh claude -p processes on demand.
205
- return { ok: true, message: `Headless agent "${name}" ready (per-invocation mode)` };
206
- }
207
-
208
183
  function startAgentSSH(name, entry, command, env) {
209
184
  const { host, user, key } = entry.ssh;
210
185
  const sshArgs = [];
@@ -248,33 +223,6 @@ export function stopAgent(name, entry) {
248
223
  // No tmux session
249
224
  }
250
225
 
251
- // Kill headless processes (per-invocation pids directory)
252
- const fathomDir = path.join(entry.projectDir, ".fathom");
253
- const pidsDir = path.join(fathomDir, "pids");
254
- try {
255
- const files = fs.readdirSync(pidsDir);
256
- for (const file of files) {
257
- try {
258
- const pid = parseInt(file, 10);
259
- if (!isNaN(pid)) {
260
- process.kill(pid);
261
- messages.push(`Killed headless process: PID ${pid}`);
262
- stopped = true;
263
- }
264
- } catch {
265
- // Already dead
266
- }
267
- try { fs.unlinkSync(path.join(pidsDir, file)); } catch { /* */ }
268
- }
269
- } catch {
270
- // pids dir doesn't exist
271
- }
272
-
273
- // Clean up legacy files
274
- for (const f of ["agent.pid", "agent-keeper.pid", "agent.pipe"]) {
275
- try { fs.unlinkSync(path.join(fathomDir, f)); } catch { /* */ }
276
- }
277
-
278
226
  if (!stopped) {
279
227
  return { ok: false, message: `No running session found for: ${name}` };
280
228
  }
@@ -289,13 +237,10 @@ export function stopAgent(name, entry) {
289
237
  */
290
238
  export function buildEntryFromConfig(projectDir, fathomConfig) {
291
239
  const agentType = fathomConfig.agents?.[0] || "claude-code";
292
- const isHeadless = agentType === "claude-sdk";
293
- const executionMode = isHeadless ? "headless" : "tmux";
294
240
  return {
295
241
  projectDir,
296
242
  agentType,
297
- executionMode,
298
- command: defaultCommand(agentType, executionMode),
243
+ command: defaultCommand(agentType),
299
244
  server: fathomConfig.server || "http://localhost:4243",
300
245
  apiKey: fathomConfig.apiKey || "",
301
246
  vault: fathomConfig.vault || "vault",
package/src/cli.js CHANGED
@@ -144,14 +144,6 @@ function copyScripts(targetDir) {
144
144
  }
145
145
  }
146
146
 
147
- // --- Headless agent integration ----------------------------------------------
148
-
149
- const HEADLESS_CMDS = {
150
- "claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
151
- "claude-sdk": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
152
- "gemini": (prompt) => ["gemini", prompt],
153
- };
154
-
155
147
  function buildIntegrationPrompt(blob) {
156
148
  return [
157
149
  "The following instructions were generated by fathom-mcp init for this project.",
@@ -180,8 +172,14 @@ function detectFathomServer() {
180
172
  }
181
173
  }
182
174
 
183
- function runAgentHeadless(agentKey, prompt) {
184
- const cmdBuilder = HEADLESS_CMDS[agentKey];
175
+ // Agent commands for non-interactive prompt integration (init --non-interactive)
176
+ const AGENT_PROMPT_CMDS = {
177
+ "claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
178
+ "gemini": (prompt) => ["gemini", prompt],
179
+ };
180
+
181
+ function runAgentPrompt(agentKey, prompt) {
182
+ const cmdBuilder = AGENT_PROMPT_CMDS[agentKey];
185
183
  if (!cmdBuilder) return null;
186
184
  const [cmd, ...args] = cmdBuilder(prompt);
187
185
  try {
@@ -265,13 +263,6 @@ const AGENTS = {
265
263
  hasHooks: true,
266
264
  nextSteps: 'Add to CLAUDE.md: `ToolSearch query="+fathom" max_results=20`',
267
265
  },
268
- "claude-sdk": {
269
- name: "Claude SDK (headless)",
270
- detect: (cwd) => fs.existsSync(path.join(cwd, ".claude")),
271
- configWriter: writeMcpJson,
272
- hasHooks: true,
273
- nextSteps: "Headless Claude Code — no TUI, structured I/O. Start with: npx fathom-mcp start",
274
- },
275
266
  "gemini": {
276
267
  name: "Gemini CLI",
277
268
  detect: (cwd) => fs.existsSync(path.join(cwd, ".gemini")),
@@ -358,7 +349,7 @@ async function runInit(flags = {}) {
358
349
  const agent = AGENTS[key];
359
350
  const isDetected = detected.includes(key);
360
351
  const mark = isDetected ? "✓" : " ";
361
- const markers = { "claude-code": ".claude/", "claude-sdk": ".claude/", "gemini": ".gemini/" };
352
+ const markers = { "claude-code": ".claude/", "gemini": ".gemini/" };
362
353
  const hint = isDetected ? ` (${markers[key] || key} found)` : "";
363
354
  console.log(` ${mark} ${agent.name}${hint}`);
364
355
  }
@@ -458,7 +449,7 @@ async function runInit(flags = {}) {
458
449
  : "vault";
459
450
 
460
451
  // 9. Hooks — ask if any hook-supporting agent is selected (Claude Code, Claude SDK, Gemini CLI)
461
- const hasClaude = selectedAgents.includes("claude-code") || selectedAgents.includes("claude-sdk");
452
+ const hasClaude = selectedAgents.includes("claude-code");
462
453
  const hasGemini = selectedAgents.includes("gemini");
463
454
  const hasHookAgent = hasClaude || hasGemini;
464
455
  let enableRecallHook = false;
@@ -584,6 +575,11 @@ async function runInit(flags = {}) {
584
575
  }
585
576
  }
586
577
 
578
+ // Register in CLI agent registry (for ls/start/stop)
579
+ const entry = buildEntryFromConfig(cwd, configData);
580
+ registryAddAgent(workspace, entry);
581
+ console.log(` ✓ Registered agent "${workspace}" in CLI registry`);
582
+
587
583
  // Context-aware next steps
588
584
  console.log(`\n Done! Fathom MCP is configured for workspace "${workspace}".`);
589
585
  console.log(` Vault mode: ${vaultMode}`);
@@ -628,12 +624,12 @@ async function runInit(flags = {}) {
628
624
 
629
625
  if (instructionsBlob) {
630
626
  const prompt = buildIntegrationPrompt(instructionsBlob);
631
- const cmdParts = HEADLESS_CMDS[primaryAgent]?.(prompt);
627
+ const cmdBuilder = AGENT_PROMPT_CMDS[primaryAgent];
632
628
 
633
629
  if (nonInteractive) {
634
- if (cmdParts) {
630
+ if (cmdBuilder) {
635
631
  console.log(`\n Integrating instructions via ${AGENTS[primaryAgent].name}...`);
636
- const result = runAgentHeadless(primaryAgent, prompt);
632
+ const result = runAgentPrompt(primaryAgent, prompt);
637
633
  if (result !== null) {
638
634
  console.log(result);
639
635
  } else {
@@ -644,9 +640,9 @@ async function runInit(flags = {}) {
644
640
  printInstructionsFallback(agentMdPath, selectedAgents);
645
641
  }
646
642
  } else {
647
- if (cmdParts) {
648
- const [cmd, ...args] = cmdParts;
649
- const flagArgs = args.slice(0, -1).join(" ");
643
+ if (cmdBuilder) {
644
+ const [cmd, ...sampleArgs] = cmdBuilder("<prompt>");
645
+ const flagArgs = sampleArgs.slice(0, -1).join(" ");
650
646
  const displayCmd = `${cmd} ${flagArgs} <prompt>`;
651
647
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
652
648
  console.log("\n" + "─".repeat(60));
@@ -659,7 +655,7 @@ async function runInit(flags = {}) {
659
655
 
660
656
  if (integrate) {
661
657
  console.log(`\n Running ${AGENTS[primaryAgent].name}...`);
662
- const result = runAgentHeadless(primaryAgent, prompt);
658
+ const result = runAgentPrompt(primaryAgent, prompt);
663
659
  if (result !== null) {
664
660
  console.log(result);
665
661
  } else {
@@ -679,7 +675,7 @@ async function runInit(flags = {}) {
679
675
  }
680
676
 
681
677
  function printInstructionsFallback(agentMdPath, selectedAgents) {
682
- const hasNonClaude = selectedAgents.some((k) => k !== "claude-code" && k !== "claude-sdk");
678
+ const hasNonClaude = selectedAgents.some((k) => k !== "claude-code");
683
679
  const docTarget = hasNonClaude
684
680
  ? "your CLAUDE.md, AGENTS.md, or equivalent"
685
681
  : "your CLAUDE.md";
@@ -763,8 +759,8 @@ async function runUpdate() {
763
759
  const sessionStartCmd = "bash ~/.config/fathom-mcp/scripts/fathom-sessionstart.sh";
764
760
  const registeredHooks = [];
765
761
 
766
- // Claude Code / Claude SDK
767
- const hasClaude = agents.includes("claude-code") || agents.includes("claude-sdk")
762
+ // Claude Code
763
+ const hasClaude = agents.includes("claude-code")
768
764
  || fs.existsSync(path.join(projectDir, ".claude"));
769
765
  if (hasClaude) {
770
766
  const settingsPath = path.join(projectDir, ".claude", "settings.local.json");
@@ -856,12 +852,11 @@ function runList() {
856
852
  }
857
853
 
858
854
  // Header
859
- const cols = { name: 16, type: 13, mode: 11, status: 10 };
855
+ const cols = { name: 16, type: 13, status: 10 };
860
856
  console.log(
861
857
  "\n " +
862
858
  "NAME".padEnd(cols.name) +
863
859
  "TYPE".padEnd(cols.type) +
864
- "MODE".padEnd(cols.mode) +
865
860
  "STATUS".padEnd(cols.status) +
866
861
  "DIR",
867
862
  );
@@ -869,7 +864,6 @@ function runList() {
869
864
  for (const name of names) {
870
865
  const entry = agents[name];
871
866
  const type = entry.agentType || "claude-code";
872
- const mode = entry.executionMode || "tmux";
873
867
  const status = entry.ssh ? "[ssh]" : isAgentRunning(name, entry);
874
868
  const dir = entry.projectDir.replace(process.env.HOME, "~");
875
869
 
@@ -877,7 +871,6 @@ function runList() {
877
871
  " " +
878
872
  name.padEnd(cols.name) +
879
873
  type.padEnd(cols.type) +
880
- mode.padEnd(cols.mode) +
881
874
  status.padEnd(cols.status) +
882
875
  dir,
883
876
  );
@@ -954,9 +947,8 @@ async function runAdd(argv) {
954
947
 
955
948
  const name = await ask(rl, " Agent name", nameArg || defaultName);
956
949
  const agentProjectDir = await ask(rl, " Project directory", defaults.projectDir);
957
- const agentType = await ask(rl, " Agent type (claude-code|claude-sdk|gemini|manual)", defaults.agentType);
958
- const executionMode = await ask(rl, " Execution mode (tmux|headless)", defaults.executionMode);
959
- const command = await ask(rl, " Command", defaultCommand(agentType, executionMode));
950
+ const agentType = await ask(rl, " Agent type (claude-code|gemini|manual)", defaults.agentType);
951
+ const command = await ask(rl, " Command", defaultCommand(agentType));
960
952
  const server = await ask(rl, " Server URL", defaults.server);
961
953
  const apiKey = await ask(rl, " API key", defaults.apiKey);
962
954
  const vault = await ask(rl, " Vault subdirectory", defaults.vault || "vault");
@@ -968,7 +960,6 @@ async function runAdd(argv) {
968
960
  const entry = {
969
961
  projectDir: path.resolve(agentProjectDir),
970
962
  agentType,
971
- executionMode,
972
963
  command,
973
964
  server,
974
965
  apiKey,
package/src/config.js CHANGED
@@ -20,7 +20,7 @@ const DEFAULTS = {
20
20
  workspace: "",
21
21
  vault: "vault",
22
22
  vaultMode: "local", // hosted | synced | local | none
23
- server: "http://localhost:4243",
23
+ server: "http://127.0.0.1:4243",
24
24
  apiKey: "",
25
25
  description: "",
26
26
  agents: [],
package/src/index.js CHANGED
@@ -483,6 +483,26 @@ const telegramTools = [
483
483
  },
484
484
  ];
485
485
 
486
+ // --- Primary-agent-only tools (policy gate) ----------------------------------
487
+
488
+ const primaryAgentTools = [
489
+ {
490
+ name: "fathom_session_inject",
491
+ description:
492
+ "Inject a keystroke into a workspace's tmux session. Primary agent only. " +
493
+ "Used by the policy gate to respond to permission prompts. " +
494
+ "Keys must be a single digit 1-9 (to select a numbered option) or a named key (Enter, Escape).",
495
+ inputSchema: {
496
+ type: "object",
497
+ properties: {
498
+ workspace: { type: "string", description: "Target workspace name (e.g. 'navier-stokes')" },
499
+ keys: { type: "string", description: "Keys to send — single digit 1-9 or named key (Enter, Escape)" },
500
+ },
501
+ required: ["workspace", "keys"],
502
+ },
503
+ },
504
+ ];
505
+
486
506
  // --- Server setup & dispatch -------------------------------------------------
487
507
 
488
508
  const server = new Server(
@@ -501,7 +521,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
501
521
  } catch {
502
522
  // If settings unavailable, hide telegram tools
503
523
  }
504
- const allTools = [...tools, ...(showTelegram ? telegramTools : [])];
524
+ const allTools = [...tools, ...(showTelegram ? [...telegramTools, ...primaryAgentTools] : [])];
505
525
  return { tools: allTools };
506
526
  });
507
527
 
@@ -797,6 +817,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
797
817
  }
798
818
  break;
799
819
  }
820
+ // --- Session injection (policy gate) ---
821
+ case "fathom_session_inject": {
822
+ if (!args.workspace) { result = { error: "workspace is required" }; break; }
823
+ if (!args.keys) { result = { error: "keys is required" }; break; }
824
+ result = await client.injectKeys(args.workspace, args.keys);
825
+ break;
826
+ }
800
827
  case "fathom_telegram_send_voice": {
801
828
  const voiceContactArg = args.contact;
802
829
  if (!voiceContactArg) { result = { error: "contact is required" }; break; }
@@ -298,6 +298,14 @@ export function createClient(config) {
298
298
  });
299
299
  }
300
300
 
301
+ // --- Session injection (policy gate) ----------------------------------------
302
+
303
+ async function injectKeys(targetWorkspace, keys) {
304
+ return request("POST", `/api/session/${encodeURIComponent(targetWorkspace)}/inject`, {
305
+ body: { keys },
306
+ });
307
+ }
308
+
301
309
  // --- Settings --------------------------------------------------------------
302
310
 
303
311
  async function getSettings() {
@@ -361,6 +369,7 @@ export function createClient(config) {
361
369
  telegramSendVoice,
362
370
  telegramStatus,
363
371
  speak,
372
+ injectKeys,
364
373
  getSettings,
365
374
  getApiKey,
366
375
  rotateKey,
@@ -54,14 +54,20 @@ export function createWSConnection(config) {
54
54
  function connect() {
55
55
  if (closed) return;
56
56
 
57
+ // Redact token from URL for logging
58
+ const redactedUrl = wsUrl.replace(/token=[^&]+/, "token=***");
59
+ console.error(`[ws] connecting to ${redactedUrl}`);
60
+
57
61
  try {
58
62
  ws = new WebSocket(wsUrl);
59
- } catch {
63
+ } catch (err) {
64
+ console.error(`[ws] connection constructor failed: ${err.message}`);
60
65
  scheduleReconnect();
61
66
  return;
62
67
  }
63
68
 
64
69
  ws.on("open", () => {
70
+ console.error(`[ws] connected — sending hello (agent=${agent}, vault_mode=${vaultMode})`);
65
71
  reconnectDelay = INITIAL_RECONNECT_MS;
66
72
 
67
73
  // Send hello handshake
@@ -85,10 +91,12 @@ export function createWSConnection(config) {
85
91
 
86
92
  switch (msg.type) {
87
93
  case "welcome":
94
+ console.error(`[ws] welcome received — connection established for workspace=${workspace}`);
88
95
  break;
89
96
 
90
97
  case "inject":
91
98
  case "ping_fire":
99
+ console.error(`[ws] received ${msg.type} (${(msg.text || "").length} chars)`);
92
100
  injectMessage(msg.text || "");
93
101
  break;
94
102
 
@@ -101,18 +109,21 @@ export function createWSConnection(config) {
101
109
  break;
102
110
 
103
111
  case "error":
112
+ console.error(`[ws] server error: ${msg.message || JSON.stringify(msg)}`);
104
113
  // Server rejected us — don't reconnect immediately
105
114
  reconnectDelay = MAX_RECONNECT_MS;
106
115
  break;
107
116
  }
108
117
  });
109
118
 
110
- ws.on("close", () => {
119
+ ws.on("close", (code, reason) => {
120
+ console.error(`[ws] closed (code=${code}, reason=${reason || "none"})`);
111
121
  stopKeepalive();
112
122
  if (!closed) scheduleReconnect();
113
123
  });
114
124
 
115
- ws.on("error", () => {
125
+ ws.on("error", (err) => {
126
+ console.error(`[ws] error: ${err.message}`);
116
127
  // Error always followed by close event — reconnect handled there
117
128
  stopKeepalive();
118
129
  });
@@ -144,6 +155,7 @@ export function createWSConnection(config) {
144
155
 
145
156
  function scheduleReconnect() {
146
157
  if (closed) return;
158
+ console.error(`[ws] reconnecting in ${reconnectDelay}ms`);
147
159
  setTimeout(connect, reconnectDelay);
148
160
  reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
149
161
  }
@@ -152,13 +164,6 @@ export function createWSConnection(config) {
152
164
 
153
165
  function injectMessage(text) {
154
166
  if (!text) return;
155
-
156
- if (agent === "claude-sdk") {
157
- // Headless agents are per-invocation — injection handled server-side.
158
- // Nothing to do client-side.
159
- return;
160
- }
161
-
162
167
  injectToTmux(text);
163
168
  }
164
169
 
@@ -177,8 +182,8 @@ export function createWSConnection(config) {
177
182
  timeout: 5000,
178
183
  stdio: "ignore",
179
184
  });
180
- } catch {
181
- // tmux not available or pane not found — non-fatal
185
+ } catch (err) {
186
+ console.error(`[ws] tmux inject failed for ${pane}: ${err.message}`);
182
187
  }
183
188
  }
184
189