borgmcp 1.0.21 → 1.0.22

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,76 @@
1
+ /**
2
+ * gh#853 — bare `borg` (no-args) interactive launch menu.
3
+ *
4
+ * When `borg` is run with NO arguments in a TTY, offer a small launch selector
5
+ * instead of launching immediately:
6
+ * 1. Launch (default) — the configured agent (Enter selects).
7
+ * 2. Launch with <other> instead — the OTHER installed agent, ONE-SHOT
8
+ * (does NOT persist the preference).
9
+ * 3. Launch all — runLaunchAll for the active cube.
10
+ *
11
+ * The option-set, the selection→action mapping, and the show/collapse decision
12
+ * are pure functions so they're unit-testable without a real TTY. claude.ts
13
+ * main() is thin glue: it computes the inputs (default cli, other-installed cli,
14
+ * launch-all targets), gates on shouldShowLaunchMenu, runs the orchestrator with
15
+ * the real readline prompt, then dispatches the returned action.
16
+ *
17
+ * Load-bearing safety: TTY-only + bare-args-only (shouldShowLaunchMenu) so every
18
+ * scripted/programmatic `borg` and every explicit subcommand/flag is untouched.
19
+ */
20
+ import type { BorgCli } from './cubes.js';
21
+ export type LaunchMenuAction = {
22
+ kind: 'launch';
23
+ cli: BorgCli;
24
+ } | {
25
+ kind: 'launch-all';
26
+ };
27
+ export interface LaunchMenuOption {
28
+ /** The keystroke that selects this option (sequential: '1', '2', …). */
29
+ key: string;
30
+ label: string;
31
+ action: LaunchMenuAction;
32
+ }
33
+ export interface LaunchMenuInputs {
34
+ /** The configured/resolved current agent (option 1). */
35
+ defaultCli: BorgCli;
36
+ /** The installed agent that is NOT the default, or null if not installed. */
37
+ otherInstalledCli: BorgCli | null;
38
+ /** True iff there's an active cube with >=1 discoverable drone (option 3). */
39
+ hasLaunchAllTargets: boolean;
40
+ }
41
+ /**
42
+ * Gate: the menu fires ONLY for bare `borg` (no args) in a TTY. Any explicit
43
+ * subcommand/flag, or a non-TTY (piped/scripted/CI) invocation, falls straight
44
+ * through to the existing default launch — no menu, no behavior change.
45
+ */
46
+ export declare function shouldShowLaunchMenu(args: {
47
+ extraArgs: string[];
48
+ stdinIsTTY: boolean;
49
+ stdoutIsTTY: boolean;
50
+ }): boolean;
51
+ /**
52
+ * The context-filtered option set. Option 1 is always present; options 2/3 are
53
+ * included only when applicable. Keys are sequential with no gaps, so a hidden
54
+ * middle option never produces a "1) … 3) …" gap menu.
55
+ */
56
+ export declare function buildLaunchMenuOptions(inputs: LaunchMenuInputs): LaunchMenuOption[];
57
+ /** Map a raw prompt answer to an action. Empty/Enter → option 1 (default). */
58
+ export declare function resolveLaunchMenuChoice(options: LaunchMenuOption[], rawInput: string): {
59
+ ok: true;
60
+ action: LaunchMenuAction;
61
+ } | {
62
+ ok: false;
63
+ };
64
+ /** The rendered menu text (prompt suffix `[1]:` defaults to option 1 on Enter). */
65
+ export declare function renderLaunchMenu(options: LaunchMenuOption[]): string;
66
+ /**
67
+ * Orchestrate the menu with an injected readline-style prompt. Collapses to a
68
+ * direct default launch (no render, no prompt) when only option 1 applies.
69
+ * Re-prompts on invalid input up to `maxAttempts`, then falls back to the safe
70
+ * default (option 1) so a fat-fingered session still launches.
71
+ */
72
+ export declare function runBareLaunchMenu(inputs: LaunchMenuInputs, prompt: (message: string) => Promise<string>, opts?: {
73
+ maxAttempts?: number;
74
+ warn?: (message: string) => void;
75
+ }): Promise<LaunchMenuAction>;
76
+ //# sourceMappingURL=bare-launch-menu.d.ts.map
@@ -0,0 +1,5 @@
1
+ const u={claude:"Claude",codex:"Codex"};function f(n){return n.extraArgs.length===0&&n.stdinIsTTY&&n.stdoutIsTTY}function h(n){const t=[{key:"1",label:`Launch (default \xB7 ${u[n.defaultCli]})`,action:{kind:"launch",cli:n.defaultCli}}];return n.otherInstalledCli&&t.push({key:String(t.length+1),label:`Launch with ${u[n.otherInstalledCli]} instead (one-shot)`,action:{kind:"launch",cli:n.otherInstalledCli}}),n.hasLaunchAllTargets&&t.push({key:String(t.length+1),label:"Launch all (this cube's drone worktrees)",action:{kind:"launch-all"}}),t}function s(n,t){const e=t.trim();if(e==="")return{ok:!0,action:n[0].action};const o=n.find(a=>a.key===e);return o?{ok:!0,action:o.action}:{ok:!1}}function d(n){return`borg \u2014 how do you want to launch?
2
+ ${n.map(e=>` ${e.key}) ${e.label}`).join(`
3
+ `)}
4
+ [1]: `}async function k(n,t,e={}){const o=h(n);if(o.length===1)return o[0].action;const a=e.maxAttempts??3,c=d(o);for(let l=0;l<a;l++){const i=await t(l===0?c:`Invalid choice.
5
+ ${c}`),r=s(o,i);if(r.ok)return r.action;e.warn?.(`invalid launch-menu selection: ${JSON.stringify(i.trim())}`)}return o[0].action}export{h as buildLaunchMenuOptions,d as renderLaunchMenu,s as resolveLaunchMenuChoice,k as runBareLaunchMenu,f as shouldShowLaunchMenu};
package/dist/claude.js CHANGED
@@ -1,13 +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 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(`
2
+ import{spawn as R}from"child_process";import{randomUUID as D}from"node:crypto";import{basename as L}from"node:path";import{createInterface as E}from"node:readline/promises";import t from"chalk";import{findProjectRoot as M,getActiveCube as O,inboxPathForDrone as F,setCodexWakeTarget as H,pruneDeadCodexWakeTargets as N}from"./cubes.js";import{handleVersionFlag as B,getPackageVersion as h}from"./version.js";import{isHelpFlag as G,setupHelpText as W,topLevelHelpText as Y}from"./cli-help.js";import{runSpawn as _}from"./spawn.js";import{parseSyncArgs as U,runSync as V}from"./sync.js";import{parseAssimilateArgs as j}from"./parse-assimilate-args.js";import{runAssimilate as K}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as q}from"./assimilate-deps.js";import{parseLaunchAllArgs as S}from"./parse-launch-all-args.js";import{runLaunchAll as y}from"./launch-all-cmd.js";import{buildDefaultLaunchAllDeps as w}from"./launch-all-deps.js";import{discoverDroneCandidates as z}from"./launch-all-discovery.js";import{runBareLaunchMenu as X,shouldShowLaunchMenu as J}from"./bare-launch-menu.js";import{setTerminalTitle as Q}from"./terminal-title.js";import{initConsolePrefix as Z,consolePrefix as s}from"./console-prefix.js";import{initDebugFromArgv as ee}from"./debug.js";import{fetchLatestBorgmcpVersion as re,compareVersionsForStaleness as oe}from"./stale-version-check.js";import{defaultCliChoiceDeps as se,detectCliAvailability as x,installedCliNames as T,parseCliFlag as te,resolveCliChoice as ie}from"./cli-platform.js";import{getRefreshToken as ae,getIdToken as ne}from"./config.js";import{composeGetStarted as ce,shouldShowGetStarted as le}from"./get-started.js";import{prepareCodexRemoteLaunch as de,withCodexCwdArg as pe,defaultCodexRemoteDeps as ue,checkCodexBridgeHealthy as me}from"./codex-remote.js";import{findLoadedCodexThread as fe}from"./codex-app-server.js";import{buildAgentKickoffPrompt as ge,recordCodexWakeTarget as he,socketPathFromRemoteArgs as A}from"./codex-launch.js";import{codexBorgSessionConfigArgs as we}from"./launch-gate.js";import{addCodexMcpServer as xe,addCodexSessionStartHook as Ce,addCodexUserPromptSubmitHook as ve,addMcpServer as ke,addProjectSessionStartHook as be,addUserPromptSubmitHook as $e,isCodexMcpServerConfigured as Se,isMcpServerConfigured as ye,removeSessionStartHook as Te}from"./config-utils.js";async function Ae(){ee(process.argv),B(),await Z();const c=(async()=>{if(!process.stderr.isTTY)return;const e=h(),o=await re();if(!o)return;const n=oe(e,o);n.stale&&n.message&&process.stderr.write(`${s()}${n.message}
3
+ `)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(Y(h())),process.exit(0)),process.argv[2]==="setup"){G(process.argv[3])&&(process.stdout.write(W(h())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){const e=j(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${s()}\u25FC borg assimilate: ${e.error}
4
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const o=q(),n=await K({role:e.role,flags:e.flags},o);process.exit(n)}if(process.argv[2]==="spawn"){const e=await _();process.exit(e)}if(process.argv[2]==="sync"){const e=U(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${s()}\u25FC borg sync: ${e.error}
5
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const o=await V({},e.options);process.exit(o)}if(process.argv[2]==="launch-all"){const e=S(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${s()}\u25FC borg launch-all: ${e.error}
6
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const o=w(),n=await y(e.args,o);process.exit(n)}if(le(await ae()!==null,await ne()!==null)){const e=T(x()).length>0;process.stdout.write(ce(e)),process.exit(0)}const a=te(process.argv.slice(2));a.error&&(process.stderr.write(t.red(`${s()}\u25FC ${a.error}
7
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const C=async e=>{const o=E({input:process.stdin,output:process.stdout});try{return await o.question(e)}finally{o.close()}};let r=await ie(a.cli,se(C,()=>process.stdin.isTTY===!0));Ie();const i=await O();if(J({extraArgs:process.argv.slice(2),stdinIsTTY:process.stdin.isTTY===!0,stdoutIsTTY:process.stdout.isTTY===!0})){const e=T(x()).find(u=>u!==r)??null;let o=!1;i&&(o=(await z({targetCubeId:i.cubeId},w())).length>0);const n=await X({defaultCli:r,otherInstalledCli:e,hasLaunchAllTargets:o},C);if(n.kind==="launch-all"){const u=S([]),P=u.ok?await y(u.args,w()):1;process.exit(P)}r=n.cli}const l=a.rest;Q(i?{label:i.droneLabel,cubeName:i.name}:null,L(process.cwd()));const I=i&&r==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${F(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([c,new Promise(e=>setTimeout(e,2e3))]);const v=r==="codex"?`borg-wake-${D()}`:null;let m,k=[],f={...process.env,BORG_SESSION:"1"},d=null,p=null;if(r==="codex"&&!l.includes("--remote")){console.error(`${s()}${t.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await de(ue());e.warning?(console.error(`${s()}${t.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.",k=e.args,f={...process.env,...e.env,BORG_SESSION:"1"},d=A(e.args),p=e.server?.cleanup??null}else r==="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=A(l),d&&(f={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));const b=ge({cli:r,codexWakeNonce:v,monitorClause:I,codexWakePathClause:m});let g=[...l,b];r==="codex"&&(g=[...we(),...k,...pe(g,process.cwd())]),console.error(`${s()}${t.blue(`\u25FC Launching ${r==="claude"?"Claude Code":"Codex"}\u2026`)}`);const $=R(r,g,{stdio:"inherit",shell:!1,env:f});r==="codex"&&i&&d&&(he({deps:{setCodexWakeTarget:H,findLoadedCodexThread:fe},cubeId:i.cubeId,droneId:i.droneId,socketPath:d,passthroughArgs:l,previewNeedle:v??b.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),N(e=>me(e))),$.on("error",e=>{if(p)try{p()}catch{}e.code==="ENOENT"?(console.error(`${s()}${t.red(`
8
+ \u25FC Failed to launch ${r}`)}`),console.error(`${s()}${t.gray(`Make sure ${r} is installed.
9
+ `)}`)):console.error(`${s()}${t.red(`
10
+ \u25FC Failed to launch ${r}: ${e.message}
11
+ `)}`),process.exit(1)}),$.on("exit",e=>{if(p)try{p()}catch{}process.exit(e??0)})}function Ie(){const c=x();if(c.claude)try{ye()||ke(),be(M(process.cwd())),Te(),$e()}catch(a){console.error(`${s()}${t.yellow(`warning: Claude Code integration check failed: ${a?.message??a}`)}`)}if(c.codex)try{Se()||xe(),Ce(),ve()}catch(a){console.error(`${s()}${t.yellow(`warning: Codex integration check failed: ${a?.message??a}`)}`)}}Ae().catch(c=>{console.error(`${s()}${t.red(`
12
12
  \u25FC Error: ${c.message}
13
13
  `)}`),process.exit(1)});
@@ -1,4 +1,4 @@
1
- import { getActiveCube, getCodexWakeTarget } from './cubes.js';
1
+ import { getActiveCube, getCodexWakeTarget, setCodexWakeTarget } from './cubes.js';
2
2
  import { CodexAppServerClient } from './codex-app-server.js';
3
3
  import { checkCodexBridgeHealthy } from './codex-remote.js';
4
4
  export declare const CODEX_WAKE_PROMPT = "New Borg cube-log activity arrived.";
@@ -35,7 +35,10 @@ export declare function probeCodexBridgeArmed(active: {
35
35
  export interface CodexWakeDeps {
36
36
  getActiveCube?: typeof getActiveCube;
37
37
  getCodexWakeTarget?: typeof getCodexWakeTarget;
38
- createClient?: (socketPath: string) => Pick<CodexAppServerClient, 'connect' | 'readThread' | 'startTurn' | 'close'>;
38
+ setCodexWakeTarget?: typeof setCodexWakeTarget;
39
+ createClient?: (socketPath: string) => Pick<CodexAppServerClient, 'connect' | 'readThread' | 'startTurn' | 'loadedThreadIds' | 'close'>;
40
+ env?: NodeJS.ProcessEnv;
41
+ cwd?: () => string;
39
42
  sleep?: (ms: number) => Promise<void>;
40
43
  now?: () => number;
41
44
  }
@@ -1,2 +1,2 @@
1
- import{getActiveCube as h,getCodexWakeTarget as f}from"./cubes.js";import{CodexAppServerClient as g}from"./codex-app-server.js";import{checkCodexBridgeHealthy as p}from"./codex-remote.js";import{recordEventReceipt as C}from"./health-beat.js";const v="New Borg cube-log activity arrived.";function B(e){return`New Borg cube-log activity arrived:
2
- ${e}`}const k="Borg cube activity arrived while you were busy. Run `borg:read-log unread_only=true` and DRAIN \u2014 repeat until the returned page is under the limit and behind_by is 0 \u2014 so no entries are skipped.",y=5e3,x=15*6e4;function w(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function S(e=process.env){return w(e)?"codex":"claude"}function _(e=process.env){return w(e)?{enabled:!0}:{enabled:!1}}async function U(e,t={}){try{const r=await(t.getCodexWakeTarget??f)(e.cubeId,e.droneId);return r?(t.checkBridge??p)(r.socketPath):!1}catch{return null}}let s=!1;const d=[],c=new Set,u=[],b=100;let l=!1;function T(e){return new Promise(t=>setTimeout(t,e))}function M(e=v,t=process.env,a={}){_(t).enabled&&(d.push({reason:e,deps:a}),!s&&(s=!0,P().finally(()=>{s=!1})))}async function P(){for(;d.length>0;){const e=d.shift();await m(e.reason,e.deps)}}async function m(e,t){try{const a=await(t.getActiveCube??h)();if(!a)return;const r=await(t.getCodexWakeTarget??f)(a.cubeId,a.droneId);if(!r)return;const o=`${r.threadId}\0${e}`;if(c.has(o))return;const n=t.createClient?t.createClient(r.socketPath):new g(r.socketPath);await n.connect();try{if((await n.readThread(r.threadId))?.status?.type==="active"){I(t);return}await n.startTurn(r.threadId,e),C(),E(o)}finally{n.close()}}catch{}}function I(e){l||(l=!0,A(e).finally(()=>{l=!1}))}async function A(e){const t=e.sleep??T,a=e.now??Date.now,r=a()+x;for(;a()<r;){await t(y);try{const o=await(e.getActiveCube??h)();if(!o)continue;const n=await(e.getCodexWakeTarget??f)(o.cubeId,o.droneId);if(!n)continue;const i=e.createClient?e.createClient(n.socketPath):new g(n.socketPath);await i.connect();try{if((await i.readThread(n.threadId))?.status?.type==="active")continue;await i.startTurn(n.threadId,k),C();return}finally{i.close()}}catch{}}}function H(){s=!1,d.length=0,c.clear(),u.length=0,l=!1}function E(e){if(!c.has(e))for(c.add(e),u.push(e);u.length>b;){const t=u.shift();t&&c.delete(t)}}export{k as CODEX_CATCHUP_PROMPT,v as CODEX_WAKE_PROMPT,B as formatCodexWakePrompt,w as isCodexRemoteWakeEnabled,U as probeCodexBridgeArmed,H as resetCodexWakeForTests,_ as resolveCodexWakeTarget,S as resolveSessionAgentKind,M as wakeCodexViaAppServer};
1
+ import{getActiveCube as k,getCodexWakeTarget as w,setCodexWakeTarget as x}from"./cubes.js";import{CodexAppServerClient as T}from"./codex-app-server.js";import{checkCodexBridgeHealthy as m}from"./codex-remote.js";import{recordEventReceipt as p}from"./health-beat.js";import{codexAppServerSocketFromEnv as P,pickFreshThread as b,wakeTargetChanged as I}from"./codex-wake-resolve.js";const W="New Borg cube-log activity arrived.";function $(e){return`New Borg cube-log activity arrived:
2
+ ${e}`}const _="Borg cube activity arrived while you were busy. Run `borg:read-log unread_only=true` and DRAIN \u2014 repeat until the returned page is under the limit and behind_by is 0 \u2014 so no entries are skipped.",A=5e3,E=15*6e4;function v(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function q(e=process.env){return v(e)?"codex":"claude"}function R(e=process.env){return v(e)?{enabled:!0}:{enabled:!1}}async function G(e,t={}){try{const n=await(t.getCodexWakeTarget??w)(e.cubeId,e.droneId);return n?(t.checkBridge??m)(n.socketPath):!1}catch{return null}}let l=!1;const h=[],u=new Set,f=[],S=100;let g=!1;function K(e){return new Promise(t=>setTimeout(t,e))}function C(e,t){return t.createClient?t.createClient(e):new T(e)}async function y(e,t){const r=P(t.env??process.env);if(r){const a=C(r,t);await a.connect();try{const o=await a.loadedThreadIds(),i=[];for(const s of o){const d=await a.readThread(s);d&&i.push({id:d.id,cwd:d.cwd,updatedAt:d.updatedAt})}const c=b(i,{cwd:(t.cwd??(()=>process.cwd()))()});return c?(await O(e,{socketPath:r,threadId:c},t),{socketPath:r,threadId:c}):null}finally{a.close()}}const n=await(t.getCodexWakeTarget??w)(e.cubeId,e.droneId);return n?{socketPath:n.socketPath,threadId:n.threadId}:null}async function O(e,t,r){try{const n=r.getCodexWakeTarget??w,a=r.setCodexWakeTarget??x,o=await n(e.cubeId,e.droneId),i=o?{socketPath:o.socketPath,threadId:o.threadId}:null;I(i,t)&&await a(e.cubeId,e.droneId,t)}catch{}}function Q(e=W,t=process.env,r={}){R(t).enabled&&(h.push({reason:e,deps:r}),!l&&(l=!0,D().finally(()=>{l=!1})))}async function D(){for(;h.length>0;){const e=h.shift();await B(e.reason,e.deps)}}async function B(e,t){try{const r=await(t.getActiveCube??k)();if(!r)return;const n=await y(r,t);if(!n)return;const{socketPath:a,threadId:o}=n,i=`${o}\0${e}`;if(u.has(i))return;const c=C(a,t);await c.connect();try{if((await c.readThread(o))?.status?.type==="active"){U(t);return}await c.startTurn(o,e),p(),M(i)}finally{c.close()}}catch{}}function U(e){g||(g=!0,F(e).finally(()=>{g=!1}))}async function F(e){const t=e.sleep??K,r=e.now??Date.now,n=r()+E;for(;r()<n;){await t(A);try{const a=await(e.getActiveCube??k)();if(!a)continue;const o=await y(a,e);if(!o)continue;const{socketPath:i,threadId:c}=o,s=C(i,e);await s.connect();try{if((await s.readThread(c))?.status?.type==="active")continue;await s.startTurn(c,_),p();return}finally{s.close()}}catch{}}}function Y(){l=!1,h.length=0,u.clear(),f.length=0,g=!1}function M(e){if(!u.has(e))for(u.add(e),f.push(e);f.length>S;){const t=f.shift();t&&u.delete(t)}}export{_ as CODEX_CATCHUP_PROMPT,W as CODEX_WAKE_PROMPT,$ as formatCodexWakePrompt,v as isCodexRemoteWakeEnabled,G as probeCodexBridgeArmed,Y as resetCodexWakeForTests,R as resolveCodexWakeTarget,q as resolveSessionAgentKind,Q as wakeCodexViaAppServer};
@@ -1 +1 @@
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};
1
+ import{mkdirSync as v,chmodSync as w,readdirSync as g,rmSync as C,writeFileSync as k,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";import{codexAppServerSocketConfigArgs as N}from"./codex-wake-resolve.js";const I=u(S(),".config","borgmcp","codex-remote");function G(e,r){return M(e)?e:["--cd",r,...e]}function M(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 X(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 T(e,r){let a;try{a=g(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 j(e){const r=e.runtimeDir??I,a=e.isAlive??x,i=e.readyTimeoutMs??8e3,c=e.pollIntervalMs??250;try{v(r,{recursive:!0,mode:448}),w(r,448),T(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{k(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 q(){return{spawnAppServer:e=>{const r=A("codex",["app-server",...R(),...N(e),"--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{I as DEFAULT_CODEX_REMOTE_DIR,X as checkCodexBridgeHealthy,q as defaultCodexRemoteDeps,x as defaultIsAlive,j as prepareCodexRemoteLaunch,G as withCodexCwdArg};
@@ -0,0 +1,69 @@
1
+ /**
2
+ * gh#855 — pure helpers for FRESH codex wake-target re-resolution.
3
+ *
4
+ * Root cause of codex deaf-when-idle: the wake target (socket + thread) was
5
+ * resolved once at launch and never refreshed, so a missed/stale launch probe
6
+ * left the drone permanently deaf. Phase 1 makes the waking borg-mcp child
7
+ * authoritative about its OWN live app-server socket — the socket is injected
8
+ * into the child's pinned env at spawn (codex-remote.ts, via the #851 `-c
9
+ * mcp_servers.borg.env.X` channel) — and re-resolves the loaded thread FRESH on
10
+ * every wake (loadedThreadIds is a re-runnable RPC).
11
+ *
12
+ * These are the pure pieces; the IO orchestration lives in codex-app-wake.ts.
13
+ */
14
+ /** Pinned-env var carrying THIS drone's live app-server socket (set at spawn). */
15
+ export declare const BORG_CODEX_APP_SERVER_SOCKET_ENV = "BORG_CODEX_APP_SERVER_SOCKET";
16
+ /** The live app-server socket for this borg-mcp child, or null (un-upgraded launch). */
17
+ export declare function codexAppServerSocketFromEnv(env?: NodeJS.ProcessEnv): string | null;
18
+ /**
19
+ * The per-launch codex config override that pins THIS app-server's live socket
20
+ * into the borg-mcp child's [mcp_servers.borg.env] — the same `-c` channel the
21
+ * #851 BORG_SESSION marker rides (codex MCP children read only the pinned env,
22
+ * never inherited env). The socketPath is borg-generated (randomBytes under
23
+ * ~/.config/borgmcp/codex-remote), never user input; TOML-quoted exactly like
24
+ * the BORG_SESSION override, so there is zero injection surface.
25
+ */
26
+ export declare function codexAppServerSocketConfigArgs(socketPath: string): string[];
27
+ export interface CodexThreadInfo {
28
+ id: string;
29
+ cwd?: string;
30
+ updatedAt?: number;
31
+ }
32
+ /**
33
+ * Pick the loaded thread to wake on the live socket. Each borg-owned app-server
34
+ * is fresh-per-launch / single-session, so the common case is exactly one loaded
35
+ * thread. When more than one is loaded, prefer the thread whose cwd matches this
36
+ * drone's working directory (sibling worktrees have distinct cwds), then the
37
+ * newest by updatedAt — always deterministic. No loaded thread → null (no wake
38
+ * this cycle; the next wake retries, so a transient empty list never causes
39
+ * permanent deafness).
40
+ */
41
+ export declare function pickFreshThread(threads: CodexThreadInfo[], opts: {
42
+ cwd: string;
43
+ }): string | null;
44
+ /**
45
+ * Pure prune: drop wake-target entries whose app-server socket is positively
46
+ * dead (liveness === false), so the file self-heals. Keeps alive (true) and
47
+ * indeterminate (null) entries — false-deaf-avoidance, mirroring
48
+ * checkCodexBridgeHealthy's tri-state. Returns the surviving map + whether
49
+ * anything changed (so the caller writes only on change).
50
+ */
51
+ export declare function pruneDeadWakeTargets<T extends {
52
+ socketPath: string;
53
+ }>(targets: Record<string, T>, socketLiveness: (socketPath: string) => boolean | null): {
54
+ targets: Record<string, T>;
55
+ changed: boolean;
56
+ };
57
+ /**
58
+ * Whether the freshly-resolved target differs from what's already recorded —
59
+ * so the self-healing cache write happens only on change (no file thrash on a
60
+ * busy cube re-resolving the same socket+thread every wake).
61
+ */
62
+ export declare function wakeTargetChanged(existing: {
63
+ socketPath: string;
64
+ threadId: string;
65
+ } | null, fresh: {
66
+ socketPath: string;
67
+ threadId: string;
68
+ }): boolean;
69
+ //# sourceMappingURL=codex-wake-resolve.d.ts.map
@@ -0,0 +1 @@
1
+ const u="BORG_CODEX_APP_SERVER_SOCKET";function p(e=process.env){const t=e[u];return t&&t.length>0?t:null}function f(e){return["-c",`mcp_servers.borg.env.${u}="${e}"`]}function s(e,t){if(e.length===0)return null;if(e.length===1)return e[0].id;const r=e.filter(n=>n.cwd===t.cwd),o=r.length>0?r:e;let c=o[0];for(const n of o)(n.updatedAt??0)>(c.updatedAt??0)&&(c=n);return c.id}function a(e,t){const r={};let o=!1;for(const[c,n]of Object.entries(e)){if(t(n.socketPath)===!1){o=!0;continue}r[c]=n}return{targets:r,changed:o}}function d(e,t){return!e||e.socketPath!==t.socketPath||e.threadId!==t.threadId}export{u as BORG_CODEX_APP_SERVER_SOCKET_ENV,f as codexAppServerSocketConfigArgs,p as codexAppServerSocketFromEnv,s as pickFreshThread,a as pruneDeadWakeTargets,d as wakeTargetChanged};
package/dist/cubes.d.ts CHANGED
@@ -82,4 +82,14 @@ export declare function readAllProjectIdentities(): Promise<Array<{
82
82
  export declare function setProjectCliPreference(cli: BorgCli): Promise<void>;
83
83
  export declare function setCodexWakeTarget(cubeId: string, droneId: string, target: Omit<CodexWakeTargetRecord, 'updatedAt'>): Promise<void>;
84
84
  export declare function getCodexWakeTarget(cubeId: string, droneId: string): Promise<CodexWakeTargetRecord | null>;
85
+ /**
86
+ * gh#855: drop wake-target entries whose app-server socket is positively dead,
87
+ * so the file self-heals (stale dead-socket entries from crashed prior launches
88
+ * don't linger and mislead probeCodexBridgeArmed / health-beat). Pure prune
89
+ * decision lives in codex-wake-resolve.ts (false-deaf-avoidance: keeps alive +
90
+ * indeterminate); this is the thin read → prune → write-only-on-change glue.
91
+ * The liveness check is injected (claude.ts wires checkCodexBridgeHealthy) so
92
+ * cubes.ts stays free of the codex-remote dependency.
93
+ */
94
+ export declare function pruneDeadCodexWakeTargets(socketLiveness: (socketPath: string) => boolean | null): Promise<void>;
85
95
  //# sourceMappingURL=cubes.d.ts.map
package/dist/cubes.js CHANGED
@@ -1,4 +1,4 @@
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};
1
+ import{existsSync as E}from"node:fs";import{mkdir as f,readFile as p,writeFile as y,unlink as C}from"node:fs/promises";import{homedir as I}from"node:os";import{dirname as c,join as o,resolve as x}from"node:path";import{pruneDeadWakeTargets as F}from"./codex-wake-resolve.js";const a=o(I(),".config","borgmcp"),s=o(a,"cubes.json"),d=o(a,"launch.json"),g=o(a,"codex-wake-targets.json"),N=o(a,"inboxes");function i(t=process.cwd()){let e=x(t);for(;;){if(E(o(e,".git")))return e;const r=c(e);if(r===e)return x(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(N,t,`${e}.log`)}function k(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 k(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 O(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function w(){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 O(e)?e:null}catch{return null}}async function A(t){await f(c(d),{recursive:!0}),await y(d,JSON.stringify(t,null,2)+`
3
+ `,{mode:384})}function b(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 T(t){return t!==null&&typeof t=="object"&&typeof t.targets=="object"&&t.targets!==null&&!Array.isArray(t.targets)}async function j(){let t;try{t=await p(g,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return T(e)?e:null}catch{return null}}async function m(t){await f(c(g),{recursive:!0}),await y(g,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 v(t){const e=await l()??{projects:{}};e.projects[i()]=t,await h(e)}function J(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 R(){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 C(s)}catch(r){if(r?.code!=="ENOENT")throw r}return}await h(t)}}async function U(){const t=await w();if(!t)return null;const e=t.projects[i()];return e?.cli==="claude"||e?.cli==="codex"?e.cli:null}async function B(t){const e=await w();if(!e)return null;const r=e.projects[i(t)];return r?.cli==="claude"||r?.cli==="codex"?r.cli:null}async function K(){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 X(t){const e=await w()??{projects:{}};e.projects[i()]={cli:t},await A(e)}async function G(t,e,r){const n=await j()??{targets:{}};n.targets[b(t,e)]={...r,updatedAt:new Date().toISOString()},await m(n)}async function H(t,e){const r=await j();if(!r)return null;const n=r.targets[b(t,e)];return!n||typeof n.threadId!="string"||typeof n.socketPath!="string"?null:n}async function q(t){const e=await j();if(!e)return;const{targets:r,changed:n}=F(e.targets,t);n&&await m({...e,targets:r})}export{J as activeCubeWithFreshRegenIdentity,R as clearActiveCube,i as findProjectRoot,$ as getActiveCube,H as getCodexWakeTarget,U as getProjectCliPreference,B as getProjectCliPreferenceForPath,_ as inboxPathForDrone,q as pruneDeadCodexWakeTargets,K as readAllProjectIdentities,v as setActiveCube,G as setCodexWakeTarget,X as setProjectCliPreference};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
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",