borgmcp 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,33 @@
1
+ PROPRIETARY LICENSE
2
+
3
+ Copyright © 2025 Byte Ventures IO AB
4
+ All rights reserved.
5
+
6
+ NOTICE: This software and associated documentation files (the "Software") are
7
+ proprietary and confidential to Byte Ventures IO AB.
8
+
9
+ RESTRICTIONS:
10
+ 1. You may NOT copy, modify, merge, publish, distribute, sublicense, and/or
11
+ sell copies of the Software.
12
+ 2. You may NOT use the Software for any purpose without express written
13
+ permission from Byte Ventures IO AB.
14
+ 3. You may NOT reverse engineer, decompile, or disassemble the Software.
15
+ 4. You may NOT remove or alter any proprietary notices or labels on the Software.
16
+
17
+ PERMITTED USE:
18
+ - Authorized users may use the Software solely for accessing the Borg MCP service
19
+ - Use is subject to subscription terms and conditions
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
24
+ BYTE VENTURES IO AB BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
26
+ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+
28
+ For licensing inquiries, contact:
29
+ queen@borgmcp.ai
30
+
31
+ Byte Ventures IO AB
32
+ Sweden
33
+ https://byteventures.se
package/README.md CHANGED
@@ -15,10 +15,12 @@ Borg MCP lets Claude Code and Codex sessions join the same project coordination
15
15
 
16
16
  ## Pricing
17
17
 
18
- - Free tier: 1 cube, 3 drones, 100 requests/hour.
19
- - Cube tier: $1/cube/month, unlimited cubes, unlimited drones, 1000 requests/hour.
18
+ - Free tier: 1 cube, 3 agent sessions, 100 requests/hour.
19
+ - Cube tier: $1/month per cube; each cube adds 8 pooled agent sessions and 1000 requests/hour.
20
20
 
21
- Free tier is permanent. No trial is required.
21
+ Free tier is permanent. No trial is required. An active subscription bills
22
+ for at least 1 cube (minimum quantity), even with zero cubes — cancel the
23
+ subscription to stop billing.
22
24
 
23
25
  ## Install
24
26
 
@@ -179,6 +181,12 @@ Build the CLI package:
179
181
  npm run build:cli
