claude-code-swarm 0.3.10 → 0.3.12

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.
@@ -10,9 +10,11 @@ import fs from "fs";
10
10
  import path from "path";
11
11
  import { execSync } from "child_process";
12
12
  import { SESSIONLOG_DIR, SESSIONLOG_STATE_PATH, sessionPaths } from "./paths.mjs";
13
+ import { readConfig } from "./config.mjs";
13
14
  import { resolveTeamName, resolveScope } from "./config.mjs";
14
15
  import { sendToSidecar, ensureSidecar } from "./sidecar-client.mjs";
15
16
  import { fireAndForgetTrajectory } from "./map-connection.mjs";
17
+ import { resolvePackage } from "./swarmkit-resolver.mjs";
16
18
 
17
19
  /**
18
20
  * Check if sessionlog is installed and active.
@@ -39,6 +41,60 @@ export function checkSessionlogStatus() {
39
41
  }
40
42
  }
41
43
 
44
+ /**
45
+ * Check if sessionlog's standalone hooks are installed in .claude/settings.json.
46
+ * Reads the file directly — no dependency on resolvePackage("sessionlog").
47
+ * Looks for any SessionStart hook command containing "sessionlog " as a sentinel
48
+ * (if session-start is there, all 12 hooks were installed together).
49
+ */
50
+ export function hasStandaloneHooks() {
51
+ try {
52
+ const settingsPath = path.join(process.cwd(), ".claude", "settings.json");
53
+ const content = fs.readFileSync(settingsPath, "utf-8");
54
+ const settings = JSON.parse(content);
55
+ const hooks = settings.hooks?.SessionStart ?? [];
56
+ return hooks.some(m => m.hooks?.some(h => h.command?.includes("sessionlog ")));
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Auto-enable sessionlog if it is installed but not yet enabled.
64
+ * Tries the programmatic API first (dynamic import), then falls back to CLI.
65
+ * Best-effort — returns true if enabled, false otherwise. Never throws.
66
+ */
67
+ export async function ensureSessionlogEnabled() {
68
+ const status = checkSessionlogStatus();
69
+ if (status === "active") return true;
70
+ if (status === "not installed") return false;
71
+
72
+ // Status is "installed but not enabled" — try to enable it
73
+
74
+ // 1. Try programmatic API via dynamic import
75
+ // skipAgentHooks: true — agent hooks are managed by cc-swarm's hooks.json
76
+ try {
77
+ const sessionlogMod = await resolvePackage("sessionlog");
78
+ if (sessionlogMod?.enable) {
79
+ const result = await sessionlogMod.enable({ agent: "claude-code", skipAgentHooks: true });
80
+ if (result.enabled) return true;
81
+ }
82
+ } catch {
83
+ // Fall through to CLI
84
+ }
85
+
86
+ // 2. Fallback to CLI
87
+ try {
88
+ execSync("sessionlog enable --agent claude-code --skip-agent-hooks", {
89
+ stdio: "ignore",
90
+ timeout: 15_000,
91
+ });
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
42
98
  /**
43
99
  * Find the active (non-ended) sessionlog session file.
44
100
  * Returns parsed SessionState or null.
@@ -78,31 +134,54 @@ export function findActiveSession(sessionlogDir = SESSIONLOG_DIR) {
78
134
 
79
135
  /**
80
136
  * Build a MAP TrajectoryCheckpoint from sessionlog state.
81
- * Metadata contents are filtered by sync level.
137
+ *
138
+ * Conforms to sessionlog's SessionSyncCheckpoint wire format (snake_case,
139
+ * top-level fields) so OpenHive's sync listener can extract fields correctly.
140
+ * Extra sessionlog-specific fields go in `metadata` for passthrough.
82
141
  */
83
142
  export function buildTrajectoryCheckpoint(state, syncLevel, config) {
84
143
  const teamName = resolveTeamName(config);
85
- const agentId = `${teamName}-sidecar`;
86
144
 
87
145
  const id =
88
146
  state.lastCheckpointID ||
89
147
  `${state.sessionID}-step${state.stepCount || 0}`;
90
148
 
91
- const label = `Turn ${state.turnID || "?"} (step ${state.stepCount || 0}, ${state.phase || "unknown"})`;
149
+ // Wire format fields (top-level, snake_case) always present
150
+ const checkpoint = {
151
+ id,
152
+ session_id: state.sessionID,
153
+ agent: `${teamName}-sidecar`,
154
+ files_touched: [],
155
+ checkpoints_count: 0,
156
+ };
92
157
 
158
+ // Metadata — sessionlog-specific fields for passthrough
93
159
  const metadata = {
94
160
  phase: state.phase,
95
161
  turnId: state.turnID,
96
162
  startedAt: state.startedAt,
163
+ label: `Turn ${state.turnID || "?"} (step ${state.stepCount || 0}, ${state.phase || "unknown"})`,
97
164
  };
98
165
  if (state.endedAt) metadata.endedAt = state.endedAt;
99
166
 
100
167
  if (syncLevel === "metrics" || syncLevel === "full") {
168
+ // Promote to top-level wire format fields
169
+ checkpoint.files_touched = state.filesTouched || [];
170
+ checkpoint.checkpoints_count = (state.turnCheckpointIDs || []).length;
171
+ if (state.tokenUsage) {
172
+ checkpoint.token_usage = {
173
+ input_tokens: state.tokenUsage.inputTokens ?? state.tokenUsage.input ?? 0,
174
+ output_tokens: state.tokenUsage.outputTokens ?? state.tokenUsage.output ?? 0,
175
+ cache_creation_tokens: state.tokenUsage.cacheCreationTokens ?? 0,
176
+ cache_read_tokens: state.tokenUsage.cacheReadTokens ?? 0,
177
+ api_call_count: state.tokenUsage.apiCallCount ?? 0,
178
+ };
179
+ }
180
+
181
+ // Keep in metadata for sessionlog consumers
101
182
  metadata.stepCount = state.stepCount;
102
- metadata.filesTouched = state.filesTouched;
103
183
  metadata.lastCheckpointID = state.lastCheckpointID;
104
184
  metadata.turnCheckpointIDs = state.turnCheckpointIDs;
105
- if (state.tokenUsage) metadata.tokenUsage = state.tokenUsage;
106
185
  }
107
186
 
108
187
  if (syncLevel === "full") {
@@ -113,13 +192,7 @@ export function buildTrajectoryCheckpoint(state, syncLevel, config) {
113
192
  }
114
193
  }
115
194
 
116
- return {
117
- id,
118
- agentId,
119
- sessionId: state.sessionID,
120
- label,
121
- metadata,
122
- };
195
+ return { ...checkpoint, metadata };
123
196
  }
124
197
 
125
198
  /**
@@ -176,7 +249,9 @@ export async function annotateSwarmSession(config, sessionId) {
176
249
 
177
250
  let createSessionStore;
178
251
  try {
179
- ({ createSessionStore } = await import("sessionlog"));
252
+ const sessionlogMod = await resolvePackage("sessionlog");
253
+ if (!sessionlogMod) return;
254
+ ({ createSessionStore } = sessionlogMod);
180
255
  } catch {
181
256
  // sessionlog not available as a module
182
257
  return;
@@ -200,3 +275,63 @@ export async function annotateSwarmSession(config, sessionId) {
200
275
  // Non-critical — session may not exist yet or annotate failed
201
276
  }
202
277
  }
278
+
279
+ /**
280
+ * Dispatch a sessionlog hook event programmatically.
281
+ * Replaces the CLI pattern: `sessionlog hooks claude-code <hookName>`
282
+ * Uses resolvePackage("sessionlog") to call the lifecycle handler directly.
283
+ * Best-effort — never throws.
284
+ *
285
+ * @param {string} hookName - Sessionlog hook name (e.g. "session-start", "stop")
286
+ * @param {object} hookData - Raw hook event data from Claude Code stdin
287
+ */
288
+ export async function dispatchSessionlogHook(hookName, hookData) {
289
+ // Decide whether plugin dispatch should handle this hook.
290
+ // config.sessionlog.mode: "plugin" (always dispatch), "standalone" (never dispatch), "auto" (check)
291
+ const config = readConfig();
292
+ const mode = config.sessionlog?.mode || "auto";
293
+ if (mode === "standalone") return;
294
+ if (mode === "auto" && hasStandaloneHooks()) return;
295
+
296
+ let sessionlogMod;
297
+ try {
298
+ sessionlogMod = await resolvePackage("sessionlog");
299
+ } catch {
300
+ return;
301
+ }
302
+ if (!sessionlogMod) return;
303
+
304
+ const {
305
+ isEnabled,
306
+ getAgent,
307
+ hasHookSupport,
308
+ createLifecycleHandler,
309
+ createSessionStore,
310
+ createCheckpointStore,
311
+ } = sessionlogMod;
312
+
313
+ // Pass cwd explicitly — sessionlog's defaults use git rev-parse which
314
+ // resolves against the OS working directory, not process.cwd().
315
+ const cwd = process.cwd();
316
+
317
+ // Bail if sessionlog is not enabled in this repo
318
+ try {
319
+ if (typeof isEnabled === "function" && !(await isEnabled(cwd))) return;
320
+ } catch {
321
+ return;
322
+ }
323
+
324
+ const agent = getAgent("claude-code");
325
+ if (!agent || (typeof hasHookSupport === "function" && !hasHookSupport(agent))) return;
326
+
327
+ const event = agent.parseHookEvent(hookName, JSON.stringify(hookData));
328
+ if (!event) return;
329
+
330
+ const handler = createLifecycleHandler({
331
+ sessionStore: createSessionStore(cwd),
332
+ checkpointStore: createCheckpointStore(cwd),
333
+ cwd,
334
+ });
335
+
336
+ await handler.dispatch(agent, event);
337
+ }
@@ -97,14 +97,35 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
97
97
  const { inboxInstance, meshPeer, transportMode = "websocket" } = opts;
98
98
  const useMeshRegistry = transportMode === "mesh" && inboxInstance;
99
99
 
100
+ // Connection-ready gate: commands that need `conn` await this promise.
101
+ // If connection is already available, resolves immediately.
102
+ // When connection arrives later (via setConnection), resolves the pending promise.
103
+ let _connReadyResolve;
104
+ let _connReady = conn
105
+ ? Promise.resolve(conn)
106
+ : new Promise((resolve) => { _connReadyResolve = resolve; });
107
+
108
+ const CONN_WAIT_TIMEOUT_MS = opts.connWaitTimeoutMs ?? 10_000;
109
+
110
+ /**
111
+ * Wait for the MAP connection to become available.
112
+ * Returns the connection or null if timed out.
113
+ */
114
+ async function waitForConn() {
115
+ if (conn) return conn;
116
+ const timeout = new Promise((resolve) => setTimeout(() => resolve(null), CONN_WAIT_TIMEOUT_MS));
117
+ return Promise.race([_connReady, timeout]);
118
+ }
119
+
100
120
  const handler = async (command, client) => {
101
121
  const { action } = command;
102
122
 
103
123
  try {
104
124
  switch (action) {
105
125
  case "emit": {
106
- if (conn) {
107
- await conn.send(
126
+ const c = conn || await waitForConn();
127
+ if (c) {
128
+ await c.send(
108
129
  { scope },
109
130
  command.event,
110
131
  command.meta || { relationship: "broadcast" }
@@ -115,8 +136,9 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
115
136
  }
116
137
 
117
138
  case "send": {
118
- if (conn) {
119
- await conn.send(command.to, command.payload, command.meta);
139
+ const c = conn || await waitForConn();
140
+ if (c) {
141
+ await c.send(command.to, command.payload, command.meta);
120
142
  }
121
143
  respond(client, { ok: true });
122
144
  break;
@@ -160,10 +182,15 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
160
182
  log.error("spawn (mesh) failed", { error: err.message });
161
183
  respond(client, { ok: false, error: err.message });
162
184
  }
163
- } else if (conn) {
164
- // WebSocket mode: use MAP SDK
185
+ } else {
186
+ // WebSocket mode: use MAP SDK (wait for connection if needed)
187
+ const c = conn || await waitForConn();
188
+ if (!c) {
189
+ respond(client, { ok: false, error: "no connection (timed out waiting)" });
190
+ break;
191
+ }
165
192
  try {
166
- const result = await conn.spawn({
193
+ const result = await c.spawn({
167
194
  agentId,
168
195
  name,
169
196
  role,
@@ -191,8 +218,6 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
191
218
  log.error("spawn failed", { error: err.message });
192
219
  respond(client, { ok: false, error: err.message });
193
220
  }
194
- } else {
195
- respond(client, { ok: false, error: "no connection" });
196
221
  }
197
222
  break;
198
223
  }
@@ -229,7 +254,7 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
229
254
  }
230
255
  }
231
256
  } else if (conn) {
232
- // WebSocket mode: use MAP SDK
257
+ // WebSocket mode: use MAP SDK (best-effort, no wait — local cleanup is priority)
233
258
  try {
234
259
  await conn.callExtension("map/agents/unregister", {
235
260
  agentId,
@@ -263,15 +288,16 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
263
288
  }
264
289
 
265
290
  case "trajectory-checkpoint": {
266
- if (conn) {
291
+ const c = conn || await waitForConn();
292
+ if (c) {
267
293
  try {
268
- await conn.callExtension("trajectory/checkpoint", {
294
+ await c.callExtension("trajectory/checkpoint", {
269
295
  checkpoint: command.checkpoint,
270
296
  });
271
297
  respond(client, { ok: true, method: "trajectory" });
272
298
  } catch (err) {
273
299
  log.warn("trajectory/checkpoint not supported, falling back to broadcast", { error: err.message });
274
- await conn.send(
300
+ await c.send(
275
301
  { scope },
276
302
  {
277
303
  type: "trajectory.checkpoint",
@@ -288,7 +314,7 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
288
314
  respond(client, { ok: true, method: "broadcast-fallback" });
289
315
  }
290
316
  } else {
291
- respond(client, { ok: false, error: "no connection" });
317
+ respond(client, { ok: false, error: "no connection (timed out waiting)" });
292
318
  }
293
319
  break;
294
320
  }
@@ -299,9 +325,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
299
325
  // both mesh and websocket modes.
300
326
 
301
327
  case "bridge-task-created": {
302
- if (conn) {
328
+ const c = conn || await waitForConn();
329
+ if (c) {
303
330
  try {
304
- await conn.send({ scope }, {
331
+ await c.send({ scope }, {
305
332
  type: "task.created",
306
333
  task: command.task,
307
334
  _origin: command.agentId || "opentasks",
@@ -313,9 +340,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
313
340
  }
314
341
 
315
342
  case "bridge-task-status": {
316
- if (conn) {
343
+ const c = conn || await waitForConn();
344
+ if (c) {
317
345
  try {
318
- await conn.send({ scope }, {
346
+ await c.send({ scope }, {
319
347
  type: "task.status",
320
348
  taskId: command.taskId,
321
349
  previous: command.previous || "open",
@@ -324,7 +352,7 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
324
352
  }, { relationship: "broadcast" });
325
353
  // Also emit task.completed for terminal states
326
354
  if (command.current === "completed" || command.current === "closed") {
327
- await conn.send({ scope }, {
355
+ await c.send({ scope }, {
328
356
  type: "task.completed",
329
357
  taskId: command.taskId,
330
358
  _origin: command.agentId || "opentasks",
@@ -337,9 +365,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
337
365
  }
338
366
 
339
367
  case "bridge-task-assigned": {
340
- if (conn) {
368
+ const c = conn || await waitForConn();
369
+ if (c) {
341
370
  try {
342
- await conn.send({ scope }, {
371
+ await c.send({ scope }, {
343
372
  type: "task.assigned",
344
373
  taskId: command.taskId,
345
374
  agentId: command.assignee,
@@ -352,7 +381,8 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
352
381
  }
353
382
 
354
383
  case "state": {
355
- if (conn) {
384
+ const c = conn || await waitForConn();
385
+ if (c) {
356
386
  try {
357
387
  if (command.agentId) {
358
388
  // State update for a specific child agent
@@ -379,9 +409,9 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
379
409
  }
380
410
  } else {
381
411
  // State update for the sidecar agent itself
382
- await conn.updateState(command.state);
412
+ await c.updateState(command.state);
383
413
  if (command.metadata) {
384
- await conn.updateMetadata(command.metadata);
414
+ await c.updateMetadata(command.metadata);
385
415
  }
386
416
  }
387
417
  } catch {
@@ -406,9 +436,17 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
406
436
  }
407
437
  };
408
438
 
409
- // Allow updating the connection reference
439
+ // Allow updating the connection reference (also resolves any pending waitForConn)
410
440
  handler.setConnection = (newConn) => {
411
441
  conn = newConn;
442
+ if (newConn && _connReadyResolve) {
443
+ _connReadyResolve(newConn);
444
+ _connReadyResolve = null;
445
+ }
446
+ // Reset the gate for future disconnection/reconnection cycles
447
+ if (!newConn) {
448
+ _connReady = new Promise((resolve) => { _connReadyResolve = resolve; });
449
+ }
412
450
  };
413
451
 
414
452
  return handler;
@@ -10,47 +10,23 @@
10
10
 
11
11
  import fs from "fs";
12
12
  import path from "path";
13
- import { createRequire } from "module";
14
- import { getGlobalNodeModules } from "./swarmkit-resolver.mjs";
13
+ import { resolvePackage } from "./swarmkit-resolver.mjs";
15
14
  import { createLogger } from "./log.mjs";
16
15
 
17
16
  const log = createLogger("skilltree");
18
17
 
19
- const require = createRequire(import.meta.url);
20
-
21
18
  let _skillTree = undefined;
22
19
 
23
20
  /**
24
21
  * Load the skill-tree module. Returns null if not available.
25
- * Tries local require first, then falls back to global node_modules.
22
+ * Uses resolvePackage() for consistent global fallback resolution.
26
23
  */
27
- function loadSkillTree() {
24
+ async function loadSkillTree() {
28
25
  if (_skillTree !== undefined) return _skillTree;
29
26
 
30
- // 1. Local require (works if skill-tree is in node_modules or NODE_PATH)
31
- try {
32
- _skillTree = require("skill-tree");
33
- return _skillTree;
34
- } catch {
35
- // Not locally available
36
- }
37
-
38
- // 2. Global node_modules fallback (where swarmkit installs it)
39
- const globalNm = getGlobalNodeModules();
40
- if (globalNm) {
41
- const globalPath = path.join(globalNm, "skill-tree");
42
- if (fs.existsSync(globalPath)) {
43
- try {
44
- _skillTree = require(globalPath);
45
- return _skillTree;
46
- } catch {
47
- // require failed
48
- }
49
- }
50
- }
51
-
52
- _skillTree = null;
53
- return null;
27
+ const mod = await resolvePackage("skill-tree");
28
+ _skillTree = mod || null;
29
+ return _skillTree;
54
30
  }
55
31
 
56
32
  /**
@@ -93,7 +69,7 @@ export function parseSkillTreeExtension(manifest) {
93
69
  * @returns {Promise<string>} Rendered loadout markdown, or empty string on failure
94
70
  */
95
71
  export async function compileRoleLoadout(roleName, criteria, config) {
96
- const st = loadSkillTree();
72
+ const st = await loadSkillTree();
97
73
  if (!st?.createSkillBank) return "";
98
74
 
99
75
  try {
@@ -120,8 +120,56 @@ export async function resolveSwarmkit() {
120
120
  }
121
121
  }
122
122
 
123
+ /**
124
+ * Resolve an optional global package by name.
125
+ * Tries bare import first (works if in local dependencies), then falls back
126
+ * to absolute path via global node_modules (where swarmkit installs packages).
127
+ *
128
+ * ESM dynamic import() doesn't respect runtime NODE_PATH changes, so bare
129
+ * imports fail for packages only installed globally. This helper works around
130
+ * that by using absolute paths as a fallback.
131
+ *
132
+ * Results are cached in-memory. Returns the module or null. Never throws.
133
+ *
134
+ * @param {string} name - Package name (e.g. "agent-inbox", "sessionlog")
135
+ * @returns {Promise<object|null>}
136
+ */
137
+ const _packageCache = new Map();
138
+
139
+ export async function resolvePackage(name) {
140
+ if (_packageCache.has(name)) return _packageCache.get(name);
141
+
142
+ // 1. Try bare import (works for local dependencies)
143
+ try {
144
+ const mod = await import(/* @vite-ignore */ name);
145
+ _packageCache.set(name, mod);
146
+ return mod;
147
+ } catch {
148
+ // Not locally resolvable
149
+ }
150
+
151
+ // 2. Try global node_modules (where swarmkit installs)
152
+ const globalNm = getGlobalNodeModules();
153
+ if (globalNm) {
154
+ const globalPath = path.join(globalNm, name);
155
+ if (fs.existsSync(globalPath)) {
156
+ try {
157
+ const mod = await import(/* @vite-ignore */ globalPath);
158
+ _packageCache.set(name, mod);
159
+ return mod;
160
+ } catch {
161
+ // Global path exists but import failed
162
+ }
163
+ }
164
+ }
165
+
166
+ _packageCache.set(name, null);
167
+ return null;
168
+ }
169
+
123
170
  /** Reset cached state (for testing) */
124
171
  export function _resetCache() {
125
172
  _globalPrefix = undefined;
126
173
  _swarmkit = undefined;
174
+ _packageCache.clear();
127
175
  }
@@ -1,95 +0,0 @@
1
- #!/bin/bash
2
- # Wrapper script to run agent-inbox MCP server
3
- # When the sidecar's inbox socket exists, runs in proxy mode (IPC client).
4
- # Otherwise falls back to standalone mode with its own storage.
5
- # Exits silently if inbox is not enabled or not installed.
6
-
7
- # Check if inbox is enabled in config
8
- ENABLED=false
9
- if [ -f .swarm/claude-swarm/config.json ]; then
10
- ENABLED=$(node -e "
11
- try {
12
- const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
13
- const envEnabled = (process.env.SWARM_INBOX_ENABLED || '').toLowerCase();
14
- const isEnabled = ['true', '1', 'yes'].includes(envEnabled) || c.inbox?.enabled === true;
15
- process.stdout.write(isEnabled ? 'true' : 'false');
16
- } catch { process.stdout.write('false'); }
17
- " 2>/dev/null || echo "false")
18
- elif [ -n "$SWARM_INBOX_ENABLED" ]; then
19
- case "$(echo "$SWARM_INBOX_ENABLED" | tr '[:upper:]' '[:lower:]')" in
20
- true|1|yes) ENABLED=true ;;
21
- esac
22
- fi
23
-
24
- if [ "$ENABLED" != "true" ]; then
25
- # Not enabled — exit silently so Claude Code doesn't show an error
26
- sleep 0.1
27
- exit 0
28
- fi
29
-
30
- # Read scope from config (defaults to MAP scope or "default")
31
- SCOPE="default"
32
- if [ -f .swarm/claude-swarm/config.json ]; then
33
- CONFIGURED_SCOPE=$(node -e "
34
- try {
35
- const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
36
- const s = c.inbox?.scope || c.map?.scope || process.env.SWARM_INBOX_SCOPE || '';
37
- if (s) process.stdout.write(s);
38
- } catch {}
39
- " 2>/dev/null)
40
- if [ -n "$CONFIGURED_SCOPE" ]; then
41
- SCOPE="$CONFIGURED_SCOPE"
42
- fi
43
- fi
44
-
45
- if [ -n "$SWARM_INBOX_SCOPE" ]; then
46
- SCOPE="$SWARM_INBOX_SCOPE"
47
- fi
48
-
49
- export INBOX_SCOPE="$SCOPE"
50
-
51
- # Discover sidecar inbox socket for proxy mode
52
- # Check well-known paths: .swarm/claude-swarm/tmp/map/inbox.sock
53
- INBOX_SOCK=""
54
- if [ -S .swarm/claude-swarm/tmp/map/inbox.sock ]; then
55
- INBOX_SOCK=".swarm/claude-swarm/tmp/map/inbox.sock"
56
- fi
57
-
58
- # Also check per-session paths
59
- if [ -z "$INBOX_SOCK" ] && [ -d .swarm/claude-swarm/tmp/map/sessions ]; then
60
- # Find the most recently modified inbox.sock in session dirs
61
- INBOX_SOCK=$(find .swarm/claude-swarm/tmp/map/sessions -name inbox.sock -type s 2>/dev/null | head -1)
62
- fi
63
-
64
- # If inbox socket found, enable proxy mode
65
- if [ -n "$INBOX_SOCK" ]; then
66
- export INBOX_SOCKET_PATH="$INBOX_SOCK"
67
- fi
68
-
69
- # Try to find the agent-inbox module entry point
70
- INBOX_MAIN=""
71
-
72
- # 1. Check global npm root (swarmkit installs here)
73
- GLOBAL_ROOT=$(npm root -g 2>/dev/null)
74
- if [ -n "$GLOBAL_ROOT" ] && [ -f "$GLOBAL_ROOT/agent-inbox/dist/index.js" ]; then
75
- INBOX_MAIN="$GLOBAL_ROOT/agent-inbox/dist/index.js"
76
- fi
77
-
78
- # 2. Check plugin directory's node_modules (dev installs)
79
- if [ -z "$INBOX_MAIN" ] && [ -n "$CLAUDE_PLUGIN_ROOT" ] && [ -f "$CLAUDE_PLUGIN_ROOT/node_modules/agent-inbox/dist/index.js" ]; then
80
- INBOX_MAIN="$CLAUDE_PLUGIN_ROOT/node_modules/agent-inbox/dist/index.js"
81
- fi
82
-
83
- # 3. Fallback: try require.resolve from CWD
84
- if [ -z "$INBOX_MAIN" ]; then
85
- INBOX_MAIN=$(node -e "try { console.log(require.resolve('agent-inbox/dist/index.js')); } catch {}" 2>/dev/null)
86
- fi
87
-
88
- if [ -n "$INBOX_MAIN" ]; then
89
- # Uses proxy mode when INBOX_SOCKET_PATH is set, standalone otherwise
90
- exec node "$INBOX_MAIN" mcp
91
- fi
92
-
93
- # agent-inbox not installed — log to stderr and exit cleanly
94
- echo "[agent-inbox-mcp] agent-inbox not found. Install with: npm install -g agent-inbox or install via swarmkit" >&2
95
- exit 0