borgmcp 1.0.45 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assimilate-cmd.js +40 -40
- package/dist/cleanup-cmd.js +14 -13
- package/dist/config-utils.d.ts +17 -0
- package/dist/config-utils.js +3 -3
- package/dist/cubes.js +5 -5
- package/dist/launch-all-cmd.js +30 -26
- package/dist/log-stream.d.ts +39 -3
- package/dist/log-stream.js +9 -9
- package/dist/setup-confirm.d.ts +31 -0
- package/dist/setup-confirm.js +2 -2
- package/dist/setup.js +28 -28
- package/package.json +1 -1
package/dist/assimilate-cmd.js
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import{dirname as
|
|
2
|
-
`),1}if(r.flags.worktree!==void 0){const t=
|
|
3
|
-
`),1}let
|
|
4
|
-
`)}}let l=null;if(
|
|
5
|
-
`);let
|
|
6
|
-
`),
|
|
7
|
-
`),1;let s,
|
|
1
|
+
import{dirname as ne,basename as C}from"node:path";import{randomUUID as oe}from"node:crypto";import{roleSlug as ie,matchRoleByName as ae,pickDefaultRole as le}from"./role-resolver.js";import{deriveCubeName as se,parseGitRemote as ce,sanitizeRemoteUrl as ue}from"./cube-name.js";import{validateName as K}from"./name-validator.js";import{renderAssimilationWelcome as me}from"./assimilate-welcome.js";import{shellEscape as fe}from"./shell-escape.js";import{withCodexCwdArg as de}from"./codex-remote.js";import{buildAgentKickoffPrompt as he,buildKickoffWakePathClause as ge,recordCodexWakeTarget as we,socketPathFromRemoteArgs as be}from"./codex-launch.js";import{perWorktreeBranchName as O,adoptWorktree as ke,computeWorktreePath as q,localBranchExists as z,isMerged as ye}from"./worktree-lifecycle.js";import{DroneEvictedError as ve}from"./drone-lifecycle.js";import{codexBorgSessionConfigArgs as $e}from"./launch-gate.js";import{resolveLaunchEnv as pe,resolveOllamaBaseUrl as xe,parseModel as Se}from"./model-presets.js";async function Ke(r,e){if(r.role!==void 0){const t=K(r.role);if(!t.ok)return e.stderr(t.error+`
|
|
2
|
+
`),1}if(r.flags.worktree!==void 0){const t=K(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
|
|
3
|
+
`),1}let a=await e.getCachedAuth();if(!a){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;a=await e.runSetup()}const n=e.findProjectRoot(e.cwd());let o;if(r.flags.cubeName)o=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],n),i=t.status===0?t.stdout:null;if(o=se(n,i),i){const u=ue(i),m=u?ce(u):null;u&&!m&&o&&e.stderr(`couldn't parse git remote '${u}' \u2014 using directory name '${o}' as cube name
|
|
4
|
+
`)}}let l=null;if(o&&o.includes("@")&&o.includes(":")){const t=o.lastIndexOf(":");l={ownerEmail:o.substring(0,t),cubeName:o.substring(t+1)},o=l.cubeName}const R=e.cwd();e.stderr(`Checking your cubes\u2026
|
|
5
|
+
`);let A;try{A=await e.listCubes(a.apiUrl,a.token)}catch(t){const i=t instanceof Error?t.message:String(t);if(i.includes("Authentication required")||i.includes("Authentication expired"))e.stderr(`Re-authenticating...
|
|
6
|
+
`),a=await e.runSetup(),A=await e.listCubes(a.apiUrl,a.token);else throw t}const N=A.find(t=>t.name===o);if(!N&&l)return e.stderr(`No cube named '${l.cubeName}' accessible to you owned by '${l.ownerEmail}'. Did you accept their invite? See borgmcp.ai/dashboard.
|
|
7
|
+
`),1;let s,P;if(N)s=await e.getCube(a.apiUrl,a.token,N.id),P=!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 i=await e.listTemplates(a.apiUrl,a.token),u=["First drone joining a new cube. Apply a template?"];i.forEach(($,p)=>{const _=p===0?" (default)":"";u.push(` ${p+1}) ${$.name}${_} \u2014 ${$.description}`)}),u.push(` ${i.length+1}) skip \u2014 no template`);const m=(await e.prompt(u.join(`
|
|
8
8
|
`)+`
|
|
9
|
-
[1]: `)).trim(),y=m===""?1:parseInt(m,10);if(Number.isNaN(y)||y<1||y>
|
|
10
|
-
`),1;t=y<=
|
|
9
|
+
[1]: `)).trim(),y=m===""?1:parseInt(m,10);if(Number.isNaN(y)||y<1||y>i.length+1)return e.stderr(`invalid choice "${m}"
|
|
10
|
+
`),1;t=y<=i.length?i[y-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
|
-
`),1;t="starter"}e.stderr(
|
|
12
|
+
`),1;t="starter"}e.stderr(o?`Creating cube '${o}'\u2026
|
|
13
13
|
`:`Creating your cube\u2026
|
|
14
|
-
`),s=await e.createCube(
|
|
14
|
+
`),s=await e.createCube(a.apiUrl,a.token,t?{name:o??void 0,template:t}:{name:o??void 0}),P=!0}let d;if(r.role!==void 0){if(d=ae(s.roles,r.role),!d){const t=s.roles.map(m=>m.name).join(", "),i=_e(r.role,s.roles.map(m=>m.name)),u=i?` Did you mean "${i}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${s.name}". Available: ${t}.${u}
|
|
15
15
|
(Use --template <name> on first-drone setup or run \`borg_create-role\` from inside Claude.)
|
|
16
|
-
`),1}}else if(d=
|
|
17
|
-
`),1;const
|
|
18
|
-
`),1;const
|
|
19
|
-
`),1}const
|
|
20
|
-
`);let c;try{c=await e.assimilate(
|
|
21
|
-
`),1;const
|
|
22
|
-
`),1}const
|
|
23
|
-
`):
|
|
24
|
-
`);const
|
|
16
|
+
`),1}}else if(d=le(s.roles,{isFirstDrone:P}),!d)return e.stderr(`cube "${s.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 x=await e.getActiveCube();let S;if(x&&r.flags.here)if(x.cubeId===s.id)S=x.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
|
|
18
|
+
`),1;const H=r.flags.worktree!==void 0||x!==null&&!r.flags.here,V=S??x?.droneId??null,T=H?null:await e.getLaunchModel(s.id,n,V),b=r.flags.model??T?.model??d.default_model??null,I=xe(process.env,b!=null&&b===T?.model?T?.ollamaBaseUrl:void 0);if(b){const t=await e.checkModelReachable(b,e.fetch,I);if(!t.ok)return e.stderr(`${t.message}
|
|
19
|
+
`),1}const h=await e.resolveCli(r.flags.cli);e.stderr(`Joining cube '${s.name}' as ${d.name}\u2026
|
|
20
|
+
`);let c;try{c=await e.assimilate(a.apiUrl,a.token,{cube_id:s.id,role_id:d.id,hostname:e.getHostname(),agent_kind:h,model:b,...S?{prior_drone_id:S}:{}})}catch(t){if(t instanceof ve&&S!=null)return e.stderr(`seat evicted \u2014 this worktree's saved seat was evicted from the cube. Re-assimilate fresh from a terminal, or remove this worktree.
|
|
21
|
+
`),1;const i=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${i}
|
|
22
|
+
`),1}const k=s.roles.find(t=>t.id===c.role_id)??d;c.reattached?e.stderr(`re-attached to existing seat ${c.drone_label} (session token rotated, no new drone minted)
|
|
23
|
+
`):k.id!==d.id&&e.stderr(`Note: your invite didn't grant the "${d.name}" role \u2014 assimilated as "${k.name}" instead.
|
|
24
|
+
`);const Q=H;let g=null;if(Q){const t=e.runSync("git",["rev-parse","--verify","HEAD"],n);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
|
|
25
25
|
Fix: create at least one commit (\`git commit --allow-empty -m "initial"\`)
|
|
26
26
|
OR: pass --here to skip the sibling spawn and use the current directory
|
|
27
|
-
`),1;e.runSync("git",["fetch","origin"],
|
|
28
|
-
`);const
|
|
29
|
-
`),1;const
|
|
30
|
-
`),1;e.stderr(`spawned sibling worktree at ${
|
|
31
|
-
`),e.chdir(
|
|
32
|
-
`),
|
|
33
|
-
`):e.stderr(`manual cleanup needed: \`git worktree remove --force ${
|
|
34
|
-
`)}return 1}e.setTerminalTitle(c.drone_label,s.name);const
|
|
35
|
-
`)}if(!
|
|
36
|
-
`),e.stderr(
|
|
37
|
-
`)}await e.probeMcpReady()||e.stderr(`warning: borg-mcp readiness probe did not complete within the timeout; launching ${
|
|
38
|
-
`);const
|
|
39
|
-
`),
|
|
40
|
-
Agent exited. You were working in ${
|
|
27
|
+
`),1;e.runSync("git",["fetch","origin"],n);let i="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],n).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],n).status===0&&(i="origin/master");const m=t.stdout.trim(),y=e.runSync("git",["rev-parse",i],n).stdout.trim();m!==y&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${i} (${y.slice(0,7)}); new worktree will start on ${i}
|
|
28
|
+
`);const $=C(n),p=r.flags.worktree??ie(k.name);if(p.length===0)return e.stderr(`cannot derive a worktree name from role "${k.name}"; pass an explicit --worktree <name>
|
|
29
|
+
`),1;const _=e.homedir();let f=q(_,$,p),v=O(C(f),$),Y=2;for(;e.pathExists(f)||Ee(e,n,f)||z(e.runSync,n,v)&&!ye(e.runSync,n,v,i);)f=q(_,$,p,Y),v=O(C(f),$),Y++;e.mkdirp(ne(f));const G=z(e.runSync,n,v)?e.runSync("git",["worktree","add",f,v],n):e.runSync("git",["worktree","add","-b",v,f,i],n);if(G.status!==0)return e.stderr(`git worktree add failed: ${J(G.stderr)}
|
|
30
|
+
`),1;e.stderr(`spawned sibling worktree at ${f} on branch ${v} (${i}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
|
|
31
|
+
`),e.chdir(f),e.stderr(Ce(f,v,n)),g=e.cwd()}try{await e.setActiveCube({cubeId:c.cube_id,droneId:c.drone_id,name:s.name,sessionToken:c.session_token,droneLabel:c.drone_label,apiUrl:a.apiUrl,roleName:k.name,isHumanSeat:k.is_human_seat,...k.role_class?{roleClass:k.role_class}:{}})}catch(t){const i=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${i}
|
|
32
|
+
`),g){const u=e.runSync("git",["worktree","remove","--force",g],n);u.status===0?e.stderr(`rolled back spawned worktree at ${g}
|
|
33
|
+
`):e.stderr(`manual cleanup needed: \`git worktree remove --force ${g}\` (rollback attempt failed: ${J(u.stderr).trim()||"unknown"})
|
|
34
|
+
`)}return 1}e.setTerminalTitle(c.drone_label,s.name);const X=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(me(k.name,s.name,X));const w=e.cwd();try{e.installProjectSessionHook(w)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${w}; it will be re-attempted on the next borg launch
|
|
35
|
+
`)}if(!g){e.runSync("git",["fetch","origin","--prune"],w);const t=O(C(w),C(n)),i=ke(e.runSync,w,t,"origin/main");i.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
|
|
36
|
+
`),e.stderr(Re(w,t))):i.message&&e.stderr(`worktree sync: ${i.message}
|
|
37
|
+
`)}await e.probeMcpReady()||e.stderr(`warning: borg-mcp readiness probe did not complete within the timeout; launching ${h} anyway \u2014 the kickoff prompt's ToolSearch fallback will recover if the MCP server takes longer to start.
|
|
38
|
+
`);const Z=e.getInboxPath(c.cube_id,c.drone_id),U=h==="codex"?`borg-wake-${oe()}`:null,ee=ge(h==="codex"?"codex":"claude",h==="claude"?Z:null);let D,M=[],E,L=null,j=null;const B=e.findProjectRoot(w);b?await e.setLaunchModel(c.cube_id,B,{model:b,ollamaBaseUrl:Se(b).kind==="ollama"?I:null}):await e.clearLaunchModel(c.cube_id,B);const F=pe(b,I),W={...process.env,...F.set,BORG_SESSION:"1"};for(const t of F.unset)delete W[t];if(h==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
|
|
39
|
+
`),D="\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."):D="Codex wake-path capability check passed: remote-control socket established for this session.",M=t.args,Object.keys(t.env).length>0&&Object.assign(W,t.env),L=be(t.args),j=t.server?.cleanup??null}E=[he({cli:h,codexWakeNonce:U,monitorClause:ee,codexWakePathClause:D})],h==="codex"&&(E=[...$e(),...M,...de(E,w)]);const te=e.exec(h,E,w,W);h==="codex"&&L&&U&&we({deps:e,cubeId:c.cube_id,droneId:c.drone_id,socketPath:L,cwd:w,previewNeedle:U,launchedAtSeconds:Math.floor(Date.now()/1e3)});const re=await te;if(j)try{j()}catch{}return g&&R!==g&&e.stderr(`
|
|
40
|
+
Agent exited. You were working in ${g}; your shell is back in ${R}.
|
|
41
41
|
To return:
|
|
42
|
-
cd ${
|
|
43
|
-
`),
|
|
44
|
-
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 ${
|
|
45
|
-
`}function
|
|
42
|
+
cd ${fe(g)}
|
|
43
|
+
`),re}function Ce(r,e,a){return`
|
|
44
|
+
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 ${a}\` or operate on the primary checkout ${a}: 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).
|
|
45
|
+
`}function Re(r,e){return`
|
|
46
46
|
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.
|
|
47
|
-
`}function
|
|
48
|
-
`).some(
|
|
47
|
+
`}function J(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function Ee(r,e,a){const n=r.runSync("git",["worktree","list","--porcelain"],e);return n.status!==0?!1:n.stdout.split(`
|
|
48
|
+
`).some(o=>o===`worktree ${a}`)}function _e(r,e){if(e.length===0)return null;const a=r.toLowerCase();let n=null;for(const o of e){const l=Ae(a,o.toLowerCase());l<=2&&(n===null||l<n.distance)&&(n={name:o,distance:l})}return n?n.name:null}function Ae(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const a=new Array(e.length+1),n=new Array(e.length+1);for(let o=0;o<=e.length;o++)a[o]=o;for(let o=1;o<=r.length;o++){n[0]=o;for(let l=1;l<=e.length;l++){const R=r[o-1]===e[l-1]?0:1;n[l]=Math.min(n[l-1]+1,a[l]+1,a[l-1]+R)}for(let l=0;l<=e.length;l++)a[l]=n[l]}return a[e.length]}export{Ke as runAssimilate,J as safeStderr,_e as suggestRoleName};
|
package/dist/cleanup-cmd.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import{spawnSync as
|
|
2
|
-
`)){if(!s.startsWith("!!"))continue;const
|
|
3
|
-
`))
|
|
4
|
-
`)),1;const
|
|
5
|
-
`)),0;
|
|
6
|
-
`));for(const o of
|
|
7
|
-
`)}if(
|
|
8
|
-
`)),!n.prune)return
|
|
9
|
-
`)),0;let
|
|
10
|
-
`));continue}if(o.wtBranch){const
|
|
11
|
-
`)),
|
|
12
|
-
`))}
|
|
13
|
-
`))
|
|
1
|
+
import{spawnSync as I}from"node:child_process";import{realpathSync as W}from"node:fs";import{sep as R,basename as v,dirname as A}from"node:path";import{homedir as D}from"node:os";import c from"chalk";import{classifyDirty as L,isMerged as B,localBranchExists as x,perWorktreeBranchName as _,worktreesHome as C}from"./worktree-lifecycle.js";import{readAllProjectIdentities as F}from"./cubes.js";import{defaultProbeSeat as O}from"./seat-probe.js";const S="origin/main",j=new Set(["node_modules","dist","build",".next","coverage",".wrangler",".playwright-mcp",".claude"]),G=new Set([".DS_Store","worker-configuration.d.ts"]),H=[".log",".tsbuildinfo",".tmp"];function M(r){const n=r.replace(/\/+$/,""),e=n.split("/").filter(s=>s.length>0);if(e.some(s=>j.has(s)))return!0;const t=e[e.length-1]??n;return!!(G.has(t)||H.some(s=>t.endsWith(s)))}function T(r,n){const e=r("git",["status","--porcelain","--ignored"],n);if(e.status!==0)return["<git status --ignored failed \u2014 cannot verify clean>"];const t=[];for(const s of e.stdout.split(`
|
|
2
|
+
`)){if(!s.startsWith("!!"))continue;const a=s.slice(3).trim();a.length!==0&&(M(a)||t.push(a))}return t}const z={runSync:(r,n,e)=>{const t=I(r,n,{cwd:e,encoding:"utf-8"});return{status:t.status,stdout:t.stdout??"",stderr:t.stderr??""}},homeDir:()=>D(),cwd:()=>process.cwd(),listSeats:()=>F(),probeSeat:O,realpath:r=>W(r),stdout:r=>process.stdout.write(r),stderr:r=>process.stderr.write(r)};function K(r,n,e){let t,s;try{t=r(n),s=r(e)}catch{return!1}if(s===t)return!1;const a=t.endsWith(R)?t:t+R;return s.startsWith(a)}function oe(r){return N(r).map(n=>n.path)}function N(r){const n=[];let e=null;for(const t of r.split(`
|
|
3
|
+
`))t.startsWith("worktree ")?(e&&n.push(e),e={path:t.slice(9).trim(),branch:null}):t.startsWith("branch ")&&e&&(e.branch=t.slice(7).trim().replace(/^refs\/heads\//,""));return e&&n.push(e),n}async function X(r,n,e,t){const{runSync:s}=r,a=L(s,n),u=a.staged.length+a.unstaged.length+a.untracked.length;if(u>0)return{reason:"SURVIVES-dirty",detail:`${u} uncommitted file(s)`};const d=T(s,n);if(d.length>0)return{reason:"SURVIVES-clobber",detail:`gitignored local state: ${d.slice(0,3).join(", ")}${d.length>3?` (+${d.length-3})`:""}`};if(e===null)return{reason:"SURVIVES-detached",detail:"detached HEAD \u2014 cannot verify merged"};if(!B(s,n,e,S))return{reason:"SURVIVES-unmerged",detail:`${e} not merged into ${S}`};switch(await r.probeSeat(t.sessionToken,t.apiUrl)){case"evicted":return{reason:"PRUNABLE",detail:"410 DRONE_EVICTED (clean + merged)"};case"frozen":return{reason:"SURVIVES-frozen",detail:"423 DRONE_FROZEN (reversible)"};case"live":return{reason:"SURVIVES-live",detail:"seat resolves (drone alive)"};default:return{reason:"UNKNOWN-indeterminate",detail:"probe returned 401/network/transient (or gh#877 not yet deployed) \u2014 not deleting"}}}async function Y(r){const{runSync:n,realpath:e}=r,t=r.cwd();if(n("git",["rev-parse","--show-toplevel"],t).status!==0)return{rows:[],error:`not in a git repository (cwd: ${t})`};const s=n("git",["fetch","origin","--prune"],t);if(s.status!==0)return{rows:[],error:`git fetch origin failed: ${s.stderr.trim()}`};const a=n("git",["worktree","list","--porcelain"],t);if(a.status!==0)return{rows:[],error:`git worktree list failed: ${a.stderr.trim()}`};const u=N(a.stdout),d=new Map(u.map(i=>[i.path,i.branch])),h=u.map(i=>i.path),E=h[0],b=r.homeDir(),g=C(b),o=k(e,t),p=E?k(e,E):null,y=await r.listSeats(),f=new Map;for(const{projectPath:i,cube:w}of y){const m=k(e,i);m&&f.set(m,w)}const l=[];for(const i of h){const w=k(e,i);if(!w||p&&w===p)continue;const m=f.get(w),U=K(e,g,i);if(w===o){l.push({worktreePath:i,wtBranch:null,reason:"SURVIVES-self",detail:"current worktree"});continue}if(!U){m&&l.push({worktreePath:i,wtBranch:null,reason:"LEGACY-manual-review",detail:"borg seat outside worktreesHome (pre-gh#556 sibling)"});continue}if(!m){l.push({worktreePath:i,wtBranch:null,reason:"UNKNOWN-no-seat",detail:"no cubes.json seat \u2014 manual review"});continue}const $=d.get(i)??null,{reason:P,detail:V}=await X(r,i,$,m);l.push({worktreePath:i,wtBranch:$,reason:P,detail:V})}return{rows:l}}function k(r,n){try{return r(n)}catch{return null}}async function se(r={},n={prune:!1}){const e={...z,...r},{stdout:t,stderr:s,runSync:a}=e,{rows:u,error:d}=await Y(e);if(d)return s(c.red(`\u25FC borg cleanup: ${d}
|
|
4
|
+
`)),1;const h=u.filter(o=>o.reason==="PRUNABLE"),E=u.filter(o=>o.reason!=="PRUNABLE");if(u.length===0)return t(c.blue(`\u25FC borg cleanup: no borg-managed worktrees found.
|
|
5
|
+
`)),0;t(c.bold(`\u25FC borg cleanup report:
|
|
6
|
+
`));for(const o of u){const p=o.reason==="PRUNABLE"?c.yellow(o.reason):c.gray(o.reason);t(` ${p} ${o.worktreePath}${o.detail?c.gray(` \u2014 ${o.detail}`):""}
|
|
7
|
+
`)}if(t(c.gray(`\u25FC ${h.length} prunable, ${E.length} kept.
|
|
8
|
+
`)),!n.prune)return h.length>0&&t(c.gray("\u25FC Dry-run \u2014 nothing deleted. Re-run with `--prune` to remove the PRUNABLE worktree(s).\n")),0;if(h.length===0)return t(c.blue(`\u25FC Nothing to prune.
|
|
9
|
+
`)),0;let b=0,g=0;for(const o of h){const p=a("git",["worktree","remove",o.worktreePath],e.cwd());if(p.status!==0){o.prune="remove-failed",g++,s(c.red(` \u2717 worktree remove ${o.worktreePath}: ${p.stderr.trim()}
|
|
10
|
+
`));continue}if(o.wtBranch){const l=a("git",["branch","-d",o.wtBranch],e.cwd());if(l.status!==0){o.prune="branch-delete-failed",s(c.yellow(` \u26A0 removed worktree but \`git branch -d ${o.wtBranch}\` refused: ${l.stderr.trim()}
|
|
11
|
+
`)),b++;continue}}let y="";const f=_(v(o.worktreePath),v(A(o.worktreePath)));if(f!==o.wtBranch&&x(a,e.cwd(),f)&&B(a,e.cwd(),f,S)){const l=a("git",["branch","-d",f],e.cwd());l.status===0?y=` + base ${f}`:s(c.yellow(` \u26A0 left dangling base branch ${f}: \`git branch -d\` refused: ${l.stderr.trim()}
|
|
12
|
+
`))}o.prune="removed",b++,t(c.blue(` \u2713 pruned ${o.worktreePath}${o.wtBranch?` + branch ${o.wtBranch}`:""}${y}
|
|
13
|
+
`))}return t(c.gray(`\u25FC Pruned ${b} worktree(s)${g>0?`, ${g} failed`:""}.
|
|
14
|
+
`)),g>0?1:0}function ae(r){let n=!1;for(const e of r)if(e==="--prune")n=!0;else return{ok:!1,error:`unexpected argument: ${e}. Usage: borg cleanup [--prune]`};return{ok:!0,options:{prune:n}}}export{Y as buildCleanupReport,T as clobberClassIgnored,M as isRegenerableIgnored,K as isStrictlyUnder,ae as parseCleanupArgs,N as parseWorktreeEntries,oe as parseWorktreeList,se as runCleanup};
|
package/dist/config-utils.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export declare function isProjectSessionStartHookRegistered(projectRoot: string)
|
|
|
30
30
|
* mutating settings. Returns false on any read error (safe-default).
|
|
31
31
|
*/
|
|
32
32
|
export declare function isSessionStartHookRegistered(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Peek: true iff the Claude UserPromptSubmit audit hook (`borg-log-audit`) is
|
|
35
|
+
* registered. Non-mutating mirror of addUserPromptSubmitHook's idempotency
|
|
36
|
+
* check; used by isClaudeHookConfigPending (gh#844).
|
|
37
|
+
*/
|
|
38
|
+
export declare function isUserPromptSubmitHookRegistered(): boolean;
|
|
33
39
|
/**
|
|
34
40
|
* Inverse of addSessionStartHook: remove any SessionStart hook entry whose
|
|
35
41
|
* inner hooks array contains a `borg-regen` command. If multiple commands
|
|
@@ -92,4 +98,15 @@ export declare function addCodexMcpServer(): void;
|
|
|
92
98
|
export declare function addCodexSessionStartHook(): boolean;
|
|
93
99
|
export declare function addCodexUserPromptSubmitHook(): boolean;
|
|
94
100
|
export declare function isCodexHookRegistered(eventName: 'SessionStart' | 'UserPromptSubmit' | 'Stop', command: string, hooksPath?: string): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Peek: true iff the Codex SessionStart orientation hook (`borg-regen`) is
|
|
103
|
+
* registered. Non-mutating mirror of addCodexSessionStartHook; used to gate
|
|
104
|
+
* that writer + the gh#844 disclosure on whether it would actually mutate.
|
|
105
|
+
*/
|
|
106
|
+
export declare function isCodexSessionStartHookRegistered(hooksPath?: string): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Peek: true iff the Codex UserPromptSubmit audit hook (`borg-log-audit`) is
|
|
109
|
+
* registered. Non-mutating mirror of addCodexUserPromptSubmitHook.
|
|
110
|
+
*/
|
|
111
|
+
export declare function isCodexUserPromptSubmitHookRegistered(hooksPath?: string): boolean;
|
|
95
112
|
//# sourceMappingURL=config-utils.d.ts.map
|
package/dist/config-utils.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{execSync as
|
|
2
|
-
`,"utf-8")}function
|
|
3
|
-
`,"utf-8")}function
|
|
1
|
+
import{execSync as l}from"child_process";import n from"fs";import h from"os";import i from"path";import{fileURLToPath as b}from"url";import{dirname as O}from"path";const R=b(import.meta.url),U=O(R),a="borg-regen",m="borg-log-audit",v=i.join(h.homedir(),".claude.json"),E=i.join(h.homedir(),".codex","config.toml"),u=i.join(h.homedir(),".codex","hooks.json"),H="borg";function p(){return i.join(h.homedir(),".claude","settings.json")}function d(){const r=p();if(!n.existsSync(r))return{};const e=n.readFileSync(r,"utf-8");return e.trim()?JSON.parse(e):{}}function y(r){const e=p();n.mkdirSync(i.dirname(e),{recursive:!0}),n.writeFileSync(e,JSON.stringify(r,null,2)+`
|
|
2
|
+
`,"utf-8")}function S(r){if(!n.existsSync(r))return{};const e=n.readFileSync(r,"utf-8");return e.trim()?JSON.parse(e):{}}function A(r,e){n.mkdirSync(i.dirname(r),{recursive:!0}),n.writeFileSync(r,JSON.stringify(e,null,2)+`
|
|
3
|
+
`,"utf-8")}function L(){return P(p())}function D(r){return P(x(r))}function N(r){return j(x(r))}function x(r){return i.join(r,".claude","settings.local.json")}function P(r){let e;try{e=S(r)}catch(t){return console.error(`\u26A0 Could not parse ${r}: ${t.message}. Skipping hook registration; you can add it manually.`),!1}return e.hooks??={},e.hooks.SessionStart??=[],e.hooks.SessionStart.some(t=>Array.isArray(t?.hooks)&&t.hooks.some(s=>s?.type==="command"&&s?.command===a))?!1:(e.hooks.SessionStart.push({matcher:"*",hooks:[{type:"command",command:a}]}),A(r,e),!0)}function j(r){let e;try{e=S(r)}catch{return!1}const o=e?.hooks?.SessionStart;return Array.isArray(o)?o.some(t=>Array.isArray(t?.hooks)&&t.hooks.some(s=>s?.type==="command"&&s?.command===a)):!1}function T(){let r;try{r=d()}catch{return!1}const e=r?.hooks?.SessionStart;return Array.isArray(e)?e.some(o=>Array.isArray(o?.hooks)&&o.hooks.some(t=>t?.type==="command"&&t?.command===a)):!1}function J(){let r;try{r=d()}catch{return!1}const e=r?.hooks?.UserPromptSubmit;return Array.isArray(e)?e.some(o=>Array.isArray(o?.hooks)&&o.hooks.some(t=>t?.type==="command"&&t?.command===m)):!1}function K(){let r;try{r=d()}catch{return!1}if(!r?.hooks?.SessionStart)return!1;let e=!1;return r.hooks.SessionStart=r.hooks.SessionStart.map(o=>{if(!Array.isArray(o?.hooks))return o;const t=o.hooks.filter(s=>!(s?.type==="command"&&s?.command===a));return t.length!==o.hooks.length?(e=!0,{...o,hooks:t}):o}).filter(o=>Array.isArray(o?.hooks)&&o.hooks.length>0),r.hooks.SessionStart.length===0&&delete r.hooks.SessionStart,Object.keys(r.hooks).length===0&&delete r.hooks,e&&y(r),e}function X(){let r;try{r=d()}catch(o){return console.error(`\u26A0 Could not parse ${p()}: ${o.message}. Skipping audit hook registration.`),!1}return r.hooks??={},r.hooks.UserPromptSubmit??=[],r.hooks.UserPromptSubmit.some(o=>Array.isArray(o?.hooks)&&o.hooks.some(t=>t?.type==="command"&&t?.command===m))?!1:(r.hooks.UserPromptSubmit.push({matcher:"*",hooks:[{type:"command",command:m}]}),y(r),!0)}function W(){let r;try{r=d()}catch{return!1}if(!r?.hooks?.UserPromptSubmit)return!1;let e=!1;return r.hooks.UserPromptSubmit=r.hooks.UserPromptSubmit.map(o=>{if(!Array.isArray(o?.hooks))return o;const t=o.hooks.filter(s=>!(s?.type==="command"&&s?.command===m));return t.length!==o.hooks.length?(e=!0,{...o,hooks:t}):o}).filter(o=>Array.isArray(o?.hooks)&&o.hooks.length>0),r.hooks.UserPromptSubmit.length===0&&delete r.hooks.UserPromptSubmit,Object.keys(r.hooks).length===0&&delete r.hooks,e&&y(r),e}function Q(r=v){try{if(!n.existsSync(r))return!1;const e=n.readFileSync(r,"utf-8");if(!e.trim())return!1;const o=JSON.parse(e);if(!o||typeof o!="object")return!1;const t=o.mcpServers;return!t||typeof t!="object"||Array.isArray(t)?!1:H in t}catch{return!1}}function V(r=E){try{if(!n.existsSync(r))return!1;const e=n.readFileSync(r,"utf-8");return/^\s*\[mcp_servers\.borg\]\s*$/m.test(e)&&/^\s*BORG_CODEX_REMOTE_WAKE\s*=\s*"1"\s*$/m.test(e)}catch{return!1}}function q(){return i.join(U,"index.js")}function z(){try{try{l("claude mcp remove --scope user borg",{stdio:"ignore"})}catch{}l("claude mcp add --scope user borg borg-mcp",{stdio:"inherit",env:{...process.env,BORG_API_URL:process.env.BORG_API_URL||"https://api.borgmcp.ai"}})}catch(r){throw r.message?.includes("command not found")?new Error("Claude CLI not found. Please install Claude Code first."):new Error(`Failed to add MCP server: ${r.message}`)}}function Y(){try{try{l("codex mcp remove borg",{stdio:"ignore"})}catch{}l("codex mcp add borg --env BORG_API_URL="+M(process.env.BORG_API_URL||"https://api.borgmcp.ai")+" --env BORG_CODEX_REMOTE_WAKE=1 -- borg-mcp",{stdio:"inherit",env:{...process.env,BORG_API_URL:process.env.BORG_API_URL||"https://api.borgmcp.ai",BORG_CODEX_REMOTE_WAKE:"1"}})}catch(r){throw r.message?.includes("command not found")?new Error("Codex CLI not found. Please install Codex first."):new Error(`Failed to add MCP server to Codex: ${r.message}`)}}function M(r){return`'${r.replace(/'/g,"'\\''")}'`}function _(r,e,o={}){let t;try{t=S(u)}catch(f){return console.error(`\u26A0 Could not parse ${u}: ${f.message}. Skipping Codex hook registration.`),!1}t.hooks??={},t.hooks[r]??=[];const s=t.hooks[r];if(!Array.isArray(s)||s.some(f=>Array.isArray(f?.hooks)&&f.hooks.some(g=>g?.type==="command"&&g?.command===e)))return!1;const c={hooks:[{type:"command",command:e}]};return o.matcher&&(c.matcher=o.matcher),typeof o.timeout=="number"&&(c.hooks[0].timeout=o.timeout),s.push(c),A(u,t),!0}function Z(){return _("SessionStart",a,{matcher:"startup|resume",timeout:30})}function rr(){return _("UserPromptSubmit",m,{timeout:10})}function C(r,e,o=u){try{const s=S(o)?.hooks?.[r];return Array.isArray(s)?s.some(k=>Array.isArray(k?.hooks)&&k.hooks.some(c=>c?.type==="command"&&c?.command===e)):!1}catch{return!1}}function er(r=u){return C("SessionStart",a,r)}function or(r=u){return C("UserPromptSubmit",m,r)}export{Y as addCodexMcpServer,Z as addCodexSessionStartHook,rr as addCodexUserPromptSubmitHook,z as addMcpServer,D as addProjectSessionStartHook,L as addSessionStartHook,X as addUserPromptSubmitHook,q as getBinaryPath,C as isCodexHookRegistered,V as isCodexMcpServerConfigured,er as isCodexSessionStartHookRegistered,or as isCodexUserPromptSubmitHookRegistered,Q as isMcpServerConfigured,N as isProjectSessionStartHookRegistered,T as isSessionStartHookRegistered,J as isUserPromptSubmitHookRegistered,K as removeSessionStartHook,W as removeUserPromptSubmitHook};
|
package/dist/cubes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{existsSync as
|
|
2
|
-
`)}let J=0;async function
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`)}async function ft(t,e,r,n=
|
|
1
|
+
import{existsSync as k}from"node:fs";import{mkdir as A,readFile as f,writeFile as _,unlink as w,rename as U}from"node:fs/promises";import{homedir as W}from"node:os";import{dirname as F,join as c,resolve as N}from"node:path";import{pruneDeadWakeTargets as v}from"./codex-wake-resolve.js";import{MODEL_DESCRIPTOR_REGEX as R}from"./model-presets.js";const l=c(W(),".config","borgmcp"),g=c(l,"cubes.json"),O=c(l,"launch.json"),L=c(l,"codex-wake-targets.json"),m=c(l,"launch-models.json"),D=c(l,"inboxes");function s(t=process.cwd()){let e=N(t);for(;;){if(k(c(e,".git")))return e;const r=F(e);if(r===e)return N(t);e=r}}const a=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function Z(t,e){if(!a.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!a.test(e))throw new Error(`Invalid droneId: ${e}`);return c(D,t,`${e}.log`)}function B(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function p(){let t;try{t=await f(g,"utf8")}catch(r){if(r?.code==="ENOENT")return null;throw r}let e;try{e=JSON.parse(t)}catch{return null}return B(e)?e:null}async function I(t){await d(g,JSON.stringify(t,null,2)+`
|
|
2
|
+
`)}let J=0;async function d(t,e,r={}){const n=r.io??{writeFile:_,rename:U,unlink:w},o=r.mode??384;await A(F(t),{recursive:!0});const i=`${t}.${process.pid}.${J++}.tmp`;try{await n.writeFile(i,e,{mode:o}),await n.rename(i,t)}catch(u){try{await n.unlink(i)}catch{}throw u}}function M(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function h(){let t;try{t=await f(O,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return M(e)?e:null}catch{return null}}async function P(t){await d(O,JSON.stringify(t,null,2)+`
|
|
3
|
+
`)}function $(t,e){if(!a.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!a.test(e))throw new Error(`Invalid droneId: ${e}`);return`${t}:${e}`}function K(t){return t!==null&&typeof t=="object"&&typeof t.targets=="object"&&t.targets!==null&&!Array.isArray(t.targets)}async function j(){let t;try{t=await f(L,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return K(e)?e:null}catch{return null}}async function S(t){await d(L,JSON.stringify(t,null,2)+`
|
|
4
|
+
`)}async function tt(){const t=await p();if(!t)return null;const e=s(),r=t.projects[e];return!r||typeof r.cubeId!="string"||!r.cubeId||typeof r.droneId!="string"||!r.droneId?null:r}async function et(t){const e=await p()??{projects:{}};e.projects[s()]=t,await I(e)}function rt(t,e){const r=e.cube?.name??t.name,n=e.drone?.label??t.droneLabel;return r===t.name&&n===t.droneLabel?t:{...t,name:r,droneLabel:n}}async function nt(){const t=await p();if(!t)return;const e=s();if(e in t.projects){if(delete t.projects[e],Object.keys(t.projects).length===0){try{await w(g)}catch(r){if(r?.code!=="ENOENT")throw r}return}await I(t)}}async function ot(){const t=await h();if(!t)return null;const e=t.projects[s()];return e?.cli==="claude"||e?.cli==="codex"?e.cli:null}async function it(t){const e=await h();if(!e)return null;const r=e.projects[s(t)];return r?.cli==="claude"||r?.cli==="codex"?r.cli:null}async function ct(){const t=await p();return t?Object.entries(t.projects).filter(([,e])=>e!==null&&typeof e=="object"&&typeof e.cubeId=="string"&&e.cubeId.length>0&&typeof e.droneId=="string"&&e.droneId.length>0).map(([e,r])=>({projectPath:e,cube:r})):[]}async function at(t){const e=await h()??{projects:{}};e.projects[s()]={cli:t},await P(e)}async function st(t,e,r){const n=await j()??{targets:{}};n.targets[$(t,e)]={...r,updatedAt:new Date().toISOString()},await S(n)}async function lt(t,e){const r=await j();if(!r)return null;const n=r.targets[$(t,e)];return!n||typeof n.threadId!="string"||typeof n.socketPath!="string"?null:n}async function ut(t){const e=await j();if(!e)return;const{targets:r,changed:n}=v(e.targets,t);n&&await S({...e,targets:r})}function x(t,e){if(!a.test(t))throw new Error(`Invalid cubeId: ${t}`);if(typeof e!="string"||e.length===0)throw new Error(`Invalid worktree path: ${e}`);return`${t}:${e}`}function X(t,e){if(!a.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!a.test(e))throw new Error(`Invalid droneId: ${e}`);return`${t}:${e}`}function T(t){return!t||typeof t.model!="string"||!R.test(t.model)?null:{model:t.model,ollamaBaseUrl:typeof t.ollamaBaseUrl=="string"?t.ollamaBaseUrl:null}}function G(t){return t!==null&&typeof t=="object"&&typeof t.models=="object"&&t.models!==null&&!Array.isArray(t.models)}async function E(t){let e;try{e=await f(t,"utf8")}catch(r){if(r?.code==="ENOENT")return null;throw r}try{const r=JSON.parse(e);return G(r)?r:null}catch{return null}}async function b(t,e){await d(t,JSON.stringify(e,null,2)+`
|
|
5
|
+
`)}async function ft(t,e,r,n=m){const o=await E(n)??{models:{}};o.models[x(t,e)]={model:r.model,ollamaBaseUrl:r.ollamaBaseUrl},await b(n,o)}async function pt(t,e,r=null,n=m){const o=await E(n);if(!o)return null;const i=x(t,e),u=T(o.models[i]);if(u)return u;if(!r||!a.test(r))return null;const C=X(t,r),y=T(o.models[C]);return y?(o.models[i]=y,delete o.models[C],await b(n,o),y):null}async function dt(t,e,r=m){const n=await E(r);if(!n)return;const o=x(t,e);if(o in n.models){if(delete n.models[o],Object.keys(n.models).length===0){try{await w(r)}catch(i){if(i?.code!=="ENOENT")throw i}return}await b(r,n)}}export{rt as activeCubeWithFreshRegenIdentity,d as atomicWriteFile,nt as clearActiveCube,dt as clearLaunchModel,s as findProjectRoot,tt as getActiveCube,lt as getCodexWakeTarget,pt as getLaunchModel,ot as getProjectCliPreference,it as getProjectCliPreferenceForPath,Z as inboxPathForDrone,ut as pruneDeadCodexWakeTargets,ct as readAllProjectIdentities,et as setActiveCube,st as setCodexWakeTarget,ft as setLaunchModel,at as setProjectCliPreference};
|
package/dist/launch-all-cmd.js
CHANGED
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
import{discoverDroneCandidates as
|
|
1
|
+
import{discoverDroneCandidates as A}from"./launch-all-discovery.js";import{resolveBorgPath as x}from"./launch-all-command.js";import{sweepStaleLocks as I,isLockLive as S}from"./launch-all-locks.js";import{runTmuxBackend as D}from"./backends/launch-all-tmux.js";import{runWindowsBackend as T}from"./backends/launch-all-windows.js";import{runPastelistBackend as N}from"./backends/launch-all-pastelist.js";const L=`borg launch-all: tmux not found.
|
|
2
2
|
macOS: brew install tmux
|
|
3
3
|
Debian: sudo apt install tmux
|
|
4
4
|
Fedora: sudo dnf install tmux
|
|
5
|
-
`;function p(
|
|
6
|
-
`)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`),e.
|
|
10
|
-
`)
|
|
11
|
-
`),
|
|
12
|
-
`),
|
|
13
|
-
`))
|
|
14
|
-
`)
|
|
15
|
-
`);
|
|
16
|
-
`),
|
|
17
|
-
`)
|
|
18
|
-
`);
|
|
19
|
-
`);continue}
|
|
20
|
-
`),
|
|
21
|
-
`),0;
|
|
22
|
-
`);
|
|
23
|
-
`);
|
|
24
|
-
`),
|
|
25
|
-
`),
|
|
26
|
-
`)
|
|
5
|
+
`;function p(t){try{return t.runSync("tmux",["-V"]),!0}catch{return!1}}function _(t){try{return/microsoft|wsl/i.test(t.runSync("uname",["-r"]))}catch{return!1}}function C(t){return/^drone-\d+$/i.test(t)||t.toLowerCase()==="drone"}async function M(t,e){if(t.cubeName!==void 0){const a=(await e.readAllProjectIdentities()).filter(l=>l.cube.name===t.cubeName);if(a.length===0)return{error:`no cube named '${t.cubeName}' found in cubes.json \u2014 has any drone assimilated into it?`};if(a.length>1){const l=a.map(m=>` ${m.cube.cubeId} (seat in ${m.projectPath})`).join(`
|
|
6
|
+
`);return{error:`'${t.cubeName}' is ambiguous \u2014 ${a.length} cubes in cubes.json share that name:
|
|
7
|
+
${l}
|
|
8
|
+
cd into the intended project and re-run without --cube-name (resolves the active cube), or remove the stale seat(s) from cubes.json.`}}return{cubeId:a[0].cube.cubeId,name:t.cubeName}}const r=await e.getActiveCube();return r?{cubeId:r.cubeId,name:r.name}:{error:"no active cube in this directory; run `borg assimilate` first, or pass a cube name explicitly"}}function E(t,e){const r=t.flags.mode;if(e.platform()==="win32"&&!_(e))return e.stderr(`native Windows is not supported for interactive launch; using pastelist mode instead (WSL + tmux is the recommended Windows path)
|
|
9
|
+
`),{backend:"pastelist"};if(r==="windows")return{backend:"windows"};if(r==="pastelist")return{backend:"pastelist"};const a=p(e);return r==="tmux"?a?{backend:"tmux"}:{hardFail:L}:a?{backend:"tmux"}:(e.stderr(L+`Falling back to pastelist mode (paste the commands below).
|
|
10
|
+
`),{backend:"pastelist"})}function U(t,e){return t.flags.noAttach||!e.isTTY()?"none":e.getEnv("TMUX")?"switch":"attach"}function j(t,e){e.stdout(` tmux attach -t ${t} # re-attach later
|
|
11
|
+
`),e.stdout(` tmux list-windows -t ${t} # list all drone windows
|
|
12
|
+
`),e.stdout(` tmux kill-session -t ${t} # stop all drones
|
|
13
|
+
`)}function O(t){return`borg-${t.replace(/[^a-zA-Z0-9_-]/g,"-")}`}async function P(t,e,r,u,a,l=Date.now,m=d=>new Promise(h=>setTimeout(h,d))){const d=new Map;for(const f of a)d.set(f,"unconfirmed");const h=l()+1e4;for(let f=0;f<20&&!(l()>=h);f++){let c;try{c=await t.getRoster(e,r,u)}catch(i){const b=i instanceof Error?i.message:String(i),o=/Authentication required/i.test(b);t.stderr(`roster confirmation skipped (${o?"token rotated mid-launch":b}); launched drones may still be live \u2014 re-check with \`borg_roster\`.
|
|
14
|
+
`);break}for(const i of c.drones)d.get(i.id)==="unconfirmed"&&i.seen_since===!0&&d.set(i.id,"verified");if([...d.values()].every(i=>i==="verified"))break;await m(500)}return d}const F=2e3;function W(t,e){if(t!==void 0&&Number.isInteger(t)&&t>=0)return t;const r=e===void 0?"":e.trim(),u=r===""?NaN:Number(r);return Number.isInteger(u)&&u>=0?u:F}async function X(t,e,r={}){const u=r.now??Date.now,a=r.nowISO??(()=>new Date().toISOString()),l=r.borgPath??x(),m=r.sleep??(n=>new Promise(s=>setTimeout(s,n))),d=W(t.flags.launchDelayMs,e.getEnv("BORG_LAUNCH_DELAY_MS")),h=await M(t,e);if("error"in h)return e.stderr(`borg launch-all: ${h.error}
|
|
15
|
+
`),1;const{cubeId:f,name:c}=h;I(e,f,u());const i=await A({targetCubeId:f,only:t.flags.only},e);if(i.length===0)return t.flags.only!==void 0?(e.stdout(`No worktrees matched --only '${t.flags.only}' for cube '${c}'
|
|
16
|
+
`),C(t.flags.only)||e.stderr(`note: --only '${t.flags.only}' is matched best-effort by drone label; role-name matching needs a drone session and is not available here.
|
|
17
|
+
`)):e.stdout(`No worktrees found for cube '${c}' \u2014 have you run \`borg assimilate --worktree\` to create any drone seats?
|
|
18
|
+
`),0;const b=[];for(const n of i){const s=S(e,f,n.worktreeDir,u());if(s.live&&!t.flags.force){e.stderr(`skipping ${n.droneLabel} (${n.worktreeDir}): appears live. Use --force to re-launch.
|
|
19
|
+
`);continue}s.live&&t.flags.force&&e.stderr(`--force: re-launching ${n.droneLabel} (${n.worktreeDir}); the running session's token will be displaced.
|
|
20
|
+
`),b.push(n)}if(b.length===0)return e.stdout(`All ${i.length} drone(s) for cube '${c}' appear live; nothing to launch (use --force to re-launch).
|
|
21
|
+
`),0;const o=[];for(const n of b){let s;try{s=await e.probeSeat(n.sessionToken,n.apiUrl)}catch{s="indeterminate"}if(s==="evicted"){e.stderr(`skipping ${n.droneLabel} (${n.worktreeDir}): seat no longer in cube (evicted) \u2014 run \`borg cleanup --prune\` to remove the worktree, or \`borg assimilate\` to re-seat fresh.
|
|
22
|
+
`);continue}if(s==="frozen"){e.stderr(`skipping ${n.droneLabel} (${n.worktreeDir}): seat frozen (subscription downgrade) \u2014 paused, not relaunching; it resumes automatically when billing is restored.
|
|
23
|
+
`);continue}s==="indeterminate"&&e.stderr(`note: could not confirm ${n.droneLabel}'s seat is live (network/transient) \u2014 launching anyway.
|
|
24
|
+
`),o.push(n)}if(o.length===0)return e.stdout(`All ${b.length} discovered drone(s) for cube '${c}' have evicted/frozen seats; nothing to launch.
|
|
25
|
+
`),0;if(t.flags.dryRun){e.stdout(`borg launch-all (dry-run): would launch ${o.length} drone(s) for cube '${c}':
|
|
26
|
+
`);for(const n of o)e.stdout(` ${n.droneLabel} ${n.worktreeDir}
|
|
27
|
+
`);return 0}if(o.length>6&&!t.flags.yes&&(await e.prompt(`About to launch ${o.length} drones for cube '${c}'. Continue? [y/N]: `)).trim().toLowerCase()!=="y")return e.stdout(`Aborted.
|
|
28
|
+
`),0;const w=E(t,e);if("hardFail"in w)return e.stderr(w.hardFail),1;const g=O(c),k=a();try{if(w.backend==="tmux"){const n=U(t,e);await D(o,{sessionName:g,borgPath:l,attachMode:n,launchedAtISO:k,launchDelayMs:d,sleep:m},e),n==="none"&&(e.isTTY()||e.stderr(`Launching in detached mode \u2014 stdout is non-TTY. Attach manually with: tmux attach -t ${g}
|
|
29
|
+
`),j(g,e))}else if(w.backend==="windows")await T(o,{borgPath:l,platform:e.platform(),launchedAtISO:k,launchDelayMs:d,sleep:m},e);else return N(o,l,e),0}catch(n){return e.stderr(`borg launch-all: ${n instanceof Error?n.message:String(n)}
|
|
30
|
+
`),1}const v=o[0].sessionToken,y=o[0].apiUrl;let $=null;v&&y?$=await P(e,v,y,k,o.map(n=>n.droneId),r.now,r.sleep):e.stderr(`roster reconciliation skipped \u2014 no session token available
|
|
27
31
|
`),e.stdout(`
|
|
28
|
-
borg launch-all: launched ${o.length} drones for cube '${
|
|
32
|
+
borg launch-all: launched ${o.length} drones for cube '${c}'
|
|
29
33
|
|
|
30
|
-
`);for(const
|
|
34
|
+
`);for(const n of o){const s=$?$.get(n.droneId)==="verified"?"VERIFIED":"unconfirmed (may still be joining)":"launched";e.stdout(` ${n.droneLabel} ${n.worktreeDir} ${s}
|
|
31
35
|
`)}return e.stdout(`
|
|
32
36
|
Attach: tmux attach -t ${g}
|
|
33
|
-
`),0}export{
|
|
37
|
+
`),0}export{F as DEFAULT_LAUNCH_DELAY_MS,W as resolveLaunchDelayMs,X as runLaunchAll};
|
package/dist/log-stream.d.ts
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* MCP tool can probe without perturbing the stream (no second
|
|
24
24
|
* connection, no second auth — just an in-process state snapshot).
|
|
25
25
|
*/
|
|
26
|
-
import {
|
|
26
|
+
import { getActiveCube } from './cubes.js';
|
|
27
|
+
import { acquireStreamLease, type StreamOwnershipSnapshot } from './stream-owner.js';
|
|
27
28
|
export declare const INBOX_TAIL_LINES_CAP = 512;
|
|
28
29
|
export declare const INBOX_TAIL_TRIM_THRESHOLD_LINES: number;
|
|
29
30
|
export type RunLoopHealth = 'connected' | 'reconnecting' | 'silent-inert' | 'never-started';
|
|
@@ -71,12 +72,21 @@ export declare function ensureCodexHeartbeatStarted(start?: () => ReturnType<typ
|
|
|
71
72
|
/**
|
|
72
73
|
* gh#861 finding 3: tear down the codex heartbeat timer — the teardown seam for
|
|
73
74
|
* the periodic interval. Called when the active cube is cleared (nothing to inject
|
|
74
|
-
* into) or the tick detects a dead app-server socket.
|
|
75
|
-
*
|
|
75
|
+
* into) or the tick detects a dead app-server socket. Also clears any pending
|
|
76
|
+
* deferred re-arm (gh#866 item 3) so a cube-cleared teardown doesn't leave a
|
|
77
|
+
* stray re-arm queued. Re-armable: ensureCodexHeartbeatStarted starts a fresh
|
|
78
|
+
* timer once an active cube returns.
|
|
76
79
|
*/
|
|
77
80
|
export declare function stopCodexHeartbeat(): void;
|
|
78
81
|
/** Test-only alias of the production teardown seam (re-testable idempotence). */
|
|
79
82
|
export declare function __resetCodexHeartbeatForTest(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Test-only override of the gh#866-item3 deferred re-arm delay so the
|
|
85
|
+
* mid-session re-arm lifecycle is drivable with a tiny real delay instead of the
|
|
86
|
+
* 20-minute cadence. Reset by __resetCodexHeartbeatForTest.
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
export declare function __setCodexReArmDelayForTest(ms: number): void;
|
|
80
90
|
export declare function startLogStream(opts?: {
|
|
81
91
|
runForever?: () => void;
|
|
82
92
|
}): void;
|
|
@@ -110,6 +120,32 @@ export interface StreamDeps {
|
|
|
110
120
|
*/
|
|
111
121
|
onInboxReceipt?: (active: ActiveCube, token: string) => void;
|
|
112
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Test-only injection seam for runLoop (gh#866 item 2). Production calls
|
|
125
|
+
* `runLoop()` with no args → every dep falls back to the real import and the
|
|
126
|
+
* loop runs forever, exactly as before (zero behavior change). Tests pass stubs
|
|
127
|
+
* plus a `maxIterations` bound to drive a fixed number of iterations through the
|
|
128
|
+
* heartbeat-lifecycle seams — teardown (`stopCodexHeartbeat` on a cleared cube)
|
|
129
|
+
* and re-arm (`ensureCodexHeartbeatStarted` on an active cube) — without real
|
|
130
|
+
* network/keychain IO.
|
|
131
|
+
*/
|
|
132
|
+
export interface RunLoopTestDeps {
|
|
133
|
+
getActiveCube?: typeof getActiveCube;
|
|
134
|
+
acquireStreamLease?: typeof acquireStreamLease;
|
|
135
|
+
sleep?: (ms: number) => Promise<void>;
|
|
136
|
+
maxIterations?: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Test-only entry to the bounded form of runLoop (gh#866 item 2). Drives a
|
|
140
|
+
* fixed number of iterations with injected deps so the heartbeat teardown
|
|
141
|
+
* (cleared cube → `stopCodexHeartbeat`) and re-arm (active cube →
|
|
142
|
+
* `ensureCodexHeartbeatStarted`) seams are exercisable without real
|
|
143
|
+
* network/keychain IO. Never called in production.
|
|
144
|
+
*
|
|
145
|
+
* @internal — test-only surface; mirrors the `__…ForTest` convention used by
|
|
146
|
+
* `__resetStreamStateForTest` / `__resetCodexHeartbeatForTest`.
|
|
147
|
+
*/
|
|
148
|
+
export declare function __runLoopForTest(testDeps: RunLoopTestDeps): Promise<void>;
|
|
113
149
|
export interface ActiveCube {
|
|
114
150
|
cubeId: string;
|
|
115
151
|
droneId: string;
|
package/dist/log-stream.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import{promises as
|
|
1
|
+
import{promises as E}from"node:fs";import oe from"node:os";import L from"node:path";import{getActiveCube as ae,inboxPathForDrone as X}from"./cubes.js";import{DroneEvictedError as J,DRONE_EVICTED_CODE as ie,EVICTED_RESULT_MARKER as se,errorCodeFromBody as ce}from"./drone-lifecycle.js";import{CODEX_HEARTBEAT_CADENCE_MS as U,fireCodexHeartbeatTick as le,formatCodexWakePrompt as de,resolveSessionAgentKind as ue,startCodexHeartbeat as fe,wakeCodexViaAppServer as pe}from"./codex-app-wake.js";import{getValidToken as me}from"./remote-client.js";import{recordEventReceipt as we,emitHealthBeat as he,getCachedMonitorHealthy as be,getCachedWakeArmed as ge}from"./health-beat.js";import{getPackageVersion as ye}from"./version.js";import{acquireStreamLease as q,readOwnershipSnapshot as S}from"./stream-owner.js";const z=9e4,Ie=2e3,_e=500,Ee=3e4,Se=50,F=512,Ze=F*2;function Te(){try{const e=oe.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const Ae=Date.now(),a={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function xe(e,t=Date.now()-Ae){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 et(){return{...a,runLoopHealth:xe(a)}}function tt(){a.connected=!1,a.lastWireActivityAt=null,a.lastContentEventAt=null,a.lastHeartbeatAt=null,a.lastPersistedEventId=null,a.reconnectAttempts=0,a.runLoopRestartCount=0,a.ownership={state:"unowned"}}let k=null,T=null,P=U;function W(e=Ce){k||(k=e())}function Ce(){return fe({tick:()=>{le({isStreamOwner:()=>a.ownership?.state==="owner",onAppServerSocketDead:ke})}})}function ke(){B(),He()}function He(e=P){T||(T=setTimeout(()=>{T=null,W()},e),T.unref?.())}function B(){k&&clearInterval(k),k=null,T&&clearTimeout(T),T=null}function nt(){B(),P=U}function rt(e){P=e}function De(){(async()=>{for(;;){try{await Q(),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
|
-
`)}a.runLoopRestartCount+=1,await
|
|
4
|
-
`);return}a.connected=!1;const
|
|
5
|
-
`),
|
|
3
|
+
`)}a.runLoopRestartCount+=1,await ne(5e3)}})()}function ot(e={}){W(),(e.runForever??De)()}const Y={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:Fe,hasInboxEntryId:Ve,getToken:me,wakeCodex:pe,heartbeatTimeoutMs:z,hwmDivergenceGraceMs:Ie,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:ve};function ve(e,t){we(),he(e,{sseConnected:!0,inboxMonitorHealthy:be(),wakeArmed:ge(),agentKind:ue(),hostname:Te(),version:ye(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function Q(e={}){const t=e.getActiveCube??ae,i=e.acquireStreamLease??q,r=e.sleep??ne,c=e.maxIterations??1/0;let o=0,s=0,l=null,g=null,u=null,h=null;for(;o<c;){o+=1;const d=await t();if(!d){u&&(await u.release(),u=null,h=null),a.connected=!1,a.ownership={state:"unowned"},B(),await r(5e3);continue}W(),d.cubeId!==g&&(g=d.cubeId,l=null);const H=`${d.cubeId}:${d.droneId}`;if(u&&h!==H&&(await u.release(),u=null,h=null),u||(u=await i(d.cubeId,d.droneId),h=u?H:null),!u){a.connected=!1,a.ownership=await S(d.cubeId,d.droneId),await r(5e3);continue}a.ownership=await S(d.cubeId,d.droneId);let y=!1;try{const w=new AbortController,b=async()=>{try{await u.refresh()||(y=!0,w.abort(new Error("stream ownership lost")))}catch(p){y=!0,w.abort(p instanceof Error?p:new Error(String(p)))}},x=setInterval(()=>{b()},Math.max(1e3,Math.floor(z/2)));try{await Z(d,l,p=>{l=p},{abortSignal:w.signal})}finally{clearInterval(x)}if(y){u=null,h=null,a.connected=!1,a.ownership=await S(d.cubeId,d.droneId),await r(5e3);continue}s=0,a.reconnectAttempts=0}catch(w){if(y){u=null,h=null,a.connected=!1,a.ownership=await S(d.cubeId,d.droneId),await r(5e3);continue}if(w instanceof J){u&&await u.release().catch(()=>{}),u=null,h=null,a.connected=!1,a.ownership=await S(d.cubeId,d.droneId),process.stderr.write(`[borg-mcp log stream] drone evicted \u2014 stream terminated (no reconnect).
|
|
4
|
+
`);return}a.connected=!1;const b=Math.min(_e*2**s,Ee)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(b)}ms: ${w?.message??w}
|
|
5
|
+
`),s+=1,a.reconnectAttempts=s,await r(b)}}}function at(e){return Q(e)}async function Z(e,t,i,r={}){const{fetchImpl:c,appendLine:o,hasInboxEntryId:s,getToken:l,wakeCodex:g,heartbeatTimeoutMs:u,hwmDivergenceGraceMs:h,abortSignal:d,onInboxReceipt:H}={...Y,...r},y=await l(),w={Authorization:`Bearer ${y}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&(w["Last-Event-ID"]=t);const b=new AbortController,x=()=>{try{b.abort(d.reason??new Error("external abort"))}catch{}};d.aborted&&x(),d.addEventListener("abort",x,{once:!0});let p=null;const V=()=>{p&&clearTimeout(p),p=setTimeout(()=>{try{b.abort(new Error("heartbeat watchdog timeout"))}catch{}},u)};V();let G=t,m=null,I=null;const C=()=>{I&&(clearTimeout(I.timer),I=null)};let D=null;const v=(n,f)=>{const R={id:n,created_at:f};D&&f&&D.created_at&&A(R,D)<=0||(D=R,G=n,a.lastPersistedEventId=n,i(n))},M=n=>{n&&(m=!m||A(n,m)>0?n:m,I&&A(m,I.hwm)>=0&&C())},re=n=>{if(I?.hwm.id===n.id)return;C();const f=setTimeout(()=>{if(m&&A(m,n)>=0){C();return}try{b.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},h);I={hwm:n,timer:f}},N=new Set,O=[];let j=t!==null;const K=async n=>{const f=Oe(Me(n.data,n.id));return j&&await s(e.cubeId,e.droneId,n.id,f)?(v(n.id,n.data?.created_at??""),"persisted-skip"):(await o(e.cubeId,e.droneId,f),g(de(f)),H(e,y),"written")},$=n=>{for(N.add(n.id),O.push(n.id);O.length>Se;){const f=O.shift();f&&N.delete(f)}v(n.id,n.data?.created_at??""),M(te(n))};let _;try{_=await c(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:w,signal:b.signal})}catch(n){throw p&&clearTimeout(p),n}if(!_.ok||!_.body){if(p&&clearTimeout(p),_.status===410){const n=await _.text().catch(()=>"");if(ce(n)===ie)throw new J}throw new Error(`stream HTTP ${_.status}`)}a.connected=!0;try{for await(const n of Re(_.body)){V();const f=new Date().toISOString();if(a.lastWireActivityAt=f,(n.type==="log"||n.type==="bookmark")&&(a.lastContentEventAt=f),n.type==="eviction"){a.lastContentEventAt=f;try{await o(e.cubeId,e.droneId,$e(n.reason))}catch{}break}if(n.type==="heartbeat"){if(a.lastHeartbeatAt=f,n.hwm&&m===null){M(n.hwm),G===null&&v(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&m&&A(n.hwm,m)<=0){C();continue}n.hwm&&m&&A(n.hwm,m)>0&&re(n.hwm);continue}if(n.type==="bookmark"){j=!1;continue}if(n.type==="log"){if(N.has(n.id)){v(n.id,n.data?.created_at??""),M(te(n));continue}const R=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 K(n)==="persisted-skip")continue;$(n);continue}if(n.data?.drone_id===e.droneId&&!R){$(n);continue}if(await K(n)==="persisted-skip")continue;$(n)}}}finally{d.removeEventListener("abort",x),p&&clearTimeout(p),C(),a.connected=!1}}async function it(e,t,i,r={}){const{ownerDeps:c,ownerStaleMs:o}={...Y,...r},s=await q(e.cubeId,e.droneId,o,c);if(!s)return a.connected=!1,a.ownership=await S(e.cubeId,e.droneId,c),"skipped";a.ownership=await S(e.cubeId,e.droneId,c);try{return await Z(e,t,i,r),"streamed"}finally{await s.release()}}async function*Re(e){const t=e.getReader(),i=new TextDecoder;let r="";try{for(;;){const{value:c,done:o}=await t.read();if(o){if(r.trim()){const l=ee(r);l&&(yield l)}return}r+=i.decode(c,{stream:!0});let s;for(;(s=r.indexOf(`
|
|
6
6
|
|
|
7
|
-
`))!==-1;){const l=
|
|
8
|
-
`))
|
|
9
|
-
`);if(!t)return null;if(t==="log"){if(!i)return null;let
|
|
10
|
-
`,"utf-8"),await
|
|
7
|
+
`))!==-1;){const l=r.slice(0,s);r=r.slice(s+2);const g=ee(l);g&&(yield g)}}}finally{try{t.releaseLock()}catch{}}}function ee(e){let t=null,i=null,r=[];for(const o of e.split(`
|
|
8
|
+
`))o.startsWith("event:")?t=o.slice(6).trim():o.startsWith("id:")?i=o.slice(3).trim():o.startsWith("data:")&&r.push(o.slice(5).trim());const c=r.join(`
|
|
9
|
+
`);if(!t)return null;if(t==="log"){if(!i)return null;let o;try{o=JSON.parse(c)}catch{return null}return{type:"log",id:i,data:o}}if(t==="heartbeat"){let o=null,s=null;try{const l=JSON.parse(c);o=typeof l.ts=="string"?l.ts:null,s=Le(l.hwm)}catch{}return{type:"heartbeat",ts:o,hwm:s}}if(t==="bookmark"){let o=null;try{const s=JSON.parse(c);o=typeof s.as_of=="string"?s.as_of:null}catch{}return{type:"bookmark",as_of:o}}if(t==="eviction"){let o=null,s=null;try{const l=JSON.parse(c);o=typeof l.cube_id=="string"?l.cube_id:null,s=typeof l.reason=="string"?l.reason:null}catch{}return{type:"eviction",cube_id:o,reason:s}}return{type:"unknown",raw:e}}function Le(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 A(e,t){const i=Date.parse(e.created_at),r=Date.parse(t.created_at);return Number.isFinite(i)&&Number.isFinite(r)&&i!==r?i-r:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function te(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 Me(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function Ne(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function Oe(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),i=e.drone_label??"?",r=e.role_name??"?",c=typeof e.message=="string"?e.message:"",o=Ne(e.id,e.entry_id),s=o?`[entry_id: ${o}] `:"",l=c.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${i} (${r}): ${s}${l}`}function $e(e){const t=new Date().toISOString(),i=e&&e.trim().length>0?e:"evicted from cube";return`${t} SYSTEM (eviction): ${se} ${i} \u2014 confirm with any borg_* call; on DRONE_EVICTED (410) shut down: print the terminal message, TaskStop the inbox Monitor, do NOT reschedule /loop.`}async function Fe(e,t,i){const r=X(e,t);await Pe(r,i,F)}async function Pe(e,t,i=F,r=i*2){await E.mkdir(L.dirname(e),{recursive:!0}),await E.appendFile(e,t+`
|
|
10
|
+
`,"utf-8"),await We(e,i,r)}async function We(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 c=(await E.readFile(e,"utf-8")).split(/\r?\n/);if(c.at(-1)===""&&c.pop(),c.length<=i)return;const o=c.slice(-t),s=L.join(L.dirname(e),`.${L.basename(e)}.${process.pid}.${Date.now()}.tmp`);try{await E.writeFile(s,o.join(`
|
|
11
11
|
`)+`
|
|
12
|
-
`,"utf-8"),await
|
|
12
|
+
`,"utf-8"),await E.rename(s,e)}catch(l){throw await E.rm(s,{force:!0}),l}}function Be(e,t,i){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const r=t?i.replace(`[entry_id: ${t}] `,""):i;return!!(r&&e.split(/\r?\n/).includes(r))}async function Ve(e,t,i,r){const c=X(e,t);let o;try{o=await E.readFile(c,"utf-8")}catch(s){if(s?.code==="ENOENT")return!1;throw s}return Be(o,i,r)}function ne(e){return new Promise(t=>setTimeout(t,e))}export{F as INBOX_TAIL_LINES_CAP,Ze as INBOX_TAIL_TRIM_THRESHOLD_LINES,nt as __resetCodexHeartbeatForTest,tt as __resetStreamStateForTest,at as __runLoopForTest,rt as __setCodexReArmDelayForTest,Pe as appendCappedInboxLine,xe as classifyRunLoopHealth,A as compareBroadcastHwm,W as ensureCodexHeartbeatStarted,$e as formatEvictionSentinelLine,Oe as formatInboxLine,et as getStreamStatus,Be as inboxRawHasEntry,Re as parseSSE,ot as startLogStream,B as stopCodexHeartbeat,Z as streamOnce,it as streamOnceIfOwner,We as trimInboxFileToRecentLines};
|
package/dist/setup-confirm.d.ts
CHANGED
|
@@ -70,4 +70,35 @@ export declare function confirmConfigMutation(deps: ConfirmConfigMutationDeps):
|
|
|
70
70
|
* --no-browser/--device scan in setup.ts).
|
|
71
71
|
*/
|
|
72
72
|
export declare function parseYesFlag(argv: string[]): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* gh#844 — whether Step-1 has ANY config mutation worth disclosing.
|
|
75
|
+
*
|
|
76
|
+
* The disclosure + confirm prompt exists to obtain informed consent for the
|
|
77
|
+
* config writes Step-1 performs (gh#818). On a pure refresh — the normal
|
|
78
|
+
* OAuth-refresh re-run where every DETECTED agent CLI already has the full
|
|
79
|
+
* borg setup (MCP server registered AND every hook write already applied) —
|
|
80
|
+
* there is no mutation to consent to, so the prompt is redundant and skipped.
|
|
81
|
+
*
|
|
82
|
+
* CRITICAL (gh#844 SR finding 8d9c732e): the gate must cover the FULL disclosed
|
|
83
|
+
* mutation set — MCP registration AND every hook write (claude UserPromptSubmit
|
|
84
|
+
* + the legacy SessionStart removal; codex SessionStart + UserPromptSubmit) —
|
|
85
|
+
* not MCP registration alone. Otherwise an MCP-configured user with a pending
|
|
86
|
+
* hook write (e.g. a pre-gh#673 upgrader whose legacy global hook must be
|
|
87
|
+
* removed) would have settings.json mutated with consent silently skipped.
|
|
88
|
+
* Caller derives `claudeHookPending`/`codexHookPending` from the SAME peeks
|
|
89
|
+
* that gate the individual writers, so the gate and the writers cannot drift.
|
|
90
|
+
*
|
|
91
|
+
* Scoped to DETECTED CLIs: a claude-only user is never gated on codex config
|
|
92
|
+
* state (and vice-versa). A naive `!isMcpServerConfigured() ||
|
|
93
|
+
* !isCodexMcpServerConfigured()` gate would mis-fire for single-CLI users —
|
|
94
|
+
* the absent CLI's config is unconfigured, so the OR would always be true.
|
|
95
|
+
*/
|
|
96
|
+
export declare function setupMutationPending(deps: {
|
|
97
|
+
claude: boolean;
|
|
98
|
+
codex: boolean;
|
|
99
|
+
claudeMcpConfigured: boolean;
|
|
100
|
+
codexMcpConfigured: boolean;
|
|
101
|
+
claudeHookPending: boolean;
|
|
102
|
+
codexHookPending: boolean;
|
|
103
|
+
}): boolean;
|
|
73
104
|
//# sourceMappingURL=setup-confirm.d.ts.map
|
package/dist/setup-confirm.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function
|
|
2
|
-
`)}async function
|
|
1
|
+
function r(e){const o=[];return e.claude&&(o.push({file:"~/.claude.json",change:"registers the borg MCP server"}),o.push({file:"~/.claude/settings.json",change:"adds a UserPromptSubmit hook"})),e.codex&&(o.push({file:"~/.codex/config.toml",change:"registers the borg MCP server"}),o.push({file:"~/.codex/hooks.json",change:"adds SessionStart + UserPromptSubmit hooks"})),o}function t(e){const o=[];o.push("borg setup will register the borg MCP server in your agent config:");for(const n of e)o.push(` \u2022 ${n.file} (${n.change})`);return o.push('These changes are additive and reversible \u2014 remove the "borg" entries to undo.'),o.join(`
|
|
2
|
+
`)}async function i(e){return!e.isTTY||e.yes||await e.confirm()?"proceed":"abort"}function s(e){return e.includes("--yes")||e.includes("-y")}function c(e){return e.claude&&(!e.claudeMcpConfigured||e.claudeHookPending)||e.codex&&(!e.codexMcpConfigured||e.codexHookPending)}export{r as configMutationTargets,i as confirmConfigMutation,t as formatConfigMutationDisclosure,s as parseYesFlag,c as setupMutationPending};
|
package/dist/setup.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
\u25FC Borg MCP Setup Wizard \u25FC`));const c=process.argv.includes("--no-browser")||process.argv.includes("--device");let t=null,n=null;try{t=
|
|
2
|
+
import k from"prompts";import e from"chalk";import w from"open";import C from"which";import{authenticateWithGoogle as F}from"./auth.js";import{checkSubscriptionStatus as i,createSubscription as P,probeSession as $}from"./remote-client.js";import{setupActionForSession as A}from"./setup-action.js";import{confirmConfigMutation as H,configMutationTargets as M,formatConfigMutationDisclosure as R,parseYesFlag as U,setupMutationPending as T}from"./setup-confirm.js";import{retrySubscriptionCheck as S}from"./subscription-retry.js";import{addMcpServer as D,addUserPromptSubmitHook as q,addCodexMcpServer as I,addCodexSessionStartHook as L,addCodexUserPromptSubmitHook as Y,isMcpServerConfigured as N,isCodexMcpServerConfigured as W,isSessionStartHookRegistered as j,isUserPromptSubmitHookRegistered as B,isCodexSessionStartHookRegistered as E,isCodexUserPromptSubmitHookRegistered as G,removeSessionStartHook as O}from"./config-utils.js";import{handleVersionFlag as z}from"./version.js";import{initDebugFromArgv as Q}from"./debug.js";async function V(){Q(process.argv),z(),console.log(e.blue.bold(`
|
|
3
|
+
\u25FC Borg MCP Setup Wizard \u25FC`));const c=process.argv.includes("--no-browser")||process.argv.includes("--device");let t=null,n=null;try{t=C.sync("claude")}catch{}try{n=C.sync("codex")}catch{}t&&console.log(e.gray(`Found Claude CLI: ${t}`)),n&&console.log(e.gray(`Found Codex CLI: ${n}`)),(t||n)&&console.log(""),!t&&!n&&(console.error(e.red(`\u25FC No supported agent CLI found
|
|
4
4
|
`)),console.error(e.yellow("Please install Claude Code or Codex first:")),console.error(e.gray(" Claude Code: https://claude.ai/download")),console.error(e.gray(` Codex: https://developers.openai.com/codex
|
|
5
|
-
`)),process.exit(1)),console.log(e.blue("\u25FC Agent CLI Integration"));const
|
|
5
|
+
`)),process.exit(1)),console.log(e.blue("\u25FC Agent CLI Integration"));const v=U(process.argv),a=t!==null,l=n!==null,d=N(),p=W(),h=a&&j(),m=a&&!B(),b=l&&!E(),f=l&&!G();if(T({claude:a,codex:l,claudeMcpConfigured:d,codexMcpConfigured:p,claudeHookPending:h||m,codexHookPending:b||f})&&(console.log(R(M({claude:a,codex:l}))),await H({isTTY:process.stdin.isTTY===!0,yes:v,confirm:async()=>{const{proceed:o}=await k({type:"confirm",name:"proceed",message:"Continue with these changes?",initial:!0});return o===!0}})==="abort"&&(console.log(e.yellow(`
|
|
6
6
|
\u25FC Setup cancelled \u2014 no changes made.
|
|
7
|
-
`)),process.exit(0)),console.log(""),t)try{
|
|
8
|
-
\u25FC Failed to configure Claude Code: ${
|
|
9
|
-
`)),process.exit(1)}if(n)try{
|
|
10
|
-
\u25FC Failed to configure Codex: ${
|
|
11
|
-
`)),process.exit(1)}console.log(""),console.log(e.blue("\u25FC Google Authentication"));const
|
|
12
|
-
`));else if(
|
|
13
|
-
\u25FC Could not reach Google to verify your session (network issue).`)),console.error(e.yellow("Re-run `borg setup` when your connection is back.\n")),process.exit(1);else try{await
|
|
14
|
-
\u25FC Authentication failed: ${
|
|
15
|
-
`)),console.error(e.yellow("Re-run `borg setup` to try again.\n")),process.exit(1)}console.log(e.blue("\u25FC Subscription Check"));let s;try{s=await i()}catch(
|
|
16
|
-
\u25FC Subscription check failed: ${
|
|
17
|
-
`)),s={hasAccess:!1}}if(s=await
|
|
7
|
+
`)),process.exit(0))),console.log(""),t)try{d||D(),h&&O(),m&&q(),console.log(e.green("\u25FC borg configured for Claude Code"))}catch(r){console.error(e.red(`
|
|
8
|
+
\u25FC Failed to configure Claude Code: ${r.message}
|
|
9
|
+
`)),process.exit(1)}if(n)try{p||I(),b&&L(),f&&Y(),console.log(e.green("\u25FC borg configured for Codex"))}catch(r){console.error(e.red(`
|
|
10
|
+
\u25FC Failed to configure Codex: ${r.message}
|
|
11
|
+
`)),process.exit(1)}console.log(""),console.log(e.blue("\u25FC Google Authentication"));const y=A(await $());if(y==="skip")console.log(e.green(`\u25FC Already signed in
|
|
12
|
+
`));else if(y==="retry")console.error(e.yellow(`
|
|
13
|
+
\u25FC Could not reach Google to verify your session (network issue).`)),console.error(e.yellow("Re-run `borg setup` when your connection is back.\n")),process.exit(1);else try{await F(c?{noBrowser:!0}:void 0)}catch(r){console.error(e.red(`
|
|
14
|
+
\u25FC Authentication failed: ${r.message}
|
|
15
|
+
`)),console.error(e.yellow("Re-run `borg setup` to try again.\n")),process.exit(1)}console.log(e.blue("\u25FC Subscription Check"));let s;try{s=await i()}catch(r){console.error(e.yellow(`
|
|
16
|
+
\u25FC Subscription check failed: ${r.message}`)),console.error(e.gray(`\u25FC Retrying before falling back to the Free tier...
|
|
17
|
+
`)),s={hasAccess:!1}}if(s=await S(s,{check:i,sleep:r=>new Promise(o=>setTimeout(o,r)),onRetry:(r,o)=>console.log(e.gray(`\u25FC Checking subscription... (attempt ${r}/${o})`))}),s.hasAccess)if(console.log(e.green("\u25FC Active subscription found")),s.expiresAt){const r=new Date(s.expiresAt);console.log(e.gray(` Expires: ${r.toLocaleDateString()}
|
|
18
18
|
`))}else console.log("");else{console.log(e.green("\u25FC You're on the Free tier \u2014 permanent, no card needed: 1 cube + 3 agent sessions + 100 req/hr.")),console.log(e.gray(`\u25FC Start using borgmcp right now. Upgrade any time: $1/month per cube, each cube adds 8 pooled agent sessions + 1000 req/hr.
|
|
19
|
-
`));const{subscribeMethod:
|
|
19
|
+
`));const{subscribeMethod:r}=await k({type:"select",name:"subscribeMethod",message:"You're ready on the Free tier. Want to do more?",choices:[{title:"\u25FC Continue on the Free tier (recommended)",value:"skip",description:"Start now \u2014 1 cube, 3 agent sessions, 100 req/hr. No payment required."},{title:"\u25FC Upgrade to Cube tier \u2014 $1/month per cube",value:"web",description:"Each cube adds 8 pooled agent sessions + 1000 req/hr. Opens the subscribe page in your browser."},{title:"\u25FC Quick Stripe checkout",value:"stripe",description:"Fast upgrade checkout in the browser"},{title:"\u25FC I already subscribed \u2014 re-check",value:"recheck",description:"Re-check now \u2014 a just-completed subscription can take a moment to activate"}]});switch(r===void 0&&console.log(e.yellow(`
|
|
20
20
|
\u25FC No subscription option selected \u2014 continuing on the Free tier.
|
|
21
|
-
`)),
|
|
22
|
-
\u25FC Opening: https://borgmcp.ai/subscribe`));try{await
|
|
23
|
-
`)),await
|
|
24
|
-
\u25FC ${
|
|
25
|
-
`))}break;case"stripe":try{const
|
|
26
|
-
\u25FC Opening Stripe: ${
|
|
27
|
-
`)),await
|
|
28
|
-
\u25FC Failed to create checkout: ${
|
|
21
|
+
`)),r){case"web":console.log(e.blue(`
|
|
22
|
+
\u25FC Opening: https://borgmcp.ai/subscribe`));try{await w("https://borgmcp.ai/subscribe"),console.log(e.gray(`\u25FC Waiting for subscription (checking every 5s for 2 min)...
|
|
23
|
+
`)),await x()}catch(o){console.error(e.yellow(`
|
|
24
|
+
\u25FC ${o.message}`)),console.log(e.green(`\u25FC Continuing on the Free tier. Upgrade any time from https://borgmcp.ai/subscribe.
|
|
25
|
+
`))}break;case"stripe":try{const o=await P();console.log(e.blue(`
|
|
26
|
+
\u25FC Opening Stripe: ${o}`)),await w(o),console.log(e.gray(`\u25FC Waiting for subscription...
|
|
27
|
+
`)),await x()}catch(o){console.error(e.red(`
|
|
28
|
+
\u25FC Failed to create checkout: ${o.message}
|
|
29
29
|
`)),console.log(e.green(`\u25FC Continuing on the Free tier. Upgrade any time from https://borgmcp.ai/subscribe.
|
|
30
|
-
`))}break;case"recheck":try{let
|
|
30
|
+
`))}break;case"recheck":try{let o;try{o=await i()}catch{o={hasAccess:!1}}o=await S(o,{check:i,sleep:g=>new Promise(u=>setTimeout(u,g)),onRetry:(g,u)=>console.log(e.gray(`\u25FC Re-checking subscription... (attempt ${g}/${u})`))}),o.hasAccess?console.log(e.green(`
|
|
31
31
|
\u25FC Subscription found!
|
|
32
32
|
`)):console.log(e.yellow(`
|
|
33
33
|
\u25FC No subscription found \u2014 continuing on the Free tier.
|
|
34
|
-
`))}catch(
|
|
35
|
-
\u25FC Failed to recheck: ${
|
|
34
|
+
`))}catch(o){console.error(e.red(`
|
|
35
|
+
\u25FC Failed to recheck: ${o.message}
|
|
36
36
|
`)),console.log(e.green(`\u25FC Continuing on the Free tier.
|
|
37
37
|
`))}break;case"skip":console.log(e.green(`
|
|
38
38
|
\u25FC You're all set on the Free tier: 1 cube, 3 agent sessions, 100 req/hr.
|
|
39
39
|
`));break}}console.log(e.green.bold(`Setup complete!
|
|
40
40
|
`)),console.log(e.yellow(`\u{1F504} Restart Claude Code/Codex (or open a new session) for the changes to take effect.
|
|
41
41
|
`)),console.log(e.gray("\u25FC Next steps:")),console.log(e.gray('1. cd into your project, then run "borg assimilate" to join a cube')),console.log(e.gray(" (this creates/joins the cube and launches your agent)")),console.log(e.gray(`2. Manage cubes and subscription at https://borgmcp.ai/dashboard
|
|
42
|
-
`))}async function
|
|
43
|
-
`));return}}catch{}}throw new Error("Timeout - Run setup again after subscribing")}
|
|
42
|
+
`))}async function x(){for(let t=0;t<24;t++){await new Promise(n=>setTimeout(n,5e3));try{if((await i()).hasAccess){console.log(e.green(`\u25FC Subscription activated!
|
|
43
|
+
`));return}}catch{}}throw new Error("Timeout - Run setup again after subscribing")}V().catch(c=>{console.error(e.red(`
|
|
44
44
|
\u25FC Setup failed: ${c.message}
|
|
45
45
|
`)),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
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",
|