borgmcp 0.2.0-beta.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +2 -2
  2. package/dist/auth.js +5 -5
  3. package/dist/auth.js.map +1 -1
  4. package/dist/claude.d.ts +9 -4
  5. package/dist/claude.d.ts.map +1 -1
  6. package/dist/claude.js +62 -26
  7. package/dist/claude.js.map +1 -1
  8. package/dist/config-utils.d.ts +32 -0
  9. package/dist/config-utils.d.ts.map +1 -1
  10. package/dist/config-utils.js +159 -0
  11. package/dist/config-utils.js.map +1 -1
  12. package/dist/cubes.d.ts +51 -0
  13. package/dist/cubes.d.ts.map +1 -0
  14. package/dist/cubes.js +161 -0
  15. package/dist/cubes.js.map +1 -0
  16. package/dist/inbox.d.ts +33 -0
  17. package/dist/inbox.d.ts.map +1 -0
  18. package/dist/inbox.js +125 -0
  19. package/dist/inbox.js.map +1 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +701 -25
  23. package/dist/index.js.map +1 -1
  24. package/dist/log-audit.d.ts +27 -0
  25. package/dist/log-audit.d.ts.map +1 -0
  26. package/dist/log-audit.js +161 -0
  27. package/dist/log-audit.js.map +1 -0
  28. package/dist/postinstall.d.ts +1 -1
  29. package/dist/postinstall.d.ts.map +1 -1
  30. package/dist/postinstall.js +4 -4
  31. package/dist/postinstall.js.map +1 -1
  32. package/dist/regen-format.d.ts +59 -0
  33. package/dist/regen-format.d.ts.map +1 -0
  34. package/dist/regen-format.js +172 -0
  35. package/dist/regen-format.js.map +1 -0
  36. package/dist/regen.d.ts +19 -0
  37. package/dist/regen.d.ts.map +1 -0
  38. package/dist/regen.js +36 -0
  39. package/dist/regen.js.map +1 -0
  40. package/dist/remote-client.d.ts +186 -2
  41. package/dist/remote-client.d.ts.map +1 -1
  42. package/dist/remote-client.js +232 -11
  43. package/dist/remote-client.js.map +1 -1
  44. package/dist/setup.js +40 -33
  45. package/dist/setup.js.map +1 -1
  46. package/dist/sync.js +1 -1
  47. package/dist/sync.js.map +1 -1
  48. package/dist/templates.d.ts +33 -0
  49. package/dist/templates.d.ts.map +1 -0
  50. package/dist/templates.js +96 -0
  51. package/dist/templates.js.map +1 -0
  52. package/package.json +11 -10
package/dist/index.js CHANGED
@@ -6,16 +6,101 @@
6
6
  * 1. Connects to Claude Code via stdio transport
7
7
  * 2. Authenticates via Google OAuth device flow
8
8
  * 3. Proxies MCP tools to remote server at api.borgmcp.ai
9
- * 4. Provides borg:regen tool for centralized context retrieval
9
+ * 4. Provides the borg: cube tool surface (assimilate / cube / role /
10
+ * roster / read-log) so Claude can act as a Drone in a hive of
11
+ * collaborating sessions.
10
12
  */
11
13
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
14
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
14
- import { regenerateContext, checkSubscriptionStatus, createSubscription, } from './remote-client.js';
15
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
16
+ import { assimilate, getCubeInfo, getRoleInfo, getRoster, readLog, appendLog, regen, listCubes, createCube, updateCube, deleteCube, createRole, updateRole, deleteRole, reassignDrone, getCube, checkSubscriptionStatus, createSubscription, API_URL, } from './remote-client.js';
17
+ import { getTemplate, listTemplateNames } from './templates.js';
18
+ import { getActiveCube, setActiveCube } from './cubes.js';
19
+ import { addSessionStartHook, addUserPromptSubmitHook } from './config-utils.js';
20
+ import { humanAgo, formatRegenMarkdown, getDronePlaybook, getDroneMode } from './regen-format.js';
21
+ import { startInboxPoller } from './inbox.js';
22
+ import open from 'open';
23
+ /**
24
+ * Apply a template's roles to a cube. Roles are merged by name:
25
+ * - existing role with same name → updateRole with the template's fields
26
+ * - no existing role with that name → createRole
27
+ * Returns counts for the caller to report.
28
+ */
29
+ async function applyTemplateToCube(cubeId, templateRoles) {
30
+ const { roles: existing } = await getCube(cubeId);
31
+ const byName = new Map(existing.map((r) => [r.name, r]));
32
+ let created = 0;
33
+ let updated = 0;
34
+ for (const tr of templateRoles) {
35
+ const match = byName.get(tr.name);
36
+ if (match) {
37
+ await updateRole(match.id, {
38
+ short_description: tr.short_description,
39
+ detailed_description: tr.detailed_description,
40
+ is_default: tr.is_default === true,
41
+ is_coordinator: tr.is_coordinator === true,
42
+ });
43
+ updated += 1;
44
+ }
45
+ else {
46
+ await createRole(cubeId, {
47
+ name: tr.name,
48
+ short_description: tr.short_description,
49
+ detailed_description: tr.detailed_description,
50
+ is_default: tr.is_default === true,
51
+ is_coordinator: tr.is_coordinator === true,
52
+ });
53
+ created += 1;
54
+ }
55
+ }
56
+ return { created, updated };
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
+ }
15
68
  /**
16
69
  * Main entry point - MCP stdio server
17
70
  */
