claude-code-swarm 0.3.11 → 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 +3 -7
- package/hooks/hooks.json +97 -10
- package/package.json +3 -3
- package/scripts/map-hook.mjs +12 -1
- package/scripts/map-sidecar.mjs +106 -0
- package/src/__tests__/bootstrap.test.mjs +34 -5
- package/src/__tests__/helpers.mjs +1 -0
- package/src/__tests__/opentasks-connector.test.mjs +216 -0
- package/src/__tests__/sessionlog-e2e.test.mjs +270 -0
- package/src/__tests__/sessionlog.test.mjs +256 -13
- package/src/__tests__/sidecar-server.test.mjs +2 -2
- package/src/bootstrap.mjs +17 -4
- package/src/config.mjs +6 -2
- package/src/content-provider.mjs +176 -0
- package/src/index.mjs +3 -0
- package/src/map-connection.mjs +6 -2
- package/src/opentasks-connector.mjs +86 -0
- package/src/sessionlog.mjs +163 -12
- 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
|
},
|
|
@@ -12,9 +12,7 @@
|
|
|
12
12
|
"${CLAUDE_PLUGIN_ROOT}/.claude-plugin/mcp-launcher.mjs",
|
|
13
13
|
"opentasks"
|
|
14
14
|
],
|
|
15
|
-
"env": {
|
|
16
|
-
"OPENTASKS_WORKING_DIR": "${workspaceFolder}"
|
|
17
|
-
}
|
|
15
|
+
"env": {}
|
|
18
16
|
},
|
|
19
17
|
"agent-inbox": {
|
|
20
18
|
"command": "node",
|
|
@@ -30,9 +28,7 @@
|
|
|
30
28
|
"${CLAUDE_PLUGIN_ROOT}/.claude-plugin/mcp-launcher.mjs",
|
|
31
29
|
"minimem"
|
|
32
30
|
],
|
|
33
|
-
"env": {
|
|
34
|
-
"MINIMEM_WORKING_DIR": "${workspaceFolder}"
|
|
35
|
-
}
|
|
31
|
+
"env": {}
|
|
36
32
|
}
|
|
37
33
|
}
|
|
38
34
|
}
|
package/hooks/hooks.json
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
9
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.mjs\""
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "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
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"SessionEnd": [
|
|
19
|
+
{
|
|
20
|
+
"matcher": "",
|
|
21
|
+
"hooks": [
|
|
22
|
+
{
|
|
23
|
+
"type": "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"
|
|
10
25
|
}
|
|
11
26
|
]
|
|
12
27
|
}
|
|
@@ -17,7 +32,22 @@
|
|
|
17
32
|
"hooks": [
|
|
18
33
|
{
|
|
19
34
|
"type": "command",
|
|
20
|
-
"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
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"type": "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
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"PreToolUse": [
|
|
45
|
+
{
|
|
46
|
+
"matcher": "Task",
|
|
47
|
+
"hooks": [
|
|
48
|
+
{
|
|
49
|
+
"type": "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"
|
|
21
51
|
}
|
|
22
52
|
]
|
|
23
53
|
}
|
|
@@ -28,7 +58,16 @@
|
|
|
28
58
|
"hooks": [
|
|
29
59
|
{
|
|
30
60
|
"type": "command",
|
|
31
|
-
"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
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"matcher": "Task",
|
|
67
|
+
"hooks": [
|
|
68
|
+
{
|
|
69
|
+
"type": "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"
|
|
32
71
|
}
|
|
33
72
|
]
|
|
34
73
|
},
|
|
@@ -37,7 +76,11 @@
|
|
|
37
76
|
"hooks": [
|
|
38
77
|
{
|
|
39
78
|
"type": "command",
|
|
40
|
-
"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
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"type": "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"
|
|
41
84
|
}
|
|
42
85
|
]
|
|
43
86
|
},
|
|
@@ -46,7 +89,47 @@
|
|
|
46
89
|
"hooks": [
|
|
47
90
|
{
|
|
48
91
|
"type": "command",
|
|
49
|
-
"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
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"type": "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
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"matcher": "TodoWrite",
|
|
102
|
+
"hooks": [
|
|
103
|
+
{
|
|
104
|
+
"type": "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
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"matcher": "EnterPlanMode",
|
|
111
|
+
"hooks": [
|
|
112
|
+
{
|
|
113
|
+
"type": "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
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"matcher": "ExitPlanMode",
|
|
120
|
+
"hooks": [
|
|
121
|
+
{
|
|
122
|
+
"type": "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
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"matcher": "Skill",
|
|
129
|
+
"hooks": [
|
|
130
|
+
{
|
|
131
|
+
"type": "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"
|
|
50
133
|
}
|
|
51
134
|
]
|
|
52
135
|
}
|
|
@@ -57,11 +140,15 @@
|
|
|
57
140
|
"hooks": [
|
|
58
141
|
{
|
|
59
142
|
"type": "command",
|
|
60
|
-
"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
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"type": "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"
|
|
61
148
|
},
|
|
62
149
|
{
|
|
63
150
|
"type": "command",
|
|
64
|
-
"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"
|
|
65
152
|
}
|
|
66
153
|
]
|
|
67
154
|
}
|
|
@@ -72,7 +159,7 @@
|
|
|
72
159
|
"hooks": [
|
|
73
160
|
{
|
|
74
161
|
"type": "command",
|
|
75
|
-
"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"
|
|
76
163
|
}
|
|
77
164
|
]
|
|
78
165
|
}
|
|
@@ -83,7 +170,7 @@
|
|
|
83
170
|
"hooks": [
|
|
84
171
|
{
|
|
85
172
|
"type": "command",
|
|
86
|
-
"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"
|
|
87
174
|
}
|
|
88
175
|
]
|
|
89
176
|
}
|
|
@@ -94,7 +181,7 @@
|
|
|
94
181
|
"hooks": [
|
|
95
182
|
{
|
|
96
183
|
"type": "command",
|
|
97
|
-
"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"
|
|
98
185
|
}
|
|
99
186
|
]
|
|
100
187
|
}
|
|
@@ -105,7 +192,7 @@
|
|
|
105
192
|
"hooks": [
|
|
106
193
|
{
|
|
107
194
|
"type": "command",
|
|
108
|
-
"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"
|
|
109
196
|
}
|
|
110
197
|
]
|
|
111
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-hook.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* teammate-idle — Update teammate state to idle
|
|
20
20
|
* task-completed — Complete task in opentasks + emit bridge event
|
|
21
21
|
* opentasks-mcp-used — Bridge opentasks MCP tool use → MAP task sync payload
|
|
22
|
+
* sessionlog-dispatch — Dispatch a sessionlog lifecycle hook via programmatic API
|
|
22
23
|
*
|
|
23
24
|
* Usage: node map-hook.mjs <action>
|
|
24
25
|
* Hook event data is read from stdin (JSON).
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
handleNativeTaskCreatedEvent,
|
|
49
50
|
handleNativeTaskUpdatedEvent,
|
|
50
51
|
} from "../src/map-events.mjs";
|
|
51
|
-
import { syncSessionlog } from "../src/sessionlog.mjs";
|
|
52
|
+
import { syncSessionlog, dispatchSessionlogHook } from "../src/sessionlog.mjs";
|
|
52
53
|
import { findSocketPath, pushSyncEvent } from "../src/opentasks-client.mjs";
|
|
53
54
|
|
|
54
55
|
const action = process.argv[2];
|
|
@@ -184,6 +185,15 @@ async function handleNativeTaskUpdated(hookData, sessionId) {
|
|
|
184
185
|
await handleNativeTaskUpdatedEvent(config, hookData, sessionId);
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
async function handleSessionlogDispatch(hookData) {
|
|
189
|
+
const sessionlogHookName = process.argv[3];
|
|
190
|
+
if (!sessionlogHookName) {
|
|
191
|
+
log.warn("sessionlog-dispatch: missing hook name argument");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await dispatchSessionlogHook(sessionlogHookName, hookData);
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
188
198
|
|
|
189
199
|
async function main() {
|
|
@@ -204,6 +214,7 @@ async function main() {
|
|
|
204
214
|
case "opentasks-mcp-used": await handleOpentasksMcpUsed(hookData, sessionId); break;
|
|
205
215
|
case "native-task-created": await handleNativeTaskCreated(hookData, sessionId); break;
|
|
206
216
|
case "native-task-updated": await handleNativeTaskUpdated(hookData, sessionId); break;
|
|
217
|
+
case "sessionlog-dispatch": await handleSessionlogDispatch(hookData); break;
|
|
207
218
|
default:
|
|
208
219
|
log.warn("unknown action", { action });
|
|
209
220
|
}
|
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);
|
|
@@ -22,6 +22,8 @@ vi.mock("../map-events.mjs", () => ({
|
|
|
22
22
|
|
|
23
23
|
vi.mock("../sessionlog.mjs", () => ({
|
|
24
24
|
checkSessionlogStatus: vi.fn(() => "not installed"),
|
|
25
|
+
ensureSessionlogEnabled: vi.fn().mockResolvedValue(false),
|
|
26
|
+
hasStandaloneHooks: vi.fn().mockReturnValue(false),
|
|
25
27
|
syncSessionlog: vi.fn().mockResolvedValue(undefined),
|
|
26
28
|
annotateSwarmSession: vi.fn().mockResolvedValue(undefined),
|
|
27
29
|
}));
|
|
@@ -86,7 +88,7 @@ const { bootstrap, backgroundInit } = await import("../bootstrap.mjs");
|
|
|
86
88
|
const { readConfig } = await import("../config.mjs");
|
|
87
89
|
const { killSidecar, startSidecar } = await import("../sidecar-client.mjs");
|
|
88
90
|
const { sendCommand } = await import("../map-events.mjs");
|
|
89
|
-
const { checkSessionlogStatus, syncSessionlog, annotateSwarmSession } = await import("../sessionlog.mjs");
|
|
91
|
+
const { checkSessionlogStatus, ensureSessionlogEnabled, hasStandaloneHooks, syncSessionlog, annotateSwarmSession } = await import("../sessionlog.mjs");
|
|
90
92
|
const { pluginDir, ensureOpentasksDir, ensureSessionDir, listSessionDirs } = await import("../paths.mjs");
|
|
91
93
|
const { findSocketPath, isDaemonAlive, ensureDaemon } = await import("../opentasks-client.mjs");
|
|
92
94
|
const { resolveSwarmkit, configureNodePath } = await import("../swarmkit-resolver.mjs");
|
|
@@ -161,16 +163,43 @@ describe("bootstrap", () => {
|
|
|
161
163
|
});
|
|
162
164
|
|
|
163
165
|
describe("sessionlog", () => {
|
|
164
|
-
it("
|
|
166
|
+
it("defers to standalone when standalone hooks are present", async () => {
|
|
167
|
+
hasStandaloneHooks.mockReturnValue(true);
|
|
165
168
|
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: true }));
|
|
166
169
|
const result = await bootstrap();
|
|
167
|
-
expect(result.sessionlogStatus).toBe("
|
|
170
|
+
expect(result.sessionlogStatus).toBe("active (standalone)");
|
|
171
|
+
expect(ensureSessionlogEnabled).not.toHaveBeenCalled();
|
|
168
172
|
});
|
|
169
173
|
|
|
170
|
-
it("
|
|
174
|
+
it("enables sessionlog when no standalone hooks", async () => {
|
|
175
|
+
hasStandaloneHooks.mockReturnValue(false);
|
|
176
|
+
ensureSessionlogEnabled.mockResolvedValue(true);
|
|
171
177
|
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: true }));
|
|
178
|
+
const result = await bootstrap();
|
|
179
|
+
expect(result.sessionlogStatus).toBe("active");
|
|
180
|
+
expect(ensureSessionlogEnabled).toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("reports status when enable fails", async () => {
|
|
184
|
+
hasStandaloneHooks.mockReturnValue(false);
|
|
185
|
+
ensureSessionlogEnabled.mockResolvedValue(false);
|
|
186
|
+
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: true }));
|
|
187
|
+
const result = await bootstrap();
|
|
188
|
+
expect(result.sessionlogStatus).toBe("installed but not enabled");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns 'checking' when hasStandaloneHooks throws", async () => {
|
|
192
|
+
hasStandaloneHooks.mockImplementation(() => { throw new Error("unexpected"); });
|
|
193
|
+
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: true }));
|
|
194
|
+
const result = await bootstrap();
|
|
195
|
+
expect(result.sessionlogStatus).toBe("checking");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("does not check standalone when disabled", async () => {
|
|
199
|
+
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: false }));
|
|
172
200
|
await bootstrap();
|
|
173
|
-
expect(
|
|
201
|
+
expect(hasStandaloneHooks).not.toHaveBeenCalled();
|
|
202
|
+
expect(ensureSessionlogEnabled).not.toHaveBeenCalled();
|
|
174
203
|
});
|
|
175
204
|
});
|
|
176
205
|
|
|
@@ -32,6 +32,7 @@ export function makeConfig(overrides = {}) {
|
|
|
32
32
|
sessionlog: {
|
|
33
33
|
enabled: overrides.sessionlogEnabled ?? false,
|
|
34
34
|
sync: overrides.sessionlogSync ?? "off",
|
|
35
|
+
mode: overrides.sessionlogMode ?? "auto",
|
|
35
36
|
},
|
|
36
37
|
opentasks: {
|
|
37
38
|
enabled: overrides.opentasksEnabled ?? false,
|