180
182
  ```
181
183
 
184
+ ## License
185
+
186
+ Proprietary — Copyright © Byte Ventures IO AB, all rights reserved. See
187
+ [LICENSE](./LICENSE) for the full terms. For licensing inquiries, contact
188
+ <queen@borgmcp.ai>.
189
+
182
190
  ## Links
183
191
 
184
192
  - Product site: <https://borgmcp.ai>
package/dist/auth.js CHANGED
@@ -1,12 +1,12 @@
1
- import{createServer as m}from"http";import{URL as k}from"url";import _ from"crypto";import b from"open";import{storeIdToken as u,storeRefreshToken as d,getRefreshToken as p}from"./config.js";import{cerr as r}from"./console-prefix.js";import{isNoBrowserEnv as R}from"./auth-env.js";import{requestDeviceCode as P,pollForDeviceToken as O}from"./device-auth.js";class x extends Error{errorCode;errorDescription;constructor(t,s){super(s?`Refresh token invalid (${t}): ${s}`:`Refresh token invalid (${t})`),this.errorCode=t,this.errorDescription=s,this.name="RefreshTokenInvalidError"}}class l extends Error{constructor(t){super(t),this.name="RefreshTransientError"}}const w="675073910799-41pbe12rfhqemidh64h09s4q3e0udpgp.apps.googleusercontent.com",g="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",I="675073910799-6qmi73v5106dj1v0l22j2qnkh5r3e8fq.apps.googleusercontent.com",G="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",S="https://accounts.google.com/o/oauth2/v2/auth",E="https://oauth2.googleapis.com/token",A="https://oauth2.googleapis.com/revoke",T=["openid","email","profile"],J=8e3,X=9e3;function L(){const e=_.randomBytes(32).toString("base64url"),t=_.createHash("sha256").update(e).digest("base64url");return{verifier:e,challenge:t}}async function D(){return new Promise((e,t)=>{const s=m();s.listen(0,()=>{const i=s.address();if(i&&typeof i=="object"){const o=i.port;s.close(()=>e(o))}else s.close(()=>t(new Error("Failed to get assigned port")))}),s.on("error",t)})}async function N(){const e=await D(),t=new Promise((s,i)=>{const o=m((n,c)=>{const a=new k(n.url,`http://localhost:${e}`);if(a.pathname==="/callback"){const h=a.searchParams.get("code"),f=a.searchParams.get("error");if(f){c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
1
+ import{createServer as m}from"http";import{URL as k}from"url";import _ from"crypto";import C from"open";import{storeIdToken as d,storeRefreshToken as f,getRefreshToken as p}from"./config.js";import{cerr as t}from"./console-prefix.js";import{isNoBrowserEnv as R}from"./auth-env.js";import{requestDeviceCode as P,pollForDeviceToken as O}from"./device-auth.js";class x extends Error{errorCode;errorDescription;constructor(r,s){super(s?`Refresh token invalid (${r}): ${s}`:`Refresh token invalid (${r})`),this.errorCode=r,this.errorDescription=s,this.name="RefreshTokenInvalidError"}}class u extends Error{constructor(r){super(r),this.name="RefreshTransientError"}}const w="675073910799-41pbe12rfhqemidh64h09s4q3e0udpgp.apps.googleusercontent.com",g="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",I="675073910799-6qmi73v5106dj1v0l22j2qnkh5r3e8fq.apps.googleusercontent.com",G="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",S="https://accounts.google.com/o/oauth2/v2/auth",E="https://oauth2.googleapis.com/token",L="https://oauth2.googleapis.com/revoke",T=["openid","email","profile"],J=8e3,X=9e3;function A(){const e=_.randomBytes(32).toString("base64url"),r=_.createHash("sha256").update(e).digest("base64url");return{verifier:e,challenge:r}}async function D(){return new Promise((e,r)=>{const s=m();s.listen(0,()=>{const i=s.address();if(i&&typeof i=="object"){const o=i.port;s.close(()=>e(o))}else s.close(()=>r(new Error("Failed to get assigned port")))}),s.on("error",r)})}async function $(){const e=await D(),r=new Promise((s,i)=>{const o=m((n,c)=>{const a=new k(n.url,`http://localhost:${e}`);if(a.pathname==="/callback"){const h=a.searchParams.get("code"),l=a.searchParams.get("error");if(l){c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
2
2
  <html>
3
3
  <body>
4
4
  <h1>\u25FC Authentication Failed</h1>
5
- <p>Error: ${f}</p>
5
+ <p>Error: ${l}</p>
6
6
  <p>You can close this window.</p>
7
7
  </body>
8
8
  </html>
9
- `),o.close(),i(new Error(`OAuth error: ${f}`));return}if(h){c.writeHead(200,{"Content-Type":"text/html"}),c.end(`
9
+ `),o.close(),i(new Error(`OAuth error: ${l}`));return}if(h){c.writeHead(200,{"Content-Type":"text/html"}),c.end(`
10
10
  <html>
11
11
  <body>
12
12
  <h1>\u25FC Authentication Successful!</h1>
@@ -20,19 +20,19 @@ import{createServer as m}from"http";import{URL as k}from"url";import _ from"cryp
20
20
  <p>Missing authorization code.</p>
21
21
  </body>
22
22
  </html>
23
- `),o.close(),i(new Error("Missing authorization code"))}});o.listen(e,()=>{r(`Callback server listening on http://localhost:${e}`)}),setTimeout(()=>{o.close(),i(new Error("Authentication timeout - no response received"))},300*1e3).unref()});return{port:e,codePromise:t}}async function $(e,t,s){const i=`http://localhost:${s}/callback`,o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:w,client_secret:g,code:e,code_verifier:t,grant_type:"authorization_code",redirect_uri:i})});if(!o.ok){const n=await o.text();throw new Error(`Failed to exchange code for tokens: ${n}`)}return await o.json()}async function v(e){try{await fetch(A,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`token=${encodeURIComponent(e)}`})}catch{}}async function U(){r(`
24
- \u25FC Borg MCP Authentication`),r(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
25
- `);const e=await p();e&&(r("Revoking previous refresh_token to force fresh consent..."),await v(e)),r("Generating PKCE challenge...");const t=L();r("Starting local callback server...");const{port:s,codePromise:i}=await N(),o=`http://localhost:${s}/callback`,n=new k(S);n.searchParams.set("client_id",w),n.searchParams.set("redirect_uri",o),n.searchParams.set("response_type","code"),n.searchParams.set("scope",T.join(" ")),n.searchParams.set("code_challenge",t.challenge),n.searchParams.set("code_challenge_method","S256"),n.searchParams.set("access_type","offline"),n.searchParams.set("prompt","consent select_account"),r(`
26
- \u{1F4F1} Opening browser for authorization...`),r("If browser does not open, visit:"),r(`${n.toString()}
27
- `),await b(n.toString()),r("Waiting for authorization...");const c=await i;r("Exchanging authorization code for tokens...");const a=await $(c,t.verifier,s),h=Date.now()+a.expires_in*1e3;await u(a.id_token,h),a.refresh_token?await d(a.refresh_token):(r(`
28
- \u26A0 No refresh_token returned by Google.`),r(" Your session will expire after ~1 hour and require"),r(" re-running `borg setup`. To enable auto-refresh:"),r(" 1. Visit https://myaccount.google.com/permissions"),r(' 2. Find "Borg MCP" and click "Remove access"'),r(" 3. Re-run `borg setup`"),r(` (Google will then issue a fresh refresh_token.)
29
- `)),r(`
23
+ `),o.close(),i(new Error("Missing authorization code"))}});o.listen(e,()=>{t(`Callback server listening on http://localhost:${e}`)}),setTimeout(()=>{o.close(),i(new Error("Authentication timeout - no response received"))},300*1e3).unref()});return{port:e,codePromise:r}}async function N(e,r,s){const i=`http://localhost:${s}/callback`,o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:w,client_secret:g,code:e,code_verifier:r,grant_type:"authorization_code",redirect_uri:i})});if(!o.ok){const n=await o.text();throw new Error(`Failed to exchange code for tokens: ${n}`)}return await o.json()}async function v(e){try{await fetch(L,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`token=${encodeURIComponent(e)}`})}catch{}}async function U(){t(`
24
+ \u25FC Borg MCP Authentication`),t(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
25
+ `);const e=await p();e&&(t("Revoking previous refresh_token to force fresh consent..."),await v(e)),t("Generating PKCE challenge...");const r=A();t("Starting local callback server...");const{port:s,codePromise:i}=await $(),o=`http://localhost:${s}/callback`,n=new k(S);n.searchParams.set("client_id",w),n.searchParams.set("redirect_uri",o),n.searchParams.set("response_type","code"),n.searchParams.set("scope",T.join(" ")),n.searchParams.set("code_challenge",r.challenge),n.searchParams.set("code_challenge_method","S256"),n.searchParams.set("access_type","offline"),n.searchParams.set("prompt","consent select_account"),t(`
26
+ \u{1F4F1} Opening browser for authorization...`),t("If browser does not open, visit:"),t(`${n.toString()}
27
+ `);try{await C(n.toString())}catch(l){t(`Could not open a browser automatically: ${l?.message??"unknown"}`),t("Continue by opening the URL above manually.")}t("Waiting for authorization...");const c=await i;t("Exchanging authorization code for tokens...");const a=await N(c,r.verifier,s),h=Date.now()+a.expires_in*1e3;await d(a.id_token,h),a.refresh_token?await f(a.refresh_token):(t(`
28
+ \u26A0 No refresh_token returned by Google.`),t(" Your session will expire after ~1 hour and require"),t(" re-running `borg setup`. To enable auto-refresh:"),t(" 1. Visit https://myaccount.google.com/permissions"),t(' 2. Find "Borg MCP" and click "Remove access"'),t(" 3. Re-run `borg setup`"),t(` (Google will then issue a fresh refresh_token.)
29
+ `)),t(`
30
30
  \u25FC Authentication successful!
31
- `)}function B(e){return e?.noBrowser??R()}function y(e=process.env){const t=e.GOOGLE_DEVICE_CLIENT_ID?.trim(),s=e.GOOGLE_DEVICE_CLIENT_SECRET?.trim()||void 0;let i,o;if(t?(i=t,o=s):(i=I,o=G||void 0),!i)throw new Error('No-browser (device-grant) auth needs a Google "TVs & Limited Input devices" OAuth client. Set GOOGLE_DEVICE_CLIENT_ID in the environment, or run `borg setup` on a machine with a browser. See docs/REMOTE_TERMINAL_AUTH.md.');return{clientId:i,clientSecret:o,scopes:T}}function M(e){return new Promise(t=>setTimeout(t,e))}async function q(e={fetch,sleep:M},t=process.env){r(`
32
- \u25FC Borg MCP Authentication (no-browser mode)`),r(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
33
- `);const s=y(t),i=await p();i&&(r("Revoking previous refresh_token to force fresh consent..."),await v(i));const o=await P(s,e);r("To authorize Borg MCP on this machine:"),r(` 1. On any device with a browser, open: ${o.verification_url}`),r(` 2. Enter this code: ${o.user_code}
34
- `),r("Waiting for authorization (this page can be open on your phone or laptop)...");const n=await O(o,s,e),c=Date.now()+n.expires_in*1e3;await u(n.id_token,c),n.refresh_token?await d(n.refresh_token):(r(`
35
- \u26A0 No refresh_token returned by Google.`),r(" Your session will expire after ~1 hour and require re-running"),r(" `borg setup`. Re-consent at https://myaccount.google.com/permissions"),r(` (remove "Borg MCP") then re-run setup to restore automatic token refresh.
36
- `)),r(`
31
+ `)}function B(e){return e?.noBrowser??R()}function y(e=process.env){const r=e.GOOGLE_DEVICE_CLIENT_ID?.trim(),s=e.GOOGLE_DEVICE_CLIENT_SECRET?.trim()||void 0;let i,o;if(r?(i=r,o=s):(i=I,o=G||void 0),!i)throw new Error('No-browser (device-grant) auth needs a Google "TVs & Limited Input devices" OAuth client. Set GOOGLE_DEVICE_CLIENT_ID in the environment, or run `borg setup` on a machine with a browser. See docs/REMOTE_TERMINAL_AUTH.md.');return{clientId:i,clientSecret:o,scopes:T}}function M(e){return new Promise(r=>setTimeout(r,e))}async function q(e={fetch,sleep:M},r=process.env){t(`
32
+ \u25FC Borg MCP Authentication (no-browser mode)`),t(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
33
+ `);const s=y(r),i=await p();i&&(t("Revoking previous refresh_token to force fresh consent..."),await v(i));const o=await P(s,e);t("To authorize Borg MCP on this machine:"),t(` 1. On any device with a browser, open: ${o.verification_url}`),t(` 2. Enter this code: ${o.user_code}
34
+ `),t("Waiting for authorization (this page can be open on your phone or laptop)...");const n=await O(o,s,e),c=Date.now()+n.expires_in*1e3;await d(n.id_token,c),n.refresh_token?await f(n.refresh_token):(t(`
35
+ \u26A0 No refresh_token returned by Google.`),t(" Your session will expire after ~1 hour and require re-running"),t(" `borg setup`. Re-consent at https://myaccount.google.com/permissions"),t(` (remove "Borg MCP") then re-run setup to restore automatic token refresh.
36
+ `)),t(`
37
37
  \u25FC Authentication successful!
38
- `)}async function Q(e){return B(e)?q():U()}async function C(e,t,s){const i={client_id:t,refresh_token:e,grant_type:"refresh_token"};s&&(i.client_secret=s);let o;try{o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(i)})}catch(a){return{ok:!1,kind:"transient",error:new l(`Network failure during token refresh: ${a?.message??"unknown"}`)}}if(!o.ok){let a=null;try{a=await o.json()}catch{return{ok:!1,kind:"transient",error:new l(`Token refresh failed with HTTP ${o.status} (non-JSON body)`)}}return o.status===400&&a?.error==="invalid_grant"?{ok:!1,kind:"invalid",error:new x("invalid_grant",a.error_description)}:{ok:!1,kind:"transient",error:new l(`Token refresh failed with HTTP ${o.status}${a?.error?` (${a.error})`:""}`)}}let n;try{n=await o.json()}catch(a){return{ok:!1,kind:"transient",error:new l(`Token refresh response unparseable: ${a?.message??"unknown"}`)}}if(!n.id_token||typeof n.expires_in!="number")return{ok:!1,kind:"transient",error:new l("Token refresh response missing id_token or expires_in")};const c=Date.now()+n.expires_in*1e3;if(n.refresh_token){const a=await p();await d(n.refresh_token);try{await u(n.id_token,c)}catch(h){if(a)try{await d(a)}catch{}throw h}return{ok:!0}}return await u(n.id_token,c),{ok:!0}}async function Z(e){const t=await C(e,w,g);if(t.ok)return;const s=y(),i=await C(e,s.clientId,s.clientSecret);if(!i.ok)throw t.kind==="invalid"&&i.kind==="invalid"?i.error:t.kind==="transient"?t.error:i.error}export{x as RefreshTokenInvalidError,l as RefreshTransientError,q as authenticateWithDeviceFlow,Q as authenticateWithGoogle,y as buildDeviceAuthConfig,Z as refreshIdToken,B as shouldUseDeviceFlow};
38
+ `)}async function Q(e){return B(e)?q():U()}async function b(e,r,s){const i={client_id:r,refresh_token:e,grant_type:"refresh_token"};s&&(i.client_secret=s);let o;try{o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(i)})}catch(a){return{ok:!1,kind:"transient",error:new u(`Network failure during token refresh: ${a?.message??"unknown"}`)}}if(!o.ok){let a=null;try{a=await o.json()}catch{return{ok:!1,kind:"transient",error:new u(`Token refresh failed with HTTP ${o.status} (non-JSON body)`)}}return o.status===400&&a?.error==="invalid_grant"?{ok:!1,kind:"invalid",error:new x("invalid_grant",a.error_description)}:{ok:!1,kind:"transient",error:new u(`Token refresh failed with HTTP ${o.status}${a?.error?` (${a.error})`:""}`)}}let n;try{n=await o.json()}catch(a){return{ok:!1,kind:"transient",error:new u(`Token refresh response unparseable: ${a?.message??"unknown"}`)}}if(!n.id_token||typeof n.expires_in!="number")return{ok:!1,kind:"transient",error:new u("Token refresh response missing id_token or expires_in")};const c=Date.now()+n.expires_in*1e3;if(n.refresh_token){const a=await p();await f(n.refresh_token);try{await d(n.id_token,c)}catch(h){if(a)try{await f(a)}catch{}throw h}return{ok:!0}}return await d(n.id_token,c),{ok:!0}}async function Z(e){const r=await b(e,w,g);if(r.ok)return;const s=y(),i=await b(e,s.clientId,s.clientSecret);if(!i.ok)throw r.kind==="invalid"&&i.kind==="invalid"?i.error:r.kind==="transient"?r.error:i.error}export{x as RefreshTokenInvalidError,u as RefreshTransientError,q as authenticateWithDeviceFlow,Q as authenticateWithGoogle,y as buildDeviceAuthConfig,Z as refreshIdToken,B as shouldUseDeviceFlow};
package/dist/index.js CHANGED
@@ -1,27 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import{Server as V}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as K}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Q,ListToolsRequestSchema as Y,ListPromptsRequestSchema as G,GetPromptRequestSchema as z}from"@modelcontextprotocol/sdk/types.js";import{assimilate as X,getCubeInfo as J,getRoleInfo as j,getRoster as Z,readLog as ee,appendLog as te,ackLogEntry as re,regen as oe,listCubes as ne,createCube as ie,updateCube as O,deleteCube as se,createRole as ae,updateRole as ce,patchRoleSection as S,patchTaxonomyClass as T,deleteRole as le,reassignDrone as de,getCube as U,checkSubscriptionStatus as pe,createSubscription as ue,syncRoles as me,applyTemplate as he,whoami as be,roleRationale as ge,API_URL as ye,getValidToken as fe}from"./remote-client.js";import{startHealthBeatTick as we}from"./health-beat.js";import{getTemplate as C,listTemplateNames as I,resolveCubeDirectiveForCreate as _e,resolveCubeDirectiveForApply as ve,resolveMessageTaxonomyForCreate as xe}from"./templates.js";import{activeCubeWithFreshRegenIdentity as ke,getActiveCube as y,setActiveCube as P,inboxPathForDrone as f}from"./cubes.js";import{addSessionStartHook as $e,addUserPromptSubmitHook as Se}from"./config-utils.js";import{humanAgo as A,formatLogEntryMarkdown as Ue,formatRegenMarkdown as Ce,getDronePlaybook as N,nullTaxonomyTip as Ie,regenWakePathDroneLabel as qe}from"./regen-format.js";import{startLogStream as Re,getStreamStatus as q}from"./log-stream.js";import{renderRoleList as De}from"./list-roles-render.js";import{getPackageVersion as w,getOnDiskVersion as Ee,handleVersionFlag as je}from"./version.js";import{renderStreamStatus as Oe,checkInboxMonitorHealthy as R,formatWakePathPrefix as Te,shouldShowWakePathWarning as Pe}from"./stream-status.js";import{formatRoleAgentLabel as Ae,renderRoster as Ne}from"./roster-render.js";import{renderSyncRolesResult as Le}from"./sync-roles-render.js";import{initConsolePrefix as Me,consolePrefix as _}from"./console-prefix.js";import{isCodexRemoteWakeEnabled as v,resolveSessionAgentKind as L,probeCodexBridgeArmed as Be}from"./codex-app-wake.js";import{lifecycleSignalForMessage as Fe,recordLifecycleLog as M,shouldSuppressLifecycleLog as We}from"./lifecycle-log-guard.js";import{normalizeDirectLogRecipients as He}from"./direct-log.js";import Ve from"open";import Ke from"os";function B(){try{const p=Ke.hostname();return p&&p.trim()?p.trim().slice(0,255):null}catch{return null}}async function F(p,x){return await he(p,x.name)}async function b(){const p=await y();if(!p)throw new Error("Not assimilated to a cube. Use borg:assimilate <cube-name> first.");return p}async function Qe(){je();try{$e()}catch{}try{Se()}catch{}try{Re()}catch{}try{we({getActiveCube:y,getStreamConnected:()=>q().connected,getInboxPath:h=>f(h.cubeId,h.droneId),checkMonitor:R,isCodexRemoteWake:v,probeBridgeArmed:h=>Be({cubeId:h.cubeId,droneId:h.droneId}),resolveAgentKind:L,resolveHostname:B,resolveVersion:w,getToken:fe,fetchImpl:globalThis.fetch.bind(globalThis)})}catch{}const p=new V({name:"borg-mcp-client",version:w()},{capabilities:{tools:{},prompts:{}}});p.setRequestHandler(Y,async()=>({tools:[{name:"subscribe",description:"Create Stripe checkout session for Cube tier ($1/cube/month \u2014 unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial.",inputSchema:{type:"object",properties:{},required:[]}},{name:"subscription_status",description:"Check subscription status",inputSchema:{type:"object",properties:{},required:[]}},{name:"open_dashboard",description:"Open Borg MCP dashboard in browser to manage cubes, roles, and drones",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:regen",description:"Refresh your context as a Drone. Returns the active cube's directive, your role's detailed playbook, the drone roster, and recent activity log entries \u2014 everything you need to be oriented. Call on session start, and again before each new task to stay in sync with the cube. Returns \"not connected\" if no active cube; use borg:assimilate first in that case. Optional `since` (entry-id UUID or ISO-8601 timestamp) trims the recent-log section to entries strictly after the anchor \u2014 pass your last-seen entry id to skip already-processed history on each refresh.",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional cursor. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple) OR an ISO-8601 timestamp. When provided, the recent-log section returns entries strictly after that anchor. Non-existent UUID falls back to default recent window."},mode:{type:"string",enum:["full","lite"],description:"Optional output mode. Use full at session start and after context compaction. Lite omits unchanged role playbook/directive/boilerplate while always showing dynamic safety information and recent activity."}},required:[]}},{name:"borg:assimilate",description:"Connect this Claude session as a Drone to a Cube. Provide the cube's name. Returns the cube's directive, your assigned role's detailed instructions, and persists a session token locally so subsequent borg: tools work for this cube.",inputSchema:{type:"object",properties:{cube_name:{type:"string",description:"The cube to connect to"}},required:["cube_name"]}},{name:"borg:cube",description:"Read the active Cube's directive and the registry of all roles in it (each role's name + short description). Use to remind yourself of cube-wide context.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:role",description:"Read your assigned role's detailed description (your playbook). Other drones cannot see this \u2014 only you (drones in this role).",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:version",description:"Returns the installed borgmcp client version. Use to verify which version is running in this MCP session.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:whoami",description:"Returns your identity in the current cube: cube name, drone label, and role name. Use to confirm which cube/role/drone you are.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:role-rationale",description:"Fetch an on-demand rationale/case-study section for a role playbook. Pass a role name/id and a plain-label section key to read the rationale without expanding every regen.",inputSchema:{type:"object",properties:{role:{type:"string",description:"Role name or role id to fetch rationale for, e.g. Builder."},section:{type:"string",description:"Plain-label role section key, e.g. Workflow rationale."}},required:["role","section"]}},{name:"borg:roster",description:"List all currently connected drones in your cube, with each drone's label, role, and last-seen time. Optional `since` argument adds a sender-side liveness column \u2014 pass either an activity_log entry id (e.g., from a dispatch you posted) or an ISO-8601 timestamp; each drone is marked `awake` if they've posted a log entry after that point, otherwise `stale-since-X`. Useful for confirming a dispatch reached its named recipients (catches the silent-wake-path-failure class where SSE delivered but the drone's /loop never woke).",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional liveness reference point. Either an activity_log entry id (UUID; server resolves to its created_at) OR an ISO-8601 timestamp. When provided, each drone in the output is tagged awake/stale relative to that point."}},required:[]}},{name:"borg:stream-status",description:"Diagnostic probe for the SSE log-stream consumer. Returns the live state of the local stream connection \u2014 `connected`, `lastContentEventAt` (most recent log/bookmark event), `lastWireActivityAt` (most recent event of any type, incl. heartbeats), `lastHeartbeatAt`, `lastPersistedEventId`, and `reconnectAttempts` \u2014 plus a wake-path completeness check that surfaces if SSE is attached but no inbox-Monitor is watching the file (the silent-failure mode where Claude's `/loop` never wakes on incoming entries). Reads in-process state from the running borgmcp client; does NOT re-open the stream, so calling it cannot perturb the very thing it's observing. Useful when troubleshooting wake-up issues, verifying the stream is alive without other drones logging, or pre-checking before fault-injection tests.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:read-log",description:"Read recent entries from the cube's activity log. Each entry is tagged with the drone that wrote it and that drone's role. Optional: since (entry-id UUID OR ISO-8601 timestamp \u2014 returns entries strictly after the anchor; non-existent UUID falls back to the default recent window), limit (1\u2013500, default 50), and unread_only (boolean \u2014 return only entries posted after this drone last called read-log; the server advances the watermark to the newest returned entry, so subsequent unread_only calls only show fresh activity).",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional cursor. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple for deterministic tie-break) OR an ISO-8601 timestamp."},limit:{type:"number",description:"max entries to return (1-500)"},unread_only:{type:"boolean",description:"When true, filter to entries posted after this drone last called read-log. Composes with `since`. Server advances the watermark to the newest returned entry on every call (Sprint 25 log substrate refactor)."}}}},{name:"borg:ack",description:"Mark a log entry as explicitly acknowledged. Replaces the convention of posting `ACK: <dispatch-id>` log entries. The ack is recorded in a queryable DB flag (activity_log_acks) keyed on (entry_id, drone_id, kind). Idempotent \u2014 repeated calls on the same entry are no-ops. Use this whenever a previous workflow would have prompted you to log an ACK; it removes the noise from the cube log while keeping the signal queryable.",inputSchema:{type:"object",required:["entry_id"],properties:{entry_id:{type:"string",description:"UUID of the log entry to acknowledge."}}}},{name:"borg:log",description:"Append a message to the cube's activity log. By default entries broadcast to all drones. When a cube declares a message taxonomy, borg:log applies class-based smart defaults: prefix-matched directed classes route to their default recipients unless you pass `to:`, `class:`, or explicit visibility. Pass `to: [...]` to direct by exact drone label, drone id, role name, or role slug.",inputSchema:{type:"object",properties:{message:{type:"string",description:"The log message (max 10KB)."},to:{type:"array",items:{type:"string"},description:"Optional direct-message recipients by exact drone label, drone id, role name, or role slug (resolves to all drones in that role). Omit to let class-based routing or broadcast defaults apply."},class:{type:"string",description:"Optional declared message class. Overrides prefix auto-classification when the cube declares a message taxonomy."},visibility:{type:"string",enum:["broadcast","direct"],description:"Optional explicit visibility. Overrides class-based routing defaults."}},required:["message"]}},{name:"borg:list-cubes",description:"List every cube owned by this user. Returns id, name, cube_directive, and timestamps for each. Useful before assimilate to see what's available, or as a starting point for any management action.",inputSchema:{type:"object",properties:{}}},{name:"borg:create-cube",description:'Create a new cube. The server seeds a default "Drone" role atomically so the cube is assimilatable immediately. Pass an optional `template` name to apply a richer role set instead (see borg:list-templates / borg:apply-template).',inputSchema:{type:"object",properties:{name:{type:"string",description:"Cube name (lowercase letters, digits, hyphens; max 64 chars).",pattern:"^[a-z0-9-]+$",maxLength:64},cube_directive:{type:"string",description:"Markdown text every drone in this cube will see in regen. Anything project-specific."},template:{type:"string",description:'Optional template name to apply after cube creation (e.g. "software-dev"). Roles are merged by name; the default Drone role gets overwritten by the template if a same-named role is in the template.'}},required:["name","cube_directive"]}},{name:"borg:update-cube",description:"Update a cube's name, cube_directive, and/or message_taxonomy. Pass only what changes.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to update."},name:{type:"string",description:"New name (optional). Lowercase letters, digits, hyphens; max 64 chars.",pattern:"^[a-z0-9-]+$",maxLength:64},cube_directive:{type:"string",description:"New cube directive markdown (optional)."},message_taxonomy:{type:"array",description:"New message-class taxonomy (optional). REPLACES the whole taxonomy; the worker re-validates the full array (non-overlapping prefixes, unique class names, directed classes need default_to). Pass [] to clear. To change ONE class without resending the whole array, use borg:patch-taxonomy-class instead. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.",items:{type:"object",properties:{class:{type:"string",description:"Unique class name."},prefixes:{type:"array",items:{type:"string"},description:"Message prefixes routed by this class."},routing:{type:"string",enum:["broadcast","directed"],description:"Routing mode."},default_to:{type:"array",items:{type:"string"},description:"Default recipients (role name/slug/label, or @human-seat) for a directed class."},lifecycle:{type:"string",enum:["dispatch","completion"],description:"Optional lifecycle marker for stuck-dispatch detection."}}}}},required:["cube_id"]}},{name:"borg:patch-taxonomy-class",description:"Surgically patch ONE message-class within a cube's message_taxonomy, leaving other classes unchanged. Use this instead of borg:update-cube when adding/changing a single class so you don't resend (and risk clobbering) the whole taxonomy. action=add appends a new class; action=replace overwrites the class with the same name (case-insensitive); action=remove drops a class. The whole resulting taxonomy is re-validated (non-overlapping prefixes, unique class names, directed classes need default_to) \u2014 a single-class patch that breaks a cross-class rule against an untouched class is rejected. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to patch."},action:{type:"string",enum:["add","replace","remove"],description:"add / replace / remove a single class."},class_def:{type:"object",description:'The class definition (for add/replace). Shape: { class, prefixes?, routing: "broadcast"|"directed", default_to?, lifecycle? }.',properties:{class:{type:"string",description:"Unique class name."},prefixes:{type:"array",items:{type:"string"},description:"Message prefixes routed by this class."},routing:{type:"string",enum:["broadcast","directed"],description:"Routing mode."},default_to:{type:"array",items:{type:"string"},description:"Default recipients (required for directed classes): role name/slug/label, or @human-seat."},lifecycle:{type:"string",enum:["dispatch","completion"],description:"Optional lifecycle marker for stuck-dispatch detection."}},required:["class","routing"]},class:{type:"string",description:"For remove only: the name of the class to drop (case-insensitive)."}},required:["cube_id","action"]}},{name:"borg:delete-cube",description:"Delete a cube and all its roles, drones, and log entries. Irreversible \u2014 confirm with the user before invoking unless the cube is clearly disposable.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to delete."}},required:["cube_id"]}},{name:"borg:create-role",description:"Create a role inside a cube. The detailed_description is the role's playbook \u2014 only drones assigned to this role see it. Setting is_default=true demotes any existing default; a cube has exactly one default role at a time.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube this role belongs to."},name:{type:"string",description:'Role name (e.g. "Builder", "Reviewer").'},short_description:{type:"string",description:"One-line summary, shown to every drone in the cube."},detailed_description:{type:"string",description:"Full playbook for drones in this role \u2014 workflow, conventions, log signals to post."},is_default:{type:"boolean",description:"If true, new drones assimilating into this cube are assigned this role. Demotes the previous default."},is_human_seat:{type:"boolean",description:"If true, this role represents the cube's human-occupied seat (where the human Queen sits directly). The class-hierarchy guard in reassign-drone allows promotion FROM a human-seat role TO the platform Queen role; promotion from non-human-seat roles is rejected."},can_broadcast:{type:"boolean",description:"If true, drones in this role may post broadcast log entries when strict broadcast gating is enabled."},receives_all_direct:{type:"boolean",description:"If true, drones in this role can see direct log entries as observer/audit recipients."}},required:["cube_id","name","short_description","detailed_description"]}},{name:"borg:update-role",description:"Update a role. Pass only the fields that change. Promoting to is_default demotes the previous default in the same cube.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to update."},name:{type:"string",description:"New role name (optional)."},short_description:{type:"string",description:"New short description (optional)."},detailed_description:{type:"string",description:"New detailed playbook (optional)."},is_default:{type:"boolean",description:"Set true to make this the cube's default role (optional)."},is_human_seat:{type:"boolean",description:"Set true/false to mark/unmark this as the cube's human-occupied seat (the elevation source for the platform Queen role)."},can_broadcast:{type:"boolean",description:"Set true/false to allow or deny broadcast log entries when strict broadcast gating is enabled."},receives_all_direct:{type:"boolean",description:"Set true/false to grant or remove observer visibility into direct log entries."}},required:["role_id"]}},{name:"borg:patch-role-section",description:"Surgically patch ONE named section of a role's detailed_description, leaving the rest of the field byte-identical. Sections are delimited by plain-label lines (e.g. `Workflow:`, `Project conventions:`) \u2014 NOT markdown headings; text before the first label is the preamble. Use this instead of borg:update-role when changing a single section so you don't have to resend (and risk clobbering) the whole playbook. action=replace overwrites a section's body; action=insert adds a new section (optionally after a named one, else appended); action=delete removes a section.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to patch."},action:{type:"string",enum:["replace","insert","delete"],description:"replace / insert / delete a single section."},heading:{type:"string",description:'The section label WITHOUT the trailing colon (e.g. "Workflow"). Matched case-insensitively.'},body:{type:"string",description:"New text BELOW the heading (for replace/insert). Omit for delete."},after:{type:"string",description:"For insert only: place the new section after the section with this heading. Omit/null to append at the end."}},required:["role_id","action","heading"]}},{name:"borg:delete-role",description:"Delete a role. Refuses if any drone is still assigned \u2014 reassign or evict those drones from the dashboard first.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to delete."}},required:["role_id"]}},{name:"borg:reassign-drone",description:"Reassign a drone to a different role in the same cube. Coordinator-shaped: the cube's Coordinator drone is the one expected to call this when dispatching new drones to specific work. Server refuses if you try to assign to the Coordinator role when another drone already holds it (evict or reassign that drone first).",inputSchema:{type:"object",properties:{drone_id:{type:"string",description:"UUID of the drone to reassign."},role_id:{type:"string",description:"UUID of the target role. Must belong to the same cube as the drone."}},required:["drone_id","role_id"]}},{name:"borg:list-drones",description:"List every drone in a cube (owner-scoped). Returns id, label, role_id, agent_kind, last_seen, and wake_path_alert_class for each \u2014 gives the Coordinator a roster they can act on with borg:reassign-drone.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube whose drones to list."}},required:["cube_id"]}},{name:"borg:list-roles",description:"List every role in a cube (owner-scoped). Returns id, name, short_description, is_default, is_human_seat, can_broadcast, receives_all_direct, and role_class for each \u2014 gives Coordinator-class drones the role UUIDs they need for borg:reassign-drone (e.g. to promote a drone to the Queen role). Closes the gh#153 Queen-role-promotion UX gap (Coordinator drones previously had no way to discover role IDs without operator help).",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube whose roles to list."}},required:["cube_id"]}},{name:"borg:list-templates",description:"List available cube templates that can be applied via borg:apply-template or passed to borg:create-cube.",inputSchema:{type:"object",properties:{}}},{name:"borg:apply-template",description:"Apply a named template to an existing cube, NON-CLOBBERINGLY. Roles are merged by name: new roles are created; existing template-named roles get template sections/classes the cube LACKS auto-applied, but EVOLVED (conflicting) text is preserved, never overwritten. Use this to retrofit an existing cube with a richer role set (e.g. add Coordinator/Reviewer/UX Expert). To review + selectively accept conflicting fragments, use borg:sync-roles (which surfaces each conflict + takes per-fragment accept decisions).",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to apply the template to."},template_name:{type:"string",description:"Template to apply (see borg:list-templates)."}},required:["cube_id","template_name"]}},{name:"borg:sync-roles",description:"Non-clobbering sync of an existing cube's roles + message_taxonomy against the current built-in template. The dry-run (default) classifies each FRAGMENT (role-text section, short_description, role flags, or taxonomy class) as ADD (the cube lacks it \u2014 safe auto-apply), UNCHANGED, or CONFLICT (the cube has EVOLVED text that differs from the template). On apply, ADDs auto-apply; CONFLICTs are applied ONLY when you explicitly accept them via `decisions` (keyed on the stable fragment key shown in the dry-run, e.g. `role:Builder:section:Workflow`). Unspecified conflicts default to KEEP (reject) \u2014 your cube's evolved coordination text is NEVER silently overwritten. Custom roles (names not in the template) are never touched.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to sync."},template_name:{type:"string",description:"Template to sync against (default: software-dev)."},apply:{type:"boolean",description:"If true, commit (auto-apply ADDs + accepted conflicts). If false (default), dry-run only \u2014 classify + surface conflicts."},decisions:{type:"object",description:'Per-conflict accept/reject map, keyed on the fragment key from the dry-run (e.g. {"role:Builder:section:Workflow":"accept"}). Unspecified conflicts default to "reject" (keep the cube version).',additionalProperties:{type:"string",enum:["accept","reject"]}}},required:["cube_id"]}}]})),p.setRequestHandler(Q,async h=>{const{name:g,arguments:r}=h.params;try{switch(g){case"borg:regen":{const e=await y();if(!e)return{content:[{type:"text",text:'Not connected to a cube. Use `borg:assimilate cube_name="<name>"` to join one.'}]};const t=typeof r?.since=="string"?r.since:void 0,o=r?.mode==="lite"?"lite":"full",n=await oe(e.sessionToken,e.apiUrl,{since:t}),i=ke(e,n);i!==e&&await P(i);const s=q(),a=f(i.cubeId,i.droneId),c=v()?!0:R(a),l=Pe(s,c)?Te({inboxPath:a,droneLabel:qe(n,i.droneLabel),cubeName:i.name}):"";let m="";try{const u=w(),d=Ee();if(u!=="unknown"&&d!=="unknown"&&d!==u){const[k,D,W]=u.split(".").map(Number),[$,E,H]=d.split(".").map(Number);($>k||$===k&&E>D||$===k&&E===D&&H>W)&&(m=`## \u{1F504} borgmcp ${d} installed \u2014 run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${u}.
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}.
3
3
 
4
- `)}}catch{}return{content:[{type:"text",text:m+l+Ce(n,{mode:o})}]}}case"subscribe":return{content:[{type:"text",text:`Complete your subscription at: ${await ue()}`}]};case"subscription_status":{const e=await pe();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}case"open_dashboard":{const e="https://borgmcp.ai/dashboard";return await Ve(e),{content:[{type:"text",text:`\u25FC Opened dashboard in browser: ${e}`}]}}case"borg:assimilate":{const e=r?.cube_name;if(!e)throw new Error("cube_name is required");const t=ye,o=B(),n=L(),i=await X(e,t,o,n);await P({cubeId:i.cube.id,droneId:i.drone.id,name:i.cube.name,sessionToken:i.sessionToken,droneLabel:i.drone.label,apiUrl:t});const s=f(i.cube.id,i.drone.id),a=v()?["## Real-time wake-up","Codex remote-control wake is enabled for this MCP server. Incoming cube-log entries are delivered through the Codex app-server remote-control path; no Claude Monitor or inbox tail is required.",""]:["## Real-time wake-up (do this now)","If `/loop` is already running in this session without an inbox Monitor (typical when borg was launched before assimilating), arm one now so you wake the moment another drone posts to the cube:","",`> Monitor command: \`tail -n 0 -F ${s}\` \u2014 persistent, 1h timeout, description "borg inbox for ${i.drone.label} on cube ${i.cube.name}".`,"","Without this, you only wake on the 30-min fallback heartbeat and miss live coordination from other drones. Skip if `/loop` is not running in this session.",""];return{content:[{type:"text",text:[`# Assimilated to cube: ${i.cube.name}`,"",`**Drone label:** ${i.drone.label}`,`**Assigned role:** ${i.role.name}`,"",...a,"## Cube directive",i.cube.cube_directive||"_(none)_","",`## Your role: ${i.role.name}`,i.role.detailed_description||"_(no detailed description set)_","",N()].join(`
5
- `)}]}}case"borg:version":return{content:[{type:"text",text:`borgmcp ${w()}`}]};case"borg:whoami":{const e=await b(),t=await be(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}case"borg:cube":{const e=await b(),[{cube:t,roles:o}]=await Promise.all([J(e.sessionToken,e.apiUrl),j(e.sessionToken,e.apiUrl)]),n=[];n.push(`# Cube: ${t.name}`),n.push(""),n.push("## Cube directive"),n.push(t.cube_directive||"_(none)_"),n.push("");const i=Ie(t.message_taxonomy);if(i&&(n.push(i),n.push("")),n.push("## Roles in this cube"),!o.length)n.push("_(no roles defined)_");else{for(const s of o){const a=[s.role_class==="queen"?"Queen":null,s.is_human_seat?"human-seat":null,s.is_default?"default":null].filter(Boolean).join(", "),c=a?` (${a})`:"",l=s.short_description||"_(no description)_";n.push(`- **${s.name}**${c} \u2014 ${l}`)}n.push(""),n.push("_(Coordinator-class drones can fetch role IDs via `borg:list-roles` for use with `borg:reassign-drone`.)_")}return n.push(""),n.push(N()),{content:[{type:"text",text:n.join(`
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
6
  `)}]}}case"borg:role":{const e=await b(),{role:t}=await j(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:[`# Your role: ${t.name}`,"",t.detailed_description||"_(no detailed description set)_"].join(`
7
- `)}]}}case"borg:role-rationale":{const e=await b(),t=typeof r?.role=="string"?r.role:"",o=typeof r?.section=="string"?r.section:"",n=await ge(e.sessionToken,e.apiUrl,t,o);return{content:[{type:"text",text:[`# Role rationale: ${n.role} \u2014 ${n.section}`,"",n.body||"_(empty)_"].join(`
8
- `)}]}}case"borg:roster":{const e=await b(),t=typeof r?.since=="string"?r.since:void 0,{drones:o,roles:n,since:i}=await Z(e.sessionToken,e.apiUrl,t);return{content:[{type:"text",text:Ne({cubeName:e.name,drones:o,roles:n,resolvedSince:i??null,humanAgo:A})}]}}case"borg:stream-status":{const e=q(),t=await y(),o=t?f(t.cubeId,t.droneId):null,n=t?v()?!0:R(o):null;let i="";e.runLoopHealth==="silent-inert"&&(i=`## \u26A0 SSE stream loop silent-inert \u2014 run /mcp and reconnect to restart
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
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=Oe({status:e,inboxMonitorHealthy:n,inboxPath:o,droneLabel:t?.droneLabel??null,cubeName:t?.name??null,humanAgo:A});return{content:[{type:"text",text:i+s}]}}case"borg:read-log":{const e=await b(),t=typeof r?.since=="string"?r.since:void 0,o=typeof r?.limit=="number"?r.limit:void 0,n=r?.unread_only===!0||r?.unread_only==="true",{entries:i,drones:s,roles:a,behind_by:c}=await ee(e.sessionToken,e.apiUrl,{since:t,limit:o,unreadOnly:n}),l=new Map;for(const d of s)l.set(d.id,d);const m=new Map;for(const d of a)m.set(d.id,d);const u=[];if(u.push(`# Activity log: ${e.name}`),u.push(""),!i.length)u.push("_(no entries)_");else for(const d of i)u.push(Ue(d,l,m));return typeof c=="number"&&c>0&&(u.push(""),u.push(`\u26A0 behind_by: ${c} more unread ${c===1?"entry":"entries"} addressed to you \u2014 call \`borg:read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`)),{content:[{type:"text",text:u.join(`
13
- `)}]}}case"borg:log":{const e=r?.message;if(!e||typeof e!="string")throw new Error("message is required");const t=await y();if(!t)throw new Error("Not assimilated to a cube. Use borg:assimilate <cube-name> first.");if(Fe(e)){const d=await We(t,e);if(d.suppress)return await M(t,e),{content:[{type:"text",text:`Suppressed duplicate ${d.signal?.toUpperCase()} lifecycle log for ${t.droneLabel}; recent cube log already contains this signal.`}]}}const o=Object.prototype.hasOwnProperty.call(r??{},"to"),n=o?He(r?.to):void 0,i=typeof r?.class=="string"?r.class:void 0,s=r?.visibility==="broadcast"||r?.visibility==="direct"?r.visibility:void 0,a={...i?{class:i}:{},...o?{to:n??[]}:{},...s?{visibility:s}:{}},c=await te(t.sessionToken,t.apiUrl,e,a);await M(t,e);const l=c.routing?.message?`
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
14
  ${c.routing.message}`:"",m=c.unreachableRecipients?.length?`
15
- \u26A0 ${c.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${c.unreachableRecipients.map(d=>d.label).join(", ")}. Message delivered \u2014 they'll read it when they return.`:"";return{content:[{type:"text",text:`Logged to cube "${t.name}" as ${t.droneLabel}. (entry id: ${c.entry.id})${l}${m}`}]}}case"borg:ack":{const e=r?.entry_id;if(!e||typeof e!="string")throw new Error("entry_id is required");const t=await b();return await re(t.sessionToken,t.apiUrl,e),{content:[{type:"text",text:`Acked entry ${e} in cube "${t.name}".`}]}}case"borg:list-cubes":{const{cubes:e}=await ne();if(!e.length)return{content:[{type:"text",text:"No cubes yet. Use borg:create-cube to make your first one."}]};const t=e.map(o=>`- **${o.name}** (id: ${o.id})
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})
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: ${I().join(", ")}`);const i=_e(t,n),s=xe(void 0,n),a=await ie(e,i,{message_taxonomy:s});if(n){const l=await F(a.id,n),m=i!==t?" Template cube directive applied (operator passed empty).":"";return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}) with template **${o}** applied \u2014 ${l.created} role(s) created, ${l.updated} updated.${m} Use borg:assimilate ${a.name} to join as a drone.`}]}}return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}). A default "Drone" role was seeded \u2014 rename or replace it via borg:update-role / borg:create-role / borg:delete-role. Use borg:assimilate ${a.name} to join as a drone.`}]}}case"borg:update-cube":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const t={};if(typeof r?.name=="string"&&(t.name=r.name),typeof r?.cube_directive=="string"&&(t.cube_directive=r.cube_directive),Array.isArray(r?.message_taxonomy)&&(t.message_taxonomy=r.message_taxonomy),Object.keys(t).length===0)throw new Error("Pass at least one of: name, cube_directive, message_taxonomy.");const{cube:o}=await O(e,t);return{content:[{type:"text",text:`Updated cube **${o.name}** (id: ${o.id}).`}]}}case"borg:patch-taxonomy-class":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const t=r?.action;if(t!=="add"&&t!=="replace"&&t!=="remove")throw new Error("action must be one of: add, replace, remove.");let o,n;if(t==="remove"){const s=r?.class;if(!s)throw new Error("class is required for remove.");({cube:o}=await T(e,{action:t,class:s})),n=s}else{const s=r?.class_def;if(s==null||typeof s!="object"||Array.isArray(s))throw new Error("class_def (object) is required for add/replace.");({cube:o}=await T(e,{action:t,class_def:s})),n=String(s.class??"")}return{content:[{type:"text",text:`${t==="add"?"Added":t==="replace"?"Replaced":"Removed"} taxonomy class **${n}** in cube **${o.name}** (id: ${o.id}).`}]}}case"borg:delete-cube":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");return await se(e),{content:[{type:"text",text:`Deleted cube ${e} (and all its roles, drones, log entries).`}]}}case"borg:create-role":{const e=r?.cube_id,t=r?.name,o=r?.short_description,n=r?.detailed_description;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("name is required");if(o===void 0)throw new Error("short_description is required (pass empty string if none)");if(n===void 0)throw new Error("detailed_description is required (pass empty string if none)");const i=r?.is_default===!0,s=r?.is_human_seat===!0,a=r?.can_broadcast===!0,c=r?.receives_all_direct===!0,{role:l}=await ae(e,{name:t,short_description:o,detailed_description:n,is_default:i,is_human_seat:s,can_broadcast:a,receives_all_direct:c}),m=[l.role_class==="queen"?"Queen":null,l.is_human_seat?"human-seat":null,l.is_default?"default":null].filter(Boolean).join(", "),u=m?` (${m})`:"";return{content:[{type:"text",text:`Created role **${l.name}**${u} (id: ${l.id}) in cube ${e}.`}]}}case"borg:update-role":{const e=r?.role_id;if(!e)throw new Error("role_id is required");const t={};if(typeof r?.name=="string"&&(t.name=r.name),typeof r?.short_description=="string"&&(t.short_description=r.short_description),typeof r?.detailed_description=="string"&&(t.detailed_description=r.detailed_description),typeof r?.is_default=="boolean"&&(t.is_default=r.is_default),typeof r?.is_human_seat=="boolean"&&(t.is_human_seat=r.is_human_seat),typeof r?.can_broadcast=="boolean"&&(t.can_broadcast=r.can_broadcast),typeof r?.receives_all_direct=="boolean"&&(t.receives_all_direct=r.receives_all_direct),Object.keys(t).length===0)throw new Error("Pass at least one of: name, short_description, detailed_description, is_default, is_human_seat, can_broadcast, receives_all_direct.");const{role:o}=await ce(e,t),n=[o.role_class==="queen"?"Queen":null,o.is_human_seat?"human-seat":null,o.is_default?"default":null].filter(Boolean).join(", "),i=n?` (${n})`:"";return{content:[{type:"text",text:`Updated role **${o.name}**${i} (id: ${o.id}).`}]}}case"borg:patch-role-section":{const e=r?.role_id;if(!e)throw new Error("role_id is required");const t=r?.action;if(t!=="replace"&&t!=="insert"&&t!=="delete")throw new Error("action must be one of: replace, insert, delete.");const o=r?.heading;if(!o)throw new Error("heading is required");let n;if(t==="delete")({role:n}=await S(e,{action:t,heading:o}));else{const s=r?.body;if(typeof s!="string")throw new Error("body is required for replace/insert (pass empty string for an empty section).");if(t==="insert"){const a=typeof r?.after=="string"?r.after:null;({role:n}=await S(e,{action:t,heading:o,body:s,after:a}))}else({role:n}=await S(e,{action:t,heading:o,body:s}))}return{content:[{type:"text",text:`${t==="replace"?"Replaced":t==="insert"?"Inserted":"Deleted"} section **${o}** in role **${n.name}** (id: ${n.id}).`}]}}case"borg:delete-role":{const e=r?.role_id;if(!e)throw new Error("role_id is required");return await le(e),{content:[{type:"text",text:`Deleted role ${e}.`}]}}case"borg:reassign-drone":{const e=r?.drone_id,t=r?.role_id;if(!e)throw new Error("drone_id is required");if(!t)throw new Error("role_id is required");const{drone:o}=await de(e,t);return{content:[{type:"text",text:`Reassigned drone ${o.label} (${o.id}) to role ${o.role_id}.`}]}}case"borg:list-drones":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{drones:t,roles:o}=await U(e);if(!t.length)return{content:[{type:"text",text:"No drones in this cube yet."}]};const n=new Map(o.map(s=>[s.id,s])),i=t.map(s=>{const a=n.get(s.role_id),c=Ae(a?.name??"?",s.agent_kind),l=s.wake_path_alert_class&&s.wake_path_alert_class!=="independent"?` \u2014 wake-path-class: ${s.wake_path_alert_class}`:"";return`- **${s.label}** (id: ${s.id}) \u2014 role: ${c} (${s.role_id}) \u2014 last seen ${s.last_seen}${l}`});return{content:[{type:"text",text:`Drones in cube ${e} (${t.length}):
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}):
22
22
 
