borgmcp 1.0.23 → 1.0.25

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/README.md CHANGED
@@ -164,7 +164,14 @@ Then ask the agent for `borg:whoami` and `borg:roster` to verify the connection.
164
164
 
165
165
  ### Wake path warning
166
166
 
167
- If `borg:regen` or `borg:stream-status` reports a broken wake path, arm the inbox monitor command it prints. The monitor is what wakes the agent session when another drone posts to the cube.
167
+ If `borg:regen` or `borg:stream-status` reports a broken wake path, follow the
168
+ CLI-specific recovery it prints:
169
+
170
+ - Claude Code: arm the inbox monitor command. The monitor wakes the agent
171
+ session when another drone posts to the cube.
172
+ - Codex: check the remote-control socket status, relaunch with `borg --cli codex`
173
+ or `borg assimilate --cli codex` if needed, and run `borg:regen` manually when
174
+ returning to the session if no wake arrived.
168
175
 
169
176
  ## Development
170
177
 
@@ -9,6 +9,7 @@ export interface AssimilateFlags {
9
9
  here?: boolean;
10
10
  yes?: boolean;
11
11
  cli?: BorgCli;
12
+ backend?: string;
12
13
  }
13
14
  export interface AssimilateArgs {
14
15
  role: string | undefined;
@@ -80,6 +81,7 @@ export interface AssimilateDeps {
80
81
  role_id: string;
81
82
  hostname?: string | null;
82
83
  prior_drone_id?: string;
84
+ backend?: string | null;
83
85
  }) => Promise<AssimilateResult>;
84
86
  listTemplates: (apiUrl: string, token: string) => Promise<Array<{
85
87
  name: string;
@@ -99,6 +101,11 @@ export interface AssimilateDeps {
99
101
  previewIncludes: string;
100
102
  updatedAfter: number;
101
103
  }) => Promise<string | null>;
104
+ fetch: typeof fetch;
105
+ checkBackendReachable: (descriptor: string | null, fetchImpl: typeof fetch) => Promise<{
106
+ ok: boolean;
107
+ message?: string;
108
+ }>;
102
109
  }
103
110
  export declare function runAssimilate(args: AssimilateArgs, deps: AssimilateDeps): Promise<number>;
