borgmcp 1.0.21 → 1.0.23

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/auth.js CHANGED
@@ -1,12 +1,12 @@
1
- import{createServer as w}from"http";import{URL as _}from"url";import g from"crypto";import P from"open";import{storeIdToken as d,storeRefreshToken as f,getRefreshToken as p}from"./config.js";import{cerr as t}from"./console-prefix.js";import{isNoBrowserEnv as O}from"./auth-env.js";import{requestDeviceCode as I,pollForDeviceToken as x}from"./device-auth.js";class G extends Error{errorCode;errorDescription;constructor(r,s){super(s?`Refresh token invalid (${r}): ${s}`:`Refresh token invalid (${r})`),this.errorCode=r,this.errorDescription=s,this.name="RefreshTokenInvalidError"}}class u extends Error{constructor(r){super(r),this.name="RefreshTransientError"}}const m="675073910799-41pbe12rfhqemidh64h09s4q3e0udpgp.apps.googleusercontent.com",k="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",A="675073910799-6qmi73v5106dj1v0l22j2qnkh5r3e8fq.apps.googleusercontent.com",S="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",L="https://accounts.google.com/o/oauth2/v2/auth",E="https://oauth2.googleapis.com/token",$="https://oauth2.googleapis.com/revoke",T=["openid","email","profile"],Q=8e3,Z=9e3,y=300*1e3,v=y/6e4;function D(){const e=g.randomBytes(32).toString("base64url"),r=g.createHash("sha256").update(e).digest("base64url");return{verifier:e,challenge:r}}async function N(){return new Promise((e,r)=>{const s=w();s.listen(0,()=>{const i=s.address();if(i&&typeof i=="object"){const o=i.port;s.close(()=>e(o))}else s.close(()=>r(new Error("Failed to get assigned port")))}),s.on("error",r)})}async function U(){const e=await N(),r=new Promise((s,i)=>{const o=w((n,c)=>{const a=new _(n.url,`http://localhost:${e}`);if(a.pathname==="/callback"){const h=a.searchParams.get("code"),l=a.searchParams.get("error");if(l){c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
1
+ import{createServer as w}from"http";import{URL as k}from"url";import g from"crypto";import P from"open";import{storeIdToken as d,storeRefreshToken as f,getRefreshToken as p}from"./config.js";import{cerr as r}from"./console-prefix.js";import{isNoBrowserEnv as O}from"./auth-env.js";import{requestDeviceCode as I,pollForDeviceToken as x}from"./device-auth.js";class G extends Error{errorCode;errorDescription;constructor(t,s){super(s?`Refresh token invalid (${t}): ${s}`:`Refresh token invalid (${t})`),this.errorCode=t,this.errorDescription=s,this.name="RefreshTokenInvalidError"}}class l extends Error{constructor(t){super(t),this.name="RefreshTransientError"}}const m="675073910799-41pbe12rfhqemidh64h09s4q3e0udpgp.apps.googleusercontent.com",_="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",A="675073910799-6qmi73v5106dj1v0l22j2qnkh5r3e8fq.apps.googleusercontent.com",S="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",L="https://accounts.google.com/o/oauth2/v2/auth",E="https://oauth2.googleapis.com/token",$="https://oauth2.googleapis.com/revoke",T=["openid","email","profile"],Q=8e3,Z=9e3,y=300*1e3,v=y/6e4;function D(){const e=g.randomBytes(32).toString("base64url"),t=g.createHash("sha256").update(e).digest("base64url");return{verifier:e,challenge:t}}async function N(){return new Promise((e,t)=>{const s=w();s.listen(0,()=>{const i=s.address();if(i&&typeof i=="object"){const o=i.port;s.close(()=>e(o))}else s.close(()=>t(new Error("Failed to get assigned port")))}),s.on("error",t)})}async function U(){const e=await N(),t=new Promise((s,i)=>{const o=w((n,c)=>{const a=new k(n.url,`http://localhost:${e}`);if(a.pathname==="/callback"){const h=a.searchParams.get("code"),u=a.searchParams.get("error");if(u){c.writeHead(400,{"Content-Type":"text/html"}),c.end(`
2
2
  <html>
3
3
  <body>
4
4
  <h1>\u25FC Authentication Failed</h1>
5
- <p>Error: ${l}</p>
5
+ <p>Error: ${u}</p>
6
6
  <p>You can close this window.</p>
7
7
  </body>
8
8
  </html>
9
- `),o.close(),i(new Error(`OAuth error: ${l}`));return}if(h){c.writeHead(200,{"Content-Type":"text/html"}),c.end(`
9
+ `),o.close(),i(new Error(`OAuth error: ${u}`));return}if(h){c.writeHead(200,{"Content-Type":"text/html"}),c.end(`
10
10
  <html>
11
11
  <body>
12
12
  <h1>\u25FC Authentication Successful!</h1>
@@ -20,19 +20,19 @@ import{createServer as w}from"http";import{URL as _}from"url";import g from"cryp
20
20
  <p>Missing authorization code.</p>
21
21
  </body>
22
22
  </html>
23
- `),o.close(),i(new Error("Missing authorization code"))}});o.listen(e,()=>{t(`Callback server listening on http://localhost:${e}`)}),setTimeout(()=>{o.close(),i(new Error(`Authentication timed out after ${v} minutes \u2014 no authorization received from the browser. Re-run \`borg setup\` and complete the Google sign-in in the page that opens.`))},y).unref()});return{port:e,codePromise:r}}async function B(e,r,s){const i=`http://localhost:${s}/callback`,o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:m,client_secret:k,code:e,code_verifier:r,grant_type:"authorization_code",redirect_uri:i})});if(!o.ok){const n=await o.text();throw new Error(`Failed to exchange code for tokens: ${n}`)}return await o.json()}async function C(e){try{await fetch($,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`token=${encodeURIComponent(e)}`})}catch{}}async function M(){t(`
24
- \u25FC Borg MCP Authentication`),t(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
25
- `);const e=await p();e&&(t("Revoking previous refresh_token to force fresh consent..."),await C(e)),t("Generating PKCE challenge...");const r=D();t("Starting local callback server...");const{port:s,codePromise:i}=await U(),o=`http://localhost:${s}/callback`,n=new _(L);n.searchParams.set("client_id",m),n.searchParams.set("redirect_uri",o),n.searchParams.set("response_type","code"),n.searchParams.set("scope",T.join(" ")),n.searchParams.set("code_challenge",r.challenge),n.searchParams.set("code_challenge_method","S256"),n.searchParams.set("access_type","offline"),n.searchParams.set("prompt","consent select_account"),t(`
26
- \u{1F4F1} Opening browser for authorization...`),t("If browser does not open, visit:"),t(`${n.toString()}
27
- `);try{await P(n.toString())}catch(l){t(`Could not open a browser automatically: ${l?.message??"unknown"}`),t("Continue by opening the URL above manually.")}t(`Waiting for you to finish signing in (up to ${v} minutes)... this terminal continues automatically once you approve.`);const c=await i;t("Exchanging authorization code for tokens...");const a=await B(c,r.verifier,s),h=Date.now()+a.expires_in*1e3;await d(a.id_token,h),a.refresh_token?await f(a.refresh_token):(t(`
28
- \u26A0 No refresh_token returned by Google.`),t(" Your session will expire after ~1 hour and require"),t(" re-running `borg setup`. To enable auto-refresh:"),t(" 1. Visit https://myaccount.google.com/permissions"),t(' 2. Find "Borg MCP" and click "Remove access"'),t(" 3. Re-run `borg setup`"),t(` (Google will then issue a fresh refresh_token.)
29
- `)),t(`
23
+ `),o.close(),i(new Error("Missing authorization code"))}});o.listen(e,()=>{r(`Callback server listening on http://localhost:${e}`)}),setTimeout(()=>{o.close(),i(new Error(`Authentication timed out after ${v} minutes \u2014 no authorization received from the browser. Re-run \`borg setup\` and complete the Google sign-in in the page that opens.`))},y).unref()});return{port:e,codePromise:t}}async function B(e,t,s){const i=`http://localhost:${s}/callback`,o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:m,client_secret:_,code:e,code_verifier:t,grant_type:"authorization_code",redirect_uri:i})});if(!o.ok){const n=await o.text();throw new Error(`Failed to exchange code for tokens: ${n}`)}return await o.json()}async function b(e){try{await fetch($,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`token=${encodeURIComponent(e)}`})}catch{}}async function M(){r(`
24
+ \u25FC Borg MCP Authentication`),r(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
25
+ `);const e=await p();e&&(r("Revoking previous refresh_token to force fresh consent..."),await b(e)),r("Generating PKCE challenge...");const t=D();r("Starting local callback server...");const{port:s,codePromise:i}=await U(),o=`http://localhost:${s}/callback`,n=new k(L);n.searchParams.set("client_id",m),n.searchParams.set("redirect_uri",o),n.searchParams.set("response_type","code"),n.searchParams.set("scope",T.join(" ")),n.searchParams.set("code_challenge",t.challenge),n.searchParams.set("code_challenge_method","S256"),n.searchParams.set("access_type","offline"),n.searchParams.set("prompt","consent select_account"),r(`
26
+ \u{1F4F1} Opening browser for authorization...`),r("If browser does not open, visit:"),r(`${n.toString()}
27
+ `);try{await P(n.toString())}catch(u){r(`Could not open a browser automatically: ${u?.message??"unknown"}`),r("Continue by opening the URL above manually.")}r(`Waiting for you to finish signing in (up to ${v} minutes)... this terminal continues automatically once you approve.`);const c=await i;r("Exchanging authorization code for tokens...");const a=await B(c,t.verifier,s),h=Date.now()+a.expires_in*1e3;await d(a.id_token,h),a.refresh_token?await f(a.refresh_token):(r(`
28
+ \u26A0 No refresh_token returned by Google.`),r(" Your session will expire after ~1 hour and require"),r(" re-running `borg setup`. To enable auto-refresh:"),r(" 1. Visit https://myaccount.google.com/permissions"),r(' 2. Find "Borg MCP" and click "Remove access"'),r(" 3. Re-run `borg setup`"),r(` (Google will then issue a fresh refresh_token.)
29
+ `)),r(`
30
30
  \u25FC Authentication successful!
31
- `)}function H(e){return e?.noBrowser??O()}function b(e=process.env){const r=e.GOOGLE_DEVICE_CLIENT_ID?.trim(),s=e.GOOGLE_DEVICE_CLIENT_SECRET?.trim()||void 0;let i,o;if(r?(i=r,o=s):(i=A,o=S||void 0),!i)throw new Error('No-browser (device-grant) auth needs a Google "TVs & Limited Input devices" OAuth client. Set GOOGLE_DEVICE_CLIENT_ID in the environment, or run `borg setup` on a machine with a browser. See docs/REMOTE_TERMINAL_AUTH.md.');return{clientId:i,clientSecret:o,scopes:T}}function q(e){return new Promise(r=>setTimeout(r,e))}async function F(e={fetch,sleep:q},r=process.env){t(`
32
- \u25FC Borg MCP Authentication (no-browser mode)`),t(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
33
- `);const s=b(r),i=await p();i&&(t("Revoking previous refresh_token to force fresh consent..."),await C(i));const o=await I(s,e);t("To authorize Borg MCP on this machine:"),t(` 1. On any device with a browser, open: ${o.verification_url}`),t(` 2. Enter this code: ${o.user_code}
34
- `),t("Waiting for authorization (this page can be open on your phone or laptop)...");const n=await x(o,s,e),c=Date.now()+n.expires_in*1e3;await d(n.id_token,c),n.refresh_token?await f(n.refresh_token):(t(`
35
- \u26A0 No refresh_token returned by Google.`),t(" Your session will expire after ~1 hour and require re-running"),t(" `borg setup`. Re-consent at https://myaccount.google.com/permissions"),t(` (remove "Borg MCP") then re-run setup to restore automatic token refresh.
36
- `)),t(`
31
+ `)}function F(e){return e?.noBrowser??O()}function C(e=process.env){const t=e.GOOGLE_DEVICE_CLIENT_ID?.trim(),s=e.GOOGLE_DEVICE_CLIENT_SECRET?.trim()||void 0;let i,o;if(t?(i=t,o=s):(i=A,o=S||void 0),!i)throw new Error('No-browser (device-grant) auth needs a Google "TVs & Limited Input devices" OAuth client. Set GOOGLE_DEVICE_CLIENT_ID in the environment, or run `borg setup` on a machine with a browser. See docs/REMOTE_TERMINAL_AUTH.md.');return{clientId:i,clientSecret:o,scopes:T}}function H(e){return new Promise(t=>setTimeout(t,e))}async function q(e={fetch,sleep:H},t=process.env){r(`
32
+ \u25FC Borg MCP Authentication (no-browser mode)`),r(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
33
+ `);const s=C(t),i=await p();i&&(r("Revoking previous refresh_token to force fresh consent..."),await b(i));const o=await I(s,e);r("To authorize Borg MCP on this machine:"),r(` 1. On any device with a browser, open: ${o.verification_url}`),r(` 2. Enter this code: ${o.user_code}
34
+ `),r("Waiting for authorization (this page can be open on your phone or laptop)...");const n=await x(o,s,e),c=Date.now()+n.expires_in*1e3;await d(n.id_token,c),n.refresh_token?await f(n.refresh_token):(r(`
35
+ \u26A0 No refresh_token returned by Google.`),r(" Your session will expire after ~1 hour and require re-running"),r(" `borg setup`. Re-consent at https://myaccount.google.com/permissions"),r(` (remove "Borg MCP") then re-run setup to restore automatic token refresh.
36
+ `)),r(`
37
37
  \u25FC Authentication successful!
38
- `)}async function ee(e){return H(e)?F():M()}async function R(e,r,s){const i={client_id:r,refresh_token:e,grant_type:"refresh_token"};s&&(i.client_secret=s);let o;try{o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(i)})}catch(a){return{ok:!1,kind:"transient",error:new u(`Network failure during token refresh: ${a?.message??"unknown"}`)}}if(!o.ok){let a=null;try{a=await o.json()}catch{return{ok:!1,kind:"transient",error:new u(`Token refresh failed with HTTP ${o.status} (non-JSON body)`)}}return o.status===400&&a?.error==="invalid_grant"?{ok:!1,kind:"invalid",error:new G("invalid_grant",a.error_description)}:{ok:!1,kind:"transient",error:new u(`Token refresh failed with HTTP ${o.status}${a?.error?` (${a.error})`:""}`)}}let n;try{n=await o.json()}catch(a){return{ok:!1,kind:"transient",error:new u(`Token refresh response unparseable: ${a?.message??"unknown"}`)}}if(!n.id_token||typeof n.expires_in!="number")return{ok:!1,kind:"transient",error:new u("Token refresh response missing id_token or expires_in")};const c=Date.now()+n.expires_in*1e3;if(n.refresh_token){const a=await p();await f(n.refresh_token);try{await d(n.id_token,c)}catch(h){if(a)try{await f(a)}catch{}throw h}return{ok:!0}}return await d(n.id_token,c),{ok:!0}}async function te(e){const r=await R(e,m,k);if(r.ok)return;const s=b(),i=await R(e,s.clientId,s.clientSecret);if(!i.ok)throw r.kind==="invalid"&&i.kind==="invalid"?i.error:r.kind==="transient"?r.error:i.error}export{G as RefreshTokenInvalidError,u as RefreshTransientError,F as authenticateWithDeviceFlow,ee as authenticateWithGoogle,b as buildDeviceAuthConfig,te as refreshIdToken,H as shouldUseDeviceFlow};
38
+ `)}async function ee(e){return F(e)?q():M()}async function R(e,t,s){const i={client_id:t,refresh_token:e,grant_type:"refresh_token"};s&&(i.client_secret=s);let o;try{o=await fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(i)})}catch(a){return{ok:!1,kind:"transient",error:new l(`Network failure during token refresh: ${a?.message??"unknown"}`)}}if(!o.ok){let a=null;try{a=await o.json()}catch{return{ok:!1,kind:"transient",error:new l(`Token refresh failed with HTTP ${o.status} (non-JSON body)`)}}return o.status===400&&a?.error==="invalid_grant"?{ok:!1,kind:"invalid",error:new G("invalid_grant",a.error_description)}:{ok:!1,kind:"transient",error:new l(`Token refresh failed with HTTP ${o.status}${a?.error?` (${a.error})`:""}`)}}let n;try{n=await o.json()}catch(a){return{ok:!1,kind:"transient",error:new l(`Token refresh response unparseable: ${a?.message??"unknown"}`)}}if(!n.id_token||typeof n.expires_in!="number")return{ok:!1,kind:"transient",error:new l("Token refresh response missing id_token or expires_in")};const c=Date.now()+n.expires_in*1e3;try{if(n.refresh_token){const a=await p();await f(n.refresh_token);try{await d(n.id_token,c)}catch(h){if(a)try{await f(a)}catch{}throw h}}else await d(n.id_token,c);return{ok:!0}}catch(a){return{ok:!1,kind:"transient",persistFailed:!0,error:new l(`Token refresh succeeded but saving it to the credential store failed (the keychain may be locked, or a background process can't write it): ${a?.message??"unknown"}`)}}}async function te(e){const t=await R(e,m,_);if(t.ok)return;if(t.kind==="transient"&&t.persistFailed)throw t.error;const s=C(),i=await R(e,s.clientId,s.clientSecret);if(!i.ok)throw t.kind==="invalid"&&i.kind==="invalid"?i.error:t.kind==="transient"?t.error:i.error}export{G as RefreshTokenInvalidError,l as RefreshTransientError,q as authenticateWithDeviceFlow,ee as authenticateWithGoogle,C as buildDeviceAuthConfig,te as refreshIdToken,F as shouldUseDeviceFlow};
@@ -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};
@@ -1 +1 @@
1
- import{getIdToken as f,getRefreshToken as w,clearTokens as m}from"./config.js";import{refreshIdToken as j,RefreshTokenInvalidError as g,RefreshTransientError as x}from"./auth.js";import{consolePrefix as R}from"./console-prefix.js";import{debugLog as T}from"./debug.js";import{assertUuidShape as $}from"./evict-drone.js";const E=process.env.BORG_API_URL||"https://api.borgmcp.ai",k=3,C=6e4;let l=null;function b(e){return l||(l=j(e).finally(()=>{l=null}),l)}function P(e){if(e==null)return null;const n=e.trim();return/^\d+$/.test(n)?parseInt(n,10)*1e3:null}function O(e,n,t=C,o=()=>Math.random()*500){const s=e??1e3*(n+1);return Math.min(s,t)+o()}function I(e){const n=(t,o)=>`${t}${/[.:!?]$/.test(t)?"":":"} ${o}`;try{const t=JSON.parse(e);if(typeof t?.error=="string")return typeof t.details=="string"?n(t.error,t.details):t.error;if(t?.error&&typeof t.error=="object"){const o=t.error.message,s=t.error.details??t.details;if(typeof o=="string"&&typeof s=="string")return n(o,s);if(typeof o=="string")return o}if(typeof t?.message=="string"&&typeof t?.details=="string")return n(t.message,t.details);if(typeof t?.message=="string")return t.message}catch{}return e}async function A(e,n,t){const o=t.maxRetries??k;let s=e,a=0;for(;s.status===429&&a<o;){const p=O(P(s.headers.get("Retry-After")),a,t.capMs,t.jitter);t.log?.(`rate limited (429); retrying in ${Math.round(p)}ms (attempt ${a+1}/${o})`),await t.sleep(p),a++,s=await n()}return s}function L(e){return new Promise(n=>setTimeout(n,e))}async function J(){let e=await f();if(!e){const n=await w(),t=n!=null;if(n)try{await b(n),e=await f()}catch(o){if(o instanceof g&&await m(),o instanceof x)throw o}if(!e)throw new Error(t?"Authentication expired \u2014 your saved login has expired. Run: borg setup":"Authentication required \u2014 you are not signed in. Run: borg setup")}return e}async function G(){const e=await w();if(!e)return null;try{return await b(e),await f()}catch(n){if(n instanceof g&&await m(),n instanceof x)throw n;return null}}async function q(){if(await f())return"valid";const n=await w();if(!n)return"dead";try{return await b(n),await f()?"valid":"transient"}catch(t){return t instanceof g?(await m(),"dead"):"transient"}}async function r(e,n={}){let t=await J();const{droneSession:o,apiUrl:s,headers:a,...p}=n,_=s??E,y=(p.method??"GET").toUpperCase(),h=async c=>{const d={Authorization:`Bearer ${c}`,...a};o&&(d["X-Drone-Session"]=o),T(`\u2192 ${y} ${e}`);const u=await fetch(`${_}${e}`,{...p,headers:d});return T(`\u2190 ${u.status} ${y} ${e}`),u};let i=await h(t);if(i.status===401){const c=await G();c&&(t=c,i=await h(t))}if(i.status===401)throw new Error("Authentication required. Run: borg setup");if(i.status===429&&(i=await A(i,()=>h(t),{sleep:L,log:c=>console.error(`${R()}${c}`)})),!i.ok){const c=await i.text();T(`\u2717 ${i.status} ${y} ${e}: ${c}`);const d=I(c);if(i.status===429){const u=i.headers.get("Retry-After"),S=u?` (retry after ${u}s)`:"";throw new Error(`HTTP 429: rate limited${S}: ${d}`)}throw new Error(`HTTP ${i.status}: ${d}`)}return i}async function B(e,n,t,o){const s={hostname:t??null};return(o==="claude"||o==="codex")&&(s.agent_kind=o),typeof e=="string"?s.cube_name=e:(e.cube_id&&(s.cube_id=e.cube_id),e.cube_name&&(s.cube_name=e.cube_name),e.role_id&&(s.role_id=e.role_id),e.role_name&&(s.role_name=e.role_name),e.prior_drone_id&&(s.prior_drone_id=e.prior_drone_id)),await(await r("/api/assimilate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s),apiUrl:n})).json()}async function N(e,n){return await(await r("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:n})).json()}async function F(e,n){return await(await r("/api/drone/role",{method:"GET",droneSession:e,apiUrl:n})).json()}async function X(e,n){return await(await r("/api/drone/whoami",{method:"GET",droneSession:e,apiUrl:n})).json()}async function W(e,n,t){const o=t?`?since=${encodeURIComponent(t)}`:"";return await(await r(`/api/drone/roster${o}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function z(e,n,t={}){const o=new URLSearchParams;t.since&&o.set("since",t.since),t.limit!==void 0&&o.set("limit",String(t.limit)),t.unreadOnly&&o.set("unread_only","true");const s=o.toString();return await(await r(`/api/drone/log${s?`?${s}`:""}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function Q(e,n,t){await r(`/api/drone/log/${t}/ack`,{method:"POST",body:JSON.stringify({kind:"ack"}),droneSession:e,apiUrl:n})}async function V(e,n,t={}){const o=new URLSearchParams;t.since&&o.set("since",t.since);const s=o.toString();return await(await r(`/api/drone/regen${s?`?${s}`:""}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function Y(e,n,t,o){const s=new URLSearchParams({role:t,section:o});return await(await r(`/api/drone/role-rationale?${s.toString()}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function Z(e,n,t,o={}){const s={message:t,...o.visibility?{visibility:o.visibility}:{},...o.recipientDroneIds?{recipientDroneIds:o.recipientDroneIds}:{},...o.class?{class:o.class}:{},...o.to?{to:o.to}:{}};return await(await r("/api/drone/log",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:n,body:JSON.stringify(s)})).json()}async function K(e,n,t){const o={kind:t.kind??"friction",message:t.message,...t.metadata?{metadata:t.metadata}:{}};return await(await r("/api/drone/report",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:n,body:JSON.stringify(o)})).json()}async function ee(){return await(await r("/api/cubes",{method:"GET"})).json()}async function te(){return await(await r("/api/templates",{method:"GET"})).json()}async function ne(e,n,t){const o={cube_directive:n};e&&(o.name=e),t?.template&&(o.template=t.template),t&&Object.prototype.hasOwnProperty.call(t,"message_taxonomy")&&(o.message_taxonomy=t.message_taxonomy??null);const a=await(await r("/api/cubes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();return a.cube?{...a.cube,roles:a.roles??[],drones:a.drones??[]}:a}async function oe(e,n){return await(await r(`/api/cubes/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function se(e,n){return await(await r(`/api/cubes/${e}/taxonomy-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function re(e){await r(`/api/cubes/${e}`,{method:"DELETE"})}async function ae(e,n){return await(await r(`/api/cubes/${e}/roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ie(e,n){return await(await r(`/api/roles/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ce(e,n){return await(await r(`/api/roles/${e}/section-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function pe(e){await r(`/api/roles/${e}`,{method:"DELETE"})}async function de(e,n){return $(e,"drone_id"),await(await r(`/api/drones/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role_id:n})})).json()}async function ue(e){$(e,"drone_id"),await r(`/api/drones/${e}`,{method:"DELETE"})}async function fe(e){const t=await(await r(`/api/cubes/${e}`,{method:"GET"})).json();return t.cube?{...t.cube,roles:t.roles??[],drones:t.drones??[]}:t}async function le(e,n){return await(await r(`/api/cubes/${e}/apply-template`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:n})})).json()}async function ye(){return await(await r("/api/subscription/status",{method:"GET"})).json()}async function he(e,n="software-dev",t=!1,o){return await(await r(`/api/cubes/${e}/sync-roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:n,apply:t,...o?{decisions:o}:{}})})).json()}async function we(){const n=await(await r("/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!n.checkout_url)throw new Error("No checkout URL in response");return n.checkout_url}async function me(){const n=await(await r("/api/subscription/portal",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!n.portal_url)throw new Error(n.message||"No portal URL in response");return n.portal_url}export{E as API_URL,Q as ackLogEntry,Z as appendLog,le as applyTemplate,B as assimilate,ye as checkSubscriptionStatus,me as createBillingPortalSession,ne as createCube,ae as createRole,we as createSubscription,re as deleteCube,pe as deleteRole,ue as evictDrone,I as extractHttpErrorMessage,fe as getCube,N as getCubeInfo,F as getRoleInfo,W as getRoster,J as getValidToken,ee as listCubes,te as listTemplates,P as parseRetryAfterMs,ce as patchRoleSection,se as patchTaxonomyClass,q as probeSession,O as rateLimitWaitMs,z as readLog,de as reassignDrone,V as regen,A as retryOn429,Y as roleRationale,K as submitReport,he as syncRoles,oe as updateCube,ie as updateRole,X as whoami};
1
+ import{getIdToken as f,getRefreshToken as w,clearTokens as m}from"./config.js";import{refreshIdToken as j,RefreshTokenInvalidError as g,RefreshTransientError as T}from"./auth.js";import{consolePrefix as R}from"./console-prefix.js";import{debugLog as b}from"./debug.js";import{assertUuidShape as $}from"./evict-drone.js";const E=process.env.BORG_API_URL||"https://api.borgmcp.ai",k=3,C=6e4;let l=null;function x(e){return l||(l=j(e).finally(()=>{l=null}),l)}function P(e){if(e==null)return null;const n=e.trim();return/^\d+$/.test(n)?parseInt(n,10)*1e3:null}function O(e,n,t=C,o=()=>Math.random()*500){const s=e??1e3*(n+1);return Math.min(s,t)+o()}function I(e){const n=(t,o)=>`${t}${/[.:!?]$/.test(t)?"":":"} ${o}`;try{const t=JSON.parse(e);if(typeof t?.error=="string")return typeof t.details=="string"?n(t.error,t.details):t.error;if(t?.error&&typeof t.error=="object"){const o=t.error.message,s=t.error.details??t.details;if(typeof o=="string"&&typeof s=="string")return n(o,s);if(typeof o=="string")return o}if(typeof t?.message=="string"&&typeof t?.details=="string")return n(t.message,t.details);if(typeof t?.message=="string")return t.message}catch{}return e}async function A(e,n,t){const o=t.maxRetries??k;let s=e,a=0;for(;s.status===429&&a<o;){const p=O(P(s.headers.get("Retry-After")),a,t.capMs,t.jitter);t.log?.(`rate limited (429); retrying in ${Math.round(p)}ms (attempt ${a+1}/${o})`),await t.sleep(p),a++,s=await n()}return s}function L(e){return new Promise(n=>setTimeout(n,e))}async function J(){let e=await f();if(!e){const n=await w(),t=n!=null;if(n)try{await x(n),e=await f()}catch(o){if(o instanceof g)await m();else throw o instanceof T?o:new T(`Token refresh failed unexpectedly (your saved login was NOT cleared \u2014 retry; if it persists, restart the borg session): ${o?.message??"unknown"}`)}if(!e)throw new Error(t?"Authentication expired \u2014 your saved login has expired. Run: borg setup":"Authentication required \u2014 you are not signed in. Run: borg setup")}return e}async function G(){const e=await w();if(!e)return null;try{return await x(e),await f()}catch(n){if(n instanceof g&&await m(),n instanceof T)throw n;return null}}async function q(){if(await f())return"valid";const n=await w();if(!n)return"dead";try{return await x(n),await f()?"valid":"transient"}catch(t){return t instanceof g?(await m(),"dead"):"transient"}}async function r(e,n={}){let t=await J();const{droneSession:o,apiUrl:s,headers:a,...p}=n,_=s??E,y=(p.method??"GET").toUpperCase(),h=async c=>{const d={Authorization:`Bearer ${c}`,...a};o&&(d["X-Drone-Session"]=o),b(`\u2192 ${y} ${e}`);const u=await fetch(`${_}${e}`,{...p,headers:d});return b(`\u2190 ${u.status} ${y} ${e}`),u};let i=await h(t);if(i.status===401){const c=await G();c&&(t=c,i=await h(t))}if(i.status===401)throw new Error("Authentication required. Run: borg setup");if(i.status===429&&(i=await A(i,()=>h(t),{sleep:L,log:c=>console.error(`${R()}${c}`)})),!i.ok){const c=await i.text();b(`\u2717 ${i.status} ${y} ${e}: ${c}`);const d=I(c);if(i.status===429){const u=i.headers.get("Retry-After"),S=u?` (retry after ${u}s)`:"";throw new Error(`HTTP 429: rate limited${S}: ${d}`)}throw new Error(`HTTP ${i.status}: ${d}`)}return i}async function N(e,n,t,o){const s={hostname:t??null};return(o==="claude"||o==="codex")&&(s.agent_kind=o),typeof e=="string"?s.cube_name=e:(e.cube_id&&(s.cube_id=e.cube_id),e.cube_name&&(s.cube_name=e.cube_name),e.role_id&&(s.role_id=e.role_id),e.role_name&&(s.role_name=e.role_name),e.prior_drone_id&&(s.prior_drone_id=e.prior_drone_id)),await(await r("/api/assimilate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s),apiUrl:n})).json()}async function B(e,n){return await(await r("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:n})).json()}async function F(e,n){return await(await r("/api/drone/role",{method:"GET",droneSession:e,apiUrl:n})).json()}async function X(e,n){return await(await r("/api/drone/whoami",{method:"GET",droneSession:e,apiUrl:n})).json()}async function W(e,n,t){const o=t?`?since=${encodeURIComponent(t)}`:"";return await(await r(`/api/drone/roster${o}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function z(e,n,t={}){const o=new URLSearchParams;t.since&&o.set("since",t.since),t.limit!==void 0&&o.set("limit",String(t.limit)),t.unreadOnly&&o.set("unread_only","true");const s=o.toString();return await(await r(`/api/drone/log${s?`?${s}`:""}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function Q(e,n,t){await r(`/api/drone/log/${t}/ack`,{method:"POST",body:JSON.stringify({kind:"ack"}),droneSession:e,apiUrl:n})}async function V(e,n,t={}){const o=new URLSearchParams;t.since&&o.set("since",t.since);const s=o.toString();return await(await r(`/api/drone/regen${s?`?${s}`:""}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function Y(e,n,t,o){const s=new URLSearchParams({role:t,section:o});return await(await r(`/api/drone/role-rationale?${s.toString()}`,{method:"GET",droneSession:e,apiUrl:n})).json()}async function Z(e,n,t,o={}){const s={message:t,...o.visibility?{visibility:o.visibility}:{},...o.recipientDroneIds?{recipientDroneIds:o.recipientDroneIds}:{},...o.class?{class:o.class}:{},...o.to?{to:o.to}:{}};return await(await r("/api/drone/log",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:n,body:JSON.stringify(s)})).json()}async function K(e,n,t){const o={kind:t.kind??"friction",message:t.message,...t.metadata?{metadata:t.metadata}:{}};return await(await r("/api/drone/report",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:n,body:JSON.stringify(o)})).json()}async function ee(){return await(await r("/api/cubes",{method:"GET"})).json()}async function te(){return await(await r("/api/templates",{method:"GET"})).json()}async function ne(e,n,t){const o={cube_directive:n};e&&(o.name=e),t?.template&&(o.template=t.template),t&&Object.prototype.hasOwnProperty.call(t,"message_taxonomy")&&(o.message_taxonomy=t.message_taxonomy??null);const a=await(await r("/api/cubes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();return a.cube?{...a.cube,roles:a.roles??[],drones:a.drones??[]}:a}async function oe(e,n){return await(await r(`/api/cubes/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function se(e,n){return await(await r(`/api/cubes/${e}/taxonomy-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function re(e){await r(`/api/cubes/${e}`,{method:"DELETE"})}async function ae(e,n){return await(await r(`/api/cubes/${e}/roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ie(e,n){return await(await r(`/api/roles/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ce(e,n){return await(await r(`/api/roles/${e}/section-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function pe(e){await r(`/api/roles/${e}`,{method:"DELETE"})}async function de(e,n){return $(e,"drone_id"),await(await r(`/api/drones/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role_id:n})})).json()}async function ue(e){$(e,"drone_id"),await r(`/api/drones/${e}`,{method:"DELETE"})}async function fe(e){const t=await(await r(`/api/cubes/${e}`,{method:"GET"})).json();return t.cube?{...t.cube,roles:t.roles??[],drones:t.drones??[]}:t}async function le(e,n){return await(await r(`/api/cubes/${e}/apply-template`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:n})})).json()}async function ye(){return await(await r("/api/subscription/status",{method:"GET"})).json()}async function he(e,n="software-dev",t=!1,o){return await(await r(`/api/cubes/${e}/sync-roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:n,apply:t,...o?{decisions:o}:{}})})).json()}async function we(){const n=await(await r("/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!n.checkout_url)throw new Error("No checkout URL in response");return n.checkout_url}async function me(){const n=await(await r("/api/subscription/portal",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!n.portal_url)throw new Error(n.message||"No portal URL in response");return n.portal_url}export{E as API_URL,Q as ackLogEntry,Z as appendLog,le as applyTemplate,N as assimilate,ye as checkSubscriptionStatus,me as createBillingPortalSession,ne as createCube,ae as createRole,we as createSubscription,re as deleteCube,pe as deleteRole,ue as evictDrone,I as extractHttpErrorMessage,fe as getCube,B as getCubeInfo,F as getRoleInfo,W as getRoster,J as getValidToken,ee as listCubes,te as listTemplates,P as parseRetryAfterMs,ce as patchRoleSection,se as patchTaxonomyClass,q as probeSession,O as rateLimitWaitMs,z as readLog,de as reassignDrone,V as regen,A as retryOn429,Y as roleRationale,K as submitReport,he as syncRoles,oe as updateCube,ie as updateRole,X as whoami};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
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",