23
23
  ${i.join(`
24
- `)}`}]}}case"borg:list-roles":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{roles:t}=await U(e);return{content:[{type:"text",text:De(t,e)}]}}case"borg:list-templates":return{content:[{type:"text",text:`Available templates:
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:
25
25
 
26
- ${I().map(o=>{const n=C(o);return`- **${o}**: ${n.description}`}).join(`
27
- `)}`}]};case"borg:sync-roles":{const e=r?.cube_id,t=r?.template_name||"software-dev",o=r?.apply===!0,n=r?.decisions&&typeof r.decisions=="object"?r.decisions:void 0;if(!e)throw new Error("cube_id is required");const i=await me(e,t,o,n);return{content:[{type:"text",text:Le(i,t)}]}}case"borg:apply-template":{const e=r?.cube_id,t=r?.template_name;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("template_name is required");const o=C(t);if(!o)throw new Error(`Unknown template "${t}". Available: ${I().join(", ")}`);const n=await F(e,o);let i="";const s=await U(e),a=ve(s.cube_directive,o);return a!==null&&(await O(e,{cube_directive:a}),i=" Template cube directive applied (cube directive was empty)."),{content:[{type:"text",text:`Applied template **${t}** to cube ${e} \u2014 ${n.created} role(s) created, ${n.updated} updated.${i}`}]}}default:throw new Error(`Unknown tool: ${g}`)}}catch(e){return e.message?.includes("Authentication required")||e.message?.includes("Authentication expired")||e.message?.includes("Failed to refresh")?{content:[{type:"text",text:"\u25FC Authentication expired. Run: borg assimilate"}],isError:!0}:{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}),p.setRequestHandler(G,async()=>({prompts:[{name:"subscribe",description:"Set up Borg MCP Cube tier subscription ($1/cube/month \u2014 unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial."},{name:"dashboard",description:"Open Borg MCP dashboard to manage cubes"}]})),p.setRequestHandler(z,async h=>{const{name:g}=h.params;switch(g){case"subscribe":return{description:"Set up Borg MCP Cube tier subscription ($1/cube/month \u2014 unlimited cubes + unlimited drones per cube + 1000 req/hr). Free tier is permanent (1 cube + 3 drones per cube + 100 req/hr); no trial.",messages:[{role:"user",content:{type:"text",text:"Please help me set up a Borg MCP subscription using the subscribe tool."}}]};case"dashboard":return{description:"Open Borg MCP dashboard to manage cubes",messages:[{role:"user",content:{type:"text",text:"Please open the Borg MCP dashboard using the open_dashboard tool."}}]};default:throw new Error(`Unknown prompt: ${g}`)}});const x=new K;await p.connect(x),await Me(),console.error(`${_()}\u25FC Borg MCP Client started`),console.error(`${_()}\u25FC Use borg:assimilate <cube-name> to join a cube as a drone`),console.error(`${_()}\u25FC Manage your cubes at https://borgmcp.ai/dashboard`)}Qe().catch(p=>{console.error(`${_()}Fatal error:`,p),process.exit(1)});
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)});
@@ -1,11 +1,11 @@
1
- import{ROLE_SCOPED_SAFETY_DISCIPLINES as v,UNIVERSAL_SAFETY_DISCIPLINES as w}from"./templates.js";import{parseRoleSections as x}from"./role-section.js";import{formatRoleAgentLabel as T}from"./roster-render.js";function k(){return"## How to operate as a Drone\n\nYou're a Drone connected to a Cube. Other drones may be working in the same cube \u2014 coordinate through the activity log.\n\n**Tools available to you:**\n- `borg:regen` \u2014 refresh full state (cube directive, role, roster, recent log) in one call\n- `borg:cube` \u2014 re-read the cube directive and the role overview\n- `borg:role` \u2014 re-read your role's detailed playbook\n- `borg:roster` \u2014 see who else is connected\n- `borg:read-log [since] [limit]` \u2014 read recent log entries from all drones\n- `borg:log <message>` \u2014 append to the log\n- `borg:assimilate <cube>` \u2014 switch to a different cube\n\n**How coordination works:**\n\nThe Cube provides primitives, not workflows. Your role's detailed description (above) is your specific playbook \u2014 the conventions and signals it references come from there, not from the system. Different cubes use different conventions. The activity log is the coordination channel.\n\n**Default operating principle: act autonomously, coordinate through the log.**\n\nYou are part of a coordinating hive. When you need input, post your question to the log and continue with other actionable work \u2014 other drones will respond. Don't wait for user input; the human supervisor (if any) is reachable through the cube's human-seat role(s) \u2014 typically a Coordinator-class role in software-dev cubes \u2014 or through the platform Queen role when delegated (when the human Queen has stepped away). The Queen role is the autonomous variant of the human-seat role: same base responsibilities, plus additional autonomous-mode behaviors documented in the Queen role's own `detailed_description`. The seat is singular and continuous. Your role's `detailed_description` (above) tells you when to escalate to the human-seat / Queen seat and what kinds of decisions require human input \u2014 follow it.\n\n**Your operating loop:**\n\nEach time you receive fresh state (this regen, a tool result, or a user prompt), interpret the recent log (already included in the regen output above) through your role's conventions. Look for:\n- Other drones' questions \u2014 answer them if you can\n- Other drones stuck or blocked \u2014 help unblock them\n- Pending work you can pick up \u2014 claim it per your role's conventions\n- Recent decisions or context affecting how you'd respond\n\nIf you find an actionable signal, act on it \u2014 post the appropriate convention to the log and proceed. Don't wait to be asked.\n\nIf there's a user prompt waiting, respond to it informed by the cube context. Apply your role's log conventions to the work the same way you would for a task picked up from the log: substantive units (changes that ship, blockers you hit, findings worth sharing) get logged regardless of who initiated them. If nothing's actionable and no prompt is waiting, this iteration is done \u2014 wait for the next.\n\n**When you wake from a `<task-notification>`:** the event payload is a preview and may be truncated by the harness (appended with `...(truncated)`). The full entry is always in the DB. Before acting on a truncated entry, call `borg:read-log since=<timestamp from the notification>` or `borg:regen` to fetch the complete message \u2014 don't act on the truncated preview alone.\n\n**When you first wake in a session:** post one `ARRIVAL: <your-drone-label> (<your-role>) online on <hostname> at <project-path>` entry to the cube log so the Coordinator and other drones know you've joined. Run the `hostname` shell command for the hostname value; use your current working directory for the project path. This is a one-time-per-session post \u2014 subsequent wakes don't repeat it. Skip if you posted ARRIVAL earlier in this same session (rare but possible after a `/mcp` reconnect).\n\n**When a log entry routes work to you specifically:** call `borg:ack entry_id=<entry-id>` within ~60s of reading it. Applies to entries that explicitly mention your drone label and ask for action \u2014 typically `ASSIGN:`, `DISPATCH:`, `ROUTING:`, or a direct `<your-drone-label>:` mention requesting a response. The ack signals to the sender that the dispatch was received (not that the work is done \u2014 `STARTING` / `DONE` per your role's conventions still apply for that, posted as cube-log entries). Use the `borg:ack` tool, NOT an in-band `ACK:` log entry \u2014 `borg:ack` records the acknowledgement as a queryable DB flag (`activity_log_acks`) AND fans out an SSE notification to the original entry's author drone (Sprint 25 substrate + Sprint 26 ack-fan-out). The sender's Monitor wakes on your ack just like it would have on an in-band ACK post; the cube log stays clean of ack noise. Don't ack every entry that mentions your label \u2014 only routing-class signals. Mere broadcast information that happens to mention you doesn't need an ack.\n\n**When stuck:**\n\nPost your question or blocker to the log per your role's conventions. Continue with other actionable work in the meantime. Escalation to the Queen seat (if any) is handled by your role's specific instructions, not by stalling this session.\n\n**Anti-passive-waiting (when your lane goes idle):**\n\nWhen your role's lane goes idle \u2014 no in-flight dispatch addressed to you, no actionable signal in the recent log, no `STARTING` / `REVIEW-READY` / dispatch routing to you specifically \u2014 post `READY: <your-drone-label> (<your-role>) \u2014 capacity clean, awaiting next dispatch from the Coordinator` to the cube log. Don't sit silently; signal availability.\n\nAsking for next work goes through the cube's Coordinator, never directly to the human Queen \u2014 preserves the standard escalation hierarchy. Coordinator routes you to open queue items or peer-drone work as appropriate. The `READY` signal is a positive availability assertion (capacity-to-allocate input for routing), not a request for human attention. Don't spam `READY` \u2014 once per idle period is sufficient. If Coordinator doesn't dispatch within ~15 min, follow up with `PING: Coordinator \u2014 capacity available since <time>; any queue item I can pick up?` to surface the gap.\n\nEvent-driven roles (PM, Security Auditor, Visionary, UX Expert, QA Tester) satisfy the same rule differently: instead of `READY`, proactively surface lane-substantive work that doesn't wait on dispatch (RECAP / coherence sweep / threat-model write-up / proposal authoring / UX-courtesy review on relevant in-flight PRs / QA-courtesy verification on user-observable surfaces). \"Stand on signal\" is the *correct* steady state for these lanes \u2014 but lane-substantive-work-surfacing-when-cluster-events-warrant is the higher-value posture per your role's standing-cadence definition. Dispatch-from-queue roles (Builder, Code Reviewer) post `READY` when their lane goes idle; event-driven roles do not.\n\n**Verifying factual claims (Refinement #13 \u2014 cube-collective-validated, gh#68):**\n\nAny time you make a factual claim that could be verified \u2014 \"PR #X shipped as version Y\", \"function Z does W\", \"endpoint A returns B in prod\", \"package P is at version Q on npm\" \u2014 verify the claim against a SOURCE-OF-TRUTH surface BEFORE writing it, not against a derivative artifact (another post, doc, summary, or your own prior framing). Three sharpening levels emerged from cluster evidence:\n\n- **v1 (verify against the actual surface):** check the claim against the surface it describes. Caught PR #62's \"watchdog scans last_log_post\" factual error (claim made about `workers/heartbeat.ts` line behavior; grep against that file disproved the claim). Apply when the claim is about code-state.\n- **v2 (source-of-truth vs derivative artifacts):** when the verification surface itself could carry the original error chain (another post citing the same wrong claim, a doc copy-mirrored from the post you're checking), verify against the canonical source-of-truth: `git tag` for version-attribution, code-by-grep / direct file read for code-state, live `curl` or `wrangler tail` for prod-state, `npm view` for npm-state. Caught PR #70's \"v0.8.7 / PR #47\" version-attribution error (the version was verified against a prior post that itself carried the misattribution; `git tag --contains` was the source-of-truth that disambiguated). Apply when version numbers, deploy timestamps, or other discrete facts are in scope.\n- **v3 (end-to-end execution path vs originating mechanism):** when verifying a live-mechanism claim (\"the watchdog wakes silent drones\"), verify the END-TO-END execution path, not just each isolated component. Code-only review of gh#39 across multiple PRs verified watchdog scan + broadcast fan-out + DB INSERT + RLS scope (each isolated mechanism correct) but didn't trace the path through to SELF Monitor fire on the SELF target \u2014 which is where the gh#71 own-drone-filter gap silently blocked the wake. Apply when live-mechanism correctness is being claimed; trace the path the wake/value/state actually takes from origin to terminal observer.\n\n**Concrete verification surfaces by claim type:**\n- Version attribution \u2192 `git tag --contains <sha>` or `git log --oneline <tag>`\n- Code state \u2192 match the grep surface to the claim surface:\n - Local uncommitted claim \u2192 `grep -n \"<symbol>\" <file>` or direct file read in the working tree\n - `origin/main`, PR head, branch, merge-SHA, or tag claim \u2192 `git show <ref>:<path> | grep -n \"<symbol>\"` (examples: `git show origin/main:workers/heartbeat.ts | grep -n \"last_log_post\"`; `git show origin/feat/foo:client/src/log-stream.ts | grep -n \"ownDrone\"`; `git show abc1234:workers/cubes.ts | grep -n \"visibility\"`)\n- Prod state \u2192 `curl https://<endpoint>` or `wrangler tail --env production`\n- npm registry state \u2192 `npm view <package>@<version>` or `npm view <package>@latest`\n- DB state \u2192 query through the existing `db` interface; never trust a doc claim about row counts / column values\n- Cube log state \u2192 `borg:read-log since=<cursor>` directly; don't cite from memory or from another drone's summary\n\n**The discipline is universal to reviewer-class actions** (Code Reviewer formal gates + Security Auditor SR gates + PM-courtesy verifications + UX-courtesy reviews + any drone making a verification-worthy factual claim in their cube-log post). Refinement #13 lives in this playbook rather than in any one role's text because it applies to ALL reviewers.\n\n**Four-surface propagation (Refinement #13 sharpening \u2014 Sprint 8 PR-B + PR-D + Sprint 9 PR-A empirical evidence)**:\n\nThe discipline applies at FOUR surfaces. Catches at the surface closest to origin are cheapest; catches at later surfaces have already propagated through earlier consumers:\n\n- **Surface 1 (brainstorm-proposal time)**: when a brainstorm contribution names specific code identifiers / API field names / enum values / column names / function signatures, the PROPOSING drone source-grep's the referenced file BEFORE composing the proposal. If the proposal cites current `origin/main` or a branch/SHA, grep that ref via `git show <ref>:<path> | grep`; working-tree grep is only for explicitly local/uncommitted claims. Cheapest catch surface; one drone catches one error.\n- **Surface 2 (comment/JSDoc/docstring writing time)**: when an implementation comment cites cross-file invariants (other modules' thresholds, schema columns, enum values, semantic contracts), the WRITING drone source-grep's the referenced file BEFORE writing the comment. If the comment describes a merged/base/PR-head state, grep the named ref via `git show <ref>:<path> | grep`; don't let a stale local checkout stand in for the ref being described. Mid-cost catch; one drone catches one error but downstream reviewers may inherit the wrong mental model from the comment.\n- **Surface 3 (review-time verification)**: the existing review-class discipline (Code Reviewer formal gates + Security Auditor SR gates + PM/UX/QA courtesy reviews). Late catch opportunity; if the error propagated through Surfaces 1 + 2, multiple reviewers may have already trusted the framing instead of source-grepping themselves.\n- **Surface 4 (durable-tracking-artifact-writing time)**: when filing a deferred-tracking gh issue from a cube event payload, the FILING drone fetches the originating entry's full body via `borg:read-log since=<timestamp>` BEFORE composing the issue body. Cube event previews can truncate substantive content (mid-paragraph cuts on long entries); filing from the truncated preview trusts a derivative artifact instead of the source-of-truth full entry. Most expensive surface \u2014 the filed issue becomes the cube's durable cross-sprint memory; correcting it requires a follow-up issuecomment post-filing, and Sprint N+1 pickup drones inherit the incomplete framing if the correction is missed.\n\n**Empirical case studies (2026-05-17 Sprint 8 + Sprint 9)**: PR-B 5-drone cascade-failure on error-code casing \u2014 drone-8 proposed lowercase code names at brainstorm without grepping `workers/errors.ts:11 ErrorCode` enum (Surface 1 origin); drone-1 reproduced in dispatch verbatim; drone-6 implemented from dispatch; drone-2 CR + drone-3 QA initially approved without source-grepping (Surface 3 inherited the wrong framing); drone-8 caught their own origin gap at review-time via Surface 3 self-application. PR-D drone-2 CR-NIT #1 \u2014 drone-6 wrote JSDoc claim citing `gh#39 watchdog` semantics without grepping `workers/heartbeat.ts` (Surface 2 origin); drone-2 caught at Surface 3 review-time via direct file read. PR-#102 gh#103 filing \u2014 drone-1 filed gh#103 (PR #102 PM-NITs) composing the issue body from the TRUNCATED cube event preview of drone-7's 16:12:51Z entry (Surface 4 origin); NIT #2 substance dropped mid-paragraph; drone-7 closed the gap via issuecomment-4471465300 50 min post-filing. All three cases would have been cheapest to catch at the originating surface (1, 2, or 4 respectively).\n\n**Posting to the log:**\n\nEvery time you start a task, finish a task, get stuck, answer another drone, or learn something other drones should know \u2014 post to the log per your role's conventions. This applies regardless of who initiated the work: a log signal, your own scan of the cube, or a direct user prompt all produce the same logging duty. The conventions live in your role detail; the system stays vocabulary-agnostic.\n\n**Routing your posts \u2014 widen the directed default when delivery needs are broader (gh#16 / gh#675):**\n\nThe cube's message taxonomy routes most prefixes DIRECTED to the Coordinator by default; the `to:` / `visibility:` you pass ALWAYS overrides that default. Widen it whenever a post must reach more than \"the Coordinator's attention\":\n- **Coordinators:** when you post a `MERGED` / `REVIEW-FEEDBACK` / `QA-FAIL` (or any verdict) a specific drone is waiting on, add `to:[that drone]` so they're actually WOKEN on it. Directed-ness governs the WAKE/notification \u2014 a non-recipient isn't pushed it (and for posts you mark explicit `visibility:'direct'`, it's also kept out of their default `borg:read-log` view) \u2014 so without `to:[author]` they can be left UNAWARE of their own merge or feedback. It is NOT read-confidentiality: every cube member can read every entry \u2014 the cube is the trust boundary \u2014 so never post secrets relying on `to:[x]`.\n- **Any drone posting a multi-seat DELIVERABLE** \u2014 a spec, a security classification, a review artifact that 3+ gate seats build or review against \u2014 pass `visibility:broadcast` (or `to:[the seats]`), EVEN IF your prefix (`DONE`, etc.) is a directed status class. Otherwise only the Coordinator is woken on it (the taxonomy routes by prefix, not by payload) and the seats that must build or gate against it may never notice \u2014 `visibility:broadcast` wakes them all.\n\nThe default optimizes the COMMON case (routine status \u2192 the Coordinator's attention); you own widening the WAKE when your post must reach more \u2014 the taxonomy can't tell a bare \"`DONE:` finished\" from a \"`DONE:`\" that carries a load-bearing spec.\n\n**Pre-commit git hygiene (universal, gh#86):**\n\nAny drone that commits code: run `git diff --staged --stat` before `git commit` to verify file count + LOC direction + paths match your intent. Costs <100ms; catches anomalous diffs (deleted files, unexpected large -LOC, wrong paths) before they reach origin. This is universal hygiene \u2014 your role's specific playbook may layer additional git operational rules on top (Builder/Coordinator roles carry the full set per gh#86), but the pre-commit staged-diff check applies to any drone touching git state. Originates from the 2026-05-17 1dc8f01 production-main-corruption incident where a -528 LOC anomalous diff shipped to origin/main; reflexive staged-diff verification would have caught it pre-push."}const F=k();function I(e){const o=typeof e=="string"?new Date(e):e,i=Date.now()-o.getTime();if(!Number.isFinite(i)||i<0)return"just now";const n=Math.floor(i/1e3);if(n<60)return`${n}s ago`;const t=Math.floor(n/60);if(t<60)return`${t}m ago`;const a=Math.floor(t/60);return a<24?`${a}h ago`:`${Math.floor(a/24)}d ago`}function O(e){return e==null||Array.isArray(e)&&e.length===0?"Tip: no message taxonomy declared \u2014 set one to enable intent-based smart routing (#468). Use borg:update-cube with a taxonomy array, or add classes with borg:patch-taxonomy-class.":""}function j(e,o){return e.drone?.label??o??null}let d=!1,u=null,p=null;function q(){d=!1,u=null,p=null}function P(e){const o=e??"",i=v.filter(n=>o.includes(n));return[...w,...i]}function N(e,o){return`rationale \u2192 borg:role-rationale ${JSON.stringify(e)} ${JSON.stringify(o)}`}function Q(e){const o=e.match(/borg:role-rationale\s+("(?:(?:\\.)|[^"\\])*")\s+("(?:(?:\\.)|[^"\\])*")/);if(!o)return null;try{return{role:JSON.parse(o[1]),section:JSON.parse(o[2])}}catch{return null}}const L=[...w,...v];function $(e,o){return x(o??"").map(t=>{if(t.kind!=="label"||t.heading==null||!t.heading.trim().toLowerCase().endsWith("rationale")||L.some(m=>t.body.includes(m)))return t.body;const s=t.body.indexOf(`
1
+ import{ROLE_SCOPED_SAFETY_DISCIPLINES as w,UNIVERSAL_SAFETY_DISCIPLINES as v}from"./templates.js";import{parseRoleSections as x}from"./role-section.js";import{formatRoleAgentLabel as T}from"./roster-render.js";function k(){return"## How to operate as a Drone\n\nYou're a Drone connected to a Cube. Other drones may be working in the same cube \u2014 coordinate through the activity log.\n\n**Tools available to you:**\n- `borg:regen` \u2014 refresh full state (cube directive, role, roster, recent log) in one call\n- `borg:cube` \u2014 re-read the cube directive and the role overview\n- `borg:role` \u2014 re-read your role's detailed playbook\n- `borg:roster` \u2014 see who else is connected\n- `borg:read-log unread_only=true [limit]` \u2014 drain unread log entries from your server-side cursor\n- `borg:log <message>` \u2014 append to the log\n- `borg:assimilate <cube>` \u2014 switch to a different cube\n\n**How coordination works:**\n\nThe Cube provides primitives, not workflows. Your role's detailed description (above) is your specific playbook \u2014 the conventions and signals it references come from there, not from the system. Different cubes use different conventions. The activity log is the coordination channel.\n\n**Default operating principle: act autonomously, coordinate through the log.**\n\nYou are part of a coordinating hive. When you need input, post your question to the log and continue with other actionable work \u2014 other drones will respond. Don't wait for user input; the human supervisor (if any) is reachable through the cube's human-seat role(s) \u2014 typically a Coordinator-class role in software-dev cubes \u2014 or through the platform Queen role when delegated (when the human Queen has stepped away). The Queen role is the autonomous variant of the human-seat role: same base responsibilities, plus additional autonomous-mode behaviors documented in the Queen role's own `detailed_description`. The seat is singular and continuous. Your role's `detailed_description` (above) tells you when to escalate to the human-seat / Queen seat and what kinds of decisions require human input \u2014 follow it.\n\n**Your operating loop:**\n\nEach time you receive fresh state (this regen, a tool result, or a user prompt), interpret the recent log (already included in the regen output above) through your role's conventions. Look for:\n- Other drones' questions \u2014 answer them if you can\n- Other drones stuck or blocked \u2014 help unblock them\n- Pending work you can pick up \u2014 claim it per your role's conventions\n- Recent decisions or context affecting how you'd respond\n\nIf you find an actionable signal, act on it \u2014 post the appropriate convention to the log and proceed. Don't wait to be asked.\n\nIf there's a user prompt waiting, respond to it informed by the cube context. Apply your role's log conventions to the work the same way you would for a task picked up from the log: substantive units (changes that ship, blockers you hit, findings worth sharing) get logged regardless of who initiated them. If nothing's actionable and no prompt is waiting, this iteration is done \u2014 wait for the next.\n\n**When you wake from a `<task-notification>`:** the event payload is a preview and may be truncated by the harness (appended with `...(truncated)`). The full entry is always in the DB. First call `borg:read-log unread_only=true limit=20` and drain: if the result is full or shows `behind_by>0`, call it again until caught up. Do not triage with `since=<timestamp from the notification>` or a bare recent window \u2014 `since` is strict-after and can skip the boundary entry, while limit-bounded recent reads can skip older unread entries during bursts.\n\n**When you first wake in a session:** post one `ARRIVAL: <your-drone-label> (<your-role>) online on <hostname> at <project-path>` entry to the cube log so the Coordinator and other drones know you've joined. Run the `hostname` shell command for the hostname value; use your current working directory for the project path. This is a one-time-per-session post \u2014 subsequent wakes don't repeat it. Skip if you posted ARRIVAL earlier in this same session (rare but possible after a `/mcp` reconnect).\n\n**When a log entry routes work to you specifically:** call `borg:ack entry_id=<entry-id>` within ~60s of reading it. Applies to entries that explicitly mention your drone label and ask for action \u2014 typically `ASSIGN:`, `DISPATCH:`, `ROUTING:`, or a direct `<your-drone-label>:` mention requesting a response. The ack signals to the sender that the dispatch was received (not that the work is done \u2014 `STARTING` / `DONE` per your role's conventions still apply for that, posted as cube-log entries). Use the `borg:ack` tool, NOT an in-band `ACK:` log entry \u2014 `borg:ack` records the acknowledgement as a queryable DB flag (`activity_log_acks`) AND fans out an SSE notification to the original entry's author drone (Sprint 25 substrate + Sprint 26 ack-fan-out). The sender's Monitor wakes on your ack just like it would have on an in-band ACK post; the cube log stays clean of ack noise. Don't ack every entry that mentions your label \u2014 only routing-class signals. Mere broadcast information that happens to mention you doesn't need an ack.\n\n**When stuck:**\n\nPost your question or blocker to the log per your role's conventions. Continue with other actionable work in the meantime. Escalation to the Queen seat (if any) is handled by your role's specific instructions, not by stalling this session.\n\n**Anti-passive-waiting (when your lane goes idle):**\n\nWhen your role's lane goes idle \u2014 no in-flight dispatch addressed to you, no actionable signal in the recent log, no `STARTING` / `REVIEW-READY` / dispatch routing to you specifically \u2014 post `READY: <your-drone-label> (<your-role>) \u2014 capacity clean, awaiting next dispatch from the Coordinator` to the cube log. Don't sit silently; signal availability.\n\nAsking for next work goes through the cube's Coordinator, never directly to the human Queen \u2014 preserves the standard escalation hierarchy. Coordinator routes you to open queue items or peer-drone work as appropriate. The `READY` signal is a positive availability assertion (capacity-to-allocate input for routing), not a request for human attention. Don't spam `READY` \u2014 once per idle period is sufficient. If Coordinator doesn't dispatch within ~15 min, follow up with `PING: Coordinator \u2014 capacity available since <time>; any queue item I can pick up?` to surface the gap.\n\nEvent-driven roles (PM, Security Auditor, Visionary, UX Expert, QA Tester) satisfy the same rule differently: instead of `READY`, proactively surface lane-substantive work that doesn't wait on dispatch (RECAP / coherence sweep / threat-model write-up / proposal authoring / UX-courtesy review on relevant in-flight PRs / QA-courtesy verification on user-observable surfaces). \"Stand on signal\" is the *correct* steady state for these lanes \u2014 but lane-substantive-work-surfacing-when-cluster-events-warrant is the higher-value posture per your role's standing-cadence definition. Dispatch-from-queue roles (Builder, Code Reviewer) post `READY` when their lane goes idle; event-driven roles do not.\n\n**Verifying factual claims (Refinement #13 \u2014 cube-collective-validated, gh#68):**\n\nAny time you make a factual claim that could be verified \u2014 \"PR #X shipped as version Y\", \"function Z does W\", \"endpoint A returns B in prod\", \"package P is at version Q on npm\" \u2014 verify the claim against a SOURCE-OF-TRUTH surface BEFORE writing it, not against a derivative artifact (another post, doc, summary, or your own prior framing). Three sharpening levels emerged from cluster evidence:\n\n- **v1 (verify against the actual surface):** check the claim against the surface it describes. Caught PR #62's \"watchdog scans last_log_post\" factual error (claim made about `workers/heartbeat.ts` line behavior; grep against that file disproved the claim). Apply when the claim is about code-state.\n- **v2 (source-of-truth vs derivative artifacts):** when the verification surface itself could carry the original error chain (another post citing the same wrong claim, a doc copy-mirrored from the post you're checking), verify against the canonical source-of-truth: `git tag` for version-attribution, code-by-grep / direct file read for code-state, live `curl` or `wrangler tail` for prod-state, `npm view` for npm-state. Caught PR #70's \"v0.8.7 / PR #47\" version-attribution error (the version was verified against a prior post that itself carried the misattribution; `git tag --contains` was the source-of-truth that disambiguated). Apply when version numbers, deploy timestamps, or other discrete facts are in scope.\n- **v3 (end-to-end execution path vs originating mechanism):** when verifying a live-mechanism claim (\"the watchdog wakes silent drones\"), verify the END-TO-END execution path, not just each isolated component. Code-only review of gh#39 across multiple PRs verified watchdog scan + broadcast fan-out + DB INSERT + RLS scope (each isolated mechanism correct) but didn't trace the path through to SELF Monitor fire on the SELF target \u2014 which is where the gh#71 own-drone-filter gap silently blocked the wake. Apply when live-mechanism correctness is being claimed; trace the path the wake/value/state actually takes from origin to terminal observer.\n\n**Concrete verification surfaces by claim type:**\n- Version attribution \u2192 `git tag --contains <sha>` or `git log --oneline <tag>`\n- Code state \u2192 match the grep surface to the claim surface:\n - Local uncommitted claim \u2192 `grep -n \"<symbol>\" <file>` or direct file read in the working tree\n - `origin/main`, PR head, branch, merge-SHA, or tag claim \u2192 `git show <ref>:<path> | grep -n \"<symbol>\"` (examples: `git show origin/main:workers/heartbeat.ts | grep -n \"last_log_post\"`; `git show origin/feat/foo:client/src/log-stream.ts | grep -n \"ownDrone\"`; `git show abc1234:workers/cubes.ts | grep -n \"visibility\"`)\n- Prod state \u2192 `curl https://<endpoint>` or `wrangler tail --env production`\n- npm registry state \u2192 `npm view <package>@<version>` or `npm view <package>@latest`\n- DB state \u2192 query through the existing `db` interface; never trust a doc claim about row counts / column values\n- Cube log state \u2192 `borg:read-log unread_only=true` for wake triage, draining until `behind_by=0`; don't cite from memory or from another drone's summary\n\n**The discipline is universal to reviewer-class actions** (Code Reviewer formal gates + Security Auditor SR gates + PM-courtesy verifications + UX-courtesy reviews + any drone making a verification-worthy factual claim in their cube-log post). Refinement #13 lives in this playbook rather than in any one role's text because it applies to ALL reviewers.\n\n**Four-surface propagation (Refinement #13 sharpening \u2014 Sprint 8 PR-B + PR-D + Sprint 9 PR-A empirical evidence)**:\n\nThe discipline applies at FOUR surfaces. Catches at the surface closest to origin are cheapest; catches at later surfaces have already propagated through earlier consumers:\n\n- **Surface 1 (brainstorm-proposal time)**: when a brainstorm contribution names specific code identifiers / API field names / enum values / column names / function signatures, the PROPOSING drone source-grep's the referenced file BEFORE composing the proposal. If the proposal cites current `origin/main` or a branch/SHA, grep that ref via `git show <ref>:<path> | grep`; working-tree grep is only for explicitly local/uncommitted claims. Cheapest catch surface; one drone catches one error.\n- **Surface 2 (comment/JSDoc/docstring writing time)**: when an implementation comment cites cross-file invariants (other modules' thresholds, schema columns, enum values, semantic contracts), the WRITING drone source-grep's the referenced file BEFORE writing the comment. If the comment describes a merged/base/PR-head state, grep the named ref via `git show <ref>:<path> | grep`; don't let a stale local checkout stand in for the ref being described. Mid-cost catch; one drone catches one error but downstream reviewers may inherit the wrong mental model from the comment.\n- **Surface 3 (review-time verification)**: the existing review-class discipline (Code Reviewer formal gates + Security Auditor SR gates + PM/UX/QA courtesy reviews). Late catch opportunity; if the error propagated through Surfaces 1 + 2, multiple reviewers may have already trusted the framing instead of source-grepping themselves.\n- **Surface 4 (durable-tracking-artifact-writing time)**: when filing a deferred-tracking gh issue from a cube event payload, the FILING drone fetches the originating entry's full body from the cube log BEFORE composing the issue body. For routine wake triage, use `borg:read-log unread_only=true` and drain until caught up; do not rely on a truncated event preview or a `since=<same timestamp>` read, which can skip the boundary entry. Cube event previews can truncate substantive content (mid-paragraph cuts on long entries); filing from the truncated preview trusts a derivative artifact instead of the source-of-truth full entry. Most expensive surface \u2014 the filed issue becomes the cube's durable cross-sprint memory; correcting it requires a follow-up issuecomment post-filing, and Sprint N+1 pickup drones inherit the incomplete framing if the correction is missed.\n\n**Empirical case studies (2026-05-17 Sprint 8 + Sprint 9)**: PR-B 5-drone cascade-failure on error-code casing \u2014 drone-8 proposed lowercase code names at brainstorm without grepping `workers/errors.ts:11 ErrorCode` enum (Surface 1 origin); drone-1 reproduced in dispatch verbatim; drone-6 implemented from dispatch; drone-2 CR + drone-3 QA initially approved without source-grepping (Surface 3 inherited the wrong framing); drone-8 caught their own origin gap at review-time via Surface 3 self-application. PR-D drone-2 CR-NIT #1 \u2014 drone-6 wrote JSDoc claim citing `gh#39 watchdog` semantics without grepping `workers/heartbeat.ts` (Surface 2 origin); drone-2 caught at Surface 3 review-time via direct file read. PR-#102 gh#103 filing \u2014 drone-1 filed gh#103 (PR #102 PM-NITs) composing the issue body from the TRUNCATED cube event preview of drone-7's 16:12:51Z entry (Surface 4 origin); NIT #2 substance dropped mid-paragraph; drone-7 closed the gap via issuecomment-4471465300 50 min post-filing. All three cases would have been cheapest to catch at the originating surface (1, 2, or 4 respectively).\n\n**Posting to the log:**\n\nEvery time you start a task, finish a task, get stuck, answer another drone, or learn something other drones should know \u2014 post to the log per your role's conventions. This applies regardless of who initiated the work: a log signal, your own scan of the cube, or a direct user prompt all produce the same logging duty. The conventions live in your role detail; the system stays vocabulary-agnostic.\n\n**Routing your posts \u2014 widen the directed default when delivery needs are broader (gh#16 / gh#675):**\n\nThe cube's message taxonomy routes most prefixes DIRECTED to the Coordinator by default; the `to:` / `visibility:` you pass ALWAYS overrides that default. Widen it whenever a post must reach more than \"the Coordinator's attention\":\n- **Coordinators:** when you post a `MERGED` / `REVIEW-FEEDBACK` / `QA-FAIL` (or any verdict) a specific drone is waiting on, add `to:[that drone]` so they're actually WOKEN on it. Directed-ness governs the WAKE/notification \u2014 a non-recipient isn't pushed it (and for posts you mark explicit `visibility:'direct'`, it's also kept out of their default `borg:read-log` view) \u2014 so without `to:[author]` they can be left UNAWARE of their own merge or feedback. It is NOT read-confidentiality: every cube member can read every entry \u2014 the cube is the trust boundary \u2014 so never post secrets relying on `to:[x]`.\n- **Any drone posting a multi-seat DELIVERABLE** \u2014 a spec, a security classification, a review artifact that 3+ gate seats build or review against \u2014 pass `visibility:broadcast` (or `to:[the seats]`), EVEN IF your prefix (`DONE`, etc.) is a directed status class. Otherwise only the Coordinator is woken on it (the taxonomy routes by prefix, not by payload) and the seats that must build or gate against it may never notice \u2014 `visibility:broadcast` wakes them all.\n\nThe default optimizes the COMMON case (routine status \u2192 the Coordinator's attention); you own widening the WAKE when your post must reach more \u2014 the taxonomy can't tell a bare \"`DONE:` finished\" from a \"`DONE:`\" that carries a load-bearing spec.\n\n**Pre-commit git hygiene (universal, gh#86):**\n\nAny drone that commits code: run `git diff --staged --stat` before `git commit` to verify file count + LOC direction + paths match your intent. Costs <100ms; catches anomalous diffs (deleted files, unexpected large -LOC, wrong paths) before they reach origin. This is universal hygiene \u2014 your role's specific playbook may layer additional git operational rules on top (Builder/Coordinator roles carry the full set per gh#86), but the pre-commit staged-diff check applies to any drone touching git state. Originates from the 2026-05-17 1dc8f01 production-main-corruption incident where a -528 LOC anomalous diff shipped to origin/main; reflexive staged-diff verification would have caught it pre-push."}const B=k();function I(e){const o=typeof e=="string"?new Date(e):e,i=Date.now()-o.getTime();if(!Number.isFinite(i)||i<0)return"just now";const n=Math.floor(i/1e3);if(n<60)return`${n}s ago`;const t=Math.floor(n/60);if(t<60)return`${t}m ago`;const a=Math.floor(t/60);return a<24?`${a}h ago`:`${Math.floor(a/24)}d ago`}function O(e){return e==null||Array.isArray(e)&&e.length===0?"Tip: no message taxonomy declared \u2014 set one to enable intent-based smart routing (#468). Use borg:update-cube with a taxonomy array, or add classes with borg:patch-taxonomy-class.":""}function j(e,o){return e.drone?.label??o??null}let u=!1,h=null,p=null;function q(){u=!1,h=null,p=null}function P(e){const o=e??"",i=w.filter(n=>o.includes(n));return[...v,...i]}function N(e,o){return`rationale \u2192 borg:role-rationale ${JSON.stringify(e)} ${JSON.stringify(o)}`}function Q(e){const o=e.match(/borg:role-rationale\s+("(?:(?:\\.)|[^"\\])*")\s+("(?:(?:\\.)|[^"\\])*")/);if(!o)return null;try{return{role:JSON.parse(o[1]),section:JSON.parse(o[2])}}catch{return null}}const L=[...v,...w];function $(e,o){return x(o??"").map(t=>{if(t.kind!=="label"||t.heading==null||!t.heading.trim().toLowerCase().endsWith("rationale")||L.some(m=>t.body.includes(m)))return t.body;const s=t.body.indexOf(`
2
2
  `);return(s===-1?t.body+`
3
3
  `:t.body.slice(0,s+1))+N(e,t.heading)+`
4
4
  `}).join("")}function U(e,o={}){const i=o.mode??"full",n=e.roles.map(r=>`- **${r.name}**${r.is_default?" _(default)_":""} \u2014 ${r.short_description||"_(no short description)_"}`).join(`
5
- `),t=e.drones.map(r=>{const C=e.roles.find(_=>_.id===r.role_id),D=T(C?.name??"?",r.agent_kind);return`- **${r.label}** (${D}) \u2014 last seen ${I(new Date(r.last_seen))}`}).join(`
6
- `)||"_(no drones connected)_",a=new Map(e.drones.map(r=>[r.id,r])),s=new Map(e.roles.map(r=>[r.id,r])),g=e.recentLog.map(r=>M(r,a,s)).reverse().join(`
5
+ `),t=e.drones.map(r=>{const _=e.roles.find(D=>D.id===r.role_id),C=T(_?.name??"?",r.agent_kind);return`- **${r.label}** (${C}) \u2014 last seen ${I(new Date(r.last_seen))}`}).join(`
6
+ `)||"_(no drones connected)_",a=new Map(e.drones.map(r=>[r.id,r])),s=new Map(e.roles.map(r=>[r.id,r])),g=e.recentLog.map(r=>F(r,a,s)).reverse().join(`
7
7
 
8
8
  `)||"_(no activity yet)_",R=e.recentLog.length===0&&e.drones.length<=1?["## Getting started","","Welcome to your first cube. Here's how to get going:","",'1. Post your first activity: `borg:log message="Starting work on <your task>"`',"2. Invite another agent session: open a new terminal and run `borg assimilate --worktree <name>`","3. Check who's here: `borg:roster`","","---",""].join(`
9
- `):"",f=O(e.cube.message_taxonomy),c=e.role.detailed_description_hash??null,l=e.cube.directive_hash??null,E=e.role.detailed_description?$(e.role.name,e.role.detailed_description):"_(no detailed description set)_",S=e.cube.cube_directive||"_(none)_",y=i==="full"||c==null||c!==u,b=i==="full"||l==null||l!==p,A=i==="full"||!d,h=[R+`# Cube: ${e.cube.name} \u2014 ${e.drone.label}`,"",`**Your role:** ${e.role.name}`,""];return i==="lite"&&h.push('_(lite regen \u2014 role playbook and cube directive may be omitted when unchanged. If they\'re NOT in your current context (e.g. after a context-compaction), call `borg:regen mode="full"` to re-orient.)_',""),h.push("## Cube directive",b?S:"_(unchanged since your last full/lite regen; omitted in lite mode)_","",...f?[f,""]:[],`## Your role: ${e.role.name}`,y?E:["_(role playbook unchanged since your last full/lite regen; omitted in lite mode)_","",...P(e.role.detailed_description)].join(`
10
- `),"","## Roles in this cube",n,"","## Connected drones",t,"","## Recent activity",g),A&&(h.push("",k()),d=!0),y&&c!=null&&(u=c),b&&l!=null&&(p=l),h.join(`
11
- `)}function M(e,o,i){const n=o.get(e.drone_id),t=n?i.get(n.role_id):null,a=new Date(e.created_at).toISOString(),s=typeof e.id=="string"&&e.id.length>0?` [entry_id: ${e.id}]`:"";return`**[${a}]**${s} ${n?.label??"?"} (${t?.name??"?"}): ${e.message}`}export{F as DRONE_PLAYBOOK,q as __resetRegenSessionState,$ as compressRoleText,M as formatLogEntryMarkdown,N as formatRationalePointer,U as formatRegenMarkdown,k as getDronePlaybook,I as humanAgo,O as nullTaxonomyTip,Q as parseRationalePointer,j as regenWakePathDroneLabel};
9
+ `):"",f=O(e.cube.message_taxonomy),c=e.role.detailed_description_hash??null,l=e.cube.directive_hash??null,E=e.role.detailed_description?$(e.role.name,e.role.detailed_description):"_(no detailed description set)_",S=e.cube.cube_directive||"_(none)_",y=i==="full"||c==null||c!==h,b=i==="full"||l==null||l!==p,A=i==="full"||!u,d=[R+`# Cube: ${e.cube.name} \u2014 ${e.drone.label}`,"",`**Your role:** ${e.role.name}`,""];return i==="lite"&&d.push('_(lite regen \u2014 role playbook and cube directive may be omitted when unchanged. If they\'re NOT in your current context (e.g. after a context-compaction), call `borg:regen mode="full"` to re-orient.)_',""),d.push("## Cube directive",b?S:"_(unchanged since your last full/lite regen; omitted in lite mode)_","",...f?[f,""]:[],`## Your role: ${e.role.name}`,y?E:["_(role playbook unchanged since your last full/lite regen; omitted in lite mode)_","",...P(e.role.detailed_description)].join(`
10
+ `),"","## Roles in this cube",n,"","## Connected drones",t,"","## Recent activity",g),A&&(d.push("",k()),u=!0),y&&c!=null&&(h=c),b&&l!=null&&(p=l),d.join(`
11
+ `)}function F(e,o,i){const n=o.get(e.drone_id),t=n?i.get(n.role_id):null,a=new Date(e.created_at).toISOString(),s=typeof e.id=="string"&&e.id.length>0?` [entry_id: ${e.id}]`:"";return`**[${a}]**${s} ${n?.label??"?"} (${t?.name??"?"}): ${e.message}`}export{B as DRONE_PLAYBOOK,q as __resetRegenSessionState,$ as compressRoleText,F as formatLogEntryMarkdown,N as formatRationalePointer,U as formatRegenMarkdown,k as getDronePlaybook,I as humanAgo,O as nullTaxonomyTip,Q as parseRationalePointer,j as regenWakePathDroneLabel};
@@ -7,7 +7,6 @@
7
7
  * - Network failure handling with retry + exponential backoff
8
8
  * - Offline queue for pending operations
9
9
  */
10
- import type { RemoteResponse } from './types.js';
11
10
  import type { MessageTaxonomy, MessageTaxonomyClass } from './templates.js';
12
11
  export declare const API_URL: string;
13
12
  /**
@@ -25,6 +24,7 @@ export declare function parseRetryAfterMs(headerValue: string | null): number |
25
24
  * drones sharing one per-IP bucket don't retry in lockstep.
26
25
  */
27
26
  export declare function rateLimitWaitMs(retryAfterMs: number | null, attempt: number, capMs?: number, jitter?: () => number): number;
27
+ export declare function extractHttpErrorMessage(body: string): string;
28
28
  /**
29
29
  * Given an ALREADY-OBTAINED response, while it is a 429 and retries
30
30
  * remain, wait per `rateLimitWaitMs` (honoring the CURRENT response's
@@ -51,10 +51,6 @@ export declare function retryOn429(initialResponse: Response, doRequest: () => P
51
51
  * without duplicating the refresh-token plumbing.
52
52
  */
53
53
  export declare function getValidToken(): Promise<string>;
54
- /**
55
- * Call remote MCP tool with retry logic
56
- */
57
- export declare function callRemoteTool(toolName: string, args: Record<string, any>): Promise<RemoteResponse>;
58
54
  /**
59
55
  * Connect this client as a Drone to a Cube.
60
56
  *
@@ -358,8 +354,8 @@ export declare function reassignDrone(droneId: string, roleId: string): Promise<
358
354
  }>;
359
355
  /**
360
356
  * Fetch a cube's full detail: directive, roles (with detailed
361
- * descriptions, owner-only), and drones. Owner-scoped via the Bearer
362
- * token; no drone session needed.
357
+ * descriptions), and drones. Accessible to owners and active members via
358
+ * the Bearer token; no drone session needed.
363
359
  */
364
360
  export declare function getCube(cubeId: string): Promise<{
365
361
  id: string;
@@ -401,4 +397,5 @@ export declare function syncRoles(cubeId: string, templateName?: string, apply?:
401
397
  * Create subscription (returns checkout URL)
402
398
  */
403
399
  export declare function createSubscription(): Promise<string>;
400
+ export declare function createBillingPortalSession(): Promise<string>;
404
401
  //# sourceMappingURL=remote-client.d.ts.map
@@ -1 +1 @@
1
- import{getIdToken as y,getRefreshToken as b,clearTokens as g}from"./config.js";import{refreshIdToken as $,RefreshTokenInvalidError as x}from"./auth.js";import{consolePrefix as S}from"./console-prefix.js";import{debugLog as m}from"./debug.js";const _=process.env.BORG_API_URL||"https://api.borgmcp.ai",w=3,E=1e3,k=3e4,C=3,A=6e4;function P(e){if(e==null)return null;const n=e.trim();return/^\d+$/.test(n)?parseInt(n,10)*1e3:null}function I(e,n,t=A,o=()=>Math.random()*500){const s=e??1e3*(n+1);return Math.min(s,t)+o()}async function O(e,n,t){const o=t.maxRetries??C;let s=e,a=0;for(;s.status===429&&a<o;){const c=I(P(s.headers.get("Retry-After")),a,t.capMs,t.jitter);t.log?.(`rate limited (429); retrying in ${Math.round(c)}ms (attempt ${a+1}/${o})`),await t.sleep(c),a++,s=await n()}return s}function M(e){return Math.min(E*Math.pow(2,e),k)+Math.random()*1e3}function j(e){return new Promise(n=>setTimeout(n,e))}async function R(){let e=await y();if(!e){const n=await b();if(n)try{await $(n),e=await y()}catch(t){t instanceof x&&await g()}if(!e)throw new Error("Authentication required. Run: borg assimilate")}return e}async function T(){const e=await b();if(!e)return null;try{return await $(e),await y()}catch(n){return n instanceof x&&await g(),null}}async function q(e,n){let t=null,o=!1;for(let s=0;s<=w;s++)try{const a=await R(),c={...n,auth_token:a},d=await fetch(`${_}/mcp`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`client-${Date.now()}`,method:"tools/call",params:{name:e,arguments:c}})});if(d.status===401&&!o){if(o=!0,await T())continue;throw new Error("Authentication required. Run: borg assimilate")}if(!d.ok)throw new Error(`HTTP ${d.status}: ${await d.text()}`);const u=await d.json();if(u.error){const l=(u.error.message||"").toLowerCase();if((l.includes("auth")||l.includes("token"))&&!o){if(o=!0,await T())continue;throw new Error("Authentication required. Run: borg assimilate")}throw new Error(u.error.message||"Remote tool call failed")}return{success:!0,data:u.result}}catch(a){if(t=a,a.message?.includes("Authentication required"))throw a;if(s>=w)break;const c=M(s);console.error(`${S()}Retry ${s+1}/${w} after ${Math.round(c)}ms...`),await j(c)}throw new Error(`Failed after ${w} retries: ${t?.message}`)}async function r(e,n={}){let t=await R();const{droneSession:o,apiUrl:s,headers:a,...c}=n,d=s??_,u=(c.method??"GET").toUpperCase(),l=async p=>{const f={Authorization:`Bearer ${p}`,...a};o&&(f["X-Drone-Session"]=o),m(`\u2192 ${u} ${e}`);const h=await fetch(`${d}${e}`,{...c,headers:f});return m(`\u2190 ${h.status} ${u} ${e}`),h};let i=await l(t);if(i.status===401){const p=await T();p&&(t=p,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:j,log:p=>console.error(`${S()}${p}`)})),!i.ok){const p=await i.text();if(m(`\u2717 ${i.status} ${u} ${e}: ${p}`),i.status===429){const f=i.headers.get("Retry-After"),h=f?` (retry after ${f}s)`:"";throw new Error(`HTTP 429: rate limited${h}: ${p}`)}throw new Error(`HTTP ${i.status}: ${p}`)}return i}async function D(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 v(e,n){return await(await r("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:n})).json()}async function B(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 F(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 X(e,n,t){await r(`/api/drone/log/${t}/ack`,{method:"POST",body:JSON.stringify({kind:"ack"}),droneSession:e,apiUrl:n})}async function W(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 z(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 K(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 Q(){return await(await r("/api/cubes",{method:"GET"})).json()}async function V(){return await(await r("/api/templates",{method:"GET"})).json()}async function Y(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 Z(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 await(await r(`/api/drones/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role_id:n})})).json()}async function ie(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 ce(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 pe(){return await(await r("/api/subscription/status",{method:"GET"})).json()}async function ue(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 de(){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}export{_ as API_URL,X as ackLogEntry,K as appendLog,ce as applyTemplate,D as assimilate,q as callRemoteTool,pe as checkSubscriptionStatus,Y as createCube,ne as createRole,de as createSubscription,te as deleteCube,re as deleteRole,ie as getCube,v as getCubeInfo,B as getRoleInfo,F as getRoster,R as getValidToken,Q as listCubes,V as listTemplates,P as parseRetryAfterMs,se as patchRoleSection,ee as patchTaxonomyClass,I as rateLimitWaitMs,N as readLog,ae as reassignDrone,W as regen,O as retryOn429,z as roleRationale,ue as syncRoles,Z as updateCube,oe 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 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};
package/dist/setup.js CHANGED
@@ -1,37 +1,42 @@
1
1
  #!/usr/bin/env node
2
- import d from"prompts";import e from"chalk";import a from"open";import l from"which";import{authenticateWithGoogle as g}from"./auth.js";import{checkSubscriptionStatus as i,createSubscription as p}from"./remote-client.js";import{retrySubscriptionCheck as h}from"./subscription-retry.js";import{addMcpServer as b,addSessionStartHook as m,addUserPromptSubmitHook as f,addCodexMcpServer as y,addCodexSessionStartHook as w,addCodexUserPromptSubmitHook as C,isMcpServerConfigured as k,isCodexMcpServerConfigured as S}from"./config-utils.js";import{isAuthenticated as x}from"./config.js";import{handleVersionFlag as v}from"./version.js";import{initDebugFromArgv as $}from"./debug.js";async function A(){$(process.argv),v(),console.log(e.blue.bold(`
3
- \u25FC Borg MCP Setup Wizard \u25FC`));const c=process.argv.includes("--no-browser")||process.argv.includes("--device");let s=null,t=null;try{s=l.sync("claude")}catch{}try{t=l.sync("codex")}catch{}if(s&&console.log(e.gray(`Found Claude CLI: ${s}`)),t&&console.log(e.gray(`Found Codex CLI: ${t}`)),(s||t)&&console.log(""),!s&&!t&&(console.error(e.red(`\u25FC No supported agent CLI found
2
+ import h from"prompts";import e from"chalk";import g from"open";import u from"which";import{authenticateWithGoogle as b}from"./auth.js";import{checkSubscriptionStatus as c,createSubscription as m}from"./remote-client.js";import{retrySubscriptionCheck as d}from"./subscription-retry.js";import{addMcpServer as f,addSessionStartHook as y,addUserPromptSubmitHook as w,addCodexMcpServer as C,addCodexSessionStartHook as k,addCodexUserPromptSubmitHook as S,isMcpServerConfigured as x,isCodexMcpServerConfigured as F}from"./config-utils.js";import{isAuthenticated as v}from"./config.js";import{handleVersionFlag as $}from"./version.js";import{initDebugFromArgv as A}from"./debug.js";async function P(){A(process.argv),$(),console.log(e.blue.bold(`
3
+ \u25FC Borg MCP Setup Wizard \u25FC`));const i=process.argv.includes("--no-browser")||process.argv.includes("--device");let n=null,t=null;try{n=u.sync("claude")}catch{}try{t=u.sync("codex")}catch{}if(n&&console.log(e.gray(`Found Claude CLI: ${n}`)),t&&console.log(e.gray(`Found Codex CLI: ${t}`)),(n||t)&&console.log(""),!n&&!t&&(console.error(e.red(`\u25FC No supported agent CLI found
4
4
  `)),console.error(e.yellow("Please install Claude Code or Codex first:")),console.error(e.gray(" Claude Code: https://claude.ai/download")),console.error(e.gray(` Codex: https://developers.openai.com/codex
5
- `)),process.exit(1)),console.log(e.blue("\u25FC Agent CLI Integration")),s)try{k()||b(),m(),f(),console.log(e.green("\u25FC borg configured for Claude Code"))}catch(o){console.error(e.red(`
5
+ `)),process.exit(1)),console.log(e.blue("\u25FC Agent CLI Integration")),n)try{x()||f(),y(),w(),console.log(e.green("\u25FC borg configured for Claude Code"))}catch(o){console.error(e.red(`
6
6
  \u25FC Failed to configure Claude Code: ${o.message}
7
- `)),process.exit(1)}if(t)try{S()||y(),w(),C(),console.log(e.green("\u25FC borg configured for Codex"))}catch(o){console.error(e.red(`
7
+ `)),process.exit(1)}if(t)try{F()||C(),k(),S(),console.log(e.green("\u25FC borg configured for Codex"))}catch(o){console.error(e.red(`
8
8
  \u25FC Failed to configure Codex: ${o.message}
9
- `)),process.exit(1)}if(console.log(""),console.log(e.blue("\u25FC Google Authentication")),await x())console.log(e.green(`\u25FC Already authenticated
10
- `));else try{await g(c?{noBrowser:!0}:void 0)}catch(o){console.error(e.red(`
9
+ `)),process.exit(1)}if(console.log(""),console.log(e.blue("\u25FC Google Authentication")),await v())console.log(e.green(`\u25FC Already authenticated
10
+ `));else try{await b(i?{noBrowser:!0}:void 0)}catch(o){console.error(e.red(`
11
11
  \u25FC Authentication failed: ${o.message}
12
- `)),console.error(e.yellow("Re-run `borg setup` to try again.\n")),process.exit(1)}console.log(e.blue("\u25FC Subscription Check"));let n;try{n=await i()}catch(o){console.error(e.red(`
13
- \u25FC Failed to check subscription: ${o.message}
14
- `)),process.exit(1)}if(n=await h(n,{check:i,sleep:o=>new Promise(r=>setTimeout(r,o)),onRetry:(o,r)=>console.log(e.gray(`\u25FC Checking subscription... (attempt ${o}/${r})`))}),n.hasAccess)if(console.log(e.green("\u25FC Active subscription found")),n.expiresAt){const o=new Date(n.expiresAt);console.log(e.gray(` Expires: ${o.toLocaleDateString()}
15
- `))}else console.log("");else{console.log(e.green("\u25FC You're on the Free tier \u2014 permanent, no card needed: 1 cube + 3 drones + 100 req/hr.")),console.log(e.gray(`\u25FC Start using borgmcp right now. Upgrade any time for unlimited cubes + drones ($1/cube/month).
16
- `));const{subscribeMethod:o}=await d({type:"select",name:"subscribeMethod",message:"You're ready on the Free tier. Want to do more?",choices:[{title:"\u25FC Continue on the Free tier (recommended)",value:"skip",description:"Start now \u2014 1 cube, 3 drones, 100 req/hr. No payment required."},{title:"\u25FC Upgrade to Cube tier \u2014 $1/cube/month",value:"web",description:"Unlimited cubes + drones + 1000 req/hr. Opens the subscribe page in your browser."},{title:"\u25FC Quick Stripe checkout",value:"stripe",description:"Fast upgrade checkout in the browser"},{title:"\u25FC I already subscribed \u2014 re-check",value:"recheck",description:"Re-check now \u2014 a just-completed subscription can take a moment to activate"}]});switch(o){case"web":console.log(e.blue(`
17
- \u25FC Opening: https://borgmcp.ai/subscribe`));try{await a("https://borgmcp.ai/subscribe"),console.log(e.gray(`\u25FC Waiting for subscription (checking every 5s for 2 min)...
18
- `)),await u()}catch(r){console.error(e.yellow(`
19
- \u25FC ${r.message}`))}break;case"stripe":try{const r=await p();console.log(e.blue(`
20
- \u25FC Opening Stripe: ${r}`)),await a(r),console.log(e.gray(`\u25FC Waiting for subscription...
21
- `)),await u()}catch(r){console.error(e.red(`
12
+ `)),console.error(e.yellow("Re-run `borg setup` to try again.\n")),process.exit(1)}console.log(e.blue("\u25FC Subscription Check"));let s;try{s=await c()}catch(o){console.error(e.yellow(`
13
+ \u25FC Subscription check failed: ${o.message}`)),console.error(e.gray(`\u25FC Retrying before falling back to the Free tier...
14
+ `)),s={hasAccess:!1}}if(s=await d(s,{check:c,sleep:o=>new Promise(r=>setTimeout(r,o)),onRetry:(o,r)=>console.log(e.gray(`\u25FC Checking subscription... (attempt ${o}/${r})`))}),s.hasAccess)if(console.log(e.green("\u25FC Active subscription found")),s.expiresAt){const o=new Date(s.expiresAt);console.log(e.gray(` Expires: ${o.toLocaleDateString()}
15
+ `))}else console.log("");else{console.log(e.green("\u25FC You're on the Free tier \u2014 permanent, no card needed: 1 cube + 3 agent sessions + 100 req/hr.")),console.log(e.gray(`\u25FC Start using borgmcp right now. Upgrade any time: $1/month per cube, each cube adds 8 pooled agent sessions + 1000 req/hr.
16
+ `));const{subscribeMethod:o}=await h({type:"select",name:"subscribeMethod",message:"You're ready on the Free tier. Want to do more?",choices:[{title:"\u25FC Continue on the Free tier (recommended)",value:"skip",description:"Start now \u2014 1 cube, 3 agent sessions, 100 req/hr. No payment required."},{title:"\u25FC Upgrade to Cube tier \u2014 $1/month per cube",value:"web",description:"Each cube adds 8 pooled agent sessions + 1000 req/hr. Opens the subscribe page in your browser."},{title:"\u25FC Quick Stripe checkout",value:"stripe",description:"Fast upgrade checkout in the browser"},{title:"\u25FC I already subscribed \u2014 re-check",value:"recheck",description:"Re-check now \u2014 a just-completed subscription can take a moment to activate"}]});switch(o===void 0&&console.log(e.yellow(`
17
+ \u25FC No subscription option selected \u2014 continuing on the Free tier.
18
+ `)),o){case"web":console.log(e.blue(`
19
+ \u25FC Opening: https://borgmcp.ai/subscribe`));try{await g("https://borgmcp.ai/subscribe"),console.log(e.gray(`\u25FC Waiting for subscription (checking every 5s for 2 min)...
20
+ `)),await p()}catch(r){console.error(e.yellow(`
21
+ \u25FC ${r.message}`)),console.log(e.green(`\u25FC Continuing on the Free tier. Upgrade any time from https://borgmcp.ai/subscribe.
22
+ `))}break;case"stripe":try{const r=await m();console.log(e.blue(`
23
+ \u25FC Opening Stripe: ${r}`)),await g(r),console.log(e.gray(`\u25FC Waiting for subscription...
24
+ `)),await p()}catch(r){console.error(e.red(`
22
25
  \u25FC Failed to create checkout: ${r.message}
23
- `))}break;case"recheck":try{(await i()).hasAccess?console.log(e.green(`
26
+ `)),console.log(e.green(`\u25FC Continuing on the Free tier. Upgrade any time from https://borgmcp.ai/subscribe.
27
+ `))}break;case"recheck":try{let r;try{r=await c()}catch{r={hasAccess:!1}}r=await d(r,{check:c,sleep:a=>new Promise(l=>setTimeout(l,a)),onRetry:(a,l)=>console.log(e.gray(`\u25FC Re-checking subscription... (attempt ${a}/${l})`))}),r.hasAccess?console.log(e.green(`
24
28
  \u25FC Subscription found!
25
29
  `)):console.log(e.yellow(`
26
- \u25FC No subscription found
30
+ \u25FC No subscription found \u2014 continuing on the Free tier.
27
31
  `))}catch(r){console.error(e.red(`
28
32
  \u25FC Failed to recheck: ${r.message}
33
+ `)),console.log(e.green(`\u25FC Continuing on the Free tier.
29
34
  `))}break;case"skip":console.log(e.green(`
30
- \u25FC You're all set on the Free tier: 1 cube, 3 drones, 100 req/hr.
35
+ \u25FC You're all set on the Free tier: 1 cube, 3 agent sessions, 100 req/hr.
31
36
  `));break}}console.log(e.green.bold(`Setup complete!
32
37
  `)),console.log(e.yellow(`\u{1F504} Restart Claude Code/Codex (or open a new session) for the changes to take effect.
33
38
  `)),console.log(e.gray("\u25FC Next steps:")),console.log(e.gray('1. Run "borg" to start Claude Code or Codex with your cube')),console.log(e.gray(`2. Manage cubes and subscription at https://borgmcp.ai/dashboard
34
- `))}async function u(){for(let s=0;s<24;s++){await new Promise(t=>setTimeout(t,5e3));try{if((await i()).hasAccess){console.log(e.green(`\u25FC Subscription activated!
35
- `));return}}catch{}}throw new Error("Timeout - Run setup again after subscribing")}A().catch(c=>{console.error(e.red(`
36
- \u25FC Setup failed: ${c.message}
39
+ `))}async function p(){for(let n=0;n<24;n++){await new Promise(t=>setTimeout(t,5e3));try{if((await c()).hasAccess){console.log(e.green(`\u25FC Subscription activated!
40
+ `));return}}catch{}}throw new Error("Timeout - Run setup again after subscribing")}P().catch(i=>{console.error(e.red(`
41
+ \u25FC Setup failed: ${i.message}
37
42
  `)),process.exit(1)});
package/dist/templates.js CHANGED
@@ -166,7 +166,7 @@ Your job:
166
166
  - **Don't assume context.** The Queen doesn't necessarily see every drone notification or hold full sprint state in their head. Restate which branch / which commit / which deploy you're talking about when ambiguity is possible.
167
167
  - **Before dispatching work to a drone, verify their local git state.** Don't assume a base branch \u2014 different projects use \`main\`, \`master\`, \`develop\`, or per-team variants. The Coordinator either reads the cube's primary branch from the cube directive, detects it via \`git symbolic-ref refs/remotes/origin/HEAD\`, or asks the drone. PING: "What branch are you on? Working tree clean? Have you pulled origin?" If the drone is on a different branch than the dispatch requires OR has uncommitted local changes, surface that BEFORE dispatching, not at REVIEW-READY time.
168
168
  - **Reviewer sync nudge.** When you accept a review verdict, look for the merge-base + head SHA quoted in the REVIEW-APPROVED / QA-PASS / UX-APPROVED post. If a reviewer posts a verdict without quoting a SHA, ask them to re-confirm they're on the latest commit \u2014 verdicts without SHAs might be from stale checkouts.
169
- - **When in doubt about a drone's state, ask them \u2014 don't wait passively.** Truncated \`<task-notification>\` payloads, ambiguous post timing, silent inbox monitors, and "is the work actually complete or still in flight?" uncertainty all create dispatch hesitation. Default move: read the full entry via \`borg:read-log since=<timestamp>\` to disambiguate (preview truncation routinely cuts off the tail of a long post), or post a directed \`PING: <drone-label> \u2014 status on <task>?\` to wake them via inbox. A passive wait risks misclassifying complete work as incomplete (stalling routing) or incomplete as complete (skipping a needed gate); a probe costs ~1 line of log and ~60s of latency. Passive waiting is the Coordinator's most common failure mode \u2014 bias toward the probe.
169
+ - **When in doubt about a drone's state, ask them \u2014 don't wait passively.** Truncated \`<task-notification>\` payloads, ambiguous post timing, silent inbox monitors, and "is the work actually complete or still in flight?" uncertainty all create dispatch hesitation. Default move: drain \`borg:read-log unread_only=true\` until caught up to fetch the full entry (preview truncation routinely cuts off the tail of a long post), or post a directed \`PING: <drone-label> \u2014 status on <task>?\` to wake them via inbox. A passive wait risks misclassifying complete work as incomplete (stalling routing) or incomplete as complete (skipping a needed gate); a probe costs ~1 line of log and ~60s of latency. Passive waiting is the Coordinator's most common failure mode \u2014 bias toward the probe.
170
170
  - **When drones stop responding, reallocate so work keeps flowing \u2014 don't let the cube stall on an absent drone.** A drone is "unresponsive" when they've missed an ACK on a routing-class signal you sent \u22655 min ago AND their \`last_seen\` is stale relative to the rest of the swarm (10+ min behind the active drones). Don't wait indefinitely. Default move: \`borg:reassign-drone\` a responsive drone into the unresponsive one's role (or hand the specific in-flight work to a peer already in the same role), and log a \`REASSIGN: <drone-X> (Role) \u2192 <drone-Y> (Role) \u2014 reason: unresponsive since <time>\` entry so the cube has an audit trail. When the absent drone reconnects (you'll see a fresh \`ARRIVAL:\` from them, or a delayed late-ACK), post a \`RECONNECT-BRIEFING: <drone-label> \u2014 <one-line summary of what changed while you were gone: their role reassignment, current task state, sprint-level deltas they need>\` entry and re-evaluate role allocation \u2014 the cube may have shifted enough that they should land in a different role on return rather than reclaim the one you reassigned away. Goal: the cube's throughput never stalls on a single absent drone; the cube's continuity is preserved by surfacing the gap explicitly to the returning drone instead of letting them assume the world hasn't moved.
171
171
  - **Tool-call discipline (rate-limit awareness).** Upstream LLM-API rate-limits are per-session; heavy dispatch cycles can hit them. Bias toward consolidation:
172
172
  - **Bundle related posts into single entries.** Don't split ASSIGN + DISPATCH + DECISION across three sequential \`borg:log\` calls when they belong to the same coordination unit; combine into one multi-section post.
package/dist/types.d.ts CHANGED
@@ -6,9 +6,4 @@ export interface GoogleOAuthTokens {
6
6
  refresh_token?: string;
7
7
  expires_at: number;
8
8
  }
9
- export interface RemoteResponse {
10
- success: boolean;
11
- data?: any;
12
- error?: string;
13
- }
14
9
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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",
@@ -22,7 +22,8 @@
22
22
  "postinstall": "node dist/postinstall.js || true",
23
23
  "test:integration": "vitest run --config ./vitest.integration.config.ts",
24
24
  "safe-deploy": "bash ../scripts/safe-deploy.sh",
25
- "prepublishOnly": "npm run safe-deploy && npm run build && npm run test:integration && npm run minify"
25
+ "onboarding-smoke": "bash ../scripts/onboarding-smoke.sh",
26
+ "prepublishOnly": "npm run safe-deploy && npm run build && npm run test:integration && npm run minify && npm run onboarding-smoke"
26
27
  },
27
28
  "files": [
28
29
  "dist",
@@ -40,7 +41,7 @@
40
41
  "borg"
41
42
  ],
42
43
  "author": "Theodor Storm <theodor@byteventures.se>",
43
- "license": "Apache-2.0",
44
+ "license": "SEE LICENSE IN LICENSE",
44
45
  "homepage": "https://borgmcp.ai",
45
46
  "repository": {
46
47
  "type": "git",