claude-code-swarm 0.3.12 → 0.3.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/hooks.json +22 -22
- package/package.json +3 -3
- package/scripts/map-sidecar.mjs +106 -0
- package/src/__tests__/opentasks-connector.test.mjs +216 -0
- package/src/__tests__/sessionlog.test.mjs +40 -0
- package/src/__tests__/sidecar-server.test.mjs +2 -2
- package/src/content-provider.mjs +176 -0
- package/src/map-connection.mjs +6 -2
- package/src/opentasks-connector.mjs +86 -0
- package/src/sessionlog.mjs +19 -0
- package/src/sidecar-server.mjs +16 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-swarm",
|
|
3
3
|
"description": "Spin up Claude Code agent teams from openteams YAML topologies with optional MAP (Multi-Agent Protocol) observability and coordination. Provides hooks for session lifecycle, agent spawn/complete tracking, and a /swarm skill to launch team configurations.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.16",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "alexngai"
|
|
7
7
|
},
|
package/hooks/hooks.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"type": "command",
|
|
13
|
-
"command": "
|
|
13
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch session-start"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"hooks": [
|
|
22
22
|
{
|
|
23
23
|
"type": "command",
|
|
24
|
-
"command": "
|
|
24
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch session-end"
|
|
25
25
|
}
|
|
26
26
|
]
|
|
27
27
|
}
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"hooks": [
|
|
33
33
|
{
|
|
34
34
|
"type": "command",
|
|
35
|
-
"command": "
|
|
35
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" inject"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"type": "command",
|
|
39
|
-
"command": "
|
|
39
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch user-prompt-submit"
|
|
40
40
|
}
|
|
41
41
|
]
|
|
42
42
|
}
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"hooks": [
|
|
48
48
|
{
|
|
49
49
|
"type": "command",
|
|
50
|
-
"command": "
|
|
50
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch pre-task"
|
|
51
51
|
}
|
|
52
52
|
]
|
|
53
53
|
}
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"hooks": [
|
|
59
59
|
{
|
|
60
60
|
"type": "command",
|
|
61
|
-
"command": "
|
|
61
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit((c.opentasks?.enabled||process.env.SWARM_OPENTASKS_ENABLED)&&(c.map?.enabled||c.map?.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED)?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" opentasks-mcp-used"
|
|
62
62
|
}
|
|
63
63
|
]
|
|
64
64
|
},
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"hooks": [
|
|
68
68
|
{
|
|
69
69
|
"type": "command",
|
|
70
|
-
"command": "
|
|
70
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task"
|
|
71
71
|
}
|
|
72
72
|
]
|
|
73
73
|
},
|
|
@@ -76,11 +76,11 @@
|
|
|
76
76
|
"hooks": [
|
|
77
77
|
{
|
|
78
78
|
"type": "command",
|
|
79
|
-
"command": "
|
|
79
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" native-task-created"
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"type": "command",
|
|
83
|
-
"command": "
|
|
83
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task-create"
|
|
84
84
|
}
|
|
85
85
|
]
|
|
86
86
|
},
|
|
@@ -89,11 +89,11 @@
|
|
|
89
89
|
"hooks": [
|
|
90
90
|
{
|
|
91
91
|
"type": "command",
|
|
92
|
-
"command": "
|
|
92
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" native-task-updated"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"type": "command",
|
|
96
|
-
"command": "
|
|
96
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task-update"
|
|
97
97
|
}
|
|
98
98
|
]
|
|
99
99
|
},
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"hooks": [
|
|
103
103
|
{
|
|
104
104
|
"type": "command",
|
|
105
|
-
"command": "
|
|
105
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-todo"
|
|
106
106
|
}
|
|
107
107
|
]
|
|
108
108
|
},
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"hooks": [
|
|
112
112
|
{
|
|
113
113
|
"type": "command",
|
|
114
|
-
"command": "
|
|
114
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-plan-enter"
|
|
115
115
|
}
|
|
116
116
|
]
|
|
117
117
|
},
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"hooks": [
|
|
121
121
|
{
|
|
122
122
|
"type": "command",
|
|
123
|
-
"command": "
|
|
123
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-plan-exit"
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"hooks": [
|
|
130
130
|
{
|
|
131
131
|
"type": "command",
|
|
132
|
-
"command": "
|
|
132
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-skill"
|
|
133
133
|
}
|
|
134
134
|
]
|
|
135
135
|
}
|
|
@@ -140,15 +140,15 @@
|
|
|
140
140
|
"hooks": [
|
|
141
141
|
{
|
|
142
142
|
"type": "command",
|
|
143
|
-
"command": "
|
|
143
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" turn-completed"
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"type": "command",
|
|
147
|
-
"command": "
|
|
147
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit((m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED)&&c.sessionlog?.sync&&c.sessionlog.sync!=='off'?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-sync"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"type": "command",
|
|
151
|
-
"command": "
|
|
151
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch stop"
|
|
152
152
|
}
|
|
153
153
|
]
|
|
154
154
|
}
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"hooks": [
|
|
160
160
|
{
|
|
161
161
|
"type": "command",
|
|
162
|
-
"command": "
|
|
162
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" subagent-start"
|
|
163
163
|
}
|
|
164
164
|
]
|
|
165
165
|
}
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
"hooks": [
|
|
171
171
|
{
|
|
172
172
|
"type": "command",
|
|
173
|
-
"command": "
|
|
173
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" subagent-stop"
|
|
174
174
|
}
|
|
175
175
|
]
|
|
176
176
|
}
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"hooks": [
|
|
182
182
|
{
|
|
183
183
|
"type": "command",
|
|
184
|
-
"command": "
|
|
184
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" teammate-idle"
|
|
185
185
|
}
|
|
186
186
|
]
|
|
187
187
|
}
|
|
@@ -192,7 +192,7 @@
|
|
|
192
192
|
"hooks": [
|
|
193
193
|
{
|
|
194
194
|
"type": "command",
|
|
195
|
-
"command": "
|
|
195
|
+
"command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};const m=c.map||{};process.exit(m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" task-completed"
|
|
196
196
|
}
|
|
197
197
|
]
|
|
198
198
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-swarm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"swarm-generate-agents": "./scripts/generate-agents.mjs"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@multi-agent-protocol/sdk": "^0.1.
|
|
21
|
+
"@multi-agent-protocol/sdk": "^0.1.8",
|
|
22
22
|
"agentic-mesh": "^0.2.0",
|
|
23
23
|
"js-yaml": "^4.1.0"
|
|
24
24
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"agent-inbox": "^0.1.9",
|
|
57
57
|
"minimem": "^0.1.0",
|
|
58
|
-
"opentasks": "^0.0.
|
|
58
|
+
"opentasks": "^0.0.8",
|
|
59
59
|
"skill-tree": "^0.1.5",
|
|
60
60
|
"vitest": "^4.0.18",
|
|
61
61
|
"ws": "^8.0.0"
|
package/scripts/map-sidecar.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { SOCKET_PATH, PID_PATH, INBOX_SOCKET_PATH, sessionPaths, pluginDir } fro
|
|
|
24
24
|
import { connectToMAP } from "../src/map-connection.mjs";
|
|
25
25
|
import { createMeshPeer, createMeshInbox } from "../src/mesh-connection.mjs";
|
|
26
26
|
import { createSocketServer, createCommandHandler } from "../src/sidecar-server.mjs";
|
|
27
|
+
import { createContentProvider } from "../src/content-provider.mjs";
|
|
27
28
|
import { readConfig } from "../src/config.mjs";
|
|
28
29
|
import { createLogger, init as initLog } from "../src/log.mjs";
|
|
29
30
|
import { configureNodePath, resolvePackage } from "../src/swarmkit-resolver.mjs";
|
|
@@ -50,6 +51,38 @@ const RECONNECT_INTERVAL_MS = parseInt(getArg("reconnect-interval", ""), 10) ||
|
|
|
50
51
|
// Auth credential for server-driven auth negotiation (opaque — type determined by server)
|
|
51
52
|
const AUTH_CREDENTIAL = getArg("credential", "");
|
|
52
53
|
|
|
54
|
+
// Project context for swarm identification (sent as agent metadata)
|
|
55
|
+
import { execSync } from "child_process";
|
|
56
|
+
function getProjectContext() {
|
|
57
|
+
const context = {};
|
|
58
|
+
try { context.project = path.basename(process.cwd()); } catch {}
|
|
59
|
+
try {
|
|
60
|
+
context.branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
61
|
+
encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"],
|
|
62
|
+
}).trim();
|
|
63
|
+
} catch {}
|
|
64
|
+
try {
|
|
65
|
+
const config = readConfig();
|
|
66
|
+
if (config.template) context.template = config.template;
|
|
67
|
+
|
|
68
|
+
// Include task_graph metadata when opentasks is enabled
|
|
69
|
+
if (config.opentasks?.enabled) {
|
|
70
|
+
try {
|
|
71
|
+
const opentasksDir = path.resolve(".opentasks");
|
|
72
|
+
const configPath = path.join(opentasksDir, "config.json");
|
|
73
|
+
const taskGraph = { path: opentasksDir };
|
|
74
|
+
if (fs.existsSync(configPath)) {
|
|
75
|
+
const otConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
76
|
+
if (otConfig.location?.hash) taskGraph.location_hash = otConfig.location.hash;
|
|
77
|
+
}
|
|
78
|
+
context.task_graph = taskGraph;
|
|
79
|
+
} catch { /* opentasks config not available */ }
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
return context;
|
|
83
|
+
}
|
|
84
|
+
const PROJECT_CONTEXT = getProjectContext();
|
|
85
|
+
|
|
53
86
|
// Configure NODE_PATH so dynamic imports of globally-installed packages
|
|
54
87
|
// (@multi-agent-protocol/sdk, agent-inbox, agentic-mesh) resolve correctly.
|
|
55
88
|
// Must happen before any dynamic import() calls.
|
|
@@ -186,6 +219,7 @@ function startSlowReconnectLoop() {
|
|
|
186
219
|
scope: MAP_SCOPE,
|
|
187
220
|
systemId: SYSTEM_ID,
|
|
188
221
|
credential: AUTH_CREDENTIAL || undefined,
|
|
222
|
+
projectContext: PROJECT_CONTEXT,
|
|
189
223
|
onMessage: () => resetInactivityTimer(),
|
|
190
224
|
});
|
|
191
225
|
|
|
@@ -300,15 +334,77 @@ async function tryMeshTransport() {
|
|
|
300
334
|
return true;
|
|
301
335
|
}
|
|
302
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Register opentasks notification handlers on a MAP connection.
|
|
339
|
+
* Delegates to the extracted opentasks-connector module for testability.
|
|
340
|
+
*/
|
|
341
|
+
async function registerOpenTasksHandler(conn) {
|
|
342
|
+
const { registerOpenTasksHandler: _register } = await import("../src/opentasks-connector.mjs");
|
|
343
|
+
return _register(conn, {
|
|
344
|
+
scope: MAP_SCOPE,
|
|
345
|
+
onActivity: resetInactivityTimer,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
303
349
|
/**
|
|
304
350
|
* Start with direct MAP SDK WebSocket transport (fallback).
|
|
305
351
|
*/
|
|
352
|
+
/**
|
|
353
|
+
* Register the trajectory/content.request notification handler on a connection.
|
|
354
|
+
* When the hub sends a content request, the sidecar reads the transcript
|
|
355
|
+
* from sessionlog and responds with a trajectory/content.response notification.
|
|
356
|
+
*/
|
|
357
|
+
function registerContentHandler(conn) {
|
|
358
|
+
if (!conn || typeof conn.onNotification !== "function") return;
|
|
359
|
+
|
|
360
|
+
const contentProvider = createContentProvider();
|
|
361
|
+
|
|
362
|
+
conn.onNotification("trajectory/content.request", async (params) => {
|
|
363
|
+
const requestId = params?.request_id;
|
|
364
|
+
const checkpointId = params?.checkpoint_id;
|
|
365
|
+
if (!requestId) return;
|
|
366
|
+
|
|
367
|
+
log.info("content request received", { requestId, checkpointId });
|
|
368
|
+
resetInactivityTimer();
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const content = checkpointId ? await contentProvider(checkpointId) : null;
|
|
372
|
+
|
|
373
|
+
if (content) {
|
|
374
|
+
conn.sendNotification("trajectory/content.response", {
|
|
375
|
+
request_id: requestId,
|
|
376
|
+
transcript: content.transcript,
|
|
377
|
+
metadata: content.metadata,
|
|
378
|
+
prompts: content.prompts,
|
|
379
|
+
context: content.context,
|
|
380
|
+
});
|
|
381
|
+
log.info("content response sent", { requestId, size: content.transcript.length });
|
|
382
|
+
} else {
|
|
383
|
+
conn.sendNotification("trajectory/content.response", {
|
|
384
|
+
request_id: requestId,
|
|
385
|
+
error: "Content not found",
|
|
386
|
+
});
|
|
387
|
+
log.warn("content not found", { requestId, checkpointId });
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
log.error("content provider error", { requestId, error: err.message });
|
|
391
|
+
try {
|
|
392
|
+
conn.sendNotification("trajectory/content.response", {
|
|
393
|
+
request_id: requestId,
|
|
394
|
+
error: err.message,
|
|
395
|
+
});
|
|
396
|
+
} catch { /* ignore */ }
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
306
401
|
async function startWebSocketTransport() {
|
|
307
402
|
connection = await connectToMAP({
|
|
308
403
|
server: MAP_SERVER,
|
|
309
404
|
scope: MAP_SCOPE,
|
|
310
405
|
systemId: SYSTEM_ID,
|
|
311
406
|
credential: AUTH_CREDENTIAL || undefined,
|
|
407
|
+
projectContext: PROJECT_CONTEXT,
|
|
312
408
|
onMessage: () => {
|
|
313
409
|
resetInactivityTimer();
|
|
314
410
|
},
|
|
@@ -316,6 +412,16 @@ async function startWebSocketTransport() {
|
|
|
316
412
|
|
|
317
413
|
transportMode = "websocket";
|
|
318
414
|
|
|
415
|
+
// Register trajectory content handler for on-demand transcript serving
|
|
416
|
+
if (connection) {
|
|
417
|
+
registerContentHandler(connection);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Register opentasks connector for remote graph queries (only when opentasks is enabled)
|
|
421
|
+
if (connection && PROJECT_CONTEXT.task_graph) {
|
|
422
|
+
await registerOpenTasksHandler(connection);
|
|
423
|
+
}
|
|
424
|
+
|
|
319
425
|
// Start agent-inbox with MAP connection (legacy mode)
|
|
320
426
|
if (INBOX_CONFIG && connection) {
|
|
321
427
|
inboxInstance = await startLegacyAgentInbox(connection);
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { registerOpenTasksHandler } from "../opentasks-connector.mjs";
|
|
3
|
+
|
|
4
|
+
// ── Mock factories ──────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const MOCK_METHODS = {
|
|
7
|
+
QUERY_REQUEST: "opentasks/query.request",
|
|
8
|
+
LINK_REQUEST: "opentasks/link.request",
|
|
9
|
+
ANNOTATE_REQUEST: "opentasks/annotate.request",
|
|
10
|
+
TASK_REQUEST: "opentasks/task.request",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function createMockConnection() {
|
|
14
|
+
const handlers = new Map();
|
|
15
|
+
return {
|
|
16
|
+
onNotification: vi.fn((method, handler) => {
|
|
17
|
+
handlers.set(method, handler);
|
|
18
|
+
}),
|
|
19
|
+
sendNotification: vi.fn(),
|
|
20
|
+
// Test helper: fire a notification as if the hub sent it
|
|
21
|
+
_fireNotification(method, params) {
|
|
22
|
+
const handler = handlers.get(method);
|
|
23
|
+
if (handler) return handler(params);
|
|
24
|
+
},
|
|
25
|
+
_handlers: handlers,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createMockOpentasks() {
|
|
30
|
+
const connector = {
|
|
31
|
+
handleNotification: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
createClient: vi.fn(() => ({ /* mock client */ })),
|
|
35
|
+
createMAPConnector: vi.fn(() => connector),
|
|
36
|
+
MAP_CONNECTOR_METHODS: { ...MOCK_METHODS },
|
|
37
|
+
_connector: connector,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createMockOpentasksClient(socketPath = "/tmp/opentasks/daemon.sock") {
|
|
42
|
+
return {
|
|
43
|
+
findSocketPath: vi.fn(() => socketPath),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe("registerOpenTasksHandler", () => {
|
|
50
|
+
let mockConn;
|
|
51
|
+
let mockOpentasks;
|
|
52
|
+
let mockOtClient;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockConn = createMockConnection();
|
|
56
|
+
mockOpentasks = createMockOpentasks();
|
|
57
|
+
mockOtClient = createMockOpentasksClient();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const callRegister = (connOverride, optsOverride = {}) =>
|
|
61
|
+
registerOpenTasksHandler(connOverride ?? mockConn, {
|
|
62
|
+
scope: "swarm:test",
|
|
63
|
+
importOpentasks: async () => mockOpentasks,
|
|
64
|
+
importOpentasksClient: async () => mockOtClient,
|
|
65
|
+
...optsOverride,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("creates a client with the socket path from findSocketPath", async () => {
|
|
69
|
+
await callRegister();
|
|
70
|
+
|
|
71
|
+
expect(mockOtClient.findSocketPath).toHaveBeenCalled();
|
|
72
|
+
expect(mockOpentasks.createClient).toHaveBeenCalledWith({
|
|
73
|
+
socketPath: "/tmp/opentasks/daemon.sock",
|
|
74
|
+
autoConnect: true,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("creates a MAP connector with the client and a send function", async () => {
|
|
79
|
+
await callRegister();
|
|
80
|
+
|
|
81
|
+
expect(mockOpentasks.createMAPConnector).toHaveBeenCalledTimes(1);
|
|
82
|
+
const callArgs = mockOpentasks.createMAPConnector.mock.calls[0][0];
|
|
83
|
+
|
|
84
|
+
// Should pass the client returned by createClient
|
|
85
|
+
expect(callArgs.client).toBeDefined();
|
|
86
|
+
// Should pass a send function
|
|
87
|
+
expect(typeof callArgs.send).toBe("function");
|
|
88
|
+
// Should pass agentId derived from scope
|
|
89
|
+
expect(callArgs.agentId).toBe("swarm:test-sidecar");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("registers onNotification for all 4 request methods", async () => {
|
|
93
|
+
await callRegister();
|
|
94
|
+
|
|
95
|
+
expect(mockConn.onNotification).toHaveBeenCalledTimes(4);
|
|
96
|
+
|
|
97
|
+
const registeredMethods = mockConn.onNotification.mock.calls.map((c) => c[0]);
|
|
98
|
+
expect(registeredMethods).toContain(MOCK_METHODS.QUERY_REQUEST);
|
|
99
|
+
expect(registeredMethods).toContain(MOCK_METHODS.LINK_REQUEST);
|
|
100
|
+
expect(registeredMethods).toContain(MOCK_METHODS.ANNOTATE_REQUEST);
|
|
101
|
+
expect(registeredMethods).toContain(MOCK_METHODS.TASK_REQUEST);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("forwards notifications to connector.handleNotification", async () => {
|
|
105
|
+
await callRegister();
|
|
106
|
+
|
|
107
|
+
const params = { request_id: "req-1", query: "status:open" };
|
|
108
|
+
await mockConn._fireNotification(MOCK_METHODS.QUERY_REQUEST, params);
|
|
109
|
+
|
|
110
|
+
expect(mockOpentasks._connector.handleNotification).toHaveBeenCalledWith(
|
|
111
|
+
MOCK_METHODS.QUERY_REQUEST,
|
|
112
|
+
params,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("passes empty object when notification params are missing", async () => {
|
|
117
|
+
await callRegister();
|
|
118
|
+
|
|
119
|
+
await mockConn._fireNotification(MOCK_METHODS.TASK_REQUEST, undefined);
|
|
120
|
+
|
|
121
|
+
expect(mockOpentasks._connector.handleNotification).toHaveBeenCalledWith(
|
|
122
|
+
MOCK_METHODS.TASK_REQUEST,
|
|
123
|
+
{},
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("calls the send function on the connector via sendNotification", async () => {
|
|
128
|
+
await callRegister();
|
|
129
|
+
|
|
130
|
+
// Get the send function that was passed to createMAPConnector
|
|
131
|
+
const sendFn = mockOpentasks.createMAPConnector.mock.calls[0][0].send;
|
|
132
|
+
sendFn("opentasks/query.response", { data: "result" });
|
|
133
|
+
|
|
134
|
+
expect(mockConn.sendNotification).toHaveBeenCalledWith(
|
|
135
|
+
"opentasks/query.response",
|
|
136
|
+
{ data: "result" },
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does not throw when sendNotification fails in the send callback", async () => {
|
|
141
|
+
mockConn.sendNotification.mockImplementation(() => {
|
|
142
|
+
throw new Error("connection closed");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await callRegister();
|
|
146
|
+
|
|
147
|
+
const sendFn = mockOpentasks.createMAPConnector.mock.calls[0][0].send;
|
|
148
|
+
// Should not throw
|
|
149
|
+
expect(() => sendFn("method", {})).not.toThrow();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("calls onActivity callback when a notification fires", async () => {
|
|
153
|
+
const onActivity = vi.fn();
|
|
154
|
+
await callRegister(undefined, { onActivity });
|
|
155
|
+
|
|
156
|
+
await mockConn._fireNotification(MOCK_METHODS.LINK_REQUEST, { request_id: "r1" });
|
|
157
|
+
|
|
158
|
+
expect(onActivity).toHaveBeenCalledTimes(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("does nothing when conn is null", async () => {
|
|
162
|
+
// Should not throw
|
|
163
|
+
await registerOpenTasksHandler(null, {
|
|
164
|
+
scope: "swarm:test",
|
|
165
|
+
importOpentasks: async () => mockOpentasks,
|
|
166
|
+
importOpentasksClient: async () => mockOtClient,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(mockOpentasks.createClient).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("does nothing when conn lacks onNotification", async () => {
|
|
173
|
+
await registerOpenTasksHandler({ sendNotification: vi.fn() }, {
|
|
174
|
+
scope: "swarm:test",
|
|
175
|
+
importOpentasks: async () => mockOpentasks,
|
|
176
|
+
importOpentasksClient: async () => mockOtClient,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(mockOpentasks.createClient).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("does nothing when opentasks module is missing createMAPConnector", async () => {
|
|
183
|
+
const brokenModule = { createClient: vi.fn() }; // no createMAPConnector
|
|
184
|
+
|
|
185
|
+
await registerOpenTasksHandler(mockConn, {
|
|
186
|
+
scope: "swarm:test",
|
|
187
|
+
importOpentasks: async () => brokenModule,
|
|
188
|
+
importOpentasksClient: async () => mockOtClient,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(mockConn.onNotification).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("does nothing when opentasks import throws", async () => {
|
|
195
|
+
await registerOpenTasksHandler(mockConn, {
|
|
196
|
+
scope: "swarm:test",
|
|
197
|
+
importOpentasks: async () => { throw new Error("module not found"); },
|
|
198
|
+
importOpentasksClient: async () => mockOtClient,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(mockConn.onNotification).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("uses custom socket path from findSocketPath", async () => {
|
|
205
|
+
const customClient = createMockOpentasksClient("/custom/path/daemon.sock");
|
|
206
|
+
|
|
207
|
+
await callRegister(undefined, {
|
|
208
|
+
importOpentasksClient: async () => customClient,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(mockOpentasks.createClient).toHaveBeenCalledWith({
|
|
212
|
+
socketPath: "/custom/path/daemon.sock",
|
|
213
|
+
autoConnect: true,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -188,6 +188,46 @@ describe("sessionlog", () => {
|
|
|
188
188
|
expect(cp.token_usage.input_tokens).toBe(800);
|
|
189
189
|
expect(cp.token_usage.output_tokens).toBe(400);
|
|
190
190
|
});
|
|
191
|
+
|
|
192
|
+
it("includes project name from cwd in metadata", () => {
|
|
193
|
+
const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig());
|
|
194
|
+
expect(cp.metadata.project).toBeDefined();
|
|
195
|
+
expect(typeof cp.metadata.project).toBe("string");
|
|
196
|
+
expect(cp.metadata.project.length).toBeGreaterThan(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("includes git branch as top-level wire format field", () => {
|
|
200
|
+
const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig());
|
|
201
|
+
// branch may be null in CI/non-git environments, but should be defined
|
|
202
|
+
expect("branch" in cp).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("includes firstPrompt from session state when available", () => {
|
|
206
|
+
const state = { ...baseState, firstPrompt: "fix the bug in server.ts" };
|
|
207
|
+
const cp = buildTrajectoryCheckpoint(state, "lifecycle", makeConfig());
|
|
208
|
+
expect(cp.metadata.firstPrompt).toBe("fix the bug in server.ts");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("truncates long firstPrompt to 200 chars", () => {
|
|
212
|
+
const state = { ...baseState, firstPrompt: "x".repeat(300) };
|
|
213
|
+
const cp = buildTrajectoryCheckpoint(state, "lifecycle", makeConfig());
|
|
214
|
+
expect(cp.metadata.firstPrompt.length).toBe(200);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("omits firstPrompt when not in session state", () => {
|
|
218
|
+
const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig());
|
|
219
|
+
expect(cp.metadata.firstPrompt).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("includes template from config when configured", () => {
|
|
223
|
+
const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig({ template: "gsd" }));
|
|
224
|
+
expect(cp.metadata.template).toBe("gsd");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("omits template when not configured", () => {
|
|
228
|
+
const cp = buildTrajectoryCheckpoint(baseState, "lifecycle", makeConfig({ template: "" }));
|
|
229
|
+
expect(cp.metadata.template).toBeUndefined();
|
|
230
|
+
});
|
|
191
231
|
});
|
|
192
232
|
|
|
193
233
|
describe("ensureSessionlogEnabled", () => {
|
|
@@ -229,13 +229,13 @@ describe("sidecar-server", () => {
|
|
|
229
229
|
|
|
230
230
|
it("falls back to broadcast with trajectory.checkpoint payload when callExtension throws", async () => {
|
|
231
231
|
mockConnection.callExtension.mockRejectedValueOnce(new Error("not supported"));
|
|
232
|
-
const cp = { id: "cp1",
|
|
232
|
+
const cp = { id: "cp1", agent: "a", session_id: "s", files_touched: [], token_usage: null, metadata: { phase: "active" } };
|
|
233
233
|
await handler({ action: "trajectory-checkpoint", checkpoint: cp }, mockClient);
|
|
234
234
|
expect(mockConnection.send).toHaveBeenCalled();
|
|
235
235
|
const [, payload] = mockConnection.send.mock.calls[0];
|
|
236
236
|
expect(payload.type).toBe("trajectory.checkpoint");
|
|
237
237
|
expect(payload.checkpoint.id).toBe("cp1");
|
|
238
|
-
expect(payload.checkpoint.
|
|
238
|
+
expect(payload.checkpoint.agent).toBe("a");
|
|
239
239
|
expect(payload.checkpoint.metadata).toEqual({ phase: "active" });
|
|
240
240
|
});
|
|
241
241
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* content-provider.mjs — Trajectory content provider for claude-code-swarm
|
|
3
|
+
*
|
|
4
|
+
* Provides session transcript content for on-demand trajectory/content requests
|
|
5
|
+
* from the hub. Reads from sessionlog's session state to find the transcript,
|
|
6
|
+
* then serves the raw Claude Code JSONL.
|
|
7
|
+
*
|
|
8
|
+
* Two content sources:
|
|
9
|
+
* 1. Live session: reads directly from state.transcriptPath (active JSONL file)
|
|
10
|
+
* 2. Committed checkpoint: reads from sessionlog's checkpoint store (full.jsonl)
|
|
11
|
+
*
|
|
12
|
+
* Returns { metadata, transcript, prompts, context } matching the
|
|
13
|
+
* SessionContentProvider type from @multi-agent-protocol/sdk.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { SESSIONLOG_DIR } from "./paths.mjs";
|
|
19
|
+
import { resolvePackage } from "./swarmkit-resolver.mjs";
|
|
20
|
+
import { createLogger } from "./log.mjs";
|
|
21
|
+
|
|
22
|
+
const log = createLogger("content-provider");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a content provider function for the sidecar.
|
|
26
|
+
* The provider receives a checkpointId and returns transcript content.
|
|
27
|
+
*
|
|
28
|
+
* For live sessions, checkpointId may be the session ID or a checkpoint ID.
|
|
29
|
+
* We search sessionlog state to find the transcript path.
|
|
30
|
+
*
|
|
31
|
+
* @returns {Function} SessionContentProvider-compatible async function
|
|
32
|
+
*/
|
|
33
|
+
export function createContentProvider() {
|
|
34
|
+
return async function provideContent(checkpointId) {
|
|
35
|
+
try {
|
|
36
|
+
// 1. Try to find a live session with this checkpoint or session ID
|
|
37
|
+
const liveContent = await readLiveSessionContent(checkpointId);
|
|
38
|
+
if (liveContent) return liveContent;
|
|
39
|
+
|
|
40
|
+
// 2. Try to read from committed checkpoint store
|
|
41
|
+
const committedContent = await readCommittedContent(checkpointId);
|
|
42
|
+
if (committedContent) return committedContent;
|
|
43
|
+
|
|
44
|
+
log.warn("content not found for checkpoint", { checkpointId });
|
|
45
|
+
return null;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
log.warn("content provider error", { checkpointId, error: err.message });
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read transcript content from a live (non-ended) sessionlog session.
|
|
55
|
+
* Searches all session state files for one that matches the checkpoint ID
|
|
56
|
+
* or has a matching session ID, then reads the transcript from disk.
|
|
57
|
+
*/
|
|
58
|
+
async function readLiveSessionContent(checkpointId) {
|
|
59
|
+
if (!fs.existsSync(SESSIONLOG_DIR)) return null;
|
|
60
|
+
|
|
61
|
+
let files;
|
|
62
|
+
try {
|
|
63
|
+
files = fs.readdirSync(SESSIONLOG_DIR).filter(f => f.endsWith(".json"));
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const f of files) {
|
|
69
|
+
try {
|
|
70
|
+
const statePath = path.join(SESSIONLOG_DIR, f);
|
|
71
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
72
|
+
|
|
73
|
+
// Match by session ID, checkpoint ID, or checkpoint in turnCheckpointIDs
|
|
74
|
+
const isMatch =
|
|
75
|
+
state.sessionID === checkpointId ||
|
|
76
|
+
state.lastCheckpointID === checkpointId ||
|
|
77
|
+
(state.turnCheckpointIDs || []).includes(checkpointId);
|
|
78
|
+
|
|
79
|
+
if (!isMatch) continue;
|
|
80
|
+
|
|
81
|
+
// Read the transcript from the path stored in session state
|
|
82
|
+
const transcriptPath = state.transcriptPath;
|
|
83
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
84
|
+
log.warn("transcript path not found", { sessionID: state.sessionID, transcriptPath });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const transcript = fs.readFileSync(transcriptPath, "utf-8");
|
|
89
|
+
|
|
90
|
+
// Extract prompts from transcript (user messages)
|
|
91
|
+
const prompts = extractPrompts(transcript);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
metadata: {
|
|
95
|
+
sessionID: state.sessionID,
|
|
96
|
+
phase: state.phase,
|
|
97
|
+
stepCount: state.stepCount || 0,
|
|
98
|
+
filesTouched: state.filesTouched || [],
|
|
99
|
+
tokenUsage: state.tokenUsage || {},
|
|
100
|
+
startedAt: state.startedAt,
|
|
101
|
+
endedAt: state.endedAt,
|
|
102
|
+
source: "live",
|
|
103
|
+
},
|
|
104
|
+
transcript,
|
|
105
|
+
prompts,
|
|
106
|
+
context: `Session ${state.sessionID} (${state.phase})`,
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
// Skip malformed files
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read transcript content from sessionlog's committed checkpoint store.
|
|
118
|
+
* Uses sessionlog's library API via resolvePackage.
|
|
119
|
+
*/
|
|
120
|
+
async function readCommittedContent(checkpointId) {
|
|
121
|
+
try {
|
|
122
|
+
const sessionlogMod = await resolvePackage("sessionlog");
|
|
123
|
+
if (!sessionlogMod?.createCheckpointStore) return null;
|
|
124
|
+
|
|
125
|
+
const store = sessionlogMod.createCheckpointStore();
|
|
126
|
+
if (!store?.readSessionContent) return null;
|
|
127
|
+
|
|
128
|
+
// Try reading committed content (session index 0)
|
|
129
|
+
const content = await store.readSessionContent(checkpointId, 0);
|
|
130
|
+
if (!content) return null;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
metadata: {
|
|
134
|
+
...content.metadata,
|
|
135
|
+
source: "committed",
|
|
136
|
+
},
|
|
137
|
+
transcript: content.transcript,
|
|
138
|
+
prompts: content.prompts,
|
|
139
|
+
context: content.context,
|
|
140
|
+
};
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract user prompts from a Claude Code JSONL transcript.
|
|
148
|
+
*/
|
|
149
|
+
function extractPrompts(transcript) {
|
|
150
|
+
const prompts = [];
|
|
151
|
+
for (const line of transcript.split("\n")) {
|
|
152
|
+
if (!line.trim()) continue;
|
|
153
|
+
try {
|
|
154
|
+
const entry = JSON.parse(line);
|
|
155
|
+
if (entry.type === "user") {
|
|
156
|
+
const msg = entry.message;
|
|
157
|
+
if (typeof msg === "string") {
|
|
158
|
+
prompts.push(msg);
|
|
159
|
+
} else if (msg?.content) {
|
|
160
|
+
if (typeof msg.content === "string") {
|
|
161
|
+
prompts.push(msg.content);
|
|
162
|
+
} else if (Array.isArray(msg.content)) {
|
|
163
|
+
const text = msg.content
|
|
164
|
+
.filter(b => b.type === "text" && b.text)
|
|
165
|
+
.map(b => b.text)
|
|
166
|
+
.join("\n");
|
|
167
|
+
if (text) prompts.push(text);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// Skip malformed lines
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return prompts.join("\n---\n");
|
|
176
|
+
}
|
package/src/map-connection.mjs
CHANGED
|
@@ -24,7 +24,7 @@ const log = createLogger("map");
|
|
|
24
24
|
* authRequired challenge with the server's preferred method + this credential.
|
|
25
25
|
* When absent, uses the standard SDK connect() for open mode servers.
|
|
26
26
|
*/
|
|
27
|
-
export async function connectToMAP({ server, scope, systemId, onMessage, credential }) {
|
|
27
|
+
export async function connectToMAP({ server, scope, systemId, onMessage, credential, projectContext }) {
|
|
28
28
|
try {
|
|
29
29
|
const mapSdk = await resolvePackage("@multi-agent-protocol/sdk");
|
|
30
30
|
if (!mapSdk) throw new Error("@multi-agent-protocol/sdk not available");
|
|
@@ -38,12 +38,16 @@ export async function connectToMAP({ server, scope, systemId, onMessage, credent
|
|
|
38
38
|
role: "sidecar",
|
|
39
39
|
scopes: [scope],
|
|
40
40
|
capabilities: {
|
|
41
|
-
trajectory: { canReport: true },
|
|
41
|
+
trajectory: { canReport: true, canServeContent: true },
|
|
42
42
|
tasks: { canCreate: true, canAssign: true, canUpdate: true, canList: true },
|
|
43
|
+
...(projectContext?.task_graph ? {
|
|
44
|
+
opentasks: { canQuery: true, canLink: true, canAnnotate: true, canTask: true },
|
|
45
|
+
} : {}),
|
|
43
46
|
},
|
|
44
47
|
metadata: {
|
|
45
48
|
systemId,
|
|
46
49
|
type: "claude-code-swarm-sidecar",
|
|
50
|
+
...(projectContext || {}),
|
|
47
51
|
},
|
|
48
52
|
reconnection: {
|
|
49
53
|
enabled: true,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opentasks-connector.mjs — MAP connector registration for opentasks
|
|
3
|
+
*
|
|
4
|
+
* Extracted from map-sidecar.mjs for testability.
|
|
5
|
+
* Registers notification handlers on a MAP connection so that when the hub
|
|
6
|
+
* (or another agent) sends opentasks/*.request notifications, the connector
|
|
7
|
+
* queries the local daemon and sends back opentasks/*.response.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createLogger } from "./log.mjs";
|
|
11
|
+
|
|
12
|
+
const log = createLogger("opentasks-connector");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register opentasks notification handlers on a MAP connection.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} conn - MAP connection with onNotification/sendNotification
|
|
18
|
+
* @param {object} options
|
|
19
|
+
* @param {string} options.scope - MAP scope (e.g. "swarm:gsd")
|
|
20
|
+
* @param {() => void} [options.onActivity] - Called on each notification (for inactivity timer reset)
|
|
21
|
+
* @param {() => Promise<object>} [options.importOpentasks] - Override for dynamic import("opentasks")
|
|
22
|
+
* @param {() => Promise<object>} [options.importOpentasksClient] - Override for dynamic import of opentasks-client
|
|
23
|
+
*/
|
|
24
|
+
export async function registerOpenTasksHandler(conn, options = {}) {
|
|
25
|
+
if (!conn || typeof conn.onNotification !== "function") return;
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
scope = "swarm:default",
|
|
29
|
+
onActivity,
|
|
30
|
+
importOpentasks,
|
|
31
|
+
importOpentasksClient,
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const opentasks = importOpentasks
|
|
36
|
+
? await importOpentasks()
|
|
37
|
+
: await import("opentasks");
|
|
38
|
+
|
|
39
|
+
if (!opentasks?.createMAPConnector || !opentasks?.createClient) {
|
|
40
|
+
log.debug("opentasks MAP connector not available (missing exports)");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { createMAPConnector, createClient, MAP_CONNECTOR_METHODS } = opentasks;
|
|
45
|
+
|
|
46
|
+
const opentasksClient = importOpentasksClient
|
|
47
|
+
? await importOpentasksClient()
|
|
48
|
+
: await import("./opentasks-client.mjs");
|
|
49
|
+
|
|
50
|
+
const { findSocketPath } = opentasksClient;
|
|
51
|
+
const socketPath = findSocketPath();
|
|
52
|
+
const client = createClient({ socketPath, autoConnect: true });
|
|
53
|
+
|
|
54
|
+
const connector = createMAPConnector({
|
|
55
|
+
client,
|
|
56
|
+
send: (method, params) => {
|
|
57
|
+
try {
|
|
58
|
+
conn.sendNotification(method, params);
|
|
59
|
+
} catch {
|
|
60
|
+
log.debug("failed to send opentasks response", { method });
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
agentId: `${scope}-sidecar`,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Register handlers for all 4 request methods
|
|
67
|
+
const requestMethods = [
|
|
68
|
+
MAP_CONNECTOR_METHODS.QUERY_REQUEST,
|
|
69
|
+
MAP_CONNECTOR_METHODS.LINK_REQUEST,
|
|
70
|
+
MAP_CONNECTOR_METHODS.ANNOTATE_REQUEST,
|
|
71
|
+
MAP_CONNECTOR_METHODS.TASK_REQUEST,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const method of requestMethods) {
|
|
75
|
+
conn.onNotification(method, async (params) => {
|
|
76
|
+
log.debug("opentasks request received", { method, requestId: params?.request_id });
|
|
77
|
+
if (onActivity) onActivity();
|
|
78
|
+
connector.handleNotification(method, params || {});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
log.info("opentasks connector registered", { methods: requestMethods.length });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log.debug("opentasks connector not available", { error: err.message });
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/sessionlog.mjs
CHANGED
|
@@ -16,6 +16,20 @@ import { sendToSidecar, ensureSidecar } from "./sidecar-client.mjs";
|
|
|
16
16
|
import { fireAndForgetTrajectory } from "./map-connection.mjs";
|
|
17
17
|
import { resolvePackage } from "./swarmkit-resolver.mjs";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Get the current git branch name. Returns null if not in a git repo.
|
|
21
|
+
*/
|
|
22
|
+
function getGitBranch() {
|
|
23
|
+
try {
|
|
24
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
27
|
+
}).trim() || null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
/**
|
|
20
34
|
* Check if sessionlog is installed and active.
|
|
21
35
|
* Returns 'active', 'installed but not enabled', or 'not installed'.
|
|
@@ -151,6 +165,7 @@ export function buildTrajectoryCheckpoint(state, syncLevel, config) {
|
|
|
151
165
|
id,
|
|
152
166
|
session_id: state.sessionID,
|
|
153
167
|
agent: `${teamName}-sidecar`,
|
|
168
|
+
branch: getGitBranch(),
|
|
154
169
|
files_touched: [],
|
|
155
170
|
checkpoints_count: 0,
|
|
156
171
|
};
|
|
@@ -161,6 +176,10 @@ export function buildTrajectoryCheckpoint(state, syncLevel, config) {
|
|
|
161
176
|
turnId: state.turnID,
|
|
162
177
|
startedAt: state.startedAt,
|
|
163
178
|
label: `Turn ${state.turnID || "?"} (step ${state.stepCount || 0}, ${state.phase || "unknown"})`,
|
|
179
|
+
// Project context for display
|
|
180
|
+
project: path.basename(process.cwd()),
|
|
181
|
+
firstPrompt: state.firstPrompt ? state.firstPrompt.slice(0, 200) : undefined,
|
|
182
|
+
template: config.template || undefined,
|
|
164
183
|
};
|
|
165
184
|
if (state.endedAt) metadata.endedAt = state.endedAt;
|
|
166
185
|
|
package/src/sidecar-server.mjs
CHANGED
|
@@ -94,6 +94,7 @@ export function createSocketServer(socketPath, onCommand) {
|
|
|
94
94
|
export function createCommandHandler(connection, scope, registeredAgents, opts = {}) {
|
|
95
95
|
// Use a getter pattern so the connection ref can be updated
|
|
96
96
|
let conn = connection;
|
|
97
|
+
let _trajectoryResourceId = null; // Cached resource_id from server response
|
|
97
98
|
const { inboxInstance, meshPeer, transportMode = "websocket" } = opts;
|
|
98
99
|
const useMeshRegistry = transportMode === "mesh" && inboxInstance;
|
|
99
100
|
|
|
@@ -291,10 +292,17 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
291
292
|
const c = conn || await waitForConn();
|
|
292
293
|
if (c) {
|
|
293
294
|
try {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
// Include cached resource_id if available from a previous response
|
|
296
|
+
const payload = { checkpoint: command.checkpoint };
|
|
297
|
+
if (_trajectoryResourceId) {
|
|
298
|
+
payload.resource_id = _trajectoryResourceId;
|
|
299
|
+
}
|
|
300
|
+
const result = await c.callExtension("trajectory/checkpoint", payload);
|
|
301
|
+
// Cache resource_id from server response for subsequent calls
|
|
302
|
+
if (result?.resource_id) {
|
|
303
|
+
_trajectoryResourceId = result.resource_id;
|
|
304
|
+
}
|
|
305
|
+
respond(client, { ok: true, method: "trajectory", resource_id: result?.resource_id });
|
|
298
306
|
} catch (err) {
|
|
299
307
|
log.warn("trajectory/checkpoint not supported, falling back to broadcast", { error: err.message });
|
|
300
308
|
await c.send(
|
|
@@ -303,9 +311,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
303
311
|
type: "trajectory.checkpoint",
|
|
304
312
|
checkpoint: {
|
|
305
313
|
id: command.checkpoint.id,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
314
|
+
agent: command.checkpoint.agent,
|
|
315
|
+
session_id: command.checkpoint.session_id,
|
|
316
|
+
files_touched: command.checkpoint.files_touched,
|
|
317
|
+
token_usage: command.checkpoint.token_usage,
|
|
309
318
|
metadata: command.checkpoint.metadata,
|
|
310
319
|
},
|
|
311
320
|
},
|