borgmcp 1.0.5 → 1.0.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.
- package/dist/assimilate-cmd.js +39 -497
- package/dist/assimilate-deps.js +3 -177
- package/dist/assimilate-welcome.js +2 -24
- package/dist/auth-env.js +1 -107
- package/dist/auth.js +23 -612
- package/dist/claude.js +11 -281
- package/dist/cli-help.js +29 -50
- package/dist/cli-platform.js +4 -94
- package/dist/codex-app-server.js +4 -228
- package/dist/codex-app-wake.js +2 -122
- package/dist/codex-launch.js +1 -81
- package/dist/codex-remote.js +1 -250
- package/dist/config-utils.js +3 -385
- package/dist/config.js +1 -190
- package/dist/console-prefix.js +1 -86
- package/dist/cube-name.js +1 -65
- package/dist/cubes.js +4 -269
- package/dist/debug.js +1 -71
- package/dist/device-auth.js +1 -167
- package/dist/direct-log.js +1 -11
- package/dist/health-beat.js +1 -168
- package/dist/inbox-monitor.js +1 -129
- package/dist/index.js +26 -1378
- package/dist/lifecycle-log-guard.js +2 -93
- package/dist/list-roles-render.js +6 -39
- package/dist/log-audit.js +3 -186
- package/dist/log-stream.js +9 -848
- package/dist/name-validator.js +1 -22
- package/dist/parse-assimilate-args.js +1 -82
- package/dist/postinstall.js +8 -22
- package/dist/regen-format.js +11 -329
- package/dist/regen.js +5 -83
- package/dist/remote-client.js +1 -695
- package/dist/role-resolver.js +1 -36
- package/dist/role-section.js +8 -208
- package/dist/roster-render.js +3 -96
- package/dist/setup.js +36 -251
- package/dist/shell-escape.js +1 -22
- package/dist/spawn.js +10 -29
- package/dist/stale-version-check.js +1 -102
- package/dist/stream-owner.js +2 -202
- package/dist/stream-status.js +3 -211
- package/dist/subscription-retry.js +1 -23
- package/dist/sync-roles-render.js +3 -118
- package/dist/sync.js +22 -286
- package/dist/templates.js +120 -563
- package/dist/terminal-title.js +1 -68
- package/dist/token-crypto.js +1 -91
- package/dist/token-store.js +1 -222
- package/dist/types.js +0 -5
- package/dist/version.js +2 -78
- package/dist/worktree-lifecycle.js +2 -173
- package/package.json +11 -2
- package/dist/assimilate-cmd.d.ts.map +0 -1
- package/dist/assimilate-cmd.js.map +0 -1
- package/dist/assimilate-deps.d.ts.map +0 -1
- package/dist/assimilate-deps.js.map +0 -1
- package/dist/assimilate-welcome.d.ts.map +0 -1
- package/dist/assimilate-welcome.js.map +0 -1
- package/dist/auth-env.d.ts.map +0 -1
- package/dist/auth-env.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/claude.d.ts.map +0 -1
- package/dist/claude.js.map +0 -1
- package/dist/cli-help.d.ts.map +0 -1
- package/dist/cli-help.js.map +0 -1
- package/dist/cli-platform.d.ts.map +0 -1
- package/dist/cli-platform.js.map +0 -1
- package/dist/codex-app-server.d.ts.map +0 -1
- package/dist/codex-app-server.js.map +0 -1
- package/dist/codex-app-wake.d.ts.map +0 -1
- package/dist/codex-app-wake.js.map +0 -1
- package/dist/codex-launch.d.ts.map +0 -1
- package/dist/codex-launch.js.map +0 -1
- package/dist/codex-remote.d.ts.map +0 -1
- package/dist/codex-remote.js.map +0 -1
- package/dist/config-utils.d.ts.map +0 -1
- package/dist/config-utils.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/console-prefix.d.ts.map +0 -1
- package/dist/console-prefix.js.map +0 -1
- package/dist/cube-name.d.ts.map +0 -1
- package/dist/cube-name.js.map +0 -1
- package/dist/cubes.d.ts.map +0 -1
- package/dist/cubes.js.map +0 -1
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js.map +0 -1
- package/dist/device-auth.d.ts.map +0 -1
- package/dist/device-auth.js.map +0 -1
- package/dist/direct-log.d.ts.map +0 -1
- package/dist/direct-log.js.map +0 -1
- package/dist/health-beat.d.ts.map +0 -1
- package/dist/health-beat.js.map +0 -1
- package/dist/inbox-monitor.d.ts.map +0 -1
- package/dist/inbox-monitor.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle-log-guard.d.ts.map +0 -1
- package/dist/lifecycle-log-guard.js.map +0 -1
- package/dist/list-roles-render.d.ts.map +0 -1
- package/dist/list-roles-render.js.map +0 -1
- package/dist/log-audit.d.ts.map +0 -1
- package/dist/log-audit.js.map +0 -1
- package/dist/log-stream.d.ts.map +0 -1
- package/dist/log-stream.js.map +0 -1
- package/dist/name-validator.d.ts.map +0 -1
- package/dist/name-validator.js.map +0 -1
- package/dist/parse-assimilate-args.d.ts.map +0 -1
- package/dist/parse-assimilate-args.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/regen-format.d.ts.map +0 -1
- package/dist/regen-format.js.map +0 -1
- package/dist/regen.d.ts.map +0 -1
- package/dist/regen.js.map +0 -1
- package/dist/remote-client.d.ts.map +0 -1
- package/dist/remote-client.js.map +0 -1
- package/dist/role-resolver.d.ts.map +0 -1
- package/dist/role-resolver.js.map +0 -1
- package/dist/role-section.d.ts.map +0 -1
- package/dist/role-section.js.map +0 -1
- package/dist/roster-render.d.ts.map +0 -1
- package/dist/roster-render.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/shell-escape.d.ts.map +0 -1
- package/dist/shell-escape.js.map +0 -1
- package/dist/spawn.d.ts.map +0 -1
- package/dist/spawn.js.map +0 -1
- package/dist/stale-version-check.d.ts.map +0 -1
- package/dist/stale-version-check.js.map +0 -1
- package/dist/stream-owner.d.ts.map +0 -1
- package/dist/stream-owner.js.map +0 -1
- package/dist/stream-status.d.ts.map +0 -1
- package/dist/stream-status.js.map +0 -1
- package/dist/subscription-retry.d.ts.map +0 -1
- package/dist/subscription-retry.js.map +0 -1
- package/dist/sync-roles-render.d.ts.map +0 -1
- package/dist/sync-roles-render.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js.map +0 -1
- package/dist/terminal-title.d.ts.map +0 -1
- package/dist/terminal-title.js.map +0 -1
- package/dist/token-crypto.d.ts.map +0 -1
- package/dist/token-crypto.js.map +0 -1
- package/dist/token-store.d.ts.map +0 -1
- package/dist/token-store.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/worktree-lifecycle.d.ts.map +0 -1
- package/dist/worktree-lifecycle.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1379 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import { initConsolePrefix, consolePrefix } from './console-prefix.js';
|
|
29
|
-
import { isCodexRemoteWakeEnabled, resolveSessionAgentKind, probeCodexBridgeArmed, } from './codex-app-wake.js';
|
|
30
|
-
import { lifecycleSignalForMessage, recordLifecycleLog, shouldSuppressLifecycleLog, } from './lifecycle-log-guard.js';
|
|
31
|
-
import { normalizeDirectLogRecipients, } from './direct-log.js';
|
|
32
|
-
import open from 'open';
|
|
33
|
-
import os from 'os';
|
|
34
|
-
function resolveRuntimeHostname() {
|
|
35
|
-
try {
|
|
36
|
-
const h = os.hostname();
|
|
37
|
-
return h && h.trim() ? h.trim().slice(0, 255) : null;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Apply a template's roles + message_taxonomy to a cube.
|
|
45
|
-
*
|
|
46
|
-
* gh#473 PR2 — delegates to the NON-CLOBBERING server route. New roles
|
|
47
|
-
* are inserted; existing template-named roles get ADD fragments (template
|
|
48
|
-
* sections/classes the cube lacks) auto-applied, but EVOLVED (conflicting)
|
|
49
|
-
* fragments are surfaced server-side and KEPT — never silently
|
|
50
|
-
* overwritten. The old per-role blanket `updateRole`/whole-taxonomy
|
|
51
|
-
* `updateCube` overwrite path is removed. Operators who want to take the
|
|
52
|
-
* template version of a conflicting fragment use `borg:sync-roles` with a
|
|
53
|
-
* `decisions` map. Returns `{ created, updated }` for the caller's toast.
|
|
54
|
-
*/
|
|
55
|
-
async function applyTemplateToCube(cubeId, template) {
|
|
56
|
-
return await applyTemplate(cubeId, template.name);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Throw a friendly error if the client has not been assimilated to a cube.
|
|
60
|
-
*/
|
|
61
|
-
async function requireActiveCube() {
|
|
62
|
-
const active = await getActiveCube();
|
|
63
|
-
if (!active) {
|
|
64
|
-
throw new Error('Not assimilated to a cube. Use borg:assimilate <cube-name> first.');
|
|
65
|
-
}
|
|
66
|
-
return active;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Main entry point - MCP stdio server
|
|
70
|
-
*/
|
|
71
|
-
async function main() {
|
|
72
|
-
// Honor `--version` / `-v` BEFORE any side-effecting work (hooks,
|
|
73
|
-
// OAuth checks, stream consumer spawn, MCP handshake). Lets
|
|
74
|
-
// operators run `borg-mcp --version` cleanly to confirm the
|
|
75
|
-
// installed client version.
|
|
76
|
-
handleVersionFlag();
|
|
77
|
-
// Auto-register the SessionStart hook so existing users get borg-regen
|
|
78
|
-
// auto-orientation on session start without re-running borg setup. Idempotent.
|
|
79
|
-
try {
|
|
80
|
-
addSessionStartHook();
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
// Silent on failure — never break the MCP server because of hook registration.
|
|
84
|
-
}
|
|
85
|
-
// Auto-register the UserPromptSubmit audit hook so the drone gets a
|
|
86
|
-
// nudge if the previous assistant span used state-changing tools
|
|
87
|
-
// without calling borg:log. Domain-agnostic — knows nothing about git
|
|
88
|
-
// or any specific convention. Idempotent.
|
|
89
|
-
try {
|
|
90
|
-
addUserPromptSubmitHook();
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
// Silent on failure — same rationale as above.
|
|
94
|
-
}
|
|
95
|
-
// Spawn the SSE log-stream consumer. This gives drones real-time
|
|
96
|
-
// wakeup: when another drone posts to the cube, the worker pushes
|
|
97
|
-
// an `event: log` over SSE, the consumer appends one line to the
|
|
98
|
-
// per-drone inbox file (see inboxPathForDrone in cubes.ts), and the
|
|
99
|
-
// launcher's Monitor wakes the active /loop iteration immediately.
|
|
100
|
-
// Same inbox-file shape as the prior long-poll path — the file is
|
|
101
|
-
// still the harness-side wake primitive — only the wire layer
|
|
102
|
-
// changed. See:
|
|
103
|
-
// docs/superpowers/specs/2026-05-11-server-push-log-subscription.md
|
|
104
|
-
// Failure here is non-fatal — the launcher's fallback heartbeat
|
|
105
|
-
// still keeps things moving.
|
|
106
|
-
try {
|
|
107
|
-
startLogStream();
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// Silent — never break the MCP server because of stream setup.
|
|
111
|
-
}
|
|
112
|
-
// gh#541 WU-2: periodic client health beat. The MCP-client child POSTs its
|
|
113
|
-
// wake-path receipt + SSE/Monitor state to /api/drone/health every ~60s —
|
|
114
|
-
// below the agent classifier (independent of agent tool-calls), so the
|
|
115
|
-
// server can tell a DEAF seat (Monitor down) from a merely POST-BLOCKED one
|
|
116
|
-
// even during a classifier outage. Best-effort; never breaks the server.
|
|
117
|
-
try {
|
|
118
|
-
startHealthBeatTick({
|
|
119
|
-
getActiveCube,
|
|
120
|
-
getStreamConnected: () => getStreamStatus().connected,
|
|
121
|
-
getInboxPath: (active) => inboxPathForDrone(active.cubeId, active.droneId),
|
|
122
|
-
checkMonitor: checkInboxMonitorHealthy,
|
|
123
|
-
// gh#633: agnostic wake_armed — codex drones probe the app-server bridge
|
|
124
|
-
// (no tail-F Monitor by design); claude falls through to checkMonitor.
|
|
125
|
-
isCodexRemoteWake: isCodexRemoteWakeEnabled,
|
|
126
|
-
probeBridgeArmed: (active) => probeCodexBridgeArmed({ cubeId: active.cubeId, droneId: active.droneId }),
|
|
127
|
-
// gh#634: live runtime agent_kind, beated to self-heal the recorded column.
|
|
128
|
-
resolveAgentKind: resolveSessionAgentKind,
|
|
129
|
-
resolveHostname: resolveRuntimeHostname,
|
|
130
|
-
resolveVersion: getPackageVersion,
|
|
131
|
-
getToken: getValidToken,
|
|
132
|
-
fetchImpl: globalThis.fetch.bind(globalThis),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// Silent — never break the MCP server because of health-beat setup.
|
|
137
|
-
}
|
|
138
|
-
// Create MCP server. `version` is the installed borgmcp version
|
|
139
|
-
// (T1.4 of 0.6.0): read at runtime from package.json so Claude
|
|
140
|
-
// Code's `/mcp` view shows the real version instead of the
|
|
141
|
-
// long-standing hardcoded "0.1.0".
|
|
142
|
-
const server = new Server({
|
|
143
|
-
name: 'borg-mcp-client',
|
|
144
|
-
version: getPackageVersion(),
|
|
145
|
-
}, {
|
|
146
|
-
capabilities: {
|
|
147
|
-
tools: {},
|
|
148
|
-
prompts: {},
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
// Register tool listing
|
|
152
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
153
|
-
return {
|
|
154
|
-
tools: [
|
|
155
|
-
{
|
|
156
|
-
name: 'subscribe',
|
|
157
|
-
description: 'Create Stripe checkout session for Cube tier ($1/cube/month — unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial.',
|
|
158
|
-
inputSchema: {
|
|
159
|
-
type: 'object',
|
|
160
|
-
properties: {},
|
|
161
|
-
required: [],
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
name: 'subscription_status',
|
|
166
|
-
description: 'Check subscription status',
|
|
167
|
-
inputSchema: {
|
|
168
|
-
type: 'object',
|
|
169
|
-
properties: {},
|
|
170
|
-
required: [],
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
name: 'open_dashboard',
|
|
175
|
-
description: 'Open Borg MCP dashboard in browser to manage cubes, roles, and drones',
|
|
176
|
-
inputSchema: {
|
|
177
|
-
type: 'object',
|
|
178
|
-
properties: {},
|
|
179
|
-
required: [],
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
name: 'borg:regen',
|
|
184
|
-
description: "Refresh your context as a Drone. Returns the active cube's directive, " +
|
|
185
|
-
"your role's detailed playbook, the drone roster, and recent activity log entries — " +
|
|
186
|
-
'everything you need to be oriented. Call on session start, and again before each new ' +
|
|
187
|
-
'task to stay in sync with the cube. Returns "not connected" if no active cube; use ' +
|
|
188
|
-
'borg:assimilate first in that case. ' +
|
|
189
|
-
'Optional `since` (entry-id UUID or ISO-8601 timestamp) trims the recent-log section ' +
|
|
190
|
-
'to entries strictly after the anchor — pass your last-seen entry id to skip ' +
|
|
191
|
-
'already-processed history on each refresh.',
|
|
192
|
-
inputSchema: {
|
|
193
|
-
type: 'object',
|
|
194
|
-
properties: {
|
|
195
|
-
since: {
|
|
196
|
-
type: 'string',
|
|
197
|
-
description: 'Optional cursor. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple) OR an ISO-8601 timestamp. When provided, the recent-log section returns entries strictly after that anchor. Non-existent UUID falls back to default recent window.',
|
|
198
|
-
},
|
|
199
|
-
mode: {
|
|
200
|
-
type: 'string',
|
|
201
|
-
enum: ['full', 'lite'],
|
|
202
|
-
description: 'Optional output mode. Use full at session start and after context compaction. Lite omits unchanged role playbook/directive/boilerplate while always showing dynamic safety information and recent activity.',
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
required: [],
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
name: 'borg:assimilate',
|
|
210
|
-
description: "Connect this Claude session as a Drone to a Cube. Provide the cube's name. " +
|
|
211
|
-
"Returns the cube's directive, your assigned role's detailed instructions, " +
|
|
212
|
-
'and persists a session token locally so subsequent borg: tools work for this cube.',
|
|
213
|
-
inputSchema: {
|
|
214
|
-
type: 'object',
|
|
215
|
-
properties: {
|
|
216
|
-
cube_name: {
|
|
217
|
-
type: 'string',
|
|
218
|
-
description: 'The cube to connect to',
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
required: ['cube_name'],
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
name: 'borg:cube',
|
|
226
|
-
description: "Read the active Cube's directive and the registry of all roles in it " +
|
|
227
|
-
"(each role's name + short description). Use to remind yourself of cube-wide context.",
|
|
228
|
-
inputSchema: {
|
|
229
|
-
type: 'object',
|
|
230
|
-
properties: {},
|
|
231
|
-
required: [],
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
name: 'borg:role',
|
|
236
|
-
description: "Read your assigned role's detailed description (your playbook). " +
|
|
237
|
-
'Other drones cannot see this — only you (drones in this role).',
|
|
238
|
-
inputSchema: {
|
|
239
|
-
type: 'object',
|
|
240
|
-
properties: {},
|
|
241
|
-
required: [],
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
name: 'borg:version',
|
|
246
|
-
description: 'Returns the installed borgmcp client version. Use to verify which version is running in this MCP session.',
|
|
247
|
-
inputSchema: {
|
|
248
|
-
type: 'object',
|
|
249
|
-
properties: {},
|
|
250
|
-
required: [],
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
name: 'borg:whoami',
|
|
255
|
-
description: 'Returns your identity in the current cube: cube name, drone label, and role name. Use to confirm which cube/role/drone you are.',
|
|
256
|
-
inputSchema: {
|
|
257
|
-
type: 'object',
|
|
258
|
-
properties: {},
|
|
259
|
-
required: [],
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
name: 'borg:role-rationale',
|
|
264
|
-
description: "Fetch an on-demand rationale/case-study section for a role playbook. " +
|
|
265
|
-
"Pass a role name/id and a plain-label section key to read the rationale without expanding every regen.",
|
|
266
|
-
inputSchema: {
|
|
267
|
-
type: 'object',
|
|
268
|
-
properties: {
|
|
269
|
-
role: {
|
|
270
|
-
type: 'string',
|
|
271
|
-
description: 'Role name or role id to fetch rationale for, e.g. Builder.',
|
|
272
|
-
},
|
|
273
|
-
section: {
|
|
274
|
-
type: 'string',
|
|
275
|
-
description: 'Plain-label role section key, e.g. Workflow rationale.',
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
required: ['role', 'section'],
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: 'borg:roster',
|
|
283
|
-
description: "List all currently connected drones in your cube, with each drone's label, role, and last-seen time. Optional `since` argument adds a sender-side liveness column — pass either an activity_log entry id (e.g., from a dispatch you posted) or an ISO-8601 timestamp; each drone is marked `awake` if they've posted a log entry after that point, otherwise `stale-since-X`. Useful for confirming a dispatch reached its named recipients (catches the silent-wake-path-failure class where SSE delivered but the drone's /loop never woke).",
|
|
284
|
-
inputSchema: {
|
|
285
|
-
type: 'object',
|
|
286
|
-
properties: {
|
|
287
|
-
since: {
|
|
288
|
-
type: 'string',
|
|
289
|
-
description: 'Optional liveness reference point. Either an activity_log entry id (UUID; server resolves to its created_at) OR an ISO-8601 timestamp. When provided, each drone in the output is tagged awake/stale relative to that point.',
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
required: [],
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
name: 'borg:stream-status',
|
|
297
|
-
description: "Diagnostic probe for the SSE log-stream consumer. Returns the live state of the local stream connection — `connected`, `lastContentEventAt` (most recent log/bookmark event), `lastWireActivityAt` (most recent event of any type, incl. heartbeats), `lastHeartbeatAt`, `lastPersistedEventId`, and `reconnectAttempts` — plus a wake-path completeness check that surfaces if SSE is attached but no inbox-Monitor is watching the file (the silent-failure mode where Claude's `/loop` never wakes on incoming entries). Reads in-process state from the running borgmcp client; does NOT re-open the stream, so calling it cannot perturb the very thing it's observing. Useful when troubleshooting wake-up issues, verifying the stream is alive without other drones logging, or pre-checking before fault-injection tests.",
|
|
298
|
-
inputSchema: {
|
|
299
|
-
type: 'object',
|
|
300
|
-
properties: {},
|
|
301
|
-
required: [],
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
name: 'borg:read-log',
|
|
306
|
-
description: "Read recent entries from the cube's activity log. Each entry is tagged " +
|
|
307
|
-
"with the drone that wrote it and that drone's role. Optional: since (entry-id UUID " +
|
|
308
|
-
'OR ISO-8601 timestamp — returns entries strictly after the anchor; non-existent UUID ' +
|
|
309
|
-
'falls back to the default recent window), limit (1–500, default 50), and ' +
|
|
310
|
-
'unread_only (boolean — return only entries posted after this drone last called ' +
|
|
311
|
-
'read-log; the server advances the watermark to the newest returned entry, so ' +
|
|
312
|
-
'subsequent unread_only calls only show fresh activity).',
|
|
313
|
-
inputSchema: {
|
|
314
|
-
type: 'object',
|
|
315
|
-
properties: {
|
|
316
|
-
since: {
|
|
317
|
-
type: 'string',
|
|
318
|
-
description: 'Optional cursor. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple for deterministic tie-break) OR an ISO-8601 timestamp.',
|
|
319
|
-
},
|
|
320
|
-
limit: {
|
|
321
|
-
type: 'number',
|
|
322
|
-
description: 'max entries to return (1-500)',
|
|
323
|
-
},
|
|
324
|
-
unread_only: {
|
|
325
|
-
type: 'boolean',
|
|
326
|
-
description: 'When true, filter to entries posted after this drone last called read-log. Composes with `since`. Server advances the watermark to the newest returned entry on every call (Sprint 25 log substrate refactor).',
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
name: 'borg:ack',
|
|
333
|
-
description: 'Mark a log entry as explicitly acknowledged. Replaces the convention of posting `ACK: <dispatch-id>` log entries. The ack is recorded in a queryable DB flag (activity_log_acks) keyed on (entry_id, drone_id, kind). Idempotent — repeated calls on the same entry are no-ops. Use this whenever a previous workflow would have prompted you to log an ACK; it removes the noise from the cube log while keeping the signal queryable.',
|
|
334
|
-
inputSchema: {
|
|
335
|
-
type: 'object',
|
|
336
|
-
required: ['entry_id'],
|
|
337
|
-
properties: {
|
|
338
|
-
entry_id: {
|
|
339
|
-
type: 'string',
|
|
340
|
-
description: 'UUID of the log entry to acknowledge.',
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
name: 'borg:log',
|
|
347
|
-
description: 'Append a message to the cube\'s activity log. By default entries broadcast to all drones. When a cube declares a message taxonomy, borg:log applies class-based smart defaults: prefix-matched directed classes route to their default recipients unless you pass `to:`, `class:`, or explicit visibility. Pass `to: [...]` to direct by exact drone label, drone id, role name, or role slug.',
|
|
348
|
-
inputSchema: {
|
|
349
|
-
type: 'object',
|
|
350
|
-
properties: {
|
|
351
|
-
message: { type: 'string', description: 'The log message (max 10KB).' },
|
|
352
|
-
to: {
|
|
353
|
-
type: 'array',
|
|
354
|
-
items: { type: 'string' },
|
|
355
|
-
description: 'Optional direct-message recipients by exact drone label, drone id, role name, or role slug (resolves to all drones in that role). Omit to let class-based routing or broadcast defaults apply.',
|
|
356
|
-
},
|
|
357
|
-
class: {
|
|
358
|
-
type: 'string',
|
|
359
|
-
description: 'Optional declared message class. Overrides prefix auto-classification when the cube declares a message taxonomy.',
|
|
360
|
-
},
|
|
361
|
-
visibility: {
|
|
362
|
-
type: 'string',
|
|
363
|
-
enum: ['broadcast', 'direct'],
|
|
364
|
-
description: 'Optional explicit visibility. Overrides class-based routing defaults.',
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
required: ['message'],
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
name: 'borg:list-cubes',
|
|
372
|
-
description: 'List every cube owned by this user. Returns id, name, cube_directive, and timestamps for each. Useful before assimilate to see what\'s available, or as a starting point for any management action.',
|
|
373
|
-
inputSchema: { type: 'object', properties: {} },
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
name: 'borg:create-cube',
|
|
377
|
-
description: 'Create a new cube. The server seeds a default "Drone" role atomically so the cube is assimilatable immediately. ' +
|
|
378
|
-
'Pass an optional `template` name to apply a richer role set instead (see borg:list-templates / borg:apply-template).',
|
|
379
|
-
inputSchema: {
|
|
380
|
-
type: 'object',
|
|
381
|
-
properties: {
|
|
382
|
-
name: {
|
|
383
|
-
type: 'string',
|
|
384
|
-
description: 'Cube name (lowercase letters, digits, hyphens; max 64 chars).',
|
|
385
|
-
pattern: '^[a-z0-9-]+$',
|
|
386
|
-
maxLength: 64,
|
|
387
|
-
},
|
|
388
|
-
cube_directive: { type: 'string', description: 'Markdown text every drone in this cube will see in regen. Anything project-specific.' },
|
|
389
|
-
template: {
|
|
390
|
-
type: 'string',
|
|
391
|
-
description: 'Optional template name to apply after cube creation (e.g. "software-dev"). Roles are merged by name; the default Drone role gets overwritten by the template if a same-named role is in the template.',
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
required: ['name', 'cube_directive'],
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
{
|
|
398
|
-
name: 'borg:update-cube',
|
|
399
|
-
description: 'Update a cube\'s name, cube_directive, and/or message_taxonomy. Pass only what changes.',
|
|
400
|
-
inputSchema: {
|
|
401
|
-
type: 'object',
|
|
402
|
-
properties: {
|
|
403
|
-
cube_id: { type: 'string', description: 'UUID of the cube to update.' },
|
|
404
|
-
name: {
|
|
405
|
-
type: 'string',
|
|
406
|
-
description: 'New name (optional). Lowercase letters, digits, hyphens; max 64 chars.',
|
|
407
|
-
pattern: '^[a-z0-9-]+$',
|
|
408
|
-
maxLength: 64,
|
|
409
|
-
},
|
|
410
|
-
cube_directive: { type: 'string', description: 'New cube directive markdown (optional).' },
|
|
411
|
-
message_taxonomy: {
|
|
412
|
-
type: 'array',
|
|
413
|
-
description: 'New message-class taxonomy (optional). REPLACES the whole taxonomy; the worker re-validates the full array (non-overlapping prefixes, unique class names, directed classes need default_to). Pass [] to clear. To change ONE class without resending the whole array, use borg:patch-taxonomy-class instead. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.',
|
|
414
|
-
items: {
|
|
415
|
-
type: 'object',
|
|
416
|
-
properties: {
|
|
417
|
-
class: { type: 'string', description: 'Unique class name.' },
|
|
418
|
-
prefixes: { type: 'array', items: { type: 'string' }, description: 'Message prefixes routed by this class.' },
|
|
419
|
-
routing: { type: 'string', enum: ['broadcast', 'directed'], description: 'Routing mode.' },
|
|
420
|
-
default_to: { type: 'array', items: { type: 'string' }, description: 'Default recipients (role name/slug/label, or @human-seat) for a directed class.' },
|
|
421
|
-
lifecycle: { type: 'string', enum: ['dispatch', 'completion'], description: 'Optional lifecycle marker for stuck-dispatch detection.' },
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
required: ['cube_id'],
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
name: 'borg:patch-taxonomy-class',
|
|
431
|
-
description: "Surgically patch ONE message-class within a cube's message_taxonomy, leaving other classes unchanged. Use this instead of borg:update-cube when adding/changing a single class so you don't resend (and risk clobbering) the whole taxonomy. action=add appends a new class; action=replace overwrites the class with the same name (case-insensitive); action=remove drops a class. The whole resulting taxonomy is re-validated (non-overlapping prefixes, unique class names, directed classes need default_to) — a single-class patch that breaks a cross-class rule against an untouched class is rejected. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.",
|
|
432
|
-
inputSchema: {
|
|
433
|
-
type: 'object',
|
|
434
|
-
properties: {
|
|
435
|
-
cube_id: { type: 'string', description: 'UUID of the cube to patch.' },
|
|
436
|
-
action: { type: 'string', enum: ['add', 'replace', 'remove'], description: 'add / replace / remove a single class.' },
|
|
437
|
-
class_def: {
|
|
438
|
-
type: 'object',
|
|
439
|
-
description: 'The class definition (for add/replace). Shape: { class, prefixes?, routing: "broadcast"|"directed", default_to?, lifecycle? }.',
|
|
440
|
-
properties: {
|
|
441
|
-
class: { type: 'string', description: 'Unique class name.' },
|
|
442
|
-
prefixes: { type: 'array', items: { type: 'string' }, description: 'Message prefixes routed by this class.' },
|
|
443
|
-
routing: { type: 'string', enum: ['broadcast', 'directed'], description: 'Routing mode.' },
|
|
444
|
-
default_to: { type: 'array', items: { type: 'string' }, description: 'Default recipients (required for directed classes): role name/slug/label, or @human-seat.' },
|
|
445
|
-
lifecycle: { type: 'string', enum: ['dispatch', 'completion'], description: 'Optional lifecycle marker for stuck-dispatch detection.' },
|
|
446
|
-
},
|
|
447
|
-
required: ['class', 'routing'],
|
|
448
|
-
},
|
|
449
|
-
class: { type: 'string', description: 'For remove only: the name of the class to drop (case-insensitive).' },
|
|
450
|
-
},
|
|
451
|
-
required: ['cube_id', 'action'],
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
{
|
|
455
|
-
name: 'borg:delete-cube',
|
|
456
|
-
description: 'Delete a cube and all its roles, drones, and log entries. Irreversible — confirm with the user before invoking unless the cube is clearly disposable.',
|
|
457
|
-
inputSchema: {
|
|
458
|
-
type: 'object',
|
|
459
|
-
properties: {
|
|
460
|
-
cube_id: { type: 'string', description: 'UUID of the cube to delete.' },
|
|
461
|
-
},
|
|
462
|
-
required: ['cube_id'],
|
|
463
|
-
},
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
name: 'borg:create-role',
|
|
467
|
-
description: 'Create a role inside a cube. The detailed_description is the role\'s playbook — only drones assigned to this role see it. Setting is_default=true demotes any existing default; a cube has exactly one default role at a time.',
|
|
468
|
-
inputSchema: {
|
|
469
|
-
type: 'object',
|
|
470
|
-
properties: {
|
|
471
|
-
cube_id: { type: 'string', description: 'UUID of the cube this role belongs to.' },
|
|
472
|
-
name: { type: 'string', description: 'Role name (e.g. "Builder", "Reviewer").' },
|
|
473
|
-
short_description: { type: 'string', description: 'One-line summary, shown to every drone in the cube.' },
|
|
474
|
-
detailed_description: { type: 'string', description: 'Full playbook for drones in this role — workflow, conventions, log signals to post.' },
|
|
475
|
-
is_default: { type: 'boolean', description: 'If true, new drones assimilating into this cube are assigned this role. Demotes the previous default.' },
|
|
476
|
-
is_human_seat: { type: 'boolean', description: 'If true, this role represents the cube\'s human-occupied seat (where the human Queen sits directly). The class-hierarchy guard in reassign-drone allows promotion FROM a human-seat role TO the platform Queen role; promotion from non-human-seat roles is rejected.' },
|
|
477
|
-
can_broadcast: { type: 'boolean', description: 'If true, drones in this role may post broadcast log entries when strict broadcast gating is enabled.' },
|
|
478
|
-
receives_all_direct: { type: 'boolean', description: 'If true, drones in this role can see direct log entries as observer/audit recipients.' },
|
|
479
|
-
},
|
|
480
|
-
required: ['cube_id', 'name', 'short_description', 'detailed_description'],
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
{
|
|
484
|
-
name: 'borg:update-role',
|
|
485
|
-
description: 'Update a role. Pass only the fields that change. Promoting to is_default demotes the previous default in the same cube.',
|
|
486
|
-
inputSchema: {
|
|
487
|
-
type: 'object',
|
|
488
|
-
properties: {
|
|
489
|
-
role_id: { type: 'string', description: 'UUID of the role to update.' },
|
|
490
|
-
name: { type: 'string', description: 'New role name (optional).' },
|
|
491
|
-
short_description: { type: 'string', description: 'New short description (optional).' },
|
|
492
|
-
detailed_description: { type: 'string', description: 'New detailed playbook (optional).' },
|
|
493
|
-
is_default: { type: 'boolean', description: 'Set true to make this the cube\'s default role (optional).' },
|
|
494
|
-
is_human_seat: { type: 'boolean', description: 'Set true/false to mark/unmark this as the cube\'s human-occupied seat (the elevation source for the platform Queen role).' },
|
|
495
|
-
can_broadcast: { type: 'boolean', description: 'Set true/false to allow or deny broadcast log entries when strict broadcast gating is enabled.' },
|
|
496
|
-
receives_all_direct: { type: 'boolean', description: 'Set true/false to grant or remove observer visibility into direct log entries.' },
|
|
497
|
-
},
|
|
498
|
-
required: ['role_id'],
|
|
499
|
-
},
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
name: 'borg:patch-role-section',
|
|
503
|
-
description: "Surgically patch ONE named section of a role's detailed_description, leaving the rest of the field byte-identical. Sections are delimited by plain-label lines (e.g. `Workflow:`, `Project conventions:`) — NOT markdown headings; text before the first label is the preamble. Use this instead of borg:update-role when changing a single section so you don't have to resend (and risk clobbering) the whole playbook. action=replace overwrites a section's body; action=insert adds a new section (optionally after a named one, else appended); action=delete removes a section.",
|
|
504
|
-
inputSchema: {
|
|
505
|
-
type: 'object',
|
|
506
|
-
properties: {
|
|
507
|
-
role_id: { type: 'string', description: 'UUID of the role to patch.' },
|
|
508
|
-
action: { type: 'string', enum: ['replace', 'insert', 'delete'], description: 'replace / insert / delete a single section.' },
|
|
509
|
-
heading: { type: 'string', description: 'The section label WITHOUT the trailing colon (e.g. "Workflow"). Matched case-insensitively.' },
|
|
510
|
-
body: { type: 'string', description: 'New text BELOW the heading (for replace/insert). Omit for delete.' },
|
|
511
|
-
after: { type: 'string', description: 'For insert only: place the new section after the section with this heading. Omit/null to append at the end.' },
|
|
512
|
-
},
|
|
513
|
-
required: ['role_id', 'action', 'heading'],
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
{
|
|
517
|
-
name: 'borg:delete-role',
|
|
518
|
-
description: 'Delete a role. Refuses if any drone is still assigned — reassign or evict those drones from the dashboard first.',
|
|
519
|
-
inputSchema: {
|
|
520
|
-
type: 'object',
|
|
521
|
-
properties: {
|
|
522
|
-
role_id: { type: 'string', description: 'UUID of the role to delete.' },
|
|
523
|
-
},
|
|
524
|
-
required: ['role_id'],
|
|
525
|
-
},
|
|
526
|
-
},
|
|
527
|
-
{
|
|
528
|
-
name: 'borg:reassign-drone',
|
|
529
|
-
description: 'Reassign a drone to a different role in the same cube. Coordinator-shaped: the cube\'s Coordinator drone is the one expected to call this when dispatching new drones to specific work. ' +
|
|
530
|
-
'Server refuses if you try to assign to the Coordinator role when another drone already holds it (evict or reassign that drone first).',
|
|
531
|
-
inputSchema: {
|
|
532
|
-
type: 'object',
|
|
533
|
-
properties: {
|
|
534
|
-
drone_id: { type: 'string', description: 'UUID of the drone to reassign.' },
|
|
535
|
-
role_id: { type: 'string', description: 'UUID of the target role. Must belong to the same cube as the drone.' },
|
|
536
|
-
},
|
|
537
|
-
required: ['drone_id', 'role_id'],
|
|
538
|
-
},
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
name: 'borg:list-drones',
|
|
542
|
-
description: 'List every drone in a cube (owner-scoped). Returns id, label, role_id, agent_kind, last_seen, and wake_path_alert_class for each — gives the Coordinator a roster they can act on with borg:reassign-drone.',
|
|
543
|
-
inputSchema: {
|
|
544
|
-
type: 'object',
|
|
545
|
-
properties: {
|
|
546
|
-
cube_id: { type: 'string', description: 'UUID of the cube whose drones to list.' },
|
|
547
|
-
},
|
|
548
|
-
required: ['cube_id'],
|
|
549
|
-
},
|
|
550
|
-
},
|
|
551
|
-
{
|
|
552
|
-
name: 'borg:list-roles',
|
|
553
|
-
description: 'List every role in a cube (owner-scoped). Returns id, name, short_description, is_default, is_human_seat, can_broadcast, receives_all_direct, and role_class for each — gives Coordinator-class drones the role UUIDs they need for borg:reassign-drone (e.g. to promote a drone to the Queen role). Closes the gh#153 Queen-role-promotion UX gap (Coordinator drones previously had no way to discover role IDs without operator help).',
|
|
554
|
-
inputSchema: {
|
|
555
|
-
type: 'object',
|
|
556
|
-
properties: {
|
|
557
|
-
cube_id: { type: 'string', description: 'UUID of the cube whose roles to list.' },
|
|
558
|
-
},
|
|
559
|
-
required: ['cube_id'],
|
|
560
|
-
},
|
|
561
|
-
},
|
|
562
|
-
{
|
|
563
|
-
name: 'borg:list-templates',
|
|
564
|
-
description: 'List available cube templates that can be applied via borg:apply-template or passed to borg:create-cube.',
|
|
565
|
-
inputSchema: { type: 'object', properties: {} },
|
|
566
|
-
},
|
|
567
|
-
{
|
|
568
|
-
name: 'borg:apply-template',
|
|
569
|
-
description: 'Apply a named template to an existing cube, NON-CLOBBERINGLY. Roles are merged by name: new roles are created; existing template-named roles get template sections/classes the cube LACKS auto-applied, but EVOLVED (conflicting) text is preserved, never overwritten. Use this to retrofit an existing cube with a richer role set (e.g. add Coordinator/Reviewer/UX Expert). To review + selectively accept conflicting fragments, use borg:sync-roles (which surfaces each conflict + takes per-fragment accept decisions).',
|
|
570
|
-
inputSchema: {
|
|
571
|
-
type: 'object',
|
|
572
|
-
properties: {
|
|
573
|
-
cube_id: { type: 'string', description: 'UUID of the cube to apply the template to.' },
|
|
574
|
-
template_name: { type: 'string', description: 'Template to apply (see borg:list-templates).' },
|
|
575
|
-
},
|
|
576
|
-
required: ['cube_id', 'template_name'],
|
|
577
|
-
},
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
name: 'borg:sync-roles',
|
|
581
|
-
description: 'Non-clobbering sync of an existing cube\'s roles + message_taxonomy against the current built-in template. The dry-run (default) classifies each FRAGMENT (role-text section, short_description, role flags, or taxonomy class) as ADD (the cube lacks it — safe auto-apply), UNCHANGED, or CONFLICT (the cube has EVOLVED text that differs from the template). On apply, ADDs auto-apply; CONFLICTs are applied ONLY when you explicitly accept them via `decisions` (keyed on the stable fragment key shown in the dry-run, e.g. `role:Builder:section:Workflow`). Unspecified conflicts default to KEEP (reject) — your cube\'s evolved coordination text is NEVER silently overwritten. Custom roles (names not in the template) are never touched.',
|
|
582
|
-
inputSchema: {
|
|
583
|
-
type: 'object',
|
|
584
|
-
properties: {
|
|
585
|
-
cube_id: { type: 'string', description: 'UUID of the cube to sync.' },
|
|
586
|
-
template_name: { type: 'string', description: 'Template to sync against (default: software-dev).' },
|
|
587
|
-
apply: { type: 'boolean', description: 'If true, commit (auto-apply ADDs + accepted conflicts). If false (default), dry-run only — classify + surface conflicts.' },
|
|
588
|
-
decisions: {
|
|
589
|
-
type: 'object',
|
|
590
|
-
description: 'Per-conflict accept/reject map, keyed on the fragment key from the dry-run (e.g. {"role:Builder:section:Workflow":"accept"}). Unspecified conflicts default to "reject" (keep the cube version).',
|
|
591
|
-
additionalProperties: { type: 'string', enum: ['accept', 'reject'] },
|
|
592
|
-
},
|
|
593
|
-
},
|
|
594
|
-
required: ['cube_id'],
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
],
|
|
598
|
-
};
|
|
599
|
-
});
|
|
600
|
-
// Register tool execution handler
|
|
601
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
602
|
-
const { name, arguments: args } = request.params;
|
|
603
|
-
try {
|
|
604
|
-
switch (name) {
|
|
605
|
-
case 'borg:regen': {
|
|
606
|
-
const active = await getActiveCube();
|
|
607
|
-
if (!active) {
|
|
608
|
-
return {
|
|
609
|
-
content: [
|
|
610
|
-
{
|
|
611
|
-
type: 'text',
|
|
612
|
-
text: 'Not connected to a cube. Use `borg:assimilate cube_name="<name>"` to join one.',
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
const since = typeof args?.since === 'string' ? args.since : undefined;
|
|
618
|
-
const mode = args?.mode === 'lite' ? 'lite' : 'full';
|
|
619
|
-
const result = await regen(active.sessionToken, active.apiUrl, { since });
|
|
620
|
-
const freshActive = activeCubeWithFreshRegenIdentity(active, result);
|
|
621
|
-
if (freshActive !== active) {
|
|
622
|
-
await setActiveCube(freshActive);
|
|
623
|
-
}
|
|
624
|
-
// Wake-path self-heal (gh#43): SSE delivery to the inbox file
|
|
625
|
-
// is independent from Claude Code waking on file writes. The
|
|
626
|
-
// latter requires a `tail -F` Monitor against the inbox path;
|
|
627
|
-
// if that Monitor dies (or was never armed across a session
|
|
628
|
-
// boundary), the drone misses every incoming entry until the
|
|
629
|
-
// /loop fallback heartbeat. Because regen runs on every /loop
|
|
630
|
-
// iteration, surfacing the breakage here gives self-healing at
|
|
631
|
-
// worst-case latency = the heartbeat interval. Mirrors the
|
|
632
|
-
// State-5 self-arm instruction in stream-status.ts.
|
|
633
|
-
const streamStatus = getStreamStatus();
|
|
634
|
-
const inboxPath = inboxPathForDrone(freshActive.cubeId, freshActive.droneId);
|
|
635
|
-
const inboxMonitorHealthy = isCodexRemoteWakeEnabled()
|
|
636
|
-
? true
|
|
637
|
-
: checkInboxMonitorHealthy(inboxPath);
|
|
638
|
-
const prefix = shouldShowWakePathWarning(streamStatus, inboxMonitorHealthy)
|
|
639
|
-
? formatWakePathPrefix({
|
|
640
|
-
inboxPath,
|
|
641
|
-
droneLabel: regenWakePathDroneLabel(result, freshActive.droneLabel),
|
|
642
|
-
cubeName: freshActive.name,
|
|
643
|
-
})
|
|
644
|
-
: '';
|
|
645
|
-
// gh#285: version-mismatch nudge when on-disk package is newer.
|
|
646
|
-
let versionHeader = '';
|
|
647
|
-
try {
|
|
648
|
-
const running = getPackageVersion();
|
|
649
|
-
const onDisk = getOnDiskVersion();
|
|
650
|
-
if (running !== 'unknown' && onDisk !== 'unknown' && onDisk !== running) {
|
|
651
|
-
const [rMaj, rMin, rPat] = running.split('.').map(Number);
|
|
652
|
-
const [dMaj, dMin, dPat] = onDisk.split('.').map(Number);
|
|
653
|
-
const onDiskNewer = dMaj > rMaj || (dMaj === rMaj && dMin > rMin) || (dMaj === rMaj && dMin === rMin && dPat > rPat);
|
|
654
|
-
if (onDiskNewer) {
|
|
655
|
-
versionHeader = `## 🔄 borgmcp ${onDisk} installed — run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${running}.\n\n`;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
catch { /* never break regen */ }
|
|
660
|
-
return { content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(result, { mode }) }] };
|
|
661
|
-
}
|
|
662
|
-
case 'subscribe': {
|
|
663
|
-
const checkoutUrl = await createSubscription();
|
|
664
|
-
return {
|
|
665
|
-
content: [
|
|
666
|
-
{
|
|
667
|
-
type: 'text',
|
|
668
|
-
text: `Complete your subscription at: ${checkoutUrl}`,
|
|
669
|
-
},
|
|
670
|
-
],
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
case 'subscription_status': {
|
|
674
|
-
const status = await checkSubscriptionStatus();
|
|
675
|
-
return {
|
|
676
|
-
content: [
|
|
677
|
-
{
|
|
678
|
-
type: 'text',
|
|
679
|
-
text: JSON.stringify(status, null, 2),
|
|
680
|
-
},
|
|
681
|
-
],
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
case 'open_dashboard': {
|
|
685
|
-
const dashboardUrl = 'https://borgmcp.ai/dashboard';
|
|
686
|
-
await open(dashboardUrl);
|
|
687
|
-
return {
|
|
688
|
-
content: [
|
|
689
|
-
{
|
|
690
|
-
type: 'text',
|
|
691
|
-
text: `◼ Opened dashboard in browser: ${dashboardUrl}`,
|
|
692
|
-
},
|
|
693
|
-
],
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
case 'borg:assimilate': {
|
|
697
|
-
const cubeName = args?.cube_name;
|
|
698
|
-
if (!cubeName)
|
|
699
|
-
throw new Error('cube_name is required');
|
|
700
|
-
// First-call assimilate uses the env-or-prod default; we then
|
|
701
|
-
// persist that same URL so all subsequent drone calls hit the
|
|
702
|
-
// worker that issued the session token.
|
|
703
|
-
const apiUrl = API_URL;
|
|
704
|
-
// gh#104: capture user-controlled machine name (os.hostname() —
|
|
705
|
-
// e.g. "MacBook", "Mac Studio") for dashboard display. NOT a
|
|
706
|
-
// hardware fingerprint (Queen scope narrowing). Defensive:
|
|
707
|
-
// empty/error → null; never blocks the assimilate.
|
|
708
|
-
const hostname = resolveRuntimeHostname();
|
|
709
|
-
// gh#634: persist the live session's agent kind. The old
|
|
710
|
-
// cwd-keyed CLI preference file can miss sibling-worktree Codex
|
|
711
|
-
// launches; the remote-wake env is injected into this MCP child.
|
|
712
|
-
const agentKind = resolveSessionAgentKind();
|
|
713
|
-
const result = await assimilate(cubeName, apiUrl, hostname, agentKind);
|
|
714
|
-
await setActiveCube({
|
|
715
|
-
cubeId: result.cube.id,
|
|
716
|
-
droneId: result.drone.id,
|
|
717
|
-
name: result.cube.name,
|
|
718
|
-
sessionToken: result.sessionToken,
|
|
719
|
-
droneLabel: result.drone.label,
|
|
720
|
-
apiUrl,
|
|
721
|
-
});
|
|
722
|
-
const inboxPath = inboxPathForDrone(result.cube.id, result.drone.id);
|
|
723
|
-
const wakeHelp = isCodexRemoteWakeEnabled()
|
|
724
|
-
? [
|
|
725
|
-
`## Real-time wake-up`,
|
|
726
|
-
`Codex remote-control wake is enabled for this MCP server. Incoming cube-log entries are delivered through the Codex app-server remote-control path; no Claude Monitor or inbox tail is required.`,
|
|
727
|
-
``,
|
|
728
|
-
]
|
|
729
|
-
: [
|
|
730
|
-
`## Real-time wake-up (do this now)`,
|
|
731
|
-
`If \`/loop\` is already running in this session without an inbox Monitor (typical when borg was launched before assimilating), arm one now so you wake the moment another drone posts to the cube:`,
|
|
732
|
-
``,
|
|
733
|
-
`> Monitor command: \`tail -n 0 -F ${inboxPath}\` — persistent, 1h timeout, description "borg inbox for ${result.drone.label} on cube ${result.cube.name}".`,
|
|
734
|
-
``,
|
|
735
|
-
`Without this, you only wake on the 30-min fallback heartbeat and miss live coordination from other drones. Skip if \`/loop\` is not running in this session.`,
|
|
736
|
-
``,
|
|
737
|
-
];
|
|
738
|
-
const text = [
|
|
739
|
-
`# Assimilated to cube: ${result.cube.name}`,
|
|
740
|
-
``,
|
|
741
|
-
`**Drone label:** ${result.drone.label}`,
|
|
742
|
-
`**Assigned role:** ${result.role.name}`,
|
|
743
|
-
``,
|
|
744
|
-
...wakeHelp,
|
|
745
|
-
`## Cube directive`,
|
|
746
|
-
result.cube.cube_directive || '_(none)_',
|
|
747
|
-
``,
|
|
748
|
-
`## Your role: ${result.role.name}`,
|
|
749
|
-
result.role.detailed_description || '_(no detailed description set)_',
|
|
750
|
-
``,
|
|
751
|
-
getDronePlaybook(),
|
|
752
|
-
].join('\n');
|
|
753
|
-
return { content: [{ type: 'text', text }] };
|
|
754
|
-
}
|
|
755
|
-
case 'borg:version': {
|
|
756
|
-
return { content: [{ type: 'text', text: `borgmcp ${getPackageVersion()}` }] };
|
|
757
|
-
}
|
|
758
|
-
case 'borg:whoami': {
|
|
759
|
-
const active = await requireActiveCube();
|
|
760
|
-
const result = await whoami(active.sessionToken, active.apiUrl);
|
|
761
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
762
|
-
}
|
|
763
|
-
case 'borg:cube': {
|
|
764
|
-
// No-cache invariant (T1.2 — 0.7.0): both `getCubeInfo` and
|
|
765
|
-
// `getRoleInfo` MUST fetch fresh per invocation. Never
|
|
766
|
-
// memoize / cache the cube or role payload in process memory
|
|
767
|
-
// or in `cubes.json` — `borg:reassign-drone` changes the
|
|
768
|
-
// calling drone's role in the DB, and subsequent
|
|
769
|
-
// `borg:cube` reads MUST reflect the new role within one
|
|
770
|
-
// round-trip. Locked in by the regression probe at
|
|
771
|
-
// `client/__tests__/integration/01-role-cache-freshness.integration.ts`;
|
|
772
|
-
// a future refactor that introduces caching here will fail
|
|
773
|
-
// that probe. See drone-8's 19:38:56 observation + drone-6's
|
|
774
|
-
// 06:00:31 finding for the original incident trace.
|
|
775
|
-
const active = await requireActiveCube();
|
|
776
|
-
const [{ cube, roles }] = await Promise.all([
|
|
777
|
-
getCubeInfo(active.sessionToken, active.apiUrl),
|
|
778
|
-
getRoleInfo(active.sessionToken, active.apiUrl),
|
|
779
|
-
]);
|
|
780
|
-
const lines = [];
|
|
781
|
-
lines.push(`# Cube: ${cube.name}`);
|
|
782
|
-
lines.push('');
|
|
783
|
-
lines.push('## Cube directive');
|
|
784
|
-
lines.push(cube.cube_directive || '_(none)_');
|
|
785
|
-
lines.push('');
|
|
786
|
-
const taxonomyTip = nullTaxonomyTip(cube.message_taxonomy);
|
|
787
|
-
if (taxonomyTip) {
|
|
788
|
-
lines.push(taxonomyTip);
|
|
789
|
-
lines.push('');
|
|
790
|
-
}
|
|
791
|
-
lines.push('## Roles in this cube');
|
|
792
|
-
if (!roles.length) {
|
|
793
|
-
lines.push('_(no roles defined)_');
|
|
794
|
-
}
|
|
795
|
-
else {
|
|
796
|
-
for (const r of roles) {
|
|
797
|
-
const tags = [
|
|
798
|
-
r.role_class === 'queen' ? 'Queen' : null,
|
|
799
|
-
r.is_human_seat ? 'human-seat' : null,
|
|
800
|
-
r.is_default ? 'default' : null,
|
|
801
|
-
].filter(Boolean).join(', ');
|
|
802
|
-
const marker = tags ? ` (${tags})` : '';
|
|
803
|
-
const desc = r.short_description || '_(no description)_';
|
|
804
|
-
lines.push(`- **${r.name}**${marker} — ${desc}`);
|
|
805
|
-
}
|
|
806
|
-
// Sprint 6 / gh#153 discoverability nudge per drone-7 UX-FEEDBACK:
|
|
807
|
-
// point Coordinator-class readers at the tool that surfaces role IDs.
|
|
808
|
-
lines.push('');
|
|
809
|
-
lines.push('_(Coordinator-class drones can fetch role IDs via `borg:list-roles` for use with `borg:reassign-drone`.)_');
|
|
810
|
-
}
|
|
811
|
-
lines.push('');
|
|
812
|
-
lines.push(getDronePlaybook());
|
|
813
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
814
|
-
}
|
|
815
|
-
case 'borg:role': {
|
|
816
|
-
// No-cache invariant (T1.2 — 0.7.0): `getRoleInfo` MUST fetch
|
|
817
|
-
// fresh per invocation. The user-observable failure mode if
|
|
818
|
-
// this rots: a drone reassigned by `borg:reassign-drone`
|
|
819
|
-
// continues to read its OLD role until session restart, even
|
|
820
|
-
// though the DB has the new role. drone-8 hit a sub-second
|
|
821
|
-
// version of this on 2026-05-11 (timing race with reassign
|
|
822
|
-
// commit, not a code-level cache — confirmed by drone-6's
|
|
823
|
-
// 06:00:31 code reading). The regression probe at
|
|
824
|
-
// `client/__tests__/integration/01-role-cache-freshness.integration.ts`
|
|
825
|
-
// is the load-bearing test; if a future refactor caches the
|
|
826
|
-
// role payload anywhere (memoize, KV write on assimilate,
|
|
827
|
-
// session-token-keyed lookup, anything), the probe fails.
|
|
828
|
-
const active = await requireActiveCube();
|
|
829
|
-
const { role } = await getRoleInfo(active.sessionToken, active.apiUrl);
|
|
830
|
-
const text = [
|
|
831
|
-
`# Your role: ${role.name}`,
|
|
832
|
-
``,
|
|
833
|
-
role.detailed_description || '_(no detailed description set)_',
|
|
834
|
-
].join('\n');
|
|
835
|
-
return { content: [{ type: 'text', text }] };
|
|
836
|
-
}
|
|
837
|
-
case 'borg:role-rationale': {
|
|
838
|
-
const active = await requireActiveCube();
|
|
839
|
-
const role = typeof args?.role === 'string' ? args.role : '';
|
|
840
|
-
const section = typeof args?.section === 'string' ? args.section : '';
|
|
841
|
-
const result = await roleRationale(active.sessionToken, active.apiUrl, role, section);
|
|
842
|
-
const text = [
|
|
843
|
-
`# Role rationale: ${result.role} — ${result.section}`,
|
|
844
|
-
'',
|
|
845
|
-
result.body || '_(empty)_',
|
|
846
|
-
].join('\n');
|
|
847
|
-
return { content: [{ type: 'text', text }] };
|
|
848
|
-
}
|
|
849
|
-
case 'borg:roster': {
|
|
850
|
-
const active = await requireActiveCube();
|
|
851
|
-
const since = typeof args?.since === 'string' ? args.since : undefined;
|
|
852
|
-
const { drones, roles, since: resolvedSince } = await getRoster(active.sessionToken, active.apiUrl, since);
|
|
853
|
-
const text = renderRoster({
|
|
854
|
-
cubeName: active.name,
|
|
855
|
-
drones,
|
|
856
|
-
roles,
|
|
857
|
-
resolvedSince: resolvedSince ?? null,
|
|
858
|
-
humanAgo,
|
|
859
|
-
});
|
|
860
|
-
return { content: [{ type: 'text', text }] };
|
|
861
|
-
}
|
|
862
|
-
case 'borg:stream-status': {
|
|
863
|
-
// Probe the in-process SSE consumer state. Does NOT require
|
|
864
|
-
// an active cube — if the consumer hasn't started or is
|
|
865
|
-
// between cubes, the snapshot still reports current values.
|
|
866
|
-
// Also probes wake-path completeness (T1.2): is anyone tailing
|
|
867
|
-
// the inbox file? Without that, SSE delivery still works but
|
|
868
|
-
// the harness `/loop` never wakes on the file write.
|
|
869
|
-
const status = getStreamStatus();
|
|
870
|
-
const active = await getActiveCube();
|
|
871
|
-
const inboxPath = active
|
|
872
|
-
? inboxPathForDrone(active.cubeId, active.droneId)
|
|
873
|
-
: null;
|
|
874
|
-
const inboxMonitorHealthy = active
|
|
875
|
-
? isCodexRemoteWakeEnabled()
|
|
876
|
-
? true
|
|
877
|
-
: checkInboxMonitorHealthy(inboxPath)
|
|
878
|
-
: null;
|
|
879
|
-
let silentInertWarning = '';
|
|
880
|
-
if (status.runLoopHealth === 'silent-inert') {
|
|
881
|
-
silentInertWarning = '## ⚠ SSE stream loop silent-inert — run /mcp and reconnect to restart\n\n' +
|
|
882
|
-
'The log-stream consumer started but never connected. This drone will not receive real-time cube events.\n\n';
|
|
883
|
-
}
|
|
884
|
-
const text = renderStreamStatus({
|
|
885
|
-
status,
|
|
886
|
-
inboxMonitorHealthy,
|
|
887
|
-
inboxPath,
|
|
888
|
-
droneLabel: active?.droneLabel ?? null,
|
|
889
|
-
cubeName: active?.name ?? null,
|
|
890
|
-
humanAgo,
|
|
891
|
-
});
|
|
892
|
-
return { content: [{ type: 'text', text: silentInertWarning + text }] };
|
|
893
|
-
}
|
|
894
|
-
case 'borg:read-log': {
|
|
895
|
-
const active = await requireActiveCube();
|
|
896
|
-
const since = typeof args?.since === 'string' ? args.since : undefined;
|
|
897
|
-
const limit = typeof args?.limit === 'number' ? args.limit : undefined;
|
|
898
|
-
const unreadOnly = args?.unread_only === true || args?.unread_only === 'true';
|
|
899
|
-
const { entries, drones, roles, behind_by } = await readLog(active.sessionToken, active.apiUrl, {
|
|
900
|
-
since,
|
|
901
|
-
limit,
|
|
902
|
-
unreadOnly,
|
|
903
|
-
});
|
|
904
|
-
const droneById = new Map();
|
|
905
|
-
for (const d of drones)
|
|
906
|
-
droneById.set(d.id, d);
|
|
907
|
-
const roleById = new Map();
|
|
908
|
-
for (const r of roles)
|
|
909
|
-
roleById.set(r.id, r);
|
|
910
|
-
const lines = [];
|
|
911
|
-
lines.push(`# Activity log: ${active.name}`);
|
|
912
|
-
lines.push('');
|
|
913
|
-
if (!entries.length) {
|
|
914
|
-
lines.push('_(no entries)_');
|
|
915
|
-
}
|
|
916
|
-
else {
|
|
917
|
-
for (const e of entries) {
|
|
918
|
-
lines.push(formatLogEntryMarkdown(e, droneById, roleById));
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
// gh#709 part B: nudge a behind drone to drain. `behind_by` is the
|
|
922
|
-
// count of entries VISIBLE to this drone still unread AFTER this read
|
|
923
|
-
// advanced the watermark — if > 0 you under-read (limit-capped or a
|
|
924
|
-
// non-cursor read) and will skip messages unless you keep reading.
|
|
925
|
-
if (typeof behind_by === 'number' && behind_by > 0) {
|
|
926
|
-
lines.push('');
|
|
927
|
-
lines.push(`⚠ behind_by: ${behind_by} more unread ${behind_by === 1 ? 'entry' : 'entries'} addressed to you — call \`borg:read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`);
|
|
928
|
-
}
|
|
929
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
930
|
-
}
|
|
931
|
-
case 'borg:log': {
|
|
932
|
-
const message = args?.message;
|
|
933
|
-
if (!message || typeof message !== 'string')
|
|
934
|
-
throw new Error('message is required');
|
|
935
|
-
const active = await getActiveCube();
|
|
936
|
-
if (!active)
|
|
937
|
-
throw new Error('Not assimilated to a cube. Use borg:assimilate <cube-name> first.');
|
|
938
|
-
if (lifecycleSignalForMessage(message)) {
|
|
939
|
-
const decision = await shouldSuppressLifecycleLog(active, message);
|
|
940
|
-
if (decision.suppress) {
|
|
941
|
-
await recordLifecycleLog(active, message);
|
|
942
|
-
return {
|
|
943
|
-
content: [
|
|
944
|
-
{
|
|
945
|
-
type: 'text',
|
|
946
|
-
text: `Suppressed duplicate ${decision.signal?.toUpperCase()} lifecycle log for ${active.droneLabel}; recent cube log already contains this signal.`,
|
|
947
|
-
},
|
|
948
|
-
],
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
const hasTo = Object.prototype.hasOwnProperty.call(args ?? {}, 'to');
|
|
953
|
-
const recipients = hasTo ? normalizeDirectLogRecipients(args?.to) : undefined;
|
|
954
|
-
const explicitClass = typeof args?.class === 'string' ? args.class : undefined;
|
|
955
|
-
const visibility = args?.visibility === 'broadcast' || args?.visibility === 'direct'
|
|
956
|
-
? args.visibility
|
|
957
|
-
: undefined;
|
|
958
|
-
const appendOpts = {
|
|
959
|
-
...(explicitClass ? { class: explicitClass } : {}),
|
|
960
|
-
...(hasTo ? { to: recipients ?? [] } : {}),
|
|
961
|
-
...(visibility ? { visibility } : {}),
|
|
962
|
-
};
|
|
963
|
-
const result = await appendLog(active.sessionToken, active.apiUrl, message, appendOpts);
|
|
964
|
-
await recordLifecycleLog(active, message);
|
|
965
|
-
const echo = result.routing?.message ? `\n${result.routing.message}` : '';
|
|
966
|
-
// gh#534: surface to the SENDER which directed recipients are
|
|
967
|
-
// currently unreachable via the wake path. The message is delivered
|
|
968
|
-
// regardless (persisted server-side); they read it when they return.
|
|
969
|
-
const unreachable = result.unreachableRecipients?.length
|
|
970
|
-
? `\n⚠ ${result.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${result.unreachableRecipients
|
|
971
|
-
.map((r) => r.label)
|
|
972
|
-
.join(', ')}. Message delivered — they'll read it when they return.`
|
|
973
|
-
: '';
|
|
974
|
-
const text = `Logged to cube "${active.name}" as ${active.droneLabel}. (entry id: ${result.entry.id})${echo}${unreachable}`;
|
|
975
|
-
return { content: [{ type: 'text', text }] };
|
|
976
|
-
}
|
|
977
|
-
case 'borg:ack': {
|
|
978
|
-
const entryId = args?.entry_id;
|
|
979
|
-
if (!entryId || typeof entryId !== 'string') {
|
|
980
|
-
throw new Error('entry_id is required');
|
|
981
|
-
}
|
|
982
|
-
const active = await requireActiveCube();
|
|
983
|
-
await ackLogEntry(active.sessionToken, active.apiUrl, entryId);
|
|
984
|
-
return {
|
|
985
|
-
content: [
|
|
986
|
-
{ type: 'text', text: `Acked entry ${entryId} in cube "${active.name}".` },
|
|
987
|
-
],
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
case 'borg:list-cubes': {
|
|
991
|
-
const { cubes } = await listCubes();
|
|
992
|
-
if (!cubes.length) {
|
|
993
|
-
return { content: [{ type: 'text', text: 'No cubes yet. Use borg:create-cube to make your first one.' }] };
|
|
994
|
-
}
|
|
995
|
-
const lines = cubes.map((c) => `- **${c.name}** (id: ${c.id})\n ${(c.cube_directive || '_(no directive set)_').split('\n')[0].slice(0, 120)}`);
|
|
996
|
-
return { content: [{ type: 'text', text: `Your cubes (${cubes.length}):\n\n${lines.join('\n\n')}` }] };
|
|
997
|
-
}
|
|
998
|
-
case 'borg:create-cube': {
|
|
999
|
-
const name = args?.name;
|
|
1000
|
-
const cubeDirective = args?.cube_directive;
|
|
1001
|
-
const templateName = args?.template;
|
|
1002
|
-
if (!name)
|
|
1003
|
-
throw new Error('name is required');
|
|
1004
|
-
if (cubeDirective === undefined)
|
|
1005
|
-
throw new Error('cube_directive is required (pass empty string if none)');
|
|
1006
|
-
// Resolve template (validates name early so the cube isn't
|
|
1007
|
-
// created in a partial state if the template name is wrong).
|
|
1008
|
-
let template = null;
|
|
1009
|
-
if (templateName) {
|
|
1010
|
-
template = getTemplate(templateName);
|
|
1011
|
-
if (!template) {
|
|
1012
|
-
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
// Sprint 14: template cube_directive fills empty operator input.
|
|
1016
|
-
// Operator-supplied text takes precedence — templates fill
|
|
1017
|
-
// the blank, never stomp.
|
|
1018
|
-
const resolvedCubeDirective = resolveCubeDirectiveForCreate(cubeDirective, template);
|
|
1019
|
-
// v0.9.2: createCube now returns the flat shape directly
|
|
1020
|
-
// (see remote-client unwrap). `cube.id` / `cube.name` work
|
|
1021
|
-
// verbatim on the returned object.
|
|
1022
|
-
const resolvedMessageTaxonomy = resolveMessageTaxonomyForCreate(undefined, template);
|
|
1023
|
-
const cube = await createCube(name, resolvedCubeDirective, {
|
|
1024
|
-
message_taxonomy: resolvedMessageTaxonomy,
|
|
1025
|
-
});
|
|
1026
|
-
// Apply template roles if requested. Merges by name: any role the
|
|
1027
|
-
// server auto-seeded (e.g. "Drone") that the template doesn't
|
|
1028
|
-
// also include stays put; templated roles upsert.
|
|
1029
|
-
if (template) {
|
|
1030
|
-
const summary = await applyTemplateToCube(cube.id, template);
|
|
1031
|
-
const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
|
|
1032
|
-
? ' Template cube directive applied (operator passed empty).'
|
|
1033
|
-
: '';
|
|
1034
|
-
const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote} Use borg:assimilate ${cube.name} to join as a drone.`;
|
|
1035
|
-
return { content: [{ type: 'text', text }] };
|
|
1036
|
-
}
|
|
1037
|
-
const text = `Created cube **${cube.name}** (id: ${cube.id}). A default "Drone" role was seeded — rename or replace it via borg:update-role / borg:create-role / borg:delete-role. Use borg:assimilate ${cube.name} to join as a drone.`;
|
|
1038
|
-
return { content: [{ type: 'text', text }] };
|
|
1039
|
-
}
|
|
1040
|
-
case 'borg:update-cube': {
|
|
1041
|
-
const cubeId = args?.cube_id;
|
|
1042
|
-
if (!cubeId)
|
|
1043
|
-
throw new Error('cube_id is required');
|
|
1044
|
-
const updates = {};
|
|
1045
|
-
if (typeof args?.name === 'string')
|
|
1046
|
-
updates.name = args.name;
|
|
1047
|
-
if (typeof args?.cube_directive === 'string')
|
|
1048
|
-
updates.cube_directive = args.cube_directive;
|
|
1049
|
-
if (Array.isArray(args?.message_taxonomy))
|
|
1050
|
-
updates.message_taxonomy = args.message_taxonomy;
|
|
1051
|
-
if (Object.keys(updates).length === 0)
|
|
1052
|
-
throw new Error('Pass at least one of: name, cube_directive, message_taxonomy.');
|
|
1053
|
-
const { cube } = await updateCube(cubeId, updates);
|
|
1054
|
-
return { content: [{ type: 'text', text: `Updated cube **${cube.name}** (id: ${cube.id}).` }] };
|
|
1055
|
-
}
|
|
1056
|
-
case 'borg:patch-taxonomy-class': {
|
|
1057
|
-
const cubeId = args?.cube_id;
|
|
1058
|
-
if (!cubeId)
|
|
1059
|
-
throw new Error('cube_id is required');
|
|
1060
|
-
const action = args?.action;
|
|
1061
|
-
if (action !== 'add' && action !== 'replace' && action !== 'remove') {
|
|
1062
|
-
throw new Error('action must be one of: add, replace, remove.');
|
|
1063
|
-
}
|
|
1064
|
-
let cube;
|
|
1065
|
-
let label;
|
|
1066
|
-
if (action === 'remove') {
|
|
1067
|
-
const className = args?.class;
|
|
1068
|
-
if (!className)
|
|
1069
|
-
throw new Error('class is required for remove.');
|
|
1070
|
-
({ cube } = await patchTaxonomyClass(cubeId, { action, class: className }));
|
|
1071
|
-
label = className;
|
|
1072
|
-
}
|
|
1073
|
-
else {
|
|
1074
|
-
const classDef = args?.class_def;
|
|
1075
|
-
if (classDef == null || typeof classDef !== 'object' || Array.isArray(classDef)) {
|
|
1076
|
-
throw new Error('class_def (object) is required for add/replace.');
|
|
1077
|
-
}
|
|
1078
|
-
({ cube } = await patchTaxonomyClass(cubeId, { action, class_def: classDef }));
|
|
1079
|
-
label = String(classDef.class ?? '');
|
|
1080
|
-
}
|
|
1081
|
-
const verb = action === 'add' ? 'Added' : action === 'replace' ? 'Replaced' : 'Removed';
|
|
1082
|
-
return { content: [{ type: 'text', text: `${verb} taxonomy class **${label}** in cube **${cube.name}** (id: ${cube.id}).` }] };
|
|
1083
|
-
}
|
|
1084
|
-
case 'borg:delete-cube': {
|
|
1085
|
-
const cubeId = args?.cube_id;
|
|
1086
|
-
if (!cubeId)
|
|
1087
|
-
throw new Error('cube_id is required');
|
|
1088
|
-
await deleteCube(cubeId);
|
|
1089
|
-
return { content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }] };
|
|
1090
|
-
}
|
|
1091
|
-
case 'borg:create-role': {
|
|
1092
|
-
const cubeId = args?.cube_id;
|
|
1093
|
-
const name = args?.name;
|
|
1094
|
-
const shortDesc = args?.short_description;
|
|
1095
|
-
const detailedDesc = args?.detailed_description;
|
|
1096
|
-
if (!cubeId)
|
|
1097
|
-
throw new Error('cube_id is required');
|
|
1098
|
-
if (!name)
|
|
1099
|
-
throw new Error('name is required');
|
|
1100
|
-
if (shortDesc === undefined)
|
|
1101
|
-
throw new Error('short_description is required (pass empty string if none)');
|
|
1102
|
-
if (detailedDesc === undefined)
|
|
1103
|
-
throw new Error('detailed_description is required (pass empty string if none)');
|
|
1104
|
-
const isDefault = args?.is_default === true;
|
|
1105
|
-
const isHumanSeat = args?.is_human_seat === true;
|
|
1106
|
-
const canBroadcast = args?.can_broadcast === true;
|
|
1107
|
-
const receivesAllDirect = args?.receives_all_direct === true;
|
|
1108
|
-
const { role } = await createRole(cubeId, {
|
|
1109
|
-
name,
|
|
1110
|
-
short_description: shortDesc,
|
|
1111
|
-
detailed_description: detailedDesc,
|
|
1112
|
-
is_default: isDefault,
|
|
1113
|
-
is_human_seat: isHumanSeat,
|
|
1114
|
-
can_broadcast: canBroadcast,
|
|
1115
|
-
receives_all_direct: receivesAllDirect,
|
|
1116
|
-
});
|
|
1117
|
-
const tags = [
|
|
1118
|
-
role.role_class === 'queen' ? 'Queen' : null,
|
|
1119
|
-
role.is_human_seat ? 'human-seat' : null,
|
|
1120
|
-
role.is_default ? 'default' : null,
|
|
1121
|
-
].filter(Boolean).join(', ');
|
|
1122
|
-
const tag = tags ? ` (${tags})` : '';
|
|
1123
|
-
return { content: [{ type: 'text', text: `Created role **${role.name}**${tag} (id: ${role.id}) in cube ${cubeId}.` }] };
|
|
1124
|
-
}
|
|
1125
|
-
case 'borg:update-role': {
|
|
1126
|
-
const roleId = args?.role_id;
|
|
1127
|
-
if (!roleId)
|
|
1128
|
-
throw new Error('role_id is required');
|
|
1129
|
-
const updates = {};
|
|
1130
|
-
if (typeof args?.name === 'string')
|
|
1131
|
-
updates.name = args.name;
|
|
1132
|
-
if (typeof args?.short_description === 'string')
|
|
1133
|
-
updates.short_description = args.short_description;
|
|
1134
|
-
if (typeof args?.detailed_description === 'string')
|
|
1135
|
-
updates.detailed_description = args.detailed_description;
|
|
1136
|
-
if (typeof args?.is_default === 'boolean')
|
|
1137
|
-
updates.is_default = args.is_default;
|
|
1138
|
-
if (typeof args?.is_human_seat === 'boolean')
|
|
1139
|
-
updates.is_human_seat = args.is_human_seat;
|
|
1140
|
-
if (typeof args?.can_broadcast === 'boolean')
|
|
1141
|
-
updates.can_broadcast = args.can_broadcast;
|
|
1142
|
-
if (typeof args?.receives_all_direct === 'boolean')
|
|
1143
|
-
updates.receives_all_direct = args.receives_all_direct;
|
|
1144
|
-
if (Object.keys(updates).length === 0)
|
|
1145
|
-
throw new Error('Pass at least one of: name, short_description, detailed_description, is_default, is_human_seat, can_broadcast, receives_all_direct.');
|
|
1146
|
-
const { role } = await updateRole(roleId, updates);
|
|
1147
|
-
const tags = [
|
|
1148
|
-
role.role_class === 'queen' ? 'Queen' : null,
|
|
1149
|
-
role.is_human_seat ? 'human-seat' : null,
|
|
1150
|
-
role.is_default ? 'default' : null,
|
|
1151
|
-
].filter(Boolean).join(', ');
|
|
1152
|
-
const tag = tags ? ` (${tags})` : '';
|
|
1153
|
-
return { content: [{ type: 'text', text: `Updated role **${role.name}**${tag} (id: ${role.id}).` }] };
|
|
1154
|
-
}
|
|
1155
|
-
case 'borg:patch-role-section': {
|
|
1156
|
-
const roleId = args?.role_id;
|
|
1157
|
-
if (!roleId)
|
|
1158
|
-
throw new Error('role_id is required');
|
|
1159
|
-
const action = args?.action;
|
|
1160
|
-
if (action !== 'replace' && action !== 'insert' && action !== 'delete') {
|
|
1161
|
-
throw new Error('action must be one of: replace, insert, delete.');
|
|
1162
|
-
}
|
|
1163
|
-
const heading = args?.heading;
|
|
1164
|
-
if (!heading)
|
|
1165
|
-
throw new Error('heading is required');
|
|
1166
|
-
let role;
|
|
1167
|
-
if (action === 'delete') {
|
|
1168
|
-
({ role } = await patchRoleSection(roleId, { action, heading }));
|
|
1169
|
-
}
|
|
1170
|
-
else {
|
|
1171
|
-
const body = args?.body;
|
|
1172
|
-
if (typeof body !== 'string') {
|
|
1173
|
-
throw new Error('body is required for replace/insert (pass empty string for an empty section).');
|
|
1174
|
-
}
|
|
1175
|
-
if (action === 'insert') {
|
|
1176
|
-
const after = (typeof args?.after === 'string' ? args.after : null);
|
|
1177
|
-
({ role } = await patchRoleSection(roleId, { action, heading, body, after }));
|
|
1178
|
-
}
|
|
1179
|
-
else {
|
|
1180
|
-
({ role } = await patchRoleSection(roleId, { action, heading, body }));
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
const verb = action === 'replace' ? 'Replaced' : action === 'insert' ? 'Inserted' : 'Deleted';
|
|
1184
|
-
return { content: [{ type: 'text', text: `${verb} section **${heading}** in role **${role.name}** (id: ${role.id}).` }] };
|
|
1185
|
-
}
|
|
1186
|
-
case 'borg:delete-role': {
|
|
1187
|
-
const roleId = args?.role_id;
|
|
1188
|
-
if (!roleId)
|
|
1189
|
-
throw new Error('role_id is required');
|
|
1190
|
-
await deleteRole(roleId);
|
|
1191
|
-
return { content: [{ type: 'text', text: `Deleted role ${roleId}.` }] };
|
|
1192
|
-
}
|
|
1193
|
-
case 'borg:reassign-drone': {
|
|
1194
|
-
const droneId = args?.drone_id;
|
|
1195
|
-
const roleId = args?.role_id;
|
|
1196
|
-
if (!droneId)
|
|
1197
|
-
throw new Error('drone_id is required');
|
|
1198
|
-
if (!roleId)
|
|
1199
|
-
throw new Error('role_id is required');
|
|
1200
|
-
const { drone } = await reassignDrone(droneId, roleId);
|
|
1201
|
-
return { content: [{ type: 'text', text: `Reassigned drone ${drone.label} (${drone.id}) to role ${drone.role_id}.` }] };
|
|
1202
|
-
}
|
|
1203
|
-
case 'borg:list-drones': {
|
|
1204
|
-
const cubeId = args?.cube_id;
|
|
1205
|
-
if (!cubeId)
|
|
1206
|
-
throw new Error('cube_id is required');
|
|
1207
|
-
const { drones, roles } = await getCube(cubeId);
|
|
1208
|
-
if (!drones.length) {
|
|
1209
|
-
return { content: [{ type: 'text', text: 'No drones in this cube yet.' }] };
|
|
1210
|
-
}
|
|
1211
|
-
const rolesById = new Map(roles.map((r) => [r.id, r]));
|
|
1212
|
-
const lines = drones.map((d) => {
|
|
1213
|
-
const r = rolesById.get(d.role_id);
|
|
1214
|
-
const roleLabel = formatRoleAgentLabel(r?.name ?? '?', d.agent_kind);
|
|
1215
|
-
const wakeClass = d.wake_path_alert_class && d.wake_path_alert_class !== 'independent'
|
|
1216
|
-
? ` — wake-path-class: ${d.wake_path_alert_class}`
|
|
1217
|
-
: '';
|
|
1218
|
-
return `- **${d.label}** (id: ${d.id}) — role: ${roleLabel} (${d.role_id}) — last seen ${d.last_seen}${wakeClass}`;
|
|
1219
|
-
});
|
|
1220
|
-
return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n${lines.join('\n')}` }] };
|
|
1221
|
-
}
|
|
1222
|
-
case 'borg:list-roles': {
|
|
1223
|
-
// Sprint 6 / gh#153: surface role IDs to Coordinator-class drones
|
|
1224
|
-
// for use with borg:reassign-drone (e.g. promoting a drone to Queen).
|
|
1225
|
-
// Uses the same owner-scoped GET /api/cubes/:id endpoint as
|
|
1226
|
-
// borg:list-drones — data is already accessible to the cube owner
|
|
1227
|
-
// via the dashboard surface; this tool just makes role IDs
|
|
1228
|
-
// discoverable inside the MCP tool namespace per drone-7's UX-FEEDBACK.
|
|
1229
|
-
// Render logic extracted to list-roles-render.ts for testability
|
|
1230
|
-
// per drone-3 QA-FAIL 2026-05-18T13:27:53Z.
|
|
1231
|
-
const cubeId = args?.cube_id;
|
|
1232
|
-
if (!cubeId)
|
|
1233
|
-
throw new Error('cube_id is required');
|
|
1234
|
-
const { roles } = await getCube(cubeId);
|
|
1235
|
-
return { content: [{ type: 'text', text: renderRoleList(roles, cubeId) }] };
|
|
1236
|
-
}
|
|
1237
|
-
case 'borg:list-templates': {
|
|
1238
|
-
const names = listTemplateNames();
|
|
1239
|
-
const lines = names.map((n) => {
|
|
1240
|
-
const t = getTemplate(n);
|
|
1241
|
-
return `- **${n}**: ${t.description}`;
|
|
1242
|
-
});
|
|
1243
|
-
return { content: [{ type: 'text', text: `Available templates:\n\n${lines.join('\n')}` }] };
|
|
1244
|
-
}
|
|
1245
|
-
case 'borg:sync-roles': {
|
|
1246
|
-
const cubeId = args?.cube_id;
|
|
1247
|
-
const templateName = args?.template_name || 'software-dev';
|
|
1248
|
-
const apply = args?.apply === true;
|
|
1249
|
-
// gh#473 PR2 — per-conflict-fragment accept/reject decisions.
|
|
1250
|
-
// Keyed on the stable fragment key surfaced by the dry-run (e.g.
|
|
1251
|
-
// `role:Builder:section:Workflow`). Unspecified conflicts default
|
|
1252
|
-
// to REJECT (keep the cube's evolved text).
|
|
1253
|
-
const decisions = args?.decisions && typeof args.decisions === 'object'
|
|
1254
|
-
? args.decisions
|
|
1255
|
-
: undefined;
|
|
1256
|
-
if (!cubeId)
|
|
1257
|
-
throw new Error('cube_id is required');
|
|
1258
|
-
const result = await syncRoles(cubeId, templateName, apply, decisions);
|
|
1259
|
-
return { content: [{ type: 'text', text: renderSyncRolesResult(result, templateName) }] };
|
|
1260
|
-
}
|
|
1261
|
-
case 'borg:apply-template': {
|
|
1262
|
-
const cubeId = args?.cube_id;
|
|
1263
|
-
const templateName = args?.template_name;
|
|
1264
|
-
if (!cubeId)
|
|
1265
|
-
throw new Error('cube_id is required');
|
|
1266
|
-
if (!templateName)
|
|
1267
|
-
throw new Error('template_name is required');
|
|
1268
|
-
const template = getTemplate(templateName);
|
|
1269
|
-
if (!template) {
|
|
1270
|
-
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
1271
|
-
}
|
|
1272
|
-
const summary = await applyTemplateToCube(cubeId, template);
|
|
1273
|
-
// Sprint 14: optionally write template's cube_directive to the
|
|
1274
|
-
// cube. No-clobber discipline — only fills empty directives,
|
|
1275
|
-
// never overwrites operator-customized text.
|
|
1276
|
-
let cubeDirectiveNote = '';
|
|
1277
|
-
const cubeForRules = await getCube(cubeId);
|
|
1278
|
-
const newCubeDirective = resolveCubeDirectiveForApply(cubeForRules.cube_directive, template);
|
|
1279
|
-
if (newCubeDirective !== null) {
|
|
1280
|
-
await updateCube(cubeId, { cube_directive: newCubeDirective });
|
|
1281
|
-
cubeDirectiveNote = ' Template cube directive applied (cube directive was empty).';
|
|
1282
|
-
}
|
|
1283
|
-
return { content: [{ type: 'text', text: `Applied template **${templateName}** to cube ${cubeId} — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote}` }] };
|
|
1284
|
-
}
|
|
1285
|
-
default:
|
|
1286
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
catch (error) {
|
|
1290
|
-
// Better error messages for auth/subscription issues
|
|
1291
|
-
if (error.message?.includes('Authentication required') ||
|
|
1292
|
-
error.message?.includes('Authentication expired') ||
|
|
1293
|
-
error.message?.includes('Failed to refresh')) {
|
|
1294
|
-
return {
|
|
1295
|
-
content: [
|
|
1296
|
-
{
|
|
1297
|
-
type: 'text',
|
|
1298
|
-
text: '◼ Authentication expired. Run: borg assimilate',
|
|
1299
|
-
},
|
|
1300
|
-
],
|
|
1301
|
-
isError: true,
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
return {
|
|
1305
|
-
content: [
|
|
1306
|
-
{
|
|
1307
|
-
type: 'text',
|
|
1308
|
-
text: `Error: ${error.message}`,
|
|
1309
|
-
},
|
|
1310
|
-
],
|
|
1311
|
-
isError: true,
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
|
-
});
|
|
1315
|
-
// Register prompts listing
|
|
1316
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1317
|
-
return {
|
|
1318
|
-
prompts: [
|
|
1319
|
-
{
|
|
1320
|
-
name: 'subscribe',
|
|
1321
|
-
description: 'Set up Borg MCP Cube tier subscription ($1/cube/month — unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial.',
|
|
1322
|
-
},
|
|
1323
|
-
{
|
|
1324
|
-
name: 'dashboard',
|
|
1325
|
-
description: 'Open Borg MCP dashboard to manage cubes',
|
|
1326
|
-
},
|
|
1327
|
-
],
|
|
1328
|
-
};
|
|
1329
|
-
});
|
|
1330
|
-
// Register prompt getter
|
|
1331
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1332
|
-
const { name } = request.params;
|
|
1333
|
-
switch (name) {
|
|
1334
|
-
case 'subscribe':
|
|
1335
|
-
return {
|
|
1336
|
-
description: 'Set up Borg MCP Cube tier subscription ($1/cube/month — unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial.',
|
|
1337
|
-
messages: [
|
|
1338
|
-
{
|
|
1339
|
-
role: 'user',
|
|
1340
|
-
content: {
|
|
1341
|
-
type: 'text',
|
|
1342
|
-
text: 'Please help me set up a Borg MCP subscription using the subscribe tool.',
|
|
1343
|
-
},
|
|
1344
|
-
},
|
|
1345
|
-
],
|
|
1346
|
-
};
|
|
1347
|
-
case 'dashboard':
|
|
1348
|
-
return {
|
|
1349
|
-
description: 'Open Borg MCP dashboard to manage cubes',
|
|
1350
|
-
messages: [
|
|
1351
|
-
{
|
|
1352
|
-
role: 'user',
|
|
1353
|
-
content: {
|
|
1354
|
-
type: 'text',
|
|
1355
|
-
text: 'Please open the Borg MCP dashboard using the open_dashboard tool.',
|
|
1356
|
-
},
|
|
1357
|
-
},
|
|
1358
|
-
],
|
|
1359
|
-
};
|
|
1360
|
-
default:
|
|
1361
|
-
throw new Error(`Unknown prompt: ${name}`);
|
|
1362
|
-
}
|
|
1363
|
-
});
|
|
1364
|
-
// Create stdio transport
|
|
1365
|
-
const transport = new StdioServerTransport();
|
|
1366
|
-
// Connect server to transport
|
|
1367
|
-
await server.connect(transport);
|
|
1368
|
-
// Resolve drone self-identification prefix before any console output
|
|
1369
|
-
// (gh#25). Falls back to `[unassimilated · <repo>]` if no cube cached.
|
|
1370
|
-
await initConsolePrefix();
|
|
1371
|
-
console.error(`${consolePrefix()}◼ Borg MCP Client started`);
|
|
1372
|
-
console.error(`${consolePrefix()}◼ Use borg:assimilate <cube-name> to join a cube as a drone`);
|
|
1373
|
-
console.error(`${consolePrefix()}◼ Manage your cubes at https://borgmcp.ai/dashboard`);
|
|
1374
|
-
}
|
|
1375
|
-
main().catch((error) => {
|
|
1376
|
-
console.error(`${consolePrefix()}Fatal error:`, error);
|
|
1377
|
-
process.exit(1);
|
|
1378
|
-
});
|
|
1379
|
-
//# sourceMappingURL=index.js.map
|
|
2
|
+
import{Server as V}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as K}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Q,ListToolsRequestSchema as Y,ListPromptsRequestSchema as G,GetPromptRequestSchema as z}from"@modelcontextprotocol/sdk/types.js";import{assimilate as X,getCubeInfo as J,getRoleInfo as j,getRoster as Z,readLog as ee,appendLog as te,ackLogEntry as re,regen as oe,listCubes as ne,createCube as ie,updateCube as O,deleteCube as se,createRole as ae,updateRole as ce,patchRoleSection as S,patchTaxonomyClass as T,deleteRole as le,reassignDrone as de,getCube as U,checkSubscriptionStatus as pe,createSubscription as ue,syncRoles as me,applyTemplate as he,whoami as be,roleRationale as ge,API_URL as ye,getValidToken as fe}from"./remote-client.js";import{startHealthBeatTick as we}from"./health-beat.js";import{getTemplate as C,listTemplateNames as I,resolveCubeDirectiveForCreate as _e,resolveCubeDirectiveForApply as ve,resolveMessageTaxonomyForCreate as xe}from"./templates.js";import{activeCubeWithFreshRegenIdentity as ke,getActiveCube as y,setActiveCube as P,inboxPathForDrone as f}from"./cubes.js";import{addSessionStartHook as $e,addUserPromptSubmitHook as Se}from"./config-utils.js";import{humanAgo as A,formatLogEntryMarkdown as Ue,formatRegenMarkdown as Ce,getDronePlaybook as N,nullTaxonomyTip as Ie,regenWakePathDroneLabel as qe}from"./regen-format.js";import{startLogStream as Re,getStreamStatus as q}from"./log-stream.js";import{renderRoleList as De}from"./list-roles-render.js";import{getPackageVersion as w,getOnDiskVersion as Ee,handleVersionFlag as je}from"./version.js";import{renderStreamStatus as Oe,checkInboxMonitorHealthy as R,formatWakePathPrefix as Te,shouldShowWakePathWarning as Pe}from"./stream-status.js";import{formatRoleAgentLabel as Ae,renderRoster as Ne}from"./roster-render.js";import{renderSyncRolesResult as Le}from"./sync-roles-render.js";import{initConsolePrefix as Me,consolePrefix as _}from"./console-prefix.js";import{isCodexRemoteWakeEnabled as v,resolveSessionAgentKind as L,probeCodexBridgeArmed as Be}from"./codex-app-wake.js";import{lifecycleSignalForMessage as Fe,recordLifecycleLog as M,shouldSuppressLifecycleLog as We}from"./lifecycle-log-guard.js";import{normalizeDirectLogRecipients as He}from"./direct-log.js";import Ve from"open";import Ke from"os";function B(){try{const p=Ke.hostname();return p&&p.trim()?p.trim().slice(0,255):null}catch{return null}}async function F(p,x){return await he(p,x.name)}async function b(){const p=await y();if(!p)throw new Error("Not assimilated to a cube. Use borg:assimilate <cube-name> first.");return p}async function Qe(){je();try{$e()}catch{}try{Se()}catch{}try{Re()}catch{}try{we({getActiveCube:y,getStreamConnected:()=>q().connected,getInboxPath:h=>f(h.cubeId,h.droneId),checkMonitor:R,isCodexRemoteWake:v,probeBridgeArmed:h=>Be({cubeId:h.cubeId,droneId:h.droneId}),resolveAgentKind:L,resolveHostname:B,resolveVersion:w,getToken:fe,fetchImpl:globalThis.fetch.bind(globalThis)})}catch{}const p=new V({name:"borg-mcp-client",version:w()},{capabilities:{tools:{},prompts:{}}});p.setRequestHandler(Y,async()=>({tools:[{name:"subscribe",description:"Create Stripe checkout session for Cube tier ($1/cube/month \u2014 unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial.",inputSchema:{type:"object",properties:{},required:[]}},{name:"subscription_status",description:"Check subscription status",inputSchema:{type:"object",properties:{},required:[]}},{name:"open_dashboard",description:"Open Borg MCP dashboard in browser to manage cubes, roles, and drones",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:regen",description:"Refresh your context as a Drone. Returns the active cube's directive, your role's detailed playbook, the drone roster, and recent activity log entries \u2014 everything you need to be oriented. Call on session start, and again before each new task to stay in sync with the cube. Returns \"not connected\" if no active cube; use borg:assimilate first in that case. Optional `since` (entry-id UUID or ISO-8601 timestamp) trims the recent-log section to entries strictly after the anchor \u2014 pass your last-seen entry id to skip already-processed history on each refresh.",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional cursor. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple) OR an ISO-8601 timestamp. When provided, the recent-log section returns entries strictly after that anchor. Non-existent UUID falls back to default recent window."},mode:{type:"string",enum:["full","lite"],description:"Optional output mode. Use full at session start and after context compaction. Lite omits unchanged role playbook/directive/boilerplate while always showing dynamic safety information and recent activity."}},required:[]}},{name:"borg:assimilate",description:"Connect this Claude session as a Drone to a Cube. Provide the cube's name. Returns the cube's directive, your assigned role's detailed instructions, and persists a session token locally so subsequent borg: tools work for this cube.",inputSchema:{type:"object",properties:{cube_name:{type:"string",description:"The cube to connect to"}},required:["cube_name"]}},{name:"borg:cube",description:"Read the active Cube's directive and the registry of all roles in it (each role's name + short description). Use to remind yourself of cube-wide context.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:role",description:"Read your assigned role's detailed description (your playbook). Other drones cannot see this \u2014 only you (drones in this role).",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:version",description:"Returns the installed borgmcp client version. Use to verify which version is running in this MCP session.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:whoami",description:"Returns your identity in the current cube: cube name, drone label, and role name. Use to confirm which cube/role/drone you are.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:role-rationale",description:"Fetch an on-demand rationale/case-study section for a role playbook. Pass a role name/id and a plain-label section key to read the rationale without expanding every regen.",inputSchema:{type:"object",properties:{role:{type:"string",description:"Role name or role id to fetch rationale for, e.g. Builder."},section:{type:"string",description:"Plain-label role section key, e.g. Workflow rationale."}},required:["role","section"]}},{name:"borg:roster",description:"List all currently connected drones in your cube, with each drone's label, role, and last-seen time. Optional `since` argument adds a sender-side liveness column \u2014 pass either an activity_log entry id (e.g., from a dispatch you posted) or an ISO-8601 timestamp; each drone is marked `awake` if they've posted a log entry after that point, otherwise `stale-since-X`. Useful for confirming a dispatch reached its named recipients (catches the silent-wake-path-failure class where SSE delivered but the drone's /loop never woke).",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional liveness reference point. Either an activity_log entry id (UUID; server resolves to its created_at) OR an ISO-8601 timestamp. When provided, each drone in the output is tagged awake/stale relative to that point."}},required:[]}},{name:"borg:stream-status",description:"Diagnostic probe for the SSE log-stream consumer. Returns the live state of the local stream connection \u2014 `connected`, `lastContentEventAt` (most recent log/bookmark event), `lastWireActivityAt` (most recent event of any type, incl. heartbeats), `lastHeartbeatAt`, `lastPersistedEventId`, and `reconnectAttempts` \u2014 plus a wake-path completeness check that surfaces if SSE is attached but no inbox-Monitor is watching the file (the silent-failure mode where Claude's `/loop` never wakes on incoming entries). Reads in-process state from the running borgmcp client; does NOT re-open the stream, so calling it cannot perturb the very thing it's observing. Useful when troubleshooting wake-up issues, verifying the stream is alive without other drones logging, or pre-checking before fault-injection tests.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:read-log",description:"Read recent entries from the cube's activity log. Each entry is tagged with the drone that wrote it and that drone's role. Optional: since (entry-id UUID OR ISO-8601 timestamp \u2014 returns entries strictly after the anchor; non-existent UUID falls back to the default recent window), limit (1\u2013500, default 50), and unread_only (boolean \u2014 return only entries posted after this drone last called read-log; the server advances the watermark to the newest returned entry, so subsequent unread_only calls only show fresh activity).",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional cursor. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple for deterministic tie-break) OR an ISO-8601 timestamp."},limit:{type:"number",description:"max entries to return (1-500)"},unread_only:{type:"boolean",description:"When true, filter to entries posted after this drone last called read-log. Composes with `since`. Server advances the watermark to the newest returned entry on every call (Sprint 25 log substrate refactor)."}}}},{name:"borg:ack",description:"Mark a log entry as explicitly acknowledged. Replaces the convention of posting `ACK: <dispatch-id>` log entries. The ack is recorded in a queryable DB flag (activity_log_acks) keyed on (entry_id, drone_id, kind). Idempotent \u2014 repeated calls on the same entry are no-ops. Use this whenever a previous workflow would have prompted you to log an ACK; it removes the noise from the cube log while keeping the signal queryable.",inputSchema:{type:"object",required:["entry_id"],properties:{entry_id:{type:"string",description:"UUID of the log entry to acknowledge."}}}},{name:"borg:log",description:"Append a message to the cube's activity log. By default entries broadcast to all drones. When a cube declares a message taxonomy, borg:log applies class-based smart defaults: prefix-matched directed classes route to their default recipients unless you pass `to:`, `class:`, or explicit visibility. Pass `to: [...]` to direct by exact drone label, drone id, role name, or role slug.",inputSchema:{type:"object",properties:{message:{type:"string",description:"The log message (max 10KB)."},to:{type:"array",items:{type:"string"},description:"Optional direct-message recipients by exact drone label, drone id, role name, or role slug (resolves to all drones in that role). Omit to let class-based routing or broadcast defaults apply."},class:{type:"string",description:"Optional declared message class. Overrides prefix auto-classification when the cube declares a message taxonomy."},visibility:{type:"string",enum:["broadcast","direct"],description:"Optional explicit visibility. Overrides class-based routing defaults."}},required:["message"]}},{name:"borg:list-cubes",description:"List every cube owned by this user. Returns id, name, cube_directive, and timestamps for each. Useful before assimilate to see what's available, or as a starting point for any management action.",inputSchema:{type:"object",properties:{}}},{name:"borg:create-cube",description:'Create a new cube. The server seeds a default "Drone" role atomically so the cube is assimilatable immediately. Pass an optional `template` name to apply a richer role set instead (see borg:list-templates / borg:apply-template).',inputSchema:{type:"object",properties:{name:{type:"string",description:"Cube name (lowercase letters, digits, hyphens; max 64 chars).",pattern:"^[a-z0-9-]+$",maxLength:64},cube_directive:{type:"string",description:"Markdown text every drone in this cube will see in regen. Anything project-specific."},template:{type:"string",description:'Optional template name to apply after cube creation (e.g. "software-dev"). Roles are merged by name; the default Drone role gets overwritten by the template if a same-named role is in the template.'}},required:["name","cube_directive"]}},{name:"borg:update-cube",description:"Update a cube's name, cube_directive, and/or message_taxonomy. Pass only what changes.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to update."},name:{type:"string",description:"New name (optional). Lowercase letters, digits, hyphens; max 64 chars.",pattern:"^[a-z0-9-]+$",maxLength:64},cube_directive:{type:"string",description:"New cube directive markdown (optional)."},message_taxonomy:{type:"array",description:"New message-class taxonomy (optional). REPLACES the whole taxonomy; the worker re-validates the full array (non-overlapping prefixes, unique class names, directed classes need default_to). Pass [] to clear. To change ONE class without resending the whole array, use borg:patch-taxonomy-class instead. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.",items:{type:"object",properties:{class:{type:"string",description:"Unique class name."},prefixes:{type:"array",items:{type:"string"},description:"Message prefixes routed by this class."},routing:{type:"string",enum:["broadcast","directed"],description:"Routing mode."},default_to:{type:"array",items:{type:"string"},description:"Default recipients (role name/slug/label, or @human-seat) for a directed class."},lifecycle:{type:"string",enum:["dispatch","completion"],description:"Optional lifecycle marker for stuck-dispatch detection."}}}}},required:["cube_id"]}},{name:"borg:patch-taxonomy-class",description:"Surgically patch ONE message-class within a cube's message_taxonomy, leaving other classes unchanged. Use this instead of borg:update-cube when adding/changing a single class so you don't resend (and risk clobbering) the whole taxonomy. action=add appends a new class; action=replace overwrites the class with the same name (case-insensitive); action=remove drops a class. The whole resulting taxonomy is re-validated (non-overlapping prefixes, unique class names, directed classes need default_to) \u2014 a single-class patch that breaks a cross-class rule against an untouched class is rejected. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to patch."},action:{type:"string",enum:["add","replace","remove"],description:"add / replace / remove a single class."},class_def:{type:"object",description:'The class definition (for add/replace). Shape: { class, prefixes?, routing: "broadcast"|"directed", default_to?, lifecycle? }.',properties:{class:{type:"string",description:"Unique class name."},prefixes:{type:"array",items:{type:"string"},description:"Message prefixes routed by this class."},routing:{type:"string",enum:["broadcast","directed"],description:"Routing mode."},default_to:{type:"array",items:{type:"string"},description:"Default recipients (required for directed classes): role name/slug/label, or @human-seat."},lifecycle:{type:"string",enum:["dispatch","completion"],description:"Optional lifecycle marker for stuck-dispatch detection."}},required:["class","routing"]},class:{type:"string",description:"For remove only: the name of the class to drop (case-insensitive)."}},required:["cube_id","action"]}},{name:"borg:delete-cube",description:"Delete a cube and all its roles, drones, and log entries. Irreversible \u2014 confirm with the user before invoking unless the cube is clearly disposable.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to delete."}},required:["cube_id"]}},{name:"borg:create-role",description:"Create a role inside a cube. The detailed_description is the role's playbook \u2014 only drones assigned to this role see it. Setting is_default=true demotes any existing default; a cube has exactly one default role at a time.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube this role belongs to."},name:{type:"string",description:'Role name (e.g. "Builder", "Reviewer").'},short_description:{type:"string",description:"One-line summary, shown to every drone in the cube."},detailed_description:{type:"string",description:"Full playbook for drones in this role \u2014 workflow, conventions, log signals to post."},is_default:{type:"boolean",description:"If true, new drones assimilating into this cube are assigned this role. Demotes the previous default."},is_human_seat:{type:"boolean",description:"If true, this role represents the cube's human-occupied seat (where the human Queen sits directly). The class-hierarchy guard in reassign-drone allows promotion FROM a human-seat role TO the platform Queen role; promotion from non-human-seat roles is rejected."},can_broadcast:{type:"boolean",description:"If true, drones in this role may post broadcast log entries when strict broadcast gating is enabled."},receives_all_direct:{type:"boolean",description:"If true, drones in this role can see direct log entries as observer/audit recipients."}},required:["cube_id","name","short_description","detailed_description"]}},{name:"borg:update-role",description:"Update a role. Pass only the fields that change. Promoting to is_default demotes the previous default in the same cube.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to update."},name:{type:"string",description:"New role name (optional)."},short_description:{type:"string",description:"New short description (optional)."},detailed_description:{type:"string",description:"New detailed playbook (optional)."},is_default:{type:"boolean",description:"Set true to make this the cube's default role (optional)."},is_human_seat:{type:"boolean",description:"Set true/false to mark/unmark this as the cube's human-occupied seat (the elevation source for the platform Queen role)."},can_broadcast:{type:"boolean",description:"Set true/false to allow or deny broadcast log entries when strict broadcast gating is enabled."},receives_all_direct:{type:"boolean",description:"Set true/false to grant or remove observer visibility into direct log entries."}},required:["role_id"]}},{name:"borg:patch-role-section",description:"Surgically patch ONE named section of a role's detailed_description, leaving the rest of the field byte-identical. Sections are delimited by plain-label lines (e.g. `Workflow:`, `Project conventions:`) \u2014 NOT markdown headings; text before the first label is the preamble. Use this instead of borg:update-role when changing a single section so you don't have to resend (and risk clobbering) the whole playbook. action=replace overwrites a section's body; action=insert adds a new section (optionally after a named one, else appended); action=delete removes a section.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to patch."},action:{type:"string",enum:["replace","insert","delete"],description:"replace / insert / delete a single section."},heading:{type:"string",description:'The section label WITHOUT the trailing colon (e.g. "Workflow"). Matched case-insensitively.'},body:{type:"string",description:"New text BELOW the heading (for replace/insert). Omit for delete."},after:{type:"string",description:"For insert only: place the new section after the section with this heading. Omit/null to append at the end."}},required:["role_id","action","heading"]}},{name:"borg:delete-role",description:"Delete a role. Refuses if any drone is still assigned \u2014 reassign or evict those drones from the dashboard first.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to delete."}},required:["role_id"]}},{name:"borg:reassign-drone",description:"Reassign a drone to a different role in the same cube. Coordinator-shaped: the cube's Coordinator drone is the one expected to call this when dispatching new drones to specific work. Server refuses if you try to assign to the Coordinator role when another drone already holds it (evict or reassign that drone first).",inputSchema:{type:"object",properties:{drone_id:{type:"string",description:"UUID of the drone to reassign."},role_id:{type:"string",description:"UUID of the target role. Must belong to the same cube as the drone."}},required:["drone_id","role_id"]}},{name:"borg:list-drones",description:"List every drone in a cube (owner-scoped). Returns id, label, role_id, agent_kind, last_seen, and wake_path_alert_class for each \u2014 gives the Coordinator a roster they can act on with borg:reassign-drone.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube whose drones to list."}},required:["cube_id"]}},{name:"borg:list-roles",description:"List every role in a cube (owner-scoped). Returns id, name, short_description, is_default, is_human_seat, can_broadcast, receives_all_direct, and role_class for each \u2014 gives Coordinator-class drones the role UUIDs they need for borg:reassign-drone (e.g. to promote a drone to the Queen role). Closes the gh#153 Queen-role-promotion UX gap (Coordinator drones previously had no way to discover role IDs without operator help).",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube whose roles to list."}},required:["cube_id"]}},{name:"borg:list-templates",description:"List available cube templates that can be applied via borg:apply-template or passed to borg:create-cube.",inputSchema:{type:"object",properties:{}}},{name:"borg:apply-template",description:"Apply a named template to an existing cube, NON-CLOBBERINGLY. Roles are merged by name: new roles are created; existing template-named roles get template sections/classes the cube LACKS auto-applied, but EVOLVED (conflicting) text is preserved, never overwritten. Use this to retrofit an existing cube with a richer role set (e.g. add Coordinator/Reviewer/UX Expert). To review + selectively accept conflicting fragments, use borg:sync-roles (which surfaces each conflict + takes per-fragment accept decisions).",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to apply the template to."},template_name:{type:"string",description:"Template to apply (see borg:list-templates)."}},required:["cube_id","template_name"]}},{name:"borg:sync-roles",description:"Non-clobbering sync of an existing cube's roles + message_taxonomy against the current built-in template. The dry-run (default) classifies each FRAGMENT (role-text section, short_description, role flags, or taxonomy class) as ADD (the cube lacks it \u2014 safe auto-apply), UNCHANGED, or CONFLICT (the cube has EVOLVED text that differs from the template). On apply, ADDs auto-apply; CONFLICTs are applied ONLY when you explicitly accept them via `decisions` (keyed on the stable fragment key shown in the dry-run, e.g. `role:Builder:section:Workflow`). Unspecified conflicts default to KEEP (reject) \u2014 your cube's evolved coordination text is NEVER silently overwritten. Custom roles (names not in the template) are never touched.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to sync."},template_name:{type:"string",description:"Template to sync against (default: software-dev)."},apply:{type:"boolean",description:"If true, commit (auto-apply ADDs + accepted conflicts). If false (default), dry-run only \u2014 classify + surface conflicts."},decisions:{type:"object",description:'Per-conflict accept/reject map, keyed on the fragment key from the dry-run (e.g. {"role:Builder:section:Workflow":"accept"}). Unspecified conflicts default to "reject" (keep the cube version).',additionalProperties:{type:"string",enum:["accept","reject"]}}},required:["cube_id"]}}]})),p.setRequestHandler(Q,async h=>{const{name:g,arguments:r}=h.params;try{switch(g){case"borg:regen":{const e=await y();if(!e)return{content:[{type:"text",text:'Not connected to a cube. Use `borg:assimilate cube_name="<name>"` to join one.'}]};const t=typeof r?.since=="string"?r.since:void 0,o=r?.mode==="lite"?"lite":"full",n=await oe(e.sessionToken,e.apiUrl,{since:t}),i=ke(e,n);i!==e&&await P(i);const s=q(),a=f(i.cubeId,i.droneId),c=v()?!0:R(a),l=Pe(s,c)?Te({inboxPath:a,droneLabel:qe(n,i.droneLabel),cubeName:i.name}):"";let m="";try{const u=w(),d=Ee();if(u!=="unknown"&&d!=="unknown"&&d!==u){const[k,D,W]=u.split(".").map(Number),[$,E,H]=d.split(".").map(Number);($>k||$===k&&E>D||$===k&&E===D&&H>W)&&(m=`## \u{1F504} borgmcp ${d} installed \u2014 run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${u}.
|
|
3
|
+
|
|
4
|
+
`)}}catch{}return{content:[{type:"text",text:m+l+Ce(n,{mode:o})}]}}case"subscribe":return{content:[{type:"text",text:`Complete your subscription at: ${await ue()}`}]};case"subscription_status":{const e=await pe();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}case"open_dashboard":{const e="https://borgmcp.ai/dashboard";return await Ve(e),{content:[{type:"text",text:`\u25FC Opened dashboard in browser: ${e}`}]}}case"borg:assimilate":{const e=r?.cube_name;if(!e)throw new Error("cube_name is required");const t=ye,o=B(),n=L(),i=await X(e,t,o,n);await P({cubeId:i.cube.id,droneId:i.drone.id,name:i.cube.name,sessionToken:i.sessionToken,droneLabel:i.drone.label,apiUrl:t});const s=f(i.cube.id,i.drone.id),a=v()?["## Real-time wake-up","Codex remote-control wake is enabled for this MCP server. Incoming cube-log entries are delivered through the Codex app-server remote-control path; no Claude Monitor or inbox tail is required.",""]:["## Real-time wake-up (do this now)","If `/loop` is already running in this session without an inbox Monitor (typical when borg was launched before assimilating), arm one now so you wake the moment another drone posts to the cube:","",`> Monitor command: \`tail -n 0 -F ${s}\` \u2014 persistent, 1h timeout, description "borg inbox for ${i.drone.label} on cube ${i.cube.name}".`,"","Without this, you only wake on the 30-min fallback heartbeat and miss live coordination from other drones. Skip if `/loop` is not running in this session.",""];return{content:[{type:"text",text:[`# Assimilated to cube: ${i.cube.name}`,"",`**Drone label:** ${i.drone.label}`,`**Assigned role:** ${i.role.name}`,"",...a,"## Cube directive",i.cube.cube_directive||"_(none)_","",`## Your role: ${i.role.name}`,i.role.detailed_description||"_(no detailed description set)_","",N()].join(`
|
|
5
|
+
`)}]}}case"borg:version":return{content:[{type:"text",text:`borgmcp ${w()}`}]};case"borg:whoami":{const e=await b(),t=await be(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}case"borg:cube":{const e=await b(),[{cube:t,roles:o}]=await Promise.all([J(e.sessionToken,e.apiUrl),j(e.sessionToken,e.apiUrl)]),n=[];n.push(`# Cube: ${t.name}`),n.push(""),n.push("## Cube directive"),n.push(t.cube_directive||"_(none)_"),n.push("");const i=Ie(t.message_taxonomy);if(i&&(n.push(i),n.push("")),n.push("## Roles in this cube"),!o.length)n.push("_(no roles defined)_");else{for(const s of o){const a=[s.role_class==="queen"?"Queen":null,s.is_human_seat?"human-seat":null,s.is_default?"default":null].filter(Boolean).join(", "),c=a?` (${a})`:"",l=s.short_description||"_(no description)_";n.push(`- **${s.name}**${c} \u2014 ${l}`)}n.push(""),n.push("_(Coordinator-class drones can fetch role IDs via `borg:list-roles` for use with `borg:reassign-drone`.)_")}return n.push(""),n.push(N()),{content:[{type:"text",text:n.join(`
|
|
6
|
+
`)}]}}case"borg:role":{const e=await b(),{role:t}=await j(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:[`# Your role: ${t.name}`,"",t.detailed_description||"_(no detailed description set)_"].join(`
|
|
7
|
+
`)}]}}case"borg:role-rationale":{const e=await b(),t=typeof r?.role=="string"?r.role:"",o=typeof r?.section=="string"?r.section:"",n=await ge(e.sessionToken,e.apiUrl,t,o);return{content:[{type:"text",text:[`# Role rationale: ${n.role} \u2014 ${n.section}`,"",n.body||"_(empty)_"].join(`
|
|
8
|
+
`)}]}}case"borg:roster":{const e=await b(),t=typeof r?.since=="string"?r.since:void 0,{drones:o,roles:n,since:i}=await Z(e.sessionToken,e.apiUrl,t);return{content:[{type:"text",text:Ne({cubeName:e.name,drones:o,roles:n,resolvedSince:i??null,humanAgo:A})}]}}case"borg:stream-status":{const e=q(),t=await y(),o=t?f(t.cubeId,t.droneId):null,n=t?v()?!0:R(o):null;let i="";e.runLoopHealth==="silent-inert"&&(i=`## \u26A0 SSE stream loop silent-inert \u2014 run /mcp and reconnect to restart
|
|
9
|
+
|
|
10
|
+
The log-stream consumer started but never connected. This drone will not receive real-time cube events.
|
|
11
|
+
|
|
12
|
+
`);const s=Oe({status:e,inboxMonitorHealthy:n,inboxPath:o,droneLabel:t?.droneLabel??null,cubeName:t?.name??null,humanAgo:A});return{content:[{type:"text",text:i+s}]}}case"borg:read-log":{const e=await b(),t=typeof r?.since=="string"?r.since:void 0,o=typeof r?.limit=="number"?r.limit:void 0,n=r?.unread_only===!0||r?.unread_only==="true",{entries:i,drones:s,roles:a,behind_by:c}=await ee(e.sessionToken,e.apiUrl,{since:t,limit:o,unreadOnly:n}),l=new Map;for(const d of s)l.set(d.id,d);const m=new Map;for(const d of a)m.set(d.id,d);const u=[];if(u.push(`# Activity log: ${e.name}`),u.push(""),!i.length)u.push("_(no entries)_");else for(const d of i)u.push(Ue(d,l,m));return typeof c=="number"&&c>0&&(u.push(""),u.push(`\u26A0 behind_by: ${c} more unread ${c===1?"entry":"entries"} addressed to you \u2014 call \`borg:read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`)),{content:[{type:"text",text:u.join(`
|
|
13
|
+
`)}]}}case"borg:log":{const e=r?.message;if(!e||typeof e!="string")throw new Error("message is required");const t=await y();if(!t)throw new Error("Not assimilated to a cube. Use borg:assimilate <cube-name> first.");if(Fe(e)){const d=await We(t,e);if(d.suppress)return await M(t,e),{content:[{type:"text",text:`Suppressed duplicate ${d.signal?.toUpperCase()} lifecycle log for ${t.droneLabel}; recent cube log already contains this signal.`}]}}const o=Object.prototype.hasOwnProperty.call(r??{},"to"),n=o?He(r?.to):void 0,i=typeof r?.class=="string"?r.class:void 0,s=r?.visibility==="broadcast"||r?.visibility==="direct"?r.visibility:void 0,a={...i?{class:i}:{},...o?{to:n??[]}:{},...s?{visibility:s}:{}},c=await te(t.sessionToken,t.apiUrl,e,a);await M(t,e);const l=c.routing?.message?`
|
|
14
|
+
${c.routing.message}`:"",m=c.unreachableRecipients?.length?`
|
|
15
|
+
\u26A0 ${c.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${c.unreachableRecipients.map(d=>d.label).join(", ")}. Message delivered \u2014 they'll read it when they return.`:"";return{content:[{type:"text",text:`Logged to cube "${t.name}" as ${t.droneLabel}. (entry id: ${c.entry.id})${l}${m}`}]}}case"borg:ack":{const e=r?.entry_id;if(!e||typeof e!="string")throw new Error("entry_id is required");const t=await b();return await re(t.sessionToken,t.apiUrl,e),{content:[{type:"text",text:`Acked entry ${e} in cube "${t.name}".`}]}}case"borg:list-cubes":{const{cubes:e}=await ne();if(!e.length)return{content:[{type:"text",text:"No cubes yet. Use borg:create-cube to make your first one."}]};const t=e.map(o=>`- **${o.name}** (id: ${o.id})
|
|
16
|
+
${(o.cube_directive||"_(no directive set)_").split(`
|
|
17
|
+
`)[0].slice(0,120)}`);return{content:[{type:"text",text:`Your cubes (${e.length}):
|
|
18
|
+
|
|
19
|
+
${t.join(`
|
|
20
|
+
|
|
21
|
+
`)}`}]}}case"borg:create-cube":{const e=r?.name,t=r?.cube_directive,o=r?.template;if(!e)throw new Error("name is required");if(t===void 0)throw new Error("cube_directive is required (pass empty string if none)");let n=null;if(o&&(n=C(o),!n))throw new Error(`Unknown template "${o}". Available: ${I().join(", ")}`);const i=_e(t,n),s=xe(void 0,n),a=await ie(e,i,{message_taxonomy:s});if(n){const l=await F(a.id,n),m=i!==t?" Template cube directive applied (operator passed empty).":"";return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}) with template **${o}** applied \u2014 ${l.created} role(s) created, ${l.updated} updated.${m} Use borg:assimilate ${a.name} to join as a drone.`}]}}return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}). A default "Drone" role was seeded \u2014 rename or replace it via borg:update-role / borg:create-role / borg:delete-role. Use borg:assimilate ${a.name} to join as a drone.`}]}}case"borg:update-cube":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const t={};if(typeof r?.name=="string"&&(t.name=r.name),typeof r?.cube_directive=="string"&&(t.cube_directive=r.cube_directive),Array.isArray(r?.message_taxonomy)&&(t.message_taxonomy=r.message_taxonomy),Object.keys(t).length===0)throw new Error("Pass at least one of: name, cube_directive, message_taxonomy.");const{cube:o}=await O(e,t);return{content:[{type:"text",text:`Updated cube **${o.name}** (id: ${o.id}).`}]}}case"borg:patch-taxonomy-class":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const t=r?.action;if(t!=="add"&&t!=="replace"&&t!=="remove")throw new Error("action must be one of: add, replace, remove.");let o,n;if(t==="remove"){const s=r?.class;if(!s)throw new Error("class is required for remove.");({cube:o}=await T(e,{action:t,class:s})),n=s}else{const s=r?.class_def;if(s==null||typeof s!="object"||Array.isArray(s))throw new Error("class_def (object) is required for add/replace.");({cube:o}=await T(e,{action:t,class_def:s})),n=String(s.class??"")}return{content:[{type:"text",text:`${t==="add"?"Added":t==="replace"?"Replaced":"Removed"} taxonomy class **${n}** in cube **${o.name}** (id: ${o.id}).`}]}}case"borg:delete-cube":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");return await se(e),{content:[{type:"text",text:`Deleted cube ${e} (and all its roles, drones, log entries).`}]}}case"borg:create-role":{const e=r?.cube_id,t=r?.name,o=r?.short_description,n=r?.detailed_description;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("name is required");if(o===void 0)throw new Error("short_description is required (pass empty string if none)");if(n===void 0)throw new Error("detailed_description is required (pass empty string if none)");const i=r?.is_default===!0,s=r?.is_human_seat===!0,a=r?.can_broadcast===!0,c=r?.receives_all_direct===!0,{role:l}=await ae(e,{name:t,short_description:o,detailed_description:n,is_default:i,is_human_seat:s,can_broadcast:a,receives_all_direct:c}),m=[l.role_class==="queen"?"Queen":null,l.is_human_seat?"human-seat":null,l.is_default?"default":null].filter(Boolean).join(", "),u=m?` (${m})`:"";return{content:[{type:"text",text:`Created role **${l.name}**${u} (id: ${l.id}) in cube ${e}.`}]}}case"borg:update-role":{const e=r?.role_id;if(!e)throw new Error("role_id is required");const t={};if(typeof r?.name=="string"&&(t.name=r.name),typeof r?.short_description=="string"&&(t.short_description=r.short_description),typeof r?.detailed_description=="string"&&(t.detailed_description=r.detailed_description),typeof r?.is_default=="boolean"&&(t.is_default=r.is_default),typeof r?.is_human_seat=="boolean"&&(t.is_human_seat=r.is_human_seat),typeof r?.can_broadcast=="boolean"&&(t.can_broadcast=r.can_broadcast),typeof r?.receives_all_direct=="boolean"&&(t.receives_all_direct=r.receives_all_direct),Object.keys(t).length===0)throw new Error("Pass at least one of: name, short_description, detailed_description, is_default, is_human_seat, can_broadcast, receives_all_direct.");const{role:o}=await ce(e,t),n=[o.role_class==="queen"?"Queen":null,o.is_human_seat?"human-seat":null,o.is_default?"default":null].filter(Boolean).join(", "),i=n?` (${n})`:"";return{content:[{type:"text",text:`Updated role **${o.name}**${i} (id: ${o.id}).`}]}}case"borg:patch-role-section":{const e=r?.role_id;if(!e)throw new Error("role_id is required");const t=r?.action;if(t!=="replace"&&t!=="insert"&&t!=="delete")throw new Error("action must be one of: replace, insert, delete.");const o=r?.heading;if(!o)throw new Error("heading is required");let n;if(t==="delete")({role:n}=await S(e,{action:t,heading:o}));else{const s=r?.body;if(typeof s!="string")throw new Error("body is required for replace/insert (pass empty string for an empty section).");if(t==="insert"){const a=typeof r?.after=="string"?r.after:null;({role:n}=await S(e,{action:t,heading:o,body:s,after:a}))}else({role:n}=await S(e,{action:t,heading:o,body:s}))}return{content:[{type:"text",text:`${t==="replace"?"Replaced":t==="insert"?"Inserted":"Deleted"} section **${o}** in role **${n.name}** (id: ${n.id}).`}]}}case"borg:delete-role":{const e=r?.role_id;if(!e)throw new Error("role_id is required");return await le(e),{content:[{type:"text",text:`Deleted role ${e}.`}]}}case"borg:reassign-drone":{const e=r?.drone_id,t=r?.role_id;if(!e)throw new Error("drone_id is required");if(!t)throw new Error("role_id is required");const{drone:o}=await de(e,t);return{content:[{type:"text",text:`Reassigned drone ${o.label} (${o.id}) to role ${o.role_id}.`}]}}case"borg:list-drones":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{drones:t,roles:o}=await U(e);if(!t.length)return{content:[{type:"text",text:"No drones in this cube yet."}]};const n=new Map(o.map(s=>[s.id,s])),i=t.map(s=>{const a=n.get(s.role_id),c=Ae(a?.name??"?",s.agent_kind),l=s.wake_path_alert_class&&s.wake_path_alert_class!=="independent"?` \u2014 wake-path-class: ${s.wake_path_alert_class}`:"";return`- **${s.label}** (id: ${s.id}) \u2014 role: ${c} (${s.role_id}) \u2014 last seen ${s.last_seen}${l}`});return{content:[{type:"text",text:`Drones in cube ${e} (${t.length}):
|
|
22
|
+
|
|
23
|
+
${i.join(`
|
|
24
|
+
`)}`}]}}case"borg:list-roles":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{roles:t}=await U(e);return{content:[{type:"text",text:De(t,e)}]}}case"borg:list-templates":return{content:[{type:"text",text:`Available templates:
|
|
25
|
+
|
|
26
|
+
${I().map(o=>{const n=C(o);return`- **${o}**: ${n.description}`}).join(`
|
|
27
|
+
`)}`}]};case"borg:sync-roles":{const e=r?.cube_id,t=r?.template_name||"software-dev",o=r?.apply===!0,n=r?.decisions&&typeof r.decisions=="object"?r.decisions:void 0;if(!e)throw new Error("cube_id is required");const i=await me(e,t,o,n);return{content:[{type:"text",text:Le(i,t)}]}}case"borg:apply-template":{const e=r?.cube_id,t=r?.template_name;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("template_name is required");const o=C(t);if(!o)throw new Error(`Unknown template "${t}". Available: ${I().join(", ")}`);const n=await F(e,o);let i="";const s=await U(e),a=ve(s.cube_directive,o);return a!==null&&(await O(e,{cube_directive:a}),i=" Template cube directive applied (cube directive was empty)."),{content:[{type:"text",text:`Applied template **${t}** to cube ${e} \u2014 ${n.created} role(s) created, ${n.updated} updated.${i}`}]}}default:throw new Error(`Unknown tool: ${g}`)}}catch(e){return e.message?.includes("Authentication required")||e.message?.includes("Authentication expired")||e.message?.includes("Failed to refresh")?{content:[{type:"text",text:"\u25FC Authentication expired. Run: borg assimilate"}],isError:!0}:{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}),p.setRequestHandler(G,async()=>({prompts:[{name:"subscribe",description:"Set up Borg MCP Cube tier subscription ($1/cube/month \u2014 unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial."},{name:"dashboard",description:"Open Borg MCP dashboard to manage cubes"}]})),p.setRequestHandler(z,async h=>{const{name:g}=h.params;switch(g){case"subscribe":return{description:"Set up Borg MCP Cube tier subscription ($1/cube/month \u2014 unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial.",messages:[{role:"user",content:{type:"text",text:"Please help me set up a Borg MCP subscription using the subscribe tool."}}]};case"dashboard":return{description:"Open Borg MCP dashboard to manage cubes",messages:[{role:"user",content:{type:"text",text:"Please open the Borg MCP dashboard using the open_dashboard tool."}}]};default:throw new Error(`Unknown prompt: ${g}`)}});const x=new K;await p.connect(x),await Me(),console.error(`${_()}\u25FC Borg MCP Client started`),console.error(`${_()}\u25FC Use borg:assimilate <cube-name> to join a cube as a drone`),console.error(`${_()}\u25FC Manage your cubes at https://borgmcp.ai/dashboard`)}Qe().catch(p=>{console.error(`${_()}Fatal error:`,p),process.exit(1)});
|