claude-code-swarm 0.3.5 → 0.3.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.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/run-agent-inbox-mcp.sh +22 -3
  4. package/.gitattributes +3 -0
  5. package/.opentasks/config.json +9 -0
  6. package/.opentasks/graph.jsonl +0 -0
  7. package/e2e/helpers/opentasks-daemon.mjs +149 -0
  8. package/e2e/tier6-live-inbox-flow.test.mjs +938 -0
  9. package/e2e/tier7-hooks.test.mjs +992 -0
  10. package/e2e/tier7-minimem.test.mjs +461 -0
  11. package/e2e/tier7-opentasks.test.mjs +513 -0
  12. package/e2e/tier7-skilltree.test.mjs +506 -0
  13. package/e2e/vitest.config.e2e.mjs +1 -1
  14. package/package.json +6 -2
  15. package/references/agent-inbox/package-lock.json +2 -2
  16. package/references/agent-inbox/package.json +1 -1
  17. package/references/agent-inbox/src/index.ts +16 -2
  18. package/references/agent-inbox/src/ipc/ipc-server.ts +58 -0
  19. package/references/agent-inbox/src/mcp/mcp-proxy.ts +326 -0
  20. package/references/agent-inbox/src/types.ts +26 -0
  21. package/references/agent-inbox/test/ipc-new-commands.test.ts +200 -0
  22. package/references/agent-inbox/test/mcp-proxy.test.ts +191 -0
  23. package/references/minimem/package-lock.json +2 -2
  24. package/references/minimem/package.json +1 -1
  25. package/scripts/bootstrap.mjs +8 -1
  26. package/scripts/map-hook.mjs +6 -2
  27. package/scripts/map-sidecar.mjs +19 -0
  28. package/scripts/team-loader.mjs +15 -8
  29. package/skills/swarm/SKILL.md +16 -22
  30. package/src/__tests__/agent-generator.test.mjs +9 -10
  31. package/src/__tests__/context-output.test.mjs +13 -14
  32. package/src/__tests__/e2e-inbox-integration.test.mjs +732 -0
  33. package/src/__tests__/e2e-live-inbox.test.mjs +597 -0
  34. package/src/__tests__/inbox-integration.test.mjs +298 -0
  35. package/src/__tests__/integration.test.mjs +12 -11
  36. package/src/__tests__/skilltree-client.test.mjs +47 -1
  37. package/src/agent-generator.mjs +79 -88
  38. package/src/bootstrap.mjs +24 -3
  39. package/src/context-output.mjs +238 -64
  40. package/src/index.mjs +2 -0
  41. package/src/sidecar-server.mjs +30 -0
  42. package/src/skilltree-client.mjs +50 -5
