borgmcp 1.0.12 → 1.0.13

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.
@@ -1,41 +1,45 @@
1
- import{dirname as V,basename as C,join as H}from"node:path";import{randomUUID as J}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+`
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
2
  `),1}if(r.flags.worktree!==void 0){const t=O(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
3
- `),1}let o=await e.getCachedAuth();if(!o){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;o=await e.runSetup()}const a=e.findProjectRoot(e.cwd());let i;if(r.flags.cubeName)i=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],a),n=t.status===0?t.stdout:null;if(i=ee(a,n),n){const c=re(n),m=c?te(c):null;c&&!m&&i&&e.stderr(`couldn't parse git remote '${c}' \u2014 using directory name '${i}' as cube name
4
- `)}}let s=null;if(i&&i.includes("@")&&i.includes(":")){const t=i.lastIndexOf(":");s={ownerEmail:i.substring(0,t),cubeName:i.substring(t+1)},i=s.cubeName}const p=e.cwd();let R;try{R=await e.listCubes(o.apiUrl,o.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...
5
- `),o=await e.runSetup(),R=await e.listCubes(o.apiUrl,o.token);else throw t}const E=R.find(t=>t.name===i);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.
6
- `),1;let l,A;if(E)l=await e.getCube(o.apiUrl,o.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 n=await e.listTemplates(o.apiUrl,o.token),c=["First drone joining a new cube. Apply a template?"];n.forEach((y,k)=>{const S=k===0?" (default)":"";c.push(` ${k+1}) ${y.name}${S} \u2014 ${y.description}`)}),c.push(` ${n.length+1}) skip \u2014 no template`);const m=(await e.prompt(c.join(`
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
5
+ `);let R;try{R=await e.listCubes(i.apiUrl,i.token)}catch(t){const o=t instanceof Error?t.message:String(t);if(o.includes("Authentication required")||o.includes("Authentication expired"))e.stderr(`Re-authenticating...
6
+ `),i=await e.runSetup(),R=await e.listCubes(i.apiUrl,i.token);else throw t}const E=R.find(t=>t.name===n);if(!E&&s)return e.stderr(`No cube named '${s.cubeName}' accessible to you owned by '${s.ownerEmail}'. Did you accept their invite? See borgmcp.ai/dashboard.
7
+ `),1;let l,A;if(E)l=await e.getCube(i.apiUrl,i.token,E.id),A=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const o=await e.listTemplates(i.apiUrl,i.token),c=["First drone joining a new cube. Apply a template?"];o.forEach((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
8
  `)+`
8
- [1]: `)).trim(),w=m===""?1:parseInt(m,10);if(Number.isNaN(w)||w<1||w>n.length+1)return e.stderr(`invalid choice "${m}"
9
- `),1;t=w<=n.length?n[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(),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.
10
11
  Pass --template <name>, --no-template, or --yes (defaults to starter).
11
- `),1;t="starter"}l=await e.createCube(o.apiUrl,o.token,t?{name:i??void 0,template:t}:{name:i??void 0}),A=!0}let g;if(r.role!==void 0){if(g=X(l.roles,r.role),!g){const t=l.roles.map(m=>m.name).join(", "),n=ge(r.role,l.roles.map(m=>m.name)),c=n?` Did you mean "${n}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${l.name}". Available: ${t}.${c}
12
+ `),1;t="starter"}e.stderr(n?`Creating cube '${n}'\u2026
13
+ `:`Creating your cube\u2026
14
+ `),l=await e.createCube(i.apiUrl,i.token,t?{name:n??void 0,template:t}:{name:n??void 0}),A=!0}let d;if(r.role!==void 0){if(d=X(l.roles,r.role),!d){const t=l.roles.map(m=>m.name).join(", "),o=ge(r.role,l.roles.map(m=>m.name)),c=o?` Did you mean "${o}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${l.name}". Available: ${t}.${c}
12
15
  (Use --template <name> on first-drone setup or run \`borg:create-role\` from inside Claude.)
13
- `),1}}else if(g=Z(l.roles,{isFirstDrone:A}),!g)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.
14
- `),1;const $=await e.getActiveCube();let N;if($&&r.flags.here)if($.cubeId===l.id)N=$.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
15
- `),1;let u;try{u=await e.assimilate(o.apiUrl,o.token,{cube_id:l.id,role_id:g.id,hostname:e.getHostname(),...N?{prior_drone_id:N}:{}})}catch(t){const n=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${n}
16
- `),1}const v=l.roles.find(t=>t.id===u.role_id)??g;u.reattached?e.stderr(`re-attached to existing seat ${u.drone_label} (session token rotated, no new drone minted)
17
- `):v.id!==g.id&&e.stderr(`Note: your invite didn't grant the "${g.name}" role \u2014 assimilated as "${v.name}" instead.
18
- `);const M=r.flags.worktree!==void 0||$!==null&&!r.flags.here;let f=null;if(M){const t=e.runSync("git",["rev-parse","--verify","HEAD"],a);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
16
+ `),1}}else if(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
18
+ `),1;e.stderr(`Joining cube '${l.name}' as ${d.name}\u2026
19
+ `);let u;try{u=await e.assimilate(i.apiUrl,i.token,{cube_id:l.id,role_id:d.id,hostname:e.getHostname(),...N?{prior_drone_id:N}:{}})}catch(t){const o=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${o}
20
+ `),1}const 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.
19
23
  Fix: create at least one commit (\`git commit --allow-empty -m "initial"\`)
20
24
  OR: pass --here to skip the sibling spawn and use the current directory
21
- `),1;e.runSync("git",["fetch","origin"],a);let n="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],a).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],a).status===0&&(n="origin/master");const m=t.stdout.trim(),w=e.runSync("git",["rev-parse",n],a).stdout.trim();m!==w&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${n} (${w.slice(0,7)}); new worktree will start on ${n}
22
- `);const y=V(a),k=C(a),S=r.flags.worktree??Q(v.name);let b=H(y,`${k}-${S}`),L=2;for(;e.pathExists(b)||de(e,a,b);)b=H(y,`${k}-${S}-${L}`),L++;const j=F(C(b),k),W=e.runSync("git",["worktree","add","-b",j,b,n],a);if(W.status!==0)return e.stderr(`git worktree add failed: ${B(W.stderr)}
23
- `),1;e.stderr(`spawned sibling worktree at ${b} on branch ${j} (${n}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
24
- `),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:o.apiUrl})}catch(t){const n=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${n}
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
29
  `),f){const c=e.runSync("git",["worktree","remove","--force",f],a);c.status===0?e.stderr(`rolled back spawned worktree at ${f}
26
30
  `):e.stderr(`manual cleanup needed: \`git worktree remove --force ${f}\` (rollback attempt failed: ${B(c.stderr).trim()||"unknown"})
27
- `)}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),d=e.cwd();try{e.installProjectSessionHook(d)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${d}; it will be re-attempted on the next borg launch
28
- `)}if(!f){e.runSync("git",["fetch","origin","--prune"],d);const t=F(C(d),C(a)),n=ce(e.runSync,d,t,"origin/main");n.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
29
- `),e.stderr(fe(d,t))):n.message&&e.stderr(`worktree sync: ${n.message}
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
33
+ `),e.stderr(fe(g,t))):o.message&&e.stderr(`worktree sync: ${o.message}
30
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.
31
- `);const G=e.getInboxPath(u.cube_id,u.drone_id),T=h==="codex"?`borg-wake-${J()}`: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}
32
- `),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,d)]);const z=e.exec(h,x,d,{...D??{},BORG_SESSION:"1"});h==="codex"&&P&&T&&se({deps:e,cubeId:u.cube_id,droneId:u.drone_id,socketPath:P,cwd:d,previewNeedle:T,launchedAtSeconds:Math.floor(Date.now()/1e3)});const K=await z;if(_)try{_()}catch{}return f&&p!==f&&e.stderr(`
33
- Agent exited. You were working in ${f}; your shell is back in ${p}.
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 ${$}.
34
38
  To return:
35
39
  cd ${oe(f)}
36
- `),K}function me(r,e,o){return`
37
- 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 ${o}\` or operate on the primary checkout ${o}: 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).
40
+ `),K}function me(r,e,i){return`
41
+ WORKTREE STEERING: You are in worktree ${r} on branch ${e}. Do ALL work HERE \u2014 cut your feature branch (fix/.../feat/...) off ${e} in THIS worktree, use relative paths / your cwd. NEVER \`git -C ${i}\` or operate on the primary checkout ${i}: the same branch can't be checked out in two worktrees, so work created in the primary won't reach your wt-branch without manual surgery (cherry-pick/merge).
38
42
  `}function fe(r,e){return`
39
43
  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.
40
- `}function B(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function de(r,e,o){const a=r.runSync("git",["worktree","list","--porcelain"],e);return a.status!==0?!1:a.stdout.split(`
41
- `).some(i=>i===`worktree ${o}`)}function ge(r,e){if(e.length===0)return null;const o=r.toLowerCase();let a=null;for(const i of e){const s=he(o,i.toLowerCase());s<=2&&(a===null||s<a.distance)&&(a={name:i,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 o=new Array(e.length+1),a=new Array(e.length+1);for(let i=0;i<=e.length;i++)o[i]=i;for(let i=1;i<=r.length;i++){a[0]=i;for(let s=1;s<=e.length;s++){const p=r[i-1]===e[s-1]?0:1;a[s]=Math.min(a[s-1]+1,o[s]+1,o[s-1]+p)}for(let s=0;s<=e.length;s++)o[s]=a[s]}return o[e.length]}export{Te as runAssimilate,B as safeStderr,ge as suggestRoleName};
44
+ `}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};
@@ -1,2 +1,2 @@
1
- const c="\x1B[32m",t="\x1B[0m";function r(e,n,o){return[`${o?`${c}\u2713${t}`:"\u2713"} Joined as \`${e}\` in cube \`${n}\`.`,"","Next: run `borg:regen` inside Claude to see your cube.","You're set up \u2014 your team can now see you in the cube.",""].join(`
2
- `)}export{r as renderAssimilationWelcome};
1
+ const t="\x1B[32m",r="\x1B[0m";function c(e,o,n){return[`${n?`${t}\u2713${r}`:"\u2713"} Joined as \`${e}\` in cube \`${o}\`.`,"","Next: ask your agent to run the `borg:regen` tool to see your cube.","Add a teammate: run `borg assimilate <role>` in another terminal.","You're set up \u2014 your team can now see you in the cube.",""].join(`
2
+ `)}export{c as renderAssimilationWelcome};
package/dist/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- import{createServer as m}from"http";import{URL as k}from"url";import _ from"crypto";import C from"open";import{storeIdToken as d,storeRefreshToken as f,getRefreshToken as p}from"./config.js";import{cerr as t}from"./console-prefix.js";import{isNoBrowserEnv as R}from"./auth-env.js";import{requestDeviceCode as P,pollForDeviceToken as O}from"./device-auth.js";class x extends Error{errorCode;errorDescription;constructor(r,s){super(s?`Refresh token invalid (${r}): ${s}`:`Refresh token invalid (${r})`),this.errorCode=r,this.errorDescription=s,this.name="RefreshTokenInvalidError"}}class u extends Error{constructor(r){super(r),this.name="RefreshTransientError"}}const w="675073910799-41pbe12rfhqemidh64h09s4q3e0udpgp.apps.googleusercontent.com",g="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",I="675073910799-6qmi73v5106dj1v0l22j2qnkh5r3e8fq.apps.googleusercontent.com",G="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",S="https://accounts.google.com/o/oauth2/v2/auth",E="https://oauth2.googleapis.com/token",L="https://oauth2.googleapis.com/revoke",T=["openid","email","profile"],J=8e3,X=9e3;function A(){const e=_.randomBytes(32).toString("base64url"),r=_.createHash("sha256").update(e).digest("base64url");return{verifier:e,challenge:r}}async function D(){return new Promise((e,r)=>{const s=m();s.listen(0,()=>{const i=s.address();if(i&&typeof i=="object"){const o=i.port;s.close(()=>e(o))}else s.close(()=>r(new Error("Failed to get assigned port")))}),s.on("error",r)})}async function $(){const e=await D(),r=new Promise((s,i)=>{const o=m((n,c)=>{const a=new k(n.url,`http://localhost:${e}`);if(a.pathname==="/callback"){const h=a.searchParams.get("code"),l=a.searchParams.get("error");if(l){c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
1
+ import{createServer as w}from"http";import{URL as _}from"url";import g from"crypto";import P from"open";import{storeIdToken as d,storeRefreshToken as f,getRefreshToken as p}from"./config.js";import{cerr as t}from"./console-prefix.js";import{isNoBrowserEnv as O}from"./auth-env.js";import{requestDeviceCode as I,pollForDeviceToken as x}from"./device-auth.js";class G extends Error{errorCode;errorDescription;constructor(r,s){super(s?`Refresh token invalid (${r}): ${s}`:`Refresh token invalid (${r})`),this.errorCode=r,this.errorDescription=s,this.name="RefreshTokenInvalidError"}}class u extends Error{constructor(r){super(r),this.name="RefreshTransientError"}}const m="675073910799-41pbe12rfhqemidh64h09s4q3e0udpgp.apps.googleusercontent.com",k="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",A="675073910799-6qmi73v5106dj1v0l22j2qnkh5r3e8fq.apps.googleusercontent.com",S="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",L="https://accounts.google.com/o/oauth2/v2/auth",E="https://oauth2.googleapis.com/token",$="https://oauth2.googleapis.com/revoke",T=["openid","email","profile"],Q=8e3,Z=9e3,y=300*1e3,v=y/6e4;function D(){const e=g.randomBytes(32).toString("base64url"),r=g.createHash("sha256").update(e).digest("base64url");return{verifier:e,challenge:r}}async function N(){return new Promise((e,r)=>{const s=w();s.listen(0,()=>{const i=s.address();if(i&&typeof i=="object"){const o=i.port;s.close(()=>e(o))}else s.close(()=>r(new Error("Failed to get assigned port")))}),s.on("error",r)})}async function U(){const e=await N(),r=new Promise((s,i)=>{const o=w((n,c)=>{const a=new _(n.url,`http://localhost:${e}`);if(a.pathname==="/callback"){const h=a.searchParams.get("code"),l=a.searchParams.get("error");if(l){c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
2
2
  <html>
3
3
  <body>
4
4
  <h1>\u25FC Authentication Failed</h1>
@@ -20,19 +20,19 @@ import{createServer as m}from"http";import{URL as k}from"url";import _ from"cryp
20
20
  <p>Missing authorization code.</p>
21
21
  </body>
22
22
  </html>
23
- `),o.close(),i(new Error("Missing authorization code"))}});o.listen(e,()=>{t(`Callback server listening on http://localhost:${e}`)}),setTimeout(()=>{o.close(),i(new Error("Authentication timeout - no response received"))},300*1e3).unref()});return{port:e,codePromise:r}}async function N(e,r,s){const i=`http://localhost:${s}/callback`,o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:w,client_secret:g,code:e,code_verifier:r,grant_type:"authorization_code",redirect_uri:i})});if(!o.ok){const n=await o.text();throw new Error(`Failed to exchange code for tokens: ${n}`)}return await o.json()}async function v(e){try{await fetch(L,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`token=${encodeURIComponent(e)}`})}catch{}}async function U(){t(`
23
+ `),o.close(),i(new Error("Missing authorization code"))}});o.listen(e,()=>{t(`Callback server listening on http://localhost:${e}`)}),setTimeout(()=>{o.close(),i(new Error(`Authentication timed out after ${v} minutes \u2014 no authorization received from the browser. Re-run \`borg setup\` and complete the Google sign-in in the page that opens.`))},y).unref()});return{port:e,codePromise:r}}async function B(e,r,s){const i=`http://localhost:${s}/callback`,o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:m,client_secret:k,code:e,code_verifier:r,grant_type:"authorization_code",redirect_uri:i})});if(!o.ok){const n=await o.text();throw new Error(`Failed to exchange code for tokens: ${n}`)}return await o.json()}async function C(e){try{await fetch($,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`token=${encodeURIComponent(e)}`})}catch{}}async function M(){t(`
24
24
  \u25FC Borg MCP Authentication`),t(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
25
- `);const e=await p();e&&(t("Revoking previous refresh_token to force fresh consent..."),await v(e)),t("Generating PKCE challenge...");const r=A();t("Starting local callback server...");const{port:s,codePromise:i}=await $(),o=`http://localhost:${s}/callback`,n=new k(S);n.searchParams.set("client_id",w),n.searchParams.set("redirect_uri",o),n.searchParams.set("response_type","code"),n.searchParams.set("scope",T.join(" ")),n.searchParams.set("code_challenge",r.challenge),n.searchParams.set("code_challenge_method","S256"),n.searchParams.set("access_type","offline"),n.searchParams.set("prompt","consent select_account"),t(`
25
+ `);const e=await p();e&&(t("Revoking previous refresh_token to force fresh consent..."),await C(e)),t("Generating PKCE challenge...");const r=D();t("Starting local callback server...");const{port:s,codePromise:i}=await U(),o=`http://localhost:${s}/callback`,n=new _(L);n.searchParams.set("client_id",m),n.searchParams.set("redirect_uri",o),n.searchParams.set("response_type","code"),n.searchParams.set("scope",T.join(" ")),n.searchParams.set("code_challenge",r.challenge),n.searchParams.set("code_challenge_method","S256"),n.searchParams.set("access_type","offline"),n.searchParams.set("prompt","consent select_account"),t(`
26
26
  \u{1F4F1} Opening browser for authorization...`),t("If browser does not open, visit:"),t(`${n.toString()}
27
- `);try{await C(n.toString())}catch(l){t(`Could not open a browser automatically: ${l?.message??"unknown"}`),t("Continue by opening the URL above manually.")}t("Waiting for authorization...");const c=await i;t("Exchanging authorization code for tokens...");const a=await N(c,r.verifier,s),h=Date.now()+a.expires_in*1e3;await d(a.id_token,h),a.refresh_token?await f(a.refresh_token):(t(`
27
+ `);try{await P(n.toString())}catch(l){t(`Could not open a browser automatically: ${l?.message??"unknown"}`),t("Continue by opening the URL above manually.")}t(`Waiting for you to finish signing in (up to ${v} minutes)... this terminal continues automatically once you approve.`);const c=await i;t("Exchanging authorization code for tokens...");const a=await B(c,r.verifier,s),h=Date.now()+a.expires_in*1e3;await d(a.id_token,h),a.refresh_token?await f(a.refresh_token):(t(`
28
28
  \u26A0 No refresh_token returned by Google.`),t(" Your session will expire after ~1 hour and require"),t(" re-running `borg setup`. To enable auto-refresh:"),t(" 1. Visit https://myaccount.google.com/permissions"),t(' 2. Find "Borg MCP" and click "Remove access"'),t(" 3. Re-run `borg setup`"),t(` (Google will then issue a fresh refresh_token.)
29
29
  `)),t(`
30
30
  \u25FC Authentication successful!
31
- `)}function B(e){return e?.noBrowser??R()}function y(e=process.env){const r=e.GOOGLE_DEVICE_CLIENT_ID?.trim(),s=e.GOOGLE_DEVICE_CLIENT_SECRET?.trim()||void 0;let i,o;if(r?(i=r,o=s):(i=I,o=G||void 0),!i)throw new Error('No-browser (device-grant) auth needs a Google "TVs & Limited Input devices" OAuth client. Set GOOGLE_DEVICE_CLIENT_ID in the environment, or run `borg setup` on a machine with a browser. See docs/REMOTE_TERMINAL_AUTH.md.');return{clientId:i,clientSecret:o,scopes:T}}function M(e){return new Promise(r=>setTimeout(r,e))}async function q(e={fetch,sleep:M},r=process.env){t(`
31
+ `)}function H(e){return e?.noBrowser??O()}function b(e=process.env){const r=e.GOOGLE_DEVICE_CLIENT_ID?.trim(),s=e.GOOGLE_DEVICE_CLIENT_SECRET?.trim()||void 0;let i,o;if(r?(i=r,o=s):(i=A,o=S||void 0),!i)throw new Error('No-browser (device-grant) auth needs a Google "TVs & Limited Input devices" OAuth client. Set GOOGLE_DEVICE_CLIENT_ID in the environment, or run `borg setup` on a machine with a browser. See docs/REMOTE_TERMINAL_AUTH.md.');return{clientId:i,clientSecret:o,scopes:T}}function q(e){return new Promise(r=>setTimeout(r,e))}async function F(e={fetch,sleep:q},r=process.env){t(`
32
32
  \u25FC Borg MCP Authentication (no-browser mode)`),t(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
33
- `);const s=y(r),i=await p();i&&(t("Revoking previous refresh_token to force fresh consent..."),await v(i));const o=await P(s,e);t("To authorize Borg MCP on this machine:"),t(` 1. On any device with a browser, open: ${o.verification_url}`),t(` 2. Enter this code: ${o.user_code}
34
- `),t("Waiting for authorization (this page can be open on your phone or laptop)...");const n=await O(o,s,e),c=Date.now()+n.expires_in*1e3;await d(n.id_token,c),n.refresh_token?await f(n.refresh_token):(t(`
33
+ `);const s=b(r),i=await p();i&&(t("Revoking previous refresh_token to force fresh consent..."),await C(i));const o=await I(s,e);t("To authorize Borg MCP on this machine:"),t(` 1. On any device with a browser, open: ${o.verification_url}`),t(` 2. Enter this code: ${o.user_code}
34
+ `),t("Waiting for authorization (this page can be open on your phone or laptop)...");const n=await x(o,s,e),c=Date.now()+n.expires_in*1e3;await d(n.id_token,c),n.refresh_token?await f(n.refresh_token):(t(`
35
35
  \u26A0 No refresh_token returned by Google.`),t(" Your session will expire after ~1 hour and require re-running"),t(" `borg setup`. Re-consent at https://myaccount.google.com/permissions"),t(` (remove "Borg MCP") then re-run setup to restore automatic token refresh.
36
36
  `)),t(`
37
37
  \u25FC Authentication successful!
38
- `)}async function Q(e){return B(e)?q():U()}async function b(e,r,s){const i={client_id:r,refresh_token:e,grant_type:"refresh_token"};s&&(i.client_secret=s);let o;try{o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(i)})}catch(a){return{ok:!1,kind:"transient",error:new u(`Network failure during token refresh: ${a?.message??"unknown"}`)}}if(!o.ok){let a=null;try{a=await o.json()}catch{return{ok:!1,kind:"transient",error:new u(`Token refresh failed with HTTP ${o.status} (non-JSON body)`)}}return o.status===400&&a?.error==="invalid_grant"?{ok:!1,kind:"invalid",error:new x("invalid_grant",a.error_description)}:{ok:!1,kind:"transient",error:new u(`Token refresh failed with HTTP ${o.status}${a?.error?` (${a.error})`:""}`)}}let n;try{n=await o.json()}catch(a){return{ok:!1,kind:"transient",error:new u(`Token refresh response unparseable: ${a?.message??"unknown"}`)}}if(!n.id_token||typeof n.expires_in!="number")return{ok:!1,kind:"transient",error:new u("Token refresh response missing id_token or expires_in")};const c=Date.now()+n.expires_in*1e3;if(n.refresh_token){const a=await p();await f(n.refresh_token);try{await d(n.id_token,c)}catch(h){if(a)try{await f(a)}catch{}throw h}return{ok:!0}}return await d(n.id_token,c),{ok:!0}}async function Z(e){const r=await b(e,w,g);if(r.ok)return;const s=y(),i=await b(e,s.clientId,s.clientSecret);if(!i.ok)throw r.kind==="invalid"&&i.kind==="invalid"?i.error:r.kind==="transient"?r.error:i.error}export{x as RefreshTokenInvalidError,u as RefreshTransientError,q as authenticateWithDeviceFlow,Q as authenticateWithGoogle,y as buildDeviceAuthConfig,Z as refreshIdToken,B as shouldUseDeviceFlow};
38
+ `)}async function ee(e){return H(e)?F():M()}async function R(e,r,s){const i={client_id:r,refresh_token:e,grant_type:"refresh_token"};s&&(i.client_secret=s);let o;try{o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(i)})}catch(a){return{ok:!1,kind:"transient",error:new u(`Network failure during token refresh: ${a?.message??"unknown"}`)}}if(!o.ok){let a=null;try{a=await o.json()}catch{return{ok:!1,kind:"transient",error:new u(`Token refresh failed with HTTP ${o.status} (non-JSON body)`)}}return o.status===400&&a?.error==="invalid_grant"?{ok:!1,kind:"invalid",error:new G("invalid_grant",a.error_description)}:{ok:!1,kind:"transient",error:new u(`Token refresh failed with HTTP ${o.status}${a?.error?` (${a.error})`:""}`)}}let n;try{n=await o.json()}catch(a){return{ok:!1,kind:"transient",error:new u(`Token refresh response unparseable: ${a?.message??"unknown"}`)}}if(!n.id_token||typeof n.expires_in!="number")return{ok:!1,kind:"transient",error:new u("Token refresh response missing id_token or expires_in")};const c=Date.now()+n.expires_in*1e3;if(n.refresh_token){const a=await p();await f(n.refresh_token);try{await d(n.id_token,c)}catch(h){if(a)try{await f(a)}catch{}throw h}return{ok:!0}}return await d(n.id_token,c),{ok:!0}}async function te(e){const r=await R(e,m,k);if(r.ok)return;const s=b(),i=await R(e,s.clientId,s.clientSecret);if(!i.ok)throw r.kind==="invalid"&&i.kind==="invalid"?i.error:r.kind==="transient"?r.error:i.error}export{G as RefreshTokenInvalidError,u as RefreshTransientError,F as authenticateWithDeviceFlow,ee as authenticateWithGoogle,b as buildDeviceAuthConfig,te as refreshIdToken,H as shouldUseDeviceFlow};
@@ -0,0 +1,2 @@
1
+ export declare function composeInstallBanner(hasAgentCli: boolean): string;
2
+ //# sourceMappingURL=postinstall-banner.d.ts.map
@@ -0,0 +1,2 @@
1
+ function n(o){const e=["","\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557","\u2551 \u25FC Borg MCP Installed \u25FC \u2551","\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",""];return o?e.push("Next step:"," borg setup",""):e.push("\u26A0 No agent CLI detected. Borg runs on top of Claude Code or Codex \u2014"," install one first:"," Claude Code: https://claude.ai/download"," Codex: https://developers.openai.com/codex","","Then run:"," borg setup",""),e.join(`
2
+ `)}export{n as composeInstallBanner};
@@ -2,7 +2,10 @@
2
2
  /**
3
3
  * Post-install script
4
4
  *
5
- * Detects local vs global installation and rejects local installs
5
+ * Detects local vs global installation and rejects local installs.
6
+ * gh#653 B1: also detects whether an agent CLI (Claude Code / Codex) is present
7
+ * and adjusts the "next step" banner so a user with no agent CLI is told to
8
+ * install one FIRST rather than being sent into `borg setup`'s dead-end.
6
9
  */
7
10
  export {};
8
11
  //# sourceMappingURL=postinstall.d.ts.map
@@ -1,9 +1,6 @@
1
1
  #!/usr/bin/env node
2
- const o=process.env.npm_config_global==="true";o||(console.error(`
2
+ import{detectCliAvailability as l,installedCliNames as e}from"./cli-platform.js";import{composeInstallBanner as r}from"./postinstall-banner.js";const n=process.env.npm_config_global==="true";n||(console.error(`
3
3
  \u25FC Error: borg must be installed globally
4
4
  `),console.error("Please install with:"),console.error(` npm install -g borgmcp
5
5
  `),console.error(`Local installation is not supported.
6
- `),process.exit(1)),console.log(`
7
- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`),console.log("\u2551 \u25FC Borg MCP Installed \u25FC \u2551"),console.log(`\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
8
- `),console.log("Next step:"),console.log(` borg setup
9
- `);
6
+ `),process.exit(1));let o=!0;try{o=e(l()).length>0}catch{o=!0}console.log(r(o));
package/dist/setup.js CHANGED
@@ -35,7 +35,7 @@ import h from"prompts";import e from"chalk";import g from"open";import u from"wh
35
35
  \u25FC You're all set on the Free tier: 1 cube, 3 agent sessions, 100 req/hr.
36
36
  `));break}}console.log(e.green.bold(`Setup complete!
37
37
  `)),console.log(e.yellow(`\u{1F504} Restart Claude Code/Codex (or open a new session) for the changes to take effect.
38
- `)),console.log(e.gray("\u25FC Next steps:")),console.log(e.gray('1. Run "borg" to start Claude Code or Codex with your cube')),console.log(e.gray(`2. Manage cubes and subscription at https://borgmcp.ai/dashboard
38
+ `)),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
39
39
  `))}async function p(){for(let n=0;n<24;n++){await new Promise(t=>setTimeout(t,5e3));try{if((await c()).hasAccess){console.log(e.green(`\u25FC Subscription activated!
40
40
  `));return}}catch{}}throw new Error("Timeout - Run setup again after subscribing")}P().catch(i=>{console.error(e.red(`
41
41
  \u25FC Setup failed: ${i.message}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
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",