18
71
  async function main() {
72
+ // Auto-register the SessionStart hook so existing users get borg-regen
73
+ // auto-orientation on session start without re-running borg setup. Idempotent.
74
+ try {
75
+ addSessionStartHook();
76
+ }
77
+ catch (err) {
78
+ // Silent on failure — never break the MCP server because of hook registration.
79
+ }
80
+ // Auto-register the UserPromptSubmit audit hook so the drone gets a
81
+ // nudge if the previous assistant span used state-changing tools
82
+ // without calling borg:log. Domain-agnostic — knows nothing about git
83
+ // or any specific convention. Idempotent.
84
+ try {
85
+ addUserPromptSubmitHook();
86
+ }
87
+ catch (err) {
88
+ // Silent on failure — same rationale as above.
89
+ }
90
+ // Spawn the long-poll background poller. This gives drones real-time
91
+ // wakeup: when another drone posts to the cube, a line gets appended
92
+ // to the per-cube inbox file (see inboxPathForCube in cubes.ts) and
93
+ // the launcher's Monitor wakes the active /loop iteration immediately.
94
+ // No truncation needed — the launcher's Monitor uses `tail -n 0 -F`,
95
+ // which starts at end-of-file and only emits new appends. Failure
96
+ // here is non-fatal — the launcher's fallback heartbeat still keeps
97
+ // things moving.
98
+ try {
99
+ startInboxPoller();
100
+ }
101
+ catch {
102
+ // Silent — never break the MCP server because of inbox setup.
103
+ }
19
104
  // Create MCP server
20
105
  const server = new Server({
21
106
  name: 'borg-mcp-client',
@@ -23,6 +108,7 @@ async function main() {
23
108
  }, {
24
109
  capabilities: {
25
110
  tools: {},
111
+ prompts: {},
26
112
  },
27
113
  });
28
114
  // Register tool listing
@@ -31,7 +117,25 @@ async function main() {
31
117
  tools: [
32
118
  {
33
119
  name: 'subscribe',
34
- description: 'Create Stripe checkout session ($2/month, 7-day trial)',
120
+ description: 'Create Stripe checkout session ($1/month, 7-day trial)',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {},
124
+ required: [],
125
+ },
126
+ },
127
+ {
128
+ name: 'subscription_status',
129
+ description: 'Check subscription status',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {},
133
+ required: [],
134
+ },
135
+ },
136
+ {
137
+ name: 'open_dashboard',
138
+ description: 'Open Borg MCP dashboard in browser to manage cubes, roles, and drones',
35
139
  inputSchema: {
36
140
  type: 'object',
37
141
  properties: {},
@@ -40,30 +144,232 @@ async function main() {
40
144
  },
41
145
  {
42
146
  name: 'borg:regen',
43
- description: 'Regenerate centralized context from your active context entries (requires subscription). Returns markdown-formatted hive mind context.',
147
+ description: "Refresh your context as a Drone. Returns the active cube's ground rules, " +
148
+ "your role's detailed playbook, the drone roster, and recent activity log entries — " +
149
+ 'everything you need to be oriented. Call on session start, and again before each new ' +
150
+ 'task to stay in sync with the cube. Returns "not connected" if no active cube; use ' +
151
+ 'borg:assimilate first in that case.',
152
+ inputSchema: {
153
+ type: 'object',
154
+ properties: {},
155
+ required: [],
156
+ },
157
+ },
158
+ {
159
+ name: 'borg:assimilate',
160
+ description: "Connect this Claude session as a Drone to a Cube. Provide the cube's name. " +
161
+ "Returns the cube's ground rules, your assigned role's detailed instructions, " +
162
+ 'and persists a session token locally so subsequent borg: tools work for this cube.',
44
163
  inputSchema: {
45
164
  type: 'object',
46
165
  properties: {
47
- categories: {
48
- type: 'array',
49
- items: {
50
- type: 'string',
51
- },
52
- description: 'Optional: Filter by specific categories (e.g., ["code-style", "commit-rules"])',
166
+ cube_name: {
167
+ type: 'string',
168
+ description: 'The cube to connect to',
53
169
  },
54
170
  },
171
+ required: ['cube_name'],
172
+ },
173
+ },
174
+ {
175
+ name: 'borg:cube',
176
+ description: "Read the active Cube's ground rules and the registry of all roles in it " +
177
+ "(each role's name + short description). Use to remind yourself of cube-wide context.",
178
+ inputSchema: {
179
+ type: 'object',
180
+ properties: {},
55
181
  required: [],
56
182
  },
57
183
  },
58
184
  {
59
- name: 'subscription_status',
60
- description: 'Check subscription status',
185
+ name: 'borg:role',
186
+ description: "Read your assigned role's detailed description (your playbook). " +
187
+ 'Other drones cannot see this — only you (drones in this role).',
188
+ inputSchema: {
189
+ type: 'object',
190
+ properties: {},
191
+ required: [],
192
+ },
193
+ },
194
+ {
195
+ name: 'borg:roster',
196
+ description: "List all currently connected drones in your cube, with each drone's label, role, and last-seen time.",
61
197
  inputSchema: {
62
198
  type: 'object',
63
199
  properties: {},
64
200
  required: [],
65
201
  },
66
202
  },
203
+ {
204
+ name: 'borg:read-log',
205
+ description: "Read recent entries from the cube's shared activity log. Each entry is tagged " +
206
+ "with the drone that wrote it and that drone's role. Optional: since (ISO-8601 " +
207
+ 'timestamp, returns entries strictly after) and limit (1–500, default 50).',
208
+ inputSchema: {
209
+ type: 'object',
210
+ properties: {
211
+ since: {
212
+ type: 'string',
213
+ description: 'ISO-8601 timestamp',
214
+ },
215
+ limit: {
216
+ type: 'number',
217
+ description: 'max entries to return (1-500)',
218
+ },
219
+ },
220
+ },
221
+ },
222
+ {
223
+ name: 'borg:log',
224
+ description: 'Append a message to the cube\'s shared activity log. All connected drones will see your entry, tagged with your drone label and role name. Use this to coordinate work, post review-ready signals, share findings, or anything else the cube\'s ground rules and your role description direct you to communicate.',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ message: { type: 'string', description: 'The log message (max 10KB).' },
229
+ },
230
+ required: ['message'],
231
+ },
232
+ },
233
+ {
234
+ name: 'borg:list-cubes',
235
+ description: 'List every cube owned by this user. Returns id, name, ground_rules, and timestamps for each. Useful before assimilate to see what\'s available, or as a starting point for any management action.',
236
+ inputSchema: { type: 'object', properties: {} },
237
+ },
238
+ {
239
+ name: 'borg:create-cube',
240
+ description: 'Create a new cube. The server seeds a default "Drone" role atomically so the cube is assimilatable immediately. ' +
241
+ 'Pass an optional `template` name to apply a richer role set instead (see borg:list-templates / borg:apply-template).',
242
+ inputSchema: {
243
+ type: 'object',
244
+ properties: {
245
+ name: {
246
+ type: 'string',
247
+ description: 'Cube name (lowercase letters, digits, hyphens; max 64 chars).',
248
+ pattern: '^[a-z0-9-]+$',
249
+ maxLength: 64,
250
+ },
251
+ ground_rules: { type: 'string', description: 'Markdown text every drone in this cube will see in regen. Anything project-specific.' },
252
+ template: {
253
+ type: 'string',
254
+ 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.',
255
+ },
256
+ },
257
+ required: ['name', 'ground_rules'],
258
+ },
259
+ },
260
+ {
261
+ name: 'borg:update-cube',
262
+ description: 'Update a cube\'s name and/or ground_rules. Pass only what changes.',
263
+ inputSchema: {
264
+ type: 'object',
265
+ properties: {
266
+ cube_id: { type: 'string', description: 'UUID of the cube to update.' },
267
+ name: {
268
+ type: 'string',
269
+ description: 'New name (optional). Lowercase letters, digits, hyphens; max 64 chars.',
270
+ pattern: '^[a-z0-9-]+$',
271
+ maxLength: 64,
272
+ },
273
+ ground_rules: { type: 'string', description: 'New ground rules markdown (optional).' },
274
+ },
275
+ required: ['cube_id'],
276
+ },
277
+ },
278
+ {
279
+ name: 'borg:delete-cube',
280
+ 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.',
281
+ inputSchema: {
282
+ type: 'object',
283
+ properties: {
284
+ cube_id: { type: 'string', description: 'UUID of the cube to delete.' },
285
+ },
286
+ required: ['cube_id'],
287
+ },
288
+ },
289
+ {
290
+ name: 'borg:create-role',
291
+ 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.',
292
+ inputSchema: {
293
+ type: 'object',
294
+ properties: {
295
+ cube_id: { type: 'string', description: 'UUID of the cube this role belongs to.' },
296
+ name: { type: 'string', description: 'Role name (e.g. "Builder", "Reviewer").' },
297
+ short_description: { type: 'string', description: 'One-line summary, shown to every drone in the cube.' },
298
+ detailed_description: { type: 'string', description: 'Full playbook for drones in this role — workflow, conventions, log signals to post.' },
299
+ is_default: { type: 'boolean', description: 'If true, new drones assimilating into this cube are assigned this role (when the Coordinator seat is taken or there is no Coordinator role). Demotes the previous default.' },
300
+ is_coordinator: { type: 'boolean', description: 'If true, this role becomes the cube\'s Coordinator (Queen seat, supervised mode). At most one Coordinator role per cube — promoting demotes the previous one.' },
301
+ },
302
+ required: ['cube_id', 'name', 'short_description', 'detailed_description'],
303
+ },
304
+ },
305
+ {
306
+ name: 'borg:update-role',
307
+ description: 'Update a role. Pass only the fields that change. Promoting to is_default demotes the previous default in the same cube.',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {
311
+ role_id: { type: 'string', description: 'UUID of the role to update.' },
312
+ name: { type: 'string', description: 'New role name (optional).' },
313
+ short_description: { type: 'string', description: 'New short description (optional).' },
314
+ detailed_description: { type: 'string', description: 'New detailed playbook (optional).' },
315
+ is_default: { type: 'boolean', description: 'Set true to make this the cube\'s default role (optional).' },
316
+ is_coordinator: { type: 'boolean', description: 'Set true to promote this role to Coordinator (singleton per cube; demotes the previous one). Setting false on the current Coordinator role demotes it.' },
317
+ },
318
+ required: ['role_id'],
319
+ },
320
+ },
321
+ {
322
+ name: 'borg:delete-role',
323
+ description: 'Delete a role. Refuses if any drone is still assigned — reassign or evict those drones from the dashboard first.',
324
+ inputSchema: {
325
+ type: 'object',
326
+ properties: {
327
+ role_id: { type: 'string', description: 'UUID of the role to delete.' },
328
+ },
329
+ required: ['role_id'],
330
+ },
331
+ },
332
+ {
333
+ name: 'borg:reassign-drone',
334
+ 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. ' +
335
+ 'Server refuses if you try to assign to the Coordinator role when another drone already holds it (evict or reassign that drone first).',
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ drone_id: { type: 'string', description: 'UUID of the drone to reassign.' },
340
+ role_id: { type: 'string', description: 'UUID of the target role. Must belong to the same cube as the drone.' },
341
+ },
342
+ required: ['drone_id', 'role_id'],
343
+ },
344
+ },
345
+ {
346
+ name: 'borg:list-drones',
347
+ description: 'List every drone in a cube (owner-scoped). Returns id, label, role_id, last_seen for each — gives the Coordinator a roster they can act on with borg:reassign-drone.',
348
+ inputSchema: {
349
+ type: 'object',
350
+ properties: {
351
+ cube_id: { type: 'string', description: 'UUID of the cube whose drones to list.' },
352
+ },
353
+ required: ['cube_id'],
354
+ },
355
+ },
356
+ {
357
+ name: 'borg:list-templates',
358
+ description: 'List available cube templates that can be applied via borg:apply-template or passed to borg:create-cube.',
359
+ inputSchema: { type: 'object', properties: {} },
360
+ },
361
+ {
362
+ name: 'borg:apply-template',
363
+ description: 'Apply a named template to an existing cube. Roles are merged by name: existing roles with the same name get their fields overwritten; new ones are created. Use this to retrofit an existing cube with a richer role set (e.g. add Coordinator/Reviewer/UX Expert to a cube that started bare).',
364
+ inputSchema: {
365
+ type: 'object',
366
+ properties: {
367
+ cube_id: { type: 'string', description: 'UUID of the cube to apply the template to.' },
368
+ template_name: { type: 'string', description: 'Template to apply (see borg:list-templates).' },
369
+ },
370
+ required: ['cube_id', 'template_name'],
371
+ },
372
+ },
67
373
  ],