@@ -0,0 +1,506 @@
1
+ /**
2
+ * Tier 7: Skill-Tree Integration Tests
3
+ *
4
+ * Tests the skill-tree loadout compilation pipeline without LLM calls:
5
+ * 1. parseSkillTreeExtension extracts skilltree namespace from manifest
6
+ * 2. inferProfileFromRole maps role names to profiles
7
+ * 3. compileAllRoleLoadouts processes full manifests
8
+ * 4. Loadouts are cached in skill-loadouts.json per template
9
+ * 5. AGENT.md generation embeds skill loadouts
10
+ * 6. Bootstrap context output includes skill-tree section
11
+ *
12
+ * No LLM calls — exercises pure computation and file I/O.
13
+ *
14
+ * Run:
15
+ * npx vitest run --config e2e/vitest.config.e2e.mjs e2e/tier7-skilltree.test.mjs
16
+ */
17
+
18
+ import { describe, it, expect, afterEach } from "vitest";
19
+ import fs from "fs";
20
+ import path from "path";
21
+ import { fileURLToPath } from "url";
22
+ import { createWorkspace } from "./helpers/workspace.mjs";
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const SHORT_TMPDIR = "/tmp";
26
+
27
+ // Import source modules
28
+ const {
29
+ parseSkillTreeExtension,
30
+ inferProfileFromRole,
31
+ compileRoleLoadout,
32
+ compileAllRoleLoadouts,
33
+ } = await import("../src/skilltree-client.mjs");
34
+
35
+ const { buildCapabilitiesContext } = await import("../src/context-output.mjs");
36
+ const { generateAgentMd } = await import("../src/agent-generator.mjs");
37
+
38
+ // Check if skill-tree is available for compilation tests
39
+ let skillTreeAvailable = false;
40
+ try {
41
+ const st = await import("skill-tree");
42
+ skillTreeAvailable = !!st.createSkillBank;
43
+ } catch {
44
+ // Not installed or no createSkillBank export
45
+ }
46
+
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+ // Group 1: parseSkillTreeExtension — YAML namespace parsing
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+
51
+ describe(
52
+ "tier7: skilltree parseSkillTreeExtension",
53
+ { timeout: 15_000 },
54
+ () => {
55
+ it("extracts defaults and roles from manifest", () => {
56
+ const manifest = {
57
+ skilltree: {
58
+ defaults: {
59
+ profile: "implementation",
60
+ maxSkills: 6,
61
+ },
62
+ roles: {
63
+ orchestrator: { profile: "code-review" },
64
+ executor: { profile: "implementation", tags: ["development"] },
65
+ verifier: { profile: "testing" },
66
+ },
67
+ },
68
+ };
69
+
70
+ const { defaults, roles } = parseSkillTreeExtension(manifest);
71
+
72
+ expect(defaults.profile).toBe("implementation");
73
+ expect(defaults.maxSkills).toBe(6);
74
+ expect(roles.orchestrator.profile).toBe("code-review");
75
+ expect(roles.executor.profile).toBe("implementation");
76
+ expect(roles.executor.tags).toEqual(["development"]);
77
+ expect(roles.verifier.profile).toBe("testing");
78
+ });
79
+
80
+ it("returns empty defaults and roles when no skilltree namespace", () => {
81
+ const { defaults, roles } = parseSkillTreeExtension({});
82
+ expect(defaults).toEqual({});
83
+ expect(roles).toEqual({});
84
+ });
85
+
86
+ it("returns empty defaults and roles for null manifest", () => {
87
+ const { defaults, roles } = parseSkillTreeExtension(null);
88
+ expect(defaults).toEqual({});
89
+ expect(roles).toEqual({});
90
+ });
91
+
92
+ it("handles missing defaults or roles gracefully", () => {
93
+ const { defaults, roles } = parseSkillTreeExtension({
94
+ skilltree: { defaults: { profile: "testing" } },
95
+ });
96
+ expect(defaults.profile).toBe("testing");
97
+ expect(roles).toEqual({});
98
+ });
99
+
100
+ it("handles missing defaults with roles present", () => {
101
+ const { defaults, roles } = parseSkillTreeExtension({
102
+ skilltree: {
103
+ roles: { executor: { profile: "implementation" } },
104
+ },
105
+ });
106
+ expect(defaults).toEqual({});
107
+ expect(roles.executor.profile).toBe("implementation");
108
+ });
109
+ }
110
+ );
111
+
112
+ // ─────────────────────────────────────────────────────────────────────────────
113
+ // Group 2: inferProfileFromRole — role name to profile mapping
114
+ // ─────────────────────────────────────────────────────────────────────────────
115
+
116
+ describe(
117
+ "tier7: skilltree inferProfileFromRole",
118
+ { timeout: 15_000 },
119
+ () => {
120
+ it("maps known role names to profiles", () => {
121
+ expect(inferProfileFromRole("executor")).toBe("implementation");
122
+ expect(inferProfileFromRole("developer")).toBe("implementation");
123
+ expect(inferProfileFromRole("debugger")).toBe("debugging");
124
+ expect(inferProfileFromRole("verifier")).toBe("testing");
125
+ expect(inferProfileFromRole("qa")).toBe("testing");
126
+ expect(inferProfileFromRole("architect")).toBe("refactoring");
127
+ expect(inferProfileFromRole("tech-writer")).toBe("documentation");
128
+ expect(inferProfileFromRole("security-auditor")).toBe("security");
129
+ expect(inferProfileFromRole("plan-checker")).toBe("code-review");
130
+ expect(inferProfileFromRole("integration-checker")).toBe("code-review");
131
+ });
132
+
133
+ it("handles partial matches (e.g. senior-developer)", () => {
134
+ expect(inferProfileFromRole("senior-developer")).toBe("implementation");
135
+ expect(inferProfileFromRole("lead-executor")).toBe("implementation");
136
+ expect(inferProfileFromRole("junior-debugger")).toBe("debugging");
137
+ });
138
+
139
+ it("returns empty string for unknown roles", () => {
140
+ expect(inferProfileFromRole("orchestrator")).toBe("");
141
+ expect(inferProfileFromRole("coordinator")).toBe("");
142
+ expect(inferProfileFromRole("unknown-role")).toBe("");
143
+ });
144
+ }
145
+ );
146
+
147
+ // ─────────────────────────────────────────────────────────────────────────────
148
+ // Group 3: compileRoleLoadout — single role compilation
149
+ // ─────────────────────────────────────────────────────────────────────────────
150
+
151
+ describe(
152
+ "tier7: skilltree compileRoleLoadout",
153
+ { timeout: 30_000 },
154
+ () => {
155
+ let workspace;
156
+
157
+ afterEach(() => {
158
+ if (workspace) {
159
+ workspace.cleanup();
160
+ workspace = null;
161
+ }
162
+ });
163
+
164
+ it("returns empty string when basePath does not exist", async () => {
165
+ const result = await compileRoleLoadout("executor", { profile: "implementation" }, {
166
+ basePath: "/tmp/nonexistent-skill-tree-" + Date.now(),
167
+ });
168
+ expect(result).toBe("");
169
+ });
170
+
171
+ it("returns empty string when no criteria specified", async () => {
172
+ const result = await compileRoleLoadout("executor", {}, {
173
+ basePath: "/tmp/nonexistent-skill-tree-" + Date.now(),
174
+ });
175
+ expect(result).toBe("");
176
+ });
177
+
178
+ it.skipIf(!skillTreeAvailable)(
179
+ "compiles loadout from profile when skill-tree basePath exists",
180
+ async () => {
181
+ workspace = createWorkspace({
182
+ tmpdir: SHORT_TMPDIR,
183
+ prefix: "t7-st-compile-",
184
+ config: { template: "gsd", skilltree: { enabled: true } },
185
+ });
186
+
187
+ const stDir = path.join(workspace.dir, ".swarm", "skill-tree");
188
+ fs.mkdirSync(stDir, { recursive: true });
189
+
190
+ // Initialize skill-tree in the workspace
191
+ try {
192
+ const { createSkillBank } = await import("skill-tree");
193
+ const bank = createSkillBank({ storage: { basePath: stDir } });
194
+ await bank.initialize();
195
+ await bank.shutdown();
196
+ } catch (err) {
197
+ console.log("[tier7] skill-tree init skipped:", err.message);
198
+ return;
199
+ }
200
+
201
+ const result = await compileRoleLoadout("executor", { profile: "implementation" }, {
202
+ basePath: stDir,
203
+ });
204
+
205
+ console.log("[tier7] compiled loadout length:", result.length);
206
+ // May be empty if no skills are loaded in the test bank
207
+ // The key assertion is that it didn't throw
208
+ }
209
+ );
210
+ }
211
+ );
212
+
213
+ // ─────────────────────────────────────────────────────────────────────────────
214
+ // Group 4: compileAllRoleLoadouts — full manifest compilation
215
+ // ─────────────────────────────────────────────────────────────────────────────
216
+
217
+ describe(
218
+ "tier7: skilltree compileAllRoleLoadouts",
219
+ { timeout: 30_000 },
220
+ () => {
221
+ it("processes all roles with overrides and defaults", async () => {
222
+ const manifest = {
223
+ roles: ["orchestrator", "executor", "verifier", "debugger"],
224
+ skilltree: {
225
+ defaults: { profile: "implementation" },
226
+ roles: {
227
+ orchestrator: { profile: "code-review" },
228
+ verifier: { profile: "testing" },
229
+ },
230
+ },
231
+ };
232
+
233
+ // With a nonexistent basePath, all compilations return empty
234
+ // but the function should process all roles without error
235
+ const result = await compileAllRoleLoadouts(manifest, {
236
+ basePath: "/tmp/nonexistent-st-" + Date.now(),
237
+ });
238
+
239
+ // Result is an object mapping role names to { content, profile }
240
+ expect(typeof result).toBe("object");
241
+ console.log("[tier7] compiled roles:", Object.keys(result).join(", "));
242
+ });
243
+
244
+ it("skips roles with no criteria and no inferred profile", async () => {
245
+ const manifest = {
246
+ roles: ["custom-role-xyz"],
247
+ // No skilltree namespace at all
248
+ };
249
+
250
+ const result = await compileAllRoleLoadouts(manifest, {
251
+ basePath: "/tmp/nonexistent-st-" + Date.now(),
252
+ });
253
+
254
+ // custom-role-xyz has no match in ROLE_PROFILE_MAP and no defaults
255
+ expect(Object.keys(result)).not.toContain("custom-role-xyz");
256
+ });
257
+
258
+ it("uses defaultProfile from config as fallback", async () => {
259
+ const manifest = {
260
+ roles: ["custom-role-xyz"],
261
+ // No skilltree namespace
262
+ };
263
+
264
+ const result = await compileAllRoleLoadouts(manifest, {
265
+ basePath: "/tmp/nonexistent-st-" + Date.now(),
266
+ defaultProfile: "implementation",
267
+ });
268
+
269
+ // With defaultProfile, it should attempt compilation
270
+ // (returns empty since basePath doesn't exist, but no error)
271
+ console.log("[tier7] with defaultProfile, compiled:", Object.keys(result).join(", "));
272
+ });
273
+
274
+ it("auto-infers profiles from role names", async () => {
275
+ const manifest = {
276
+ roles: ["executor", "debugger", "verifier"],
277
+ // No skilltree namespace — should use inferProfileFromRole
278
+ };
279
+
280
+ const result = await compileAllRoleLoadouts(manifest, {
281
+ basePath: "/tmp/nonexistent-st-" + Date.now(),
282
+ });
283
+
284
+ // These roles have known mappings in ROLE_PROFILE_MAP
285
+ // Compilation returns empty (no basePath), but the function ran without error
286
+ console.log("[tier7] auto-inferred roles processed:", Object.keys(result).join(", "));
287
+ });
288
+ }
289
+ );
290
+
291
+ // ─────────────────────────────────────────────────────────────────────────────
292
+ // Group 5: Context Output — buildCapabilitiesContext includes skill-tree
293
+ // ─────────────────────────────────────────────────────────────────────────────
294
+
295
+ describe(
296
+ "tier7: skilltree context output",
297
+ { timeout: 15_000 },
298
+ () => {
299
+ it("includes per-role skills section when enabled and ready", () => {
300
+ const context = buildCapabilitiesContext({
301
+ skilltreeEnabled: true,
302
+ skilltreeStatus: "ready",
303
+ });
304
+
305
+ expect(context).toContain("### Per-Role Skills");
306
+ expect(context).toContain("skill loadout");
307
+ });
308
+
309
+ it("shows status when installed but not ready", () => {
310
+ const context = buildCapabilitiesContext({
311
+ skilltreeEnabled: true,
312
+ skilltreeStatus: "installed",
313
+ });
314
+
315
+ expect(context).toContain("### Per-Role Skills");
316
+ });
317
+
318
+ it("omits skill-tree section when disabled", () => {
319
+ const context = buildCapabilitiesContext({
320
+ skilltreeEnabled: false,
321
+ skilltreeStatus: "disabled",
322
+ });
323
+
324
+ expect(context).not.toContain("### Per-Role Skills");
325
+ });
326
+
327
+ it("main agent context describes team-wide loadout config", () => {
328
+ const context = buildCapabilitiesContext({
329
+ role: null,
330
+ skilltreeEnabled: true,
331
+ skilltreeStatus: "ready",
332
+ });
333
+
334
+ expect(context).toContain("skill loadout");
335
+ expect(context).toContain("profiles");
336
+ });
337
+
338
+ it("spawned agent context references embedded skills section", () => {
339
+ const context = buildCapabilitiesContext({
340
+ role: "executor",
341
+ teamName: "gsd",
342
+ skilltreeEnabled: true,
343
+ skilltreeStatus: "ready",
344
+ });
345
+
346
+ expect(context).toContain("## Skills");
347
+ expect(context).toContain("skill loadout");
348
+ });
349
+
350
+ it("spawned agent context shows profile name when provided", () => {
351
+ const context = buildCapabilitiesContext({
352
+ role: "executor",
353
+ teamName: "gsd",
354
+ skilltreeEnabled: true,
355
+ skilltreeStatus: "ready",
356
+ skillProfile: "implementation",
357
+ });
358
+
359
+ expect(context).toContain("implementation");
360
+ });
361
+ }
362
+ );
363
+
364
+ // ─────────────────────────────────────────────────────────────────────────────
365
+ // Group 6: Agent Generation — AGENT.md embeds skill loadout
366
+ // ─────────────────────────────────────────────────────────────────────────────
367
+
368
+ describe(
369
+ "tier7: skilltree agent generation",
370
+ { timeout: 15_000 },
371
+ () => {
372
+ it("generateAgentMd includes Skills section when loadout provided", () => {
373
+ const md = generateAgentMd({
374
+ roleName: "executor",
375
+ teamName: "gsd",
376
+ position: "spawned",
377
+ description: "Executor agent",
378
+ tools: ["Read", "Write", "Bash"],
379
+ skillContent: "# Role: executor\n\nTest content.",
380
+ manifest: {},
381
+ skilltreeEnabled: true,
382
+ skilltreeStatus: "ready",
383
+ skillLoadout: "## Skill: Clean Code\n\nWrite clean, readable code.",
384
+ skillProfile: "implementation",
385
+ });
386
+
387
+ expect(md).toContain("## Skills");
388
+ expect(md).toContain("Clean Code");
389
+ expect(md).toContain("Write clean, readable code");
390
+ });
391
+
392
+ it("generateAgentMd omits Skills section when no loadout", () => {
393
+ const md = generateAgentMd({
394
+ roleName: "executor",
395
+ teamName: "gsd",
396
+ position: "spawned",
397
+ description: "Executor agent",
398
+ tools: ["Read", "Write", "Bash"],
399
+ skillContent: "# Role: executor\n\nTest content.",
400
+ manifest: {},
401
+ skilltreeEnabled: true,
402
+ skilltreeStatus: "ready",
403
+ // No skillLoadout
404
+ });
405
+
406
+ // Should not have the actual "## Skills" heading as a standalone section
407
+ // (it may appear in capabilities context as a reference like "## Skills section above")
408
+ expect(md).not.toMatch(/^## Skills$/m);
409
+ });
410
+
411
+ it("generateAgentMd includes capabilities context with skill-tree info", () => {
412
+ const md = generateAgentMd({
413
+ roleName: "verifier",
414
+ teamName: "gsd",
415
+ position: "spawned",
416
+ description: "Verifier agent",
417
+ tools: ["Read", "Bash"],
418
+ skillContent: "# Role: verifier\n\nTest content.",
419
+ manifest: {},
420
+ skilltreeEnabled: true,
421
+ skilltreeStatus: "ready",
422
+ skillProfile: "testing",
423
+ });
424
+
425
+ expect(md).toContain("### Per-Role Skills");
426
+ expect(md).toContain("testing");
427
+ });
428
+ }
429
+ );
430
+
431
+ // ─────────────────────────────────────────────────────────────────────────────
432
+ // Group 7: Cached Loadouts — skill-loadouts.json read by agent generator
433
+ // ─────────────────────────────────────────────────────────────────────────────
434
+
435
+ describe(
436
+ "tier7: skilltree cached loadouts",
437
+ { timeout: 15_000 },
438
+ () => {
439
+ let workspace;
440
+
441
+ afterEach(() => {
442
+ if (workspace) {
443
+ workspace.cleanup();
444
+ workspace = null;
445
+ }
446
+ });
447
+
448
+ it("agent generator reads skill-loadouts.json for loadout content", async () => {
449
+ workspace = createWorkspace({
450
+ tmpdir: SHORT_TMPDIR,
451
+ prefix: "t7-st-cache-",
452
+ config: { template: "gsd", skilltree: { enabled: true } },
453
+ });
454
+
455
+ const cacheDir = path.join(workspace.dir, ".swarm", "claude-swarm", "tmp", "teams", "gsd");
456
+ const agentsDir = path.join(cacheDir, "agents");
457
+ fs.mkdirSync(agentsDir, { recursive: true });
458
+
459
+ // Write a cached loadouts file
460
+ const loadouts = {
461
+ executor: {
462
+ content: "## Skill: Test-Driven Development\n\nAlways write tests first.",
463
+ profile: "implementation",
464
+ },
465
+ verifier: {
466
+ content: "## Skill: Code Review\n\nReview code thoroughly.",
467
+ profile: "testing",
468
+ },
469
+ };
470
+ fs.writeFileSync(
471
+ path.join(cacheDir, "skill-loadouts.json"),
472
+ JSON.stringify(loadouts)
473
+ );
474
+
475
+ // Verify the file is readable and parseable
476
+ const read = JSON.parse(
477
+ fs.readFileSync(path.join(cacheDir, "skill-loadouts.json"), "utf-8")
478
+ );
479
+ expect(read.executor.content).toContain("Test-Driven Development");
480
+ expect(read.executor.profile).toBe("implementation");
481
+ expect(read.verifier.content).toContain("Code Review");
482
+ });
483
+
484
+ it("handles legacy string format in skill-loadouts.json", () => {
485
+ // The agent generator handles both { content, profile } and plain string
486
+ const legacyLoadouts = {
487
+ executor: "## Skill: Legacy Format\n\nOld-style loadout.",
488
+ };
489
+
490
+ // Simulate the getLoadout helper from agent-generator
491
+ function getLoadout(roleName) {
492
+ const entry = legacyLoadouts[roleName];
493
+ if (!entry) return { content: "", profile: "" };
494
+ if (typeof entry === "string") return { content: entry, profile: "" };
495
+ return { content: entry.content || "", profile: entry.profile || "" };
496
+ }
497
+
498
+ const { content, profile } = getLoadout("executor");
499
+ expect(content).toContain("Legacy Format");
500
+ expect(profile).toBe("");
501
+
502
+ const { content: missing } = getLoadout("nonexistent");
503
+ expect(missing).toBe("");
504
+ });
505
+ }
506
+ );
@@ -12,7 +12,7 @@ export default defineConfig({
12
12
  maxForks: 1,
13
13
  server: {
14
14
  deps: {
15
- external: ["openteams", "agent-inbox", "@multi-agent-protocol/sdk"],
15
+ external: ["openteams", "agent-inbox", "@multi-agent-protocol/sdk", "opentasks", "minimem", "skill-tree"],
16
16
  },
17
17
  },
18
18
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
5
5
  "type": "module",
6
6
  "exports": {
@@ -47,6 +47,7 @@
47
47
  "test:e2e:tier3": "vitest run --config e2e/vitest.config.e2e.mjs -t tier3",
48
48
  "test:e2e:tier4": "vitest run --config e2e/vitest.config.e2e.mjs -t tier4",
49
49
  "test:e2e:tier5": "vitest run --config e2e/vitest.config.e2e.mjs -t tier5",
50
+ "test:e2e:tier7": "vitest run --config e2e/vitest.config.e2e.mjs -t tier7",
50
51
  "version:patch": "npm version patch --no-git-tag-version && node scripts/sync-version.mjs",
51
52
  "version:minor": "npm version minor --no-git-tag-version && node scripts/sync-version.mjs",
52
53
  "version:major": "npm version major --no-git-tag-version && node scripts/sync-version.mjs"
@@ -56,7 +57,10 @@
56
57
  },
57
58
  "devDependencies": {
58
59
  "@multi-agent-protocol/sdk": "^0.1.4",
59
- "agent-inbox": "^0.1.7",
60
+ "agent-inbox": "^0.1.9",
61
+ "minimem": "^0.1.0",
62
+ "opentasks": "^0.0.6",
63
+ "skill-tree": "^0.1.5",
60
64
  "vitest": "^4.0.18",
61
65
  "ws": "^8.0.0"
62
66
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "agent-inbox",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "agent-inbox",
9
- "version": "0.1.8",
9
+ "version": "0.1.9",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-inbox",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Agent Inbox — message routing, traceability, and MCP tools for multi-agent systems",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,6 +26,7 @@ export { TraceabilityLayer } from "./traceability/traceability.js";
26
26
  export { IpcServer } from "./ipc/ipc-server.js";
27
27
  export { MapClient } from "./map/map-client.js";
28
28
  export { InboxMcpServer } from "./mcp/mcp-server.js";
29
+ export { InboxMcpProxy } from "./mcp/mcp-proxy.js";
29
30
  export { MailJsonRpcServer } from "./jsonrpc/mail-server.js";
30
31
  export { PushNotifier, formatInboxMarkdown, parseCommand } from "./push/notifier.js";
31
32
  export type { InboxMessageEvent, NotifierConfig, InboxFileEntry } from "./push/notifier.js";
@@ -346,8 +347,21 @@ const isMainModule =
346
347
  if (isMainModule) {
347
348
  const mode = process.argv[2] ?? "ipc";
348
349
 
349
- if (mode === "mcp") {
350
- // MCP-only mode (stdio transport for Claude Code integration)
350
+ if (mode === "mcp-proxy" || (mode === "mcp" && process.env.INBOX_SOCKET_PATH)) {
351
+ // MCP proxy mode forward all tool calls to an existing inbox IPC socket.
352
+ // Auto-detected when INBOX_SOCKET_PATH is set in mcp mode.
353
+ const { InboxMcpProxy } = await import("./mcp/mcp-proxy.js");
354
+ const socketPath = process.env.INBOX_SOCKET_PATH ?? defaultSocketPath();
355
+ const agentId = process.env.INBOX_AGENT_ID ?? "anonymous";
356
+ const scope = process.env.INBOX_SCOPE ?? "default";
357
+ console.error(`[agent-inbox] MCP proxy mode → ${socketPath} (agent: ${agentId})`);
358
+ const proxy = new InboxMcpProxy(socketPath, agentId, scope);
359
+ proxy.start().catch((err) => {
360
+ console.error("MCP proxy failed:", err);
361
+ process.exit(1);
362
+ });
363
+ } else if (mode === "mcp") {
364
+ // MCP standalone mode (stdio transport, own storage)
351
365
  const config = loadConfig();
352
366
  const sqlitePath = process.env.INBOX_SQLITE_PATH;
353
367
  const storage: Storage = sqlitePath
@@ -117,6 +117,12 @@ export class IpcServer {
117
117
  case "check_inbox":
118
118
  return this.handleCheckInbox(command);
119
119
 
120
+ case "read_thread":
121
+ return this.handleReadThread(command);
122
+
123
+ case "list_agents":
124
+ return this.handleListAgents(command);
125
+
120
126
  case "emit":
121
127
  // For Phase 1, emit is acknowledged but not forwarded to MAP
122
128
  return { ok: true };
@@ -170,6 +176,58 @@ export class IpcServer {
170
176
  return { ok: true, messages };
171
177
  }
172
178
 
179
+ private handleReadThread(
180
+ command: Extract<IpcCommand, { action: "read_thread" }>
181
+ ): IpcResponse {
182
+ const messages = this.storage.getThread({
183
+ threadTag: command.threadTag,
184
+ scope: command.scope ?? "default",
185
+ });
186
+ return {
187
+ ok: true,
188
+ threadTag: command.threadTag,
189
+ count: messages.length,
190
+ messages: messages.map((m) => ({
191
+ ...m,
192
+ // Flatten for IPC consumers — keep full message but ensure key fields are accessible
193
+ })),
194
+ };
195
+ }
196
+
197
+ private handleListAgents(
198
+ command: Extract<IpcCommand, { action: "list_agents" }>
199
+ ): IpcResponse {
200
+ const agents = this.storage.listAgents(command.scope);
201
+ const localList = agents.map((a) => ({
202
+ agentId: a.agent_id,
203
+ name: a.display_name,
204
+ scope: a.scope,
205
+ status: a.status,
206
+ program: a.program,
207
+ lastActive: a.last_active_at,
208
+ location: "local" as const,
209
+ }));
210
+
211
+ let federatedList: IpcResponse["agents"] = [];
212
+ if (command.includeFederated && this.router.getFederation?.()) {
213
+ const federation = this.router.getFederation!()!;
214
+ const routingTable = federation.routing.getTable();
215
+ federatedList = routingTable.map((entry) => ({
216
+ agentId: entry.agentId,
217
+ peerId: entry.peerId,
218
+ scope: command.scope ?? "default",
219
+ status: entry.status,
220
+ location: "federated" as const,
221
+ }));
222
+ }
223
+
224
+ return {
225
+ ok: true,
226
+ count: localList.length + (federatedList?.length ?? 0),
227
+ agents: [...localList, ...(federatedList ?? [])],
228
+ };
229
+ }
230
+
173
231
  private handleNotify(
174
232
  command: Extract<IpcCommand, { action: "notify" }>
175
233
  ): IpcResponse {