borgmcp 1.0.19 → 1.0.21

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.
@@ -0,0 +1,4 @@
1
+ import type { DroneCandidate } from '../launch-all-discovery.js';
2
+ import type { LaunchAllDeps } from '../launch-all-deps.js';
3
+ export declare function runPastelistBackend(candidates: DroneCandidate[], borgPath: string, deps: LaunchAllDeps): void;
4
+ //# sourceMappingURL=launch-all-pastelist.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{buildLaunchCommand as a}from"../launch-all-command.js";function i(o,r,n){n.stdout(`# borg launch-all: open each worktree in a terminal window and run:
2
+
3
+ `);for(const t of o)n.stdout(a(t.worktreeDir,r)+`
4
+ `)}export{i as runPastelistBackend};
@@ -0,0 +1,12 @@
1
+ import type { DroneCandidate } from '../launch-all-discovery.js';
2
+ import type { LaunchAllDeps } from '../launch-all-deps.js';
3
+ export interface TmuxOpts {
4
+ sessionName: string;
5
+ borgPath: string;
6
+ /** 'attach' = attach-session; 'switch' = switch-client (nested tmux); 'none' = skip. */
7
+ attachMode: 'attach' | 'switch' | 'none';
8
+ /** ISO-8601 captured before the first send-keys (lock-marker launchedAt). */
9
+ launchedAtISO: string;
10
+ }
11
+ export declare function runTmuxBackend(candidates: DroneCandidate[], opts: TmuxOpts, deps: LaunchAllDeps): Promise<void>;
12
+ //# sourceMappingURL=launch-all-tmux.d.ts.map
@@ -0,0 +1 @@
1
+ import{buildLaunchCommand as h}from"../launch-all-command.js";import{writeLockMarker as d}from"../launch-all-locks.js";async function k(o,a,t){const{sessionName:e,borgPath:s,attachMode:c,launchedAtISO:u}=a,m=t.runSyncExitCode("tmux",["has-session","-t",e])===0;for(let r=0;r<o.length;r++){const n=o[r];let i;r===0&&!m?i=t.runSync("tmux",["new-session","-d","-P","-F","#{window_id}","-s",e,"-c",n.worktreeDir]).trim():i=t.runSync("tmux",["new-window","-P","-F","#{window_id}","-t",e,"-c",n.worktreeDir]).trim(),t.runSync("tmux",["rename-window","-t",i,n.droneLabel]);const w=h(n.worktreeDir,s,{keepOpenOnFail:!0});t.runSync("tmux",["send-keys","-t",i,w,"Enter"]),d(t,n.cubeId,n.droneLabel,n.worktreeDir,u)}c==="switch"?t.attachInteractive("tmux",["switch-client","-t",e]):c==="attach"&&t.attachInteractive("tmux",["attach-session","-t",e])}export{k as runTmuxBackend};
@@ -0,0 +1,9 @@
1
+ import type { DroneCandidate } from '../launch-all-discovery.js';
2
+ import type { LaunchAllDeps } from '../launch-all-deps.js';
3
+ export interface WindowsOpts {
4
+ borgPath: string;
5
+ platform: NodeJS.Platform;
6
+ launchedAtISO: string;
7
+ }
8
+ export declare function runWindowsBackend(candidates: DroneCandidate[], opts: WindowsOpts, deps: LaunchAllDeps): Promise<void>;
9
+ //# sourceMappingURL=launch-all-windows.d.ts.map
@@ -0,0 +1,12 @@
1
+ import{buildLaunchCommand as l}from"../launch-all-command.js";import{writeLockMarker as m}from"../launch-all-locks.js";function s(o){return o.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}class u extends Error{}function f(o,t,e){const a=e.pathExists("/Applications/iTerm.app"),i=e.pathExists("/Applications/Terminal.app");if(!a&&!i)throw new u(`borg launch-all: --mode windows requires a compatible terminal app.
2
+ Not found: iTerm.app, Terminal.app
3
+ Install iTerm2 (https://iterm2.com) or use --mode tmux (brew install tmux).
4
+ `);for(const n of o){const r=l(n.worktreeDir,t.borgPath),c=a?`tell application "iTerm"
5
+ tell current window to create tab with default profile command "${s(r)}"
6
+ end tell`:`tell application "Terminal"
7
+ do script "${s(r)}"
8
+ activate
9
+ end tell`;e.runSync("osascript",["-e",c]),m(e,n.cubeId,n.droneLabel,n.worktreeDir,t.launchedAtISO)}}function p(o,t,e){const a=e.getEnv("BORG_TERMINAL")||e.getEnv("TERMINAL"),i=["gnome-terminal","konsole","kitty","wezterm","xterm"];let n=a;if(!n){for(const r of i)if(e.runSyncExitCode("which",[r])===0){n=r;break}}if(!n)throw new u(`borg launch-all: --mode windows requires a terminal emulator.
10
+ Not found. Set $BORG_TERMINAL=<path> or use --mode tmux.
11
+ `);for(const r of o){const c=l(r.worktreeDir,t.borgPath,{keepOpenOnFail:!0});e.runSync(n,["-e","sh","-c",c]),m(e,r.cubeId,r.droneLabel,r.worktreeDir,t.launchedAtISO)}}const d=/[\x00-\x1f\x7f]/;async function b(o,t,e){const a=o.filter(i=>d.test(i.worktreeDir)?(e.stderr(`skipping ${i.droneLabel} (${JSON.stringify(i.worktreeDir)}): worktree path contains a control character \u2014 unsafe for --mode windows; use --mode tmux instead.
12
+ `),!1):!0);t.platform==="darwin"?f(a,t,e):p(a,t,e)}export{b as runWindowsBackend};
package/dist/claude.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as S}from"child_process";import{randomUUID as y}from"node:crypto";import{basename as A}from"node:path";import{createInterface as P}from"node:readline/promises";import s from"chalk";import{findProjectRoot as T,getActiveCube as R,inboxPathForDrone as I,setCodexWakeTarget as E}from"./cubes.js";import{handleVersionFlag as D,getPackageVersion as g}from"./version.js";import{isHelpFlag as O,setupHelpText as F,topLevelHelpText as N}from"./cli-help.js";import{runSpawn as H}from"./spawn.js";import{parseSyncArgs as M,runSync as B}from"./sync.js";import{parseAssimilateArgs as G}from"./parse-assimilate-args.js";import{runAssimilate as L}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as _}from"./assimilate-deps.js";import{setTerminalTitle as W}from"./terminal-title.js";import{initConsolePrefix as U,consolePrefix as r}from"./console-prefix.js";import{initDebugFromArgv as V}from"./debug.js";import{fetchLatestBorgmcpVersion as j,compareVersionsForStaleness as K}from"./stale-version-check.js";import{defaultCliChoiceDeps as Y,detectCliAvailability as v,installedCliNames as q,parseCliFlag as z,resolveCliChoice as X}from"./cli-platform.js";import{getRefreshToken as J,getIdToken as Q}from"./config.js";import{composeGetStarted as Z,shouldShowGetStarted as ee}from"./get-started.js";import{prepareCodexRemoteLaunch as re,withCodexCwdArg as oe,defaultCodexRemoteDeps as se}from"./codex-remote.js";import{findLoadedCodexThread as te}from"./codex-app-server.js";import{buildAgentKickoffPrompt as ie,recordCodexWakeTarget as ae,socketPathFromRemoteArgs as k}from"./codex-launch.js";import{codexBorgSessionConfigArgs as ne}from"./launch-gate.js";import{addCodexMcpServer as ce,addCodexSessionStartHook as le,addCodexUserPromptSubmitHook as de,addMcpServer as pe,addProjectSessionStartHook as me,addUserPromptSubmitHook as ue,isCodexMcpServerConfigured as fe,isMcpServerConfigured as ge,removeSessionStartHook as he}from"./config-utils.js";async function we(){V(process.argv),D(),await U();const n=(async()=>{if(!process.stderr.isTTY)return;const e=g(),a=await j();if(!a)return;const p=K(e,a);p.stale&&p.message&&process.stderr.write(`${r()}${p.message}
3
- `)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(N(g())),process.exit(0)),process.argv[2]==="setup"){O(process.argv[3])&&(process.stdout.write(F(g())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){const e=G(process.argv.slice(3));e.ok||(process.stderr.write(s.red(`${r()}\u25FC borg assimilate: ${e.error}
4
- `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const a=_(),p=await L({role:e.role,flags:e.flags},a);process.exit(p)}if(process.argv[2]==="spawn"){const e=await H();process.exit(e)}if(process.argv[2]==="sync"){const e=M(process.argv.slice(3));e.ok||(process.stderr.write(s.red(`${r()}\u25FC borg sync: ${e.error}
5
- `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const a=await B({},e.options);process.exit(a)}if(ee(await J()!==null,await Q()!==null)){const e=q(v()).length>0;process.stdout.write(Z(e)),process.exit(0)}const t=z(process.argv.slice(2));t.error&&(process.stderr.write(s.red(`${r()}\u25FC ${t.error}
6
- `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const b=async e=>{const a=P({input:process.stdin,output:process.stdout});try{return await a.question(e)}finally{a.close()}},o=await X(t.cli,Y(b,()=>process.stdin.isTTY===!0));xe();const c=t.rest,i=await R();W(i?{label:i.droneLabel,cubeName:i.name}:null,A(process.cwd()));const $=i&&o==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${I(i.cubeId,i.droneId)}\` 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. `:"";await Promise.race([n,new Promise(e=>setTimeout(e,2e3))]);const h=o==="codex"?`borg-wake-${y()}`:null;let m,w=[],u={...process.env,BORG_SESSION:"1"},l=null,d=null;if(o==="codex"&&!c.includes("--remote")){console.error(`${r()}${s.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await re(se());e.warning?(console.error(`${r()}${s.yellow(`warning: ${e.warning}`)}`),m="\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."):m="Codex wake-path capability check passed: remote-control socket established for this session.",w=e.args,u={...process.env,...e.env,BORG_SESSION:"1"},l=k(e.args),d=e.server?.cleanup??null}else o==="codex"&&c.includes("--remote")&&(m="Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg:regen manually when returning to the session.",l=k(c),l&&(u={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));const x=ie({cli:o,codexWakeNonce:h,monitorClause:$,codexWakePathClause:m});let f=[...c,x];o==="codex"&&(f=[...ne(),...w,...oe(f,process.cwd())]),console.error(`${r()}${s.blue(`\u25FC Launching ${o==="claude"?"Claude Code":"Codex"}\u2026`)}`);const C=S(o,f,{stdio:"inherit",shell:!1,env:u});o==="codex"&&i&&l&&ae({deps:{setCodexWakeTarget:E,findLoadedCodexThread:te},cubeId:i.cubeId,droneId:i.droneId,socketPath:l,passthroughArgs:c,previewNeedle:h??x.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),C.on("error",e=>{if(d)try{d()}catch{}e.code==="ENOENT"?(console.error(`${r()}${s.red(`
7
- \u25FC Failed to launch ${o}`)}`),console.error(`${r()}${s.gray(`Make sure ${o} is installed.
8
- `)}`)):console.error(`${r()}${s.red(`
9
- \u25FC Failed to launch ${o}: ${e.message}
10
- `)}`),process.exit(1)}),C.on("exit",e=>{if(d)try{d()}catch{}process.exit(e??0)})}function xe(){const n=v();if(n.claude)try{ge()||pe(),me(T(process.cwd())),he(),ue()}catch(t){console.error(`${r()}${s.yellow(`warning: Claude Code integration check failed: ${t?.message??t}`)}`)}if(n.codex)try{fe()||ce(),le(),de()}catch(t){console.error(`${r()}${s.yellow(`warning: Codex integration check failed: ${t?.message??t}`)}`)}}we().catch(n=>{console.error(`${r()}${s.red(`
11
- \u25FC Error: ${n.message}
2
+ import{spawn as S}from"child_process";import{randomUUID as y}from"node:crypto";import{basename as A}from"node:path";import{createInterface as P}from"node:readline/promises";import o from"chalk";import{findProjectRoot as T,getActiveCube as R,inboxPathForDrone as I,setCodexWakeTarget as D}from"./cubes.js";import{handleVersionFlag as E,getPackageVersion as g}from"./version.js";import{isHelpFlag as O,setupHelpText as F,topLevelHelpText as L}from"./cli-help.js";import{runSpawn as N}from"./spawn.js";import{parseSyncArgs as H,runSync as M}from"./sync.js";import{parseAssimilateArgs as B}from"./parse-assimilate-args.js";import{runAssimilate as G}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as _}from"./assimilate-deps.js";import{parseLaunchAllArgs as W}from"./parse-launch-all-args.js";import{runLaunchAll as U}from"./launch-all-cmd.js";import{buildDefaultLaunchAllDeps as V}from"./launch-all-deps.js";import{setTerminalTitle as j}from"./terminal-title.js";import{initConsolePrefix as K,consolePrefix as r}from"./console-prefix.js";import{initDebugFromArgv as Y}from"./debug.js";import{fetchLatestBorgmcpVersion as q,compareVersionsForStaleness as z}from"./stale-version-check.js";import{defaultCliChoiceDeps as X,detectCliAvailability as C,installedCliNames as J,parseCliFlag as Q,resolveCliChoice as Z}from"./cli-platform.js";import{getRefreshToken as ee,getIdToken as re}from"./config.js";import{composeGetStarted as oe,shouldShowGetStarted as se}from"./get-started.js";import{prepareCodexRemoteLaunch as te,withCodexCwdArg as ie,defaultCodexRemoteDeps as ae}from"./codex-remote.js";import{findLoadedCodexThread as ne}from"./codex-app-server.js";import{buildAgentKickoffPrompt as ce,recordCodexWakeTarget as le,socketPathFromRemoteArgs as b}from"./codex-launch.js";import{codexBorgSessionConfigArgs as de}from"./launch-gate.js";import{addCodexMcpServer as pe,addCodexSessionStartHook as me,addCodexUserPromptSubmitHook as ue,addMcpServer as fe,addProjectSessionStartHook as ge,addUserPromptSubmitHook as he,isCodexMcpServerConfigured as we,isMcpServerConfigured as xe,removeSessionStartHook as ve}from"./config-utils.js";async function Ce(){Y(process.argv),E(),await K();const c=(async()=>{if(!process.stderr.isTTY)return;const e=g(),t=await q();if(!t)return;const n=z(e,t);n.stale&&n.message&&process.stderr.write(`${r()}${n.message}
3
+ `)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(L(g())),process.exit(0)),process.argv[2]==="setup"){O(process.argv[3])&&(process.stdout.write(F(g())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){const e=B(process.argv.slice(3));e.ok||(process.stderr.write(o.red(`${r()}\u25FC borg assimilate: ${e.error}
4
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const t=_(),n=await G({role:e.role,flags:e.flags},t);process.exit(n)}if(process.argv[2]==="spawn"){const e=await N();process.exit(e)}if(process.argv[2]==="sync"){const e=H(process.argv.slice(3));e.ok||(process.stderr.write(o.red(`${r()}\u25FC borg sync: ${e.error}
5
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const t=await M({},e.options);process.exit(t)}if(process.argv[2]==="launch-all"){const e=W(process.argv.slice(3));e.ok||(process.stderr.write(o.red(`${r()}\u25FC borg launch-all: ${e.error}
6
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const t=V(),n=await U(e.args,t);process.exit(n)}if(se(await ee()!==null,await re()!==null)){const e=J(C()).length>0;process.stdout.write(oe(e)),process.exit(0)}const i=Q(process.argv.slice(2));i.error&&(process.stderr.write(o.red(`${r()}\u25FC ${i.error}
7
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const k=async e=>{const t=P({input:process.stdin,output:process.stdout});try{return await t.question(e)}finally{t.close()}},s=await Z(i.cli,X(k,()=>process.stdin.isTTY===!0));be();const l=i.rest,a=await R();j(a?{label:a.droneLabel,cubeName:a.name}:null,A(process.cwd()));const $=a&&s==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${I(a.cubeId,a.droneId)}\` 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. `:"";await Promise.race([c,new Promise(e=>setTimeout(e,2e3))]);const h=s==="codex"?`borg-wake-${y()}`:null;let m,w=[],u={...process.env,BORG_SESSION:"1"},d=null,p=null;if(s==="codex"&&!l.includes("--remote")){console.error(`${r()}${o.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await te(ae());e.warning?(console.error(`${r()}${o.yellow(`warning: ${e.warning}`)}`),m="\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."):m="Codex wake-path capability check passed: remote-control socket established for this session.",w=e.args,u={...process.env,...e.env,BORG_SESSION:"1"},d=b(e.args),p=e.server?.cleanup??null}else s==="codex"&&l.includes("--remote")&&(m="Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg:regen manually when returning to the session.",d=b(l),d&&(u={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));const x=ce({cli:s,codexWakeNonce:h,monitorClause:$,codexWakePathClause:m});let f=[...l,x];s==="codex"&&(f=[...de(),...w,...ie(f,process.cwd())]),console.error(`${r()}${o.blue(`\u25FC Launching ${s==="claude"?"Claude Code":"Codex"}\u2026`)}`);const v=S(s,f,{stdio:"inherit",shell:!1,env:u});s==="codex"&&a&&d&&le({deps:{setCodexWakeTarget:D,findLoadedCodexThread:ne},cubeId:a.cubeId,droneId:a.droneId,socketPath:d,passthroughArgs:l,previewNeedle:h??x.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),v.on("error",e=>{if(p)try{p()}catch{}e.code==="ENOENT"?(console.error(`${r()}${o.red(`
8
+ \u25FC Failed to launch ${s}`)}`),console.error(`${r()}${o.gray(`Make sure ${s} is installed.
9
+ `)}`)):console.error(`${r()}${o.red(`
10
+ \u25FC Failed to launch ${s}: ${e.message}
11
+ `)}`),process.exit(1)}),v.on("exit",e=>{if(p)try{p()}catch{}process.exit(e??0)})}function be(){const c=C();if(c.claude)try{xe()||fe(),ge(T(process.cwd())),ve(),he()}catch(i){console.error(`${r()}${o.yellow(`warning: Claude Code integration check failed: ${i?.message??i}`)}`)}if(c.codex)try{we()||pe(),me(),ue()}catch(i){console.error(`${r()}${o.yellow(`warning: Codex integration check failed: ${i?.message??i}`)}`)}}Ce().catch(c=>{console.error(`${r()}${o.red(`
12
+ \u25FC Error: ${c.message}
12
13
  `)}`),process.exit(1)});
package/dist/cli-help.js CHANGED
@@ -13,6 +13,7 @@ Usage:
13
13
  borg assimilate [role] Join a cube (creates one if needed)
14
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
+ borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)
16
17
  borg --cli claude|codex Choose agent CLI for this project
17
18
  borg --version Show installed version
18
19
 
@@ -1 +1 @@
1
- import{mkdirSync as w,chmodSync as k,readdirSync as v,rmSync as C,writeFileSync as b,readFileSync as y}from"node:fs";import{homedir as g}from"node:os";import{join as u}from"node:path";import{randomBytes as S}from"node:crypto";import{spawn as A}from"node:child_process";import{CodexAppServerClient as $}from"./codex-app-server.js";const E=u(g(),".config","borgmcp","codex-remote");function F(e,t){return R(e)?e:["--cd",t,...e]}function R(e){return e.some(t=>t==="--cd"||t.startsWith("--cd=")||t==="-C")}function x(e){try{return process.kill(e,0),!0}catch(t){return t?.code==="EPERM"}}function O(e,t={}){if(!e)return null;const c=t.isAlive??x,i=t.readPidFile??(o=>y(o,"utf-8")),a=e.replace(/\.sock$/,".pid");try{const o=Number.parseInt(i(a).trim(),10);return Number.isNaN(o)?null:c(o)}catch{return null}}function s(e){try{C(e,{force:!0})}catch{}}function M(e,t){let c;try{c=v(e)}catch{return}for(const i of c){if(!i.endsWith(".pid"))continue;const a=u(e,i),o=u(e,i.replace(/\.pid$/,".sock"));let r;try{r=Number.parseInt(y(a,"utf-8").trim(),10)}catch{s(a);continue}(Number.isNaN(r)||!t(r))&&(s(o),s(a))}}function p(e){return{args:[],env:{},warning:e}}async function B(e){const t=e.runtimeDir??E,c=e.isAlive??x,i=e.readyTimeoutMs??8e3,a=e.pollIntervalMs??250;try{w(t,{recursive:!0,mode:448}),k(t,448),M(t,c)}catch(n){return p(`Codex remote-wake disabled: could not prepare ${t} (${n?.message??n}); run borg:regen manually.`)}const o=(e.socketId??(()=>S(16).toString("hex")))(),r=u(t,`${o}.sock`),m=u(t,`${o}.pid`);let l;try{l=e.spawnAppServer(r)}catch(n){return s(r),p(`Codex remote-wake disabled: could not start \`codex app-server\` (${n?.message??n}) \u2014 is Codex installed + up to date? This session only wakes on the ~30min /loop fallback; run borg:regen manually.`)}if(l.pid!=null)try{b(m,String(l.pid))}catch{}const f=()=>{try{l.kill()}catch{}s(r),s(m)},h=Math.max(1,Math.ceil(i/a));let d=!1;for(let n=0;n<h&&!d;n++){try{d=await e.probeReady(r)}catch{d=!1}!d&&n<h-1&&await e.sleep(a)}return d?{args:["--remote",`unix://${r}`],env:{BORG_CODEX_REMOTE_WAKE:"1"},server:{pid:l.pid,socketPath:r,cleanup:f}}:(f(),p(`Codex remote-wake disabled: could not reach a Codex app-server at ${r} within ${i}ms (is Codex up to date? \`codex app-server --listen\` is required). This session only wakes on the ~30min /loop fallback \u2014 run borg:regen manually when you return.`))}function L(){return{spawnAppServer:e=>{const t=A("codex",["app-server","--listen",`unix://${e}`],{stdio:"ignore",shell:!1});return{pid:t.pid,kill:()=>{try{t.kill()}catch{}}}},probeReady:async e=>{const t=new $(e);try{return await t.connect(),await t.loadedThreadIds(),!0}catch{return!1}finally{try{t.close()}catch{}}},sleep:e=>new Promise(t=>setTimeout(t,e))}}export{E as DEFAULT_CODEX_REMOTE_DIR,O as checkCodexBridgeHealthy,L as defaultCodexRemoteDeps,x as defaultIsAlive,B as prepareCodexRemoteLaunch,F as withCodexCwdArg};
1
+ import{mkdirSync as w,chmodSync as v,readdirSync as k,rmSync as C,writeFileSync as g,readFileSync as y}from"node:fs";import{homedir as S}from"node:os";import{join as u}from"node:path";import{randomBytes as b}from"node:crypto";import{spawn as A}from"node:child_process";import{CodexAppServerClient as E}from"./codex-app-server.js";import{codexBorgSessionConfigArgs as R,BORG_SESSION_ENV as $}from"./launch-gate.js";const N=u(S(),".config","borgmcp","codex-remote");function L(e,r){return I(e)?e:["--cd",r,...e]}function I(e){return e.some(r=>r==="--cd"||r.startsWith("--cd=")||r==="-C")}function x(e){try{return process.kill(e,0),!0}catch(r){return r?.code==="EPERM"}}function W(e,r={}){if(!e)return null;const a=r.isAlive??x,i=r.readPidFile??(o=>y(o,"utf-8")),c=e.replace(/\.sock$/,".pid");try{const o=Number.parseInt(i(c).trim(),10);return Number.isNaN(o)?null:a(o)}catch{return null}}function s(e){try{C(e,{force:!0})}catch{}}function M(e,r){let a;try{a=k(e)}catch{return}for(const i of a){if(!i.endsWith(".pid"))continue;const c=u(e,i),o=u(e,i.replace(/\.pid$/,".sock"));let t;try{t=Number.parseInt(y(c,"utf-8").trim(),10)}catch{s(c);continue}(Number.isNaN(t)||!r(t))&&(s(o),s(c))}}function p(e){return{args:[],env:{},warning:e}}async function G(e){const r=e.runtimeDir??N,a=e.isAlive??x,i=e.readyTimeoutMs??8e3,c=e.pollIntervalMs??250;try{w(r,{recursive:!0,mode:448}),v(r,448),M(r,a)}catch(n){return p(`Codex remote-wake disabled: could not prepare ${r} (${n?.message??n}); run borg:regen manually.`)}const o=(e.socketId??(()=>b(16).toString("hex")))(),t=u(r,`${o}.sock`),m=u(r,`${o}.pid`);let l;try{l=e.spawnAppServer(t)}catch(n){return s(t),p(`Codex remote-wake disabled: could not start \`codex app-server\` (${n?.message??n}) \u2014 is Codex installed + up to date? This session only wakes on the ~30min /loop fallback; run borg:regen manually.`)}if(l.pid!=null)try{g(m,String(l.pid))}catch{}const f=()=>{try{l.kill()}catch{}s(t),s(m)},h=Math.max(1,Math.ceil(i/c));let d=!1;for(let n=0;n<h&&!d;n++){try{d=await e.probeReady(t)}catch{d=!1}!d&&n<h-1&&await e.sleep(c)}return d?{args:["--remote",`unix://${t}`],env:{BORG_CODEX_REMOTE_WAKE:"1"},server:{pid:l.pid,socketPath:t,cleanup:f}}:(f(),p(`Codex remote-wake disabled: could not reach a Codex app-server at ${t} within ${i}ms (is Codex up to date? \`codex app-server --listen\` is required). This session only wakes on the ~30min /loop fallback \u2014 run borg:regen manually when you return.`))}function X(){return{spawnAppServer:e=>{const r=A("codex",["app-server",...R(),"--listen",`unix://${e}`],{stdio:"ignore",shell:!1,env:{...process.env,[$]:"1"}});return{pid:r.pid,kill:()=>{try{r.kill()}catch{}}}},probeReady:async e=>{const r=new E(e);try{return await r.connect(),await r.loadedThreadIds(),!0}catch{return!1}finally{try{r.close()}catch{}}},sleep:e=>new Promise(r=>setTimeout(r,e))}}export{N as DEFAULT_CODEX_REMOTE_DIR,W as checkCodexBridgeHealthy,X as defaultCodexRemoteDeps,x as defaultIsAlive,G as prepareCodexRemoteLaunch,L as withCodexCwdArg};
package/dist/cubes.d.ts CHANGED
@@ -63,6 +63,22 @@ export declare function activeCubeWithFreshRegenIdentity(active: ActiveCube, res
63
63
  */
64
64
  export declare function clearActiveCube(): Promise<void>;
65
65
  export declare function getProjectCliPreference(): Promise<BorgCli | null>;
66
+ /**
67
+ * gh#556 Part 2 — like getProjectCliPreference, but keyed on an arbitrary
68
+ * worktree dir (launch-all reads the saved CLI preference for EACH discovered
69
+ * worktree, not just cwd). Returns null if no preference is saved for that path.
70
+ */
71
+ export declare function getProjectCliPreferenceForPath(dir: string): Promise<BorgCli | null>;
72
+ /**
73
+ * gh#556 Part 2 — returns all persisted project identities from cubes.json.
74
+ * Used by `borg launch-all` to enumerate drones across all known worktrees
75
+ * (scheme-agnostic — covers both old sibling paths and new ~/.borg paths).
76
+ * Returns an empty array if the file is absent or malformed.
77
+ */
78
+ export declare function readAllProjectIdentities(): Promise<Array<{
79
+ projectPath: string;
80
+ cube: ActiveCube;
81
+ }>>;
66
82
  export declare function setProjectCliPreference(cli: BorgCli): Promise<void>;
67
83
  export declare function setCodexWakeTarget(cubeId: string, droneId: string, target: Omit<CodexWakeTargetRecord, 'updatedAt'>): Promise<void>;
68
84
  export declare function getCodexWakeTarget(cubeId: string, droneId: string): Promise<CodexWakeTargetRecord | null>;
package/dist/cubes.js CHANGED
@@ -1,4 +1,4 @@
1
- import{existsSync as E}from"node:fs";import{mkdir as l,readFile as f,writeFile as p,unlink as m}from"node:fs/promises";import{homedir as C}from"node:os";import{dirname as c,join as o,resolve as d}from"node:path";const s=o(C(),".config","borgmcp"),a=o(s,"cubes.json"),y=o(s,"launch.json"),w=o(s,"codex-wake-targets.json"),F=o(s,"inboxes");function i(t=process.cwd()){let e=d(t);for(;;){if(E(o(e,".git")))return e;const r=c(e);if(r===e)return d(t);e=r}}const u=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function _(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return o(F,t,`${e}.log`)}function N(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function g(){let t;try{t=await f(a,"utf8")}catch(r){if(r?.code==="ENOENT")return null;throw r}let e;try{e=JSON.parse(t)}catch{return null}return N(e)?e:null}async function j(t){await l(c(a),{recursive:!0}),await p(a,JSON.stringify(t,null,2)+`
2
- `,{mode:384})}function I(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function h(){let t;try{t=await f(y,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return I(e)?e:null}catch{return null}}async function O(t){await l(c(y),{recursive:!0}),await p(y,JSON.stringify(t,null,2)+`
3
- `,{mode:384})}function x(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return`${t}:${e}`}function k(t){return t!==null&&typeof t=="object"&&typeof t.targets=="object"&&t.targets!==null&&!Array.isArray(t.targets)}async function b(){let t;try{t=await f(w,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return k(e)?e:null}catch{return null}}async function A(t){await l(c(w),{recursive:!0}),await p(w,JSON.stringify(t,null,2)+`
4
- `,{mode:384})}async function $(){const t=await g();if(!t)return null;const e=i(),r=t.projects[e];return!r||typeof r.cubeId!="string"||!r.cubeId||typeof r.droneId!="string"||!r.droneId?null:r}async function v(t){const e=await g()??{projects:{}};e.projects[i()]=t,await j(e)}function P(t,e){const r=e.cube?.name??t.name,n=e.drone?.label??t.droneLabel;return r===t.name&&n===t.droneLabel?t:{...t,name:r,droneLabel:n}}async function D(){const t=await g();if(!t)return;const e=i();if(e in t.projects){if(delete t.projects[e],Object.keys(t.projects).length===0){try{await m(a)}catch(r){if(r?.code!=="ENOENT")throw r}return}await j(t)}}async function J(){const t=await h();if(!t)return null;const e=t.projects[i()];return e?.cli==="claude"||e?.cli==="codex"?e.cli:null}async function R(t){const e=await h()??{projects:{}};e.projects[i()]={cli:t},await O(e)}async function U(t,e,r){const n=await b()??{targets:{}};n.targets[x(t,e)]={...r,updatedAt:new Date().toISOString()},await A(n)}async function B(t,e){const r=await b();if(!r)return null;const n=r.targets[x(t,e)];return!n||typeof n.threadId!="string"||typeof n.socketPath!="string"?null:n}export{P as activeCubeWithFreshRegenIdentity,D as clearActiveCube,i as findProjectRoot,$ as getActiveCube,B as getCodexWakeTarget,J as getProjectCliPreference,_ as inboxPathForDrone,v as setActiveCube,U as setCodexWakeTarget,R as setProjectCliPreference};
1
+ import{existsSync as E}from"node:fs";import{mkdir as f,readFile as p,writeFile as y,unlink as m}from"node:fs/promises";import{homedir as C}from"node:os";import{dirname as c,join as o,resolve as j}from"node:path";const a=o(C(),".config","borgmcp"),s=o(a,"cubes.json"),w=o(a,"launch.json"),d=o(a,"codex-wake-targets.json"),I=o(a,"inboxes");function i(t=process.cwd()){let e=j(t);for(;;){if(E(o(e,".git")))return e;const r=c(e);if(r===e)return j(t);e=r}}const u=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function W(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return o(I,t,`${e}.log`)}function F(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function l(){let t;try{t=await p(s,"utf8")}catch(r){if(r?.code==="ENOENT")return null;throw r}let e;try{e=JSON.parse(t)}catch{return null}return F(e)?e:null}async function h(t){await f(c(s),{recursive:!0}),await y(s,JSON.stringify(t,null,2)+`
2
+ `,{mode:384})}function N(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function g(){let t;try{t=await p(w,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return N(e)?e:null}catch{return null}}async function O(t){await f(c(w),{recursive:!0}),await y(w,JSON.stringify(t,null,2)+`
3
+ `,{mode:384})}function x(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return`${t}:${e}`}function A(t){return t!==null&&typeof t=="object"&&typeof t.targets=="object"&&t.targets!==null&&!Array.isArray(t.targets)}async function b(){let t;try{t=await p(d,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return A(e)?e:null}catch{return null}}async function k(t){await f(c(d),{recursive:!0}),await y(d,JSON.stringify(t,null,2)+`
4
+ `,{mode:384})}async function _(){const t=await l();if(!t)return null;const e=i(),r=t.projects[e];return!r||typeof r.cubeId!="string"||!r.cubeId||typeof r.droneId!="string"||!r.droneId?null:r}async function $(t){const e=await l()??{projects:{}};e.projects[i()]=t,await h(e)}function v(t,e){const r=e.cube?.name??t.name,n=e.drone?.label??t.droneLabel;return r===t.name&&n===t.droneLabel?t:{...t,name:r,droneLabel:n}}async function D(){const t=await l();if(!t)return;const e=i();if(e in t.projects){if(delete t.projects[e],Object.keys(t.projects).length===0){try{await m(s)}catch(r){if(r?.code!=="ENOENT")throw r}return}await h(t)}}async function J(){const t=await g();if(!t)return null;const e=t.projects[i()];return e?.cli==="claude"||e?.cli==="codex"?e.cli:null}async function R(t){const e=await g();if(!e)return null;const r=e.projects[i(t)];return r?.cli==="claude"||r?.cli==="codex"?r.cli:null}async function U(){const t=await l();return t?Object.entries(t.projects).filter(([,e])=>e!==null&&typeof e=="object"&&typeof e.cubeId=="string"&&e.cubeId.length>0&&typeof e.droneId=="string"&&e.droneId.length>0).map(([e,r])=>({projectPath:e,cube:r})):[]}async function B(t){const e=await g()??{projects:{}};e.projects[i()]={cli:t},await O(e)}async function K(t,e,r){const n=await b()??{targets:{}};n.targets[x(t,e)]={...r,updatedAt:new Date().toISOString()},await k(n)}async function X(t,e){const r=await b();if(!r)return null;const n=r.targets[x(t,e)];return!n||typeof n.threadId!="string"||typeof n.socketPath!="string"?null:n}export{v as activeCubeWithFreshRegenIdentity,D as clearActiveCube,i as findProjectRoot,_ as getActiveCube,X as getCodexWakeTarget,J as getProjectCliPreference,R as getProjectCliPreferenceForPath,W as inboxPathForDrone,U as readAllProjectIdentities,$ as setActiveCube,K as setCodexWakeTarget,B as setProjectCliPreference};
@@ -0,0 +1,11 @@
1
+ import type { LaunchAllArgs } from './parse-launch-all-args.js';
2
+ import type { LaunchAllDeps } from './launch-all-deps.js';
3
+ export interface RunLaunchAllOptions {
4
+ /** Injectable clock/sleep for deterministic reconciliation tests. */
5
+ now?: () => number;
6
+ sleep?: (ms: number) => Promise<void>;
7
+ nowISO?: () => string;
8
+ borgPath?: string;
9
+ }
10
+ export declare function runLaunchAll(args: LaunchAllArgs, deps: LaunchAllDeps, opts?: RunLaunchAllOptions): Promise<number>;
11
+ //# sourceMappingURL=launch-all-cmd.d.ts.map
@@ -0,0 +1,29 @@
1
+ import{discoverDroneCandidates as y}from"./launch-all-discovery.js";import{resolveBorgPath as v}from"./launch-all-command.js";import{sweepStaleLocks as x,isLockLive as I}from"./launch-all-locks.js";import{runTmuxBackend as S}from"./backends/launch-all-tmux.js";import{runWindowsBackend as A}from"./backends/launch-all-windows.js";import{runPastelistBackend as L}from"./backends/launch-all-pastelist.js";const $=`borg launch-all: tmux not found.
2
+ macOS: brew install tmux
3
+ Debian: sudo apt install tmux
4
+ Fedora: sudo dnf install tmux
5
+ `;function T(t){try{return t.runSync("tmux",["-V"]),!0}catch{return!1}}function D(t){try{return/microsoft|wsl/i.test(t.runSync("uname",["-r"]))}catch{return!1}}function N(t){return/^drone-\d+$/i.test(t)||t.toLowerCase()==="drone"}async function C(t,e){if(t.cubeName!==void 0){const s=(await e.readAllProjectIdentities()).filter(l=>l.cube.name===t.cubeName);return s.length===0?{error:`no cube named '${t.cubeName}' found in cubes.json \u2014 has any drone assimilated into it?`}:{cubeId:s[0].cube.cubeId,name:t.cubeName}}const o=await e.getActiveCube();return o?{cubeId:o.cubeId,name:o.name}:{error:"no active cube in this directory; run `borg assimilate` first, or pass a cube name explicitly"}}function O(t,e){const o=t.flags.mode;if(e.platform()==="win32"&&!D(e))return e.stderr(`native Windows is not supported for interactive launch; using pastelist mode instead (WSL + tmux is the recommended Windows path)
6
+ `),{backend:"pastelist"};if(o==="windows")return{backend:"windows"};if(o==="pastelist")return{backend:"pastelist"};const s=T(e);return o==="tmux"?s?{backend:"tmux"}:{hardFail:$}:s?{backend:"tmux"}:(e.stderr($+`Falling back to pastelist mode (paste the commands below).
7
+ `),{backend:"pastelist"})}function W(t,e){return t.flags.noAttach||!e.isTTY()?"none":e.getEnv("TMUX")?"switch":"attach"}function F(t,e){e.stdout(` tmux attach -t ${t} # re-attach later
8
+ `),e.stdout(` tmux list-windows -t ${t} # list all drone windows
9
+ `),e.stdout(` tmux kill-session -t ${t} # stop all drones
10
+ `)}function P(t){return`borg-${t.replace(/[^a-zA-Z0-9_-]/g,"-")}`}async function B(t,e,o,f,s,l=Date.now,d=a=>new Promise(i=>setTimeout(i,a))){const a=new Map;for(const u of s)a.set(u,"unconfirmed");const i=l()+1e4;for(let u=0;u<20&&!(l()>=i);u++){let r;try{r=await t.getRoster(e,o,f)}catch{break}for(const c of r.drones)a.get(c.id)==="unconfirmed"&&c.seen_since===!0&&a.set(c.id,"verified");if([...a.values()].every(c=>c==="verified"))break;await d(500)}return a}async function p(t,e,o={}){const f=o.now??Date.now,s=o.nowISO??(()=>new Date().toISOString()),l=o.borgPath??v(),d=await C(t,e);if("error"in d)return e.stderr(`borg launch-all: ${d.error}
11
+ `),1;const{cubeId:a,name:i}=d;x(e,a,f());const u=await y({targetCubeId:a,only:t.flags.only},e);if(u.length===0)return t.flags.only!==void 0?(e.stdout(`No worktrees matched --only '${t.flags.only}' for cube '${i}'
12
+ `),N(t.flags.only)||e.stderr(`note: --only '${t.flags.only}' is matched best-effort by drone label; role-name matching needs a drone session and is not available here.
13
+ `)):e.stdout(`No worktrees found for cube '${i}' \u2014 have you run \`borg assimilate --worktree\` to create any drone seats?
14
+ `),0;const r=[];for(const n of u){const h=I(e,a,n.worktreeDir,f());if(h.live&&!t.flags.force){e.stderr(`skipping ${n.droneLabel} (${n.worktreeDir}): appears live. Use --force to re-launch.
15
+ `);continue}h.live&&t.flags.force&&e.stderr(`--force: re-launching ${n.droneLabel} (${n.worktreeDir}); the running session's token will be displaced.
16
+ `),r.push(n)}if(r.length===0)return e.stdout(`All ${u.length} drone(s) for cube '${i}' appear live; nothing to launch (use --force to re-launch).
17
+ `),0;if(t.flags.dryRun){e.stdout(`borg launch-all (dry-run): would launch ${r.length} drone(s) for cube '${i}':
18
+ `);for(const n of r)e.stdout(` ${n.droneLabel} ${n.worktreeDir}
19
+ `);return 0}if(r.length>6&&!t.flags.yes&&(await e.prompt(`About to launch ${r.length} drones for cube '${i}'. Continue? [y/N]: `)).trim().toLowerCase()!=="y")return e.stdout(`Aborted.
20
+ `),0;const c=O(t,e);if("hardFail"in c)return e.stderr(c.hardFail),1;const m=P(i),b=s();try{if(c.backend==="tmux"){const n=W(t,e);await S(r,{sessionName:m,borgPath:l,attachMode:n,launchedAtISO:b},e),n==="none"&&(e.isTTY()||e.stderr(`Launching in detached mode \u2014 stdout is non-TTY. Attach manually with: tmux attach -t ${m}
21
+ `),F(m,e))}else if(c.backend==="windows")await A(r,{borgPath:l,platform:e.platform(),launchedAtISO:b},e);else return L(r,l,e),0}catch(n){return e.stderr(`borg launch-all: ${n instanceof Error?n.message:String(n)}
22
+ `),1}const k=r[0].sessionToken,g=r[0].apiUrl;let w=null;k&&g?w=await B(e,k,g,b,r.map(n=>n.droneId),o.now,o.sleep):e.stderr(`roster reconciliation skipped \u2014 no session token available
23
+ `),e.stdout(`
24
+ borg launch-all: launched ${r.length} drones for cube '${i}'
25
+
26
+ `);for(const n of r){const h=w?w.get(n.droneId)==="verified"?"VERIFIED":"unconfirmed (may still be joining)":"launched";e.stdout(` ${n.droneLabel} ${n.worktreeDir} ${h}
27
+ `)}return e.stdout(`
28
+ Attach: tmux attach -t ${m}
29
+ `),0}export{p as runLaunchAll};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The borg binary that invoked launch-all (spec §5.1). `process.argv[1]` is the
3
+ * absolute path to the running borg script — deterministic, independent of $PATH
4
+ * inside the spawned window's shell (npm link / global / local .bin all work).
5
+ */
6
+ export declare function resolveBorgPath(): string;
7
+ /**
8
+ * The shell command run inside each worktree's window/tab.
9
+ * `keepOpenOnFail` wraps a `|| read` pause so a failed assimilate doesn't close
10
+ * the tmux window before the operator reads the error (tmux convenience only;
11
+ * the pastelist backend omits it — the operator owns their own shell).
12
+ */
13
+ export declare function buildLaunchCommand(worktreeDir: string, borgPath: string, opts?: {
14
+ keepOpenOnFail?: boolean;
15
+ }): string;
16
+ //# sourceMappingURL=launch-all-command.d.ts.map
@@ -0,0 +1 @@
1
+ import{shellEscape as r}from"./shell-escape.js";function n(){return process.argv[1]}function i(o,s,t={}){const e=`cd ${r(o)} && ${r(s)} assimilate --here`;return t.keepOpenOnFail?`${e} || { echo "borg assimilate failed \u2014 press Enter to close"; read _; }`:e}export{i as buildLaunchCommand,n as resolveBorgPath};
@@ -0,0 +1,83 @@
1
+ import type { ActiveCube } from './cubes.js';
2
+ /** Subprocess runner — sync, returns stdout, THROWS on non-zero exit or ENOENT. */
3
+ export type RunSyncFn = (cmd: string, args: string[]) => string;
4
+ export interface LaunchAllDeps {
5
+ /** Subprocess — sync, throws on non-zero exit (git, tmux -V, ...). */
6
+ runSync: (cmd: string, args: string[], opts?: {
7
+ cwd?: string;
8
+ }) => string;
9
+ /** Subprocess — sync, returns exit code WITHOUT throwing (tmux has-session). */
10
+ runSyncExitCode: (cmd: string, args: string[]) => number;
11
+ /**
12
+ * Interactive subprocess with INHERITED stdio (terminal handover) — used for
13
+ * `tmux attach-session` / `switch-client`, where a captured stdout would not
14
+ * render the TUI. Spec §10's capture-runSync cannot interactively attach; this
15
+ * seam completes that (real: spawnSync stdio:'inherit').
16
+ */
17
+ attachInteractive: (cmd: string, args: string[]) => void;
18
+ /** Absolute path of the current working directory. */
19
+ cwd: () => string;
20
+ /** True iff the path exists on disk. */
21
+ pathExists: (p: string) => boolean;
22
+ /** $HOME / os.homedir(). */
23
+ homedir: () => string;
24
+ /** mkdir -p (recursive; no chmod of existing parents). */
25
+ mkdirp: (dir: string) => void;
26
+ /** Read a file; returns null on ENOENT (never throws for absence). */
27
+ readFileOpt: (p: string) => string | null;
28
+ /** Write a file (mode default 0o600). */
29
+ writeFile: (p: string, content: string, mode?: number) => void;
30
+ /** Unlink a file; does NOT throw on ENOENT. */
31
+ unlinkOpt: (p: string) => void;
32
+ /** mtime in ms, or null if absent. */
33
+ statMtime: (p: string) => number | null;
34
+ /** Directory entries, or [] if absent. */
35
+ listDir: (p: string) => string[];
36
+ /** Cached auth (OAuth/user token) for roster reconciliation + role lookup. */
37
+ getCachedAuth: () => Promise<{
38
+ token: string;
39
+ apiUrl: string;
40
+ } | null>;
41
+ /** Roster call (wraps getRoster from remote-client.ts). */
42
+ getRoster: (token: string, apiUrl: string, since?: string) => Promise<{
43
+ drones: Array<{
44
+ id: string;
45
+ seen_since?: boolean;
46
+ }>;
47
+ }>;
48
+ /** getCube for --only tier-2 role-name resolution (best-effort). */
49
+ getCube: (apiUrl: string, token: string, cubeId: string) => Promise<{
50
+ id: string;
51
+ name: string;
52
+ roles: Array<{
53
+ id: string;
54
+ name: string;
55
+ }>;
56
+ }>;
57
+ /** Saved CLI preference for a worktree path (launch.json). */
58
+ getCliPreferenceForPath: (projectPath: string) => Promise<'claude' | 'codex' | null>;
59
+ /** All persisted project identities from cubes.json. */
60
+ readAllProjectIdentities: () => Promise<Array<{
61
+ projectPath: string;
62
+ cube: ActiveCube;
63
+ }>>;
64
+ /** findProjectRoot (cubes.ts export). */
65
+ findProjectRoot: (dir: string) => string;
66
+ /** Active cube for the cwd (cubes.ts getActiveCube), null if none. */
67
+ getActiveCube: () => Promise<ActiveCube | null>;
68
+ /** Interactive confirmation prompt. */
69
+ prompt: (message: string) => Promise<string>;
70
+ /** TTY check (stdin). */
71
+ isTTY: () => boolean;
72
+ /** Environment variable accessor (e.g. $BORG_TERMINAL, $TMUX). */
73
+ getEnv: (name: string) => string | undefined;
74
+ /** process.platform (injectable for native-Windows/WSL backend-selection tests). */
75
+ platform: () => NodeJS.Platform;
76
+ /** stderr writer. */
77
+ stderr: (line: string) => void;
78
+ /** stdout writer. */
79
+ stdout: (line: string) => void;
80
+ }
81
+ /** Real-IO factory wiring production modules (spec §10). Test code stubs LaunchAllDeps directly. */
82
+ export declare function buildDefaultLaunchAllDeps(): LaunchAllDeps;
83
+ //# sourceMappingURL=launch-all-deps.d.ts.map
@@ -0,0 +1 @@
1
+ import{spawnSync as o}from"node:child_process";import{existsSync as n,mkdirSync as s,readFileSync as c,writeFileSync as u,unlinkSync as a,statSync as d,readdirSync as l}from"node:fs";import{homedir as p}from"node:os";import{createInterface as m}from"node:readline/promises";import{readAllProjectIdentities as f,getProjectCliPreferenceForPath as y,findProjectRoot as h,getActiveCube as g}from"./cubes.js";import{getRoster as w,getCube as S,getValidToken as P,API_URL as A}from"./remote-client.js";function I(){return{runSync:(t,r,e)=>{const i=o(t,r,{encoding:"utf-8",cwd:e?.cwd});if(i.error)throw i.error;if(i.status!==0)throw new Error(`${t} exited ${i.status}: ${(i.stderr??"").toString().trim()}`);return(i.stdout??"").toString()},runSyncExitCode:(t,r)=>o(t,r,{encoding:"utf-8"}).status??1,attachInteractive:(t,r)=>{o(t,r,{stdio:"inherit"})},cwd:()=>process.cwd(),pathExists:t=>n(t),homedir:()=>p(),mkdirp:t=>{s(t,{recursive:!0})},readFileOpt:t=>{try{return c(t,"utf-8")}catch{return null}},writeFile:(t,r,e)=>{u(t,r,{mode:e??384})},unlinkOpt:t=>{try{a(t)}catch{}},statMtime:t=>{try{return d(t).mtimeMs}catch{return null}},listDir:t=>{try{return l(t)}catch{return[]}},getCachedAuth:async()=>{try{return{token:await P(),apiUrl:A}}catch{return null}},getRoster:(t,r,e)=>w(t,r,e),getCube:(t,r,e)=>S(e),getCliPreferenceForPath:t=>y(t),readAllProjectIdentities:()=>f(),findProjectRoot:t=>h(t),getActiveCube:()=>g(),prompt:async t=>{const r=m({input:process.stdin,output:process.stdout});try{return(await r.question(t)).trim()}finally{r.close()}},isTTY:()=>process.stdin.isTTY===!0,getEnv:t=>process.env[t],platform:()=>process.platform,stderr:t=>{process.stderr.write(t)},stdout:t=>{process.stdout.write(t)}}}export{I as buildDefaultLaunchAllDeps};
@@ -0,0 +1,33 @@
1
+ import type { LaunchAllDeps, RunSyncFn } from './launch-all-deps.js';
2
+ export interface DroneCandidate {
3
+ worktreeDir: string;
4
+ cubeId: string;
5
+ droneId: string;
6
+ droneLabel: string;
7
+ sessionToken: string;
8
+ apiUrl: string;
9
+ }
10
+ /**
11
+ * --only TIER-1 (local, no server call): exact case-insensitive droneLabel match,
12
+ * OR droneLabel prefix match (`--only drone` matches `drone-1`, `drone-2`, ...).
13
+ * Tier-2 (role-name) matching is best-effort in the orchestrator (spec §8.4).
14
+ */
15
+ export declare function matchesOnlyLabel(droneLabel: string, only: string): boolean;
16
+ /**
17
+ * Enumerate the LINKED worktree paths from `git worktree list --porcelain`,
18
+ * dropping the main worktree (always block[0]). Throws a user-readable error if
19
+ * the command fails (not inside a git repo).
20
+ */
21
+ export declare function enumerateLinkedWorktrees(runSync: RunSyncFn): string[];
22
+ export interface DiscoverOpts {
23
+ targetCubeId: string;
24
+ /** --only filter (tier-1 label match applied here). */
25
+ only?: string;
26
+ }
27
+ /**
28
+ * Full discovery pipeline (spec §3.5): enumerate → cubes.json lookup → filter
29
+ * (dir-present / has-entry / cubeId-match / UUID-valid / --only) → candidates in
30
+ * stable porcelain order.
31
+ */
32
+ export declare function discoverDroneCandidates(opts: DiscoverOpts, deps: LaunchAllDeps): Promise<DroneCandidate[]>;
33
+ //# sourceMappingURL=launch-all-discovery.d.ts.map
@@ -0,0 +1,4 @@
1
+ const u=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function c(o){return u.test(o)}function d(o,t){const i=o.toLowerCase(),e=t.toLowerCase();return i===e||i.startsWith(e)}function l(o){let t;try{t=o("git",["worktree","list","--porcelain"])}catch(e){throw new Error(`launch-all: git worktree list failed \u2014 must be run from inside a git repository
2
+ (inner: ${e instanceof Error?e.message:String(e)})`)}return t.trim().split(/\n\n+/).slice(1).map(e=>{const s=e.match(/^worktree (.+)$/m);return s?s[1].trim():null}).filter(e=>e!==null)}async function f(o,t){const i=l((r,n)=>t.runSync(r,n)),e=await t.readAllProjectIdentities(),s=new Map(e.map(r=>[r.projectPath,r.cube])),a=[];for(const r of i){if(!t.pathExists(r)){t.stderr(`skipping ${r}: directory not found (orphaned worktree \u2014 run \`git worktree prune\`)
3
+ `);continue}const n=s.get(r);if(n&&n.cubeId===o.targetCubeId){if(!c(n.cubeId)||!c(n.droneId)){t.stderr(`skipping ${r}: cubes.json entry has malformed cubeId/droneId \u2014 re-assimilate to fix
4
+ `);continue}o.only!==void 0&&!d(n.droneLabel,o.only)||a.push({worktreeDir:r,cubeId:n.cubeId,droneId:n.droneId,droneLabel:n.droneLabel,sessionToken:n.sessionToken,apiUrl:n.apiUrl})}}return a}export{f as discoverDroneCandidates,l as enumerateLinkedWorktrees,d as matchesOnlyLabel};
@@ -0,0 +1,21 @@
1
+ import type { LaunchAllDeps } from './launch-all-deps.js';
2
+ export declare const LOCK_STALE_MS: number;
3
+ /** SHA-1 hex of the worktree abs path → fixed-length collision-safe filename. */
4
+ export declare function worktreeLockName(absPath: string): string;
5
+ export declare function locksDir(homeDir: string, cubeId: string): string;
6
+ export declare function lockPath(homeDir: string, cubeId: string, absPath: string): string;
7
+ export interface LockMarker {
8
+ launchedAt: string;
9
+ droneLabel: string;
10
+ worktreeDir: string;
11
+ }
12
+ /** Write the launch marker (mkdir -p the cube's locks dir first). Mode 0o600. */
13
+ export declare function writeLockMarker(deps: LaunchAllDeps, cubeId: string, droneLabel: string, worktreeDir: string, launchedAtISO: string): void;
14
+ /** Delete mtime-stale (>5min) `.pid` markers in locks/<cubeId>/ (crash cleanup). */
15
+ export declare function sweepStaleLocks(deps: LaunchAllDeps, cubeId: string, nowMs: number): void;
16
+ /** True iff a fresh (<=5min by its launchedAt content) marker exists for the seat. */
17
+ export declare function isLockLive(deps: LaunchAllDeps, cubeId: string, worktreeDir: string, nowMs: number): {
18
+ live: boolean;
19
+ launchedAt?: string;
20
+ };
21
+ //# sourceMappingURL=launch-all-locks.d.ts.map
@@ -0,0 +1 @@
1
+ import{createHash as s}from"node:crypto";import{join as a}from"node:path";const u=300*1e3;function m(t){return s("sha1").update(t,"utf8").digest("hex")}function l(t,r){return a(t,".config","borgmcp","locks",r)}function f(t,r,e){return a(l(t,r),m(e)+".pid")}function k(t,r,e,i,n){t.mkdirp(l(t.homedir(),r));const o={launchedAt:n,droneLabel:e,worktreeDir:i};t.writeFile(f(t.homedir(),r,i),JSON.stringify(o),384)}function d(t,r,e){const i=l(t.homedir(),r);for(const n of t.listDir(i)){if(!n.endsWith(".pid"))continue;const o=a(i,n),c=t.statMtime(o);c!==null&&e-c>u&&t.unlinkOpt(o)}}function x(t,r,e,i){const n=t.readFileOpt(f(t.homedir(),r,e));if(n===null)return{live:!1};try{const o=JSON.parse(n),c=Date.parse(o.launchedAt);return Number.isFinite(c)?{live:i-c<=u,launchedAt:o.launchedAt}:{live:!1}}catch{return{live:!1}}}export{u as LOCK_STALE_MS,x as isLockLive,f as lockPath,l as locksDir,d as sweepStaleLocks,m as worktreeLockName,k as writeLockMarker};
@@ -0,0 +1,22 @@
1
+ export interface LaunchAllFlags {
2
+ mode?: 'tmux' | 'windows' | 'pastelist';
3
+ only?: string;
4
+ dryRun?: boolean;
5
+ cli?: 'claude' | 'codex';
6
+ noAttach?: boolean;
7
+ yes?: boolean;
8
+ force?: boolean;
9
+ }
10
+ export interface LaunchAllArgs {
11
+ cubeName?: string;
12
+ flags: LaunchAllFlags;
13
+ }
14
+ export type ParseLaunchAllResult = {
15
+ ok: true;
16
+ args: LaunchAllArgs;
17
+ } | {
18
+ ok: false;
19
+ error: string;
20
+ };
21
+ export declare function parseLaunchAllArgs(rawArgs: string[]): ParseLaunchAllResult;
22
+ //# sourceMappingURL=parse-launch-all-args.d.ts.map
@@ -0,0 +1 @@
1
+ const s="--mode <tmux|windows|pastelist>, --only <name>, --dry-run, --cli <claude|codex>, --no-attach, --yes/-y, --force";function c(o){const r={};let n;for(let t=0;t<o.length;t++){const a=o[t];switch(a){case"--mode":{const e=o[++t];if(e!=="tmux"&&e!=="windows"&&e!=="pastelist")return{ok:!1,error:`--mode must be one of tmux|windows|pastelist (got: ${e??"<missing>"})`};r.mode=e;break}case"--only":{const e=o[++t];if(e===void 0||e.startsWith("--"))return{ok:!1,error:"--only requires a value (role name or drone label)"};r.only=e;break}case"--cli":{const e=o[++t];if(e!=="claude"&&e!=="codex")return{ok:!1,error:`--cli must be one of claude|codex (got: ${e??"<missing>"})`};r.cli=e;break}case"--dry-run":r.dryRun=!0;break;case"--no-attach":r.noAttach=!0;break;case"--yes":case"-y":r.yes=!0;break;case"--force":r.force=!0;break;default:if(a.startsWith("-"))return{ok:!1,error:`unknown flag: ${a}. Supported: ${s}`};if(n!==void 0)return{ok:!1,error:`unexpected extra argument: ${a}`};n=a;break}}return{ok:!0,args:{cubeName:n,flags:r}}}export{c as parseLaunchAllArgs};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
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",