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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/mcp-launcher.mjs +176 -0
- package/.claude-plugin/plugin.json +18 -13
- package/CLAUDE.md +10 -0
- package/hooks/hooks.json +87 -0
- package/package.json +1 -4
- package/scripts/map-hook.mjs +12 -1
- package/scripts/map-sidecar.mjs +4 -2
- package/src/__tests__/bootstrap.test.mjs +80 -5
- package/src/__tests__/e2e-main-agent-registration.test.mjs +229 -0
- package/src/__tests__/e2e-reconnection.test.mjs +5 -5
- package/src/__tests__/helpers.mjs +1 -0
- package/src/__tests__/sessionlog-e2e.test.mjs +270 -0
- package/src/__tests__/sessionlog.test.mjs +217 -14
- package/src/__tests__/sidecar-server.test.mjs +4 -4
- package/src/__tests__/swarmkit-resolver.test.mjs +168 -0
- package/src/bootstrap.mjs +31 -4
- package/src/config.mjs +6 -2
- package/src/index.mjs +3 -0
- package/src/map-connection.mjs +10 -3
- package/src/mesh-connection.mjs +10 -3
- package/src/sessionlog.mjs +148 -13
- package/src/sidecar-server.mjs +63 -25
- package/src/skilltree-client.mjs +7 -31
- package/src/swarmkit-resolver.mjs +48 -0
- package/.claude-plugin/run-agent-inbox-mcp.sh +0 -95
- package/.claude-plugin/run-minimem-mcp.sh +0 -98
- package/.claude-plugin/run-opentasks-mcp.sh +0 -65
- package/scripts/dev-link.mjs +0 -179
package/src/sessionlog.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/sidecar-server.mjs
CHANGED
|
@@ -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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
291
|
+
const c = conn || await waitForConn();
|
|
292
|
+
if (c) {
|
|
267
293
|
try {
|
|
268
|
-
await
|
|
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
|
|
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
|
-
|
|
328
|
+
const c = conn || await waitForConn();
|
|
329
|
+
if (c) {
|
|
303
330
|
try {
|
|
304
|
-
await
|
|
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
|
-
|
|
343
|
+
const c = conn || await waitForConn();
|
|
344
|
+
if (c) {
|
|
317
345
|
try {
|
|
318
|
-
await
|
|
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
|
|
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
|
-
|
|
368
|
+
const c = conn || await waitForConn();
|
|
369
|
+
if (c) {
|
|
341
370
|
try {
|
|
342
|
-
await
|
|
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
|
-
|
|
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
|
|
412
|
+
await c.updateState(command.state);
|
|
383
413
|
if (command.metadata) {
|
|
384
|
-
await
|
|
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;
|
package/src/skilltree-client.mjs
CHANGED
|
@@ -10,47 +10,23 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from "fs";
|
|
12
12
|
import path from "path";
|
|
13
|
-
import {
|
|
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
|
-
*
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|