104
111
  /**
@@ -1,46 +1,47 @@
1
- import{dirname as J,basename as S}from"node:path";import{randomUUID as V}from"node:crypto";import{roleSlug as Q,matchRoleByName as X,pickDefaultRole as Z}from"./role-resolver.js";import{deriveCubeName as ee,parseGitRemote as te,sanitizeRemoteUrl as re}from"./cube-name.js";import{validateName as H}from"./name-validator.js";import{renderAssimilationWelcome as ne}from"./assimilate-welcome.js";import{shellEscape as oe}from"./shell-escape.js";import{withCodexCwdArg as ie}from"./codex-remote.js";import{buildAgentKickoffPrompt as ae,recordCodexWakeTarget as se,socketPathFromRemoteArgs as le}from"./codex-launch.js";import{perWorktreeBranchName as O,adoptWorktree as ce,computeWorktreePath as F}from"./worktree-lifecycle.js";import{codexBorgSessionConfigArgs as ue}from"./launch-gate.js";async function Te(r,e){if(r.role!==void 0){const t=H(r.role);if(!t.ok)return e.stderr(t.error+`
2
- `),1}if(r.flags.worktree!==void 0){const t=H(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
3
- `),1}let i=await e.getCachedAuth();if(!i){if(!e.isTTY()&&!r.flags.yes)return e.stderr("borg setup required and stdin is non-interactive. Run `borg setup` first in an interactive terminal, then `borg assimilate`.\n"),1;i=await e.runSetup()}const a=e.findProjectRoot(e.cwd());let n;if(r.flags.cubeName)n=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],a),o=t.status===0?t.stdout:null;if(n=ee(a,o),o){const c=re(o),m=c?te(c):null;c&&!m&&n&&e.stderr(`couldn't parse git remote '${c}' \u2014 using directory name '${n}' as cube name
4
- `)}}let s=null;if(n&&n.includes("@")&&n.includes(":")){const t=n.lastIndexOf(":");s={ownerEmail:n.substring(0,t),cubeName:n.substring(t+1)},n=s.cubeName}const p=e.cwd();e.stderr(`Checking your cubes\u2026
5
- `);let R;try{R=await e.listCubes(i.apiUrl,i.token)}catch(t){const o=t instanceof Error?t.message:String(t);if(o.includes("Authentication required")||o.includes("Authentication expired"))e.stderr(`Re-authenticating...
6
- `),i=await e.runSetup(),R=await e.listCubes(i.apiUrl,i.token);else throw t}const E=R.find(t=>t.name===n);if(!E&&s)return e.stderr(`No cube named '${s.cubeName}' accessible to you owned by '${s.ownerEmail}'. Did you accept their invite? See borgmcp.ai/dashboard.
7
- `),1;let l,A;if(E)l=await e.getCube(i.apiUrl,i.token,E.id),A=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const o=await e.listTemplates(i.apiUrl,i.token),c=["First drone joining a new cube. Apply a template?"];o.forEach((k,y)=>{const C=y===0?" (default)":"";c.push(` ${y+1}) ${k.name}${C} \u2014 ${k.description}`)}),c.push(` ${o.length+1}) skip \u2014 no template`);const m=(await e.prompt(c.join(`
1
+ import{dirname as Q,basename as S}from"node:path";import{randomUUID as X}from"node:crypto";import{roleSlug as Z,matchRoleByName as ee,pickDefaultRole as te}from"./role-resolver.js";import{deriveCubeName as re,parseGitRemote as ne,sanitizeRemoteUrl as oe}from"./cube-name.js";import{validateName as B}from"./name-validator.js";import{renderAssimilationWelcome as ie}from"./assimilate-welcome.js";import{shellEscape as ae}from"./shell-escape.js";import{withCodexCwdArg as se}from"./codex-remote.js";import{buildAgentKickoffPrompt as le,recordCodexWakeTarget as ce,socketPathFromRemoteArgs as ue}from"./codex-launch.js";import{perWorktreeBranchName as F,adoptWorktree as me,computeWorktreePath as M}from"./worktree-lifecycle.js";import{codexBorgSessionConfigArgs as fe}from"./launch-gate.js";import{resolveLaunchEnv as de}from"./backend-presets.js";async function je(r,e){if(r.role!==void 0){const t=B(r.role);if(!t.ok)return e.stderr(t.error+`
2
+ `),1}if(r.flags.worktree!==void 0){const t=B(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
3
+ `),1}let i=await e.getCachedAuth();if(!i){if(!e.isTTY()&&!r.flags.yes)return e.stderr("borg setup required and stdin is non-interactive. Run `borg setup` first in an interactive terminal, then `borg assimilate`.\n"),1;i=await e.runSetup()}const a=e.findProjectRoot(e.cwd());let n;if(r.flags.cubeName)n=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],a),o=t.status===0?t.stdout:null;if(n=re(a,o),o){const c=oe(o),m=c?ne(c):null;c&&!m&&n&&e.stderr(`couldn't parse git remote '${c}' \u2014 using directory name '${n}' as cube name
4
+ `)}}let s=null;if(n&&n.includes("@")&&n.includes(":")){const t=n.lastIndexOf(":");s={ownerEmail:n.substring(0,t),cubeName:n.substring(t+1)},n=s.cubeName}const v=e.cwd();e.stderr(`Checking your cubes\u2026
5
+ `);let E;try{E=await e.listCubes(i.apiUrl,i.token)}catch(t){const o=t instanceof Error?t.message:String(t);if(o.includes("Authentication required")||o.includes("Authentication expired"))e.stderr(`Re-authenticating...
6
+ `),i=await e.runSetup(),E=await e.listCubes(i.apiUrl,i.token);else throw t}const A=E.find(t=>t.name===n);if(!A&&s)return e.stderr(`No cube named '${s.cubeName}' accessible to you owned by '${s.ownerEmail}'. Did you accept their invite? See borgmcp.ai/dashboard.
7
+ `),1;let l,N;if(A)l=await e.getCube(i.apiUrl,i.token,A.id),N=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const o=await e.listTemplates(i.apiUrl,i.token),c=["First drone joining a new cube. Apply a template?"];o.forEach((k,y)=>{const R=y===0?" (default)":"";c.push(` ${y+1}) ${k.name}${R} \u2014 ${k.description}`)}),c.push(` ${o.length+1}) skip \u2014 no template`);const m=(await e.prompt(c.join(`
8
8
  `)+`
9
9
  [1]: `)).trim(),b=m===""?1:parseInt(m,10);if(Number.isNaN(b)||b<1||b>o.length+1)return e.stderr(`invalid choice "${m}"
10
10
  `),1;t=b<=o.length?o[b-1].name:void 0}else{if(!r.flags.yes)return e.stderr(`cube creation needs a template choice but stdin is non-interactive.
11
11
  Pass --template <name>, --no-template, or --yes (defaults to starter).
12
12
  `),1;t="starter"}e.stderr(n?`Creating cube '${n}'\u2026
13
13
  `:`Creating your cube\u2026
14
- `),l=await e.createCube(i.apiUrl,i.token,t?{name:n??void 0,template:t}:{name:n??void 0}),A=!0}let d;if(r.role!==void 0){if(d=X(l.roles,r.role),!d){const t=l.roles.map(m=>m.name).join(", "),o=ge(r.role,l.roles.map(m=>m.name)),c=o?` Did you mean "${o}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${l.name}". Available: ${t}.${c}
14
+ `),l=await e.createCube(i.apiUrl,i.token,t?{name:n??void 0,template:t}:{name:n??void 0}),N=!0}let f;if(r.role!==void 0){if(f=ee(l.roles,r.role),!f){const t=l.roles.map(m=>m.name).join(", "),o=be(r.role,l.roles.map(m=>m.name)),c=o?` Did you mean "${o}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${l.name}". Available: ${t}.${c}
15
15
  (Use --template <name> on first-drone setup or run \`borg:create-role\` from inside Claude.)
16
- `),1}}else if(d=Z(l.roles,{isFirstDrone:A}),!d)return e.stderr(`cube "${l.name}" has no default or human-seat role; cannot infer a role. Either pass a role argument explicitly (e.g. \`borg assimilate builder\`) or run \`borg:create-role\` from inside Claude to set up roles.
17
- `),1;const v=await e.getActiveCube();let N;if(v&&r.flags.here)if(v.cubeId===l.id)N=v.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
18
- `),1;e.stderr(`Joining cube '${l.name}' as ${d.name}\u2026
19
- `);let u;try{u=await e.assimilate(i.apiUrl,i.token,{cube_id:l.id,role_id:d.id,hostname:e.getHostname(),...N?{prior_drone_id:N}:{}})}catch(t){const o=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${o}
20
- `),1}const $=l.roles.find(t=>t.id===u.role_id)??d;u.reattached?e.stderr(`re-attached to existing seat ${u.drone_label} (session token rotated, no new drone minted)
21
- `):$.id!==d.id&&e.stderr(`Note: your invite didn't grant the "${d.name}" role \u2014 assimilated as "${$.name}" instead.
22
- `);const M=r.flags.worktree!==void 0||v!==null&&!r.flags.here;let f=null;if(M){const t=e.runSync("git",["rev-parse","--verify","HEAD"],a);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
16
+ `),1}}else if(f=te(l.roles,{isFirstDrone:N}),!f)return e.stderr(`cube "${l.name}" has no default or human-seat role; cannot infer a role. Either pass a role argument explicitly (e.g. \`borg assimilate builder\`) or run \`borg:create-role\` from inside Claude to set up roles.
17
+ `),1;const p=await e.getActiveCube();let T;if(p&&r.flags.here)if(p.cubeId===l.id)T=p.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
18
+ `),1;const x=r.flags.backend??f.default_backend??null;if(x){const t=await e.checkBackendReachable(x,e.fetch);if(!t.ok)return e.stderr(`${t.message}
19
+ `),1}e.stderr(`Joining cube '${l.name}' as ${f.name}\u2026
20
+ `);let u;try{u=await e.assimilate(i.apiUrl,i.token,{cube_id:l.id,role_id:f.id,hostname:e.getHostname(),backend:x,...T?{prior_drone_id:T}:{}})}catch(t){const o=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${o}
21
+ `),1}const $=l.roles.find(t=>t.id===u.role_id)??f;u.reattached?e.stderr(`re-attached to existing seat ${u.drone_label} (session token rotated, no new drone minted)
22
+ `):$.id!==f.id&&e.stderr(`Note: your invite didn't grant the "${f.name}" role \u2014 assimilated as "${$.name}" instead.
23
+ `);const G=r.flags.worktree!==void 0||p!==null&&!r.flags.here;let d=null;if(G){const t=e.runSync("git",["rev-parse","--verify","HEAD"],a);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
23
24
  Fix: create at least one commit (\`git commit --allow-empty -m "initial"\`)
24
25
  OR: pass --here to skip the sibling spawn and use the current directory
25
26
  `),1;e.runSync("git",["fetch","origin"],a);let o="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],a).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],a).status===0&&(o="origin/master");const m=t.stdout.trim(),b=e.runSync("git",["rev-parse",o],a).stdout.trim();m!==b&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${o} (${b.slice(0,7)}); new worktree will start on ${o}
26
- `);const k=S(a),y=r.flags.worktree??Q($.name);if(y.length===0)return e.stderr(`cannot derive a worktree name from role "${$.name}"; pass an explicit --worktree <name>
27
- `),1;const C=e.homedir();let h=F(C,k,y),W=2;for(;e.pathExists(h)||de(e,a,h);)h=F(C,k,y,W),W++;e.mkdirp(J(h));const D=O(S(h),k),L=e.runSync("git",["worktree","add","-b",D,h,o],a);if(L.status!==0)return e.stderr(`git worktree add failed: ${B(L.stderr)}
28
- `),1;e.stderr(`spawned sibling worktree at ${h} on branch ${D} (${o}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
29
- `),e.chdir(h),e.stderr(me(h,D,a)),f=e.cwd()}try{await e.setActiveCube({cubeId:u.cube_id,droneId:u.drone_id,name:l.name,sessionToken:u.session_token,droneLabel:u.drone_label,apiUrl:i.apiUrl})}catch(t){const o=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${o}
30
- `),f){const c=e.runSync("git",["worktree","remove","--force",f],a);c.status===0?e.stderr(`rolled back spawned worktree at ${f}
31
- `):e.stderr(`manual cleanup needed: \`git worktree remove --force ${f}\` (rollback attempt failed: ${B(c.stderr).trim()||"unknown"})
32
- `)}return 1}e.setTerminalTitle(u.drone_label,l.name);const Y=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(ne($.name,l.name,Y));const w=await e.resolveCli(r.flags.cli),g=e.cwd();try{e.installProjectSessionHook(g)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${g}; it will be re-attempted on the next borg launch
33
- `)}if(!f){e.runSync("git",["fetch","origin","--prune"],g);const t=O(S(g),S(a)),o=ce(e.runSync,g,t,"origin/main");o.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
34
- `),e.stderr(fe(g,t))):o.message&&e.stderr(`worktree sync: ${o.message}
27
+ `);const k=S(a),y=r.flags.worktree??Z($.name);if(y.length===0)return e.stderr(`cannot derive a worktree name from role "${$.name}"; pass an explicit --worktree <name>
28
+ `),1;const R=e.homedir();let g=M(R,k,y),O=2;for(;e.pathExists(g)||we(e,a,g);)g=M(R,k,y,O),O++;e.mkdirp(Q(g));const U=F(S(g),k),H=e.runSync("git",["worktree","add","-b",U,g,o],a);if(H.status!==0)return e.stderr(`git worktree add failed: ${Y(H.stderr)}
29
+ `),1;e.stderr(`spawned sibling worktree at ${g} on branch ${U} (${o}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
30
+ `),e.chdir(g),e.stderr(he(g,U,a)),d=e.cwd()}try{await e.setActiveCube({cubeId:u.cube_id,droneId:u.drone_id,name:l.name,sessionToken:u.session_token,droneLabel:u.drone_label,apiUrl:i.apiUrl})}catch(t){const o=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${o}
31
+ `),d){const c=e.runSync("git",["worktree","remove","--force",d],a);c.status===0?e.stderr(`rolled back spawned worktree at ${d}
32
+ `):e.stderr(`manual cleanup needed: \`git worktree remove --force ${d}\` (rollback attempt failed: ${Y(c.stderr).trim()||"unknown"})
33
+ `)}return 1}e.setTerminalTitle(u.drone_label,l.name);const q=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(ie($.name,l.name,q));const w=await e.resolveCli(r.flags.cli),h=e.cwd();try{e.installProjectSessionHook(h)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${h}; it will be re-attempted on the next borg launch
34
+ `)}if(!d){e.runSync("git",["fetch","origin","--prune"],h);const t=F(S(h),S(a)),o=me(e.runSync,h,t,"origin/main");o.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
35
+ `),e.stderr(ge(h,t))):o.message&&e.stderr(`worktree sync: ${o.message}
35
36
  `)}await e.probeMcpReady()||e.stderr(`warning: borg-mcp readiness probe did not complete within the timeout; launching ${w} anyway \u2014 the kickoff prompt's ToolSearch fallback will recover if the MCP server takes longer to start.
36
- `);const G=e.getInboxPath(u.cube_id,u.drone_id),T=w==="codex"?`borg-wake-${V()}`:null,q=w==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${G}\` so each event's task-notification title summarizes the new cube log entry (drone label, role, and first ~80 chars of the message body) \u2014 letting you triage events without reading the full body. `:"";let P,U=[],x,j,I=null,_=null;if(w==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
37
- `),P="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg:regen manually whenever you return, and expect only fallback wakeups until relaunch."):P="Codex wake-path capability check passed: remote-control socket established for this session.",U=t.args,j=Object.keys(t.env).length>0?t.env:void 0,I=le(t.args),_=t.server?.cleanup??null}x=[ae({cli:w,codexWakeNonce:T,monitorClause:q,codexWakePathClause:P})],w==="codex"&&(x=[...ue(),...U,...ie(x,g)]);const z=e.exec(w,x,g,{...j??{},BORG_SESSION:"1"});w==="codex"&&I&&T&&se({deps:e,cubeId:u.cube_id,droneId:u.drone_id,socketPath:I,cwd:g,previewNeedle:T,launchedAtSeconds:Math.floor(Date.now()/1e3)});const K=await z;if(_)try{_()}catch{}return f&&p!==f&&e.stderr(`
38
- Agent exited. You were working in ${f}; your shell is back in ${p}.
37
+ `);const z=e.getInboxPath(u.cube_id,u.drone_id),P=w==="codex"?`borg-wake-${X()}`:null,K=w==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${z}\` so each event's task-notification title summarizes the new cube log entry (drone label, role, and first ~80 chars of the message body) \u2014 letting you triage events without reading the full body. `:"";let I,L=[],C,_=null,j=null;const W=de(x),D={...process.env,...W.set,BORG_SESSION:"1"};for(const t of W.unset)delete D[t];if(w==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
38
+ `),I="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg:regen manually whenever you return, and expect only fallback wakeups until relaunch."):I="Codex wake-path capability check passed: remote-control socket established for this session.",L=t.args,Object.keys(t.env).length>0&&Object.assign(D,t.env),_=ue(t.args),j=t.server?.cleanup??null}C=[le({cli:w,codexWakeNonce:P,monitorClause:K,codexWakePathClause:I})],w==="codex"&&(C=[...fe(),...L,...se(C,h)]);const J=e.exec(w,C,h,D);w==="codex"&&_&&P&&ce({deps:e,cubeId:u.cube_id,droneId:u.drone_id,socketPath:_,cwd:h,previewNeedle:P,launchedAtSeconds:Math.floor(Date.now()/1e3)});const V=await J;if(j)try{j()}catch{}return d&&v!==d&&e.stderr(`
39
+ Agent exited. You were working in ${d}; your shell is back in ${v}.
39
40
  To return:
40
- cd ${oe(f)}
41
- `),K}function me(r,e,i){return`
41
+ cd ${ae(d)}
42
+ `),V}function he(r,e,i){return`
42
43
  WORKTREE STEERING: You are in worktree ${r} on branch ${e}. Do ALL work HERE \u2014 cut your feature branch (fix/.../feat/...) off ${e} in THIS worktree, use relative paths / your cwd. NEVER \`git -C ${i}\` or operate on the primary checkout ${i}: the same branch can't be checked out in two worktrees, so work created in the primary won't reach your wt-branch without manual surgery (cherry-pick/merge).
43
- `}function fe(r,e){return`
44
+ `}function ge(r,e){return`
44
45
  WORKTREE STEERING: This checkout is now on branch ${e}. Do ALL work HERE in ${r} \u2014 cut your feature branch (fix/.../feat/...) off ${e}, use relative paths / your cwd.
45
- `}function B(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function de(r,e,i){const a=r.runSync("git",["worktree","list","--porcelain"],e);return a.status!==0?!1:a.stdout.split(`
46
- `).some(n=>n===`worktree ${i}`)}function ge(r,e){if(e.length===0)return null;const i=r.toLowerCase();let a=null;for(const n of e){const s=he(i,n.toLowerCase());s<=2&&(a===null||s<a.distance)&&(a={name:n,distance:s})}return a?a.name:null}function he(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const i=new Array(e.length+1),a=new Array(e.length+1);for(let n=0;n<=e.length;n++)i[n]=n;for(let n=1;n<=r.length;n++){a[0]=n;for(let s=1;s<=e.length;s++){const p=r[n-1]===e[s-1]?0:1;a[s]=Math.min(a[s-1]+1,i[s]+1,i[s-1]+p)}for(let s=0;s<=e.length;s++)i[s]=a[s]}return i[e.length]}export{Te as runAssimilate,B as safeStderr,ge as suggestRoleName};
46
+ `}function Y(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function we(r,e,i){const a=r.runSync("git",["worktree","list","--porcelain"],e);return a.status!==0?!1:a.stdout.split(`
47
+ `).some(n=>n===`worktree ${i}`)}function be(r,e){if(e.length===0)return null;const i=r.toLowerCase();let a=null;for(const n of e){const s=ke(i,n.toLowerCase());s<=2&&(a===null||s<a.distance)&&(a={name:n,distance:s})}return a?a.name:null}function ke(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const i=new Array(e.length+1),a=new Array(e.length+1);for(let n=0;n<=e.length;n++)i[n]=n;for(let n=1;n<=r.length;n++){a[0]=n;for(let s=1;s<=e.length;s++){const v=r[n-1]===e[s-1]?0:1;a[s]=Math.min(a[s-1]+1,i[s]+1,i[s-1]+v)}for(let s=0;s<=e.length;s++)i[s]=a[s]}return i[e.length]}export{je as runAssimilate,Y as safeStderr,be as suggestRoleName};
@@ -1,3 +1,3 @@
1
- import{spawnSync as m,spawn as l}from"node:child_process";import{existsSync as f,mkdirSync as b}from"node:fs";import{hostname as h,homedir as C}from"node:os";import{createInterface as d}from"node:readline/promises";import{API_URL as u,getValidToken as p,listCubes as _,getCube as T,createCube as y,assimilate as w,listTemplates as k}from"./remote-client.js";import{findProjectRoot as g,getActiveCube as x,setActiveCube as v,inboxPathForDrone as S,setCodexWakeTarget as A}from"./cubes.js";import{authenticateWithGoogle as P}from"./auth.js";import{addProjectSessionStartHook as R}from"./config-utils.js";import{setTerminalTitle as U}from"./terminal-title.js";import{defaultCliChoiceDeps as j,resolveCliChoice as L}from"./cli-platform.js";import{prepareCodexRemoteLaunch as D,defaultCodexRemoteDeps as H}from"./codex-remote.js";import{findLoadedCodexThread as I}from"./codex-app-server.js";function K(){return{runSync:(e,r,o)=>{const t=m(e,r,{cwd:o,encoding:"utf-8"});return{status:t.status,stdout:t.stdout??"",stderr:t.stderr??""}},pathExists:e=>f(e),cwd:()=>process.cwd(),chdir:e=>process.chdir(e),homedir:()=>C(),mkdirp:e=>b(e,{recursive:!0}),exec:(e,r,o,t)=>new Promise((s,i)=>{const a=l(e,r,{cwd:o,stdio:"inherit",shell:!1,env:t?{...process.env,...t}:process.env});a.on("error",i),a.on("exit",n=>s(n??0))}),stderr:e=>process.stderr.write(e),stdout:e=>process.stdout.write(e),prompt:async e=>{const r=d({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}},isTTY:()=>process.stdin.isTTY===!0,getHostname:()=>h(),setTerminalTitle:(e,r)=>{U({label:e,cubeName:r},r)},getActiveCube:()=>x(),setActiveCube:e=>v(e),findProjectRoot:e=>g(e),installProjectSessionHook:e=>{R(e)},getCachedAuth:async()=>{try{return{token:await p(),apiUrl:u}}catch{return null}},runSetup:async()=>(await P(),{token:await p(),apiUrl:u}),listCubes:async(e,r)=>{const{cubes:o}=await _();return o.map(t=>({id:t.id,name:t.name}))},getCube:async(e,r,o)=>{const t=await T(o);return{id:t.id,name:t.name,roles:t.roles}},createCube:async(e,r,o)=>{const t=await y(o.name,"",o.template?{template:o.template}:void 0);return{id:t.id,name:t.name,roles:t.roles}},assimilate:async(e,r,o)=>{const t=await w({cube_id:o.cube_id,role_id:o.role_id,...o.prior_drone_id?{prior_drone_id:o.prior_drone_id}:{}},void 0,o.hostname??null);return{cube_id:t.cube.id,drone_id:t.drone.id,drone_label:t.drone.label,session_token:t.sessionToken,role_id:t.role.id,reattached:t.reattached===!0}},listTemplates:async(e,r)=>{const{templates:o}=await k();return o.map(t=>({name:t.name,description:t.description}))},getInboxPath:(e,r)=>S(e,r),probeMcpReady:()=>new Promise(e=>{const r=l("borg-mcp",[],{stdio:["pipe","pipe","pipe"],shell:!1});let o="",t=!1;const s=n=>{if(!t){t=!0;try{r.kill("SIGTERM")}catch{}e(n)}},i=setTimeout(()=>s(!1),2e3);r.on("error",()=>{clearTimeout(i),s(!1)}),r.on("exit",()=>{clearTimeout(i),s(t)}),r.stdout?.on("data",n=>{o+=n.toString("utf-8");for(const c of o.split(`
2
- `))if(c.includes('"protocolVersion"')&&c.includes('"result"')){clearTimeout(i),s(!0);return}});const a=JSON.stringify({jsonrpc:"2.0",id:1,method:"initialize",params:{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"borg-assimilate-probe",version:"0.9.3"}}});try{r.stdin?.write(a+`
3
- `)}catch{clearTimeout(i),s(!1)}}),resolveCli:e=>L(e,j(async r=>{const o=d({input:process.stdin,output:process.stdout});try{return await o.question(r)}finally{o.close()}},()=>process.stdin.isTTY===!0)),prepareCodexRemoteLaunch:()=>D(H()),setCodexWakeTarget:A,findLoadedCodexThread:I}}export{K as buildDefaultAssimilateDeps};
1
+ import{spawnSync as m,spawn as l}from"node:child_process";import{existsSync as f,mkdirSync as b}from"node:fs";import{hostname as h,homedir as C}from"node:os";import{createInterface as d}from"node:readline/promises";import{API_URL as u,getValidToken as p,listCubes as _,getCube as k,createCube as T,assimilate as y,listTemplates as w}from"./remote-client.js";import{findProjectRoot as g,getActiveCube as x,setActiveCube as v,inboxPathForDrone as R,setCodexWakeTarget as S}from"./cubes.js";import{authenticateWithGoogle as A}from"./auth.js";import{addProjectSessionStartHook as P}from"./config-utils.js";import{setTerminalTitle as U}from"./terminal-title.js";import{defaultCliChoiceDeps as j,resolveCliChoice as L}from"./cli-platform.js";import{prepareCodexRemoteLaunch as D,defaultCodexRemoteDeps as H}from"./codex-remote.js";import{findLoadedCodexThread as I}from"./codex-app-server.js";import{checkBackendReachable as G}from"./backend-presets.js";function Q(){return{runSync:(e,r,o)=>{const t=m(e,r,{cwd:o,encoding:"utf-8"});return{status:t.status,stdout:t.stdout??"",stderr:t.stderr??""}},pathExists:e=>f(e),cwd:()=>process.cwd(),chdir:e=>process.chdir(e),homedir:()=>C(),mkdirp:e=>b(e,{recursive:!0}),exec:(e,r,o,t)=>new Promise((i,s)=>{const a=l(e,r,{cwd:o,stdio:"inherit",shell:!1,env:t??process.env});a.on("error",s),a.on("exit",n=>i(n??0))}),stderr:e=>process.stderr.write(e),stdout:e=>process.stdout.write(e),prompt:async e=>{const r=d({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}},isTTY:()=>process.stdin.isTTY===!0,getHostname:()=>h(),setTerminalTitle:(e,r)=>{U({label:e,cubeName:r},r)},getActiveCube:()=>x(),setActiveCube:e=>v(e),findProjectRoot:e=>g(e),installProjectSessionHook:e=>{P(e)},getCachedAuth:async()=>{try{return{token:await p(),apiUrl:u}}catch{return null}},runSetup:async()=>(await A(),{token:await p(),apiUrl:u}),listCubes:async(e,r)=>{const{cubes:o}=await _();return o.map(t=>({id:t.id,name:t.name}))},getCube:async(e,r,o)=>{const t=await k(o);return{id:t.id,name:t.name,roles:t.roles}},createCube:async(e,r,o)=>{const t=await T(o.name,"",o.template?{template:o.template}:void 0);return{id:t.id,name:t.name,roles:t.roles}},assimilate:async(e,r,o)=>{const t=await y({cube_id:o.cube_id,role_id:o.role_id,...o.prior_drone_id?{prior_drone_id:o.prior_drone_id}:{},...o.backend!=null?{backend:o.backend}:{}},void 0,o.hostname??null);return{cube_id:t.cube.id,drone_id:t.drone.id,drone_label:t.drone.label,session_token:t.sessionToken,role_id:t.role.id,reattached:t.reattached===!0}},listTemplates:async(e,r)=>{const{templates:o}=await w();return o.map(t=>({name:t.name,description:t.description}))},getInboxPath:(e,r)=>R(e,r),probeMcpReady:()=>new Promise(e=>{const r=l("borg-mcp",[],{stdio:["pipe","pipe","pipe"],shell:!1});let o="",t=!1;const i=n=>{if(!t){t=!0;try{r.kill("SIGTERM")}catch{}e(n)}},s=setTimeout(()=>i(!1),2e3);r.on("error",()=>{clearTimeout(s),i(!1)}),r.on("exit",()=>{clearTimeout(s),i(t)}),r.stdout?.on("data",n=>{o+=n.toString("utf-8");for(const c of o.split(`
2
+ `))if(c.includes('"protocolVersion"')&&c.includes('"result"')){clearTimeout(s),i(!0);return}});const a=JSON.stringify({jsonrpc:"2.0",id:1,method:"initialize",params:{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"borg-assimilate-probe",version:"0.9.3"}}});try{r.stdin?.write(a+`
3
+ `)}catch{clearTimeout(s),i(!1)}}),resolveCli:e=>L(e,j(async r=>{const o=d({input:process.stdin,output:process.stdout});try{return await o.question(r)}finally{o.close()}},()=>process.stdin.isTTY===!0)),prepareCodexRemoteLaunch:()=>D(H()),setCodexWakeTarget:S,findLoadedCodexThread:I,fetch,checkBackendReachable:G}}export{Q as buildDefaultAssimilateDeps};
package/dist/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- import{createServer as w}from"http";import{URL as k}from"url";import g from"crypto";import P from"open";import{storeIdToken as d,storeRefreshToken as f,getRefreshToken as p}from"./config.js";import{cerr as r}from"./console-prefix.js";import{isNoBrowserEnv as O}from"./auth-env.js";import{requestDeviceCode as I,pollForDeviceToken as x}from"./device-auth.js";class G 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 m="675073910799-41pbe12rfhqemidh64h09s4q3e0udpgp.apps.googleusercontent.com",_="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",A="675073910799-6qmi73v5106dj1v0l22j2qnkh5r3e8fq.apps.googleusercontent.com",S="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",L="https://accounts.google.com/o/oauth2/v2/auth",E="https://oauth2.googleapis.com/token",$="https://oauth2.googleapis.com/revoke",T=["openid","email","profile"],Q=8e3,Z=9e3,y=300*1e3,v=y/6e4;function D(){const e=g.randomBytes(32).toString("base64url"),t=g.createHash("sha256").update(e).digest("base64url");return{verifier:e,challenge:t}}async function N(){return new Promise((e,t)=>{const s=w();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 U(){const e=await N(),t=new Promise((s,i)=>{const o=w((n,c)=>{const a=new k(n.url,`http://localhost:${e}`);if(a.pathname==="/callback"){const h=a.searchParams.get("code"),u=a.searchParams.get("error");if(u){c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
1
+ import{createServer as k}from"http";import{URL as w}from"url";import g from"crypto";import P from"open";import{storeIdToken as d,storeRefreshToken as f,getRefreshToken as p,isUsingKeychainBackend as O,migrateToFileBackendWithTokens as I}from"./config.js";import{cerr as t}from"./console-prefix.js";import{isNoBrowserEnv as A}from"./auth-env.js";import{requestDeviceCode as x,pollForDeviceToken as G}from"./device-auth.js";class L extends Error{errorCode;errorDescription;constructor(r,n){super(n?`Refresh token invalid (${r}): ${n}`:`Refresh token invalid (${r})`),this.errorCode=r,this.errorDescription=n,this.name="RefreshTokenInvalidError"}}class l extends Error{constructor(r){super(r),this.name="RefreshTransientError"}}const m="675073910799-41pbe12rfhqemidh64h09s4q3e0udpgp.apps.googleusercontent.com",_="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",S="675073910799-6qmi73v5106dj1v0l22j2qnkh5r3e8fq.apps.googleusercontent.com",$="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",N="https://accounts.google.com/o/oauth2/v2/auth",T="https://oauth2.googleapis.com/token",D="https://oauth2.googleapis.com/revoke",y=["openid","email","profile"],re=8e3,ne=9e3,E=300*1e3,v=E/6e4;function U(){const e=g.randomBytes(32).toString("base64url"),r=g.createHash("sha256").update(e).digest("base64url");return{verifier:e,challenge:r}}async function B(){return new Promise((e,r)=>{const n=k();n.listen(0,()=>{const o=n.address();if(o&&typeof o=="object"){const i=o.port;n.close(()=>e(i))}else n.close(()=>r(new Error("Failed to get assigned port")))}),n.on("error",r)})}async function F(){const e=await B(),r=new Promise((n,o)=>{const i=k((s,c)=>{const a=new w(s.url,`http://localhost:${e}`);if(a.pathname==="/callback"){const h=a.searchParams.get("code"),u=a.searchParams.get("error");if(u){c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
2
2
  <html>
3
3
  <body>
4
4
  <h1>\u25FC Authentication Failed</h1>
@@ -6,33 +6,34 @@ import{createServer as w}from"http";import{URL as k}from"url";import g from"cryp
6
6
  <p>You can close this window.</p>
7
7
  </body>
8
8
  </html>
9
- `),o.close(),i(new Error(`OAuth error: ${u}`));return}if(h){c.writeHead(200,{"Content-Type":"text/html"}),c.end(`
9
+ `),i.close(),o(new Error(`OAuth error: ${u}`));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>
13
13
  <p>You can close this window and return to your terminal.</p>
14
14
  </body>
15
15
  </html>
16
- `),o.close(),s(h);return}c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
16
+ `),i.close(),n(h);return}c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
17
17
  <html>
18
18
  <body>
19
19
  <h1>\u25FC Invalid Request</h1>
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 timed out after ${v} minutes \u2014 no authorization received from the browser. Re-run \`borg setup\` and complete the Google sign-in in the page that opens.`))},y).unref()});return{port:e,codePromise:t}}async function B(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:m,client_secret:_,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 b(e){try{await fetch($,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`token=${encodeURIComponent(e)}`})}catch{}}async function M(){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 b(e)),r("Generating PKCE challenge...");const t=D();r("Starting local callback server...");const{port:s,codePromise:i}=await U(),o=`http://localhost:${s}/callback`,n=new k(L);n.searchParams.set("client_id",m),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
- `);try{await P(n.toString())}catch(u){r(`Could not open a browser automatically: ${u?.message??"unknown"}`),r("Continue by opening the URL above manually.")}r(`Waiting for you to finish signing in (up to ${v} minutes)... this terminal continues automatically once you approve.`);const c=await i;r("Exchanging authorization code for tokens...");const a=await B(c,t.verifier,s),h=Date.now()+a.expires_in*1e3;await d(a.id_token,h),a.refresh_token?await f(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
+ `),i.close(),o(new Error("Missing authorization code"))}});i.listen(e,()=>{t(`Callback server listening on http://localhost:${e}`)}),setTimeout(()=>{i.close(),o(new Error(`Authentication timed out after ${v} minutes \u2014 no authorization received from the browser. Re-run \`borg setup\` and complete the Google sign-in in the page that opens.`))},E).unref()});return{port:e,codePromise:r}}async function M(e,r,n){const o=`http://localhost:${n}/callback`,i=await fetch(T,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:m,client_secret:_,code:e,code_verifier:r,grant_type:"authorization_code",redirect_uri:o})});if(!i.ok){const s=await i.text();throw new Error(`Failed to exchange code for tokens: ${s}`)}return await i.json()}async function b(e){try{await fetch(D,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`token=${encodeURIComponent(e)}`})}catch{}}async function K(){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 b(e)),t("Generating PKCE challenge...");const r=U();t("Starting local callback server...");const{port:n,codePromise:o}=await F(),i=`http://localhost:${n}/callback`,s=new w(N);s.searchParams.set("client_id",m),s.searchParams.set("redirect_uri",i),s.searchParams.set("response_type","code"),s.searchParams.set("scope",y.join(" ")),s.searchParams.set("code_challenge",r.challenge),s.searchParams.set("code_challenge_method","S256"),s.searchParams.set("access_type","offline"),s.searchParams.set("prompt","consent select_account"),t(`
26
+ \u{1F4F1} Opening browser for authorization...`),t("If browser does not open, visit:"),t(`${s.toString()}
27
+ `);try{await P(s.toString())}catch(u){t(`Could not open a browser automatically: ${u?.message??"unknown"}`),t("Continue by opening the URL above manually.")}t(`Waiting for you to finish signing in (up to ${v} minutes)... this terminal continues automatically once you approve.`);const c=await o;t("Exchanging authorization code for tokens...");const a=await M(c,r.verifier,n),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 F(e){return e?.noBrowser??O()}function C(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=A,o=S||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 H(e){return new Promise(t=>setTimeout(t,e))}async function q(e={fetch,sleep:H},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=C(t),i=await p();i&&(r("Revoking previous refresh_token to force fresh consent..."),await b(i));const o=await I(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 x(o,s,e),c=Date.now()+n.expires_in*1e3;await d(n.id_token,c),n.refresh_token?await f(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 q(e){return e?.noBrowser??A()}function C(e=process.env){const r=e.GOOGLE_DEVICE_CLIENT_ID?.trim(),n=e.GOOGLE_DEVICE_CLIENT_SECRET?.trim()||void 0;let o,i;if(r?(o=r,i=n):(o=S,i=$||void 0),!o)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:o,clientSecret:i,scopes:y}}function H(e){return new Promise(r=>setTimeout(r,e))}async function V(e={fetch,sleep:H},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 n=C(r),o=await p();o&&(t("Revoking previous refresh_token to force fresh consent..."),await b(o));const i=await x(n,e);t("To authorize Borg MCP on this machine:"),t(` 1. On any device with a browser, open: ${i.verification_url}`),t(` 2. Enter this code: ${i.user_code}
34
+ `),t("Waiting for authorization (this page can be open on your phone or laptop)...");const s=await G(i,n,e),c=Date.now()+s.expires_in*1e3;await d(s.id_token,c),s.refresh_token?await f(s.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 ee(e){return F(e)?q():M()}async function R(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 G("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;try{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}}else await d(n.id_token,c);return{ok:!0}}catch(a){return{ok:!1,kind:"transient",persistFailed:!0,error:new l(`Token refresh succeeded but saving it to the credential store failed (the keychain may be locked, or a background process can't write it): ${a?.message??"unknown"}`)}}}async function te(e){const t=await R(e,m,_);if(t.ok)return;if(t.kind==="transient"&&t.persistFailed)throw t.error;const s=C(),i=await R(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{G as RefreshTokenInvalidError,l as RefreshTransientError,q as authenticateWithDeviceFlow,ee as authenticateWithGoogle,C as buildDeviceAuthConfig,te as refreshIdToken,F as shouldUseDeviceFlow};
38
+ `)}async function oe(e){return q(e)?V():K()}async function R(e,r,n){const o={client_id:r,refresh_token:e,grant_type:"refresh_token"};n&&(o.client_secret=n);let i;try{i=await fetch(T,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(o)})}catch(a){return{ok:!1,kind:"transient",error:new l(`Network failure during token refresh: ${a?.message??"unknown"}`)}}if(!i.ok){let a=null;try{a=await i.json()}catch{return{ok:!1,kind:"transient",error:new l(`Token refresh failed with HTTP ${i.status} (non-JSON body)`)}}return i.status===400&&a?.error==="invalid_grant"?{ok:!1,kind:"invalid",error:new L("invalid_grant",a.error_description)}:{ok:!1,kind:"transient",error:new l(`Token refresh failed with HTTP ${i.status}${a?.error?` (${a.error})`:""}`)}}let s;try{s=await i.json()}catch(a){return{ok:!1,kind:"transient",error:new l(`Token refresh response unparseable: ${a?.message??"unknown"}`)}}if(!s.id_token||typeof s.expires_in!="number")return{ok:!1,kind:"transient",error:new l("Token refresh response missing id_token or expires_in")};const c=Date.now()+s.expires_in*1e3;try{if(s.refresh_token){const a=await p();await f(s.refresh_token);try{await d(s.id_token,c)}catch(h){if(a)try{await f(a)}catch{}throw h}}else await d(s.id_token,c);return{ok:!0}}catch(a){return await z(s,c)?{ok:!0}:{ok:!1,kind:"transient",persistFailed:!0,error:new l(`Token refresh succeeded but saving it to the credential store failed (the keychain may be locked, or a background process can't write it): ${a?.message??"unknown"}`)}}}async function z(e,r){if(!e.id_token)return!1;let n=!1;try{if(!await O())return!1;const o=e.refresh_token??await p();if(!o)return!1;n=await I({idToken:e.id_token,expiresAt:r,refreshToken:o})}catch{return!1}return n?(W(),!0):!1}function W(){t(`
39
+ \u26A0 Keychain write failed \u2014 migrated this session to the encrypted-file token store (~/.borg/credentials).`),t(" This is an AVAILABILITY fallback, NOT equivalent to the OS keychain."),t(" The file backend is obfuscation-grade: its key is derived from"),t(" hostname+username+platform (non-secret), so it is WEAKER at-rest than"),t(" the keychain \u2014 same-machine/root access can re-derive it and decrypt."),t(" To restore keychain storage, fix keychain write access and re-run `borg setup`.\n")}async function ie(e){const r=await R(e,m,_);if(r.ok)return;if(r.kind==="transient"&&r.persistFailed)throw r.error;const n=C(),o=await R(e,n.clientId,n.clientSecret);if(!o.ok)throw r.kind==="invalid"&&o.kind==="invalid"?o.error:r.kind==="transient"?r.error:o.error}export{L as RefreshTokenInvalidError,l as RefreshTransientError,V as authenticateWithDeviceFlow,oe as authenticateWithGoogle,C as buildDeviceAuthConfig,ie as refreshIdToken,q as shouldUseDeviceFlow};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Backend preset registry: parse descriptors and resolve launch env.
3
+ * Descriptor format: <kind>:<model> where kind ∈ {claude, ollama}.
4
+ * Parse on first colon only (model tags contain colons).
5
+ *
6
+ * CLIENT copy of the descriptor regex — byte-identical to the worker.
7
+ */
8
+ export declare const BACKEND_DESCRIPTOR_REGEX: RegExp;
9
+ export declare function parseBackend(descriptor: string): {
10
+ kind: 'claude' | 'ollama';
11
+ model: string;
12
+ };
13
+ export declare function resolveLaunchEnv(descriptor: string | null): {
14
+ set: Record<string, string>;
15
+ unset: string[];
16
+ };
17
+ export declare function checkBackendReachable(descriptor: string | null, fetchImpl: typeof fetch): Promise<{
18
+ ok: boolean;
19
+ message?: string;
20
+ }>;
21
+ //# sourceMappingURL=backend-presets.d.ts.map
@@ -0,0 +1 @@
1
+ const s=/^(claude|ollama):[A-Za-z0-9._:\/-]+$/;function o(e){const a=e.indexOf(":");if(a<0)throw new Error(`invalid backend descriptor: ${e} (expected <kind>:<model>)`);const t=e.substring(0,a),n=e.substring(a+1);if(t!=="claude"&&t!=="ollama")throw new Error(`invalid backend kind: ${t} (expected claude or ollama)`);return{kind:t,model:n}}function c(e){if(!e)return{set:{},unset:[]};const{kind:a,model:t}=o(e);return a==="claude"?{set:{ANTHROPIC_MODEL:t},unset:[]}:{set:{ANTHROPIC_BASE_URL:"http://localhost:11434",ANTHROPIC_MODEL:t,ANTHROPIC_AUTH_TOKEN:"ollama"},unset:["ANTHROPIC_API_KEY"]}}async function i(e,a){if(!e)return{ok:!0};const{kind:t}=o(e);if(t==="claude")return{ok:!0};const n=new AbortController,l=setTimeout(()=>n.abort(),3e3);try{return(await a("http://localhost:11434/api/tags",{signal:n.signal})).ok?{ok:!0}:{ok:!1,message:"Ollama not reachable at http://localhost:11434 \u2014 start it (ollama serve) or assimilate with a Claude backend."}}catch{return{ok:!1,message:"Ollama not reachable at http://localhost:11434 \u2014 start it (ollama serve) or assimilate with a Claude backend."}}finally{clearTimeout(l)}}export{s as BACKEND_DESCRIPTOR_REGEX,i as checkBackendReachable,o as parseBackend,c as resolveLaunchEnv};
@@ -32,6 +32,8 @@ export declare function probeCodexBridgeArmed(active: {
32
32
  getCodexWakeTarget?: typeof getCodexWakeTarget;
33
33
  checkBridge?: typeof checkCodexBridgeHealthy;
34
34
  }): Promise<boolean | null>;
