borgmcp 1.0.18 → 1.0.19

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.
@@ -48,6 +48,8 @@ export interface AssimilateDeps {
48
48
  pathExists: (p: string) => boolean;
49
49
  cwd: () => string;
50
50
  chdir: (p: string) => void;
51
+ homedir: () => string;
52
+ mkdirp: (dir: string) => void;
51
53
  exec: (cmd: string, args: string[], cwd: string, env?: Record<string, string>) => Promise<number>;
52
54
  stderr: (line: string) => void;
53
55
  stdout: (line: string) => void;
@@ -1,40 +1,41 @@
1
- import{dirname as J,basename as S,join as H}from"node:path";import{randomUUID as V}from"node:crypto";import{roleSlug as Q,matchRoleByName as X,pickDefaultRole as Z}from"./role-resolver.js";import{deriveCubeName as ee,parseGitRemote as te,sanitizeRemoteUrl as re}from"./cube-name.js";import{validateName as O}from"./name-validator.js";import{renderAssimilationWelcome as ne}from"./assimilate-welcome.js";import{shellEscape as oe}from"./shell-escape.js";import{withCodexCwdArg as ie}from"./codex-remote.js";import{buildAgentKickoffPrompt as ae,recordCodexWakeTarget as se,socketPathFromRemoteArgs as le}from"./codex-launch.js";import{perWorktreeBranchName as F,adoptWorktree as ce}from"./worktree-lifecycle.js";import{codexBorgSessionConfigArgs as ue}from"./launch-gate.js";async function Te(r,e){if(r.role!==void 0){const t=O(r.role);if(!t.ok)return e.stderr(t.error+`
2
- `),1}if(r.flags.worktree!==void 0){const t=O(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
1
+ import{dirname as J,basename as S}from"node:path";import{randomUUID as V}from"node:crypto";import{roleSlug as Q,matchRoleByName as X,pickDefaultRole as Z}from"./role-resolver.js";import{deriveCubeName as ee,parseGitRemote as te,sanitizeRemoteUrl as re}from"./cube-name.js";import{validateName as H}from"./name-validator.js";import{renderAssimilationWelcome as ne}from"./assimilate-welcome.js";import{shellEscape as oe}from"./shell-escape.js";import{withCodexCwdArg as ie}from"./codex-remote.js";import{buildAgentKickoffPrompt as ae,recordCodexWakeTarget as se,socketPathFromRemoteArgs as le}from"./codex-launch.js";import{perWorktreeBranchName as O,adoptWorktree as ce,computeWorktreePath as F}from"./worktree-lifecycle.js";import{codexBorgSessionConfigArgs as ue}from"./launch-gate.js";async function Te(r,e){if(r.role!==void 0){const t=H(r.role);if(!t.ok)return e.stderr(t.error+`
2
+ `),1}if(r.flags.worktree!==void 0){const t=H(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
3
3
  `),1}let i=await e.getCachedAuth();if(!i){if(!e.isTTY()&&!r.flags.yes)return e.stderr("borg setup required and stdin is non-interactive. Run `borg setup` first in an interactive terminal, then `borg assimilate`.\n"),1;i=await e.runSetup()}const a=e.findProjectRoot(e.cwd());let n;if(r.flags.cubeName)n=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],a),o=t.status===0?t.stdout:null;if(n=ee(a,o),o){const c=re(o),m=c?te(c):null;c&&!m&&n&&e.stderr(`couldn't parse git remote '${c}' \u2014 using directory name '${n}' as cube name
4
- `)}}let s=null;if(n&&n.includes("@")&&n.includes(":")){const t=n.lastIndexOf(":");s={ownerEmail:n.substring(0,t),cubeName:n.substring(t+1)},n=s.cubeName}const $=e.cwd();e.stderr(`Checking your cubes\u2026
4
+ `)}}let s=null;if(n&&n.includes("@")&&n.includes(":")){const t=n.lastIndexOf(":");s={ownerEmail:n.substring(0,t),cubeName:n.substring(t+1)},n=s.cubeName}const p=e.cwd();e.stderr(`Checking your cubes\u2026
5
5
  `);let R;try{R=await e.listCubes(i.apiUrl,i.token)}catch(t){const o=t instanceof Error?t.message:String(t);if(o.includes("Authentication required")||o.includes("Authentication expired"))e.stderr(`Re-authenticating...
6
6
  `),i=await e.runSetup(),R=await e.listCubes(i.apiUrl,i.token);else throw t}const E=R.find(t=>t.name===n);if(!E&&s)return e.stderr(`No cube named '${s.cubeName}' accessible to you owned by '${s.ownerEmail}'. Did you accept their invite? See borgmcp.ai/dashboard.
7
- `),1;let l,A;if(E)l=await e.getCube(i.apiUrl,i.token,E.id),A=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const o=await e.listTemplates(i.apiUrl,i.token),c=["First drone joining a new cube. Apply a template?"];o.forEach((y,k)=>{const C=k===0?" (default)":"";c.push(` ${k+1}) ${y.name}${C} \u2014 ${y.description}`)}),c.push(` ${o.length+1}) skip \u2014 no template`);const m=(await e.prompt(c.join(`
7
+ `),1;let l,A;if(E)l=await e.getCube(i.apiUrl,i.token,E.id),A=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const o=await e.listTemplates(i.apiUrl,i.token),c=["First drone joining a new cube. Apply a template?"];o.forEach((k,y)=>{const C=y===0?" (default)":"";c.push(` ${y+1}) ${k.name}${C} \u2014 ${k.description}`)}),c.push(` ${o.length+1}) skip \u2014 no template`);const m=(await e.prompt(c.join(`
8
8
  `)+`
9
- [1]: `)).trim(),w=m===""?1:parseInt(m,10);if(Number.isNaN(w)||w<1||w>o.length+1)return e.stderr(`invalid choice "${m}"
10
- `),1;t=w<=o.length?o[w-1].name:void 0}else{if(!r.flags.yes)return e.stderr(`cube creation needs a template choice but stdin is non-interactive.
9
+ [1]: `)).trim(),b=m===""?1:parseInt(m,10);if(Number.isNaN(b)||b<1||b>o.length+1)return e.stderr(`invalid choice "${m}"
10
+ `),1;t=b<=o.length?o[b-1].name:void 0}else{if(!r.flags.yes)return e.stderr(`cube creation needs a template choice but stdin is non-interactive.
11
11
  Pass --template <name>, --no-template, or --yes (defaults to starter).
12
12
  `),1;t="starter"}e.stderr(n?`Creating cube '${n}'\u2026
13
13
  `:`Creating your cube\u2026
14
14
  `),l=await e.createCube(i.apiUrl,i.token,t?{name:n??void 0,template:t}:{name:n??void 0}),A=!0}let d;if(r.role!==void 0){if(d=X(l.roles,r.role),!d){const t=l.roles.map(m=>m.name).join(", "),o=ge(r.role,l.roles.map(m=>m.name)),c=o?` Did you mean "${o}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${l.name}". Available: ${t}.${c}
15
15
  (Use --template <name> on first-drone setup or run \`borg:create-role\` from inside Claude.)
16
16
  `),1}}else if(d=Z(l.roles,{isFirstDrone:A}),!d)return e.stderr(`cube "${l.name}" has no default or human-seat role; cannot infer a role. Either pass a role argument explicitly (e.g. \`borg assimilate builder\`) or run \`borg:create-role\` from inside Claude to set up roles.
17
- `),1;const p=await e.getActiveCube();let N;if(p&&r.flags.here)if(p.cubeId===l.id)N=p.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
17
+ `),1;const v=await e.getActiveCube();let N;if(v&&r.flags.here)if(v.cubeId===l.id)N=v.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
18
18
  `),1;e.stderr(`Joining cube '${l.name}' as ${d.name}\u2026
19
19
  `);let u;try{u=await e.assimilate(i.apiUrl,i.token,{cube_id:l.id,role_id:d.id,hostname:e.getHostname(),...N?{prior_drone_id:N}:{}})}catch(t){const o=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${o}
20
- `),1}const v=l.roles.find(t=>t.id===u.role_id)??d;u.reattached?e.stderr(`re-attached to existing seat ${u.drone_label} (session token rotated, no new drone minted)
21
- `):v.id!==d.id&&e.stderr(`Note: your invite didn't grant the "${d.name}" role \u2014 assimilated as "${v.name}" instead.
22
- `);const M=r.flags.worktree!==void 0||p!==null&&!r.flags.here;let f=null;if(M){const t=e.runSync("git",["rev-parse","--verify","HEAD"],a);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
20
+ `),1}const $=l.roles.find(t=>t.id===u.role_id)??d;u.reattached?e.stderr(`re-attached to existing seat ${u.drone_label} (session token rotated, no new drone minted)
21
+ `):$.id!==d.id&&e.stderr(`Note: your invite didn't grant the "${d.name}" role \u2014 assimilated as "${$.name}" instead.
22
+ `);const M=r.flags.worktree!==void 0||v!==null&&!r.flags.here;let f=null;if(M){const t=e.runSync("git",["rev-parse","--verify","HEAD"],a);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
23
23
  Fix: create at least one commit (\`git commit --allow-empty -m "initial"\`)
24
24
  OR: pass --here to skip the sibling spawn and use the current directory
25
- `),1;e.runSync("git",["fetch","origin"],a);let o="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],a).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],a).status===0&&(o="origin/master");const m=t.stdout.trim(),w=e.runSync("git",["rev-parse",o],a).stdout.trim();m!==w&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${o} (${w.slice(0,7)}); new worktree will start on ${o}
26
- `);const y=J(a),k=S(a),C=r.flags.worktree??Q(v.name);let b=H(y,`${k}-${C}`),L=2;for(;e.pathExists(b)||de(e,a,b);)b=H(y,`${k}-${C}-${L}`),L++;const j=F(S(b),k),W=e.runSync("git",["worktree","add","-b",j,b,o],a);if(W.status!==0)return e.stderr(`git worktree add failed: ${B(W.stderr)}
27
- `),1;e.stderr(`spawned sibling worktree at ${b} on branch ${j} (${o}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
28
- `),e.chdir(b),e.stderr(me(b,j,a)),f=e.cwd()}try{await e.setActiveCube({cubeId:u.cube_id,droneId:u.drone_id,name:l.name,sessionToken:u.session_token,droneLabel:u.drone_label,apiUrl:i.apiUrl})}catch(t){const o=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${o}
25
+ `),1;e.runSync("git",["fetch","origin"],a);let o="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],a).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],a).status===0&&(o="origin/master");const m=t.stdout.trim(),b=e.runSync("git",["rev-parse",o],a).stdout.trim();m!==b&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${o} (${b.slice(0,7)}); new worktree will start on ${o}
26
+ `);const k=S(a),y=r.flags.worktree??Q($.name);if(y.length===0)return e.stderr(`cannot derive a worktree name from role "${$.name}"; pass an explicit --worktree <name>
27
+ `),1;const C=e.homedir();let h=F(C,k,y),W=2;for(;e.pathExists(h)||de(e,a,h);)h=F(C,k,y,W),W++;e.mkdirp(J(h));const D=O(S(h),k),L=e.runSync("git",["worktree","add","-b",D,h,o],a);if(L.status!==0)return e.stderr(`git worktree add failed: ${B(L.stderr)}
28
+ `),1;e.stderr(`spawned sibling worktree at ${h} on branch ${D} (${o}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
29
+ `),e.chdir(h),e.stderr(me(h,D,a)),f=e.cwd()}try{await e.setActiveCube({cubeId:u.cube_id,droneId:u.drone_id,name:l.name,sessionToken:u.session_token,droneLabel:u.drone_label,apiUrl:i.apiUrl})}catch(t){const o=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${o}
29
30
  `),f){const c=e.runSync("git",["worktree","remove","--force",f],a);c.status===0?e.stderr(`rolled back spawned worktree at ${f}
30
31
  `):e.stderr(`manual cleanup needed: \`git worktree remove --force ${f}\` (rollback attempt failed: ${B(c.stderr).trim()||"unknown"})
31
- `)}return 1}e.setTerminalTitle(u.drone_label,l.name);const Y=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(ne(v.name,l.name,Y));const h=await e.resolveCli(r.flags.cli),g=e.cwd();try{e.installProjectSessionHook(g)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${g}; it will be re-attempted on the next borg launch
32
- `)}if(!f){e.runSync("git",["fetch","origin","--prune"],g);const t=F(S(g),S(a)),o=ce(e.runSync,g,t,"origin/main");o.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
32
+ `)}return 1}e.setTerminalTitle(u.drone_label,l.name);const Y=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(ne($.name,l.name,Y));const w=await e.resolveCli(r.flags.cli),g=e.cwd();try{e.installProjectSessionHook(g)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${g}; it will be re-attempted on the next borg launch
33
+ `)}if(!f){e.runSync("git",["fetch","origin","--prune"],g);const t=O(S(g),S(a)),o=ce(e.runSync,g,t,"origin/main");o.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
33
34
  `),e.stderr(fe(g,t))):o.message&&e.stderr(`worktree sync: ${o.message}
34
- `)}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.
35
- `);const G=e.getInboxPath(u.cube_id,u.drone_id),T=h==="codex"?`borg-wake-${V()}`:null,q=h==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${G}\` so each event's task-notification title summarizes the new cube log entry (drone label, role, and first ~80 chars of the message body) \u2014 letting you triage events without reading the full body. `:"";let I,U=[],x,D,P=null,_=null;if(h==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
36
- `),I="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg:regen manually whenever you return, and expect only fallback wakeups until relaunch."):I="Codex wake-path capability check passed: remote-control socket established for this session.",U=t.args,D=Object.keys(t.env).length>0?t.env:void 0,P=le(t.args),_=t.server?.cleanup??null}x=[ae({cli:h,codexWakeNonce:T,monitorClause:q,codexWakePathClause:I})],h==="codex"&&(x=[...ue(),...U,...ie(x,g)]);const z=e.exec(h,x,g,{...D??{},BORG_SESSION:"1"});h==="codex"&&P&&T&&se({deps:e,cubeId:u.cube_id,droneId:u.drone_id,socketPath:P,cwd:g,previewNeedle:T,launchedAtSeconds:Math.floor(Date.now()/1e3)});const K=await z;if(_)try{_()}catch{}return f&&$!==f&&e.stderr(`
37
- Agent exited. You were working in ${f}; your shell is back in ${$}.
35
+ `)}await e.probeMcpReady()||e.stderr(`warning: borg-mcp readiness probe did not complete within the timeout; launching ${w} anyway \u2014 the kickoff prompt's ToolSearch fallback will recover if the MCP server takes longer to start.
36
+ `);const G=e.getInboxPath(u.cube_id,u.drone_id),T=w==="codex"?`borg-wake-${V()}`:null,q=w==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${G}\` so each event's task-notification title summarizes the new cube log entry (drone label, role, and first ~80 chars of the message body) \u2014 letting you triage events without reading the full body. `:"";let P,U=[],x,j,I=null,_=null;if(w==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
37
+ `),P="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg:regen manually whenever you return, and expect only fallback wakeups until relaunch."):P="Codex wake-path capability check passed: remote-control socket established for this session.",U=t.args,j=Object.keys(t.env).length>0?t.env:void 0,I=le(t.args),_=t.server?.cleanup??null}x=[ae({cli:w,codexWakeNonce:T,monitorClause:q,codexWakePathClause:P})],w==="codex"&&(x=[...ue(),...U,...ie(x,g)]);const z=e.exec(w,x,g,{...j??{},BORG_SESSION:"1"});w==="codex"&&I&&T&&se({deps:e,cubeId:u.cube_id,droneId:u.drone_id,socketPath:I,cwd:g,previewNeedle:T,launchedAtSeconds:Math.floor(Date.now()/1e3)});const K=await z;if(_)try{_()}catch{}return f&&p!==f&&e.stderr(`
38
+ Agent exited. You were working in ${f}; your shell is back in ${p}.
38
39
  To return:
39
40
  cd ${oe(f)}
40
41
  `),K}function me(r,e,i){return`
@@ -42,4 +43,4 @@ WORKTREE STEERING: You are in worktree ${r} on branch ${e}. Do ALL work HERE \u2
42
43
  `}function fe(r,e){return`
43
44
  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.
44
45
  `}function B(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function de(r,e,i){const a=r.runSync("git",["worktree","list","--porcelain"],e);return a.status!==0?!1:a.stdout.split(`
45
- `).some(n=>n===`worktree ${i}`)}function ge(r,e){if(e.length===0)return null;const i=r.toLowerCase();let a=null;for(const n of e){const s=he(i,n.toLowerCase());s<=2&&(a===null||s<a.distance)&&(a={name:n,distance:s})}return a?a.name:null}function he(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const i=new Array(e.length+1),a=new Array(e.length+1);for(let n=0;n<=e.length;n++)i[n]=n;for(let n=1;n<=r.length;n++){a[0]=n;for(let s=1;s<=e.length;s++){const $=r[n-1]===e[s-1]?0:1;a[s]=Math.min(a[s-1]+1,i[s]+1,i[s-1]+$)}for(let s=0;s<=e.length;s++)i[s]=a[s]}return i[e.length]}export{Te as runAssimilate,B as safeStderr,ge as suggestRoleName};
46
+ `).some(n=>n===`worktree ${i}`)}function ge(r,e){if(e.length===0)return null;const i=r.toLowerCase();let a=null;for(const n of e){const s=he(i,n.toLowerCase());s<=2&&(a===null||s<a.distance)&&(a={name:n,distance:s})}return a?a.name:null}function he(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const i=new Array(e.length+1),a=new Array(e.length+1);for(let n=0;n<=e.length;n++)i[n]=n;for(let n=1;n<=r.length;n++){a[0]=n;for(let s=1;s<=e.length;s++){const p=r[n-1]===e[s-1]?0:1;a[s]=Math.min(a[s-1]+1,i[s]+1,i[s-1]+p)}for(let s=0;s<=e.length;s++)i[s]=a[s]}return i[e.length]}export{Te as runAssimilate,B as safeStderr,ge as suggestRoleName};
@@ -1,3 +1,3 @@
1
- import{spawnSync as m,spawn as l}from"node:child_process";import{existsSync as f}from"node:fs";import{hostname as b}from"node:os";import{createInterface as u}from"node:readline/promises";import{API_URL as d,getValidToken as p,listCubes as h,getCube as C,createCube as _,assimilate as T,listTemplates as y}from"./remote-client.js";import{findProjectRoot as w,getActiveCube as k,setActiveCube as g,inboxPathForDrone as x,setCodexWakeTarget as v}from"./cubes.js";import{authenticateWithGoogle as S}from"./auth.js";import{addProjectSessionStartHook as A}from"./config-utils.js";import{setTerminalTitle as P}from"./terminal-title.js";import{defaultCliChoiceDeps as R,resolveCliChoice as U}from"./cli-platform.js";import{prepareCodexRemoteLaunch as j,defaultCodexRemoteDeps as L}from"./codex-remote.js";import{findLoadedCodexThread as D}from"./codex-app-server.js";function O(){return{runSync:(e,r,o)=>{const t=m(e,r,{cwd:o,encoding:"utf-8"});return{status:t.status,stdout:t.stdout??"",stderr:t.stderr??""}},pathExists:e=>f(e),cwd:()=>process.cwd(),chdir:e=>process.chdir(e),exec:(e,r,o,t)=>new Promise((s,i)=>{const a=l(e,r,{cwd:o,stdio:"inherit",shell:!1,env:t?{...process.env,...t}:process.env});a.on("error",i),a.on("exit",n=>s(n??0))}),stderr:e=>process.stderr.write(e),stdout:e=>process.stdout.write(e),prompt:async e=>{const r=u({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}},isTTY:()=>process.stdin.isTTY===!0,getHostname:()=>b(),setTerminalTitle:(e,r)=>{P({label:e,cubeName:r},r)},getActiveCube:()=>k(),setActiveCube:e=>g(e),findProjectRoot:e=>w(e),installProjectSessionHook:e=>{A(e)},getCachedAuth:async()=>{try{return{token:await p(),apiUrl:d}}catch{return null}},runSetup:async()=>(await S(),{token:await p(),apiUrl:d}),listCubes:async(e,r)=>{const{cubes:o}=await h();return o.map(t=>({id:t.id,name:t.name}))},getCube:async(e,r,o)=>{const t=await C(o);return{id:t.id,name:t.name,roles:t.roles}},createCube:async(e,r,o)=>{const t=await _(o.name,"",o.template?{template:o.template}:void 0);return{id:t.id,name:t.name,roles:t.roles}},assimilate:async(e,r,o)=>{const t=await T({cube_id:o.cube_id,role_id:o.role_id,...o.prior_drone_id?{prior_drone_id:o.prior_drone_id}:{}},void 0,o.hostname??null);return{cube_id:t.cube.id,drone_id:t.drone.id,drone_label:t.drone.label,session_token:t.sessionToken,role_id:t.role.id,reattached:t.reattached===!0}},listTemplates:async(e,r)=>{const{templates:o}=await y();return o.map(t=>({name:t.name,description:t.description}))},getInboxPath:(e,r)=>x(e,r),probeMcpReady:()=>new Promise(e=>{const r=l("borg-mcp",[],{stdio:["pipe","pipe","pipe"],shell:!1});let o="",t=!1;const s=n=>{if(!t){t=!0;try{r.kill("SIGTERM")}catch{}e(n)}},i=setTimeout(()=>s(!1),2e3);r.on("error",()=>{clearTimeout(i),s(!1)}),r.on("exit",()=>{clearTimeout(i),s(t)}),r.stdout?.on("data",n=>{o+=n.toString("utf-8");for(const c of o.split(`
1
+ import{spawnSync as m,spawn as l}from"node:child_process";import{existsSync as f,mkdirSync as b}from"node:fs";import{hostname as h,homedir as C}from"node:os";import{createInterface as d}from"node:readline/promises";import{API_URL as u,getValidToken as p,listCubes as _,getCube as T,createCube as y,assimilate as w,listTemplates as k}from"./remote-client.js";import{findProjectRoot as g,getActiveCube as x,setActiveCube as v,inboxPathForDrone as S,setCodexWakeTarget as A}from"./cubes.js";import{authenticateWithGoogle as P}from"./auth.js";import{addProjectSessionStartHook as R}from"./config-utils.js";import{setTerminalTitle as U}from"./terminal-title.js";import{defaultCliChoiceDeps as j,resolveCliChoice as L}from"./cli-platform.js";import{prepareCodexRemoteLaunch as D,defaultCodexRemoteDeps as H}from"./codex-remote.js";import{findLoadedCodexThread as I}from"./codex-app-server.js";function K(){return{runSync:(e,r,o)=>{const t=m(e,r,{cwd:o,encoding:"utf-8"});return{status:t.status,stdout:t.stdout??"",stderr:t.stderr??""}},pathExists:e=>f(e),cwd:()=>process.cwd(),chdir:e=>process.chdir(e),homedir:()=>C(),mkdirp:e=>b(e,{recursive:!0}),exec:(e,r,o,t)=>new Promise((s,i)=>{const a=l(e,r,{cwd:o,stdio:"inherit",shell:!1,env:t?{...process.env,...t}:process.env});a.on("error",i),a.on("exit",n=>s(n??0))}),stderr:e=>process.stderr.write(e),stdout:e=>process.stdout.write(e),prompt:async e=>{const r=d({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}},isTTY:()=>process.stdin.isTTY===!0,getHostname:()=>h(),setTerminalTitle:(e,r)=>{U({label:e,cubeName:r},r)},getActiveCube:()=>x(),setActiveCube:e=>v(e),findProjectRoot:e=>g(e),installProjectSessionHook:e=>{R(e)},getCachedAuth:async()=>{try{return{token:await p(),apiUrl:u}}catch{return null}},runSetup:async()=>(await P(),{token:await p(),apiUrl:u}),listCubes:async(e,r)=>{const{cubes:o}=await _();return o.map(t=>({id:t.id,name:t.name}))},getCube:async(e,r,o)=>{const t=await T(o);return{id:t.id,name:t.name,roles:t.roles}},createCube:async(e,r,o)=>{const t=await y(o.name,"",o.template?{template:o.template}:void 0);return{id:t.id,name:t.name,roles:t.roles}},assimilate:async(e,r,o)=>{const t=await w({cube_id:o.cube_id,role_id:o.role_id,...o.prior_drone_id?{prior_drone_id:o.prior_drone_id}:{}},void 0,o.hostname??null);return{cube_id:t.cube.id,drone_id:t.drone.id,drone_label:t.drone.label,session_token:t.sessionToken,role_id:t.role.id,reattached:t.reattached===!0}},listTemplates:async(e,r)=>{const{templates:o}=await k();return o.map(t=>({name:t.name,description:t.description}))},getInboxPath:(e,r)=>S(e,r),probeMcpReady:()=>new Promise(e=>{const r=l("borg-mcp",[],{stdio:["pipe","pipe","pipe"],shell:!1});let o="",t=!1;const s=n=>{if(!t){t=!0;try{r.kill("SIGTERM")}catch{}e(n)}},i=setTimeout(()=>s(!1),2e3);r.on("error",()=>{clearTimeout(i),s(!1)}),r.on("exit",()=>{clearTimeout(i),s(t)}),r.stdout?.on("data",n=>{o+=n.toString("utf-8");for(const c of o.split(`
2
2
  `))if(c.includes('"protocolVersion"')&&c.includes('"result"')){clearTimeout(i),s(!0);return}});const a=JSON.stringify({jsonrpc:"2.0",id:1,method:"initialize",params:{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"borg-assimilate-probe",version:"0.9.3"}}});try{r.stdin?.write(a+`
3
- `)}catch{clearTimeout(i),s(!1)}}),resolveCli:e=>U(e,R(async r=>{const o=u({input:process.stdin,output:process.stdout});try{return await o.question(r)}finally{o.close()}},()=>process.stdin.isTTY===!0)),prepareCodexRemoteLaunch:()=>j(L()),setCodexWakeTarget:v,findLoadedCodexThread:D}}export{O as buildDefaultAssimilateDeps};
3
+ `)}catch{clearTimeout(i),s(!1)}}),resolveCli:e=>L(e,j(async r=>{const o=d({input:process.stdin,output:process.stdout});try{return await o.question(r)}finally{o.close()}},()=>process.stdin.isTTY===!0)),prepareCodexRemoteLaunch:()=>D(H()),setCodexWakeTarget:A,findLoadedCodexThread:I}}export{K as buildDefaultAssimilateDeps};
package/dist/cli-help.js CHANGED
@@ -1,4 +1,4 @@
1
- function r(e){return e==="--help"||e==="-h"}function n(e){return`borgmcp ${e} \u2014 run several AI coding agents on one project, together.
1
+ function r(e){return e==="--help"||e==="-h"}function o(e){return`borgmcp ${e} \u2014 run several AI coding agents on one project, together.
2
2
  They coordinate through a shared log (a "cube"). For Claude Code & Codex.
3
3
 
4
4
  Docs & quickstart: https://borgmcp.ai/get-started
@@ -11,13 +11,13 @@ Usage:
11
11
  borg setup Set up OAuth + register MCP server
12
12
  borg setup --no-browser Set up from SSH/headless terminals
13
13
  borg assimilate [role] Join a cube (creates one if needed)
14
- borg assimilate --worktree <name> Spawn a sibling worktree drone
14
+ borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)
15
15
  borg sync [--prune] Sync this worktree's branch to origin/main
16
16
  borg --cli claude|codex Choose agent CLI for this project
17
17
  borg --version Show installed version
18
18
 
19
19
  All other arguments are passed through to the selected agent CLI.
20
- `}function o(e){return`borg setup (borgmcp ${e}) \u2014 set up OAuth + register the borg MCP server
20
+ `}function n(e){return`borg setup (borgmcp ${e}) \u2014 set up OAuth + register the borg MCP server
21
21
 
22
22
  Borg MCP needs Claude Code or Codex installed first.
23
23
 
@@ -28,4 +28,4 @@ Usage:
28
28
  for SSH / headless / container terminals. Alias: --device.
29
29
  Auto-detected on SSH/headless; this forces it.
30
30
  borg setup --help Show this help
31
- `}export{r as isHelpFlag,o as setupHelpText,n as topLevelHelpText};
31
+ `}export{r as isHelpFlag,n as setupHelpText,o as topLevelHelpText};
@@ -28,6 +28,29 @@ export type RunSync = (cmd: string, args: string[], cwd?: string) => {
28
28
  * `wt-myrepo-feature`).
29
29
  */
30
30
  export declare function perWorktreeBranchName(worktreeBasename: string, repoBasename: string): string;
31
+ /**
32
+ * gh#556 Part 1 — the home for NEW drone worktrees: `<homeDir>/.borg/worktrees`.
33
+ * (`~/.borg` is the established borg home — it already holds the encrypted
34
+ * credentials file, see config.ts.)
35
+ */
36
+ export declare function worktreesHome(homeDir: string): string;
37
+ /**
38
+ * gh#556 Part 1 — where a NEW drone worktree lives:
39
+ * `<homeDir>/.borg/worktrees/<repoBase>/<suffix>` (collision variant `<suffix>-<n>`
40
+ * for n>=2; the caller loops n until the path is free).
41
+ *
42
+ * Pure (homeDir injected) so the path scheme + collision dedup + containment are
43
+ * unit-testable without touching $HOME or spawning git.
44
+ *
45
+ * Path-safety / no-traversal: `suffix` is validated upstream BEFORE it reaches here —
46
+ * `--worktree` via validateName (NAME_RE excludes `.`/`/`) or the role default via
47
+ * roleSlug (strips everything but `[a-z0-9-]`); `repoBase` is a single `basename(...)`
48
+ * component. So the result is always CONTAINED under `worktreesHome(homeDir)`.
49
+ * As defense-in-depth this throws on an EMPTY suffix — an empty leaf would let
50
+ * `join` collapse the path up to the repo-level dir (the degenerate-path bug); the
51
+ * caller also guards empty before calling, fail-loud.
52
+ */
53
+ export declare function computeWorktreePath(homeDir: string, repoBase: string, suffix: string, n?: number): string;
31
54
  /** True iff the working tree is clean (`git status --porcelain` empty). */
32
55
  export declare function isCleanTree(runSync: RunSync, cwd: string): boolean;
33
56
  export interface DirtyClassification {
@@ -1,2 +1,2 @@
1
- function f(e,s){const t=`${s}-`;return`wt-${e.startsWith(t)?e.slice(t.length):e}`}function c(e,s){const t=e("git",["status","--porcelain"],s);return t.status===0&&t.stdout.trim()===""}const d=/^\.claude\//;function l(e,s){const t=e("git",["status","--porcelain"],s),i={staged:[],unstaged:[],untracked:[],localConfig:[]};if(t.status!==0)return i;for(const n of t.stdout.split(`
2
- `)){if(!n.trim())continue;const r=n.slice(3);if(n.startsWith("??"))i.untracked.push(r);else{const a=n[0],u=n[1];a!==" "&&a!=="?"&&i.staged.push(r),u!==" "&&u!=="?"&&i.unstaged.push(r)}d.test(r)&&i.localConfig.push(r)}return i}function g(e,s,t,i){return e("git",["merge-base","--is-ancestor",t,i],s).status===0}function o(e,s,t,i){return e("git",["merge-base","--is-ancestor",t,i],s).status===0}function m(e,s,t,i){if(!c(e,s))return{action:"skipped-dirty",message:"uncommitted changes present; sync skipped (nothing discarded)"};if(!g(e,s,t,i))return{action:"skipped-diverged",message:`${t} has diverged from ${i}; resolve manually (no auto-merge/rebase)`};const n=e("git",["rev-list","--count",`${t}..${i}`],s);return n.status===0&&n.stdout.trim()==="0"?{action:"already-current"}:e("git",["merge","--ff-only",i],s).status!==0?{action:"skipped-diverged",message:"ff-only merge unexpectedly failed"}:{action:"fast-forwarded"}}function p(e,s,t){return e("git",["rev-parse","--verify","--quiet",`refs/heads/${t}`],s).status===0}function h(e,s,t,i){return c(e,s)?o(e,s,"HEAD",i)?p(e,s,t)&&!o(e,s,t,i)?{action:"blocked-target-unmerged",message:`branch ${t} exists with commits not on ${i}; resolve before adopting (a force-switch would discard them)`}:(e("git",["switch","-C",t,i],s),{action:"adopted"}):{action:"blocked-unmerged",message:`current HEAD has commits not on ${i}; commit/push or set aside before adopting`}:{action:"skipped-dirty",message:"uncommitted changes present; not switching (nothing discarded)"}}function x(e,s,t,i,n={prune:!1}){return o(e,s,t,i)?n.prune?(e("git",["branch","-d",t],s),{action:"pruned",branch:t}):{action:"announced",branch:t,message:`${t} is merged into ${i} and can be pruned: \`git branch -d ${t}\` (or re-run with --prune)`}:{action:"not-merged",branch:t}}export{h as adoptWorktree,l as classifyDirty,x as cleanupMerged,c as isCleanTree,g as isFastForward,o as isMerged,p as localBranchExists,f as perWorktreeBranchName,m as syncWorktree};
1
+ import{join as c}from"node:path";function h(e,o){const t=`${o}-`;return`wt-${e.startsWith(t)?e.slice(t.length):e}`}function p(e){return c(e,".borg","worktrees")}function x(e,o,t,r){if(t.length===0)throw new Error("computeWorktreePath: suffix must be non-empty (empty leaf would collapse the path to the repo-level dir)");const n=r!==void 0&&r>=2?`${t}-${r}`:t;return c(p(e),o,n)}function d(e,o){const t=e("git",["status","--porcelain"],o);return t.status===0&&t.stdout.trim()===""}const g=/^\.claude\//;function $(e,o){const t=e("git",["status","--porcelain"],o),r={staged:[],unstaged:[],untracked:[],localConfig:[]};if(t.status!==0)return r;for(const n of t.stdout.split(`
2
+ `)){if(!n.trim())continue;const s=n.slice(3);if(n.startsWith("??"))r.untracked.push(s);else{const a=n[0],u=n[1];a!==" "&&a!=="?"&&r.staged.push(s),u!==" "&&u!=="?"&&r.unstaged.push(s)}g.test(s)&&r.localConfig.push(s)}return r}function f(e,o,t,r){return e("git",["merge-base","--is-ancestor",t,r],o).status===0}function i(e,o,t,r){return e("git",["merge-base","--is-ancestor",t,r],o).status===0}function k(e,o,t,r){if(!d(e,o))return{action:"skipped-dirty",message:"uncommitted changes present; sync skipped (nothing discarded)"};if(!f(e,o,t,r))return{action:"skipped-diverged",message:`${t} has diverged from ${r}; resolve manually (no auto-merge/rebase)`};const n=e("git",["rev-list","--count",`${t}..${r}`],o);return n.status===0&&n.stdout.trim()==="0"?{action:"already-current"}:e("git",["merge","--ff-only",r],o).status!==0?{action:"skipped-diverged",message:"ff-only merge unexpectedly failed"}:{action:"fast-forwarded"}}function l(e,o,t){return e("git",["rev-parse","--verify","--quiet",`refs/heads/${t}`],o).status===0}function b(e,o,t,r){return d(e,o)?i(e,o,"HEAD",r)?l(e,o,t)&&!i(e,o,t,r)?{action:"blocked-target-unmerged",message:`branch ${t} exists with commits not on ${r}; resolve before adopting (a force-switch would discard them)`}:(e("git",["switch","-C",t,r],o),{action:"adopted"}):{action:"blocked-unmerged",message:`current HEAD has commits not on ${r}; commit/push or set aside before adopting`}:{action:"skipped-dirty",message:"uncommitted changes present; not switching (nothing discarded)"}}function v(e,o,t,r,n={prune:!1}){return i(e,o,t,r)?n.prune?(e("git",["branch","-d",t],o),{action:"pruned",branch:t}):{action:"announced",branch:t,message:`${t} is merged into ${r} and can be pruned: \`git branch -d ${t}\` (or re-run with --prune)`}:{action:"not-merged",branch:t}}export{b as adoptWorktree,$ as classifyDirty,v as cleanupMerged,x as computeWorktreePath,d as isCleanTree,f as isFastForward,i as isMerged,l as localBranchExists,h as perWorktreeBranchName,k as syncWorktree,p as worktreesHome};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
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",