borgmcp 1.0.48 → 1.0.50
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
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import{dirname as
|
|
2
|
-
`),1}if(r.flags.worktree!==void 0){const t=
|
|
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
|
|
4
|
-
`)}}let l=null;if(
|
|
5
|
-
`);let A;try{A=await e.listCubes(a.apiUrl,a.token)}catch(t){const
|
|
6
|
-
`),a=await e.runSetup(),A=await e.listCubes(a.apiUrl,a.token);else throw t}const
|
|
7
|
-
`),1;let
|
|
1
|
+
import{dirname as K,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 q}from"./name-validator.js";import{renderAssimilationWelcome as me}from"./assimilate-welcome.js";import{shellEscape as de}from"./shell-escape.js";import{withCodexCwdArg as fe}from"./codex-remote.js";import{buildAgentKickoffPrompt as he,buildKickoffWakePathClause as ge,recordCodexWakeTarget as we,socketPathFromRemoteArgs as be}from"./codex-launch.js";import{perWorktreeBranchName as W,adoptWorktree as ke,computeWorktreePath as z,localBranchExists as J,isMerged as ye}from"./worktree-lifecycle.js";import{DroneEvictedError as ve}from"./drone-lifecycle.js";import{codexBorgSessionConfigArgs as $e}from"./launch-gate.js";import{inboxPathForDrone as pe}from"./cubes.js";import{resolveLaunchEnv as xe,resolveOllamaBaseUrl as Se,parseModel as Ce}from"./model-presets.js";import{unlinkSync as Re}from"node:fs";import{gcOrphanInboxesForCube as Ee,defaultListInboxLogs as _e,defaultInboxLivenessDeps as Ae,isInboxLive as Ie,ORPHAN_INBOX_STALE_MS as Ne}from"./gc-orphan-inboxes.js";async function rt(r,e){if(r.role!==void 0){const t=q(r.role);if(!t.ok)return e.stderr(t.error+`
|
|
2
|
+
`),1}if(r.flags.worktree!==void 0){const t=q(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 o=e.findProjectRoot(e.cwd());let i;if(r.flags.cubeName)i=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],o),n=t.status===0?t.stdout:null;if(i=se(o,n),n){const c=ue(n),m=c?ce(c):null;c&&!m&&i&&e.stderr(`couldn't parse git remote '${c}' \u2014 using directory name '${i}' as cube name
|
|
4
|
+
`)}}let l=null;if(i&&i.includes("@")&&i.includes(":")){const t=i.lastIndexOf(":");l={ownerEmail:i.substring(0,t),cubeName:i.substring(t+1)},i=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 n=t instanceof Error?t.message:String(t);if(n.includes("Authentication required")||n.includes("Authentication expired"))e.stderr(`Re-authenticating...
|
|
6
|
+
`),a=await e.runSetup(),A=await e.listCubes(a.apiUrl,a.token);else throw t}const I=A.find(t=>t.name===i);if(!I&&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 u,N;if(I)u=await e.getCube(a.apiUrl,a.token,I.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 n=await e.listTemplates(a.apiUrl,a.token),c=["First drone joining a new cube. Apply a template?"];n.forEach(($,p)=>{const _=p===0?" (default)":"";c.push(` ${p+1}) ${$.name}${_} \u2014 ${$.description}`)}),c.push(` ${n.length+1}) skip \u2014 no template`);const m=(await e.prompt(c.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>n.length+1)return e.stderr(`invalid choice "${m}"
|
|
10
|
+
`),1;t=y<=n.length?n[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(i?`Creating cube '${i}'\u2026
|
|
13
13
|
`:`Creating your cube\u2026
|
|
14
|
-
`),
|
|
14
|
+
`),u=await e.createCube(a.apiUrl,a.token,t?{name:i??void 0,template:t}:{name:i??void 0}),N=!0}let f;if(r.role!==void 0){if(f=ae(u.roles,r.role),!f){const t=u.roles.map(m=>m.name).join(", "),n=De(r.role,u.roles.map(m=>m.name)),c=n?` Did you mean "${n}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${u.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(
|
|
17
|
-
`),1;const x=await e.getActiveCube();let S;if(x&&r.flags.here)if(x.cubeId===
|
|
18
|
-
`),1;const
|
|
19
|
-
`),1}const h=await e.resolveCli(r.flags.cli);e.stderr(`Joining cube '${
|
|
20
|
-
`);let
|
|
21
|
-
`),1;const
|
|
22
|
-
`),1}const k=
|
|
23
|
-
`):k.id!==
|
|
24
|
-
`);const Q=
|
|
16
|
+
`),1}}else if(f=le(u.roles,{isFirstDrone:N}),!f)return e.stderr(`cube "${u.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===u.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 M=r.flags.worktree!==void 0||x!==null&&!r.flags.here,X=S??x?.droneId??null,P=M?null:await e.getLaunchModel(u.id,o,X),b=r.flags.model??P?.model??f.default_model??null,L=Se(process.env,b!=null&&b===P?.model?P?.ollamaBaseUrl:void 0);if(b){const t=await e.checkModelReachable(b,e.fetch,L);if(!t.ok)return e.stderr(`${t.message}
|
|
19
|
+
`),1}const h=await e.resolveCli(r.flags.cli);e.stderr(`Joining cube '${u.name}' as ${f.name}\u2026
|
|
20
|
+
`);let s;try{s=await e.assimilate(a.apiUrl,a.token,{cube_id:u.id,role_id:f.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 n=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${n}
|
|
22
|
+
`),1}const k=u.roles.find(t=>t.id===s.role_id)??f;s.reattached?e.stderr(`re-attached to existing seat ${s.drone_label} (session token rotated, no new drone minted)
|
|
23
|
+
`):k.id!==f.id&&e.stderr(`Note: your invite didn't grant the "${f.name}" role \u2014 assimilated as "${k.name}" instead.
|
|
24
|
+
`);const Q=M;let g=null;if(Q){const t=e.runSync("git",["rev-parse","--verify","HEAD"],o);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 $=C(
|
|
29
|
-
`),1;const _=e.homedir();let
|
|
30
|
-
`),1;e.stderr(`spawned sibling worktree at ${
|
|
31
|
-
`),e.chdir(
|
|
32
|
-
`),g){const
|
|
33
|
-
`):e.stderr(`manual cleanup needed: \`git worktree remove --force ${g}\` (rollback attempt failed: ${
|
|
34
|
-
`)}return 1}e.setTerminalTitle(
|
|
35
|
-
`)}if(!g){e.runSync("git",["fetch","origin","--prune"],w);const t=
|
|
36
|
-
`),e.stderr(
|
|
27
|
+
`),1;e.runSync("git",["fetch","origin"],o);let n="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],o).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],o).status===0&&(n="origin/master");const m=t.stdout.trim(),y=e.runSync("git",["rev-parse",n],o).stdout.trim();m!==y&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${n} (${y.slice(0,7)}); new worktree will start on ${n}
|
|
28
|
+
`);const $=C(o),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 d=z(_,$,p),v=W(C(d),$),Y=2;for(;e.pathExists(d)||Te(e,o,d)||J(e.runSync,o,v)&&!ye(e.runSync,o,v,n);)d=z(_,$,p,Y),v=W(C(d),$),Y++;e.mkdirp(K(d));const G=J(e.runSync,o,v)?e.runSync("git",["worktree","add",d,v],o):e.runSync("git",["worktree","add","-b",v,d,n],o);if(G.status!==0)return e.stderr(`git worktree add failed: ${V(G.stderr)}
|
|
30
|
+
`),1;e.stderr(`spawned sibling worktree at ${d} on branch ${v} (${n}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
|
|
31
|
+
`),e.chdir(d),e.stderr(Pe(d,v,o)),g=e.cwd()}try{await e.setActiveCube({cubeId:s.cube_id,droneId:s.drone_id,name:u.name,sessionToken:s.session_token,droneLabel:s.drone_label,apiUrl:a.apiUrl,roleName:k.name,isHumanSeat:k.is_human_seat,...k.role_class?{roleClass:k.role_class}:{}})}catch(t){const n=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${n}
|
|
32
|
+
`),g){const c=e.runSync("git",["worktree","remove","--force",g],o);c.status===0?e.stderr(`rolled back spawned worktree at ${g}
|
|
33
|
+
`):e.stderr(`manual cleanup needed: \`git worktree remove --force ${g}\` (rollback attempt failed: ${V(c.stderr).trim()||"unknown"})
|
|
34
|
+
`)}return 1}try{const t=Ae(),n=K(pe(s.cube_id,s.drone_id));Ee({cubeInboxDir:n,selfDroneId:s.drone_id,deps:{listInboxLogs:_e,isLive:c=>Ie(c,t),droneState:()=>"absent",unlink:c=>Re(c),now:t.now,staleMs:Ne}})}catch{}e.setTerminalTitle(s.drone_label,u.name);const Z=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(me(k.name,u.name,Z));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=W(C(w),C(o)),n=ke(e.runSync,w,t,"origin/main");n.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
|
|
36
|
+
`),e.stderr(Le(w,t))):n.message&&e.stderr(`worktree sync: ${n.message}
|
|
37
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
|
|
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.",
|
|
38
|
+
`);const ee=e.getInboxPath(s.cube_id,s.drone_id),T=h==="codex"?`borg-wake-${oe()}`:null,te=ge(h==="codex"?"codex":"claude",h==="claude"?ee:null);let D,H=[],E,U=null,O=null;const B=e.findProjectRoot(w);b?await e.setLaunchModel(s.cube_id,B,{model:b,ollamaBaseUrl:Ce(b).kind==="ollama"?L:null}):await e.clearLaunchModel(s.cube_id,B);const F=xe(b,L),j={...process.env,...F.set,BORG_SESSION:"1"};for(const t of F.unset)delete j[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.",H=t.args,Object.keys(t.env).length>0&&Object.assign(j,t.env),U=be(t.args),O=t.server?.cleanup??null}E=[he({cli:h,codexWakeNonce:T,monitorClause:te,codexWakePathClause:D})],h==="codex"&&(E=[...$e(),...H,...fe(E,w)]);const re=e.exec(h,E,w,j);h==="codex"&&U&&T&&we({deps:e,cubeId:s.cube_id,droneId:s.drone_id,socketPath:U,cwd:w,previewNeedle:T,launchedAtSeconds:Math.floor(Date.now()/1e3)});const ne=await re;if(O)try{O()}catch{}return g&&R!==g&&e.stderr(`
|
|
40
40
|
Agent exited. You were working in ${g}; your shell is back in ${R}.
|
|
41
41
|
To return:
|
|
42
|
-
cd ${
|
|
43
|
-
`),
|
|
42
|
+
cd ${de(g)}
|
|
43
|
+
`),ne}function Pe(r,e,a){return`
|
|
44
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
|
|
45
|
+
`}function Le(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 V(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function Te(r,e,a){const o=r.runSync("git",["worktree","list","--porcelain"],e);return o.status!==0?!1:o.stdout.split(`
|
|
48
|
+
`).some(i=>i===`worktree ${a}`)}function De(r,e){if(e.length===0)return null;const a=r.toLowerCase();let o=null;for(const i of e){const l=Ue(a,i.toLowerCase());l<=2&&(o===null||l<o.distance)&&(o={name:i,distance:l})}return o?o.name:null}function Ue(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),o=new Array(e.length+1);for(let i=0;i<=e.length;i++)a[i]=i;for(let i=1;i<=r.length;i++){o[0]=i;for(let l=1;l<=e.length;l++){const R=r[i-1]===e[l-1]?0:1;o[l]=Math.min(o[l-1]+1,a[l]+1,a[l-1]+R)}for(let l=0;l<=e.length;l++)a[l]=o[l]}return a[e.length]}export{rt as runAssimilate,V as safeStderr,De as suggestRoleName};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh#702 — borg-launched Claude Code drones auto-allow the borg MCP
|
|
3
|
+
* coordination tools so a drone never prompts on borg_regen / borg_log /
|
|
4
|
+
* borg_ack / etc. mid-loop.
|
|
5
|
+
*
|
|
6
|
+
* SCOPED, by deliberate design (Option A, per-launch — avoids the gh#844
|
|
7
|
+
* settings-mutation/consent class):
|
|
8
|
+
* - ONLY `mcp__borg__*` is auto-allowed. Bash, file edits (Read/Write/Edit),
|
|
9
|
+
* web (WebFetch/WebSearch), and everything else STILL prompt. This is an
|
|
10
|
+
* allowlist ADD, NOT `--dangerously-skip-permissions` and NOT a blanket
|
|
11
|
+
* allow.
|
|
12
|
+
* - Applied ONLY by the borg launcher (a per-invocation CLI flag) — there is
|
|
13
|
+
* NO persistent user-settings write, so no consent gate is involved.
|
|
14
|
+
* - claude only. Codex parity is a separate follow-up (different permission
|
|
15
|
+
* model) — intentionally not handled here.
|
|
16
|
+
*/
|
|
17
|
+
/** The allowlist pattern matching every tool from the `borg` MCP server. */
|
|
18
|
+
export declare const BORG_MCP_ALLOWED_TOOLS = "mcp__borg__*";
|
|
19
|
+
/**
|
|
20
|
+
* Build the argv for a borg-launched `claude` process: the user's passthrough
|
|
21
|
+
* args.
|
|
22
|
+
*
|
|
23
|
+
* ORDER IS LOAD-BEARING (CR blocker 0e5c697e): `--allowedTools` is a VARIADIC
|
|
24
|
+
* option (`<tools...>`, a space-separated list), so it greedily consumes every
|
|
25
|
+
* following non-flag argv element. If the kickoff prompt came AFTER it, claude
|
|
26
|
+
* would absorb the prompt as a 2nd "allowed tool" and launch with NO kickoff.
|
|
27
|
+
* So the kickoff positional goes FIRST and the variadic flag goes LAST, where
|
|
28
|
+
* it can only consume its own single value:
|
|
29
|
+
* [...passthrough, kickoff, '--allowedTools', 'mcp__borg__*']
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildClaudeLaunchArgs(passthroughArgs: string[], kickoff: string): string[];
|
|
32
|
+
//# sourceMappingURL=claude-launch-args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const r="mcp__borg__*";function e(o,_){return[...o,_,"--allowedTools",r]}export{r as BORG_MCP_ALLOWED_TOOLS,e as buildClaudeLaunchArgs};
|
package/dist/claude.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as E}from"child_process";import{randomUUID as M}from"node:crypto";import{basename as O}from"node:path";import{createInterface as H}from"node:readline/promises";import t from"chalk";import{findProjectRoot as y,getActiveCube as F,getLaunchModel as N,inboxPathForDrone as B,setCodexWakeTarget as _,pruneDeadCodexWakeTargets as W}from"./cubes.js";import{applyOllamaLaunchEnv as G,checkModelReachable as Y}from"./model-presets.js";import{handleVersionFlag as U,getPackageVersion as h}from"./version.js";import{isHelpFlag as T,setupHelpText as V,topLevelHelpText as K,assimilateHelpText as j}from"./cli-help.js";import{runSpawn as q}from"./spawn.js";import{parseSyncArgs as
|
|
3
|
-
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(K(h())),process.exit(0)),process.argv[2]==="setup"){T(process.argv[3])&&(process.stdout.write(V(h())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){process.argv.slice(3).some(T)&&(process.stdout.write(j(h())),process.exit(0));const e=
|
|
4
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=
|
|
5
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await
|
|
6
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await
|
|
7
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=
|
|
8
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1)),
|
|
9
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
2
|
+
import{spawn as E}from"child_process";import{randomUUID as M}from"node:crypto";import{basename as O}from"node:path";import{createInterface as H}from"node:readline/promises";import t from"chalk";import{findProjectRoot as y,getActiveCube as F,getLaunchModel as N,inboxPathForDrone as B,setCodexWakeTarget as _,pruneDeadCodexWakeTargets as W}from"./cubes.js";import{applyOllamaLaunchEnv as G,checkModelReachable as Y}from"./model-presets.js";import{handleVersionFlag as U,getPackageVersion as h}from"./version.js";import{isHelpFlag as T,setupHelpText as V,topLevelHelpText as K,assimilateHelpText as j}from"./cli-help.js";import{runSpawn as q}from"./spawn.js";import{buildClaudeLaunchArgs as X}from"./claude-launch-args.js";import{parseSyncArgs as z,runSync as J}from"./sync.js";import{parseCleanupArgs as Q,runCleanup as Z}from"./cleanup-cmd.js";import{parseAssimilateArgs as ee}from"./parse-assimilate-args.js";import{runAssimilate as re}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as oe}from"./assimilate-deps.js";import{parseLaunchAllArgs as A}from"./parse-launch-all-args.js";import{unknownSubcommand as se}from"./unknown-subcommand.js";import{runLaunchAll as I}from"./launch-all-cmd.js";import{buildDefaultLaunchAllDeps as C}from"./launch-all-deps.js";import{discoverDroneCandidates as te}from"./launch-all-discovery.js";import{runBareLaunchMenu as ae,shouldShowLaunchMenu as ie}from"./bare-launch-menu.js";import{setTerminalTitle as ce}from"./terminal-title.js";import{initConsolePrefix as ne,consolePrefix as o}from"./console-prefix.js";import{initDebugFromArgv as le}from"./debug.js";import{fetchLatestBorgmcpVersion as de,compareVersionsForStaleness as pe}from"./stale-version-check.js";import{defaultCliChoiceDeps as ue,detectCliAvailability as k,installedCliNames as P,parseCliFlag as me,resolveCliChoice as fe}from"./cli-platform.js";import{getRefreshToken as ge,getIdToken as he}from"./config.js";import{composeGetStarted as we,shouldShowGetStarted as xe}from"./get-started.js";import{prepareCodexRemoteLaunch as Ce,withCodexCwdArg as ke,defaultCodexRemoteDeps as ve,checkCodexBridgeHealthy as be}from"./codex-remote.js";import{findLoadedCodexThread as $e}from"./codex-app-server.js";import{buildAgentKickoffPrompt as Se,buildKickoffWakePathClause as ye,recordCodexWakeTarget as Te,socketPathFromRemoteArgs as R}from"./codex-launch.js";import{codexBorgSessionConfigArgs as Ae}from"./launch-gate.js";import{addCodexMcpServer as Ie,addCodexSessionStartHook as Pe,addCodexUserPromptSubmitHook as Re,addMcpServer as Le,addProjectSessionStartHook as De,addUserPromptSubmitHook as Ee,isCodexMcpServerConfigured as Me,isMcpServerConfigured as Oe,removeSessionStartHook as He}from"./config-utils.js";async function Fe(){le(process.argv),U(),await ne();const n=(async()=>{if(!process.stderr.isTTY)return;const e=h(),r=await de();if(!r)return;const i=pe(e,r);i.stale&&i.message&&process.stderr.write(`${o()}${i.message}
|
|
3
|
+
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(K(h())),process.exit(0)),process.argv[2]==="setup"){T(process.argv[3])&&(process.stdout.write(V(h())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){process.argv.slice(3).some(T)&&(process.stdout.write(j(h())),process.exit(0));const e=ee(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${o()}\u25FC borg assimilate: ${e.error}
|
|
4
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=oe(),i=await re({role:e.role,flags:e.flags},r);process.exit(i)}if(process.argv[2]==="spawn"){const e=await q();process.exit(e)}if(process.argv[2]==="sync"){const e=z(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${o()}\u25FC borg sync: ${e.error}
|
|
5
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await J({},e.options);process.exit(r)}if(process.argv[2]==="cleanup"){const e=Q(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${o()}\u25FC borg cleanup: ${e.error}
|
|
6
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await Z({},e.options);process.exit(r)}if(process.argv[2]==="launch-all"){const e=A(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${o()}\u25FC borg launch-all: ${e.error}
|
|
7
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=C(),i=await I(e.args,r);process.exit(i)}const c=se(process.argv[2]);if(c!==null&&(process.stderr.write(t.red(`${o()}\u25FC unknown command: ${c}
|
|
8
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1)),xe(await ge()!==null,await he()!==null)){const e=P(k()).length>0;process.stdout.write(we(e)),process.exit(0)}const f=me(process.argv.slice(2));f.error&&(process.stderr.write(t.red(`${o()}\u25FC ${f.error}
|
|
9
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const v=async e=>{const r=H({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}};let s=await fe(f.cli,ue(v,()=>process.stdin.isTTY===!0));Ne();const a=await F();if(ie({extraArgs:process.argv.slice(2),stdinIsTTY:process.stdin.isTTY===!0,stdoutIsTTY:process.stdout.isTTY===!0})){const e=P(k()).find(m=>m!==s)??null;let r=!1;a&&(r=(await te({targetCubeId:a.cubeId},C())).length>0);const i=await ae({defaultCli:s,otherInstalledCli:e,hasLaunchAllTargets:r},v);if(i.kind==="launch-all"){const m=A([]),D=m.ok?await I(m.args,C()):1;process.exit(D)}s=i.cli}const l=f.rest;ce(a?{label:a.droneLabel,cubeName:a.name}:null,O(process.cwd()));const L=ye(s==="codex"?"codex":"claude",a&&s==="claude"?B(a.cubeId,a.droneId):null);await Promise.race([n,new Promise(e=>setTimeout(e,2e3))]);const b=s==="codex"?`borg-wake-${M()}`:null;let g,$=[],d={...process.env,BORG_SESSION:"1"},p=null,u=null;if(s==="codex"&&!l.includes("--remote")){console.error(`${o()}${t.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await Ce(ve());e.warning?(console.error(`${o()}${t.yellow(`warning: ${e.warning}`)}`),g="\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."):g="Codex wake-path capability check passed: remote-control socket established for this session.",$=e.args,d={...process.env,...e.env,BORG_SESSION:"1"},p=R(e.args),u=e.server?.cleanup??null}else s==="codex"&&l.includes("--remote")&&(g="Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg_regen manually when returning to the session.",p=R(l),p&&(d={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));if(a){const e=await N(a.cubeId,y(),a.droneId),r=G(d,e,process.env);if(d=r.env,r.probe){const i=await Y(r.probe.descriptor,fetch,r.probe.baseUrl);i.ok||console.error(`${o()}${t.yellow(`warning: ${i.message}`)}`)}}const w=Se({cli:s,codexWakeNonce:b,monitorClause:L,codexWakePathClause:g});let x;s==="codex"?x=[...Ae(),...$,...ke([...l,w],process.cwd())]:x=X(l,w),console.error(`${o()}${t.blue(`\u25FC Launching ${s==="claude"?"Claude Code":"Codex"}\u2026`)}`);const S=E(s,x,{stdio:"inherit",shell:!1,env:d});s==="codex"&&a&&p&&(Te({deps:{setCodexWakeTarget:_,findLoadedCodexThread:$e},cubeId:a.cubeId,droneId:a.droneId,socketPath:p,passthroughArgs:l,previewNeedle:b??w.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),W(e=>be(e))),S.on("error",e=>{if(u)try{u()}catch{}e.code==="ENOENT"?(console.error(`${o()}${t.red(`
|
|
10
10
|
\u25FC Failed to launch ${s}`)}`),console.error(`${o()}${t.gray(`Make sure ${s} is installed.
|
|
11
11
|
`)}`)):console.error(`${o()}${t.red(`
|
|
12
12
|
\u25FC Failed to launch ${s}: ${e.message}
|
|
13
|
-
`)}`),process.exit(1)}),S.on("exit",e=>{if(u)try{u()}catch{}process.exit(e??0)})}function
|
|
13
|
+
`)}`),process.exit(1)}),S.on("exit",e=>{if(u)try{u()}catch{}process.exit(e??0)})}function Ne(){const n=k();if(n.claude)try{Oe()||Le(),De(y(process.cwd())),He(),Ee()}catch(c){console.error(`${o()}${t.yellow(`warning: Claude Code integration check failed: ${c?.message??c}`)}`)}if(n.codex)try{Me()||Ie(),Pe(),Re()}catch(c){console.error(`${o()}${t.yellow(`warning: Codex integration check failed: ${c?.message??c}`)}`)}}Fe().catch(n=>{console.error(`${o()}${t.red(`
|
|
14
14
|
\u25FC Error: ${n.message}
|
|
15
15
|
`)}`),process.exit(1)});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** §8.2 staleness threshold — ≥30 days; conservative, well beyond any plausible offline period. */
|
|
2
|
+
export declare const ORPHAN_INBOX_STALE_MS: number;
|
|
3
|
+
/** Roster signal for a drone_id. `absent` is the safe default when no roster is available. */
|
|
4
|
+
export type DroneRosterState = 'present' | 'evicted' | 'absent';
|
|
5
|
+
export interface OrphanInboxEntry {
|
|
6
|
+
/** the drone_id parsed from the `<drone_id>.log` filename */
|
|
7
|
+
droneId: string;
|
|
8
|
+
/** absolute path to the `.log` */
|
|
9
|
+
inboxPath: string;
|
|
10
|
+
/** local mtime of the `.log`, in ms */
|
|
11
|
+
mtimeMs: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SelectOrphanInboxesArgs {
|
|
14
|
+
entries: OrphanInboxEntry[];
|
|
15
|
+
/** §2 HARD gate: true if ANY live-holder signal fires (pgrep / fresh-heartbeat / live-pid). */
|
|
16
|
+
isLive: (inboxPath: string) => boolean;
|
|
17
|
+
/** roster bonus (when available): a `present` member is never reaped. */
|
|
18
|
+
droneState: (droneId: string) => DroneRosterState;
|
|
19
|
+
now: number;
|
|
20
|
+
staleMs: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pure, FS-free selection (mirrors the `acquireInboxLock` dep-injection style so
|
|
24
|
+
* the live-safety + staleness logic is unit-pinned without touching the FS).
|
|
25
|
+
*
|
|
26
|
+
* An inbox is GC-eligible ONLY when ALL hold:
|
|
27
|
+
* §2 NO live holder — `isLive` false (the absolute gate; one live signal vetoes)
|
|
28
|
+
* §3 mtime stale past `staleMs` — the staleness belt (always required, even for evicted)
|
|
29
|
+
* §3.2 not a current roster member — `droneState` !== 'present' (roster bonus; 'absent' by default)
|
|
30
|
+
*/
|
|
31
|
+
export declare function selectOrphanInboxes(args: SelectOrphanInboxesArgs): OrphanInboxEntry[];
|
|
32
|
+
export interface InboxLivenessDeps {
|
|
33
|
+
/** raw `pgrep -f <inboxPath>` match — true if a tail process is following the file (heartbeat-independent). */
|
|
34
|
+
pgrepTailMatch: (inboxPath: string) => boolean;
|
|
35
|
+
/** mtime (ms) of the heartbeat sidecar, or null if absent/unreadable. */
|
|
36
|
+
readHeartbeatMtimeMs: (heartbeatPath: string) => number | null;
|
|
37
|
+
/** parsed PID from the pidfile, or null if absent/unparseable. */
|
|
38
|
+
readPidfilePid: (pidfilePath: string) => number | null;
|
|
39
|
+
/** kill(pid, 0) liveness: true if the process exists. */
|
|
40
|
+
isAlive: (pid: number) => boolean;
|
|
41
|
+
now: number;
|
|
42
|
+
heartbeatStaleMs?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* §2 live-safety check — the HARD gate. LIVE if ANY of three INDEPENDENT signals
|
|
46
|
+
* fire (so a single positive vetoes the delete):
|
|
47
|
+
* 1. a raw `tail` pgrep match (a wedged-but-present tail still holds the inode → KEEP)
|
|
48
|
+
* 2. a heartbeat sidecar present AND fresh (within the stale threshold)
|
|
49
|
+
* 3. a pidfile whose PID is alive (kill-0)
|
|
50
|
+
*/
|
|
51
|
+
export declare function isInboxLive(inboxPath: string, deps: InboxLivenessDeps): boolean;
|
|
52
|
+
export interface OrphanGcDeps {
|
|
53
|
+
/** list the `<drone_id>.log` entries in the cube inbox dir (excludes sidecars). */
|
|
54
|
+
listInboxLogs: (cubeInboxDir: string) => OrphanInboxEntry[];
|
|
55
|
+
isLive: (inboxPath: string) => boolean;
|
|
56
|
+
droneState: (droneId: string) => DroneRosterState;
|
|
57
|
+
unlink: (path: string) => void;
|
|
58
|
+
now: number;
|
|
59
|
+
staleMs: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Wire the GC for one cube dir: select orphans (excluding the just-assimilated
|
|
63
|
+
* drone), then unlink each orphan's triplet (`.log` + `.monitor.pid` +
|
|
64
|
+
* `.monitor.heartbeat`). Best-effort — every unlink is swallowed per-file so a
|
|
65
|
+
* single failure never aborts the sweep or blocks assimilate. Returns the paths
|
|
66
|
+
* actually removed. Never rmdir's the cube dir (a live sibling may use it).
|
|
67
|
+
*/
|
|
68
|
+
export declare function gcOrphanInboxesForCube(args: {
|
|
69
|
+
cubeInboxDir: string;
|
|
70
|
+
selfDroneId: string;
|
|
71
|
+
deps: OrphanGcDeps;
|
|
72
|
+
}): string[];
|
|
73
|
+
/** Real FS/process-backed deps, reusing the #795/#822 primitives. */
|
|
74
|
+
export declare function defaultInboxLivenessDeps(now?: number): InboxLivenessDeps;
|
|
75
|
+
/** Real directory lister: `<drone_id>.log` files only (skips `.monitor.*` sidecars). */
|
|
76
|
+
export declare function defaultListInboxLogs(cubeInboxDir: string): OrphanInboxEntry[];
|
|
77
|
+
//# sourceMappingURL=gc-orphan-inboxes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawnSync as d}from"node:child_process";import{readFileSync as m,readdirSync as p,statSync as c}from"node:fs";import{join as h}from"node:path";import{pidfilePathFor as l,heartbeatPathFor as f,HEARTBEAT_STALE_MS as M}from"./inbox-monitor.js";const y=720*60*60*1e3;function b(n){const{entries:t,isLive:e,droneState:r,now:o,staleMs:a}=n;return t.filter(i=>e(i.inboxPath)||o-i.mtimeMs<a?!1:r(i.droneId)!=="present")}function P(n,t){if(t.pgrepTailMatch(n))return!0;const e=t.readHeartbeatMtimeMs(f(n)),r=t.heartbeatStaleMs??M;if(e!==null&&t.now-e<r)return!0;const o=t.readPidfilePid(l(n));return!!(o!==null&&t.isAlive(o))}function L(n){const{cubeInboxDir:t,selfDroneId:e,deps:r}=n,o=r.listInboxLogs(t).filter(s=>s.droneId!==e),a=b({entries:o,isLive:r.isLive,droneState:r.droneState,now:r.now,staleMs:r.staleMs}),i=[];for(const s of a)for(const u of[s.inboxPath,l(s.inboxPath),f(s.inboxPath)])try{r.unlink(u),i.push(u)}catch{}return i}function v(n=Date.now()){return{pgrepTailMatch:t=>{try{const e=d("pgrep",["-f",t],{encoding:"utf-8",timeout:2e3});return e.error?!1:e.status===0&&e.stdout.trim().length>0}catch{return!1}},readHeartbeatMtimeMs:t=>{try{return c(t).mtimeMs}catch{return null}},readPidfilePid:t=>{try{const e=Number.parseInt(m(t,"utf8").trim(),10);return Number.isNaN(e)?null:e}catch{return null}},isAlive:t=>{try{return process.kill(t,0),!0}catch(e){return e?.code==="EPERM"}},now:n}}function A(n){let t;try{t=p(n)}catch{return[]}const e=[];for(const r of t){if(!r.endsWith(".log"))continue;const o=h(n,r);try{e.push({droneId:r.slice(0,-4),inboxPath:o,mtimeMs:c(o).mtimeMs})}catch{}}return e}export{y as ORPHAN_INBOX_STALE_MS,v as defaultInboxLivenessDeps,A as defaultListInboxLogs,L as gcOrphanInboxesForCube,P as isInboxLive,b as selectOrphanInboxes};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.50",
|
|
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",
|