35
+ /** gh#857 WI-2: last successful wake delivery time (for the heartbeat gate). */
36
+ export declare function getLastDeliveredAt(): number | null;
35
37
  export interface CodexWakeDeps {
36
38
  getActiveCube?: typeof getActiveCube;
37
39
  getCodexWakeTarget?: typeof getCodexWakeTarget;
@@ -41,7 +43,46 @@ export interface CodexWakeDeps {
41
43
  cwd?: () => string;
42
44
  sleep?: (ms: number) => Promise<void>;
43
45
  now?: () => number;
46
+ jitter?: () => number;
47
+ maxAttempts?: number;
48
+ isStreamOwner?: () => boolean;
49
+ onAppServerSocketDead?: () => void;
44
50
  }
45
51
  export declare function wakeCodexViaAppServer(reason?: string, env?: NodeJS.ProcessEnv, deps?: CodexWakeDeps): void;
52
+ /**
53
+ * gh#857 WI-2: codex /loop-equivalent heartbeat cadence. Tighter than claude's
54
+ * ~30-min /loop ScheduleWakeup because codex has no per-entry inbox-Monitor
55
+ * backstop — this periodic drain IS the independent second safety net.
56
+ */
57
+ export declare const CODEX_HEARTBEAT_CADENCE_MS: number;
58
+ /**
59
+ * gh#857 WI-2: one tick of the codex /loop-equivalent heartbeat — a periodic,
60
+ * independent re-engagement that injects a borg:read-log-unread DRAIN turn so an
61
+ * idle codex drone re-syncs even if every per-entry wake was missed. SKIPS when a
62
+ * delivery (per-entry wake, retry-drain, or a prior heartbeat) already landed
63
+ * within the cadence window (shouldFireHeartbeat), so an active cube with flowing
64
+ * wakes never gets a redundant injection. Unlike the per-entry path it does NOT
65
+ * consult deliveredWakeKeys — the cadence gate is the throttle, and the static
66
+ * drain prompt is intentionally re-delivered each idle window. Best-effort: a
67
+ * mid-turn thread / transient error / unresolved target just skips this tick (the
68
+ * next tick retries). Never throws.
69
+ */
70
+ export declare function fireCodexHeartbeatTick(deps?: CodexWakeDeps, cadenceMs?: number): Promise<void>;
71
+ /**
72
+ * gh#857 WI-2: start the codex /loop-equivalent heartbeat — a setInterval firing
73
+ * fireCodexHeartbeatTick every cadence. CODEX-ONLY: claude wakes via the tail-F
74
+ * inbox Monitor + /loop ScheduleWakeup and has NO app-server socket to inject
75
+ * into, so the heartbeat is intrinsically a codex mechanism. The gate reads
76
+ * agentKind LOCALLY from this child's own env (resolveSessionAgentKind →
77
+ * BORG_CODEX_REMOTE_WAKE), never a mutable/server-recorded field, so a mislabel
78
+ * can't silently defeat the backstop (gh#633 lesson). The timer is unref'd so the
79
+ * heartbeat alone never keeps the process alive. Returns the timer, or null when
80
+ * not a codex session. Injectable agentKind/intervalMs/tick for tests.
81
+ */
82
+ export declare function startCodexHeartbeat(opts?: {
83
+ agentKind?: 'claude' | 'codex';
84
+ intervalMs?: number;
85
+ tick?: () => void;
86
+ }): ReturnType<typeof setInterval> | null;
46
87
  export declare function resetCodexWakeForTests(): void;
47
88
  //# sourceMappingURL=codex-app-wake.d.ts.map
@@ -1,2 +1,2 @@
1
- import{getActiveCube as k,getCodexWakeTarget as w,setCodexWakeTarget as x}from"./cubes.js";import{CodexAppServerClient as T}from"./codex-app-server.js";import{checkCodexBridgeHealthy as m}from"./codex-remote.js";import{recordEventReceipt as p}from"./health-beat.js";import{codexAppServerSocketFromEnv as P,pickFreshThread as b,wakeTargetChanged as I}from"./codex-wake-resolve.js";const W="New Borg cube-log activity arrived.";function $(e){return`New Borg cube-log activity arrived:
2
- ${e}`}const _="Borg cube activity arrived while you were busy. Run `borg:read-log unread_only=true` and DRAIN \u2014 repeat until the returned page is under the limit and behind_by is 0 \u2014 so no entries are skipped.",A=5e3,E=15*6e4;function v(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function q(e=process.env){return v(e)?"codex":"claude"}function R(e=process.env){return v(e)?{enabled:!0}:{enabled:!1}}async function G(e,t={}){try{const n=await(t.getCodexWakeTarget??w)(e.cubeId,e.droneId);return n?(t.checkBridge??m)(n.socketPath):!1}catch{return null}}let l=!1;const h=[],u=new Set,f=[],S=100;let g=!1;function K(e){return new Promise(t=>setTimeout(t,e))}function C(e,t){return t.createClient?t.createClient(e):new T(e)}async function y(e,t){const r=P(t.env??process.env);if(r){const a=C(r,t);await a.connect();try{const o=await a.loadedThreadIds(),i=[];for(const s of o){const d=await a.readThread(s);d&&i.push({id:d.id,cwd:d.cwd,updatedAt:d.updatedAt})}const c=b(i,{cwd:(t.cwd??(()=>process.cwd()))()});return c?(await O(e,{socketPath:r,threadId:c},t),{socketPath:r,threadId:c}):null}finally{a.close()}}const n=await(t.getCodexWakeTarget??w)(e.cubeId,e.droneId);return n?{socketPath:n.socketPath,threadId:n.threadId}:null}async function O(e,t,r){try{const n=r.getCodexWakeTarget??w,a=r.setCodexWakeTarget??x,o=await n(e.cubeId,e.droneId),i=o?{socketPath:o.socketPath,threadId:o.threadId}:null;I(i,t)&&await a(e.cubeId,e.droneId,t)}catch{}}function Q(e=W,t=process.env,r={}){R(t).enabled&&(h.push({reason:e,deps:r}),!l&&(l=!0,D().finally(()=>{l=!1})))}async function D(){for(;h.length>0;){const e=h.shift();await B(e.reason,e.deps)}}async function B(e,t){try{const r=await(t.getActiveCube??k)();if(!r)return;const n=await y(r,t);if(!n)return;const{socketPath:a,threadId:o}=n,i=`${o}\0${e}`;if(u.has(i))return;const c=C(a,t);await c.connect();try{if((await c.readThread(o))?.status?.type==="active"){U(t);return}await c.startTurn(o,e),p(),M(i)}finally{c.close()}}catch{}}function U(e){g||(g=!0,F(e).finally(()=>{g=!1}))}async function F(e){const t=e.sleep??K,r=e.now??Date.now,n=r()+E;for(;r()<n;){await t(A);try{const a=await(e.getActiveCube??k)();if(!a)continue;const o=await y(a,e);if(!o)continue;const{socketPath:i,threadId:c}=o,s=C(i,e);await s.connect();try{if((await s.readThread(c))?.status?.type==="active")continue;await s.startTurn(c,_),p();return}finally{s.close()}}catch{}}}function Y(){l=!1,h.length=0,u.clear(),f.length=0,g=!1}function M(e){if(!u.has(e))for(u.add(e),f.push(e);f.length>S;){const t=f.shift();t&&u.delete(t)}}export{_ as CODEX_CATCHUP_PROMPT,W as CODEX_WAKE_PROMPT,$ as formatCodexWakePrompt,v as isCodexRemoteWakeEnabled,G as probeCodexBridgeArmed,Y as resetCodexWakeForTests,R as resolveCodexWakeTarget,q as resolveSessionAgentKind,Q as wakeCodexViaAppServer};
1
+ import{getActiveCube as C,getCodexWakeTarget as m,setCodexWakeTarget as R}from"./cubes.js";import{CodexAppServerClient as S}from"./codex-app-server.js";import{checkCodexBridgeHealthy as K}from"./codex-remote.js";import{recordEventReceipt as W}from"./health-beat.js";import{codexAppServerSocketFromEnv as O,pickFreshThread as M,wakeTargetChanged as B,wakeRetryBackoffMs as F,wakeRetryExpired as H,WAKE_RETRY_MAX_ATTEMPTS as N,shouldFireHeartbeat as j}from"./codex-wake-resolve.js";const L="New Borg cube-log activity arrived.";function ie(e){return`New Borg cube-log activity arrived:
2
+ ${e}`}const D="Borg cube activity arrived while you were busy. Run `borg:read-log unread_only=true` and DRAIN \u2014 repeat until the returned page is under the limit and behind_by is 0 \u2014 so no entries are skipped.";function P(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function X(e=process.env){return P(e)?"codex":"claude"}function q(e=process.env){return P(e)?{enabled:!0}:{enabled:!1}}async function ce(e,t={}){try{const r=await(t.getCodexWakeTarget??m)(e.cubeId,e.droneId);return r?(t.checkBridge??K)(r.socketPath):!1}catch{return null}}let f=!1;const h=[],l=new Set,w=[],$=100;let g=!1,k=null;function se(){return k}function T(e){k=(e.now??Date.now)()}let v=!1,y=!1;function p(){return y?!1:(y=!0,!0)}function A(){y=!1}function V(e){return e?.code==="ENOENT"}function Y(e){return new Promise(t=>setTimeout(t,e))}function x(e,t){return t.createClient?t.createClient(e):new S(e)}async function b(e,t){const n=O(t.env??process.env);if(n){const a=x(n,t);await a.connect();try{const o=await a.loadedThreadIds(),i=[];for(const u of o){const s=await a.readThread(u);s&&i.push({id:s.id,cwd:s.cwd,updatedAt:s.updatedAt})}const c=M(i,{cwd:(t.cwd??(()=>process.cwd()))()});return c?(await G(e,{socketPath:n,threadId:c},t),{socketPath:n,threadId:c}):null}finally{a.close()}}const r=await(t.getCodexWakeTarget??m)(e.cubeId,e.droneId);return r?{socketPath:r.socketPath,threadId:r.threadId}:null}async function G(e,t,n){try{const r=n.getCodexWakeTarget??m,a=n.setCodexWakeTarget??R,o=await r(e.cubeId,e.droneId),i=o?{socketPath:o.socketPath,threadId:o.threadId}:null;B(i,t)&&await a(e.cubeId,e.droneId,t)}catch{}}function ue(e=L,t=process.env,n={}){q(t).enabled&&(h.push({reason:e,deps:n}),!f&&(f=!0,Q().finally(()=>{f=!1})))}async function Q(){for(;h.length>0;){const e=h.shift();await U(e.reason,e.deps)}}async function U(e,t){if(!p()){E(t);return}try{const n=await(t.getActiveCube??C)();if(!n)return;const r=await b(n,t);if(!r)return;const{socketPath:a,threadId:o}=r,i=`${o}\0${e}`;if(l.has(i))return;const c=x(a,t);await c.connect();try{if((await c.readThread(o))?.status?.type==="active"){E(t);return}await c.startTurn(o,e),W(),Z(i),T(t)}finally{c.close()}}catch{E(t)}finally{A()}}function E(e){g||(g=!0,z(e).finally(()=>{g=!1}))}async function z(e){const t=e.sleep??Y,n=e.now??Date.now,r=e.jitter??(()=>Math.random()*500),a=e.maxAttempts??N,o=n();let i=0;for(;!H(o,n())&&i<a;)if(await t(F(i,r())),i++,!!p())try{const c=await(e.getActiveCube??C)();if(!c)continue;const u=await b(c,e);if(!u)continue;const{socketPath:s,threadId:I}=u,d=x(s,e);await d.connect();try{if((await d.readThread(I))?.status?.type==="active")continue;await d.startTurn(I,D),W(),T(e);return}finally{d.close()}}catch{}finally{A()}}const _=20*6e4;async function J(e={},t=_){if(v)return;const n=(e.now??Date.now)();if(j(k,n,t)&&!(e.isStreamOwner&&!e.isStreamOwner())&&p()){v=!0;try{const r=await(e.getActiveCube??C)();if(!r)return;const a=await b(r,e);if(!a)return;const o=x(a.socketPath,e);await o.connect();try{if((await o.readThread(a.threadId))?.status?.type==="active")return;await o.startTurn(a.threadId,D),T(e)}finally{o.close()}}catch(r){V(r)&&e.onAppServerSocketDead?.()}finally{v=!1,A()}}}function le(e={}){if((e.agentKind??X())!=="codex")return null;const n=e.intervalMs??_,r=e.tick??(()=>{J()}),a=setInterval(r,n);return a.unref?.(),a}function de(){f=!1,h.length=0,l.clear(),w.length=0,g=!1,k=null,v=!1,y=!1}function Z(e){if(!l.has(e))for(l.add(e),w.push(e);w.length>$;){const t=w.shift();t&&l.delete(t)}}export{D as CODEX_CATCHUP_PROMPT,_ as CODEX_HEARTBEAT_CADENCE_MS,L as CODEX_WAKE_PROMPT,J as fireCodexHeartbeatTick,ie as formatCodexWakePrompt,se as getLastDeliveredAt,P as isCodexRemoteWakeEnabled,ce as probeCodexBridgeArmed,de as resetCodexWakeForTests,q as resolveCodexWakeTarget,X as resolveSessionAgentKind,le as startCodexHeartbeat,ue as wakeCodexViaAppServer};
@@ -66,4 +66,38 @@ export declare function wakeTargetChanged(existing: {
66
66
  socketPath: string;
67
67
  threadId: string;
68
68
  }): boolean;
69
+ /** Base backoff for the first retry of a dropped/deferred wake. */
70
+ export declare const WAKE_RETRY_BASE_MS = 5000;
71
+ /** Backoff ceiling — a wedged thread is retried at most this often. */
72
+ export declare const WAKE_RETRY_CAP_MS = 60000;
73
+ /**
74
+ * Age cap: a pending wake older than this is given up (dropped from the queue).
75
+ * Generous on purpose — the WI-2 heartbeat is the backstop beyond it, and the
76
+ * server read-cursor means the next delivery (heartbeat or a fresh entry) drains
77
+ * everything anyway, so an aged-out single wake is never a permanent miss.
78
+ */
79
+ export declare const WAKE_RETRY_MAX_AGE_MS: number;
80
+ /**
81
+ * Hard iteration ceiling for the retry-drain loop — a defensive belt ALONGSIDE
82
+ * the time-based age cap. In prod the age cap (real clock) terminates the loop in
83
+ * ~45-50 attempts; this ceiling only matters if the clock fails to advance
84
+ * (pathological / a non-advancing injected clock in tests) where a time-only
85
+ * guard would hot-spin forever. Set far above any real attempt count.
86
+ */
87
+ export declare const WAKE_RETRY_MAX_ATTEMPTS = 1000;
88
+ /**
89
+ * Exponential backoff (ms) for the Nth retry of a pending wake (0-based),
90
+ * doubling from WAKE_RETRY_BASE_MS, saturating at WAKE_RETRY_CAP_MS, plus
91
+ * caller-supplied jitter so co-located sibling drones don't retry in lockstep.
92
+ */
93
+ export declare function wakeRetryBackoffMs(attempts: number, jitter?: number): number;
94
+ /** True once a pending wake has outlived the age cap (give up; heartbeat backstops). */
95
+ export declare function wakeRetryExpired(firstEnqueuedAt: number, now: number, maxAgeMs?: number): boolean;
96
+ /**
97
+ * WI-2 double-fire avoidance: the periodic heartbeat fires only when no wake (or
98
+ * prior heartbeat) delivery landed within the cadence window — so an active cube
99
+ * with flowing per-entry wakes doesn't get redundant heartbeat injections. A
100
+ * never-delivered seat (null) always fires.
101
+ */
102
+ export declare function shouldFireHeartbeat(lastDeliveredAt: number | null, now: number, cadenceMs: number): boolean;
69
103
  //# sourceMappingURL=codex-wake-resolve.d.ts.map
@@ -1 +1 @@
1
- const u="BORG_CODEX_APP_SERVER_SOCKET";function p(e=process.env){const t=e[u];return t&&t.length>0?t:null}function f(e){return["-c",`mcp_servers.borg.env.${u}="${e}"`]}function s(e,t){if(e.length===0)return null;if(e.length===1)return e[0].id;const r=e.filter(n=>n.cwd===t.cwd),o=r.length>0?r:e;let c=o[0];for(const n of o)(n.updatedAt??0)>(c.updatedAt??0)&&(c=n);return c.id}function a(e,t){const r={};let o=!1;for(const[c,n]of Object.entries(e)){if(t(n.socketPath)===!1){o=!0;continue}r[c]=n}return{targets:r,changed:o}}function d(e,t){return!e||e.socketPath!==t.socketPath||e.threadId!==t.threadId}export{u as BORG_CODEX_APP_SERVER_SOCKET_ENV,f as codexAppServerSocketConfigArgs,p as codexAppServerSocketFromEnv,s as pickFreshThread,a as pruneDeadWakeTargets,d as wakeTargetChanged};
1
+ const u="BORG_CODEX_APP_SERVER_SOCKET";function a(t=process.env){const e=t[u];return e&&e.length>0?e:null}function f(t){return["-c",`mcp_servers.borg.env.${u}="${t}"`]}function E(t,e){if(t.length===0)return null;if(t.length===1)return t[0].id;const r=t.filter(o=>o.cwd===e.cwd),n=r.length>0?r:t;let c=n[0];for(const o of n)(o.updatedAt??0)>(c.updatedAt??0)&&(c=o);return c.id}function i(t,e){const r={};let n=!1;for(const[c,o]of Object.entries(t)){if(e(o.socketPath)===!1){n=!0;continue}r[c]=o}return{targets:r,changed:n}}function d(t,e){return!t||t.socketPath!==e.socketPath||t.threadId!==e.threadId}const p=5e3,_=6e4,s=45*6e4,x=1e3;function A(t,e=0){const r=Math.max(0,t),n=p*2**r;return Math.min(n,_)+e}function R(t,e,r=s){return e-t>=r}function l(t,e,r){return t===null?!0:e-t>=r}export{u as BORG_CODEX_APP_SERVER_SOCKET_ENV,p as WAKE_RETRY_BASE_MS,_ as WAKE_RETRY_CAP_MS,s as WAKE_RETRY_MAX_AGE_MS,x as WAKE_RETRY_MAX_ATTEMPTS,f as codexAppServerSocketConfigArgs,a as codexAppServerSocketFromEnv,E as pickFreshThread,i as pruneDeadWakeTargets,l as shouldFireHeartbeat,A as wakeRetryBackoffMs,R as wakeRetryExpired,d as wakeTargetChanged};
package/dist/config.d.ts CHANGED
@@ -1,3 +1,41 @@
1
+ import { type TokenBackend } from './token-store.js';
2
+ /**
3
+ * gh#860: is THIS process's selected persistent backend the OS keychain? The
4
+ * runtime-fallback (auth.ts) gates on this so a keychain WRITE failure migrates
5
+ * to file ONLY from the keychain — a write failure already on the file backend
6
+ * is a real disk problem, not a locked keychain, and must NOT loop.
7
+ */
8
+ export declare function isUsingKeychainBackend(): Promise<boolean>;
9
+ /**
10
+ * gh#860: runtime fallback — re-point THIS process's persistent backend to the
11
+ * encrypted-file backend after a keychain WRITE failure (the temporal #858 case:
12
+ * keychain worked at setup, an aged background child later loses write access).
13
+ * This is an in-memory, per-process switch — NOT a persisted setting: keychain
14
+ * stays the default for every other install and the next fresh process re-probes.
15
+ * The durable opt-in (BORG_TOKEN_STORE=file) is the persistent counterpart.
16
+ *
17
+ * ATOMIC (gh#860 SR HIGH 3bed8571): build the file backend, write ALL token
18
+ * accounts to it, and commit the process backend switch (backendPromise) ONLY
19
+ * after every write succeeds. On any write failure: best-effort roll back the
20
+ * partial file write and DO NOT commit — the process stays on its current
21
+ * (keychain) backend, so a failed migration can never SILENTLY leave the process
22
+ * file-backed (obfuscation-grade) without the caller's at-rest warning, nor leave
23
+ * a partial credential behind. Returns true iff the tokens are durably saved to
24
+ * file (caller then warns about the at-rest tradeoff); false leaves the process
25
+ * exactly as it was (caller falls back to #858's transient surface).
26
+ *
27
+ * The file backend is obfuscation-grade (token-crypto.ts) — weaker at-rest than
28
+ * the keychain. On a true return the caller MUST surface that tradeoff.
29
+ */
30
+ export declare function migrateToFileBackendWithTokens(tokens: {
31
+ idToken: string;
32
+ expiresAt: number;
33
+ refreshToken?: string;
34
+ }, deps?: {
35
+ fileBackend?: TokenBackend;
36
+ }): Promise<boolean>;
37
+ /** Test-only: force the memoized backend so migration atomicity is testable. */
38
+ export declare function __setBackendForTest(backend: TokenBackend | null): void;
1
39
  /**
2
40
  * Store Google OAuth ID token securely in the selected backend.
3
41
  */
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import o from"os";import m from"path";import{promises as r}from"fs";import{isKeyringAvailable as w}from"./auth-env.js";import{deriveMachineKey as p}from"./token-crypto.js";import{makeKeychainBackend as y,makeEncryptedFileBackend as h,selectTokenBackend as g,readCallerManagedIdToken as T}from"./token-store.js";const i="google-id-token",c="google-refresh-token",s="token-expiry";function E(){return m.join(o.homedir(),".borg","credentials")}const F={readFile:e=>r.readFile(e,"utf8"),writeFile:async(e,t,n)=>{await r.writeFile(e,t,{mode:n}),await r.chmod(e,n)},mkdir:async(e,t)=>{await r.mkdir(e,{recursive:!0,mode:t})},rename:(e,t)=>r.rename(e,t),createExclusive:async(e,t)=>{try{return await r.writeFile(e,t,{flag:"wx",mode:384}),!0}catch(n){if(n?.code==="EEXIST")return!1;throw n}},removeFile:async e=>{try{await r.unlink(e)}catch(t){if(t?.code!=="ENOENT")throw t}},fileAgeMs:async e=>{try{const t=await r.stat(e);return Date.now()-t.mtimeMs}catch(t){if(t?.code==="ENOENT")return null;throw t}}};function b(e){const t=e?.trim().toLowerCase();if(t==="keychain")return"keychain";if(t==="file"||t==="encrypted-file")return"file"}let l=null;function a(){return l||(l=g({keyringAvailable:()=>w(),makeKeychain:()=>y(),makeFile:()=>h({filePath:E(),key:p({hostname:o.hostname(),username:o.userInfo().username,platform:process.platform}),fs:F}),forced:b(process.env.BORG_TOKEN_STORE)})),l}function u(){return T({env:process.env,readFile:e=>r.readFile(e,"utf8")})}async function K(e,t){const n=await a();await n.set(i,e),await n.set(s,t.toString())}async function _(e){await(await a()).set(c,e)}async function x(){const e=await u();if(e)return e;const t=await a(),n=await t.get(i),d=await t.get(s);if(!n||!d)return null;const f=parseInt(d,10),k=Date.now();return f-k<300*1e3?null:n}async function R(){return await u()?null:(await a()).get(c)}async function M(){const e=await a();await e.delete(i),await e.delete(c),await e.delete(s)}async function S(){return await x()!==null}export{M as clearTokens,x as getIdToken,R as getRefreshToken,S as isAuthenticated,K as storeIdToken,_ as storeRefreshToken};
1
+ import k from"os";import y from"path";import{promises as r}from"fs";import{isKeyringAvailable as T}from"./auth-env.js";import{deriveMachineKey as g}from"./token-crypto.js";import{makeKeychainBackend as E,makeEncryptedFileBackend as x,selectTokenBackend as p,readCallerManagedIdToken as F}from"./token-store.js";const u="google-id-token",f="google-refresh-token",d="token-expiry";function v(){return y.join(k.homedir(),".borg","credentials")}const b={readFile:e=>r.readFile(e,"utf8"),writeFile:async(e,t,n)=>{await r.writeFile(e,t,{mode:n}),await r.chmod(e,n)},mkdir:async(e,t)=>{await r.mkdir(e,{recursive:!0,mode:t})},rename:(e,t)=>r.rename(e,t),createExclusive:async(e,t)=>{try{return await r.writeFile(e,t,{flag:"wx",mode:384}),!0}catch(n){if(n?.code==="EEXIST")return!1;throw n}},removeFile:async e=>{try{await r.unlink(e)}catch(t){if(t?.code!=="ENOENT")throw t}},fileAgeMs:async e=>{try{const t=await r.stat(e);return Date.now()-t.mtimeMs}catch(t){if(t?.code==="ENOENT")return null;throw t}}};function N(e){const t=e?.trim().toLowerCase();if(t==="keychain")return"keychain";if(t==="file"||t==="encrypted-file")return"file"}function w(e){return{keyringAvailable:()=>T(),makeKeychain:()=>E(),makeFile:()=>x({filePath:v(),key:g({hostname:k.hostname(),username:k.userInfo().username,platform:process.platform}),fs:b}),forced:e}}let c=null;function o(){return c||(c=p(w(N(process.env.BORG_TOKEN_STORE)))),c}async function S(){return(await o()).name==="keychain"}async function R(e,t={}){const n=t.fileBackend??await p(w("file")),i=[];e.refreshToken!==void 0&&i.push([f,e.refreshToken]),i.push([u,e.idToken]),i.push([d,e.expiresAt.toString()]);const s=[];try{for(const[a,l]of i){const h=await n.get(a);await n.set(a,l),s.push({account:a,prior:h})}}catch{for(const{account:a,prior:l}of s.reverse())try{l===null?await n.delete(a):await n.set(a,l)}catch{}return!1}return c=Promise.resolve(n),!0}function M(e){c=e?Promise.resolve(e):null}function m(){return F({env:process.env,readFile:e=>r.readFile(e,"utf8")})}async function P(e,t){const n=await o();await n.set(u,e),await n.set(d,t.toString())}async function D(e){await(await o()).set(f,e)}async function O(){const e=await m();if(e)return e;const t=await o(),n=await t.get(u),i=await t.get(d);if(!n||!i)return null;const s=parseInt(i,10),a=Date.now();return s-a<300*1e3?null:n}async function U(){return await m()?null:(await o()).get(f)}async function W(){const e=await o();await e.delete(u),await e.delete(f),await e.delete(d)}async function X(){return await O()!==null}export{M as __setBackendForTest,W as clearTokens,O as getIdToken,U as getRefreshToken,X as isAuthenticated,S as isUsingKeychainBackend,R as migrateToFileBackendWithTokens,P as storeIdToken,D as storeRefreshToken};
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
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 X,ListToolsRequestSchema as z,ListPromptsRequestSchema as G,GetPromptRequestSchema as Y}from"@modelcontextprotocol/sdk/types.js";import{getCubeInfo as J,getRoleInfo as j,getRoster as Z,readLog as ee,appendLog as te,submitReport as re,ackLogEntry as oe,regen as O,listCubes as ie,createCube as ne,updateCube as T,deleteCube as se,createRole as ae,updateRole as ce,patchRoleSection as $,patchTaxonomyClass as A,deleteRole as le,reassignDrone as de,evictDrone as pe,getCube as w,checkSubscriptionStatus as ue,createBillingPortalSession as me,createSubscription as he,syncRoles as be,applyTemplate as ge,whoami as ye,roleRationale as fe,getValidToken as we}from"./remote-client.js";import{startHealthBeatTick as _e}from"./health-beat.js";import{getTemplate as U,listTemplateNames as S,resolveCubeDirectiveForCreate as ve,resolveCubeDirectiveForApply as xe,resolveMessageTaxonomyForCreate as ke}from"./templates.js";import{activeCubeWithFreshRegenIdentity as P,getActiveCube as y,setActiveCube as N,inboxPathForDrone as I}from"./cubes.js";import{addSessionStartHook as $e,addUserPromptSubmitHook as Ue}from"./config-utils.js";import{humanAgo as L,formatLogEntryMarkdown as Se,formatRegenMarkdown as M,getDronePlaybook as Ie,nullTaxonomyTip as qe,regenWakePathDroneLabel as Ee}from"./regen-format.js";import{startLogStream as Ce,getStreamStatus as q}from"./log-stream.js";import{renderRoleList as Re}from"./list-roles-render.js";import{getPackageVersion as _,getOnDiskVersion as De,handleVersionFlag as je}from"./version.js";import{renderStreamStatus as Oe,checkInboxMonitorHealthy as E,formatWakePathPrefix as Te,shouldShowWakePathWarning as Ae}from"./stream-status.js";import{formatRoleAgentLabel as Pe,renderRoster as Ne}from"./roster-render.js";import{resolveDroneIdByLabel as Le,isUuidShape as Me}from"./evict-drone.js";import{authRecoveryMessage as Be}from"./auth-recovery.js";import{classifyInSessionAssimilate as We,reattachOnlyRefusal as Fe,reattachFailureMessage as He}from"./assimilate-guard.js";import{gateAllowsActivation as Qe,borgSessionToolNotice as Ve}from"./launch-gate.js";import{renderSyncRolesResult as Ke}from"./sync-roles-render.js";import{initConsolePrefix as Xe,consolePrefix as v}from"./console-prefix.js";import{isCodexRemoteWakeEnabled as C,resolveSessionAgentKind as ze,probeCodexBridgeArmed as Ge}from"./codex-app-wake.js";import{lifecycleSignalForMessage as Ye,recordLifecycleLog as B,shouldSuppressLifecycleLog as Je}from"./lifecycle-log-guard.js";import{normalizeDirectLogRecipients as Ze}from"./direct-log.js";import W from"open";import et from"os";function tt(){try{const p=et.hostname();return p&&p.trim()?p.trim().slice(0,255):null}catch{return null}}async function F(p,x){return await ge(p,x.name)}async function f(){const p=await y();if(!p)throw new Error("Not assimilated to a cube. Use borg:assimilate <cube-name> first.");return p}async function rt(){je();try{$e()}catch{}try{Ue()}catch{}try{Ce()}catch{}try{_e({getActiveCube:y,getStreamConnected:()=>q().connected,getInboxPath:m=>I(m.cubeId,m.droneId),checkMonitor:E,isCodexRemoteWake:C,probeBridgeArmed:m=>Ge({cubeId:m.cubeId,droneId:m.droneId}),resolveAgentKind:ze,resolveHostname:tt,resolveVersion:_,getToken:we,fetchImpl:globalThis.fetch.bind(globalThis)})}catch{}const p=new V({name:"borg-mcp-client",version:_()},{capabilities:{tools:{},prompts:{}}});p.setRequestHandler(z,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:"RE-ATTACH this session to the drone seat already saved for this worktree (gh#780: this tool never creates seats). Provide the cube's name; on a match it returns the cube directive, your role's instructions, and recent activity for the EXISTING seat. To create a seat or switch cubes, run `borg assimilate` in a terminal instead.",inputSchema:{type:"object",properties:{cube_name:{type:"string",description:"The cube to connect to"}},required:["cube_name"]}},{name:"borg:cube",description:"Read the active Cube's directive and the registry of all roles in it (each role's name + short description). Use to remind yourself of cube-wide context.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:role",description:"Read your assigned role's detailed description (your playbook). Other drones cannot see this \u2014 only you (drones in this role).",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:version",description:"Returns the installed borgmcp client version. Use to verify which version is running in this MCP session.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:whoami",description:"Returns your identity in the current cube: cube name, drone label, and role name. Use to confirm which cube/role/drone you are.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:role-rationale",description:"Fetch an on-demand rationale/case-study section for a role playbook. Pass a role name/id and a plain-label section key to read the rationale without expanding every regen.",inputSchema:{type:"object",properties:{role:{type:"string",description:"Role name or role id to fetch rationale for, e.g. Builder."},section:{type:"string",description:"Plain-label role section key, e.g. Workflow rationale."}},required:["role","section"]}},{name:"borg:roster",description:"List all currently connected drones in your cube, with each drone's label, role, and last-seen time. Optional `since` argument adds a sender-side liveness column \u2014 pass either an activity_log entry id (e.g., from a dispatch you posted) or an ISO-8601 timestamp; each drone is marked `awake` if they've posted a log entry after that point, otherwise `stale-since-X`. Useful for confirming a dispatch reached its named recipients (catches the silent-wake-path-failure class where SSE delivered but the drone's /loop never woke).",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional liveness reference point. Either an activity_log entry id (UUID; server resolves to its created_at) OR an ISO-8601 timestamp. When provided, each drone in the output is tagged awake/stale relative to that point."}},required:[]}},{name:"borg:stream-status",description:"Diagnostic probe for the SSE log-stream consumer. Returns the live state of the local stream connection \u2014 `connected`, `lastContentEventAt` (most recent log/bookmark event), `lastWireActivityAt` (most recent event of any type, incl. heartbeats), `lastHeartbeatAt`, `lastPersistedEventId`, and `reconnectAttempts` \u2014 plus a wake-path completeness check that surfaces if SSE is attached but no inbox-Monitor is watching the file (the silent-failure mode where Claude's `/loop` never wakes on incoming entries). Reads in-process state from the running borgmcp client; does NOT re-open the stream, so calling it cannot perturb the very thing it's observing. Useful when troubleshooting wake-up issues, verifying the stream is alive without other drones logging, or pre-checking before fault-injection tests.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:read-log",description:"Read entries from the cube's activity log. Each entry is tagged with the drone that wrote it and that drone's role. For wake triage, prefer `unread_only=true` with a modest limit and drain until `has_more=false`; this reads oldest-unread-first from your server cursor and advances the watermark so bursts are not skipped. Optional `since` is a strict-after cursor for explicit bounded reads only; do not use it with the same timestamp as a notification preview because it can skip the boundary entry.",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional strict-after cursor for explicit bounded reads. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple for deterministic tie-break) OR an ISO-8601 timestamp. Do not use for routine wake triage; prefer unread_only."},limit:{type:"number",description:"max entries to return (1-500)"},unread_only:{type:"boolean",description:"When true, read only entries posted after this drone last called read-log, oldest-unread-first. Server advances the watermark to the newest returned entry on every call; if has_more=true, call again until has_more=false."}}}},{name:"borg:ack",description:"Mark a log entry as explicitly acknowledged. Replaces the convention of posting `ACK: <dispatch-id>` log entries. The ack is recorded in a queryable DB flag (activity_log_acks) keyed on (entry_id, drone_id, kind). Idempotent \u2014 repeated calls on the same entry are no-ops. Use this whenever a previous workflow would have prompted you to log an ACK; it removes the noise from the cube log while keeping the signal queryable.",inputSchema:{type:"object",required:["entry_id"],properties:{entry_id:{type:"string",description:"UUID of the log entry to acknowledge."}}}},{name:"borg:log",description:"Append a message to the cube's activity log. By default entries broadcast to all drones. When a cube declares a message taxonomy, borg:log applies class-based smart defaults: prefix-matched directed classes route to their default recipients unless you pass `to:`, `class:`, or explicit visibility. Pass `to: [...]` to direct by exact drone label, drone id, role name, or role slug.",inputSchema:{type:"object",properties:{message:{type:"string",description:"The log message (max 10KB)."},to:{type:"array",items:{type:"string"},description:"Optional direct-message recipients by exact drone label, drone id, role name, or role slug (resolves to all drones in that role). Omit to let class-based routing or broadcast defaults apply."},class:{type:"string",description:"Optional declared message class. Overrides prefix auto-classification when the cube declares a message taxonomy."},visibility:{type:"string",enum:["broadcast","direct"],description:"Optional explicit visibility. Overrides class-based routing defaults."}},required:["message"]}},{name:"borg:report-friction",description:"Report friction or a bug directly to the borgmcp dev team. WRITE-ONLY \u2014 you cannot read reports back. Use it when something about borg itself slowed you down, confused you, or broke: awkward UX, an unclear playbook, a missing affordance, or a bug you hit while using borg. Secrets (tokens, keys) are auto-scrubbed server-side before storage, but avoid pasting them anyway.",inputSchema:{type:"object",properties:{message:{type:"string",description:"What hit you + what you expected instead (max 10KB). Concrete and specific helps the dev team most."},kind:{type:"string",enum:["friction","bug"],description:"'friction' (default) for UX/workflow friction; 'bug' for something broken."},metadata:{type:"object",description:"Optional non-secret context. Allowed keys only: version, cube_id, os. Any other key is rejected."}},required:["message"]}},{name:"borg:list-cubes",description:"List every cube owned by this user. Returns id, name, cube_directive, and timestamps for each. Useful before assimilate to see what's available, or as a starting point for any management action.",inputSchema:{type:"object",properties:{}}},{name:"borg:create-cube",description:'Create a new cube. The server seeds a default "Drone" role atomically so the cube is assimilatable immediately. Pass an optional `template` name to apply a richer role set instead (see borg:list-templates / borg:apply-template).',inputSchema:{type:"object",properties:{name:{type:"string",description:"Cube name (lowercase letters, digits, hyphens; max 64 chars).",pattern:"^[a-z0-9-]+$",maxLength:64},cube_directive:{type:"string",description:"Markdown text every drone in this cube will see in regen. Anything project-specific."},template:{type:"string",description:'Optional template name to apply after cube creation (e.g. "software-dev"). Roles are merged by name; the default Drone role gets overwritten by the template if a same-named role is in the template.'}},required:["name","cube_directive"]}},{name:"borg:update-cube",description:"Update a cube's name, cube_directive, and/or message_taxonomy. Pass only what changes.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to update."},name:{type:"string",description:"New name (optional). Lowercase letters, digits, hyphens; max 64 chars.",pattern:"^[a-z0-9-]+$",maxLength:64},cube_directive:{type:"string",description:"New cube directive markdown (optional)."},message_taxonomy:{type:"array",description:"New message-class taxonomy (optional). REPLACES the whole taxonomy; the worker re-validates the full array (non-overlapping prefixes, unique class names, directed classes need default_to). Pass [] to clear. To change ONE class without resending the whole array, use borg:patch-taxonomy-class instead. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.",items:{type:"object",properties:{class:{type:"string",description:"Unique class name."},prefixes:{type:"array",items:{type:"string"},description:"Message prefixes routed by this class."},routing:{type:"string",enum:["broadcast","directed"],description:"Routing mode."},default_to:{type:"array",items:{type:"string"},description:"Default recipients (role name/slug/label, or @human-seat) for a directed class."},lifecycle:{type:"string",enum:["dispatch","completion"],description:"Optional lifecycle marker for stuck-dispatch detection."}}}}},required:["cube_id"]}},{name:"borg:patch-taxonomy-class",description:"Surgically patch ONE message-class within a cube's message_taxonomy, leaving other classes unchanged. Use this instead of borg:update-cube when adding/changing a single class so you don't resend (and risk clobbering) the whole taxonomy. action=add appends a new class; action=replace overwrites the class with the same name (case-insensitive); action=remove drops a class. The whole resulting taxonomy is re-validated (non-overlapping prefixes, unique class names, directed classes need default_to) \u2014 a single-class patch that breaks a cross-class rule against an untouched class is rejected. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to patch."},action:{type:"string",enum:["add","replace","remove"],description:"add / replace / remove a single class."},class_def:{type:"object",description:'The class definition (for add/replace). Shape: { class, prefixes?, routing: "broadcast"|"directed", default_to?, lifecycle? }.',properties:{class:{type:"string",description:"Unique class name."},prefixes:{type:"array",items:{type:"string"},description:"Message prefixes routed by this class."},routing:{type:"string",enum:["broadcast","directed"],description:"Routing mode."},default_to:{type:"array",items:{type:"string"},description:"Default recipients (required for directed classes): role name/slug/label, or @human-seat."},lifecycle:{type:"string",enum:["dispatch","completion"],description:"Optional lifecycle marker for stuck-dispatch detection."}},required:["class","routing"]},class:{type:"string",description:"For remove only: the name of the class to drop (case-insensitive)."}},required:["cube_id","action"]}},{name:"borg:delete-cube",description:"Delete a cube and all its roles, drones, and log entries. Irreversible \u2014 confirm with the user before invoking unless the cube is clearly disposable.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to delete."}},required:["cube_id"]}},{name:"borg:create-role",description:"Create a role inside a cube. The detailed_description is the role's playbook \u2014 only drones assigned to this role see it. Setting is_default=true demotes any existing default; a cube has exactly one default role at a time.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube this role belongs to."},name:{type:"string",description:'Role name (e.g. "Builder", "Reviewer").'},short_description:{type:"string",description:"One-line summary, shown to every drone in the cube."},detailed_description:{type:"string",description:"Full playbook for drones in this role \u2014 workflow, conventions, log signals to post."},is_default:{type:"boolean",description:"If true, new drones assimilating into this cube are assigned this role. Demotes the previous default."},is_human_seat:{type:"boolean",description:"If true, this role represents the cube's human-occupied seat (where the human Queen sits directly). The class-hierarchy guard in reassign-drone allows promotion FROM a human-seat role TO the platform Queen role; promotion from non-human-seat roles is rejected."},can_broadcast:{type:"boolean",description:"If true, drones in this role may post broadcast log entries when strict broadcast gating is enabled."},receives_all_direct:{type:"boolean",description:"If true, drones in this role can see direct log entries as observer/audit recipients."}},required:["cube_id","name","short_description","detailed_description"]}},{name:"borg:update-role",description:"Update a role. Pass only the fields that change. Promoting to is_default demotes the previous default in the same cube.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to update."},name:{type:"string",description:"New role name (optional)."},short_description:{type:"string",description:"New short description (optional)."},detailed_description:{type:"string",description:"New detailed playbook (optional)."},is_default:{type:"boolean",description:"Set true to make this the cube's default role (optional)."},is_human_seat:{type:"boolean",description:"Set true/false to mark/unmark this as the cube's human-occupied seat (the elevation source for the platform Queen role)."},can_broadcast:{type:"boolean",description:"Set true/false to allow or deny broadcast log entries when strict broadcast gating is enabled."},receives_all_direct:{type:"boolean",description:"Set true/false to grant or remove observer visibility into direct log entries."}},required:["role_id"]}},{name:"borg:patch-role-section",description:"Surgically patch ONE named section of a role's detailed_description, leaving the rest of the field byte-identical. Sections are delimited by plain-label lines (e.g. `Workflow:`, `Project conventions:`) \u2014 NOT markdown headings; text before the first label is the preamble. Use this instead of borg:update-role when changing a single section so you don't have to resend (and risk clobbering) the whole playbook. action=replace overwrites a section's body; action=insert adds a new section (optionally after a named one, else appended); action=delete removes a section.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to patch."},action:{type:"string",enum:["replace","insert","delete"],description:"replace / insert / delete a single section."},heading:{type:"string",description:'The section label WITHOUT the trailing colon (e.g. "Workflow"). Matched case-insensitively.'},body:{type:"string",description:"New text BELOW the heading (for replace/insert). Omit for delete."},after:{type:"string",description:"For insert only: place the new section after the section with this heading. Omit/null to append at the end."}},required:["role_id","action","heading"]}},{name:"borg:delete-role",description:"Delete a role. Refuses if any drone is still assigned \u2014 reassign or evict those drones from the dashboard first.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to delete."}},required:["role_id"]}},{name:"borg:reassign-drone",description:"Reassign a drone to a different role in the same cube. Coordinator-shaped: the cube's Coordinator drone is the one expected to call this when dispatching new drones to specific work. Server refuses if you try to assign to the Coordinator role when another drone already holds it (evict or reassign that drone first).",inputSchema:{type:"object",properties:{drone_id:{type:"string",description:"UUID of the drone to reassign."},role_id:{type:"string",description:"UUID of the target role. Must belong to the same cube as the drone."}},required:["drone_id","role_id"]}},{name:"borg:evict-drone",description:"Evict (soft-delete) a drone from its cube. Coordinator-shaped: the cube's Coordinator/Queen seat calls this to remove a dead, stuck, or surplus drone \u2014 it drops out of the roster and frees its slot (incl. a held Coordinator/Queen-class seat), while its activity-log history is preserved with anonymized attribution. Owner-scoped: you can only evict drones in cubes you own. Identify the drone EITHER by drone_id (UUID) OR by label + cube_id (the label as it appears in the roster/regen).",inputSchema:{type:"object",properties:{drone_id:{type:"string",description:"UUID of the drone to evict. Provide this OR (label + cube_id)."},label:{type:"string",description:'Drone label to evict, e.g. "two-of-seventeen-builder". Requires cube_id. Ignored when drone_id is given.'},cube_id:{type:"string",description:"UUID of the cube the labelled drone belongs to. Required when evicting by label."}}}},{name:"borg:list-drones",description:"List every drone in a cube (owner-scoped). Returns id, label, role_id, agent_kind, last_seen, and wake_path_alert_class for each \u2014 gives the Coordinator a roster they can act on with borg:reassign-drone.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube whose drones to list."}},required:["cube_id"]}},{name:"borg:list-roles",description:"List every role in a cube (owner-scoped). Returns id, name, short_description, is_default, is_human_seat, can_broadcast, receives_all_direct, and role_class for each \u2014 gives Coordinator-class drones the role UUIDs they need for borg:reassign-drone (e.g. to promote a drone to the Queen role). Closes the gh#153 Queen-role-promotion UX gap (Coordinator drones previously had no way to discover role IDs without operator help).",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube whose roles to list."}},required:["cube_id"]}},{name:"borg:list-templates",description:"List available cube templates that can be applied via borg:apply-template or passed to borg:create-cube.",inputSchema:{type:"object",properties:{}}},{name:"borg:apply-template",description:"Apply a named template to an existing cube, NON-CLOBBERINGLY. Roles are merged by name: new roles are created; existing template-named roles get template sections/classes the cube LACKS auto-applied, but EVOLVED (conflicting) text is preserved, never overwritten. Use this to retrofit an existing cube with a richer role set (e.g. add Coordinator/Reviewer/UX Expert). To review + selectively accept conflicting fragments, use borg:sync-roles (which surfaces each conflict + takes per-fragment accept decisions).",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to apply the template to."},template_name:{type:"string",description:"Template to apply (see borg:list-templates)."}},required:["cube_id","template_name"]}},{name:"borg:sync-roles",description:"Non-clobbering sync of an existing cube's roles + message_taxonomy against the current built-in template. The dry-run (default) classifies each FRAGMENT (role-text section, short_description, role flags, or taxonomy class) as ADD (the cube lacks it \u2014 safe auto-apply), UNCHANGED, or CONFLICT (the cube has EVOLVED text that differs from the template). On apply, ADDs auto-apply; CONFLICTs are applied ONLY when you explicitly accept them via `decisions` (keyed on the stable fragment key shown in the dry-run, e.g. `role:Builder:section:Workflow`). Unspecified conflicts default to KEEP (reject) \u2014 your cube's evolved coordination text is NEVER silently overwritten. Custom roles (names not in the template) are never touched.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to sync."},template_name:{type:"string",description:"Template to sync against (default: software-dev)."},apply:{type:"boolean",description:"If true, commit (auto-apply ADDs + accepted conflicts). If false (default), dry-run only \u2014 classify + surface conflicts."},decisions:{type:"object",description:'Per-conflict accept/reject map, keyed on the fragment key from the dry-run (e.g. {"role:Builder:section:Workflow":"accept"}). Unspecified conflicts default to "reject" (keep the cube version).',additionalProperties:{type:"string",enum:["accept","reject"]}}},required:["cube_id"]}}]})),p.setRequestHandler(X,async m=>{const{name:g,arguments:r}=m.params;if(!Qe(`tool ${g}`))return{content:[{type:"text",text:Ve(g)}],isError:!0};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,i=r?.mode==="lite"?"lite":"full",o=await O(e.sessionToken,e.apiUrl,{since:t}),s=P(e,o);s!==e&&await N(s);const n=q(),a=I(s.cubeId,s.droneId),c=C()?!0:E(a),d=Ae(n,c)?Te({inboxPath:a,droneLabel:Ee(o,s.droneLabel),cubeName:s.name}):"";let u="";try{const h=_(),l=De();if(h!=="unknown"&&l!=="unknown"&&l!==h){const[b,R,H]=h.split(".").map(Number),[k,D,Q]=l.split(".").map(Number);(k>b||k===b&&D>R||k===b&&D===R&&Q>H)&&(u=`## \u{1F504} borgmcp ${l} installed \u2014 run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${h}.
2
+ import{Server as Q}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as V}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as X,ListToolsRequestSchema as z,ListPromptsRequestSchema as G,GetPromptRequestSchema as Y}from"@modelcontextprotocol/sdk/types.js";import{getCubeInfo as J,getRoleInfo as j,getRoster as Z,readLog as ee,appendLog as te,submitReport as re,ackLogEntry as oe,regen as O,listCubes as ie,createCube as ne,updateCube as T,deleteCube as se,createRole as ae,updateRole as ce,patchRoleSection as $,patchTaxonomyClass as A,deleteRole as le,reassignDrone as de,evictDrone as pe,getCube as w,checkSubscriptionStatus as ue,createBillingPortalSession as me,createSubscription as be,syncRoles as he,applyTemplate as ge,whoami as ye,roleRationale as fe,getValidToken as we}from"./remote-client.js";import{startHealthBeatTick as _e}from"./health-beat.js";import{getTemplate as U,listTemplateNames as S,resolveCubeDirectiveForCreate as ve,resolveCubeDirectiveForApply as xe,resolveMessageTaxonomyForCreate as ke}from"./templates.js";import{activeCubeWithFreshRegenIdentity as N,getActiveCube as y,setActiveCube as P,inboxPathForDrone as I}from"./cubes.js";import{addSessionStartHook as $e,addUserPromptSubmitHook as Ue}from"./config-utils.js";import{humanAgo as L,formatLogEntryMarkdown as Se,formatRegenMarkdown as M,getDronePlaybook as Ie,nullTaxonomyTip as qe,regenWakePathDroneLabel as Ee}from"./regen-format.js";import{startLogStream as Ce,getStreamStatus as q}from"./log-stream.js";import{renderRoleList as Re}from"./list-roles-render.js";import{getPackageVersion as _,getOnDiskVersion as De,handleVersionFlag as je}from"./version.js";import{renderStreamStatus as Oe,checkInboxMonitorHealthy as E,formatWakePathPrefix as Te,shouldShowWakePathWarning as Ae}from"./stream-status.js";import{formatRoleAgentLabel as Ne,renderRoster as Pe}from"./roster-render.js";import{resolveDroneIdByLabel as Le,isUuidShape as Me}from"./evict-drone.js";import{authRecoveryMessage as Be}from"./auth-recovery.js";import{classifyInSessionAssimilate as We,reattachOnlyRefusal as Fe,reattachFailureMessage as He}from"./assimilate-guard.js";import{gateAllowsActivation as Ke,borgSessionToolNotice as Qe}from"./launch-gate.js";import{renderSyncRolesResult as Ve}from"./sync-roles-render.js";import{initConsolePrefix as Xe,consolePrefix as v}from"./console-prefix.js";import{isCodexRemoteWakeEnabled as C,resolveSessionAgentKind as ze,probeCodexBridgeArmed as Ge}from"./codex-app-wake.js";import{lifecycleSignalForMessage as Ye,recordLifecycleLog as B,shouldSuppressLifecycleLog as Je}from"./lifecycle-log-guard.js";import{normalizeDirectLogRecipients as Ze}from"./direct-log.js";import W from"open";import et from"os";function tt(){try{const p=et.hostname();return p&&p.trim()?p.trim().slice(0,255):null}catch{return null}}async function F(p,x){return await ge(p,x.name)}async function f(){const p=await y();if(!p)throw new Error("Not assimilated to a cube. Use borg:assimilate <cube-name> first.");return p}async function rt(){je();try{$e()}catch{}try{Ue()}catch{}try{Ce()}catch{}try{_e({getActiveCube:y,getStreamConnected:()=>q().connected,getInboxPath:m=>I(m.cubeId,m.droneId),checkMonitor:E,isCodexRemoteWake:C,probeBridgeArmed:m=>Ge({cubeId:m.cubeId,droneId:m.droneId}),resolveAgentKind:ze,resolveHostname:tt,resolveVersion:_,getToken:we,fetchImpl:globalThis.fetch.bind(globalThis)})}catch{}const p=new Q({name:"borg-mcp-client",version:_()},{capabilities:{tools:{},prompts:{}}});p.setRequestHandler(z,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:"RE-ATTACH this session to the drone seat already saved for this worktree (gh#780: this tool never creates seats). Provide the cube's name; on a match it returns the cube directive, your role's instructions, and recent activity for the EXISTING seat. To create a seat or switch cubes, run `borg assimilate` in a terminal instead.",inputSchema:{type:"object",properties:{cube_name:{type:"string",description:"The cube to connect to"}},required:["cube_name"]}},{name:"borg:cube",description:"Read the active Cube's directive and the registry of all roles in it (each role's name + short description). Use to remind yourself of cube-wide context.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:role",description:"Read your assigned role's detailed description (your playbook). Other drones cannot see this \u2014 only you (drones in this role).",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:version",description:"Returns the installed borgmcp client version. Use to verify which version is running in this MCP session.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:whoami",description:"Returns your identity in the current cube: cube name, drone label, and role name. Use to confirm which cube/role/drone you are.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:role-rationale",description:"Fetch an on-demand rationale/case-study section for a role playbook. Pass a role name/id and a plain-label section key to read the rationale without expanding every regen.",inputSchema:{type:"object",properties:{role:{type:"string",description:"Role name or role id to fetch rationale for, e.g. Builder."},section:{type:"string",description:"Plain-label role section key, e.g. Workflow rationale."}},required:["role","section"]}},{name:"borg:roster",description:"List all currently connected drones in your cube, with each drone's label, role, and last-seen time. Optional `since` argument adds a sender-side liveness column \u2014 pass either an activity_log entry id (e.g., from a dispatch you posted) or an ISO-8601 timestamp; each drone is marked `awake` if they've posted a log entry after that point, otherwise `stale-since-X`. Useful for confirming a dispatch reached its named recipients (catches the silent-wake-path-failure class where SSE delivered but the drone's /loop never woke).",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional liveness reference point. Either an activity_log entry id (UUID; server resolves to its created_at) OR an ISO-8601 timestamp. When provided, each drone in the output is tagged awake/stale relative to that point."}},required:[]}},{name:"borg:stream-status",description:"Diagnostic probe for the SSE log-stream consumer. Returns the live state of the local stream connection \u2014 `connected`, `lastContentEventAt` (most recent log/bookmark event), `lastWireActivityAt` (most recent event of any type, incl. heartbeats), `lastHeartbeatAt`, `lastPersistedEventId`, and `reconnectAttempts` \u2014 plus a wake-path completeness check that surfaces if SSE is attached but no inbox-Monitor is watching the file (the silent-failure mode where Claude's `/loop` never wakes on incoming entries). Reads in-process state from the running borgmcp client; does NOT re-open the stream, so calling it cannot perturb the very thing it's observing. Useful when troubleshooting wake-up issues, verifying the stream is alive without other drones logging, or pre-checking before fault-injection tests.",inputSchema:{type:"object",properties:{},required:[]}},{name:"borg:read-log",description:"Read entries from the cube's activity log. Each entry is tagged with the drone that wrote it and that drone's role. For wake triage, prefer `unread_only=true` with a modest limit and drain until `has_more=false`; this reads oldest-unread-first from your server cursor and advances the watermark so bursts are not skipped. Optional `since` is a strict-after cursor for explicit bounded reads only; do not use it with the same timestamp as a notification preview because it can skip the boundary entry.",inputSchema:{type:"object",properties:{since:{type:"string",description:"Optional strict-after cursor for explicit bounded reads. Either an activity_log entry id (UUID; server resolves to (created_at, id) tuple for deterministic tie-break) OR an ISO-8601 timestamp. Do not use for routine wake triage; prefer unread_only."},limit:{type:"number",description:"max entries to return (1-500)"},unread_only:{type:"boolean",description:"When true, read only entries posted after this drone last called read-log, oldest-unread-first. Server advances the watermark to the newest returned entry on every call; if has_more=true, call again until has_more=false."}}}},{name:"borg:ack",description:"Mark a log entry as explicitly acknowledged. Replaces the convention of posting `ACK: <dispatch-id>` log entries. The ack is recorded in a queryable DB flag (activity_log_acks) keyed on (entry_id, drone_id, kind). Idempotent \u2014 repeated calls on the same entry are no-ops. Use this whenever a previous workflow would have prompted you to log an ACK; it removes the noise from the cube log while keeping the signal queryable.",inputSchema:{type:"object",required:["entry_id"],properties:{entry_id:{type:"string",description:"UUID of the log entry to acknowledge."}}}},{name:"borg:log",description:"Append a message to the cube's activity log. By default entries broadcast to all drones. When a cube declares a message taxonomy, borg:log applies class-based smart defaults: prefix-matched directed classes route to their default recipients unless you pass `to:`, `class:`, or explicit visibility. Pass `to: [...]` to direct by exact drone label, drone id, role name, or role slug.",inputSchema:{type:"object",properties:{message:{type:"string",description:"The log message (max 10KB)."},to:{type:"array",items:{type:"string"},description:"Optional direct-message recipients by exact drone label, drone id, role name, or role slug (resolves to all drones in that role). Omit to let class-based routing or broadcast defaults apply."},class:{type:"string",description:"Optional declared message class. Overrides prefix auto-classification when the cube declares a message taxonomy."},visibility:{type:"string",enum:["broadcast","direct"],description:"Optional explicit visibility. Overrides class-based routing defaults."}},required:["message"]}},{name:"borg:report-friction",description:"Report friction or a bug directly to the borgmcp dev team. WRITE-ONLY \u2014 you cannot read reports back. Use it when something about borg itself slowed you down, confused you, or broke: awkward UX, an unclear playbook, a missing affordance, or a bug you hit while using borg. Secrets (tokens, keys) are auto-scrubbed server-side before storage, but avoid pasting them anyway.",inputSchema:{type:"object",properties:{message:{type:"string",description:"What hit you + what you expected instead (max 10KB). Concrete and specific helps the dev team most."},kind:{type:"string",enum:["friction","bug"],description:"'friction' (default) for UX/workflow friction; 'bug' for something broken."},metadata:{type:"object",description:"Optional non-secret context. Allowed keys only: version, cube_id, os. Any other key is rejected."}},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."},default_backend:{type:"string",description:'Default backend for drones assigned to this role (e.g., "claude:claude-opus-4-8" or "ollama:qwen3-coder-next:q4_K_M"). Null = inherit from cube. Optional.'}},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."},default_backend:{type:"string",description:'Default backend for drones assigned to this role (e.g., "claude:claude-opus-4-8" or "ollama:qwen3-coder-next:q4_K_M"). Null = inherit from cube. Optional.'}},required:["role_id"]}},{name:"borg:patch-role-section",description:"Surgically patch ONE named section of a role's detailed_description, leaving the rest of the field byte-identical. Sections are delimited by plain-label lines (e.g. `Workflow:`, `Project conventions:`) \u2014 NOT markdown headings; text before the first label is the preamble. Use this instead of borg:update-role when changing a single section so you don't have to resend (and risk clobbering) the whole playbook. action=replace overwrites a section's body; action=insert adds a new section (optionally after a named one, else appended); action=delete removes a section.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to patch."},action:{type:"string",enum:["replace","insert","delete"],description:"replace / insert / delete a single section."},heading:{type:"string",description:'The section label WITHOUT the trailing colon (e.g. "Workflow"). Matched case-insensitively.'},body:{type:"string",description:"New text BELOW the heading (for replace/insert). Omit for delete."},after:{type:"string",description:"For insert only: place the new section after the section with this heading. Omit/null to append at the end."}},required:["role_id","action","heading"]}},{name:"borg:delete-role",description:"Delete a role. Refuses if any drone is still assigned \u2014 reassign or evict those drones from the dashboard first.",inputSchema:{type:"object",properties:{role_id:{type:"string",description:"UUID of the role to delete."}},required:["role_id"]}},{name:"borg:reassign-drone",description:"Reassign a drone to a different role in the same cube. Coordinator-shaped: the cube's Coordinator drone is the one expected to call this when dispatching new drones to specific work. Server refuses if you try to assign to the Coordinator role when another drone already holds it (evict or reassign that drone first).",inputSchema:{type:"object",properties:{drone_id:{type:"string",description:"UUID of the drone to reassign."},role_id:{type:"string",description:"UUID of the target role. Must belong to the same cube as the drone."}},required:["drone_id","role_id"]}},{name:"borg:evict-drone",description:"Evict (soft-delete) a drone from its cube. Coordinator-shaped: the cube's Coordinator/Queen seat calls this to remove a dead, stuck, or surplus drone \u2014 it drops out of the roster and frees its slot (incl. a held Coordinator/Queen-class seat), while its activity-log history is preserved with anonymized attribution. Owner-scoped: you can only evict drones in cubes you own. Identify the drone EITHER by drone_id (UUID) OR by label + cube_id (the label as it appears in the roster/regen).",inputSchema:{type:"object",properties:{drone_id:{type:"string",description:"UUID of the drone to evict. Provide this OR (label + cube_id)."},label:{type:"string",description:'Drone label to evict, e.g. "two-of-seventeen-builder". Requires cube_id. Ignored when drone_id is given.'},cube_id:{type:"string",description:"UUID of the cube the labelled drone belongs to. Required when evicting by label."}}}},{name:"borg:list-drones",description:"List every drone in a cube (owner-scoped). Returns id, label, role_id, agent_kind, last_seen, and wake_path_alert_class for each \u2014 gives the Coordinator a roster they can act on with borg:reassign-drone.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube whose drones to list."}},required:["cube_id"]}},{name:"borg:list-roles",description:"List every role in a cube (owner-scoped). Returns id, name, short_description, is_default, is_human_seat, can_broadcast, receives_all_direct, and role_class for each \u2014 gives Coordinator-class drones the role UUIDs they need for borg:reassign-drone (e.g. to promote a drone to the Queen role). Closes the gh#153 Queen-role-promotion UX gap (Coordinator drones previously had no way to discover role IDs without operator help).",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube whose roles to list."}},required:["cube_id"]}},{name:"borg:list-templates",description:"List available cube templates that can be applied via borg:apply-template or passed to borg:create-cube.",inputSchema:{type:"object",properties:{}}},{name:"borg:apply-template",description:"Apply a named template to an existing cube, NON-CLOBBERINGLY. Roles are merged by name: new roles are created; existing template-named roles get template sections/classes the cube LACKS auto-applied, but EVOLVED (conflicting) text is preserved, never overwritten. Use this to retrofit an existing cube with a richer role set (e.g. add Coordinator/Reviewer/UX Expert). To review + selectively accept conflicting fragments, use borg:sync-roles (which surfaces each conflict + takes per-fragment accept decisions).",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to apply the template to."},template_name:{type:"string",description:"Template to apply (see borg:list-templates)."}},required:["cube_id","template_name"]}},{name:"borg:sync-roles",description:"Non-clobbering sync of an existing cube's roles + message_taxonomy against the current built-in template. The dry-run (default) classifies each FRAGMENT (role-text section, short_description, role flags, or taxonomy class) as ADD (the cube lacks it \u2014 safe auto-apply), UNCHANGED, or CONFLICT (the cube has EVOLVED text that differs from the template). On apply, ADDs auto-apply; CONFLICTs are applied ONLY when you explicitly accept them via `decisions` (keyed on the stable fragment key shown in the dry-run, e.g. `role:Builder:section:Workflow`). Unspecified conflicts default to KEEP (reject) \u2014 your cube's evolved coordination text is NEVER silently overwritten. Custom roles (names not in the template) are never touched.",inputSchema:{type:"object",properties:{cube_id:{type:"string",description:"UUID of the cube to sync."},template_name:{type:"string",description:"Template to sync against (default: software-dev)."},apply:{type:"boolean",description:"If true, commit (auto-apply ADDs + accepted conflicts). If false (default), dry-run only \u2014 classify + surface conflicts."},decisions:{type:"object",description:'Per-conflict accept/reject map, keyed on the fragment key from the dry-run (e.g. {"role:Builder:section:Workflow":"accept"}). Unspecified conflicts default to "reject" (keep the cube version).',additionalProperties:{type:"string",enum:["accept","reject"]}}},required:["cube_id"]}}]})),p.setRequestHandler(X,async m=>{const{name:g,arguments:r}=m.params;if(!Ke(`tool ${g}`))return{content:[{type:"text",text:Qe(g)}],isError:!0};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,i=r?.mode==="lite"?"lite":"full",o=await O(e.sessionToken,e.apiUrl,{since:t}),s=N(e,o);s!==e&&await P(s);const n=q(),a=I(s.cubeId,s.droneId),c=C()?!0:E(a),d=Ae(n,c)?Te({inboxPath:a,droneLabel:Ee(o,s.droneLabel),cubeName:s.name}):"";let u="";try{const b=_(),l=De();if(b!=="unknown"&&l!=="unknown"&&l!==b){const[h,R,H]=b.split(".").map(Number),[k,D,K]=l.split(".").map(Number);(k>h||k===h&&D>R||k===h&&D===R&&K>H)&&(u=`## \u{1F504} borgmcp ${l} installed \u2014 run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${b}.
3
3
 
4
- `)}}catch{}return{content:[{type:"text",text:u+d+M(o,{mode:i})}]}}case"subscribe":return{content:[{type:"text",text:`Complete your subscription at: ${await he()}`}]};case"borg:upgrade-subscription":{const e=await me();try{await W(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 W(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=await y(),i=We(t,e);if(i.kind!=="reattach")return{content:[{type:"text",text:Fe(i,e)}],isError:!0};try{const o=await O(t.sessionToken,t.apiUrl,{}),s=P(t,o);return s!==t&&await N(s),{content:[{type:"text",text:[`# Re-attached to cube: ${s.name}`,"",`**Drone label:** ${s.droneLabel}`,"**Seat:** existing identity reused \u2014 no new drone minted (gh#780)","",""].join(`
4
+ `)}}catch{}return{content:[{type:"text",text:u+d+M(o,{mode:i})}]}}case"subscribe":return{content:[{type:"text",text:`Complete your subscription at: ${await be()}`}]};case"borg:upgrade-subscription":{const e=await me();try{await W(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 W(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=await y(),i=We(t,e);if(i.kind!=="reattach")return{content:[{type:"text",text:Fe(i,e)}],isError:!0};try{const o=await O(t.sessionToken,t.apiUrl,{}),s=N(t,o);return s!==t&&await P(s),{content:[{type:"text",text:[`# Re-attached to cube: ${s.name}`,"",`**Drone label:** ${s.droneLabel}`,"**Seat:** existing identity reused \u2014 no new drone minted (gh#780)","",""].join(`
5
5
  `)+M(o,{mode:"full"})}]}}catch(o){const s=He(o??{});if(!s)throw o;return{content:[{type:"text",text:s}],isError:!0}}}case"borg:version":return{content:[{type:"text",text:`borgmcp ${_()}`}]};case"borg:whoami":{const e=await f(),t=await ye(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}case"borg:cube":{const e=await f(),[{cube:t,roles:i}]=await Promise.all([J(e.sessionToken,e.apiUrl),j(e.sessionToken,e.apiUrl)]),o=[];o.push(`# Cube: ${t.name}`),o.push(""),o.push("## Cube directive"),o.push(t.cube_directive||"_(none)_"),o.push("");const s=qe(t.message_taxonomy);if(s&&(o.push(s),o.push("")),o.push("## Roles in this cube"),!i.length)o.push("_(no roles defined)_");else{for(const n of i){const a=[n.role_class==="queen"?"Queen":null,n.is_human_seat?"human-seat":null,n.is_default?"default":null].filter(Boolean).join(", "),c=a?` (${a})`:"",d=n.short_description||"_(no description)_";o.push(`- **${n.name}**${c} \u2014 ${d}`)}o.push(""),o.push("_(Coordinator-class drones can fetch role IDs via `borg:list-roles` for use with `borg:reassign-drone`.)_")}return o.push(""),o.push(Ie()),{content:[{type:"text",text:o.join(`
6
6
  `)}]}}case"borg:role":{const e=await f(),{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
7
  `)}]}}case"borg:role-rationale":{const e=await f(),t=typeof r?.role=="string"?r.role:"",i=typeof r?.section=="string"?r.section:"",o=await fe(e.sessionToken,e.apiUrl,t,i);return{content:[{type:"text",text:[`# Role rationale: ${o.role} \u2014 ${o.section}`,"",o.body||"_(empty)_"].join(`
8
- `)}]}}case"borg:roster":{const e=await f(),t=typeof r?.since=="string"?r.since:void 0,{drones:i,roles:o,since:s}=await Z(e.sessionToken,e.apiUrl,t);return{content:[{type:"text",text:Ne({cubeName:e.name,drones:i,roles:o,resolvedSince:s??null,humanAgo:L})}]}}case"borg:stream-status":{const e=q(),t=await y(),i=t?I(t.cubeId,t.droneId):null,o=t?C()?!0:E(i):null;let s="";e.runLoopHealth==="silent-inert"&&(s=`## \u26A0 SSE stream loop silent-inert \u2014 run /mcp and reconnect to restart
8
+ `)}]}}case"borg:roster":{const e=await f(),t=typeof r?.since=="string"?r.since:void 0,{drones:i,roles:o,since:s}=await Z(e.sessionToken,e.apiUrl,t);return{content:[{type:"text",text:Pe({cubeName:e.name,drones:i,roles:o,resolvedSince:s??null,humanAgo:L})}]}}case"borg:stream-status":{const e=q(),t=await y(),i=t?I(t.cubeId,t.droneId):null,o=t?C()?!0:E(i):null;let s="";e.runLoopHealth==="silent-inert"&&(s=`## \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 n=Oe({status:e,inboxMonitorHealthy:o,inboxPath:i,droneLabel:t?.droneLabel??null,cubeName:t?.name??null,humanAgo:L});return{content:[{type:"text",text:s+n}]}}case"borg:read-log":{const e=await f(),t=typeof r?.since=="string"?r.since:void 0,i=typeof r?.limit=="number"?r.limit:void 0,o=r?.unread_only===!0||r?.unread_only==="true",{entries:s,drones:n,roles:a,behind_by:c,has_more:d}=await ee(e.sessionToken,e.apiUrl,{since:t,limit:i,unreadOnly:o}),u=new Map;for(const b of n)u.set(b.id,b);const h=new Map;for(const b of a)h.set(b.id,b);const l=[];if(l.push(`# Activity log: ${e.name}`),l.push(""),!s.length)l.push("_(no entries)_");else for(const b of s)l.push(Se(b,u,h));return d===!0?(l.push(""),l.push("\u26A0 has_more: true \u2014 call `borg:read-log unread_only=true` again until has_more=false so you finish draining unread entries.")):typeof c=="number"&&c>0&&(l.push(""),l.push(`\u26A0 behind_by: ${c} more unread ${c===1?"entry":"entries"} addressed to you \u2014 call \`borg:read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`)),{content:[{type:"text",text:l.join(`
12
+ `);const n=Oe({status:e,inboxMonitorHealthy:o,inboxPath:i,droneLabel:t?.droneLabel??null,cubeName:t?.name??null,humanAgo:L});return{content:[{type:"text",text:s+n}]}}case"borg:read-log":{const e=await f(),t=typeof r?.since=="string"?r.since:void 0,i=typeof r?.limit=="number"?r.limit:void 0,o=r?.unread_only===!0||r?.unread_only==="true",{entries:s,drones:n,roles:a,behind_by:c,has_more:d}=await ee(e.sessionToken,e.apiUrl,{since:t,limit:i,unreadOnly:o}),u=new Map;for(const h of n)u.set(h.id,h);const b=new Map;for(const h of a)b.set(h.id,h);const l=[];if(l.push(`# Activity log: ${e.name}`),l.push(""),!s.length)l.push("_(no entries)_");else for(const h of s)l.push(Se(h,u,b));return d===!0?(l.push(""),l.push("\u26A0 has_more: true \u2014 call `borg:read-log unread_only=true` again until has_more=false so you finish draining unread entries.")):typeof c=="number"&&c>0&&(l.push(""),l.push(`\u26A0 behind_by: ${c} more unread ${c===1?"entry":"entries"} addressed to you \u2014 call \`borg:read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`)),{content:[{type:"text",text:l.join(`
13
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(Ye(e)){const l=await Je(t,e);if(l.suppress)return await B(t,e),{content:[{type:"text",text:`Suppressed duplicate ${l.signal?.toUpperCase()} lifecycle log for ${t.droneLabel}; recent cube log already contains this signal.`}]}}const i=Object.prototype.hasOwnProperty.call(r??{},"to"),o=i?Ze(r?.to):void 0,s=typeof r?.class=="string"?r.class:void 0,n=r?.visibility==="broadcast"||r?.visibility==="direct"?r.visibility:void 0,a={...s?{class:s}:{},...i?{to:o??[]}:{},...n?{visibility:n}:{}},c=await te(t.sessionToken,t.apiUrl,e,a);await B(t,e);const d=c.routing?.message?`
14
14
  ${c.routing.message}`:"",u=c.unreachableRecipients?.length?`
15
15
  \u26A0 ${c.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${c.unreachableRecipients.map(l=>l.label).join(", ")}. Message delivered \u2014 they'll read it when they return.`:"";return{content:[{type:"text",text:`Logged to cube "${t.name}" as ${t.droneLabel}. (entry id: ${c.entry.id})${d}${u}`}]}}case"borg:report-friction":{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.");const i=r?.kind==="bug"?"bug":"friction",o=r?.metadata&&typeof r.metadata=="object"&&!Array.isArray(r.metadata)?r.metadata:void 0;return{content:[{type:"text",text:(await re(t.sessionToken,t.apiUrl,{kind:i,message:e,metadata:o})).ok?"Report submitted \u2014 thank you. The borgmcp team will see it. (Write-only: you cannot read reports back.)":"Report did not submit. Try again, or raise it in the cube log."}]}}case"borg:ack":{const e=r?.entry_id;if(!e||typeof e!="string")throw new Error("entry_id is required");const t=await f();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(i=>`- **${i.name}** (id: ${i.id})
@@ -18,10 +18,10 @@ ${c.routing.message}`:"",u=c.unreachableRecipients?.length?`
18
18
 
19
19
  ${t.join(`
20
20
 
21
- `)}`}]}}case"borg:create-cube":{const e=r?.name,t=r?.cube_directive,i=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 o=null;if(i&&(o=U(i),!o))throw new Error(`Unknown template "${i}". Available: ${S().join(", ")}`);const s=ve(t,o),n=ke(void 0,o),a=await ne(e,s,{message_taxonomy:n});if(o){const d=await F(a.id,o),u=s!==t?" Template cube directive applied (operator passed empty).":"";return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}) with template **${i}** applied \u2014 ${d.created} role(s) created, ${d.updated} updated.${u} Use borg:assimilate ${a.name} to join as a drone.`}]}}return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}). A default "Drone" role was seeded \u2014 rename or replace it via borg:update-role / borg:create-role / borg:delete-role. Use borg:assimilate ${a.name} to join as a drone.`}]}}case"borg:update-cube":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const t={};if(typeof r?.name=="string"&&(t.name=r.name),typeof r?.cube_directive=="string"&&(t.cube_directive=r.cube_directive),Array.isArray(r?.message_taxonomy)&&(t.message_taxonomy=r.message_taxonomy),Object.keys(t).length===0)throw new Error("Pass at least one of: name, cube_directive, message_taxonomy.");const{cube:i}=await T(e,t);return{content:[{type:"text",text:`Updated cube **${i.name}** (id: ${i.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 i,o;if(t==="remove"){const n=r?.class;if(!n)throw new Error("class is required for remove.");({cube:i}=await A(e,{action:t,class:n})),o=n}else{const n=r?.class_def;if(n==null||typeof n!="object"||Array.isArray(n))throw new Error("class_def (object) is required for add/replace.");({cube:i}=await A(e,{action:t,class_def:n})),o=String(n.class??"")}return{content:[{type:"text",text:`${t==="add"?"Added":t==="replace"?"Replaced":"Removed"} taxonomy class **${o}** in cube **${i.name}** (id: ${i.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,i=r?.short_description,o=r?.detailed_description;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("name is required");if(i===void 0)throw new Error("short_description is required (pass empty string if none)");if(o===void 0)throw new Error("detailed_description is required (pass empty string if none)");const s=r?.is_default===!0,n=r?.is_human_seat===!0,a=r?.can_broadcast===!0,c=r?.receives_all_direct===!0,{role:d}=await ae(e,{name:t,short_description:i,detailed_description:o,is_default:s,is_human_seat:n,can_broadcast:a,receives_all_direct:c}),u=[d.role_class==="queen"?"Queen":null,d.is_human_seat?"human-seat":null,d.is_default?"default":null].filter(Boolean).join(", "),h=u?` (${u})`:"";return{content:[{type:"text",text:`Created role **${d.name}**${h} (id: ${d.id}) in cube ${e}.`}]}}case"borg:update-role":{const e=r?.role_id;if(!e)throw new Error("role_id is required");const t={};if(typeof r?.name=="string"&&(t.name=r.name),typeof r?.short_description=="string"&&(t.short_description=r.short_description),typeof r?.detailed_description=="string"&&(t.detailed_description=r.detailed_description),typeof r?.is_default=="boolean"&&(t.is_default=r.is_default),typeof r?.is_human_seat=="boolean"&&(t.is_human_seat=r.is_human_seat),typeof r?.can_broadcast=="boolean"&&(t.can_broadcast=r.can_broadcast),typeof r?.receives_all_direct=="boolean"&&(t.receives_all_direct=r.receives_all_direct),Object.keys(t).length===0)throw new Error("Pass at least one of: name, short_description, detailed_description, is_default, is_human_seat, can_broadcast, receives_all_direct.");const{role:i}=await ce(e,t),o=[i.role_class==="queen"?"Queen":null,i.is_human_seat?"human-seat":null,i.is_default?"default":null].filter(Boolean).join(", "),s=o?` (${o})`:"";return{content:[{type:"text",text:`Updated role **${i.name}**${s} (id: ${i.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 i=r?.heading;if(!i)throw new Error("heading is required");let o;if(t==="delete")({role:o}=await $(e,{action:t,heading:i}));else{const n=r?.body;if(typeof n!="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:o}=await $(e,{action:t,heading:i,body:n,after:a}))}else({role:o}=await $(e,{action:t,heading:i,body:n}))}return{content:[{type:"text",text:`${t==="replace"?"Replaced":t==="insert"?"Inserted":"Deleted"} section **${i}** in role **${o.name}** (id: ${o.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:i}=await de(e,t);return{content:[{type:"text",text:`Reassigned drone ${i.label} (${i.id}) to role ${i.role_id}.`}]}}case"borg:evict-drone":{const e=r?.drone_id?.trim(),t=r?.label?.trim(),i=r?.cube_id?.trim();let o,s;if(e){if(!Me(e))throw new Error(`drone_id "${e}" is not a UUID \u2014 if that's a drone label, pass it as label + cube_id instead.`);o=e,s=e}else if(t){if(!i)throw new Error("cube_id is required when evicting by label");const{drones:n}=await w(i),a=Le(n,t);if(!a)throw new Error(`No active drone labelled "${t}" in cube ${i} (it may already be evicted; check borg:list-drones).`);o=a.id,s=a.label}else throw new Error("Provide drone_id, or label + cube_id, to identify the drone to evict");return await pe(o),{content:[{type:"text",text:`Evicted drone ${s} (${o}). Soft-deleted: removed from the roster and freed its seat; log history preserved with anonymized attribution.`}]}}case"borg:list-drones":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{drones:t,roles:i}=await w(e);if(!t.length)return{content:[{type:"text",text:"No drones in this cube yet."}]};const o=new Map(i.map(n=>[n.id,n])),s=t.map(n=>{const a=o.get(n.role_id),c=Pe(a?.name??"?",n.agent_kind),d=n.wake_path_alert_class&&n.wake_path_alert_class!=="independent"?` \u2014 wake-path-class: ${n.wake_path_alert_class}`:"";return`- **${n.label}** (id: ${n.id}) \u2014 role: ${c} (${n.role_id}) \u2014 last seen ${n.last_seen}${d}`});return{content:[{type:"text",text:`Drones in cube ${e} (${t.length}):
21
+ `)}`}]}}case"borg:create-cube":{const e=r?.name,t=r?.cube_directive,i=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 o=null;if(i&&(o=U(i),!o))throw new Error(`Unknown template "${i}". Available: ${S().join(", ")}`);const s=ve(t,o),n=ke(void 0,o),a=await ne(e,s,{message_taxonomy:n});if(o){const d=await F(a.id,o),u=s!==t?" Template cube directive applied (operator passed empty).":"";return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}) with template **${i}** applied \u2014 ${d.created} role(s) created, ${d.updated} updated.${u} Use borg:assimilate ${a.name} to join as a drone.`}]}}return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}). A default "Drone" role was seeded \u2014 rename or replace it via borg:update-role / borg:create-role / borg:delete-role. Use borg:assimilate ${a.name} to join as a drone.`}]}}case"borg:update-cube":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const t={};if(typeof r?.name=="string"&&(t.name=r.name),typeof r?.cube_directive=="string"&&(t.cube_directive=r.cube_directive),Array.isArray(r?.message_taxonomy)&&(t.message_taxonomy=r.message_taxonomy),Object.keys(t).length===0)throw new Error("Pass at least one of: name, cube_directive, message_taxonomy.");const{cube:i}=await T(e,t);return{content:[{type:"text",text:`Updated cube **${i.name}** (id: ${i.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 i,o;if(t==="remove"){const n=r?.class;if(!n)throw new Error("class is required for remove.");({cube:i}=await A(e,{action:t,class:n})),o=n}else{const n=r?.class_def;if(n==null||typeof n!="object"||Array.isArray(n))throw new Error("class_def (object) is required for add/replace.");({cube:i}=await A(e,{action:t,class_def:n})),o=String(n.class??"")}return{content:[{type:"text",text:`${t==="add"?"Added":t==="replace"?"Replaced":"Removed"} taxonomy class **${o}** in cube **${i.name}** (id: ${i.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,i=r?.short_description,o=r?.detailed_description;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("name is required");if(i===void 0)throw new Error("short_description is required (pass empty string if none)");if(o===void 0)throw new Error("detailed_description is required (pass empty string if none)");const s=r?.is_default===!0,n=r?.is_human_seat===!0,a=r?.can_broadcast===!0,c=r?.receives_all_direct===!0,{role:d}=await ae(e,{name:t,short_description:i,detailed_description:o,is_default:s,is_human_seat:n,can_broadcast:a,receives_all_direct:c,...typeof r?.default_backend=="string"?{default_backend:r.default_backend}:{}}),u=[d.role_class==="queen"?"Queen":null,d.is_human_seat?"human-seat":null,d.is_default?"default":null].filter(Boolean).join(", "),b=u?` (${u})`:"";return{content:[{type:"text",text:`Created role **${d.name}**${b} (id: ${d.id}) in cube ${e}.`}]}}case"borg:update-role":{const e=r?.role_id;if(!e)throw new Error("role_id is required");const t={};if(typeof r?.name=="string"&&(t.name=r.name),typeof r?.short_description=="string"&&(t.short_description=r.short_description),typeof r?.detailed_description=="string"&&(t.detailed_description=r.detailed_description),typeof r?.is_default=="boolean"&&(t.is_default=r.is_default),typeof r?.is_human_seat=="boolean"&&(t.is_human_seat=r.is_human_seat),typeof r?.can_broadcast=="boolean"&&(t.can_broadcast=r.can_broadcast),typeof r?.receives_all_direct=="boolean"&&(t.receives_all_direct=r.receives_all_direct),typeof r?.default_backend=="string"&&(t.default_backend=r.default_backend),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, default_backend.");const{role:i}=await ce(e,t),o=[i.role_class==="queen"?"Queen":null,i.is_human_seat?"human-seat":null,i.is_default?"default":null].filter(Boolean).join(", "),s=o?` (${o})`:"";return{content:[{type:"text",text:`Updated role **${i.name}**${s} (id: ${i.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 i=r?.heading;if(!i)throw new Error("heading is required");let o;if(t==="delete")({role:o}=await $(e,{action:t,heading:i}));else{const n=r?.body;if(typeof n!="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:o}=await $(e,{action:t,heading:i,body:n,after:a}))}else({role:o}=await $(e,{action:t,heading:i,body:n}))}return{content:[{type:"text",text:`${t==="replace"?"Replaced":t==="insert"?"Inserted":"Deleted"} section **${i}** in role **${o.name}** (id: ${o.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:i}=await de(e,t);return{content:[{type:"text",text:`Reassigned drone ${i.label} (${i.id}) to role ${i.role_id}.`}]}}case"borg:evict-drone":{const e=r?.drone_id?.trim(),t=r?.label?.trim(),i=r?.cube_id?.trim();let o,s;if(e){if(!Me(e))throw new Error(`drone_id "${e}" is not a UUID \u2014 if that's a drone label, pass it as label + cube_id instead.`);o=e,s=e}else if(t){if(!i)throw new Error("cube_id is required when evicting by label");const{drones:n}=await w(i),a=Le(n,t);if(!a)throw new Error(`No active drone labelled "${t}" in cube ${i} (it may already be evicted; check borg:list-drones).`);o=a.id,s=a.label}else throw new Error("Provide drone_id, or label + cube_id, to identify the drone to evict");return await pe(o),{content:[{type:"text",text:`Evicted drone ${s} (${o}). Soft-deleted: removed from the roster and freed its seat; log history preserved with anonymized attribution.`}]}}case"borg:list-drones":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{drones:t,roles:i}=await w(e);if(!t.length)return{content:[{type:"text",text:"No drones in this cube yet."}]};const o=new Map(i.map(n=>[n.id,n])),s=t.map(n=>{const a=o.get(n.role_id),c=Ne(a?.name??"?",n.agent_kind),d=n.wake_path_alert_class&&n.wake_path_alert_class!=="independent"?` \u2014 wake-path-class: ${n.wake_path_alert_class}`:"";return`- **${n.label}** (id: ${n.id}) \u2014 role: ${c} (${n.role_id}) \u2014 last seen ${n.last_seen}${d}`});return{content:[{type:"text",text:`Drones in cube ${e} (${t.length}):
22
22
 
23
23
  ${s.join(`
24
24
  `)}`}]}}case"borg:list-roles":{const e=r?.cube_id;if(!e)throw new Error("cube_id is required");const{roles:t}=await w(e);return{content:[{type:"text",text:Re(t,e)}]}}case"borg:list-templates":return{content:[{type:"text",text:`Available templates:
25
25
 
26
26
  ${S().map(i=>{const o=U(i);return`- **${i}**: ${o.description}`}).join(`
27
- `)}`}]};case"borg:sync-roles":{const e=r?.cube_id,t=r?.template_name||"software-dev",i=r?.apply===!0,o=r?.decisions&&typeof r.decisions=="object"?r.decisions:void 0;if(!e)throw new Error("cube_id is required");const s=await be(e,t,i,o);return{content:[{type:"text",text:Ke(s,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 i=U(t);if(!i)throw new Error(`Unknown template "${t}". Available: ${S().join(", ")}`);const o=await F(e,i);let s="";const n=await w(e),a=xe(n.cube_directive,i);return a!==null&&(await T(e,{cube_directive:a}),s=" Template cube directive applied (cube directive was empty)."),{content:[{type:"text",text:`Applied template **${t}** to cube ${e} \u2014 ${o.created} role(s) created, ${o.updated} updated.${s}`}]}}default:throw new Error(`Unknown tool: ${g}`)}}catch(e){const t=Be(e??{});return t?{content:[{type:"text",text:t}],isError:!0}:{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}),p.setRequestHandler(G,async()=>({prompts:[{name:"subscribe",description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial."},{name:"dashboard",description:"Open Borg MCP dashboard to manage cubes"}]})),p.setRequestHandler(Y,async m=>{const{name:g}=m.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 K;await p.connect(x),await Xe(),console.error(`${v()}\u25FC Borg MCP Client started`),console.error(`${v()}\u25FC Use borg:assimilate <cube-name> to join a cube as a drone`),console.error(`${v()}\u25FC Manage your cubes at https://borgmcp.ai/dashboard`)}rt().catch(p=>{console.error(`${v()}Fatal error:`,p),process.exit(1)});
27
+ `)}`}]};case"borg:sync-roles":{const e=r?.cube_id,t=r?.template_name||"software-dev",i=r?.apply===!0,o=r?.decisions&&typeof r.decisions=="object"?r.decisions:void 0;if(!e)throw new Error("cube_id is required");const s=await he(e,t,i,o);return{content:[{type:"text",text:Ve(s,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 i=U(t);if(!i)throw new Error(`Unknown template "${t}". Available: ${S().join(", ")}`);const o=await F(e,i);let s="";const n=await w(e),a=xe(n.cube_directive,i);return a!==null&&(await T(e,{cube_directive:a}),s=" Template cube directive applied (cube directive was empty)."),{content:[{type:"text",text:`Applied template **${t}** to cube ${e} \u2014 ${o.created} role(s) created, ${o.updated} updated.${s}`}]}}default:throw new Error(`Unknown tool: ${g}`)}}catch(e){const t=Be(e??{});return t?{content:[{type:"text",text:t}],isError:!0}:{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}),p.setRequestHandler(G,async()=>({prompts:[{name:"subscribe",description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial."},{name:"dashboard",description:"Open Borg MCP dashboard to manage cubes"}]})),p.setRequestHandler(Y,async m=>{const{name:g}=m.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 V;await p.connect(x),await Xe(),console.error(`${v()}\u25FC Borg MCP Client started`),console.error(`${v()}\u25FC Use borg:assimilate <cube-name> to join a cube as a drone`),console.error(`${v()}\u25FC Manage your cubes at https://borgmcp.ai/dashboard`)}rt().catch(p=>{console.error(`${v()}Fatal error:`,p),process.exit(1)});
@@ -61,14 +61,25 @@ export declare function getStreamStatus(): StreamStatus & {
61
61
  */
62
62
  export declare function __resetStreamStateForTest(): void;
63
63
  /**
64
- * Spawn the background SSE consumer loop. Fire-and-forget; the loop
65
- * runs until process exit. Errors are written to stderr (so they don't
66
- * pollute the MCP stdio channel) and the loop continues.
67
- *
68
- * Idempotent in the sense that calling twice would create two parallel
69
- * loops; the caller (`index.ts`) wires this once at startup.
64
+ * gh#857 WI-2: start the codex /loop-equivalent heartbeat AT MOST ONCE — a
65
+ * re-entrant startLogStream must not leak a second interval (idempotent start).
66
+ * No-op for claude (startCodexHeartbeat returns null on a non-codex session, so
67
+ * no timer is ever stored). Extracted + injectable (`start`) so the production
68
+ * wiring is unit-testable without running the real stream loop (QA e75339e7).
69
+ */
70
+ export declare function ensureCodexHeartbeatStarted(start?: () => ReturnType<typeof setInterval> | null): void;
71
+ /**
72
+ * gh#861 finding 3: tear down the codex heartbeat timer — the teardown seam for
73
+ * the periodic interval. Called when the active cube is cleared (nothing to inject
74
+ * into) or the tick detects a dead app-server socket. Re-armable:
75
+ * ensureCodexHeartbeatStarted starts a fresh timer once an active cube returns.
70
76
  */
71
- export declare function startLogStream(): void;
77
+ export declare function stopCodexHeartbeat(): void;
78
+ /** Test-only alias of the production teardown seam (re-testable idempotence). */
79
+ export declare function __resetCodexHeartbeatForTest(): void;
80
+ export declare function startLogStream(opts?: {
81
+ runForever?: () => void;
82
+ }): void;
72
83
  export interface StreamDeps {
73
84
  /** Override the global fetch (tests inject a controlled Response). */
74
85
  fetchImpl?: typeof fetch;
@@ -1,11 +1,11 @@
1
- import{promises as h}from"node:fs";import z from"node:os";import x from"node:path";import{getActiveCube as Q,inboxPathForDrone as B}from"./cubes.js";import{formatCodexWakePrompt as Y,resolveSessionAgentKind as Z,wakeCodexViaAppServer as ee}from"./codex-app-wake.js";import{getValidToken as te}from"./remote-client.js";import{recordEventReceipt as ne,emitHealthBeat as re,getCachedMonitorHealthy as ae,getCachedWakeArmed as oe}from"./health-beat.js";import{getPackageVersion as ie}from"./version.js";import{acquireStreamLease as G,readOwnershipSnapshot as b}from"./stream-owner.js";const j=9e4,se=2e3,ce=500,le=3e4,de=50,R=512,Oe=R*2;function ue(){try{const e=z.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const fe=Date.now(),i={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function pe(e,t=Date.now()-fe){return e.connected&&e.lastWireActivityAt?"connected":!e.connected&&e.reconnectAttempts>0?"reconnecting":!e.connected&&!e.lastWireActivityAt&&e.reconnectAttempts===0&&t>1e4?"silent-inert":"never-started"}function $e(){return{...i,runLoopHealth:pe(i)}}function ve(){i.connected=!1,i.lastWireActivityAt=null,i.lastContentEventAt=null,i.lastHeartbeatAt=null,i.lastPersistedEventId=null,i.reconnectAttempts=0,i.runLoopRestartCount=0,i.ownership={state:"unowned"}}function Pe(){(async()=>{for(;;){try{await we(),process.stderr.write(`[borg-mcp log stream] runLoop returned unexpectedly; restarting in 5s
1
+ import{promises as h}from"node:fs";import Z from"node:os";import C from"node:path";import{getActiveCube as ee,inboxPathForDrone as j}from"./cubes.js";import{fireCodexHeartbeatTick as te,formatCodexWakePrompt as ne,resolveSessionAgentKind as re,startCodexHeartbeat as ae,wakeCodexViaAppServer as oe}from"./codex-app-wake.js";import{getValidToken as ie}from"./remote-client.js";import{recordEventReceipt as se,emitHealthBeat as ce,getCachedMonitorHealthy as le,getCachedWakeArmed as de}from"./health-beat.js";import{getPackageVersion as ue}from"./version.js";import{acquireStreamLease as K,readOwnershipSnapshot as b}from"./stream-owner.js";const V=9e4,fe=2e3,pe=500,me=3e4,we=50,R=512,Ge=R*2;function he(){try{const e=Z.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const ge=Date.now(),o={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function be(e,t=Date.now()-ge){return e.connected&&e.lastWireActivityAt?"connected":!e.connected&&e.reconnectAttempts>0?"reconnecting":!e.connected&&!e.lastWireActivityAt&&e.reconnectAttempts===0&&t>1e4?"silent-inert":"never-started"}function je(){return{...o,runLoopHealth:be(o)}}function Ke(){o.connected=!1,o.lastWireActivityAt=null,o.lastContentEventAt=null,o.lastHeartbeatAt=null,o.lastPersistedEventId=null,o.reconnectAttempts=0,o.runLoopRestartCount=0,o.ownership={state:"unowned"}}let S=null;function X(e=Ie){S||(S=e())}function Ie(){return ae({tick:()=>{te({isStreamOwner:()=>o.ownership?.state==="owner",onAppServerSocketDead:O})}})}function O(){S&&clearInterval(S),S=null}function Ve(){O()}function ye(){(async()=>{for(;;){try{await Ee(),process.stderr.write(`[borg-mcp log stream] runLoop returned unexpectedly; restarting in 5s
2
2
  `)}catch(e){process.stderr.write(`[borg-mcp log stream] runLoop threw: ${e?.message??e}; restarting in 5s
3
- `)}i.runLoopRestartCount+=1,await I(5e3)}})()}const K={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:_e,hasInboxEntryId:Te,getToken:te,wakeCodex:ee,heartbeatTimeoutMs:j,hwmDivergenceGraceMs:se,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:me};function me(e,t){ne(),re(e,{sseConnected:!0,inboxMonitorHealthy:ae(),wakeArmed:oe(),agentKind:Z(),hostname:ue(),version:ie(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function we(){let e=0,t=null,o=null,a=null,s=null;for(;;){const r=await Q();if(!r){a&&(await a.release(),a=null,s=null),i.connected=!1,i.ownership={state:"unowned"},await I(5e3);continue}r.cubeId!==o&&(o=r.cubeId,t=null);const c=`${r.cubeId}:${r.droneId}`;if(a&&s!==c&&(await a.release(),a=null,s=null),a||(a=await G(r.cubeId,r.droneId),s=a?c:null),!a){i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}i.ownership=await b(r.cubeId,r.droneId);let l=!1;try{const f=new AbortController,g=async()=>{try{await a.refresh()||(l=!0,f.abort(new Error("stream ownership lost")))}catch(p){l=!0,f.abort(p instanceof Error?p:new Error(String(p)))}},C=setInterval(()=>{g()},Math.max(1e3,Math.floor(j/2)));try{await V(r,t,p=>{t=p},{abortSignal:f.signal})}finally{clearInterval(C)}if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}e=0,i.reconnectAttempts=0}catch(f){if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}i.connected=!1;const g=Math.min(ce*2**e,le)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(g)}ms: ${f?.message??f}
4
- `),e+=1,i.reconnectAttempts=e,await I(g)}}}async function V(e,t,o,a={}){const{fetchImpl:s,appendLine:r,hasInboxEntryId:c,getToken:l,wakeCodex:f,heartbeatTimeoutMs:g,hwmDivergenceGraceMs:C,abortSignal:p,onInboxReceipt:U}={...K,...a},O=await l(),$={Authorization:`Bearer ${O}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&($["Last-Event-ID"]=t);const A=new AbortController,H=()=>{try{A.abort(p.reason??new Error("external abort"))}catch{}};p.aborted&&H(),p.addEventListener("abort",H,{once:!0});let m=null;const v=()=>{m&&clearTimeout(m),m=setTimeout(()=>{try{A.abort(new Error("heartbeat watchdog timeout"))}catch{}},g)};v();let P=t,u=null,w=null;const _=()=>{w&&(clearTimeout(w.timer),w=null)};let S=null;const T=(n,d)=>{const k={id:n,created_at:d};S&&d&&S.created_at&&y(k,S)<=0||(S=k,P=n,i.lastPersistedEventId=n,o(n))},L=n=>{n&&(u=!u||y(n,u)>0?n:u,w&&y(u,w.hwm)>=0&&_())},q=n=>{if(w?.hwm.id===n.id)return;_();const d=setTimeout(()=>{if(u&&y(u,n)>=0){_();return}try{A.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},C);w={hwm:n,timer:d}},M=new Set,D=[];let W=t!==null;const F=async n=>{const d=Ie(be(n.data,n.id));return W&&await c(e.cubeId,e.droneId,n.id,d)?(T(n.id,n.data?.created_at??""),"persisted-skip"):(await r(e.cubeId,e.droneId,d),f(Y(d)),U(e,O),"written")},N=n=>{for(M.add(n.id),D.push(n.id);D.length>de;){const d=D.shift();d&&M.delete(d)}T(n.id,n.data?.created_at??""),L(J(n))};let E;try{E=await s(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:$,signal:A.signal})}catch(n){throw m&&clearTimeout(m),n}if(!E.ok||!E.body)throw m&&clearTimeout(m),new Error(`stream HTTP ${E.status}`);i.connected=!0;try{for await(const n of he(E.body)){v();const d=new Date().toISOString();if(i.lastWireActivityAt=d,(n.type==="log"||n.type==="bookmark")&&(i.lastContentEventAt=d),n.type==="heartbeat"){if(i.lastHeartbeatAt=d,n.hwm&&u===null){L(n.hwm),P===null&&T(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&u&&y(n.hwm,u)<=0){_();continue}n.hwm&&u&&y(n.hwm,u)>0&&q(n.hwm);continue}if(n.type==="bookmark"){W=!1;continue}if(n.type==="log"){if(M.has(n.id)){T(n.id,n.data?.created_at??""),L(J(n));continue}const k=typeof n.data?.message=="string"&&n.data.message.startsWith("[HEARTBEAT-PING]");if(n.data?.kind==="ack"){if(n.data?.author_drone_id===e.droneId&&await F(n)==="persisted-skip")continue;N(n);continue}if(n.data?.drone_id===e.droneId&&!k){N(n);continue}if(await F(n)==="persisted-skip")continue;N(n)}}}finally{p.removeEventListener("abort",H),m&&clearTimeout(m),_(),i.connected=!1}}async function We(e,t,o,a={}){const{ownerDeps:s,ownerStaleMs:r}={...K,...a},c=await G(e.cubeId,e.droneId,r,s);if(!c)return i.connected=!1,i.ownership=await b(e.cubeId,e.droneId,s),"skipped";i.ownership=await b(e.cubeId,e.droneId,s);try{return await V(e,t,o,a),"streamed"}finally{await c.release()}}async function*he(e){const t=e.getReader(),o=new TextDecoder;let a="";try{for(;;){const{value:s,done:r}=await t.read();if(r){if(a.trim()){const l=X(a);l&&(yield l)}return}a+=o.decode(s,{stream:!0});let c;for(;(c=a.indexOf(`
3
+ `)}o.runLoopRestartCount+=1,await y(5e3)}})()}function Xe(e={}){X(),(e.runForever??ye)()}const J={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:Ce,hasInboxEntryId:Me,getToken:ie,wakeCodex:oe,heartbeatTimeoutMs:V,hwmDivergenceGraceMs:fe,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:_e};function _e(e,t){se(),ce(e,{sseConnected:!0,inboxMonitorHealthy:le(),wakeArmed:de(),agentKind:re(),hostname:he(),version:ue(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function Ee(){let e=0,t=null,i=null,a=null,s=null;for(;;){const r=await ee();if(!r){a&&(await a.release(),a=null,s=null),o.connected=!1,o.ownership={state:"unowned"},O(),await y(5e3);continue}X(),r.cubeId!==i&&(i=r.cubeId,t=null);const c=`${r.cubeId}:${r.droneId}`;if(a&&s!==c&&(await a.release(),a=null,s=null),a||(a=await K(r.cubeId,r.droneId),s=a?c:null),!a){o.connected=!1,o.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}o.ownership=await b(r.cubeId,r.droneId);let l=!1;try{const f=new AbortController,g=async()=>{try{await a.refresh()||(l=!0,f.abort(new Error("stream ownership lost")))}catch(p){l=!0,f.abort(p instanceof Error?p:new Error(String(p)))}},H=setInterval(()=>{g()},Math.max(1e3,Math.floor(V/2)));try{await U(r,t,p=>{t=p},{abortSignal:f.signal})}finally{clearInterval(H)}if(l){a=null,s=null,o.connected=!1,o.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}e=0,o.reconnectAttempts=0}catch(f){if(l){a=null,s=null,o.connected=!1,o.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}o.connected=!1;const g=Math.min(pe*2**e,me)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(g)}ms: ${f?.message??f}
4
+ `),e+=1,o.reconnectAttempts=e,await y(g)}}}async function U(e,t,i,a={}){const{fetchImpl:s,appendLine:r,hasInboxEntryId:c,getToken:l,wakeCodex:f,heartbeatTimeoutMs:g,hwmDivergenceGraceMs:H,abortSignal:p,onInboxReceipt:Q}={...J,...a},$=await l(),F={Authorization:`Bearer ${$}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&(F["Last-Event-ID"]=t);const A=new AbortController,L=()=>{try{A.abort(p.reason??new Error("external abort"))}catch{}};p.aborted&&L(),p.addEventListener("abort",L,{once:!0});let m=null;const P=()=>{m&&clearTimeout(m),m=setTimeout(()=>{try{A.abort(new Error("heartbeat watchdog timeout"))}catch{}},g)};P();let W=t,u=null,w=null;const _=()=>{w&&(clearTimeout(w.timer),w=null)};let x=null;const T=(n,d)=>{const k={id:n,created_at:d};x&&d&&x.created_at&&I(k,x)<=0||(x=k,W=n,o.lastPersistedEventId=n,i(n))},D=n=>{n&&(u=!u||I(n,u)>0?n:u,w&&I(u,w.hwm)>=0&&_())},Y=n=>{if(w?.hwm.id===n.id)return;_();const d=setTimeout(()=>{if(u&&I(u,n)>=0){_();return}try{A.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},H);w={hwm:n,timer:d}},M=new Set,N=[];let B=t!==null;const G=async n=>{const d=ke(xe(n.data,n.id));return B&&await c(e.cubeId,e.droneId,n.id,d)?(T(n.id,n.data?.created_at??""),"persisted-skip"):(await r(e.cubeId,e.droneId,d),f(ne(d)),Q(e,$),"written")},v=n=>{for(M.add(n.id),N.push(n.id);N.length>we;){const d=N.shift();d&&M.delete(d)}T(n.id,n.data?.created_at??""),D(z(n))};let E;try{E=await s(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:F,signal:A.signal})}catch(n){throw m&&clearTimeout(m),n}if(!E.ok||!E.body)throw m&&clearTimeout(m),new Error(`stream HTTP ${E.status}`);o.connected=!0;try{for await(const n of Se(E.body)){P();const d=new Date().toISOString();if(o.lastWireActivityAt=d,(n.type==="log"||n.type==="bookmark")&&(o.lastContentEventAt=d),n.type==="heartbeat"){if(o.lastHeartbeatAt=d,n.hwm&&u===null){D(n.hwm),W===null&&T(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&u&&I(n.hwm,u)<=0){_();continue}n.hwm&&u&&I(n.hwm,u)>0&&Y(n.hwm);continue}if(n.type==="bookmark"){B=!1;continue}if(n.type==="log"){if(M.has(n.id)){T(n.id,n.data?.created_at??""),D(z(n));continue}const k=typeof n.data?.message=="string"&&n.data.message.startsWith("[HEARTBEAT-PING]");if(n.data?.kind==="ack"){if(n.data?.author_drone_id===e.droneId&&await G(n)==="persisted-skip")continue;v(n);continue}if(n.data?.drone_id===e.droneId&&!k){v(n);continue}if(await G(n)==="persisted-skip")continue;v(n)}}}finally{p.removeEventListener("abort",L),m&&clearTimeout(m),_(),o.connected=!1}}async function Je(e,t,i,a={}){const{ownerDeps:s,ownerStaleMs:r}={...J,...a},c=await K(e.cubeId,e.droneId,r,s);if(!c)return o.connected=!1,o.ownership=await b(e.cubeId,e.droneId,s),"skipped";o.ownership=await b(e.cubeId,e.droneId,s);try{return await U(e,t,i,a),"streamed"}finally{await c.release()}}async function*Se(e){const t=e.getReader(),i=new TextDecoder;let a="";try{for(;;){const{value:s,done:r}=await t.read();if(r){if(a.trim()){const l=q(a);l&&(yield l)}return}a+=i.decode(s,{stream:!0});let c;for(;(c=a.indexOf(`
5
5
 
6
- `))!==-1;){const l=a.slice(0,c);a=a.slice(c+2);const f=X(l);f&&(yield f)}}}finally{try{t.releaseLock()}catch{}}}function X(e){let t=null,o=null,a=[];for(const r of e.split(`
7
- `))r.startsWith("event:")?t=r.slice(6).trim():r.startsWith("id:")?o=r.slice(3).trim():r.startsWith("data:")&&a.push(r.slice(5).trim());const s=a.join(`
8
- `);if(!t)return null;if(t==="log"){if(!o)return null;let r;try{r=JSON.parse(s)}catch{return null}return{type:"log",id:o,data:r}}if(t==="heartbeat"){let r=null,c=null;try{const l=JSON.parse(s);r=typeof l.ts=="string"?l.ts:null,c=ge(l.hwm)}catch{}return{type:"heartbeat",ts:r,hwm:c}}if(t==="bookmark"){let r=null;try{const c=JSON.parse(s);r=typeof c.as_of=="string"?c.as_of:null}catch{}return{type:"bookmark",as_of:r}}return{type:"unknown",raw:e}}function ge(e){if(!e||typeof e!="object")return null;const t=e;return typeof t.id=="string"&&t.id.length>0&&typeof t.created_at=="string"&&t.created_at.length>0?{id:t.id,created_at:t.created_at}:null}function y(e,t){const o=Date.parse(e.created_at),a=Date.parse(t.created_at);return Number.isFinite(o)&&Number.isFinite(a)&&o!==a?o-a:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function J(e){if(e.data?.visibility==="direct"||e.data?.kind==="ack")return null;const t=e.data?.created_at;return typeof t=="string"&&t.length>0?{id:e.id,created_at:t}:null}function be(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function ye(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function Ie(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),o=e.drone_label??"?",a=e.role_name??"?",s=typeof e.message=="string"?e.message:"",r=ye(e.id,e.entry_id),c=r?`[entry_id: ${r}] `:"",l=s.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${o} (${a}): ${c}${l}`}async function _e(e,t,o){const a=B(e,t);await Ee(a,o,R)}async function Ee(e,t,o=R,a=o*2){await h.mkdir(x.dirname(e),{recursive:!0}),await h.appendFile(e,t+`
9
- `,"utf-8"),await Ae(e,o,a)}async function Ae(e,t,o=t){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");if(!Number.isInteger(o)||o<t)throw new Error("trimThresholdLines must be an integer >= maxLines");const s=(await h.readFile(e,"utf-8")).split(/\r?\n/);if(s.at(-1)===""&&s.pop(),s.length<=o)return;const r=s.slice(-t),c=x.join(x.dirname(e),`.${x.basename(e)}.${process.pid}.${Date.now()}.tmp`);try{await h.writeFile(c,r.join(`
6
+ `))!==-1;){const l=a.slice(0,c);a=a.slice(c+2);const f=q(l);f&&(yield f)}}}finally{try{t.releaseLock()}catch{}}}function q(e){let t=null,i=null,a=[];for(const r of e.split(`
7
+ `))r.startsWith("event:")?t=r.slice(6).trim():r.startsWith("id:")?i=r.slice(3).trim():r.startsWith("data:")&&a.push(r.slice(5).trim());const s=a.join(`
8
+ `);if(!t)return null;if(t==="log"){if(!i)return null;let r;try{r=JSON.parse(s)}catch{return null}return{type:"log",id:i,data:r}}if(t==="heartbeat"){let r=null,c=null;try{const l=JSON.parse(s);r=typeof l.ts=="string"?l.ts:null,c=Ae(l.hwm)}catch{}return{type:"heartbeat",ts:r,hwm:c}}if(t==="bookmark"){let r=null;try{const c=JSON.parse(s);r=typeof c.as_of=="string"?c.as_of:null}catch{}return{type:"bookmark",as_of:r}}return{type:"unknown",raw:e}}function Ae(e){if(!e||typeof e!="object")return null;const t=e;return typeof t.id=="string"&&t.id.length>0&&typeof t.created_at=="string"&&t.created_at.length>0?{id:t.id,created_at:t.created_at}:null}function I(e,t){const i=Date.parse(e.created_at),a=Date.parse(t.created_at);return Number.isFinite(i)&&Number.isFinite(a)&&i!==a?i-a:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function z(e){if(e.data?.visibility==="direct"||e.data?.kind==="ack")return null;const t=e.data?.created_at;return typeof t=="string"&&t.length>0?{id:e.id,created_at:t}:null}function xe(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function Te(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function ke(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),i=e.drone_label??"?",a=e.role_name??"?",s=typeof e.message=="string"?e.message:"",r=Te(e.id,e.entry_id),c=r?`[entry_id: ${r}] `:"",l=s.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${i} (${a}): ${c}${l}`}async function Ce(e,t,i){const a=j(e,t);await He(a,i,R)}async function He(e,t,i=R,a=i*2){await h.mkdir(C.dirname(e),{recursive:!0}),await h.appendFile(e,t+`
9
+ `,"utf-8"),await Le(e,i,a)}async function Le(e,t,i=t){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");if(!Number.isInteger(i)||i<t)throw new Error("trimThresholdLines must be an integer >= maxLines");const s=(await h.readFile(e,"utf-8")).split(/\r?\n/);if(s.at(-1)===""&&s.pop(),s.length<=i)return;const r=s.slice(-t),c=C.join(C.dirname(e),`.${C.basename(e)}.${process.pid}.${Date.now()}.tmp`);try{await h.writeFile(c,r.join(`
10
10
  `)+`
11
- `,"utf-8"),await h.rename(c,e)}catch(l){throw await h.rm(c,{force:!0}),l}}function Se(e,t,o){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const a=t?o.replace(`[entry_id: ${t}] `,""):o;return!!(a&&e.split(/\r?\n/).includes(a))}async function Te(e,t,o,a){const s=B(e,t);let r;try{r=await h.readFile(s,"utf-8")}catch(c){if(c?.code==="ENOENT")return!1;throw c}return Se(r,o,a)}function I(e){return new Promise(t=>setTimeout(t,e))}export{R as INBOX_TAIL_LINES_CAP,Oe as INBOX_TAIL_TRIM_THRESHOLD_LINES,ve as __resetStreamStateForTest,Ee as appendCappedInboxLine,pe as classifyRunLoopHealth,y as compareBroadcastHwm,Ie as formatInboxLine,$e as getStreamStatus,Se as inboxRawHasEntry,he as parseSSE,Pe as startLogStream,V as streamOnce,We as streamOnceIfOwner,Ae as trimInboxFileToRecentLines};
11
+ `,"utf-8"),await h.rename(c,e)}catch(l){throw await h.rm(c,{force:!0}),l}}function De(e,t,i){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const a=t?i.replace(`[entry_id: ${t}] `,""):i;return!!(a&&e.split(/\r?\n/).includes(a))}async function Me(e,t,i,a){const s=j(e,t);let r;try{r=await h.readFile(s,"utf-8")}catch(c){if(c?.code==="ENOENT")return!1;throw c}return De(r,i,a)}function y(e){return new Promise(t=>setTimeout(t,e))}export{R as INBOX_TAIL_LINES_CAP,Ge as INBOX_TAIL_TRIM_THRESHOLD_LINES,Ve as __resetCodexHeartbeatForTest,Ke as __resetStreamStateForTest,He as appendCappedInboxLine,be as classifyRunLoopHealth,I as compareBroadcastHwm,X as ensureCodexHeartbeatStarted,ke as formatInboxLine,je as getStreamStatus,De as inboxRawHasEntry,Se as parseSSE,Xe as startLogStream,O as stopCodexHeartbeat,U as streamOnce,Je as streamOnceIfOwner,Le as trimInboxFileToRecentLines};
@@ -1 +1 @@
1
- function a(n){let o;const t={};for(let l=0;l<n.length;l+=1){const r=n[l];if(r==="--worktree"){const e=n[l+1];if(typeof e!="string"||e.length===0)return{ok:!1,error:"--worktree requires a name argument (e.g. `--worktree drone-2`)"};t.worktree=e,l+=1}else if(r==="--template"){const e=n[l+1];if(typeof e!="string"||e.length===0)return{ok:!1,error:"--template requires a name argument (e.g. `--template software-dev`)"};t.template=e,l+=1}else if(r==="--no-template")t.noTemplate=!0;else if(r==="--cube-name"){const e=n[l+1];if(typeof e!="string"||e.length===0)return{ok:!1,error:"--cube-name requires a name argument (e.g. `--cube-name my-cube`)"};t.cubeName=e,l+=1}else if(r==="--here")t.here=!0;else if(r==="--yes"||r==="-y")t.yes=!0;else if(r==="--cli"){const e=n[l+1];if(e!=="claude"&&e!=="codex")return{ok:!1,error:"--cli requires claude or codex"};t.cli=e,l+=1}else if(r.startsWith("--cli=")){const e=r.slice(6);if(e!=="claude"&&e!=="codex")return{ok:!1,error:"--cli requires claude or codex"};t.cli=e}else{if(r.startsWith("--"))return{ok:!1,error:`unknown flag: ${r}. Supported: --worktree, --template, --no-template, --cube-name, --here, --yes, --cli`};if(o!==void 0)return{ok:!1,error:`unexpected extra argument: ${r} (already have role "${o}")`};o=r}}return t.template!==void 0&&t.noTemplate?{ok:!1,error:"--template and --no-template are mutually exclusive"}:{ok:!0,role:o,flags:t}}export{a as parseAssimilateArgs};
1
+ function c(l){let n;const t={};for(let a=0;a<l.length;a+=1){const r=l[a];if(r==="--worktree"){const e=l[a+1];if(typeof e!="string"||e.length===0)return{ok:!1,error:"--worktree requires a name argument (e.g. `--worktree drone-2`)"};t.worktree=e,a+=1}else if(r==="--template"){const e=l[a+1];if(typeof e!="string"||e.length===0)return{ok:!1,error:"--template requires a name argument (e.g. `--template software-dev`)"};t.template=e,a+=1}else if(r==="--no-template")t.noTemplate=!0;else if(r==="--cube-name"){const e=l[a+1];if(typeof e!="string"||e.length===0)return{ok:!1,error:"--cube-name requires a name argument (e.g. `--cube-name my-cube`)"};t.cubeName=e,a+=1}else if(r==="--here")t.here=!0;else if(r==="--yes"||r==="-y")t.yes=!0;else if(r==="--cli"){const e=l[a+1];if(e!=="claude"&&e!=="codex")return{ok:!1,error:"--cli requires claude or codex"};t.cli=e,a+=1}else if(r.startsWith("--cli=")){const e=r.slice(6);if(e!=="claude"&&e!=="codex")return{ok:!1,error:"--cli requires claude or codex"};t.cli=e}else if(r==="--backend"){const e=l[a+1];if(typeof e!="string"||e.length===0)return{ok:!1,error:"--backend requires a descriptor (e.g. `--backend claude:claude-opus-4-8` or `--backend ollama:qwen3-coder-next:q4_K_M`)"};if(!e.match(/^(claude|ollama):[A-Za-z0-9._:\/-]+$/))return{ok:!1,error:`invalid backend descriptor '${e}' \u2014 expected <kind>:<model> where kind is claude or ollama`};t.backend=e,a+=1}else if(r.startsWith("--backend=")){const e=r.slice(10);if(!e.match(/^(claude|ollama):[A-Za-z0-9._:\/-]+$/))return{ok:!1,error:`invalid backend descriptor '${e}' \u2014 expected <kind>:<model> where kind is claude or ollama`};t.backend=e}else{if(r.startsWith("--"))return{ok:!1,error:`unknown flag: ${r}. Supported: --worktree, --template, --no-template, --cube-name, --here, --yes, --cli, --backend`};if(n!==void 0)return{ok:!1,error:`unexpected extra argument: ${r} (already have role "${n}")`};n=r}}return t.template!==void 0&&t.noTemplate?{ok:!1,error:"--template and --no-template are mutually exclusive"}:{ok:!0,role:n,flags:t}}export{c as parseAssimilateArgs};
@@ -334,6 +334,7 @@ export declare function createRole(cubeId: string, data: {
334
334
  is_human_seat?: boolean;
335
335
  can_broadcast?: boolean;
336
336
  receives_all_direct?: boolean;
337
+ default_backend?: string;
337
338
  role_class?: 'queen' | 'worker';
338
339
  }): Promise<{
339
340
  role: any;
@@ -349,6 +350,7 @@ export declare function updateRole(roleId: string, updates: {
349
350
  is_human_seat?: boolean;
350
351
  can_broadcast?: boolean;
351
352
  receives_all_direct?: boolean;
353
+ default_backend?: string;
352
354
  role_class?: 'queen' | 'worker';
353
355
  }): Promise<{
354
356
  role: any;
@@ -3,6 +3,7 @@ export interface Role {
3
3
  name: string;
4
4
  is_default: boolean;
5
5
  is_human_seat: boolean;
6
+ default_backend?: string | null;
6
7
  }
7
8
  /**
8
9
  * Normalize a role-name argument or stored name into a slug used for
@@ -30,10 +30,20 @@ export interface RosterDrone {
30
30
  regen_count?: number | null;
31
31
  wake_path?: 'live' | 'degraded' | 'deaf' | null;
32
32
  wake_path_alert_class?: 'dead' | 'post-blocked' | 'presumed-dead' | 'systemic-post-block' | 'wake-path-deaf' | 'independent' | null;
33
+ /**
34
+ * Backend descriptor (e.g., "claude:claude-opus-4-8" or "ollama:qwen3-coder-next:q4_K_M").
35
+ * Null for drones with no explicit backend (use cube/role default).
36
+ */
37
+ backend?: string | null;
33
38
  }
34
39
  export interface RosterRole {
35
40
  id: string;
36
41
  name: string;
42
+ /**
43
+ * Default backend descriptor for drones assigned to this role.
44
+ * Null = no role default (use cube default or null for Claude).
45
+ */
46
+ default_backend?: string | null;
37
47
  }
38
48
  export declare function formatRoleAgentLabel(roleName: string, agentKind: RosterDrone['agent_kind']): string;
39
49
  export interface RenderRosterInputs {
@@ -1,3 +1,3 @@
1
- const d={degraded:"unverified",deaf:"unresponsive"};function w(t,s){return s?`${t}, ${s}`:t}function b(t){const{cubeName:s,drones:o,roles:h,resolvedSince:a,humanAgo:r}=t,c=new Map;for(const e of h)c.set(e.id,e);const n=[];if(n.push(`# Drones in cube: ${s}`),n.push(""),a&&(n.push(`_Liveness probe since ${a} (${r(a)}). \`awake\` = drone posted to the cube log after that point._`),n.push("")),!o.length)return n.push("_(no drones connected)_"),n.join(`
2
- `);for(const e of o){const k=c.get(e.role_id)?.name??"unknown",l=w(k,e.agent_kind),p=r(e.last_seen),u=e.wake_path&&e.wake_path!=="live"?` \xB7 \`wake-path:${d[e.wake_path]??e.wake_path}\``:"",_=e.wake_path_alert_class&&e.wake_path_alert_class!=="independent"?` \xB7 \`wake-path-class:${e.wake_path_alert_class}\``:"",i=typeof e.regen_count=="number"?` \xB7 \`regen-count:${e.regen_count}\``:"";if(a){const $=e.seen_since===!0?"`awake`":"`stale`";n.push(`- **${e.label}** (${l}) \u2014 last seen ${p} \xB7 ${$}${i}${u}${_}`)}else n.push(`- **${e.label}** (${l}) \u2014 last seen ${p}${i}${u}${_}`)}return n.join(`
3
- `)}export{w as formatRoleAgentLabel,b as renderRoster};
1
+ const w={degraded:"unverified",deaf:"unresponsive"};function f(t,a){return a?`${t}, ${a}`:t}function A(t){const{cubeName:a,drones:o,roles:i,resolvedSince:s,humanAgo:r}=t,c=new Map;for(const e of i)c.set(e.id,e);const n=[];if(n.push(`# Drones in cube: ${a}`),n.push(""),s&&(n.push(`_Liveness probe since ${s} (${r(s)}). \`awake\` = drone posted to the cube log after that point._`),n.push("")),!o.length)return n.push("_(no drones connected)_"),n.join(`
2
+ `);for(const e of o){const h=c.get(e.role_id)?.name??"unknown",l=f(h,e.agent_kind),k=r(e.last_seen),p=e.backend?` \xB7 \`${e.backend}\``:"",u=e.wake_path&&e.wake_path!=="live"?` \xB7 \`wake-path:${w[e.wake_path]??e.wake_path}\``:"",_=e.wake_path_alert_class&&e.wake_path_alert_class!=="independent"?` \xB7 \`wake-path-class:${e.wake_path_alert_class}\``:"",$=typeof e.regen_count=="number"?` \xB7 \`regen-count:${e.regen_count}\``:"";if(s){const d=e.seen_since===!0?"`awake`":"`stale`";n.push(`- **${e.label}** (${l}) \u2014 last seen ${k} \xB7 ${d}${$}${u}${_}${p}`)}else n.push(`- **${e.label}** (${l}) \u2014 last seen ${k}${$}${u}${_}${p}`)}return n.join(`
3
+ `)}export{f as formatRoleAgentLabel,A as renderRoster};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
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",