borgmcp 1.0.9 → 1.0.10

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pure label→id resolution for the `borg:evict-drone` tool (gh#718).
3
+ *
4
+ * `CubeStore.evictDrone` and the owner-authed `DELETE /api/drones/:id` route
5
+ * both take a drone UUID. Coordinators, however, see drone LABELS everywhere
6
+ * (roster, regen, cube log) and rarely the UUIDs. This helper lets the tool
7
+ * accept a label and resolve it to the drone id client-side, against the
8
+ * owner-scoped cube detail returned by `getCube` (the same id+label pairs
9
+ * `borg:list-drones` renders). No I/O here — a pure function so it can be
10
+ * unit-tested in isolation; the handler in index.ts owns the network calls.
11
+ */
12
+ export interface EvictableDrone {
13
+ id: string;
14
+ label: string;
15
+ }
16
+ /**
17
+ * Strict whole-string UUID shape check for the `drone_id` input (gh#782).
18
+ * The handler rejects non-UUID values before building the DELETE URL: a
19
+ * label passed as drone_id gets a clear "use label + cube_id" hint instead
20
+ * of a confusing 404, and a path-shaped value ("../cubes/<uuid>") is never
21
+ * interpolated into a request path. Case-insensitive; anchored so embedded
22
+ * or suffixed UUIDs do not pass.
23
+ */
24
+ export declare function isUuidShape(value: string): boolean;
25
+ /**
26
+ * Assert-style wrapper around isUuidShape (gh#782, reassign half). Called
27
+ * by the remote-client functions that interpolate a drone id into the
28
+ * request path (reassignDrone PATCH, evictDrone DELETE) — FIRST, before
29
+ * any token fetch or network I/O, so a path-shaped value like
30
+ * "../cubes/<uuid>" can never reach URL construction. The `label` names
31
+ * the offending input in the caller-facing error.
32
+ */
33
+ export declare function assertUuidShape(value: string, label: string): void;
34
+ /**
35
+ * Resolve an exact drone label to its `{ id, label }` within a single cube's
36
+ * drone list. Labels are unique per cube, so an exact match is unambiguous.
37
+ * Returns null when no drone carries the label (the handler turns that into a
38
+ * caller-facing error). Matching is exact (not substring) and label-only — a
39
+ * UUID passed here never resolves, because the handler routes UUIDs through the
40
+ * explicit `drone_id` input instead.
41
+ */
42
+ export declare function resolveDroneIdByLabel(drones: ReadonlyArray<EvictableDrone>, label: string): EvictableDrone | null;
43
+ //# sourceMappingURL=evict-drone.d.ts.map
@@ -0,0 +1 @@
1
+ function i(t){return/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t)}function o(t,e){if(!i(t))throw new Error(`${e} "${t}" is not a UUID`)}function f(t,e){const n=e.trim(),r=t.find(a=>a.label===n);return r?{id:r.id,label:r.label}:null}export{o as assertUuidShape,i as isUuidShape,f as resolveDroneIdByLabel};
@@ -25,6 +25,14 @@
25
25
  * Usage:
26
26
  * borg-inbox-monitor <inbox-file-path>
27
27
  */
28
+ export declare const RECENT_EMITTED_LINE_CAP = 1024;
29
+ export declare class RecentLineDeduper {
30
+ private readonly cap;
31
+ private readonly seen;
32
+ private readonly order;
33
+ constructor(cap?: number);
34
+ remember(line: string): boolean;
35
+ }
28
36
  /**
29
37
  * Pure: parse one inbox-file line and produce the pretty summary line
30
38
  * (or null if the line is a continuation or unrecognized shape).
@@ -39,6 +47,8 @@
39
47
  * Exported so tests can exercise the parsing without spawning tail.
40
48
  */
41
49
  export declare function formatEventLine(inboxLine: string): string | null;
