borgmcp 1.0.22 → 1.0.24

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};
@@ -32,6 +32,8 @@ export declare function probeCodexBridgeArmed(active: {
32
32
  getCodexWakeTarget?: typeof getCodexWakeTarget;
33
33
  checkBridge?: typeof checkCodexBridgeHealthy;
34
34
  }): Promise<boolean | null>;
35
+ /** gh#857 WI-2: last successful wake delivery time (for the heartbeat gate). */
36
+ export declare function getLastDeliveredAt(): number | null;
35
37
  export interface CodexWakeDeps {
36
38
  getActiveCube?: typeof getActiveCube;
37
39
  getCodexWakeTarget?: typeof getCodexWakeTarget;
@@ -41,7 +43,44 @@ export interface CodexWakeDeps {
41
43
  cwd?: () => string;
42
44
  sleep?: (ms: number) => Promise<void>;
43
45
  now?: () => number;
46
+ jitter?: () => number;
47
+ maxAttempts?: number;
44
48
  }
45
49
  export declare function wakeCodexViaAppServer(reason?: string, env?: NodeJS.ProcessEnv, deps?: CodexWakeDeps): void;
50
+ /**
51
+ * gh#857 WI-2: codex /loop-equivalent heartbeat cadence. Tighter than claude's
52
+ * ~30-min /loop ScheduleWakeup because codex has no per-entry inbox-Monitor
53
+ * backstop — this periodic drain IS the independent second safety net.
54
+ */
55
+ export declare const CODEX_HEARTBEAT_CADENCE_MS: number;
56
+ /**
57
+ * gh#857 WI-2: one tick of the codex /loop-equivalent heartbeat — a periodic,
58
+ * independent re-engagement that injects a borg:read-log-unread DRAIN turn so an
59
+ * idle codex drone re-syncs even if every per-entry wake was missed. SKIPS when a
60
+ * delivery (per-entry wake, retry-drain, or a prior heartbeat) already landed
61
+ * within the cadence window (shouldFireHeartbeat), so an active cube with flowing
62
+ * wakes never gets a redundant injection. Unlike the per-entry path it does NOT
63
+ * consult deliveredWakeKeys — the cadence gate is the throttle, and the static
64
+ * drain prompt is intentionally re-delivered each idle window. Best-effort: a
65
+ * mid-turn thread / transient error / unresolved target just skips this tick (the
66
+ * next tick retries). Never throws.
67
+ */
68
+ export declare function fireCodexHeartbeatTick(deps?: CodexWakeDeps, cadenceMs?: number): Promise<void>;
69
+ /**
70
+ * gh#857 WI-2: start the codex /loop-equivalent heartbeat — a setInterval firing
71
+ * fireCodexHeartbeatTick every cadence. CODEX-ONLY: claude wakes via the tail-F
72
+ * inbox Monitor + /loop ScheduleWakeup and has NO app-server socket to inject
73
+ * into, so the heartbeat is intrinsically a codex mechanism. The gate reads
74
+ * agentKind LOCALLY from this child's own env (resolveSessionAgentKind →
75
+ * BORG_CODEX_REMOTE_WAKE), never a mutable/server-recorded field, so a mislabel
76
+ * can't silently defeat the backstop (gh#633 lesson). The timer is unref'd so the
77
+ * heartbeat alone never keeps the process alive. Returns the timer, or null when
78
+ * not a codex session. Injectable agentKind/intervalMs/tick for tests.
79
+ */
80
+ export declare function startCodexHeartbeat(opts?: {
81
+ agentKind?: 'claude' | 'codex';
82
+ intervalMs?: number;
83
+ tick?: () => void;
84
+ }): ReturnType<typeof setInterval> | null;
46
85
  export declare function resetCodexWakeForTests(): void;
47
86
  //# sourceMappingURL=codex-app-wake.d.ts.map
@@ -1,2 +1,2 @@
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
+ import{getActiveCube as x,getCodexWakeTarget as C,setCodexWakeTarget as P}from"./cubes.js";import{CodexAppServerClient as _}from"./codex-app-server.js";import{checkCodexBridgeHealthy as D}from"./codex-remote.js";import{recordEventReceipt as A}from"./health-beat.js";import{codexAppServerSocketFromEnv as R,pickFreshThread as K,wakeTargetChanged as M,wakeRetryBackoffMs as S,wakeRetryExpired as B,WAKE_RETRY_MAX_ATTEMPTS as O,shouldFireHeartbeat as F}from"./codex-wake-resolve.js";const H="New Borg cube-log activity arrived.";function re(e){return`New Borg cube-log activity arrived:
2
+ ${e}`}const b="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.";function E(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function X(e=process.env){return E(e)?"codex":"claude"}function N(e=process.env){return E(e)?{enabled:!0}:{enabled:!1}}async function ne(e,t={}){try{const n=await(t.getCodexWakeTarget??C)(e.cubeId,e.droneId);return n?(t.checkBridge??D)(n.socketPath):!1}catch{return null}}let f=!1;const h=[],u=new Set,w=[],L=100;let g=!1,k=null;function ae(){return k}function p(e){k=(e.now??Date.now)()}let v=!1;function $(e){return new Promise(t=>setTimeout(t,e))}function y(e,t){return t.createClient?t.createClient(e):new _(e)}async function T(e,t){const r=R(t.env??process.env);if(r){const a=y(r,t);await a.connect();try{const o=await a.loadedThreadIds(),i=[];for(const d of o){const s=await a.readThread(d);s&&i.push({id:s.id,cwd:s.cwd,updatedAt:s.updatedAt})}const c=K(i,{cwd:(t.cwd??(()=>process.cwd()))()});return c?(await j(e,{socketPath:r,threadId:c},t),{socketPath:r,threadId:c}):null}finally{a.close()}}const n=await(t.getCodexWakeTarget??C)(e.cubeId,e.droneId);return n?{socketPath:n.socketPath,threadId:n.threadId}:null}async function j(e,t,r){try{const n=r.getCodexWakeTarget??C,a=r.setCodexWakeTarget??P,o=await n(e.cubeId,e.droneId),i=o?{socketPath:o.socketPath,threadId:o.threadId}:null;M(i,t)&&await a(e.cubeId,e.droneId,t)}catch{}}function oe(e=H,t=process.env,r={}){N(t).enabled&&(h.push({reason:e,deps:r}),!f&&(f=!0,q().finally(()=>{f=!1})))}async function q(){for(;h.length>0;){const e=h.shift();await V(e.reason,e.deps)}}async function V(e,t){try{const r=await(t.getActiveCube??x)();if(!r)return;const n=await T(r,t);if(!n)return;const{socketPath:a,threadId:o}=n,i=`${o}\0${e}`;if(u.has(i))return;const c=y(a,t);await c.connect();try{if((await c.readThread(o))?.status?.type==="active"){I(t);return}await c.startTurn(o,e),A(),Q(i),p(t)}finally{c.close()}}catch{I(t)}}function I(e){g||(g=!0,Y(e).finally(()=>{g=!1}))}async function Y(e){const t=e.sleep??$,r=e.now??Date.now,n=e.jitter??(()=>Math.random()*500),a=e.maxAttempts??O,o=r();let i=0;for(;!B(o,r())&&i<a;){await t(S(i,n())),i++;try{const c=await(e.getActiveCube??x)();if(!c)continue;const d=await T(c,e);if(!d)continue;const{socketPath:s,threadId:m}=d,l=y(s,e);await l.connect();try{if((await l.readThread(m))?.status?.type==="active")continue;await l.startTurn(m,b),A(),p(e);return}finally{l.close()}}catch{}}}const W=20*6e4;async function G(e={},t=W){if(v)return;const r=(e.now??Date.now)();if(F(k,r,t)){v=!0;try{const n=await(e.getActiveCube??x)();if(!n)return;const a=await T(n,e);if(!a)return;const o=y(a.socketPath,e);await o.connect();try{if((await o.readThread(a.threadId))?.status?.type==="active")return;await o.startTurn(a.threadId,b),p(e)}finally{o.close()}}catch{}finally{v=!1}}}function ie(e={}){if((e.agentKind??X())!=="codex")return null;const r=e.intervalMs??W,n=e.tick??(()=>{G()}),a=setInterval(n,r);return a.unref?.(),a}function ce(){f=!1,h.length=0,u.clear(),w.length=0,g=!1,k=null,v=!1}function Q(e){if(!u.has(e))for(u.add(e),w.push(e);w.length>L;){const t=w.shift();t&&u.delete(t)}}export{b as CODEX_CATCHUP_PROMPT,W as CODEX_HEARTBEAT_CADENCE_MS,H as CODEX_WAKE_PROMPT,G as fireCodexHeartbeatTick,re as formatCodexWakePrompt,ae as getLastDeliveredAt,E as isCodexRemoteWakeEnabled,ne as probeCodexBridgeArmed,ce as resetCodexWakeForTests,N as resolveCodexWakeTarget,X as resolveSessionAgentKind,ie as startCodexHeartbeat,oe as wakeCodexViaAppServer};
@@ -66,4 +66,38 @@ export declare function wakeTargetChanged(existing: {
66
66
  socketPath: string;
67
67
  threadId: string;
68
68
  }): boolean;
69
+ /** Base backoff for the first retry of a dropped/deferred wake. */
70
+ export declare const WAKE_RETRY_BASE_MS = 5000;
71
+ /** Backoff ceiling — a wedged thread is retried at most this often. */
72
+ export declare const WAKE_RETRY_CAP_MS = 60000;
73
+ /**
74
+ * Age cap: a pending wake older than this is given up (dropped from the queue).
75
+ * Generous on purpose — the WI-2 heartbeat is the backstop beyond it, and the
76
+ * server read-cursor means the next delivery (heartbeat or a fresh entry) drains
77
+ * everything anyway, so an aged-out single wake is never a permanent miss.
78
+ */
79
+ export declare const WAKE_RETRY_MAX_AGE_MS: number;
80
+ /**
81
+ * Hard iteration ceiling for the retry-drain loop — a defensive belt ALONGSIDE
82
+ * the time-based age cap. In prod the age cap (real clock) terminates the loop in
83
+ * ~45-50 attempts; this ceiling only matters if the clock fails to advance
84
+ * (pathological / a non-advancing injected clock in tests) where a time-only
85
+ * guard would hot-spin forever. Set far above any real attempt count.
86
+ */
87
+ export declare const WAKE_RETRY_MAX_ATTEMPTS = 1000;
88
+ /**
89
+ * Exponential backoff (ms) for the Nth retry of a pending wake (0-based),
90
+ * doubling from WAKE_RETRY_BASE_MS, saturating at WAKE_RETRY_CAP_MS, plus
91
+ * caller-supplied jitter so co-located sibling drones don't retry in lockstep.
92
+ */
93
+ export declare function wakeRetryBackoffMs(attempts: number, jitter?: number): number;
94
+ /** True once a pending wake has outlived the age cap (give up; heartbeat backstops). */
95
+ export declare function wakeRetryExpired(firstEnqueuedAt: number, now: number, maxAgeMs?: number): boolean;
96
+ /**
97
+ * WI-2 double-fire avoidance: the periodic heartbeat fires only when no wake (or
98
+ * prior heartbeat) delivery landed within the cadence window — so an active cube
99
+ * with flowing per-entry wakes doesn't get redundant heartbeat injections. A
100
+ * never-delivered seat (null) always fires.
101
+ */
102
+ export declare function shouldFireHeartbeat(lastDeliveredAt: number | null, now: number, cadenceMs: number): boolean;
69
103
  //# sourceMappingURL=codex-wake-resolve.d.ts.map
@@ -1 +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};
1
+ const u="BORG_CODEX_APP_SERVER_SOCKET";function a(t=process.env){const e=t[u];return e&&e.length>0?e:null}function f(t){return["-c",`mcp_servers.borg.env.${u}="${t}"`]}function E(t,e){if(t.length===0)return null;if(t.length===1)return t[0].id;const r=t.filter(o=>o.cwd===e.cwd),n=r.length>0?r:t;let c=n[0];for(const o of n)(o.updatedAt??0)>(c.updatedAt??0)&&(c=o);return c.id}function i(t,e){const r={};let n=!1;for(const[c,o]of Object.entries(t)){if(e(o.socketPath)===!1){n=!0;continue}r[c]=o}return{targets:r,changed:n}}function d(t,e){return!t||t.socketPath!==e.socketPath||t.threadId!==e.threadId}const p=5e3,_=6e4,s=45*6e4,x=1e3;function A(t,e=0){const r=Math.max(0,t),n=p*2**r;return Math.min(n,_)+e}function R(t,e,r=s){return e-t>=r}function l(t,e,r){return t===null?!0:e-t>=r}export{u as BORG_CODEX_APP_SERVER_SOCKET_ENV,p as WAKE_RETRY_BASE_MS,_ as WAKE_RETRY_CAP_MS,s as WAKE_RETRY_MAX_AGE_MS,x as WAKE_RETRY_MAX_ATTEMPTS,f as codexAppServerSocketConfigArgs,a as codexAppServerSocketFromEnv,E as pickFreshThread,i as pruneDeadWakeTargets,l as shouldFireHeartbeat,A as wakeRetryBackoffMs,R as wakeRetryExpired,d as wakeTargetChanged};
@@ -61,14 +61,18 @@ export declare function getStreamStatus(): StreamStatus & {
61
61
  */
62
62
  export declare function __resetStreamStateForTest(): void;
63
63
  /**
64
- * Spawn the background SSE consumer loop. Fire-and-forget; the loop
65
- * runs until process exit. Errors are written to stderr (so they don't
66
- * pollute the MCP stdio channel) and the loop continues.
67
- *
68
- * Idempotent in the sense that calling twice would create two parallel
69
- * loops; the caller (`index.ts`) wires this once at startup.
64
+ * gh#857 WI-2: start the codex /loop-equivalent heartbeat AT MOST ONCE — a
65
+ * re-entrant startLogStream must not leak a second interval (idempotent start).
66
+ * No-op for claude (startCodexHeartbeat returns null on a non-codex session, so
67
+ * no timer is ever stored). Extracted + injectable (`start`) so the production
68
+ * wiring is unit-testable without running the real stream loop (QA e75339e7).
70
69
  */
71
- export declare function startLogStream(): void;
70
+ export declare function ensureCodexHeartbeatStarted(start?: () => ReturnType<typeof setInterval> | null): void;
71
+ /** Test-only: stop + clear the heartbeat timer so idempotence is re-testable. */
72
+ export declare function __resetCodexHeartbeatForTest(): void;
73
+ export declare function startLogStream(opts?: {
74
+ runForever?: () => void;
75
+ }): void;
72
76
  export interface StreamDeps {
73
77
  /** Override the global fetch (tests inject a controlled Response). */
74
78
  fetchImpl?: typeof fetch;
@@ -1,11 +1,11 @@
1
- import{promises as h}from"node:fs";import z from"node:os";import x from"node:path";import{getActiveCube as Q,inboxPathForDrone as B}from"./cubes.js";import{formatCodexWakePrompt as Y,resolveSessionAgentKind as Z,wakeCodexViaAppServer as ee}from"./codex-app-wake.js";import{getValidToken as te}from"./remote-client.js";import{recordEventReceipt as ne,emitHealthBeat as re,getCachedMonitorHealthy as ae,getCachedWakeArmed as oe}from"./health-beat.js";import{getPackageVersion as ie}from"./version.js";import{acquireStreamLease as G,readOwnershipSnapshot as b}from"./stream-owner.js";const j=9e4,se=2e3,ce=500,le=3e4,de=50,R=512,Oe=R*2;function ue(){try{const e=z.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const fe=Date.now(),i={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function pe(e,t=Date.now()-fe){return e.connected&&e.lastWireActivityAt?"connected":!e.connected&&e.reconnectAttempts>0?"reconnecting":!e.connected&&!e.lastWireActivityAt&&e.reconnectAttempts===0&&t>1e4?"silent-inert":"never-started"}function $e(){return{...i,runLoopHealth:pe(i)}}function ve(){i.connected=!1,i.lastWireActivityAt=null,i.lastContentEventAt=null,i.lastHeartbeatAt=null,i.lastPersistedEventId=null,i.reconnectAttempts=0,i.runLoopRestartCount=0,i.ownership={state:"unowned"}}function Pe(){(async()=>{for(;;){try{await we(),process.stderr.write(`[borg-mcp log stream] runLoop returned unexpectedly; restarting in 5s
1
+ import{promises as h}from"node:fs";import Q from"node:os";import C from"node:path";import{getActiveCube as Y,inboxPathForDrone as G}from"./cubes.js";import{formatCodexWakePrompt as Z,resolveSessionAgentKind as ee,startCodexHeartbeat as te,wakeCodexViaAppServer as ne}from"./codex-app-wake.js";import{getValidToken as re}from"./remote-client.js";import{recordEventReceipt as ae,emitHealthBeat as oe,getCachedMonitorHealthy as ie,getCachedWakeArmed as se}from"./health-beat.js";import{getPackageVersion as ce}from"./version.js";import{acquireStreamLease as j,readOwnershipSnapshot as b}from"./stream-owner.js";const K=9e4,le=2e3,de=500,ue=3e4,fe=50,v=512,Pe=v*2;function pe(){try{const e=Q.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const me=Date.now(),i={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function we(e,t=Date.now()-me){return e.connected&&e.lastWireActivityAt?"connected":!e.connected&&e.reconnectAttempts>0?"reconnecting":!e.connected&&!e.lastWireActivityAt&&e.reconnectAttempts===0&&t>1e4?"silent-inert":"never-started"}function We(){return{...i,runLoopHealth:we(i)}}function Be(){i.connected=!1,i.lastWireActivityAt=null,i.lastContentEventAt=null,i.lastHeartbeatAt=null,i.lastPersistedEventId=null,i.reconnectAttempts=0,i.runLoopRestartCount=0,i.ownership={state:"unowned"}}let A=null;function he(e=te){A||(A=e())}function Ge(){A&&clearInterval(A),A=null}function ge(){(async()=>{for(;;){try{await Ie(),process.stderr.write(`[borg-mcp log stream] runLoop returned unexpectedly; restarting in 5s
2
2
  `)}catch(e){process.stderr.write(`[borg-mcp log stream] runLoop threw: ${e?.message??e}; restarting in 5s
3
- `)}i.runLoopRestartCount+=1,await I(5e3)}})()}const K={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:_e,hasInboxEntryId:Te,getToken:te,wakeCodex:ee,heartbeatTimeoutMs:j,hwmDivergenceGraceMs:se,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:me};function me(e,t){ne(),re(e,{sseConnected:!0,inboxMonitorHealthy:ae(),wakeArmed:oe(),agentKind:Z(),hostname:ue(),version:ie(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function we(){let e=0,t=null,o=null,a=null,s=null;for(;;){const r=await Q();if(!r){a&&(await a.release(),a=null,s=null),i.connected=!1,i.ownership={state:"unowned"},await I(5e3);continue}r.cubeId!==o&&(o=r.cubeId,t=null);const c=`${r.cubeId}:${r.droneId}`;if(a&&s!==c&&(await a.release(),a=null,s=null),a||(a=await G(r.cubeId,r.droneId),s=a?c:null),!a){i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}i.ownership=await b(r.cubeId,r.droneId);let l=!1;try{const f=new AbortController,g=async()=>{try{await a.refresh()||(l=!0,f.abort(new Error("stream ownership lost")))}catch(p){l=!0,f.abort(p instanceof Error?p:new Error(String(p)))}},C=setInterval(()=>{g()},Math.max(1e3,Math.floor(j/2)));try{await V(r,t,p=>{t=p},{abortSignal:f.signal})}finally{clearInterval(C)}if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}e=0,i.reconnectAttempts=0}catch(f){if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}i.connected=!1;const g=Math.min(ce*2**e,le)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(g)}ms: ${f?.message??f}
4
- `),e+=1,i.reconnectAttempts=e,await I(g)}}}async function V(e,t,o,a={}){const{fetchImpl:s,appendLine:r,hasInboxEntryId:c,getToken:l,wakeCodex:f,heartbeatTimeoutMs:g,hwmDivergenceGraceMs:C,abortSignal:p,onInboxReceipt:U}={...K,...a},O=await l(),$={Authorization:`Bearer ${O}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&($["Last-Event-ID"]=t);const A=new AbortController,H=()=>{try{A.abort(p.reason??new Error("external abort"))}catch{}};p.aborted&&H(),p.addEventListener("abort",H,{once:!0});let m=null;const v=()=>{m&&clearTimeout(m),m=setTimeout(()=>{try{A.abort(new Error("heartbeat watchdog timeout"))}catch{}},g)};v();let P=t,u=null,w=null;const _=()=>{w&&(clearTimeout(w.timer),w=null)};let S=null;const T=(n,d)=>{const k={id:n,created_at:d};S&&d&&S.created_at&&y(k,S)<=0||(S=k,P=n,i.lastPersistedEventId=n,o(n))},L=n=>{n&&(u=!u||y(n,u)>0?n:u,w&&y(u,w.hwm)>=0&&_())},q=n=>{if(w?.hwm.id===n.id)return;_();const d=setTimeout(()=>{if(u&&y(u,n)>=0){_();return}try{A.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},C);w={hwm:n,timer:d}},M=new Set,D=[];let W=t!==null;const F=async n=>{const d=Ie(be(n.data,n.id));return W&&await c(e.cubeId,e.droneId,n.id,d)?(T(n.id,n.data?.created_at??""),"persisted-skip"):(await r(e.cubeId,e.droneId,d),f(Y(d)),U(e,O),"written")},N=n=>{for(M.add(n.id),D.push(n.id);D.length>de;){const d=D.shift();d&&M.delete(d)}T(n.id,n.data?.created_at??""),L(J(n))};let E;try{E=await s(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:$,signal:A.signal})}catch(n){throw m&&clearTimeout(m),n}if(!E.ok||!E.body)throw m&&clearTimeout(m),new Error(`stream HTTP ${E.status}`);i.connected=!0;try{for await(const n of he(E.body)){v();const d=new Date().toISOString();if(i.lastWireActivityAt=d,(n.type==="log"||n.type==="bookmark")&&(i.lastContentEventAt=d),n.type==="heartbeat"){if(i.lastHeartbeatAt=d,n.hwm&&u===null){L(n.hwm),P===null&&T(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&u&&y(n.hwm,u)<=0){_();continue}n.hwm&&u&&y(n.hwm,u)>0&&q(n.hwm);continue}if(n.type==="bookmark"){W=!1;continue}if(n.type==="log"){if(M.has(n.id)){T(n.id,n.data?.created_at??""),L(J(n));continue}const k=typeof n.data?.message=="string"&&n.data.message.startsWith("[HEARTBEAT-PING]");if(n.data?.kind==="ack"){if(n.data?.author_drone_id===e.droneId&&await F(n)==="persisted-skip")continue;N(n);continue}if(n.data?.drone_id===e.droneId&&!k){N(n);continue}if(await F(n)==="persisted-skip")continue;N(n)}}}finally{p.removeEventListener("abort",H),m&&clearTimeout(m),_(),i.connected=!1}}async function We(e,t,o,a={}){const{ownerDeps:s,ownerStaleMs:r}={...K,...a},c=await G(e.cubeId,e.droneId,r,s);if(!c)return i.connected=!1,i.ownership=await b(e.cubeId,e.droneId,s),"skipped";i.ownership=await b(e.cubeId,e.droneId,s);try{return await V(e,t,o,a),"streamed"}finally{await c.release()}}async function*he(e){const t=e.getReader(),o=new TextDecoder;let a="";try{for(;;){const{value:s,done:r}=await t.read();if(r){if(a.trim()){const l=X(a);l&&(yield l)}return}a+=o.decode(s,{stream:!0});let c;for(;(c=a.indexOf(`
3
+ `)}i.runLoopRestartCount+=1,await y(5e3)}})()}function je(e={}){he(),(e.runForever??ge)()}const V={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:Te,hasInboxEntryId:He,getToken:re,wakeCodex:ne,heartbeatTimeoutMs:K,hwmDivergenceGraceMs:le,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:be};function be(e,t){ae(),oe(e,{sseConnected:!0,inboxMonitorHealthy:ie(),wakeArmed:se(),agentKind:ee(),hostname:pe(),version:ce(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function Ie(){let e=0,t=null,o=null,a=null,s=null;for(;;){const r=await Y();if(!r){a&&(await a.release(),a=null,s=null),i.connected=!1,i.ownership={state:"unowned"},await y(5e3);continue}r.cubeId!==o&&(o=r.cubeId,t=null);const c=`${r.cubeId}:${r.droneId}`;if(a&&s!==c&&(await a.release(),a=null,s=null),a||(a=await j(r.cubeId,r.droneId),s=a?c:null),!a){i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}i.ownership=await b(r.cubeId,r.droneId);let l=!1;try{const f=new AbortController,g=async()=>{try{await a.refresh()||(l=!0,f.abort(new Error("stream ownership lost")))}catch(p){l=!0,f.abort(p instanceof Error?p:new Error(String(p)))}},H=setInterval(()=>{g()},Math.max(1e3,Math.floor(K/2)));try{await X(r,t,p=>{t=p},{abortSignal:f.signal})}finally{clearInterval(H)}if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}e=0,i.reconnectAttempts=0}catch(f){if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}i.connected=!1;const g=Math.min(de*2**e,ue)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(g)}ms: ${f?.message??f}
4
+ `),e+=1,i.reconnectAttempts=e,await y(g)}}}async function X(e,t,o,a={}){const{fetchImpl:s,appendLine:r,hasInboxEntryId:c,getToken:l,wakeCodex:f,heartbeatTimeoutMs:g,hwmDivergenceGraceMs:H,abortSignal:p,onInboxReceipt:q}={...V,...a},O=await l(),$={Authorization:`Bearer ${O}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&($["Last-Event-ID"]=t);const S=new AbortController,L=()=>{try{S.abort(p.reason??new Error("external abort"))}catch{}};p.aborted&&L(),p.addEventListener("abort",L,{once:!0});let m=null;const F=()=>{m&&clearTimeout(m),m=setTimeout(()=>{try{S.abort(new Error("heartbeat watchdog timeout"))}catch{}},g)};F();let P=t,u=null,w=null;const _=()=>{w&&(clearTimeout(w.timer),w=null)};let T=null;const x=(n,d)=>{const k={id:n,created_at:d};T&&d&&T.created_at&&I(k,T)<=0||(T=k,P=n,i.lastPersistedEventId=n,o(n))},M=n=>{n&&(u=!u||I(n,u)>0?n:u,w&&I(u,w.hwm)>=0&&_())},z=n=>{if(w?.hwm.id===n.id)return;_();const d=setTimeout(()=>{if(u&&I(u,n)>=0){_();return}try{S.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},H);w={hwm:n,timer:d}},D=new Set,N=[];let W=t!==null;const B=async n=>{const d=Se(Ee(n.data,n.id));return W&&await c(e.cubeId,e.droneId,n.id,d)?(x(n.id,n.data?.created_at??""),"persisted-skip"):(await r(e.cubeId,e.droneId,d),f(Z(d)),q(e,O),"written")},R=n=>{for(D.add(n.id),N.push(n.id);N.length>fe;){const d=N.shift();d&&D.delete(d)}x(n.id,n.data?.created_at??""),M(U(n))};let E;try{E=await s(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:$,signal:S.signal})}catch(n){throw m&&clearTimeout(m),n}if(!E.ok||!E.body)throw m&&clearTimeout(m),new Error(`stream HTTP ${E.status}`);i.connected=!0;try{for await(const n of ye(E.body)){F();const d=new Date().toISOString();if(i.lastWireActivityAt=d,(n.type==="log"||n.type==="bookmark")&&(i.lastContentEventAt=d),n.type==="heartbeat"){if(i.lastHeartbeatAt=d,n.hwm&&u===null){M(n.hwm),P===null&&x(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&u&&I(n.hwm,u)<=0){_();continue}n.hwm&&u&&I(n.hwm,u)>0&&z(n.hwm);continue}if(n.type==="bookmark"){W=!1;continue}if(n.type==="log"){if(D.has(n.id)){x(n.id,n.data?.created_at??""),M(U(n));continue}const k=typeof n.data?.message=="string"&&n.data.message.startsWith("[HEARTBEAT-PING]");if(n.data?.kind==="ack"){if(n.data?.author_drone_id===e.droneId&&await B(n)==="persisted-skip")continue;R(n);continue}if(n.data?.drone_id===e.droneId&&!k){R(n);continue}if(await B(n)==="persisted-skip")continue;R(n)}}}finally{p.removeEventListener("abort",L),m&&clearTimeout(m),_(),i.connected=!1}}async function Ke(e,t,o,a={}){const{ownerDeps:s,ownerStaleMs:r}={...V,...a},c=await j(e.cubeId,e.droneId,r,s);if(!c)return i.connected=!1,i.ownership=await b(e.cubeId,e.droneId,s),"skipped";i.ownership=await b(e.cubeId,e.droneId,s);try{return await X(e,t,o,a),"streamed"}finally{await c.release()}}async function*ye(e){const t=e.getReader(),o=new TextDecoder;let a="";try{for(;;){const{value:s,done:r}=await t.read();if(r){if(a.trim()){const l=J(a);l&&(yield l)}return}a+=o.decode(s,{stream:!0});let c;for(;(c=a.indexOf(`
5
5
 
6
- `))!==-1;){const l=a.slice(0,c);a=a.slice(c+2);const f=X(l);f&&(yield f)}}}finally{try{t.releaseLock()}catch{}}}function X(e){let t=null,o=null,a=[];for(const r of e.split(`
6
+ `))!==-1;){const l=a.slice(0,c);a=a.slice(c+2);const f=J(l);f&&(yield f)}}}finally{try{t.releaseLock()}catch{}}}function J(e){let t=null,o=null,a=[];for(const r of e.split(`
7
7
  `))r.startsWith("event:")?t=r.slice(6).trim():r.startsWith("id:")?o=r.slice(3).trim():r.startsWith("data:")&&a.push(r.slice(5).trim());const s=a.join(`
8
- `);if(!t)return null;if(t==="log"){if(!o)return null;let r;try{r=JSON.parse(s)}catch{return null}return{type:"log",id:o,data:r}}if(t==="heartbeat"){let r=null,c=null;try{const l=JSON.parse(s);r=typeof l.ts=="string"?l.ts:null,c=ge(l.hwm)}catch{}return{type:"heartbeat",ts:r,hwm:c}}if(t==="bookmark"){let r=null;try{const c=JSON.parse(s);r=typeof c.as_of=="string"?c.as_of:null}catch{}return{type:"bookmark",as_of:r}}return{type:"unknown",raw:e}}function ge(e){if(!e||typeof e!="object")return null;const t=e;return typeof t.id=="string"&&t.id.length>0&&typeof t.created_at=="string"&&t.created_at.length>0?{id:t.id,created_at:t.created_at}:null}function y(e,t){const o=Date.parse(e.created_at),a=Date.parse(t.created_at);return Number.isFinite(o)&&Number.isFinite(a)&&o!==a?o-a:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function J(e){if(e.data?.visibility==="direct"||e.data?.kind==="ack")return null;const t=e.data?.created_at;return typeof t=="string"&&t.length>0?{id:e.id,created_at:t}:null}function be(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function ye(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function Ie(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),o=e.drone_label??"?",a=e.role_name??"?",s=typeof e.message=="string"?e.message:"",r=ye(e.id,e.entry_id),c=r?`[entry_id: ${r}] `:"",l=s.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${o} (${a}): ${c}${l}`}async function _e(e,t,o){const a=B(e,t);await Ee(a,o,R)}async function Ee(e,t,o=R,a=o*2){await h.mkdir(x.dirname(e),{recursive:!0}),await h.appendFile(e,t+`
9
- `,"utf-8"),await Ae(e,o,a)}async function Ae(e,t,o=t){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");if(!Number.isInteger(o)||o<t)throw new Error("trimThresholdLines must be an integer >= maxLines");const s=(await h.readFile(e,"utf-8")).split(/\r?\n/);if(s.at(-1)===""&&s.pop(),s.length<=o)return;const r=s.slice(-t),c=x.join(x.dirname(e),`.${x.basename(e)}.${process.pid}.${Date.now()}.tmp`);try{await h.writeFile(c,r.join(`
8
+ `);if(!t)return null;if(t==="log"){if(!o)return null;let r;try{r=JSON.parse(s)}catch{return null}return{type:"log",id:o,data:r}}if(t==="heartbeat"){let r=null,c=null;try{const l=JSON.parse(s);r=typeof l.ts=="string"?l.ts:null,c=_e(l.hwm)}catch{}return{type:"heartbeat",ts:r,hwm:c}}if(t==="bookmark"){let r=null;try{const c=JSON.parse(s);r=typeof c.as_of=="string"?c.as_of:null}catch{}return{type:"bookmark",as_of:r}}return{type:"unknown",raw:e}}function _e(e){if(!e||typeof e!="object")return null;const t=e;return typeof t.id=="string"&&t.id.length>0&&typeof t.created_at=="string"&&t.created_at.length>0?{id:t.id,created_at:t.created_at}:null}function I(e,t){const o=Date.parse(e.created_at),a=Date.parse(t.created_at);return Number.isFinite(o)&&Number.isFinite(a)&&o!==a?o-a:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function U(e){if(e.data?.visibility==="direct"||e.data?.kind==="ack")return null;const t=e.data?.created_at;return typeof t=="string"&&t.length>0?{id:e.id,created_at:t}:null}function Ee(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function Ae(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function Se(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),o=e.drone_label??"?",a=e.role_name??"?",s=typeof e.message=="string"?e.message:"",r=Ae(e.id,e.entry_id),c=r?`[entry_id: ${r}] `:"",l=s.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${o} (${a}): ${c}${l}`}async function Te(e,t,o){const a=G(e,t);await xe(a,o,v)}async function xe(e,t,o=v,a=o*2){await h.mkdir(C.dirname(e),{recursive:!0}),await h.appendFile(e,t+`
9
+ `,"utf-8"),await ke(e,o,a)}async function ke(e,t,o=t){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");if(!Number.isInteger(o)||o<t)throw new Error("trimThresholdLines must be an integer >= maxLines");const s=(await h.readFile(e,"utf-8")).split(/\r?\n/);if(s.at(-1)===""&&s.pop(),s.length<=o)return;const r=s.slice(-t),c=C.join(C.dirname(e),`.${C.basename(e)}.${process.pid}.${Date.now()}.tmp`);try{await h.writeFile(c,r.join(`
10
10
  `)+`
11
- `,"utf-8"),await h.rename(c,e)}catch(l){throw await h.rm(c,{force:!0}),l}}function Se(e,t,o){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const a=t?o.replace(`[entry_id: ${t}] `,""):o;return!!(a&&e.split(/\r?\n/).includes(a))}async function Te(e,t,o,a){const s=B(e,t);let r;try{r=await h.readFile(s,"utf-8")}catch(c){if(c?.code==="ENOENT")return!1;throw c}return Se(r,o,a)}function I(e){return new Promise(t=>setTimeout(t,e))}export{R as INBOX_TAIL_LINES_CAP,Oe as INBOX_TAIL_TRIM_THRESHOLD_LINES,ve as __resetStreamStateForTest,Ee as appendCappedInboxLine,pe as classifyRunLoopHealth,y as compareBroadcastHwm,Ie as formatInboxLine,$e as getStreamStatus,Se as inboxRawHasEntry,he as parseSSE,Pe as startLogStream,V as streamOnce,We as streamOnceIfOwner,Ae as trimInboxFileToRecentLines};
11
+ `,"utf-8"),await h.rename(c,e)}catch(l){throw await h.rm(c,{force:!0}),l}}function Ce(e,t,o){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const a=t?o.replace(`[entry_id: ${t}] `,""):o;return!!(a&&e.split(/\r?\n/).includes(a))}async function He(e,t,o,a){const s=G(e,t);let r;try{r=await h.readFile(s,"utf-8")}catch(c){if(c?.code==="ENOENT")return!1;throw c}return Ce(r,o,a)}function y(e){return new Promise(t=>setTimeout(t,e))}export{v as INBOX_TAIL_LINES_CAP,Pe as INBOX_TAIL_TRIM_THRESHOLD_LINES,Ge as __resetCodexHeartbeatForTest,Be as __resetStreamStateForTest,xe as appendCappedInboxLine,we as classifyRunLoopHealth,I as compareBroadcastHwm,he as ensureCodexHeartbeatStarted,Se as formatInboxLine,We as getStreamStatus,Ce as inboxRawHasEntry,ye as parseSSE,je as startLogStream,X as streamOnce,Ke as streamOnceIfOwner,ke as trimInboxFileToRecentLines};
@@ -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.22",
3
+ "version": "1.0.24",
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",