68
374
  };
69
375
  });
@@ -72,6 +378,22 @@ async function main() {
72
378
  const { name, arguments: args } = request.params;
73
379
  try {
74
380
  switch (name) {
381
+ case 'borg:regen': {
382
+ const active = await getActiveCube();
383
+ if (!active) {
384
+ return {
385
+ content: [
386
+ {
387
+ type: 'text',
388
+ text: 'Not connected to a cube. Use `borg:assimilate cube_name="<name>"` to join one.',
389
+ },
390
+ ],
391
+ };
392
+ }
393
+ const result = await regen(active.sessionToken, active.apiUrl);
394
+ const mode = getDroneMode(result.role?.is_coordinator === true);
395
+ return { content: [{ type: 'text', text: formatRegenMarkdown(result, mode) }] };
396
+ }
75
397
  case 'subscribe': {
76
398
  const checkoutUrl = await createSubscription();
77
399
  return {
@@ -83,29 +405,334 @@ async function main() {
83
405
  ],
84
406
  };
85
407
  }
86
- case 'borg:regen': {
87
- const categories = Array.isArray(args?.categories) ? args.categories : undefined;
88
- const context = await regenerateContext(categories);
408
+ case 'subscription_status': {
409
+ const status = await checkSubscriptionStatus();
89
410
  return {
90
411
  content: [
91
412
  {
92
413
  type: 'text',
93
- text: context,
414
+ text: JSON.stringify(status, null, 2),
94
415
  },
95
416
  ],
96
417
  };
97
418
  }
98
- case 'subscription_status': {
99
- const status = await checkSubscriptionStatus();
419
+ case 'open_dashboard': {
420
+ const dashboardUrl = 'https://borgmcp.ai/dashboard';
421
+ await open(dashboardUrl);
100
422
  return {
101
423
  content: [
102
424
  {
103
425
  type: 'text',
104
- text: JSON.stringify(status, null, 2),
426
+ text: `◼ Opened dashboard in browser: ${dashboardUrl}`,
105
427
  },
106
428
  ],
107
429
  };
108
430
  }
431
+ case 'borg:assimilate': {
432
+ const cubeName = args?.cube_name;
433
+ if (!cubeName)
434
+ throw new Error('cube_name is required');
435
+ // First-call assimilate uses the env-or-prod default; we then
436
+ // persist that same URL so all subsequent drone calls hit the
437
+ // worker that issued the session token.
438
+ const apiUrl = API_URL;
439
+ const result = await assimilate(cubeName, apiUrl);
440
+ await setActiveCube({
441
+ cubeId: result.cube.id,
442
+ droneId: result.drone.id,
443
+ name: result.cube.name,
444
+ sessionToken: result.sessionToken,
445
+ droneLabel: result.drone.label,
446
+ apiUrl,
447
+ });
448
+ const text = [
449
+ `# Assimilated to cube: ${result.cube.name}`,
450
+ ``,
451
+ `**Drone label:** ${result.drone.label}`,
452
+ `**Assigned role:** ${result.role.name}`,
453
+ `**Mode:** ${getDroneMode(result.role.is_coordinator === true)}`,
454
+ ``,
455
+ `## Ground rules`,
456
+ result.cube.ground_rules || '_(none)_',
457
+ ``,
458
+ `## Your role: ${result.role.name}`,
459
+ result.role.detailed_description || '_(no detailed description set)_',
460
+ ``,
461
+ getDronePlaybook(getDroneMode(result.role.is_coordinator === true)),
462
+ ].join('\n');
463
+ return { content: [{ type: 'text', text }] };
464
+ }
465
+ case 'borg:cube': {
466
+ const active = await requireActiveCube();
467
+ const [{ cube, roles }, { role: ownRole }] = await Promise.all([
468
+ getCubeInfo(active.sessionToken, active.apiUrl),
469
+ getRoleInfo(active.sessionToken, active.apiUrl),
470
+ ]);
471
+ // Mode is derived from this drone's own current role —
472
+ // Coordinator → supervised, anything else → autonomous.
473
+ const mode = getDroneMode(ownRole?.is_coordinator === true);
474
+ const lines = [];
475
+ lines.push(`# Cube: ${cube.name}`);
476
+ lines.push(`**Mode:** ${mode}`);
477
+ lines.push('');
478
+ lines.push('## Ground rules');
479
+ lines.push(cube.ground_rules || '_(none)_');
480
+ lines.push('');
481
+ lines.push('## Roles in this cube');
482
+ if (!roles.length) {
483
+ lines.push('_(no roles defined)_');
484
+ }
485
+ else {
486
+ for (const r of roles) {
487
+ const tags = [
488
+ r.is_coordinator ? 'Coordinator' : null,
489
+ r.is_default ? 'default' : null,
490
+ ].filter(Boolean).join(', ');
491
+ const marker = tags ? ` (${tags})` : '';
492
+ const desc = r.short_description || '_(no description)_';
493
+ lines.push(`- **${r.name}**${marker} — ${desc}`);
494
+ }
495
+ }
496
+ lines.push('');
497
+ lines.push(getDronePlaybook(mode));
498
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
499
+ }
500
+ case 'borg:role': {
501
+ const active = await requireActiveCube();
502
+ const { role } = await getRoleInfo(active.sessionToken, active.apiUrl);
503
+ const text = [
504
+ `# Your role: ${role.name}`,
505
+ ``,
506
+ role.detailed_description || '_(no detailed description set)_',
507
+ ].join('\n');
508
+ return { content: [{ type: 'text', text }] };
509
+ }
510
+ case 'borg:roster': {
511
+ const active = await requireActiveCube();
512
+ const { drones, roles } = await getRoster(active.sessionToken, active.apiUrl);
513
+ const roleById = new Map();
514
+ for (const r of roles)
515
+ roleById.set(r.id, r);
516
+ const lines = [];
517
+ lines.push(`# Drones in cube: ${active.name}`);
518
+ lines.push('');
519
+ if (!drones.length) {
520
+ lines.push('_(no drones connected)_');
521
+ }
522
+ else {
523
+ for (const d of drones) {
524
+ const role = roleById.get(d.role_id);
525
+ const roleName = role?.name ?? 'unknown';
526
+ lines.push(`- **${d.label}** (${roleName}) — last seen ${humanAgo(d.last_seen)}`);
527
+ }
528
+ }
529
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
530
+ }
531
+ case 'borg:read-log': {
532
+ const active = await requireActiveCube();
533
+ const since = typeof args?.since === 'string' ? args.since : undefined;
534
+ const limit = typeof args?.limit === 'number' ? args.limit : undefined;
535
+ const { entries, drones, roles } = await readLog(active.sessionToken, active.apiUrl, {
536
+ since,
537
+ limit,
538
+ });
539
+ const droneById = new Map();
540
+ for (const d of drones)
541
+ droneById.set(d.id, d);
542
+ const roleById = new Map();
543
+ for (const r of roles)
544
+ roleById.set(r.id, r);
545
+ const lines = [];
546
+ lines.push(`# Activity log: ${active.name}`);
547
+ lines.push('');
548
+ if (!entries.length) {
549
+ lines.push('_(no entries)_');
550
+ }
551
+ else {
552
+ for (const e of entries) {
553
+ const drone = droneById.get(e.drone_id);
554
+ const droneLabel = drone?.label ?? 'unknown-drone';
555
+ const roleName = drone ? roleById.get(drone.role_id)?.name ?? 'unknown' : 'unknown';
556
+ const ts = typeof e.created_at === 'string'
557
+ ? e.created_at
558
+ : new Date(e.created_at).toISOString();
559
+ lines.push(`**[${ts}]** ${droneLabel} (${roleName}): ${e.message}`);
560
+ }
561
+ }
562
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
563
+ }
564
+ case 'borg:log': {
565
+ const message = args?.message;
566
+ if (!message || typeof message !== 'string')
567
+ throw new Error('message is required');
568
+ const active = await getActiveCube();
569
+ if (!active)
570
+ throw new Error('Not assimilated to a cube. Use borg:assimilate <cube-name> first.');
571
+ const result = await appendLog(active.sessionToken, active.apiUrl, message);
572
+ const text = `Logged to cube "${active.name}" as ${active.droneLabel}. (entry id: ${result.entry.id})`;
573
+ return { content: [{ type: 'text', text }] };
574
+ }
575
+ case 'borg:list-cubes': {
576
+ const { cubes } = await listCubes();
577
+ if (!cubes.length) {
578
+ return { content: [{ type: 'text', text: 'No cubes yet. Use borg:create-cube to make your first one.' }] };
579
+ }
580
+ const lines = cubes.map((c) => `- **${c.name}** (id: ${c.id})\n ${(c.ground_rules || '_(no ground rules)_').split('\n')[0].slice(0, 120)}`);
581
+ return { content: [{ type: 'text', text: `Your cubes (${cubes.length}):\n\n${lines.join('\n\n')}` }] };
582
+ }
583
+ case 'borg:create-cube': {
584
+ const name = args?.name;
585
+ const groundRules = args?.ground_rules;
586
+ const templateName = args?.template;
587
+ if (!name)
588
+ throw new Error('name is required');
589
+ if (groundRules === undefined)
590
+ throw new Error('ground_rules is required (pass empty string if none)');
591
+ const { cube } = await createCube(name, groundRules);
592
+ // Apply template if requested. Merges by name: any role the
593
+ // server auto-seeded (e.g. "Drone") that the template doesn't
594
+ // also include stays put; templated roles upsert.
595
+ if (templateName) {
596
+ const template = getTemplate(templateName);
597
+ if (!template) {
598
+ throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
599
+ }
600
+ const summary = await applyTemplateToCube(cube.id, template.roles);
601
+ const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied — ${summary.created} role(s) created, ${summary.updated} updated. Use borg:assimilate ${cube.name} to join as a drone.`;
602
+ return { content: [{ type: 'text', text }] };
603
+ }
604
+ 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.`;
605
+ return { content: [{ type: 'text', text }] };
606
+ }
607
+ case 'borg:update-cube': {
608
+ const cubeId = args?.cube_id;
609
+ if (!cubeId)
610
+ throw new Error('cube_id is required');
611
+ const updates = {};
612
+ if (typeof args?.name === 'string')
613
+ updates.name = args.name;
614
+ if (typeof args?.ground_rules === 'string')
615
+ updates.ground_rules = args.ground_rules;
616
+ if (Object.keys(updates).length === 0)
617
+ throw new Error('Pass at least one of: name, ground_rules.');
618
+ const { cube } = await updateCube(cubeId, updates);
619
+ return { content: [{ type: 'text', text: `Updated cube **${cube.name}** (id: ${cube.id}).` }] };
620
+ }
621
+ case 'borg:delete-cube': {
622
+ const cubeId = args?.cube_id;
623
+ if (!cubeId)
624
+ throw new Error('cube_id is required');
625
+ await deleteCube(cubeId);
626
+ return { content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }] };
627
+ }
628
+ case 'borg:create-role': {
629
+ const cubeId = args?.cube_id;
630
+ const name = args?.name;
631
+ const shortDesc = args?.short_description;
632
+ const detailedDesc = args?.detailed_description;
633
+ if (!cubeId)
634
+ throw new Error('cube_id is required');
635
+ if (!name)
636
+ throw new Error('name is required');
637
+ if (shortDesc === undefined)
638
+ throw new Error('short_description is required (pass empty string if none)');
639
+ if (detailedDesc === undefined)
640
+ throw new Error('detailed_description is required (pass empty string if none)');
641
+ const isDefault = args?.is_default === true;
642
+ const isCoordinator = args?.is_coordinator === true;
643
+ const { role } = await createRole(cubeId, {
644
+ name,
645
+ short_description: shortDesc,
646
+ detailed_description: detailedDesc,
647
+ is_default: isDefault,
648
+ is_coordinator: isCoordinator,
649
+ });
650
+ const tags = [
651
+ role.is_coordinator ? 'Coordinator' : null,
652
+ role.is_default ? 'default' : null,
653
+ ].filter(Boolean).join(', ');
654
+ const tag = tags ? ` (${tags})` : '';
655
+ return { content: [{ type: 'text', text: `Created role **${role.name}**${tag} (id: ${role.id}) in cube ${cubeId}.` }] };
656
+ }
657
+ case 'borg:update-role': {
658
+ const roleId = args?.role_id;
659
+ if (!roleId)
660
+ throw new Error('role_id is required');
661
+ const updates = {};
662
+ if (typeof args?.name === 'string')
663
+ updates.name = args.name;
664
+ if (typeof args?.short_description === 'string')
665
+ updates.short_description = args.short_description;
666
+ if (typeof args?.detailed_description === 'string')
667
+ updates.detailed_description = args.detailed_description;
668
+ if (typeof args?.is_default === 'boolean')
669
+ updates.is_default = args.is_default;
670
+ if (typeof args?.is_coordinator === 'boolean')
671
+ updates.is_coordinator = args.is_coordinator;
672
+ if (Object.keys(updates).length === 0)
673
+ throw new Error('Pass at least one of: name, short_description, detailed_description, is_default, is_coordinator.');
674
+ const { role } = await updateRole(roleId, updates);
675
+ const tags = [
676
+ role.is_coordinator ? 'Coordinator' : null,
677
+ role.is_default ? 'default' : null,
678
+ ].filter(Boolean).join(', ');
679
+ const tag = tags ? ` (${tags})` : '';
680
+ return { content: [{ type: 'text', text: `Updated role **${role.name}**${tag} (id: ${role.id}).` }] };
681
+ }
682
+ case 'borg:delete-role': {
683
+ const roleId = args?.role_id;
684
+ if (!roleId)
685
+ throw new Error('role_id is required');
686
+ await deleteRole(roleId);
687
+ return { content: [{ type: 'text', text: `Deleted role ${roleId}.` }] };
688
+ }
689
+ case 'borg:reassign-drone': {
690
+ const droneId = args?.drone_id;
691
+ const roleId = args?.role_id;
692
+ if (!droneId)
693
+ throw new Error('drone_id is required');
694
+ if (!roleId)
695
+ throw new Error('role_id is required');
696
+ const { drone } = await reassignDrone(droneId, roleId);
697
+ return { content: [{ type: 'text', text: `Reassigned drone ${drone.label} (${drone.id}) to role ${drone.role_id}.` }] };
698
+ }
699
+ case 'borg:list-drones': {
700
+ const cubeId = args?.cube_id;
701
+ if (!cubeId)
702
+ throw new Error('cube_id is required');
703
+ const { drones, roles } = await getCube(cubeId);
704
+ if (!drones.length) {
705
+ return { content: [{ type: 'text', text: 'No drones in this cube yet.' }] };
706
+ }
707
+ const rolesById = new Map(roles.map((r) => [r.id, r]));
708
+ const lines = drones.map((d) => {
709
+ const r = rolesById.get(d.role_id);
710
+ return `- **${d.label}** (id: ${d.id}) — role: ${r?.name ?? '?'} (${d.role_id}) — last seen ${d.last_seen}`;
711
+ });
712
+ return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n${lines.join('\n')}` }] };
713
+ }
714
+ case 'borg:list-templates': {
715
+ const names = listTemplateNames();
716
+ const lines = names.map((n) => {
717
+ const t = getTemplate(n);
718
+ return `- **${n}**: ${t.description}`;
719
+ });
720
+ return { content: [{ type: 'text', text: `Available templates:\n\n${lines.join('\n')}` }] };
721
+ }
722
+ case 'borg:apply-template': {
723
+ const cubeId = args?.cube_id;
724
+ const templateName = args?.template_name;
725
+ if (!cubeId)
726
+ throw new Error('cube_id is required');
727
+ if (!templateName)
728
+ throw new Error('template_name is required');
729
+ const template = getTemplate(templateName);
730
+ if (!template) {
731
+ throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
732
+ }
733
+ const summary = await applyTemplateToCube(cubeId, template.roles);
734
+ return { content: [{ type: 'text', text: `Applied template **${templateName}** to cube ${cubeId} — ${summary.created} role(s) created, ${summary.updated} updated.` }] };
735
+ }
109
736
  default:
110
737
  throw new Error(`Unknown tool: ${name}`);
111
738
  }
@@ -113,12 +740,13 @@ async function main() {
113
740
  catch (error) {
114
741
  // Better error messages for auth/subscription issues
115
742
  if (error.message?.includes('Authentication required') ||
743
+ error.message?.includes('Authentication expired') ||
116
744
  error.message?.includes('Failed to refresh')) {
117
745
  return {
118
746
  content: [
119
747
  {
120
748
  type: 'text',
121
- text: '🔐 Authentication expired. Run: borg setup',
749
+ text: ' Authentication expired. Run: borg setup',
122
750
  },
123
751
  ],
124
752
  isError: true,
@@ -135,15 +763,63 @@ async function main() {
135
763
  };
136
764
  }
137
765
  });
766
+ // Register prompts listing
767
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
768
+ return {
769
+ prompts: [
770
+ {
771
+ name: 'subscribe',
772
+ description: 'Set up Borg MCP subscription ($1/month, 7-day trial)',
773
+ },
774
+ {
775
+ name: 'dashboard',
776
+ description: 'Open Borg MCP dashboard to manage cubes',
777
+ },
778
+ ],
779
+ };
780
+ });
781
+ // Register prompt getter
782
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
783
+ const { name } = request.params;
784
+ switch (name) {
785
+ case 'subscribe':
786
+ return {
787
+ description: 'Set up Borg MCP subscription ($1/month, 7-day trial)',
788
+ messages: [
789
+ {
790
+ role: 'user',
791
+ content: {
792
+ type: 'text',
793
+ text: 'Please help me set up a Borg MCP subscription using the subscribe tool.',
794
+ },
795
+ },
796
+ ],
797
+ };
798
+ case 'dashboard':
799
+ return {
800
+ description: 'Open Borg MCP dashboard to manage cubes',
801
+ messages: [
802
+ {
803
+ role: 'user',
804
+ content: {
805
+ type: 'text',
806
+ text: 'Please open the Borg MCP dashboard using the open_dashboard tool.',
807
+ },
808
+ },
809
+ ],
810
+ };
811
+ default:
812
+ throw new Error(`Unknown prompt: ${name}`);
813
+ }
814
+ });
138
815
  // Create stdio transport
139
816
  const transport = new StdioServerTransport();
140
817
  // Connect server to transport
141
818
  await server.connect(transport);
142
- console.error('🚀 Borg MCP Client started');
143
- console.error('💡 Use borg:regen to fetch centralized context from the hive mind');
144
- console.error('🌐 Manage your context entries at https://borgmcp.ai/dashboard');
819
+ console.error(' Borg MCP Client started');
820
+ console.error(' Use borg:assimilate <cube-name> to join a cube as a drone');
821
+ console.error(' Manage your cubes at https://borgmcp.ai/dashboard');
145
822
  }
146
- // Start the server
147
823
  main().catch((error) => {
148
824
  console.error('Fatal error:', error);
149
825
  process.exit(1);