50
+ export declare function formatFreshEventLine(inboxLine: string, deduper: RecentLineDeduper): string | null;
51
+ export declare function seedDeduperFromInboxTail(inboxPath: string, deduper: RecentLineDeduper, maxLines?: number): void;
42
52
  /**
43
53
  * Is this module being invoked as the bin entry point?
44
54
  *
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as c}from"node:child_process";import{realpathSync as l}from"node:fs";import{createInterface as a}from"node:readline";import{fileURLToPath as p}from"node:url";const f=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/;function u(r){const o=f.exec(r);if(!o)return null;const[,,n,e,i]=o,t=i.trim();return`${n} (${e}): ${t}`}function m(){const r=process.argv[2];r||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const o=c("tail",["-F","-n","0",r],{stdio:["ignore","pipe","inherit"]});o.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),process.exit(1));const n=a({input:o.stdout,crlfDelay:1/0});let e=!1;n.on("line",t=>{const s=u(t);s!==null&&console.log(s)}),o.on("error",t=>{console.error(`borg-inbox-monitor: tail failed: ${t.message}`),process.exit(1)}),o.on("exit",(t,s)=>{e&&process.exit(0),s&&process.exit(0),process.exit(t??0)});const i=t=>{e||(e=!0,n.close(),!o.killed&&!o.kill(t)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref())};process.once("SIGTERM",()=>i("SIGTERM")),process.once("SIGINT",()=>i("SIGINT"))}function x(r,o){try{return l(r)===p(o)}catch{return!1}}x(process.argv[1],import.meta.url)&&m();export{u as formatEventLine,x as isEntryInvocation};
2
+ import{spawn as u}from"node:child_process";import{readFileSync as p,realpathSync as a}from"node:fs";import{createInterface as f}from"node:readline";import{fileURLToPath as m}from"node:url";const d=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/,h=1024;class b{cap;seen=new Set;order=[];constructor(e=h){if(this.cap=e,!Number.isInteger(e)||e<1)throw new Error("cap must be a positive integer")}remember(e){if(this.seen.has(e))return!1;for(this.seen.add(e),this.order.push(e);this.order.length>this.cap;){const r=this.order.shift();r&&this.seen.delete(r)}return!0}}function l(t){const e=d.exec(t);if(!e)return null;const[,,r,s,n]=e,o=n.trim();return`${r} (${s}): ${o}`}function x(t,e){const r=l(t);return r===null?null:e.remember(t)?r:null}function E(t,e,r=512){if(!Number.isInteger(r)||r<1)throw new Error("maxLines must be a positive integer");let s;try{s=p(t,"utf-8")}catch(o){if(o?.code==="ENOENT")return;throw o}const n=s.split(/\r?\n/);n.at(-1)===""&&n.pop();for(const o of n.slice(-r))l(o)!==null&&e.remember(o)}function g(){const t=process.argv[2];t||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const e=new b;E(t,e);const r=u("tail",["-F","-n","0",t],{stdio:["ignore","pipe","inherit"]});r.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),process.exit(1));const s=f({input:r.stdout,crlfDelay:1/0});let n=!1;s.on("line",i=>{const c=x(i,e);c!==null&&console.log(c)}),r.on("error",i=>{console.error(`borg-inbox-monitor: tail failed: ${i.message}`),process.exit(1)}),r.on("exit",(i,c)=>{n&&process.exit(0),c&&process.exit(0),process.exit(i??0)});const o=i=>{n||(n=!0,s.close(),!r.killed&&!r.kill(i)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref())};process.once("SIGTERM",()=>o("SIGTERM")),process.once("SIGINT",()=>o("SIGINT"))}function I(t,e){try{return a(t)===m(e)}catch{return!1}}I(process.argv[1],import.meta.url)&&g();export{h as RECENT_EMITTED_LINE_CAP,b as RecentLineDeduper,l as formatEventLine,x as formatFreshEventLine,I as isEntryInvocation,E as seedDeduperFromInboxTail};
package/dist/index.js CHANGED
@@ -1,27 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import{Server as K}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as Q}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Y,ListToolsRequestSchema as G,ListPromptsRequestSchema as z,GetPromptRequestSchema as X}from"@modelcontextprotocol/sdk/types.js";import{assimilate as J,getCubeInfo as Z,getRoleInfo as j,getRoster as ee,readLog as te,appendLog as re,ackLogEntry as oe,regen as ne,listCubes as ie,createCube as se,updateCube as O,deleteCube as ae,createRole as ce,updateRole as le,patchRoleSection as S,patchTaxonomyClass as T,deleteRole as de,reassignDrone as pe,getCube as U,checkSubscriptionStatus as ue,createBillingPortalSession as me,createSubscription as he,syncRoles as be,applyTemplate as ge,whoami as ye,roleRationale as fe,API_URL as we,getValidToken as _e}from"./remote-client.js";import{startHealthBeatTick as ve}from"./health-beat.js";import{getTemplate as C,listTemplateNames as q,resolveCubeDirectiveForCreate as xe,resolveCubeDirectiveForApply as ke,resolveMessageTaxonomyForCreate as $e}from"./templates.js";import{activeCubeWithFreshRegenIdentity as Se,getActiveCube as y,setActiveCube as P,inboxPathForDrone as f}from"./cubes.js";import{addSessionStartHook as Ue,addUserPromptSubmitHook as Ce}from"./config-utils.js";import{humanAgo as A,formatLogEntryMarkdown as qe,formatRegenMarkdown as Ie,getDronePlaybook as N,nullTaxonomyTip as Re,regenWakePathDroneLabel as De}from"./regen-format.js";import{startLogStream as Ee,getStreamStatus as I}from"./log-stream.js";import{renderRoleList as je}from"./list-roles-render.js";import{getPackageVersion as w,getOnDiskVersion as Oe,handleVersionFlag as Te}from"./version.js";import{renderStreamStatus as Pe,checkInboxMonitorHealthy as R,formatWakePathPrefix as Ae,shouldShowWakePathWarning as Ne}from"./stream-status.js";import{formatRoleAgentLabel as Le,renderRoster as Me}from"./roster-render.js";import{renderSyncRolesResult as Be}from"./sync-roles-render.js";import{initConsolePrefix as Fe,consolePrefix as _}from"./console-prefix.js";import{isCodexRemoteWakeEnabled as v,resolveSessionAgentKind as L,probeCodexBridgeArmed as We}from"./codex-app-wake.js";import{lifecycleSignalForMessage as He,recordLifecycleLog as M,shouldSuppressLifecycleLog as Ve}from"./lifecycle-log-guard.js";import{normalizeDirectLogRecipients as Ke}from"./direct-log.js";import B from"open";import Qe from"os";function F(){try{const p=Qe.hostname();return p&&p.trim()?p.trim().slice(0,255):null}catch{return null}}async function W(p,x){return await ge(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 Ye(){Te();try{Ue()}catch{}try{Ce()}catch{}try{Ee()}catch{}try{ve({getActiveCube:y,getStreamConnected:()=>I().connected,getInboxPath:h=>f(h.cubeId,h.droneId),checkMonitor:R,isCodexRemoteWake:v,probeBridgeArmed:h=>We({cubeId:h.cubeId,droneId:h.droneId}),resolveAgentKind:L,resolveHostname:F,resolveVersion:w,getToken:_e,fetchImpl:globalThis.fetch.bind(globalThis)})}catch{}const p=new K({name:"borg-mcp-client",version:w()},{capabilities:{tools:{},prompts:{}}});p.setRequestHandler(G,async()=>({tools:[{name:"subscribe",description:"Create Stripe checkout session for Cube tier ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:upgrade-subscription",description:"Open the Stripe Billing Portal to manage Cube tier quantity ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr).",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 entries from the cube's activity log. Each entry is tagged with the drone that wrote it and that drone's role. For wake triage, prefer `unread_only=true` with a modest limit and drain until the result is not full and `behind_by` is 0; this reads oldest-unread-first from your server cursor and advances the watermark so bursts are not skipped. Optional `since` is a strict-after cursor for explicit bounded reads only; do not use it with the same timestamp as a notification preview because it can skip the boundary entry.",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional strict-after cursor for explicit bounded reads. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple for deterministic tie-break) OR an ISO-8601 timestamp. Do not use for routine wake triage; prefer unread_only."},limit:{type:"number",description:"max entries to return (1-500)"},unread_only:{type:"boolean",description:"When true, read only entries posted after this drone last called read-log, oldest-unread-first. Server advances the watermark to the newest returned entry on every call; if the result is full or behind_by > 0, call again until drained."}}}},{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(Y,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 ne(e.sessionToken,e.apiUrl,{since:t}),i=Se(e,n);i!==e&&await P(i);const s=I(),a=f(i.cubeId,i.droneId),c=v()?!0:R(a),l=Ne(s,c)?Ae({inboxPath:a,droneLabel:De(n,i.droneLabel),cubeName:i.name}):"";let m="";try{const u=w(),d=Oe();if(u!=="unknown"&&d!=="unknown"&&d!==u){const[k,D,H]=u.split(".").map(Number),[$,E,V]=d.split(".").map(Number);($>k||$===k&&E>D||$===k&&E===D&&V>H)&&(m=`## \u{1F504} borgmcp ${d} installed \u2014 run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${u}.
2
+ import{Server as V}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as K}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as z,ListToolsRequestSchema as Y,ListPromptsRequestSchema as G,GetPromptRequestSchema as X}from"@modelcontextprotocol/sdk/types.js";import{assimilate as J,getCubeInfo as Z,getRoleInfo as j,getRoster as ee,readLog as te,appendLog as re,ackLogEntry as oe,regen as ie,listCubes as ne,createCube as se,updateCube as O,deleteCube as ae,createRole as ce,updateRole as le,patchRoleSection as S,patchTaxonomyClass as T,deleteRole as de,reassignDrone as pe,evictDrone as ue,getCube as w,checkSubscriptionStatus as me,createBillingPortalSession as he,createSubscription as be,syncRoles as ge,applyTemplate as ye,whoami as fe,roleRationale as we,API_URL as _e,getValidToken as ve}from"./remote-client.js";import{startHealthBeatTick as xe}from"./health-beat.js";import{getTemplate as I,listTemplateNames as C,resolveCubeDirectiveForCreate as ke,resolveCubeDirectiveForApply as $e,resolveMessageTaxonomyForCreate as Ue}from"./templates.js";import{activeCubeWithFreshRegenIdentity as Se,getActiveCube as f,setActiveCube as P,inboxPathForDrone as _}from"./cubes.js";import{addSessionStartHook as Ie,addUserPromptSubmitHook as Ce}from"./config-utils.js";import{humanAgo as A,formatLogEntryMarkdown as qe,formatRegenMarkdown as Re,getDronePlaybook as L,nullTaxonomyTip as Ee,regenWakePathDroneLabel as De}from"./regen-format.js";import{startLogStream as je,getStreamStatus as q}from"./log-stream.js";import{renderRoleList as Oe}from"./list-roles-render.js";import{getPackageVersion as v,getOnDiskVersion as Te,handleVersionFlag as Pe}from"./version.js";import{renderStreamStatus as Ae,checkInboxMonitorHealthy as R,formatWakePathPrefix as Le,shouldShowWakePathWarning as Ne}from"./stream-status.js";import{formatRoleAgentLabel as Me,renderRoster as Be}from"./roster-render.js";import{resolveDroneIdByLabel as Fe,isUuidShape as We}from"./evict-drone.js";import{renderSyncRolesResult as He}from"./sync-roles-render.js";import{initConsolePrefix as Qe,consolePrefix as x}from"./console-prefix.js";import{isCodexRemoteWakeEnabled as k,resolveSessionAgentKind as N,probeCodexBridgeArmed as Ve}from"./codex-app-wake.js";import{lifecycleSignalForMessage as Ke,recordLifecycleLog as M,shouldSuppressLifecycleLog as ze}from"./lifecycle-log-guard.js";import{normalizeDirectLogRecipients as Ye}from"./direct-log.js";import B from"open";import Ge from"os";function F(){try{const p=Ge.hostname();return p&&p.trim()?p.trim().slice(0,255):null}catch{return null}}async function W(p,$){return await ye(p,$.name)}async function g(){const p=await f();if(!p)throw new Error("Not assimilated to a cube. Use borg:assimilate <cube-name> first.");return p}async function Xe(){Pe();try{Ie()}catch{}try{Ce()}catch{}try{je()}catch{}try{xe({getActiveCube:f,getStreamConnected:()=>q().connected,getInboxPath:m=>_(m.cubeId,m.droneId),checkMonitor:R,isCodexRemoteWake:k,probeBridgeArmed:m=>Ve({cubeId:m.cubeId,droneId:m.droneId}),resolveAgentKind:N,resolveHostname:F,resolveVersion:v,getToken:ve,fetchImpl:globalThis.fetch.bind(globalThis)})}catch{}const p=new V({name:"borg-mcp-client",version:v()},{capabilities:{tools:{},prompts:{}}});p.setRequestHandler(Y,async()=>({tools:[{name:"subscribe",description:"Create Stripe checkout session for Cube tier ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:upgrade-subscription",description:"Open the Stripe Billing Portal to manage Cube tier quantity ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr).",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 entries from the cube's activity log. Each entry is tagged with the drone that wrote it and that drone's role. For wake triage, prefer `unread_only=true` with a modest limit and drain until `has_more=false`; this reads oldest-unread-first from your server cursor and advances the watermark so bursts are not skipped. Optional `since` is a strict-after cursor for explicit bounded reads only; do not use it with the same timestamp as a notification preview because it can skip the boundary entry.",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional strict-after cursor for explicit bounded reads. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple for deterministic tie-break) OR an ISO-8601 timestamp. Do not use for routine wake triage; prefer unread_only."},limit:{type:"number",description:"max entries to return (1-500)"},unread_only:{type:"boolean",description:"When true, read only entries posted after this drone last called read-log, oldest-unread-first. Server advances the watermark to the newest returned entry on every call; if has_more=true, call again until has_more=false."}}}},{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:evict-drone",description:"Evict (soft-delete) a drone from its cube. Coordinator-shaped: the cube's Coordinator/Queen seat calls this to remove a dead, stuck, or surplus drone \u2014 it drops out of the roster and frees its slot (incl. a held Coordinator/Queen-class seat), while its activity-log history is preserved with anonymized attribution. Owner-scoped: you can only evict drones in cubes you own. Identify the drone EITHER by drone_id (UUID) OR by label + cube_id (the label as it appears in the roster/regen).",inputSchema:{type:"object",properties:{drone_id:{type:"string",description:"UUID of the drone to evict. Provide this OR (label + cube_id)."},label:{type:"string",description:'Drone label to evict, e.g. "two-of-seventeen-builder". Requires cube_id. Ignored when drone_id is given.'},cube_id:{type:"string",description:"UUID of the cube the labelled drone belongs to. Required when evicting by label."}}}},{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(z,async m=>{const{name:y,arguments:r}=m.params;try{switch(y){case"borg:regen":{const e=await f();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",i=await ie(e.sessionToken,e.apiUrl,{since:t}),n=Se(e,i);n!==e&&await P(n);const s=q(),a=_(n.cubeId,n.droneId),c=k()?!0:R(a),d=Ne(s,c)?Le({inboxPath:a,droneLabel:De(i,n.droneLabel),cubeName:n.name}):"";let u="";try{const h=v(),l=Te();if(h!=="unknown"&&l!=="unknown"&&l!==h){const[b,E,H]=h.split(".").map(Number),[U,D,Q]=l.split(".").map(Number);(U>b||U===b&&D>E||U===b&&D===E&&Q>H)&&(u=`## \u{1F504} borgmcp ${l} installed \u2014 run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${h}.
3
3
 
4
- `)}}catch{}return{content:[{type:"text",text:m+l+Ie(n,{mode:o})}]}}case"subscribe":return{content:[{type:"text",text:`Complete your subscription at: ${await he()}`}]};case"borg:upgrade-subscription":{const e=await me();try{await B(e)}catch{}return{content:[{type:"text",text:`Manage your Borg MCP subscription at: ${e}`}]}}case"subscription_status":{const e=await ue();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}case"open_dashboard":{const e="https://borgmcp.ai/dashboard";return await B(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=we,o=F(),n=L(),i=await J(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 ye(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([Z(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=Re(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 fe(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 ee(e.sessionToken,e.apiUrl,t);return{content:[{type:"text",text:Me({cubeName:e.name,drones:o,roles:n,resolvedSince:i??null,humanAgo:A})}]}}case"borg:stream-status":{const e=I(),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
4
+ `)}}catch{}return{content:[{type:"text",text:u+d+Re(i,{mode:o})}]}}case"subscribe":return{content:[{type:"text",text:`Complete your subscription at: ${await be()}`}]};case"borg:upgrade-subscription":{const e=await he();try{await B(e)}catch{}return{content:[{type:"text",text:`Manage your Borg MCP subscription at: ${e}`}]}}case"subscription_status":{const e=await me();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}case"open_dashboard":{const e="https://borgmcp.ai/dashboard";return await B(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=_e,o=F(),i=N(),n=await J(e,t,o,i);await P({cubeId:n.cube.id,droneId:n.drone.id,name:n.cube.name,sessionToken:n.sessionToken,droneLabel:n.drone.label,apiUrl:t});const s=_(n.cube.id,n.drone.id),a=k()?["## 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 ${n.drone.label} on cube ${n.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: ${n.cube.name}`,"",`**Drone label:** ${n.drone.label}`,`**Assigned role:** ${n.role.name}`,"",...a,"## Cube directive",n.cube.cube_directive||"_(none)_","",`## Your role: ${n.role.name}`,n.role.detailed_description||"_(no detailed description set)_","",L()].join(`
5
+ `)}]}}case"borg:version":return{content:[{type:"text",text:`borgmcp ${v()}`}]};case"borg:whoami":{const e=await g(),t=await fe(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}case"borg:cube":{const e=await g(),[{cube:t,roles:o}]=await Promise.all([Z(e.sessionToken,e.apiUrl),j(e.sessionToken,e.apiUrl)]),i=[];i.push(`# Cube: ${t.name}`),i.push(""),i.push("## Cube directive"),i.push(t.cube_directive||"_(none)_"),i.push("");const n=Ee(t.message_taxonomy);if(n&&(i.push(n),i.push("")),i.push("## Roles in this cube"),!o.length)i.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})`:"",d=s.short_description||"_(no description)_";i.push(`- **${s.name}**${c} \u2014 ${d}`)}i.push(""),i.push("_(Coordinator-class drones can fetch role IDs via `borg:list-roles` for use with `borg:reassign-drone`.)_")}return i.push(""),i.push(L()),{content:[{type:"text",text:i.join(`
6
+ `)}]}}case"borg:role":{const e=await g(),{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 g(),t=typeof r?.role=="string"?r.role:"",o=typeof r?.section=="string"?r.section:"",i=await we(e.sessionToken,e.apiUrl,t,o);return{content:[{type:"text",text:[`# Role rationale: ${i.role} \u2014 ${i.section}`,"",i.body||"_(empty)_"].join(`
8
+ `)}]}}case"borg:roster":{const e=await g(),t=typeof r?.since=="string"?r.since:void 0,{drones:o,roles:i,since:n}=await ee(e.sessionToken,e.apiUrl,t);return{content:[{type:"text",text:Be({cubeName:e.name,drones:o,roles:i,resolvedSince:n??null,humanAgo:A})}]}}case"borg:stream-status":{const e=q(),t=await f(),o=t?_(t.cubeId,t.droneId):null,i=t?k()?!0:R(o):null;let n="";e.runLoopHealth==="silent-inert"&&(n=`## \u26A0 SSE stream loop silent-inert \u2014 run /mcp and reconnect to restart
9
9
 
10
10
  The log-stream consumer started but never connected. This drone will not receive real-time cube events.
11
11
 
12
- `);const s=Pe({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 te(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(qe(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(He(e)){const d=await Ve(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?Ke(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 re(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 oe(t.sessionToken,t.apiUrl,e),{content:[{type:"text",text:`Acked entry ${e} in cube "${t.name}".`}]}}case"borg:list-cubes":{const{cubes:e}=await ie();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})
12
+ `);const s=Ae({status:e,inboxMonitorHealthy:i,inboxPath:o,droneLabel:t?.droneLabel??null,cubeName:t?.name??null,humanAgo:A});return{content:[{type:"text",text:n+s}]}}case"borg:read-log":{const e=await g(),t=typeof r?.since=="string"?r.since:void 0,o=typeof r?.limit=="number"?r.limit:void 0,i=r?.unread_only===!0||r?.unread_only==="true",{entries:n,drones:s,roles:a,behind_by:c,has_more:d}=await te(e.sessionToken,e.apiUrl,{since:t,limit:o,unreadOnly:i}),u=new Map;for(const b of s)u.set(b.id,b);const h=new Map;for(const b of a)h.set(b.id,b);const l=[];if(l.push(`# Activity log: ${e.name}`),l.push(""),!n.length)l.push("_(no entries)_");else for(const b of n)l.push(qe(b,u,h));return d===!0?(l.push(""),l.push("\u26A0 has_more: true \u2014 call `borg:read-log unread_only=true` again until has_more=false so you finish draining unread entries.")):typeof c=="number"&&c>0&&(l.push(""),l.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:l.join(`
13
+ `)}]}}case"borg:log":{const e=r?.message;if(!e||typeof e!="string")throw new Error("message is required");const t=await f();if(!t)throw new Error("Not assimilated to a cube. Use borg:assimilate <cube-name> first.");if(Ke(e)){const l=await ze(t,e);if(l.suppress)return await M(t,e),{content:[{type:"text",text:`Suppressed duplicate ${l.signal?.toUpperCase()} lifecycle log for ${t.droneLabel}; recent cube log already contains this signal.`}]}}const o=Object.prototype.hasOwnProperty.call(r??{},"to"),i=o?Ye(r?.to):void 0,n=typeof r?.class=="string"?r.class:void 0,s=r?.visibility==="broadcast"||r?.visibility==="direct"?r.visibility:void 0,a={...n?{class:n}:{},...o?{to:i??[]}:{},...s?{visibility:s}:{}},c=await re(t.sessionToken,t.apiUrl,e,a);await M(t,e);const d=c.routing?.message?`
14
+ ${c.routing.message}`:"",u=c.unreachableRecipients?.length?`
15
+ \u26A0 ${c.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${c.unreachableRecipients.map(l=>l.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})${d}${u}`}]}}case"borg:ack":{const e=r?.entry_id;if(!e||typeof e!="string")throw new Error("entry_id is required");const t=await g();return await oe(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
16
  ${(o.cube_directive||"_(no directive set)_").split(`
17
17
  `)[0].slice(0,120)}`);return{content:[{type:"text",text:`Your cubes (${e.length}):
18
18
 
19
19
  ${t.join(`
20
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: ${q().join(", ")}`);const i=xe(t,n),s=$e(void 0,n),a=await se(e,i,{message_taxonomy:s});if(n){const l=await W(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 ae(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 ce(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 le(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 de(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 pe(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=Le(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}):
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 i=null;if(o&&(i=I(o),!i))throw new Error(`Unknown template "${o}". Available: ${C().join(", ")}`);const n=ke(t,i),s=Ue(void 0,i),a=await se(e,n,{message_taxonomy:s});if(i){const d=await W(a.id,i),u=n!==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 ${d.created} role(s) created, ${d.updated} updated.${u} 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,i;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})),i=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})),i=String(s.class??"")}return{content:[{type:"text",text:`${t==="add"?"Added":t==="replace"?"Replaced":"Removed"} taxonomy class **${i}** 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 ae(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,i=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(i===void 0)throw new Error("detailed_description is required (pass empty string if none)");const n=r?.is_default===!0,s=r?.is_human_seat===!0,a=r?.can_broadcast===!0,c=r?.receives_all_direct===!0,{role:d}=await ce(e,{name:t,short_description:o,detailed_description:i,is_default:n,is_human_seat:s,can_broadcast:a,receives_all_direct:c}),u=[d.role_class==="queen"?"Queen":null,d.is_human_seat?"human-seat":null,d.is_default?"default":null].filter(Boolean).join(", "),h=u?` (${u})`:"";return{content:[{type:"text",text:`Created role **${d.name}**${h} (id: ${d.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 le(e,t),i=[o.role_class==="queen"?"Queen":null,o.is_human_seat?"human-seat":null,o.is_default?"default":null].filter(Boolean).join(", "),n=i?` (${i})`:"";return{content:[{type:"text",text:`Updated role **${o.name}**${n} (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 i;if(t==="delete")({role:i}=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:i}=await S(e,{action:t,heading:o,body:s,after:a}))}else({role:i}=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 **${i.name}** (id: ${i.id}).`}]}}case"borg:delete-role":{const e=r?.role_id;if(!e)throw new Error("role_id is required");return await de(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 pe(e,t);return{content:[{type:"text",text:`Reassigned drone ${o.label} (${o.id}) to role ${o.role_id}.`}]}}case"borg:evict-drone":{const e=r?.drone_id?.trim(),t=r?.label?.trim(),o=r?.cube_id?.trim();let i,n;if(e){if(!We(e))throw new Error(`drone_id "${e}" is not a UUID \u2014 if that's a drone label, pass it as label + cube_id instead.`);i=e,n=e}else if(t){if(!o)throw new Error("cube_id is required when evicting by label");const{drones:s}=await w(o),a=Fe(s,t);if(!a)throw new Error(`No active drone labelled "${t}" in cube ${o} (it may already be evicted; check borg:list-drones).`);i=a.id,n=a.label}else throw new Error("Provide drone_id, or label + cube_id, to identify the drone to evict");return await ue(i),{content:[{type:"text",text:`Evicted drone ${n} (${i}). Soft-deleted: removed from the roster and freed its seat; log history preserved with anonymized attribution.`}]}}case"borg:list-drones":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{drones:t,roles:o}=await w(e);if(!t.length)return{content:[{type:"text",text:"No drones in this cube yet."}]};const i=new Map(o.map(s=>[s.id,s])),n=t.map(s=>{const a=i.get(s.role_id),c=Me(a?.name??"?",s.agent_kind),d=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}${d}`});return{content:[{type:"text",text:`Drones in cube ${e} (${t.length}):
22
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:je(t,e)}]}}case"borg:list-templates":return{content:[{type:"text",text:`Available templates:
23
+ ${n.join(`
24
+ `)}`}]}}case"borg:list-roles":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{roles:t}=await w(e);return{content:[{type:"text",text:Oe(t,e)}]}}case"borg:list-templates":return{content:[{type:"text",text:`Available templates:
25
25
 
26
- ${q().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 be(e,t,o,n);return{content:[{type:"text",text:Be(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: ${q().join(", ")}`);const n=await W(e,o);let i="";const s=await U(e),a=ke(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(z,async()=>({prompts:[{name:"subscribe",description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial."},{name:"dashboard",description:"Open Borg MCP dashboard to manage cubes"}]})),p.setRequestHandler(X,async h=>{const{name:g}=h.params;switch(g){case"subscribe":return{description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 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 Q;await p.connect(x),await Fe(),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`)}Ye().catch(p=>{console.error(`${_()}Fatal error:`,p),process.exit(1)});
26
+ ${C().map(o=>{const i=I(o);return`- **${o}**: ${i.description}`}).join(`
27
+ `)}`}]};case"borg:sync-roles":{const e=r?.cube_id,t=r?.template_name||"software-dev",o=r?.apply===!0,i=r?.decisions&&typeof r.decisions=="object"?r.decisions:void 0;if(!e)throw new Error("cube_id is required");const n=await ge(e,t,o,i);return{content:[{type:"text",text:He(n,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=I(t);if(!o)throw new Error(`Unknown template "${t}". Available: ${C().join(", ")}`);const i=await W(e,o);let n="";const s=await w(e),a=$e(s.cube_directive,o);return a!==null&&(await O(e,{cube_directive:a}),n=" Template cube directive applied (cube directive was empty)."),{content:[{type:"text",text:`Applied template **${t}** to cube ${e} \u2014 ${i.created} role(s) created, ${i.updated} updated.${n}`}]}}default:throw new Error(`Unknown tool: ${y}`)}}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/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial."},{name:"dashboard",description:"Open Borg MCP dashboard to manage cubes"}]})),p.setRequestHandler(X,async m=>{const{name:y}=m.params;switch(y){case"subscribe":return{description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 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: ${y}`)}});const $=new K;await p.connect($),await Qe(),console.error(`${x()}\u25FC Borg MCP Client started`),console.error(`${x()}\u25FC Use borg:assimilate <cube-name> to join a cube as a drone`),console.error(`${x()}\u25FC Manage your cubes at https://borgmcp.ai/dashboard`)}Xe().catch(p=>{console.error(`${x()}Fatal error:`,p),process.exit(1)});
@@ -24,6 +24,8 @@
24
24
  * connection, no second auth — just an in-process state snapshot).
25
25
  */
