borgmcp 1.0.20 → 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.
- package/dist/bare-launch-menu.d.ts +76 -0
- package/dist/bare-launch-menu.js +5 -0
- package/dist/claude.js +10 -10
- package/dist/codex-app-wake.d.ts +5 -2
- package/dist/codex-app-wake.js +2 -2
- package/dist/codex-remote.js +1 -1
- package/dist/codex-wake-resolve.d.ts +69 -0
- package/dist/codex-wake-resolve.js +1 -0
- package/dist/cubes.d.ts +10 -0
- package/dist/cubes.js +4 -4
- package/package.json +1 -1
|
@@ -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
|
|
3
|
-
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(
|
|
4
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
5
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
6
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
7
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
8
|
-
\u25FC Failed to launch ${
|
|
9
|
-
`)}`)):console.error(`${
|
|
10
|
-
\u25FC Failed to launch ${
|
|
11
|
-
`)}`),process.exit(1)})
|
|
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)});
|
package/dist/codex-app-wake.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/codex-app-wake.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{getActiveCube as
|
|
2
|
-
${e}`}const
|
|
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};
|
package/dist/codex-remote.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mkdirSync as
|
|
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
|
|
2
|
-
`,{mode:384})}function
|
|
3
|
-
`,{mode:384})}function
|
|
4
|
-
`,{mode:384})}async function
|
|
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.
|
|
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",
|