26
26
  import { type StreamOwnershipSnapshot } from './stream-owner.js';
27
+ export declare const INBOX_TAIL_LINES_CAP = 512;
28
+ export declare const INBOX_TAIL_TRIM_THRESHOLD_LINES: number;
27
29
  export type RunLoopHealth = 'connected' | 'reconnecting' | 'silent-inert' | 'never-started';
28
30
  export interface StreamStatus {
29
31
  connected: boolean;
@@ -152,6 +154,8 @@ export declare function compareBroadcastHwm(a: BroadcastHwm, b: BroadcastHwm): n
152
154
  * "newline was here" — convention noted in the regen-format playbook.
153
155
  */
154
156
  export declare function formatInboxLine(entry: EnrichedEntry): string;
157
+ export declare function appendCappedInboxLine(inboxPath: string, line: string, maxLines?: number, trimThresholdLines?: number): Promise<void>;
158
+ export declare function trimInboxFileToRecentLines(inboxPath: string, maxLines: number, trimThresholdLines?: number): Promise<void>;
155
159
  /**
156
160
  * @internal Exported for unit tests.
157
161
  *
@@ -1,9 +1,11 @@
1
- import{promises as D}from"node:fs";import X from"node:os";import q from"node:path";import{getActiveCube as z,inboxPathForDrone as W}from"./cubes.js";import{formatCodexWakePrompt as Q,resolveSessionAgentKind as Y,wakeCodexViaAppServer as Z}from"./codex-app-wake.js";import{getValidToken as ee}from"./remote-client.js";import{recordEventReceipt as te,emitHealthBeat as ne,getCachedMonitorHealthy as re,getCachedWakeArmed as ae}from"./health-beat.js";import{getPackageVersion as oe}from"./version.js";import{acquireStreamLease as F,readOwnershipSnapshot as g}from"./stream-owner.js";const B=9e4,ie=2e3,se=500,ce=3e4,le=50;function de(){try{const e=X.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const ue=Date.now(),o={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function fe(e,t=Date.now()-ue){return e.connected&&e.lastWireActivityAt?"connected":!e.connected&&e.reconnectAttempts>0?"reconnecting":!e.connected&&!e.lastWireActivityAt&&e.reconnectAttempts===0&&t>1e4?"silent-inert":"never-started"}function De(){return{...o,runLoopHealth:fe(o)}}function Re(){o.connected=!1,o.lastWireActivityAt=null,o.lastContentEventAt=null,o.lastHeartbeatAt=null,o.lastPersistedEventId=null,o.reconnectAttempts=0,o.runLoopRestartCount=0,o.ownership={state:"unowned"}}function Ne(){(async()=>{for(;;){try{await me(),process.stderr.write(`[borg-mcp log stream] runLoop returned unexpectedly; restarting in 5s
1
+ import{promises as h}from"node:fs";import z from"node:os";import x from"node:path";import{getActiveCube as Q,inboxPathForDrone as B}from"./cubes.js";import{formatCodexWakePrompt as Y,resolveSessionAgentKind as Z,wakeCodexViaAppServer as ee}from"./codex-app-wake.js";import{getValidToken as te}from"./remote-client.js";import{recordEventReceipt as ne,emitHealthBeat as re,getCachedMonitorHealthy as ae,getCachedWakeArmed as oe}from"./health-beat.js";import{getPackageVersion as ie}from"./version.js";import{acquireStreamLease as G,readOwnershipSnapshot as b}from"./stream-owner.js";const j=9e4,se=2e3,ce=500,le=3e4,de=50,R=512,Oe=R*2;function ue(){try{const e=z.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const fe=Date.now(),i={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function pe(e,t=Date.now()-fe){return e.connected&&e.lastWireActivityAt?"connected":!e.connected&&e.reconnectAttempts>0?"reconnecting":!e.connected&&!e.lastWireActivityAt&&e.reconnectAttempts===0&&t>1e4?"silent-inert":"never-started"}function $e(){return{...i,runLoopHealth:pe(i)}}function ve(){i.connected=!1,i.lastWireActivityAt=null,i.lastContentEventAt=null,i.lastHeartbeatAt=null,i.lastPersistedEventId=null,i.reconnectAttempts=0,i.runLoopRestartCount=0,i.ownership={state:"unowned"}}function Pe(){(async()=>{for(;;){try{await we(),process.stderr.write(`[borg-mcp log stream] runLoop returned unexpectedly; restarting in 5s
2
2
  `)}catch(e){process.stderr.write(`[borg-mcp log stream] runLoop threw: ${e?.message??e}; restarting in 5s
3
- `)}o.runLoopRestartCount+=1,await y(5e3)}})()}const G={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:Ie,hasInboxEntryId:Ee,getToken:ee,wakeCodex:Z,heartbeatTimeoutMs:B,hwmDivergenceGraceMs:ie,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:pe};function pe(e,t){te(),ne(e,{sseConnected:!0,inboxMonitorHealthy:re(),wakeArmed:ae(),agentKind:Y(),hostname:de(),version:oe(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function me(){let e=0,t=null,i=null,a=null,s=null;for(;;){const r=await z();if(!r){a&&(await a.release(),a=null,s=null),o.connected=!1,o.ownership={state:"unowned"},await y(5e3);continue}r.cubeId!==i&&(i=r.cubeId,t=null);const c=`${r.cubeId}:${r.droneId}`;if(a&&s!==c&&(await a.release(),a=null,s=null),a||(a=await F(r.cubeId,r.droneId),s=a?c:null),!a){o.connected=!1,o.ownership=await g(r.cubeId,r.droneId),await y(5e3);continue}o.ownership=await g(r.cubeId,r.droneId);let l=!1;try{const f=new AbortController,h=async()=>{try{await a.refresh()||(l=!0,f.abort(new Error("stream ownership lost")))}catch(p){l=!0,f.abort(p instanceof Error?p:new Error(String(p)))}},k=setInterval(()=>{h()},Math.max(1e3,Math.floor(B/2)));try{await K(r,t,p=>{t=p},{abortSignal:f.signal})}finally{clearInterval(k)}if(l){a=null,s=null,o.connected=!1,o.ownership=await g(r.cubeId,r.droneId),await y(5e3);continue}e=0,o.reconnectAttempts=0}catch(f){if(l){a=null,s=null,o.connected=!1,o.ownership=await g(r.cubeId,r.droneId),await y(5e3);continue}o.connected=!1;const h=Math.min(se*2**e,ce)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(h)}ms: ${f?.message??f}
4
- `),e+=1,o.reconnectAttempts=e,await y(h)}}}async function K(e,t,i,a={}){const{fetchImpl:s,appendLine:r,hasInboxEntryId:c,getToken:l,wakeCodex:f,heartbeatTimeoutMs:h,hwmDivergenceGraceMs:k,abortSignal:p,onInboxReceipt:J}={...G,...a},R=await l(),N={Authorization:`Bearer ${R}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&(N["Last-Event-ID"]=t);const E=new AbortController,x=()=>{try{E.abort(p.reason??new Error("external abort"))}catch{}};p.aborted&&x(),p.addEventListener("abort",x,{once:!0});let m=null;const O=()=>{m&&clearTimeout(m),m=setTimeout(()=>{try{E.abort(new Error("heartbeat watchdog timeout"))}catch{}},h)};O();let P=t,u=null,w=null;const I=()=>{w&&(clearTimeout(w.timer),w=null)};let A=null;const S=(n,d)=>{const T={id:n,created_at:d};A&&d&&A.created_at&&b(T,A)<=0||(A=T,P=n,o.lastPersistedEventId=n,i(n))},C=n=>{n&&(u=!u||b(n,u)>0?n:u,w&&b(u,w.hwm)>=0&&I())},U=n=>{if(w?.hwm.id===n.id)return;I();const d=setTimeout(()=>{if(u&&b(u,n)>=0){I();return}try{E.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},k);w={hwm:n,timer:d}},H=new Set,M=[];let v=t!==null;const $=async n=>{const d=ye(ge(n.data,n.id));return v&&await c(e.cubeId,e.droneId,n.id,d)?(S(n.id,n.data?.created_at??""),"persisted-skip"):(await r(e.cubeId,e.droneId,d),f(Q(d)),J(e,R),"written")},L=n=>{for(H.add(n.id),M.push(n.id);M.length>le;){const d=M.shift();d&&H.delete(d)}S(n.id,n.data?.created_at??""),C(j(n))};let _;try{_=await s(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:N,signal:E.signal})}catch(n){throw m&&clearTimeout(m),n}if(!_.ok||!_.body)throw m&&clearTimeout(m),new Error(`stream HTTP ${_.status}`);o.connected=!0;try{for await(const n of we(_.body)){O();const d=new Date().toISOString();if(o.lastWireActivityAt=d,(n.type==="log"||n.type==="bookmark")&&(o.lastContentEventAt=d),n.type==="heartbeat"){if(o.lastHeartbeatAt=d,n.hwm&&u===null){C(n.hwm),P===null&&S(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&u&&b(n.hwm,u)<=0){I();continue}n.hwm&&u&&b(n.hwm,u)>0&&U(n.hwm);continue}if(n.type==="bookmark"){v=!1;continue}if(n.type==="log"){if(H.has(n.id)){S(n.id,n.data?.created_at??""),C(j(n));continue}const T=typeof n.data?.message=="string"&&n.data.message.startsWith("[HEARTBEAT-PING]");if(n.data?.kind==="ack"){if(n.data?.author_drone_id===e.droneId&&await $(n)==="persisted-skip")continue;L(n);continue}if(n.data?.drone_id===e.droneId&&!T){L(n);continue}if(await $(n)==="persisted-skip")continue;L(n)}}}finally{p.removeEventListener("abort",x),m&&clearTimeout(m),I(),o.connected=!1}}async function Oe(e,t,i,a={}){const{ownerDeps:s,ownerStaleMs:r}={...G,...a},c=await F(e.cubeId,e.droneId,r,s);if(!c)return o.connected=!1,o.ownership=await g(e.cubeId,e.droneId,s),"skipped";o.ownership=await g(e.cubeId,e.droneId,s);try{return await K(e,t,i,a),"streamed"}finally{await c.release()}}async function*we(e){const t=e.getReader(),i=new TextDecoder;let a="";try{for(;;){const{value:s,done:r}=await t.read();if(r){if(a.trim()){const l=V(a);l&&(yield l)}return}a+=i.decode(s,{stream:!0});let c;for(;(c=a.indexOf(`
3
+ `)}i.runLoopRestartCount+=1,await I(5e3)}})()}const K={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:_e,hasInboxEntryId:Te,getToken:te,wakeCodex:ee,heartbeatTimeoutMs:j,hwmDivergenceGraceMs:se,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:me};function me(e,t){ne(),re(e,{sseConnected:!0,inboxMonitorHealthy:ae(),wakeArmed:oe(),agentKind:Z(),hostname:ue(),version:ie(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function we(){let e=0,t=null,o=null,a=null,s=null;for(;;){const r=await Q();if(!r){a&&(await a.release(),a=null,s=null),i.connected=!1,i.ownership={state:"unowned"},await I(5e3);continue}r.cubeId!==o&&(o=r.cubeId,t=null);const c=`${r.cubeId}:${r.droneId}`;if(a&&s!==c&&(await a.release(),a=null,s=null),a||(a=await G(r.cubeId,r.droneId),s=a?c:null),!a){i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}i.ownership=await b(r.cubeId,r.droneId);let l=!1;try{const f=new AbortController,g=async()=>{try{await a.refresh()||(l=!0,f.abort(new Error("stream ownership lost")))}catch(p){l=!0,f.abort(p instanceof Error?p:new Error(String(p)))}},C=setInterval(()=>{g()},Math.max(1e3,Math.floor(j/2)));try{await V(r,t,p=>{t=p},{abortSignal:f.signal})}finally{clearInterval(C)}if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}e=0,i.reconnectAttempts=0}catch(f){if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}i.connected=!1;const g=Math.min(ce*2**e,le)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(g)}ms: ${f?.message??f}
4
+ `),e+=1,i.reconnectAttempts=e,await I(g)}}}async function V(e,t,o,a={}){const{fetchImpl:s,appendLine:r,hasInboxEntryId:c,getToken:l,wakeCodex:f,heartbeatTimeoutMs:g,hwmDivergenceGraceMs:C,abortSignal:p,onInboxReceipt:U}={...K,...a},O=await l(),$={Authorization:`Bearer ${O}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&($["Last-Event-ID"]=t);const A=new AbortController,H=()=>{try{A.abort(p.reason??new Error("external abort"))}catch{}};p.aborted&&H(),p.addEventListener("abort",H,{once:!0});let m=null;const v=()=>{m&&clearTimeout(m),m=setTimeout(()=>{try{A.abort(new Error("heartbeat watchdog timeout"))}catch{}},g)};v();let P=t,u=null,w=null;const _=()=>{w&&(clearTimeout(w.timer),w=null)};let S=null;const T=(n,d)=>{const k={id:n,created_at:d};S&&d&&S.created_at&&y(k,S)<=0||(S=k,P=n,i.lastPersistedEventId=n,o(n))},L=n=>{n&&(u=!u||y(n,u)>0?n:u,w&&y(u,w.hwm)>=0&&_())},q=n=>{if(w?.hwm.id===n.id)return;_();const d=setTimeout(()=>{if(u&&y(u,n)>=0){_();return}try{A.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},C);w={hwm:n,timer:d}},M=new Set,D=[];let W=t!==null;const F=async n=>{const d=Ie(be(n.data,n.id));return W&&await c(e.cubeId,e.droneId,n.id,d)?(T(n.id,n.data?.created_at??""),"persisted-skip"):(await r(e.cubeId,e.droneId,d),f(Y(d)),U(e,O),"written")},N=n=>{for(M.add(n.id),D.push(n.id);D.length>de;){const d=D.shift();d&&M.delete(d)}T(n.id,n.data?.created_at??""),L(J(n))};let E;try{E=await s(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:$,signal:A.signal})}catch(n){throw m&&clearTimeout(m),n}if(!E.ok||!E.body)throw m&&clearTimeout(m),new Error(`stream HTTP ${E.status}`);i.connected=!0;try{for await(const n of he(E.body)){v();const d=new Date().toISOString();if(i.lastWireActivityAt=d,(n.type==="log"||n.type==="bookmark")&&(i.lastContentEventAt=d),n.type==="heartbeat"){if(i.lastHeartbeatAt=d,n.hwm&&u===null){L(n.hwm),P===null&&T(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&u&&y(n.hwm,u)<=0){_();continue}n.hwm&&u&&y(n.hwm,u)>0&&q(n.hwm);continue}if(n.type==="bookmark"){W=!1;continue}if(n.type==="log"){if(M.has(n.id)){T(n.id,n.data?.created_at??""),L(J(n));continue}const k=typeof n.data?.message=="string"&&n.data.message.startsWith("[HEARTBEAT-PING]");if(n.data?.kind==="ack"){if(n.data?.author_drone_id===e.droneId&&await F(n)==="persisted-skip")continue;N(n);continue}if(n.data?.drone_id===e.droneId&&!k){N(n);continue}if(await F(n)==="persisted-skip")continue;N(n)}}}finally{p.removeEventListener("abort",H),m&&clearTimeout(m),_(),i.connected=!1}}async function We(e,t,o,a={}){const{ownerDeps:s,ownerStaleMs:r}={...K,...a},c=await G(e.cubeId,e.droneId,r,s);if(!c)return i.connected=!1,i.ownership=await b(e.cubeId,e.droneId,s),"skipped";i.ownership=await b(e.cubeId,e.droneId,s);try{return await V(e,t,o,a),"streamed"}finally{await c.release()}}async function*he(e){const t=e.getReader(),o=new TextDecoder;let a="";try{for(;;){const{value:s,done:r}=await t.read();if(r){if(a.trim()){const l=X(a);l&&(yield l)}return}a+=o.decode(s,{stream:!0});let c;for(;(c=a.indexOf(`
5
5
 
6
- `))!==-1;){const l=a.slice(0,c);a=a.slice(c+2);const f=V(l);f&&(yield f)}}}finally{try{t.releaseLock()}catch{}}}function V(e){let t=null,i=null,a=[];for(const r of e.split(`
7
- `))r.startsWith("event:")?t=r.slice(6).trim():r.startsWith("id:")?i=r.slice(3).trim():r.startsWith("data:")&&a.push(r.slice(5).trim());const s=a.join(`
8
- `);if(!t)return null;if(t==="log"){if(!i)return null;let r;try{r=JSON.parse(s)}catch{return null}return{type:"log",id:i,data:r}}if(t==="heartbeat"){let r=null,c=null;try{const l=JSON.parse(s);r=typeof l.ts=="string"?l.ts:null,c=he(l.hwm)}catch{}return{type:"heartbeat",ts:r,hwm:c}}if(t==="bookmark"){let r=null;try{const c=JSON.parse(s);r=typeof c.as_of=="string"?c.as_of:null}catch{}return{type:"bookmark",as_of:r}}return{type:"unknown",raw:e}}function he(e){if(!e||typeof e!="object")return null;const t=e;return typeof t.id=="string"&&t.id.length>0&&typeof t.created_at=="string"&&t.created_at.length>0?{id:t.id,created_at:t.created_at}:null}function b(e,t){const i=Date.parse(e.created_at),a=Date.parse(t.created_at);return Number.isFinite(i)&&Number.isFinite(a)&&i!==a?i-a:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function j(e){if(e.data?.visibility==="direct"||e.data?.kind==="ack")return null;const t=e.data?.created_at;return typeof t=="string"&&t.length>0?{id:e.id,created_at:t}:null}function ge(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function be(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function ye(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),i=e.drone_label??"?",a=e.role_name??"?",s=typeof e.message=="string"?e.message:"",r=be(e.id,e.entry_id),c=r?`[entry_id: ${r}] `:"",l=s.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${i} (${a}): ${c}${l}`}async function Ie(e,t,i){const a=W(e,t);await D.mkdir(q.dirname(a),{recursive:!0}),await D.appendFile(a,i+`
9
- `,"utf-8")}function _e(e,t,i){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const a=t?i.replace(`[entry_id: ${t}] `,""):i;return!!(a&&e.split(/\r?\n/).includes(a))}async function Ee(e,t,i,a){const s=W(e,t);let r;try{r=await D.readFile(s,"utf-8")}catch(c){if(c?.code==="ENOENT")return!1;throw c}return _e(r,i,a)}function y(e){return new Promise(t=>setTimeout(t,e))}export{Re as __resetStreamStateForTest,fe as classifyRunLoopHealth,b as compareBroadcastHwm,ye as formatInboxLine,De as getStreamStatus,_e as inboxRawHasEntry,we as parseSSE,Ne as startLogStream,K as streamOnce,Oe as streamOnceIfOwner};
6
+ `))!==-1;){const l=a.slice(0,c);a=a.slice(c+2);const f=X(l);f&&(yield f)}}}finally{try{t.releaseLock()}catch{}}}function X(e){let t=null,o=null,a=[];for(const r of e.split(`
7
+ `))r.startsWith("event:")?t=r.slice(6).trim():r.startsWith("id:")?o=r.slice(3).trim():r.startsWith("data:")&&a.push(r.slice(5).trim());const s=a.join(`
8
+ `);if(!t)return null;if(t==="log"){if(!o)return null;let r;try{r=JSON.parse(s)}catch{return null}return{type:"log",id:o,data:r}}if(t==="heartbeat"){let r=null,c=null;try{const l=JSON.parse(s);r=typeof l.ts=="string"?l.ts:null,c=ge(l.hwm)}catch{}return{type:"heartbeat",ts:r,hwm:c}}if(t==="bookmark"){let r=null;try{const c=JSON.parse(s);r=typeof c.as_of=="string"?c.as_of:null}catch{}return{type:"bookmark",as_of:r}}return{type:"unknown",raw:e}}function ge(e){if(!e||typeof e!="object")return null;const t=e;return typeof t.id=="string"&&t.id.length>0&&typeof t.created_at=="string"&&t.created_at.length>0?{id:t.id,created_at:t.created_at}:null}function y(e,t){const o=Date.parse(e.created_at),a=Date.parse(t.created_at);return Number.isFinite(o)&&Number.isFinite(a)&&o!==a?o-a:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function J(e){if(e.data?.visibility==="direct"||e.data?.kind==="ack")return null;const t=e.data?.created_at;return typeof t=="string"&&t.length>0?{id:e.id,created_at:t}:null}function be(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function ye(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function Ie(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),o=e.drone_label??"?",a=e.role_name??"?",s=typeof e.message=="string"?e.message:"",r=ye(e.id,e.entry_id),c=r?`[entry_id: ${r}] `:"",l=s.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${o} (${a}): ${c}${l}`}async function _e(e,t,o){const a=B(e,t);await Ee(a,o,R)}async function Ee(e,t,o=R,a=o*2){await h.mkdir(x.dirname(e),{recursive:!0}),await h.appendFile(e,t+`
9
+ `,"utf-8"),await Ae(e,o,a)}async function Ae(e,t,o=t){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");if(!Number.isInteger(o)||o<t)throw new Error("trimThresholdLines must be an integer >= maxLines");const s=(await h.readFile(e,"utf-8")).split(/\r?\n/);if(s.at(-1)===""&&s.pop(),s.length<=o)return;const r=s.slice(-t),c=x.join(x.dirname(e),`.${x.basename(e)}.${process.pid}.${Date.now()}.tmp`);try{await h.writeFile(c,r.join(`
10
+ `)+`
11
+ `,"utf-8"),await h.rename(c,e)}catch(l){throw await h.rm(c,{force:!0}),l}}function Se(e,t,o){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const a=t?o.replace(`[entry_id: ${t}] `,""):o;return!!(a&&e.split(/\r?\n/).includes(a))}async function Te(e,t,o,a){const s=B(e,t);let r;try{r=await h.readFile(s,"utf-8")}catch(c){if(c?.code==="ENOENT")return!1;throw c}return Se(r,o,a)}function I(e){return new Promise(t=>setTimeout(t,e))}export{R as INBOX_TAIL_LINES_CAP,Oe as INBOX_TAIL_TRIM_THRESHOLD_LINES,ve as __resetStreamStateForTest,Ee as appendCappedInboxLine,pe as classifyRunLoopHealth,y as compareBroadcastHwm,Ie as formatInboxLine,$e as getStreamStatus,Se as inboxRawHasEntry,he as parseSSE,Pe as startLogStream,V as streamOnce,We as streamOnceIfOwner,Ae as trimInboxFileToRecentLines};
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  const o=process.env.npm_config_global==="true";o||(console.error(`
3
3
  \u25FC Error: borg must be installed globally
4
- `),console.error("Please install with:"),console.error(` npm install -g borg@beta
4
+ `),console.error("Please install with:"),console.error(` npm install -g borgmcp
5
5
  `),console.error(`Local installation is not supported.
6
6
  `),process.exit(1)),console.log(`
7
7
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`),console.log("\u2551 \u25FC Borg MCP Installed \u25FC \u2551"),console.log(`\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
8
- `),console.log("Next step:"),console.log(` borg-setup
8
+ `),console.log("Next step:"),console.log(` borg setup
9
9
  `);
@@ -145,6 +145,7 @@ export declare function readLog(sessionToken: string, apiUrl: string, opts?: {
145
145
  drones: any[];
146
146
  roles: any[];
147
147
  behind_by?: number;
148
+ has_more?: boolean;
148
149
  }>;
149
150
  /**
150
151
  * Sprint 25 log substrate refactor: explicit ack on a log entry.
@@ -352,6 +353,16 @@ export declare function deleteRole(roleId: string): Promise<void>;
352
353
  export declare function reassignDrone(droneId: string, roleId: string): Promise<{
353
354
  drone: any;
354
355
  }>;
356
+ /**
357
+ * Evict (soft-delete) a drone from its cube (gh#718). Owner-authed via the
358
+ * Bearer token, exactly like reassignDrone — the worker's `DELETE
359
+ * /api/drones/:id` route scopes the delete to cubes the caller owns
360
+ * (CubeStore.evictDrone RLS owner-scope), so a non-owner can never evict
361
+ * another account's drone. The drone row is preserved with `evicted_at` set
362
+ * and its activity-log attribution anonymized; the route returns 204 No
363
+ * Content (no body).
364
+ */
365
+ export declare function evictDrone(droneId: string): Promise<void>;
355
366
  /**
356
367
  * Fetch a cube's full detail: directive, roles (with detailed
357
368
  * descriptions), and drones. Accessible to owners and active members via
@@ -1 +1 @@
1
- import{getIdToken as y,getRefreshToken as w,clearTokens as h}from"./config.js";import{refreshIdToken as T,RefreshTokenInvalidError as g,RefreshTransientError as b}from"./auth.js";import{consolePrefix as j}from"./console-prefix.js";import{debugLog as m}from"./debug.js";const S=process.env.BORG_API_URL||"https://api.borgmcp.ai",_=3,R=6e4;function E(e){if(e==null)return null;const n=e.trim();return/^\d+$/.test(n)?parseInt(n,10)*1e3:null}function C(e,n,t=R,o=()=>Math.random()*500){const s=e??1e3*(n+1);return Math.min(s,t)+o()}function P(e){const n=(t,o)=>`${t}${/[.:!?]$/.test(t)?"":":"} ${o}`;try{const t=JSON.parse(e);if(typeof t?.error=="string")return typeof t.details=="string"?n(t.error,t.details):t.error;if(t?.error&&typeof t.error=="object"){const o=t.error.message,s=t.error.details??t.details;if(typeof o=="string"&&typeof s=="string")return n(o,s);if(typeof o=="string")return o}if(typeof t?.message=="string"&&typeof t?.details=="string")return n(t.message,t.details);if(typeof t?.message=="string")return t.message}catch{}return e}async function k(e,n,t){const o=t.maxRetries??_;let s=e,a=0;for(;s.status===429&&a<o;){const p=C(E(s.headers.get("Retry-After")),a,t.capMs,t.jitter);t.log?.(`rate limited (429); retrying in ${Math.round(p)}ms (attempt ${a+1}/${o})`),await t.sleep(p),a++,s=await n()}return s}function O(e){return new Promise(n=>setTimeout(n,e))}async function I(){let e=await y();if(!e){const n=await w();if(n)try{await T(n),e=await y()}catch(t){if(t instanceof g&&await h(),t instanceof b)throw t}if(!e)throw new Error("Authentication required. Run: borg assimilate")}return e}async function A(){const e=await w();if(!e)return null;try{return await T(e),await y()}catch(n){if(n instanceof g&&await h(),n instanceof b)throw n;return null}}async function r(e,n={}){let t=await I();const{droneSession:o,apiUrl:s,headers:a,...p}=n,$=s??S,l=(p.method??"GET").toUpperCase(),f=async c=>{const u={Authorization:`Bearer ${c}`,...a};o&&(u["X-Drone-Session"]=o),m(`\u2192 ${l} ${e}`);const d=await fetch(`${$}${e}`,{...p,headers:u});return m(`\u2190 ${d.status} ${l} ${e}`),d};let i=await f(t);if(i.status===401){const c=await A();c&&(t=c,i=await f(t))}if(i.status===401)throw new Error("Authentication required. Run: borg assimilate");if(i.status===429&&(i=await k(i,()=>f(t),{sleep:O,log:c=>console.error(`${j()}${c}`)})),!i.ok){const c=await i.text();m(`\u2717 ${i.status} ${l} ${e}: ${c}`);const u=P(c);if(i.status===429){const d=i.headers.get("Retry-After"),x=d?` (retry after ${d}s)`:"";throw new Error(`HTTP 429: rate limited${x}: ${u}`)}throw new Error(`HTTP ${i.status}: ${u}`)}return i}async function U(e,n,t,o){const s={hostname:t??null};return(o==="claude"||o==="codex")&&(s.agent_kind=o),typeof e=="string"?s.cube_name=e:(e.cube_id&&(s.cube_id=e.cube_id),e.cube_name&&(s.cube_name=e.cube_name),e.role_id&&(s.role_id=e.role_id),e.role_name&&(s.role_name=e.role_name)),await(await r("/api/assimilate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s),apiUrl:n})).json()}async function D(e,n){return await(await r("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:n})).json()}async function v(e,n){return await(await r("/api/drone/role",{method:"GET",droneSession:e,apiUrl:n})).json()}async function H(e,n){return await(await r("/api/drone/whoami",{method:"GET",droneSession:e,apiUrl:n})).json()}async function q(e,n,t){const o=t?`?since=${encodeURIComponent(t)}`:"";return await(await r(`/api/drone/roster${o}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function N(e,n,t={}){const o=new URLSearchParams;t.since&&o.set("since",t.since),t.limit!==void 0&&o.set("limit",String(t.limit)),t.unreadOnly&&o.set("unread_only","true");const s=o.toString();return await(await r(`/api/drone/log${s?`?${s}`:""}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function B(e,n,t){await r(`/api/drone/log/${t}/ack`,{method:"POST",body:JSON.stringify({kind:"ack"}),droneSession:e,apiUrl:n})}async function X(e,n,t={}){const o=new URLSearchParams;t.since&&o.set("since",t.since);const s=o.toString();return await(await r(`/api/drone/regen${s?`?${s}`:""}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function W(e,n,t,o){const s=new URLSearchParams({role:t,section:o});return await(await r(`/api/drone/role-rationale?${s.toString()}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function z(e,n,t,o={}){const s={message:t,...o.visibility?{visibility:o.visibility}:{},...o.recipientDroneIds?{recipientDroneIds:o.recipientDroneIds}:{},...o.class?{class:o.class}:{},...o.to?{to:o.to}:{}};return await(await r("/api/drone/log",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:n,body:JSON.stringify(s)})).json()}async function F(){return await(await r("/api/cubes",{method:"GET"})).json()}async function Q(){return await(await r("/api/templates",{method:"GET"})).json()}async function V(e,n,t){const o={cube_directive:n};e&&(o.name=e),t?.template&&(o.template=t.template),t&&Object.prototype.hasOwnProperty.call(t,"message_taxonomy")&&(o.message_taxonomy=t.message_taxonomy??null);const a=await(await r("/api/cubes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();return a.cube?{...a.cube,roles:a.roles??[],drones:a.drones??[]}:a}async function Y(e,n){return await(await r(`/api/cubes/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function Z(e,n){return await(await r(`/api/cubes/${e}/taxonomy-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function K(e){await r(`/api/cubes/${e}`,{method:"DELETE"})}async function ee(e,n){return await(await r(`/api/cubes/${e}/roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function te(e,n){return await(await r(`/api/roles/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ne(e,n){return await(await r(`/api/roles/${e}/section-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function oe(e){await r(`/api/roles/${e}`,{method:"DELETE"})}async function se(e,n){return await(await r(`/api/drones/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role_id:n})})).json()}async function re(e){const t=await(await r(`/api/cubes/${e}`,{method:"GET"})).json();return t.cube?{...t.cube,roles:t.roles??[],drones:t.drones??[]}:t}async function ae(e,n){return await(await r(`/api/cubes/${e}/apply-template`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:n})})).json()}async function ie(){return await(await r("/api/subscription/status",{method:"GET"})).json()}async function ce(e,n="software-dev",t=!1,o){return await(await r(`/api/cubes/${e}/sync-roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:n,apply:t,...o?{decisions:o}:{}})})).json()}async function pe(){const n=await(await r("/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!n.checkout_url)throw new Error("No checkout URL in response");return n.checkout_url}async function ue(){const n=await(await r("/api/subscription/portal",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!n.portal_url)throw new Error(n.message||"No portal URL in response");return n.portal_url}export{S as API_URL,B as ackLogEntry,z as appendLog,ae as applyTemplate,U as assimilate,ie as checkSubscriptionStatus,ue as createBillingPortalSession,V as createCube,ee as createRole,pe as createSubscription,K as deleteCube,oe as deleteRole,P as extractHttpErrorMessage,re as getCube,D as getCubeInfo,v as getRoleInfo,q as getRoster,I as getValidToken,F as listCubes,Q as listTemplates,E as parseRetryAfterMs,ne as patchRoleSection,Z as patchTaxonomyClass,C as rateLimitWaitMs,N as readLog,se as reassignDrone,X as regen,k as retryOn429,W as roleRationale,ce as syncRoles,Y as updateCube,te as updateRole,H as whoami};
1
+ import{getIdToken as y,getRefreshToken as w,clearTokens as h}from"./config.js";import{refreshIdToken as T,RefreshTokenInvalidError as g,RefreshTransientError as b}from"./auth.js";import{consolePrefix as S}from"./console-prefix.js";import{debugLog as m}from"./debug.js";import{assertUuidShape as $}from"./evict-drone.js";const _=process.env.BORG_API_URL||"https://api.borgmcp.ai",E=3,R=6e4;function C(e){if(e==null)return null;const n=e.trim();return/^\d+$/.test(n)?parseInt(n,10)*1e3:null}function P(e,n,t=R,o=()=>Math.random()*500){const s=e??1e3*(n+1);return Math.min(s,t)+o()}function k(e){const n=(t,o)=>`${t}${/[.:!?]$/.test(t)?"":":"} ${o}`;try{const t=JSON.parse(e);if(typeof t?.error=="string")return typeof t.details=="string"?n(t.error,t.details):t.error;if(t?.error&&typeof t.error=="object"){const o=t.error.message,s=t.error.details??t.details;if(typeof o=="string"&&typeof s=="string")return n(o,s);if(typeof o=="string")return o}if(typeof t?.message=="string"&&typeof t?.details=="string")return n(t.message,t.details);if(typeof t?.message=="string")return t.message}catch{}return e}async function O(e,n,t){const o=t.maxRetries??E;let s=e,a=0;for(;s.status===429&&a<o;){const p=P(C(s.headers.get("Retry-After")),a,t.capMs,t.jitter);t.log?.(`rate limited (429); retrying in ${Math.round(p)}ms (attempt ${a+1}/${o})`),await t.sleep(p),a++,s=await n()}return s}function L(e){return new Promise(n=>setTimeout(n,e))}async function A(){let e=await y();if(!e){const n=await w();if(n)try{await T(n),e=await y()}catch(t){if(t instanceof g&&await h(),t instanceof b)throw t}if(!e)throw new Error("Authentication required. Run: borg assimilate")}return e}async function I(){const e=await w();if(!e)return null;try{return await T(e),await y()}catch(n){if(n instanceof g&&await h(),n instanceof b)throw n;return null}}async function r(e,n={}){let t=await A();const{droneSession:o,apiUrl:s,headers:a,...p}=n,x=s??_,f=(p.method??"GET").toUpperCase(),l=async c=>{const u={Authorization:`Bearer ${c}`,...a};o&&(u["X-Drone-Session"]=o),m(`\u2192 ${f} ${e}`);const d=await fetch(`${x}${e}`,{...p,headers:u});return m(`\u2190 ${d.status} ${f} ${e}`),d};let i=await l(t);if(i.status===401){const c=await I();c&&(t=c,i=await l(t))}if(i.status===401)throw new Error("Authentication required. Run: borg assimilate");if(i.status===429&&(i=await O(i,()=>l(t),{sleep:L,log:c=>console.error(`${S()}${c}`)})),!i.ok){const c=await i.text();m(`\u2717 ${i.status} ${f} ${e}: ${c}`);const u=k(c);if(i.status===429){const d=i.headers.get("Retry-After"),j=d?` (retry after ${d}s)`:"";throw new Error(`HTTP 429: rate limited${j}: ${u}`)}throw new Error(`HTTP ${i.status}: ${u}`)}return i}async function v(e,n,t,o){const s={hostname:t??null};return(o==="claude"||o==="codex")&&(s.agent_kind=o),typeof e=="string"?s.cube_name=e:(e.cube_id&&(s.cube_id=e.cube_id),e.cube_name&&(s.cube_name=e.cube_name),e.role_id&&(s.role_id=e.role_id),e.role_name&&(s.role_name=e.role_name)),await(await r("/api/assimilate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s),apiUrl:n})).json()}async function H(e,n){return await(await r("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:n})).json()}async function q(e,n){return await(await r("/api/drone/role",{method:"GET",droneSession:e,apiUrl:n})).json()}async function N(e,n){return await(await r("/api/drone/whoami",{method:"GET",droneSession:e,apiUrl:n})).json()}async function B(e,n,t){const o=t?`?since=${encodeURIComponent(t)}`:"";return await(await r(`/api/drone/roster${o}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function X(e,n,t={}){const o=new URLSearchParams;t.since&&o.set("since",t.since),t.limit!==void 0&&o.set("limit",String(t.limit)),t.unreadOnly&&o.set("unread_only","true");const s=o.toString();return await(await r(`/api/drone/log${s?`?${s}`:""}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function W(e,n,t){await r(`/api/drone/log/${t}/ack`,{method:"POST",body:JSON.stringify({kind:"ack"}),droneSession:e,apiUrl:n})}async function z(e,n,t={}){const o=new URLSearchParams;t.since&&o.set("since",t.since);const s=o.toString();return await(await r(`/api/drone/regen${s?`?${s}`:""}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function F(e,n,t,o){const s=new URLSearchParams({role:t,section:o});return await(await r(`/api/drone/role-rationale?${s.toString()}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function Q(e,n,t,o={}){const s={message:t,...o.visibility?{visibility:o.visibility}:{},...o.recipientDroneIds?{recipientDroneIds:o.recipientDroneIds}:{},...o.class?{class:o.class}:{},...o.to?{to:o.to}:{}};return await(await r("/api/drone/log",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:n,body:JSON.stringify(s)})).json()}async function V(){return await(await r("/api/cubes",{method:"GET"})).json()}async function Y(){return await(await r("/api/templates",{method:"GET"})).json()}async function Z(e,n,t){const o={cube_directive:n};e&&(o.name=e),t?.template&&(o.template=t.template),t&&Object.prototype.hasOwnProperty.call(t,"message_taxonomy")&&(o.message_taxonomy=t.message_taxonomy??null);const a=await(await r("/api/cubes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();return a.cube?{...a.cube,roles:a.roles??[],drones:a.drones??[]}:a}async function K(e,n){return await(await r(`/api/cubes/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ee(e,n){return await(await r(`/api/cubes/${e}/taxonomy-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function te(e){await r(`/api/cubes/${e}`,{method:"DELETE"})}async function ne(e,n){return await(await r(`/api/cubes/${e}/roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function oe(e,n){return await(await r(`/api/roles/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function se(e,n){return await(await r(`/api/roles/${e}/section-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function re(e){await r(`/api/roles/${e}`,{method:"DELETE"})}async function ae(e,n){return $(e,"drone_id"),await(await r(`/api/drones/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role_id:n})})).json()}async function ie(e){$(e,"drone_id"),await r(`/api/drones/${e}`,{method:"DELETE"})}async function ce(e){const t=await(await r(`/api/cubes/${e}`,{method:"GET"})).json();return t.cube?{...t.cube,roles:t.roles??[],drones:t.drones??[]}:t}async function pe(e,n){return await(await r(`/api/cubes/${e}/apply-template`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:n})})).json()}async function ue(){return await(await r("/api/subscription/status",{method:"GET"})).json()}async function de(e,n="software-dev",t=!1,o){return await(await r(`/api/cubes/${e}/sync-roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:n,apply:t,...o?{decisions:o}:{}})})).json()}async function fe(){const n=await(await r("/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!n.checkout_url)throw new Error("No checkout URL in response");return n.checkout_url}async function le(){const n=await(await r("/api/subscription/portal",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!n.portal_url)throw new Error(n.message||"No portal URL in response");return n.portal_url}export{_ as API_URL,W as ackLogEntry,Q as appendLog,pe as applyTemplate,v as assimilate,ue as checkSubscriptionStatus,le as createBillingPortalSession,Z as createCube,ne as createRole,fe as createSubscription,te as deleteCube,re as deleteRole,ie as evictDrone,k as extractHttpErrorMessage,ce as getCube,H as getCubeInfo,q as getRoleInfo,B as getRoster,A as getValidToken,V as listCubes,Y as listTemplates,C as parseRetryAfterMs,se as patchRoleSection,ee as patchTaxonomyClass,P as rateLimitWaitMs,X as readLog,ae as reassignDrone,z as regen,O as retryOn429,F as roleRationale,de as syncRoles,K as updateCube,oe as updateRole,N as whoami};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code and Codex. Create projects, assign roles, and share a live activity log.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",