borgmcp 1.0.15 → 1.0.17

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.
@@ -2,9 +2,10 @@
2
2
  * gh#780 companion fix: classify auth-class failures into the RIGHT
3
3
  * recovery advice for an in-session agent.
4
4
  *
5
- * Root-cause context: the pre-gh#780 funnel answered every auth failure
6
- * with "Authentication expired. Run: borg assimilate". An in-session
7
- * agent's only reachable assimilate is the borg:assimilate MCP tool, which
5
+ * Root-cause context: the pre-gh#780 funnel answered every auth failure by
6
+ * pointing the user at `borg assimilate` (the wrong remedy; gh#794 now also
7
+ * differentiates a dead saved login from never-signed-in, both `borg setup`).
8
+ * An in-session agent's only reachable assimilate is the borg:assimilate MCP tool, which
8
9
  * minted a brand-new drones row — so each auth blip spawned an orphan seat
9
10
  * (the gh#780 class). Neither failure mode is fixable by assimilating:
10
11
  * assimilate rides the same broken Bearer token.
package/dist/claude.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as S}from"child_process";import{randomUUID as y}from"node:crypto";import{basename as A}from"node:path";import{createInterface as P}from"node:readline/promises";import s from"chalk";import{findProjectRoot as T,getActiveCube as I,inboxPathForDrone as E,setCodexWakeTarget as R}from"./cubes.js";import{handleVersionFlag as D,getPackageVersion as g}from"./version.js";import{isHelpFlag as O,setupHelpText as F,topLevelHelpText as N}from"./cli-help.js";import{runSpawn as H}from"./spawn.js";import{parseSyncArgs as M,runSync as B}from"./sync.js";import{parseAssimilateArgs as G}from"./parse-assimilate-args.js";import{runAssimilate as L}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as _}from"./assimilate-deps.js";import{setTerminalTitle as W}from"./terminal-title.js";import{initConsolePrefix as U,consolePrefix as r}from"./console-prefix.js";import{initDebugFromArgv as V}from"./debug.js";import{fetchLatestBorgmcpVersion as j,compareVersionsForStaleness as K}from"./stale-version-check.js";import{defaultCliChoiceDeps as Y,detectCliAvailability as v,installedCliNames as q,parseCliFlag as z,resolveCliChoice as X}from"./cli-platform.js";import{getRefreshToken as J,getIdToken as Q}from"./config.js";import{composeGetStarted as Z,shouldShowGetStarted as ee}from"./get-started.js";import{prepareCodexRemoteLaunch as re,withCodexCwdArg as oe,defaultCodexRemoteDeps as se}from"./codex-remote.js";import{findLoadedCodexThread as te}from"./codex-app-server.js";import{buildAgentKickoffPrompt as ie,recordCodexWakeTarget as ae,socketPathFromRemoteArgs as k}from"./codex-launch.js";import{codexBorgSessionConfigArgs as ne}from"./launch-gate.js";import{addCodexMcpServer as ce,addCodexSessionStartHook as le,addCodexUserPromptSubmitHook as de,addMcpServer as pe,addProjectSessionStartHook as me,addUserPromptSubmitHook as fe,isCodexMcpServerConfigured as ue,isMcpServerConfigured as ge,removeSessionStartHook as he}from"./config-utils.js";async function we(){V(process.argv),D(),await U();const n=(async()=>{if(!process.stderr.isTTY)return;const e=g(),a=await j();if(!a)return;const p=K(e,a);p.stale&&p.message&&process.stderr.write(`${r()}${p.message}
2
+ import{spawn as S}from"child_process";import{randomUUID as y}from"node:crypto";import{basename as A}from"node:path";import{createInterface as P}from"node:readline/promises";import s from"chalk";import{findProjectRoot as T,getActiveCube as R,inboxPathForDrone as I,setCodexWakeTarget as E}from"./cubes.js";import{handleVersionFlag as D,getPackageVersion as g}from"./version.js";import{isHelpFlag as O,setupHelpText as F,topLevelHelpText as N}from"./cli-help.js";import{runSpawn as H}from"./spawn.js";import{parseSyncArgs as M,runSync as B}from"./sync.js";import{parseAssimilateArgs as G}from"./parse-assimilate-args.js";import{runAssimilate as L}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as _}from"./assimilate-deps.js";import{setTerminalTitle as W}from"./terminal-title.js";import{initConsolePrefix as U,consolePrefix as r}from"./console-prefix.js";import{initDebugFromArgv as V}from"./debug.js";import{fetchLatestBorgmcpVersion as j,compareVersionsForStaleness as K}from"./stale-version-check.js";import{defaultCliChoiceDeps as Y,detectCliAvailability as v,installedCliNames as q,parseCliFlag as z,resolveCliChoice as X}from"./cli-platform.js";import{getRefreshToken as J,getIdToken as Q}from"./config.js";import{composeGetStarted as Z,shouldShowGetStarted as ee}from"./get-started.js";import{prepareCodexRemoteLaunch as re,withCodexCwdArg as oe,defaultCodexRemoteDeps as se}from"./codex-remote.js";import{findLoadedCodexThread as te}from"./codex-app-server.js";import{buildAgentKickoffPrompt as ie,recordCodexWakeTarget as ae,socketPathFromRemoteArgs as k}from"./codex-launch.js";import{codexBorgSessionConfigArgs as ne}from"./launch-gate.js";import{addCodexMcpServer as ce,addCodexSessionStartHook as le,addCodexUserPromptSubmitHook as de,addMcpServer as pe,addProjectSessionStartHook as me,addUserPromptSubmitHook as ue,isCodexMcpServerConfigured as fe,isMcpServerConfigured as ge,removeSessionStartHook as he}from"./config-utils.js";async function we(){V(process.argv),D(),await U();const n=(async()=>{if(!process.stderr.isTTY)return;const e=g(),a=await j();if(!a)return;const p=K(e,a);p.stale&&p.message&&process.stderr.write(`${r()}${p.message}
3
3
  `)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(N(g())),process.exit(0)),process.argv[2]==="setup"){O(process.argv[3])&&(process.stdout.write(F(g())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){const e=G(process.argv.slice(3));e.ok||(process.stderr.write(s.red(`${r()}\u25FC borg assimilate: ${e.error}
4
- `)),process.exit(1));const a=_(),p=await L({role:e.role,flags:e.flags},a);process.exit(p)}if(process.argv[2]==="spawn"){const e=await H();process.exit(e)}if(process.argv[2]==="sync"){const e=M(process.argv.slice(3));e.ok||(process.stderr.write(s.red(`${r()}\u25FC borg sync: ${e.error}
5
- `)),process.exit(1));const a=await B({},e.options);process.exit(a)}if(ee(await J()!==null,await Q()!==null)){const e=q(v()).length>0;process.stdout.write(Z(e)),process.exit(0)}const t=z(process.argv.slice(2));t.error&&(process.stderr.write(s.red(`${r()}\u25FC ${t.error}
6
- `)),process.exit(1));const $=async e=>{const a=P({input:process.stdin,output:process.stdout});try{return await a.question(e)}finally{a.close()}},o=await X(t.cli,Y($,()=>process.stdin.isTTY===!0));xe();const c=t.rest,i=await I();W(i?{label:i.droneLabel,cubeName:i.name}:null,A(process.cwd()));const b=i&&o==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${E(i.cubeId,i.droneId)}\` so each event's task-notification title summarizes the new cube log entry (drone label, role, and first ~80 chars of the message body) \u2014 letting you triage events without reading the full body. `:"";await Promise.race([n,new Promise(e=>setTimeout(e,2e3))]);const h=o==="codex"?`borg-wake-${y()}`:null;let m,w=[],f={...process.env,BORG_SESSION:"1"},l=null,d=null;if(o==="codex"&&!c.includes("--remote")){console.error(`${r()}${s.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await re(se());e.warning?(console.error(`${r()}${s.yellow(`warning: ${e.warning}`)}`),m="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg:regen manually whenever you return, and expect only fallback wakeups until relaunch."):m="Codex wake-path capability check passed: remote-control socket established for this session.",w=e.args,f={...process.env,...e.env,BORG_SESSION:"1"},l=k(e.args),d=e.server?.cleanup??null}else o==="codex"&&c.includes("--remote")&&(m="Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg:regen manually when returning to the session.",l=k(c),l&&(f={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));const x=ie({cli:o,codexWakeNonce:h,monitorClause:b,codexWakePathClause:m});let u=[...c,x];o==="codex"&&(u=[...ne(),...w,...oe(u,process.cwd())]),console.error(`${r()}${s.blue(`\u25FC Launching ${o==="claude"?"Claude Code":"Codex"}\u2026`)}`);const C=S(o,u,{stdio:"inherit",shell:!1,env:f});o==="codex"&&i&&l&&ae({deps:{setCodexWakeTarget:R,findLoadedCodexThread:te},cubeId:i.cubeId,droneId:i.droneId,socketPath:l,passthroughArgs:c,previewNeedle:h??x.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),C.on("error",e=>{if(d)try{d()}catch{}e.code==="ENOENT"?(console.error(`${r()}${s.red(`
4
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const a=_(),p=await L({role:e.role,flags:e.flags},a);process.exit(p)}if(process.argv[2]==="spawn"){const e=await H();process.exit(e)}if(process.argv[2]==="sync"){const e=M(process.argv.slice(3));e.ok||(process.stderr.write(s.red(`${r()}\u25FC borg sync: ${e.error}
5
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const a=await B({},e.options);process.exit(a)}if(ee(await J()!==null,await Q()!==null)){const e=q(v()).length>0;process.stdout.write(Z(e)),process.exit(0)}const t=z(process.argv.slice(2));t.error&&(process.stderr.write(s.red(`${r()}\u25FC ${t.error}
6
+ `)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const b=async e=>{const a=P({input:process.stdin,output:process.stdout});try{return await a.question(e)}finally{a.close()}},o=await X(t.cli,Y(b,()=>process.stdin.isTTY===!0));xe();const c=t.rest,i=await R();W(i?{label:i.droneLabel,cubeName:i.name}:null,A(process.cwd()));const $=i&&o==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${I(i.cubeId,i.droneId)}\` so each event's task-notification title summarizes the new cube log entry (drone label, role, and first ~80 chars of the message body) \u2014 letting you triage events without reading the full body. `:"";await Promise.race([n,new Promise(e=>setTimeout(e,2e3))]);const h=o==="codex"?`borg-wake-${y()}`:null;let m,w=[],u={...process.env,BORG_SESSION:"1"},l=null,d=null;if(o==="codex"&&!c.includes("--remote")){console.error(`${r()}${s.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await re(se());e.warning?(console.error(`${r()}${s.yellow(`warning: ${e.warning}`)}`),m="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg:regen manually whenever you return, and expect only fallback wakeups until relaunch."):m="Codex wake-path capability check passed: remote-control socket established for this session.",w=e.args,u={...process.env,...e.env,BORG_SESSION:"1"},l=k(e.args),d=e.server?.cleanup??null}else o==="codex"&&c.includes("--remote")&&(m="Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg:regen manually when returning to the session.",l=k(c),l&&(u={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));const x=ie({cli:o,codexWakeNonce:h,monitorClause:$,codexWakePathClause:m});let f=[...c,x];o==="codex"&&(f=[...ne(),...w,...oe(f,process.cwd())]),console.error(`${r()}${s.blue(`\u25FC Launching ${o==="claude"?"Claude Code":"Codex"}\u2026`)}`);const C=S(o,f,{stdio:"inherit",shell:!1,env:u});o==="codex"&&i&&l&&ae({deps:{setCodexWakeTarget:E,findLoadedCodexThread:te},cubeId:i.cubeId,droneId:i.droneId,socketPath:l,passthroughArgs:c,previewNeedle:h??x.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),C.on("error",e=>{if(d)try{d()}catch{}e.code==="ENOENT"?(console.error(`${r()}${s.red(`
7
7
  \u25FC Failed to launch ${o}`)}`),console.error(`${r()}${s.gray(`Make sure ${o} is installed.
8
8
  `)}`)):console.error(`${r()}${s.red(`
9
9
  \u25FC Failed to launch ${o}: ${e.message}
10
- `)}`),process.exit(1)}),C.on("exit",e=>{if(d)try{d()}catch{}process.exit(e??0)})}function xe(){const n=v();if(n.claude)try{ge()||pe(),me(T(process.cwd())),he(),fe()}catch(t){console.error(`${r()}${s.yellow(`warning: Claude Code integration check failed: ${t?.message??t}`)}`)}if(n.codex)try{ue()||ce(),le(),de()}catch(t){console.error(`${r()}${s.yellow(`warning: Codex integration check failed: ${t?.message??t}`)}`)}}we().catch(n=>{console.error(`${r()}${s.red(`
10
+ `)}`),process.exit(1)}),C.on("exit",e=>{if(d)try{d()}catch{}process.exit(e??0)})}function xe(){const n=v();if(n.claude)try{ge()||pe(),me(T(process.cwd())),he(),ue()}catch(t){console.error(`${r()}${s.yellow(`warning: Claude Code integration check failed: ${t?.message??t}`)}`)}if(n.codex)try{fe()||ce(),le(),de()}catch(t){console.error(`${r()}${s.yellow(`warning: Codex integration check failed: ${t?.message??t}`)}`)}}we().catch(n=>{console.error(`${r()}${s.red(`
11
11
  \u25FC Error: ${n.message}
12
12
  `)}`),process.exit(1)});
package/dist/cli-help.js CHANGED
@@ -1,8 +1,10 @@
1
- function r(e){return e==="--help"||e==="-h"}function n(e){return`borgmcp ${e} \u2014 coordinate AI coding agents in shared cubes
2
- MCP server for Claude Code and Codex
1
+ function r(e){return e==="--help"||e==="-h"}function n(e){return`borgmcp ${e} \u2014 run several AI coding agents on one project, together.
2
+ They coordinate through a shared log (a "cube"). For Claude Code & Codex.
3
3
 
4
- Install Claude Code or Codex first. Type borg ... in your terminal;
5
- type borg:... inside your agent session after assimilation.
4
+ Docs & quickstart: https://borgmcp.ai/get-started
5
+
6
+ Install Claude Code or Codex first. Type \`borg ...\` in your terminal;
7
+ type \`borg:...\` inside your agent session once you've joined a cube ("assimilate").
6
8
 
7
9
  Usage:
8
10
  borg Launch your agent CLI with cube context
@@ -2,7 +2,8 @@
2
2
  * Drone self-identification prefix for client-emitted console messages.
3
3
  *
4
4
  * Per gh#25: when a drone session emits a console error (e.g.
5
- * "Authentication expired. Run: borg assimilate"), the Queen has no way to
5
+ * "Authentication expired — your saved login has expired. Run: borg setup"),
6
+ * the Queen has no way to
6
7
  * tell which drone window the message came from without scanning every
7
8
  * open terminal. Window title alone (set by terminal-title.ts) is
8
9
  * insufficient — the Queen reads the active terminal's output stream,
@@ -15,7 +16,12 @@
15
16
  * Format (matches the terminal-title.ts middle-dot convention so
16
17
  * surfaces stay internally consistent):
17
18
  * `[<drone-label> · <cube-name>]` (assimilated)
18
- * `[unassimilated · <repo-basename>]` (no cube cached)
19
+ * `[borg · <repo-basename>]` (no cube cached)
20
+ *
21
+ * The not-yet-assimilated shape reads as neutral metadata ("this is
22
+ * borg, in project X"), NOT a fault the user must fix — and mirrors the
23
+ * unassimilated terminal-title shape (`borg · <repo-basename>`), so the
24
+ * title bar and the console prefix agree (gh#818 P1).
19
25
  */
20
26
  /**
21
27
  * Resolve the drone-self-identification prefix from cube state and
@@ -1 +1 @@
1
- import{basename as o}from"node:path";import t from"chalk";import{getActiveCube as i}from"./cubes.js";let r=null;async function s(){if(r!==null)return r;try{const e=await i();if(e?.droneLabel&&e?.name)return r=`[${e.droneLabel} \xB7 ${e.name}]`,r}catch{}return r=`[unassimilated \xB7 ${o(process.cwd())}]`,r}function c(){return r!==null?r:`[unassimilated \xB7 ${o(process.cwd())}]`}function n(){return t.gray(c())+" "}function a(...e){if(e.length===0){console.error(n());return}typeof e[0]=="string"?console.error(n()+e[0],...e.slice(1)):console.error(n(),...e)}function m(){r=null}export{m as _resetCachedPrefixForTests,a as cerr,n as consolePrefix,c as droneIdPrefix,s as initConsolePrefix};
1
+ import{basename as t}from"node:path";import i from"chalk";import{getActiveCube as c}from"./cubes.js";let r=null;function o(){return`[borg \xB7 ${t(process.cwd())}]`}async function a(){if(r!==null)return r;try{const e=await c();if(e?.droneLabel&&e?.name)return r=`[${e.droneLabel} \xB7 ${e.name}]`,r}catch{}return r=o(),r}function f(){return r!==null?r:o()}function n(){return i.gray(f())+" "}function x(...e){if(e.length===0){console.error(n());return}typeof e[0]=="string"?console.error(n()+e[0],...e.slice(1)):console.error(n(),...e)}function m(){r=null}export{m as _resetCachedPrefixForTests,x as cerr,n as consolePrefix,f as droneIdPrefix,a as initConsolePrefix};
@@ -49,6 +49,40 @@ export declare class RecentLineDeduper {
49
49
  export declare function formatEventLine(inboxLine: string): string | null;
50
50
  export declare function formatFreshEventLine(inboxLine: string, deduper: RecentLineDeduper): string | null;
51
51
  export declare function seedDeduperFromInboxTail(inboxPath: string, deduper: RecentLineDeduper, maxLines?: number): void;
52
+ /** Holder-tracked stall state. `lastEmittedOffset` is stat-anchored. */
53
+ export interface TailStallState {
54
+ /** Inbox file size (bytes) as of the last tail delivery; seeded to EOF at arm. */
55
+ lastEmittedOffset: number;
56
+ /** Epoch ms when the CURRENT un-emitted-growth streak began; null = none. */
57
+ grewSince: number | null;
58
+ }
59
+ export type TailStallVerdict = {
60
+ kind: 'ok';
61
+ state: TailStallState;
62
+ } | {
63
+ kind: 'rotation';
64
+ state: TailStallState;
65
+ } | {
66
+ kind: 'respawn';
67
+ state: TailStallState;
68
+ };
69
+ /**
70
+ * gh#822: PURE stall evaluator. Given the current inbox size + the holder's
71
+ * stat-anchored state, decide whether the tail is healthy, rotated, or stalled.
72
+ * False-reap-safe by construction (CR 131dcd78):
73
+ * - ROTATION (item 2): `inboxSize < lastEmittedOffset` ⇒ truncation/rotation —
74
+ * re-anchor offset to the NEW size + clear the streak; NEVER treated as
75
+ * negative growth, so the detector keeps working after the very rotation
76
+ * that triggers Subclass B.
77
+ * - QUIET cube: `inboxSize === lastEmittedOffset` ⇒ no un-emitted growth ⇒ ok
78
+ * (clears any streak). A silent cube can NEVER trip a respawn.
79
+ * - GREW-but-not-emitted: `inboxSize > lastEmittedOffset`. Only when that
80
+ * un-emitted growth PERSISTS continuously past `stallThresholdMs` ⇒ respawn.
81
+ * A brief/slow grow that the tail then delivers (a later tick re-anchors
82
+ * lastEmittedOffset → size==offset) clears the streak first. So "slow" never
83
+ * trips it; only SUSTAINED un-emitted growth does. Err toward not-respawning.
84
+ */
85
+ export declare function evaluateInboxTailStall(inboxSize: number, state: TailStallState, nowMs: number, stallThresholdMs: number): TailStallVerdict;
52
86
  export interface InboxLockDeps {
53
87
  /**
54
88
  * ATOMICALLY create the pidfile WITH `content` iff it does not exist. True =
@@ -70,6 +104,16 @@ export interface InboxLockDeps {
70
104
  isAlive(pid: number): boolean;
71
105
  }
72
106
  export declare function pidfilePathFor(inboxPath: string): string;
107
+ /** gh#822: the holder-liveness heartbeat sidecar (mtime touched each tick). */
108
+ export declare function heartbeatPathFor(inboxPath: string): string;
109
+ export declare const HEARTBEAT_STALE_MS: number;
110
+ /**
111
+ * gh#822: `tail` args — ARM (`-n 0`, skip history, matches the prior shape) vs
112
+ * RECOVERY byte-seek (`-c +<N+1>`, re-read the un-emitted bytes from offset N
113
+ * FORWARD — CR build-gate item 3: NOT `-n 0`, which starts at the new EOF and
114
+ * skips exactly the bytes a stalled tail dropped).
115
+ */
116
+ export declare function tailArgsFor(inboxPath: string, fromByteOffset: number | null): string[];
73
117
  /**
74
118
  * Try to become the SOLE monitor for this inbox. Returns true if we claimed the
75
119
  * pidfile (caller proceeds to tail + must release it on exit); false if a LIVE
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as d}from"node:child_process";import{randomBytes as h}from"node:crypto";import{linkSync as x,readFileSync as a,realpathSync as b,unlinkSync as p,writeFileSync as y}from"node:fs";import{createInterface as I}from"node:readline";import{fileURLToPath as E}from"node:url";const g=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/,S=1024;class w{cap;seen=new Set;order=[];constructor(r=S){if(this.cap=r,!Number.isInteger(r)||r<1)throw new Error("cap must be a positive integer")}remember(r){if(this.seen.has(r))return!1;for(this.seen.add(r),this.order.push(r);this.order.length>this.cap;){const t=this.order.shift();t&&this.seen.delete(t)}return!0}}function m(e){const r=g.exec(e);if(!r)return null;const[,,t,n,i]=r,o=i.trim();return`${t} (${n}): ${o}`}function v(e,r){const t=m(e);return t===null?null:r.remember(e)?t:null}function N(e,r,t=512){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");let n;try{n=a(e,"utf-8")}catch(o){if(o?.code==="ENOENT")return;throw o}const i=n.split(/\r?\n/);i.at(-1)===""&&i.pop();for(const o of i.slice(-t))m(o)!==null&&r.remember(o)}function T(e){return`${e}.monitor.pid`}function $(e,r,t,n=3){const i=String(r);for(let o=0;o<n;o++){if(t.claim(e,i))return!0;const s=t.read(e);if(s===null)continue;const u=s.trim();if(u===""){t.removeIfContent(e,s);continue}const l=Number.parseInt(u,10);if(!Number.isNaN(l)&&t.isAlive(l))return!1;t.removeIfContent(e,s)}return!1}function k(){return{claim:(e,r)=>{const t=`${e}.tmp.${process.pid}.${h(6).toString("hex")}`;try{y(t,r,{mode:384});try{return x(t,e),!0}catch(n){if(n?.code==="EEXIST")return!1;throw n}}finally{try{p(t)}catch{}}},read:e=>{try{return a(e,"utf8")}catch{return null}},removeIfContent:(e,r)=>{try{a(e,"utf8")===r&&p(e)}catch{}},isAlive:e=>{try{return process.kill(e,0),!0}catch(r){return r?.code==="EPERM"}}}}function R(){const e=process.argv[2];e||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const r=T(e),t=k();$(r,process.pid,t)||process.exit(0);const n=()=>t.removeIfContent(r,String(process.pid)),i=new w;N(e,i);const o=d("tail",["-F","-n","0",e],{stdio:["ignore","pipe","inherit"]});o.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),n(),process.exit(1));const s=I({input:o.stdout,crlfDelay:1/0});let u=!1;s.on("line",c=>{const f=v(c,i);f!==null&&console.log(f)}),o.on("error",c=>{console.error(`borg-inbox-monitor: tail failed: ${c.message}`),n(),process.exit(1)}),o.on("exit",(c,f)=>{n(),u&&process.exit(0),f&&process.exit(0),process.exit(c??0)});const l=c=>{u||(u=!0,n(),s.close(),!o.killed&&!o.kill(c)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref())};process.once("SIGTERM",()=>l("SIGTERM")),process.once("SIGINT",()=>l("SIGINT"))}function D(e,r){try{return b(e)===E(r)}catch{return!1}}D(process.argv[1],import.meta.url)&&R();export{S as RECENT_EMITTED_LINE_CAP,w as RecentLineDeduper,$ as acquireInboxLock,m as formatEventLine,v as formatFreshEventLine,D as isEntryInvocation,T as pidfilePathFor,N as seedDeduperFromInboxTail};
2
+ import{spawn as w}from"node:child_process";import{randomBytes as y}from"node:crypto";import{linkSync as T,readFileSync as p,realpathSync as v,statSync as k,unlinkSync as d,writeFileSync as I}from"node:fs";import{createInterface as O}from"node:readline";import{fileURLToPath as N}from"node:url";const L=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/,_=1024;class R{cap;seen=new Set;order=[];constructor(e=_){if(this.cap=e,!Number.isInteger(e)||e<1)throw new Error("cap must be a positive integer")}remember(e){if(this.seen.has(e))return!1;for(this.seen.add(e),this.order.push(e);this.order.length>this.cap;){const r=this.order.shift();r&&this.seen.delete(r)}return!0}}function b(t){const e=L.exec(t);if(!e)return null;const[,,r,o,i]=e,n=i.trim();return`${r} (${o}): ${n}`}function $(t,e){const r=b(t);return r===null?null:e.remember(t)?r:null}function D(t,e,r=512){if(!Number.isInteger(r)||r<1)throw new Error("maxLines must be a positive integer");let o;try{o=p(t,"utf-8")}catch(n){if(n?.code==="ENOENT")return;throw n}const i=o.split(/\r?\n/);i.at(-1)===""&&i.pop();for(const n of i.slice(-r))b(n)!==null&&e.remember(n)}function F(t,e,r,o){if(t<e.lastEmittedOffset)return{kind:"rotation",state:{lastEmittedOffset:t,grewSince:null}};if(t===e.lastEmittedOffset)return{kind:"ok",state:{lastEmittedOffset:e.lastEmittedOffset,grewSince:null}};const i=e.grewSince??r,n={lastEmittedOffset:e.lastEmittedOffset,grewSince:i};return r-i>=o?{kind:"respawn",state:n}:{kind:"ok",state:n}}function A(t){return`${t}.monitor.pid`}function C(t){return`${t}.monitor.heartbeat`}const h=3e4,M=5*h,J=5*h;function G(t,e){return e===null?["-F","-n","0",t]:["-F","-c",`+${e+1}`,t]}function E(t){try{return k(t).size}catch{return 0}}function P(t,e,r,o=3){const i=String(e);for(let n=0;n<o;n++){if(r.claim(t,i))return!0;const c=r.read(t);if(c===null)continue;const l=c.trim();if(l===""){r.removeIfContent(t,c);continue}const u=Number.parseInt(l,10);if(!Number.isNaN(u)&&r.isAlive(u))return!1;r.removeIfContent(t,c)}return!1}function H(){return{claim:(t,e)=>{const r=`${t}.tmp.${process.pid}.${y(6).toString("hex")}`;try{I(r,e,{mode:384});try{return T(r,t),!0}catch(o){if(o?.code==="EEXIST")return!1;throw o}}finally{try{d(r)}catch{}}},read:t=>{try{return p(t,"utf8")}catch{return null}},removeIfContent:(t,e)=>{try{p(t,"utf8")===e&&d(t)}catch{}},isAlive:t=>{try{return process.kill(t,0),!0}catch(e){return e?.code==="EPERM"}}}}function K(){const t=process.argv[2];t||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const e=A(t),r=H();P(e,process.pid,r)||process.exit(0);const o=()=>r.removeIfContent(e,String(process.pid)),i=new R;D(t,i);let n={lastEmittedOffset:E(t),grewSince:null},c=!1,l=null;const u=a=>{const s=w("tail",G(t,a),{stdio:["ignore","pipe","inherit"]});l=s,s.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),o(),process.exit(1)),O({input:s.stdout,crlfDelay:1/0}).on("line",f=>{const m=$(f,i);m!==null&&(console.log(m),n={lastEmittedOffset:E(t),grewSince:null})}),s.on("error",f=>{s===l&&(console.error(`borg-inbox-monitor: tail failed: ${f.message}`),o(),process.exit(1))}),s.on("exit",(f,m)=>{s===l&&(o(),c&&process.exit(0),m&&process.exit(0),process.exit(f??0))})};u(null);const S=C(t),x=setInterval(()=>{try{I(S,String(Date.now()),{mode:384})}catch{}const a=F(E(t),n,Date.now(),M);if(n=a.state,a.kind==="respawn"&&!c){const s=l;u(n.lastEmittedOffset);try{s?.kill("SIGKILL")}catch{}n={lastEmittedOffset:n.lastEmittedOffset,grewSince:null}}},h);x.unref();const g=a=>{if(c)return;c=!0,clearInterval(x);try{d(S)}catch{}o();const s=l;s&&!s.killed&&!s.kill(a)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref()};process.once("SIGTERM",()=>g("SIGTERM")),process.once("SIGINT",()=>g("SIGINT"))}function q(t,e){try{return v(t)===N(e)}catch{return!1}}q(process.argv[1],import.meta.url)&&K();export{J as HEARTBEAT_STALE_MS,_ as RECENT_EMITTED_LINE_CAP,R as RecentLineDeduper,P as acquireInboxLock,F as evaluateInboxTailStall,b as formatEventLine,$ as formatFreshEventLine,C as heartbeatPathFor,q as isEntryInvocation,A as pidfilePathFor,D as seedDeduperFromInboxTail,G as tailArgsFor};
@@ -51,6 +51,28 @@ export declare function retryOn429(initialResponse: Response, doRequest: () => P
51
51
  * without duplicating the refresh-token plumbing.
52
52
  */
53
53
  export declare function getValidToken(): Promise<string>;
54
+ /**
55
+ * gh#794: the stored session's state, WITHOUT throwing — powers `borg setup`'s
56
+ * short-circuit (SR#3: short-circuit ONLY on `valid`, never past a dead token).
57
+ */
58
+ export type SessionState = 'valid' | 'dead' | 'transient';
59
+ /**
60
+ * gh#794: classify the stored session into valid | dead | transient.
61
+ *
62
+ * ⚠ EFFECTFUL — NOT a read-only probe. The expired branch ATTEMPTS a refresh,
63
+ * which on success PERSISTS the new id_token (via refreshIdToken → storeIdToken,
64
+ * AES-256-GCM-re-encrypted) and on a dead refresh_token `clearTokens()`s. So a
65
+ * `valid` result may have just refreshed-and-stored the session. `clearTokens`
66
+ * fires ONLY on `dead` (RefreshTokenInvalidError / invalid_grant), NEVER on
67
+ * `transient` — a network blip must not nuke a valid keychain (gh#34 invariant).
68
+ *
69
+ * - cached id_token still valid (outside the config.ts 5-min buffer) → 'valid'
70
+ * - expired/within-buffer + refresh succeeds → 'valid' (refreshed + persisted)
71
+ * - expired + RefreshTokenInvalidError → 'dead' (cleared — re-auth needed)
72
+ * - expired + RefreshTransientError / unknown → 'transient' (keychain intact)
73
+ * - no refresh_token at all → 'dead' (never set up / already cleared)
74
+ */
75
+ export declare function probeSession(): Promise<SessionState>;
54
76
  /**
55
77
  * Connect this client as a Drone to a Cube.
56
78
  *
@@ -1 +1 @@
1
- import{getIdToken as y,getRefreshToken as m,clearTokens as h}from"./config.js";import{refreshIdToken as T,RefreshTokenInvalidError as g,RefreshTransientError as b}from"./auth.js";import{consolePrefix as j}from"./console-prefix.js";import{debugLog as w}from"./debug.js";import{assertUuidShape as $}from"./evict-drone.js";const S=process.env.BORG_API_URL||"https://api.borgmcp.ai",E=3,R=6e4;function C(e){if(e==null)return null;const n=e.trim();return/^\d+$/.test(n)?parseInt(n,10)*1e3:null}function P(e,n,t=R,o=()=>Math.random()*500){const s=e??1e3*(n+1);return Math.min(s,t)+o()}function k(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 O(e,n,t){const o=t.maxRetries??E;let s=e,a=0;for(;s.status===429&&a<o;){const p=P(C(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 A(){let e=await y();if(!e){const n=await m();if(n)try{await T(n),e=await y()}catch(t){if(t instanceof g&&await h(),t instanceof b)throw t}if(!e)throw new Error("Authentication required. Run: borg setup")}return e}async function I(){const e=await m();if(!e)return null;try{return await T(e),await y()}catch(n){if(n instanceof g&&await h(),n instanceof b)throw n;return null}}async function r(e,n={}){let t=await A();const{droneSession:o,apiUrl:s,headers:a,...p}=n,x=s??S,f=(p.method??"GET").toUpperCase(),l=async c=>{const u={Authorization:`Bearer ${c}`,...a};o&&(u["X-Drone-Session"]=o),w(`\u2192 ${f} ${e}`);const d=await fetch(`${x}${e}`,{...p,headers:u});return w(`\u2190 ${d.status} ${f} ${e}`),d};let i=await l(t);if(i.status===401){const c=await I();c&&(t=c,i=await l(t))}if(i.status===401)throw new Error("Authentication required. Run: borg setup");if(i.status===429&&(i=await O(i,()=>l(t),{sleep:L,log:c=>console.error(`${j()}${c}`)})),!i.ok){const c=await i.text();w(`\u2717 ${i.status} ${f} ${e}: ${c}`);const u=k(c);if(i.status===429){const d=i.headers.get("Retry-After"),_=d?` (retry after ${d}s)`:"";throw new Error(`HTTP 429: rate limited${_}: ${u}`)}throw new Error(`HTTP ${i.status}: ${u}`)}return i}async function v(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 H(e,n){return await(await r("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:n})).json()}async function q(e,n){return await(await r("/api/drone/role",{method:"GET",droneSession:e,apiUrl:n})).json()}async function B(e,n){return await(await r("/api/drone/whoami",{method:"GET",droneSession:e,apiUrl:n})).json()}async function N(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 X(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 W(e,n,t){await r(`/api/drone/log/${t}/ack`,{method:"POST",body:JSON.stringify({kind:"ack"}),droneSession:e,apiUrl:n})}async function z(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 F(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 Q(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 V(){return await(await r("/api/cubes",{method:"GET"})).json()}async function Y(){return await(await r("/api/templates",{method:"GET"})).json()}async function Z(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 K(e,n){return await(await r(`/api/cubes/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ee(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 te(e){await r(`/api/cubes/${e}`,{method:"DELETE"})}async function ne(e,n){return await(await r(`/api/cubes/${e}/roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function oe(e,n){return await(await r(`/api/roles/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function se(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 re(e){await r(`/api/roles/${e}`,{method:"DELETE"})}async function ae(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 ie(e){$(e,"drone_id"),await r(`/api/drones/${e}`,{method:"DELETE"})}async function ce(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 pe(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 ue(){return await(await r("/api/subscription/status",{method:"GET"})).json()}async function de(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 fe(){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 le(){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{S as API_URL,W as ackLogEntry,Q as appendLog,pe as applyTemplate,v as assimilate,ue as checkSubscriptionStatus,le as createBillingPortalSession,Z as createCube,ne as createRole,fe as createSubscription,te as deleteCube,re as deleteRole,ie as evictDrone,k as extractHttpErrorMessage,ce as getCube,H as getCubeInfo,q as getRoleInfo,N as getRoster,A as getValidToken,V as listCubes,Y as listTemplates,C as parseRetryAfterMs,se as patchRoleSection,ee as patchTaxonomyClass,P as rateLimitWaitMs,X as readLog,ae as reassignDrone,z as regen,O as retryOn429,F as roleRationale,de as syncRoles,K as updateCube,oe as updateRole,B as whoami};
1
+ import{getIdToken as f,getRefreshToken as w,clearTokens as m}from"./config.js";import{refreshIdToken as S,RefreshTokenInvalidError as g,RefreshTransientError as x}from"./auth.js";import{consolePrefix as E}from"./console-prefix.js";import{debugLog as T}from"./debug.js";import{assertUuidShape as $}from"./evict-drone.js";const R=process.env.BORG_API_URL||"https://api.borgmcp.ai",k=3,C=6e4;let l=null;function b(e){return l||(l=S(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 I(e,n,t=C,o=()=>Math.random()*500){const s=e??1e3*(n+1);return Math.min(s,t)+o()}function O(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=I(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 G(){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 J(){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 G();const{droneSession:o,apiUrl:s,headers:a,...p}=n,_=s??R,y=(p.method??"GET").toUpperCase(),h=async c=>{const u={Authorization:`Bearer ${c}`,...a};o&&(u["X-Drone-Session"]=o),T(`\u2192 ${y} ${e}`);const d=await fetch(`${_}${e}`,{...p,headers:u});return T(`\u2190 ${d.status} ${y} ${e}`),d};let i=await h(t);if(i.status===401){const c=await J();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(`${E()}${c}`)})),!i.ok){const c=await i.text();T(`\u2717 ${i.status} ${y} ${e}: ${c}`);const u=O(c);if(i.status===429){const d=i.headers.get("Retry-After"),j=d?` (retry after ${d}s)`:"";throw new Error(`HTTP 429: rate limited${j}: ${u}`)}throw new Error(`HTTP ${i.status}: ${u}`)}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(){return await(await r("/api/cubes",{method:"GET"})).json()}async function ee(){return await(await r("/api/templates",{method:"GET"})).json()}async function te(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 ne(e,n){return await(await r(`/api/cubes/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function oe(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 se(e){await r(`/api/cubes/${e}`,{method:"DELETE"})}async function re(e,n){return await(await r(`/api/cubes/${e}/roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ae(e,n){return await(await r(`/api/roles/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json()}async function ie(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 ce(e){await r(`/api/roles/${e}`,{method:"DELETE"})}async function pe(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 de(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 fe(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 le(){return await(await r("/api/subscription/status",{method:"GET"})).json()}async function ye(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 he(){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 we(){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{R as API_URL,Q as ackLogEntry,Z as appendLog,fe as applyTemplate,B as assimilate,le as checkSubscriptionStatus,we as createBillingPortalSession,te as createCube,re as createRole,he as createSubscription,se as deleteCube,ce as deleteRole,ue as evictDrone,O as extractHttpErrorMessage,de as getCube,N as getCubeInfo,F as getRoleInfo,W as getRoster,G as getValidToken,K as listCubes,ee as listTemplates,P as parseRetryAfterMs,ie as patchRoleSection,oe as patchTaxonomyClass,q as probeSession,I as rateLimitWaitMs,z as readLog,pe as reassignDrone,V as regen,A as retryOn429,Y as roleRationale,ye as syncRoles,ne as updateCube,ae as updateRole,X as whoami};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * gh#794 (CR 9f302b15): the `borg setup` auth-step decision, extracted as a
3
+ * PURE, side-effect-free mapping so the SR#3 contract is unit-pinned WITHOUT
4
+ * importing setup.ts (which runs `main()` at module load) or mocking the
5
+ * monolithic runSetup. `tsc` proves setup.ts's switch COMPILES; the unit test
6
+ * over this helper proves it MAPS correctly — a future mis-edit (e.g. a
7
+ * 'transient' wrongly skipping → an SR#3 break) fails the mapping test instead
8
+ * of silently passing every probeSession test.
9
+ *
10
+ * - 'valid' → 'skip' (short-circuit OAuth; session is usable)
11
+ * - 'dead' → 'reauth' (full re-auth — NEVER short-circuit past a dead
12
+ * token, the exact failure #794 fixes)
13
+ * - 'transient' → 'retry' (network blip — don't re-auth, don't destroy)
14
+ *
15
+ * Type-only import of SessionState → zero runtime coupling to remote-client.
16
+ */
17
+ import type { SessionState } from './remote-client.js';
18
+ export type SetupAuthAction = 'skip' | 'reauth' | 'retry';
19
+ export declare function setupActionForSession(state: SessionState): SetupAuthAction;
20
+ //# sourceMappingURL=setup-action.d.ts.map
@@ -0,0 +1 @@
1
+ function t(e){switch(e){case"valid":return"skip";case"transient":return"retry";default:return"reauth"}}export{t as setupActionForSession};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * gh#818 P3 — disclose + confirm the `borg setup` global-config mutation.
3
+ *
4
+ * Step-1 of the setup wizard writes the user's GLOBAL agent config
5
+ * (registers the borg MCP server + hooks). Before gh#818 this happened
6
+ * silently on first run. This module adds informed-consent disclosure:
7
+ * it lists WHICH files will be written (well-known `os.homedir()` paths)
8
+ * and asks to continue before the first mutation.
9
+ *
10
+ * Pure / dep-injected (mirrors `resolveCliChoice` + `setup-action.ts`) so
11
+ * the decision is unit-testable without spawning a prompt or touching argv.
12
+ *
13
+ * SECURITY (SR-light, gh#818 221c43df): the disclosure lists file PATHS +
14
+ * at most the PUBLIC `BORG_API_URL` — there is NO token/secret in the
15
+ * written config to echo. Tokens live in the AES-256-GCM keychain, and
16
+ * Step-1 runs BEFORE OAuth, so at mutation time no token even exists.
17
+ */
18
+ export interface ConfigMutationTarget {
19
+ /** Human-readable config file path (tilde form). */
20
+ file: string;
21
+ /** What is added to it. */
22
+ change: string;
23
+ }
24
+ /**
25
+ * The set of global config files Step-1 writes, scoped to the detected
26
+ * agent CLIs. Paths mirror `config-utils.ts`:
27
+ * Claude Code: ~/.claude.json (MCP server) + ~/.claude/settings.json (hook)
28
+ * Codex: ~/.codex/config.toml (MCP server) + ~/.codex/hooks.json (hooks)
29
+ */
30
+ export declare function configMutationTargets(deps: {
31
+ claude: boolean;
32
+ codex: boolean;
33
+ }): ConfigMutationTarget[];
34
+ /**
35
+ * Disclosure text: the files Step-1 will write + an undo note. Pure so SR
36
+ * can pin "lists paths only, no secret". Lists the public `BORG_API_URL`
37
+ * only by reference (the MCP-server registration env), never a credential.
38
+ */
39
+ export declare function formatConfigMutationDisclosure(targets: ConfigMutationTarget[]): string;
40
+ export type ConfirmDecision = 'proceed' | 'abort';
41
+ export interface ConfirmConfigMutationDeps {
42
+ /** `process.stdin.isTTY === true`. */
43
+ isTTY: boolean;
44
+ /** `--yes` / `-y` present in argv. */
45
+ yes: boolean;
46
+ /**
47
+ * Injected interactive confirm — returns the user's yes(true)/no(false).
48
+ * Only invoked in the TTY-and-not-`--yes` path; NEVER called otherwise
49
+ * (so a non-TTY never reads stdin — see item 1 below).
50
+ */
51
+ confirm: () => Promise<boolean>;
52
+ }
53
+ /**
54
+ * Decide whether to proceed with the Step-1 config mutation.
55
+ *
56
+ * The six CR-binding build-gate items (gh#818, 3b3e85a5) live here:
57
+ * 1. (THE load-bearing headless no-regress) non-TTY → 'proceed' WITHOUT
58
+ * prompting. We return before touching `confirm`, so a non-TTY run
59
+ * (CI / pipe / headless) never reads stdin → no hang.
60
+ * 2. `--yes`/`-y` → 'proceed' without prompting (scripted-but-TTY +
61
+ * explicit non-interactive). No collision with --no-browser/--device.
62
+ * 3. TTY + interactive → ask; decline → 'abort' (the caller exits BEFORE
63
+ * any write).
64
+ * 6. This dep-injected shape IS the testable seam.
65
+ */
66
+ export declare function confirmConfigMutation(deps: ConfirmConfigMutationDeps): Promise<ConfirmDecision>;
67
+ /**
68
+ * Scan argv for the `--yes` / `-y` bypass. Kept separate so the flag set
69
+ * is pinned in tests (item 2 — no collision with the existing
70
+ * --no-browser/--device scan in setup.ts).
71
+ */
72
+ export declare function parseYesFlag(argv: string[]): boolean;
73
+ //# sourceMappingURL=setup-confirm.d.ts.map
@@ -0,0 +1,2 @@
1
+ function t(e){const o=[];return e.claude&&(o.push({file:"~/.claude.json",change:"registers the borg MCP server"}),o.push({file:"~/.claude/settings.json",change:"adds a UserPromptSubmit hook"})),e.codex&&(o.push({file:"~/.codex/config.toml",change:"registers the borg MCP server"}),o.push({file:"~/.codex/hooks.json",change:"adds SessionStart + UserPromptSubmit hooks"})),o}function n(e){const o=[];o.push("borg setup will register the borg MCP server in your agent config:");for(const r of e)o.push(` \u2022 ${r.file} (${r.change})`);return o.push('These changes are additive and reversible \u2014 remove the "borg" entries to undo.'),o.join(`
2
+ `)}async function s(e){return!e.isTTY||e.yes||await e.confirm()?"proceed":"abort"}function i(e){return e.includes("--yes")||e.includes("-y")}export{t as configMutationTargets,s as confirmConfigMutation,n as formatConfigMutationDisclosure,i as parseYesFlag};
package/dist/setup.js CHANGED
@@ -1,30 +1,33 @@
1
1
  #!/usr/bin/env node
2
- import h from"prompts";import e from"chalk";import g from"open";import u from"which";import{authenticateWithGoogle as b}from"./auth.js";import{checkSubscriptionStatus as c,createSubscription as m}from"./remote-client.js";import{retrySubscriptionCheck as d}from"./subscription-retry.js";import{addMcpServer as f,addUserPromptSubmitHook as y,addCodexMcpServer as w,addCodexSessionStartHook as C,addCodexUserPromptSubmitHook as k,isMcpServerConfigured as S,isCodexMcpServerConfigured as x,removeSessionStartHook as v}from"./config-utils.js";import{isAuthenticated as F}from"./config.js";import{handleVersionFlag as $}from"./version.js";import{initDebugFromArgv as A}from"./debug.js";async function P(){A(process.argv),$(),console.log(e.blue.bold(`
3
- \u25FC Borg MCP Setup Wizard \u25FC`));const i=process.argv.includes("--no-browser")||process.argv.includes("--device");let n=null,t=null;try{n=u.sync("claude")}catch{}try{t=u.sync("codex")}catch{}if(n&&console.log(e.gray(`Found Claude CLI: ${n}`)),t&&console.log(e.gray(`Found Codex CLI: ${t}`)),(n||t)&&console.log(""),!n&&!t&&(console.error(e.red(`\u25FC No supported agent CLI found
2
+ import u from"prompts";import e from"chalk";import p from"open";import d from"which";import{authenticateWithGoogle as f}from"./auth.js";import{checkSubscriptionStatus as i,createSubscription as y,probeSession as w}from"./remote-client.js";import{setupActionForSession as C}from"./setup-action.js";import{confirmConfigMutation as k,configMutationTargets as S,formatConfigMutationDisclosure as x,parseYesFlag as v}from"./setup-confirm.js";import{retrySubscriptionCheck as h}from"./subscription-retry.js";import{addMcpServer as F,addUserPromptSubmitHook as $,addCodexMcpServer as A,addCodexSessionStartHook as M,addCodexUserPromptSubmitHook as P,isMcpServerConfigured as R,isCodexMcpServerConfigured as T,removeSessionStartHook as U}from"./config-utils.js";import{handleVersionFlag as q}from"./version.js";import{initDebugFromArgv as D}from"./debug.js";async function I(){D(process.argv),q(),console.log(e.blue.bold(`
3
+ \u25FC Borg MCP Setup Wizard \u25FC`));const c=process.argv.includes("--no-browser")||process.argv.includes("--device");let t=null,n=null;try{t=d.sync("claude")}catch{}try{n=d.sync("codex")}catch{}t&&console.log(e.gray(`Found Claude CLI: ${t}`)),n&&console.log(e.gray(`Found Codex CLI: ${n}`)),(t||n)&&console.log(""),!t&&!n&&(console.error(e.red(`\u25FC No supported agent CLI found
4
4
  `)),console.error(e.yellow("Please install Claude Code or Codex first:")),console.error(e.gray(" Claude Code: https://claude.ai/download")),console.error(e.gray(` Codex: https://developers.openai.com/codex
5
- `)),process.exit(1)),console.log(e.blue("\u25FC Agent CLI Integration")),n)try{S()||f(),v(),y(),console.log(e.green("\u25FC borg configured for Claude Code"))}catch(o){console.error(e.red(`
5
+ `)),process.exit(1)),console.log(e.blue("\u25FC Agent CLI Integration"));const b=v(process.argv);if(console.log(x(S({claude:t!==null,codex:n!==null}))),await k({isTTY:process.stdin.isTTY===!0,yes:b,confirm:async()=>{const{proceed:o}=await u({type:"confirm",name:"proceed",message:"Continue with these changes?",initial:!0});return o===!0}})==="abort"&&(console.log(e.yellow(`
6
+ \u25FC Setup cancelled \u2014 no changes made.
7
+ `)),process.exit(0)),console.log(""),t)try{R()||F(),U(),$(),console.log(e.green("\u25FC borg configured for Claude Code"))}catch(o){console.error(e.red(`
6
8
  \u25FC Failed to configure Claude Code: ${o.message}
7
- `)),process.exit(1)}if(t)try{x()||w(),C(),k(),console.log(e.green("\u25FC borg configured for Codex"))}catch(o){console.error(e.red(`
9
+ `)),process.exit(1)}if(n)try{T()||A(),M(),P(),console.log(e.green("\u25FC borg configured for Codex"))}catch(o){console.error(e.red(`
8
10
  \u25FC Failed to configure Codex: ${o.message}
9
- `)),process.exit(1)}if(console.log(""),console.log(e.blue("\u25FC Google Authentication")),await F())console.log(e.green(`\u25FC Already authenticated
10
- `));else try{await b(i?{noBrowser:!0}:void 0)}catch(o){console.error(e.red(`
11
+ `)),process.exit(1)}console.log(""),console.log(e.blue("\u25FC Google Authentication"));const g=C(await w());if(g==="skip")console.log(e.green(`\u25FC Already signed in
12
+ `));else if(g==="retry")console.error(e.yellow(`
13
+ \u25FC Could not reach Google to verify your session (network issue).`)),console.error(e.yellow("Re-run `borg setup` when your connection is back.\n")),process.exit(1);else try{await f(c?{noBrowser:!0}:void 0)}catch(o){console.error(e.red(`
11
14
  \u25FC Authentication failed: ${o.message}
12
- `)),console.error(e.yellow("Re-run `borg setup` to try again.\n")),process.exit(1)}console.log(e.blue("\u25FC Subscription Check"));let s;try{s=await c()}catch(o){console.error(e.yellow(`
15
+ `)),console.error(e.yellow("Re-run `borg setup` to try again.\n")),process.exit(1)}console.log(e.blue("\u25FC Subscription Check"));let s;try{s=await i()}catch(o){console.error(e.yellow(`
13
16
  \u25FC Subscription check failed: ${o.message}`)),console.error(e.gray(`\u25FC Retrying before falling back to the Free tier...
14
- `)),s={hasAccess:!1}}if(s=await d(s,{check:c,sleep:o=>new Promise(r=>setTimeout(r,o)),onRetry:(o,r)=>console.log(e.gray(`\u25FC Checking subscription... (attempt ${o}/${r})`))}),s.hasAccess)if(console.log(e.green("\u25FC Active subscription found")),s.expiresAt){const o=new Date(s.expiresAt);console.log(e.gray(` Expires: ${o.toLocaleDateString()}
17
+ `)),s={hasAccess:!1}}if(s=await h(s,{check:i,sleep:o=>new Promise(r=>setTimeout(r,o)),onRetry:(o,r)=>console.log(e.gray(`\u25FC Checking subscription... (attempt ${o}/${r})`))}),s.hasAccess)if(console.log(e.green("\u25FC Active subscription found")),s.expiresAt){const o=new Date(s.expiresAt);console.log(e.gray(` Expires: ${o.toLocaleDateString()}
15
18
  `))}else console.log("");else{console.log(e.green("\u25FC You're on the Free tier \u2014 permanent, no card needed: 1 cube + 3 agent sessions + 100 req/hr.")),console.log(e.gray(`\u25FC Start using borgmcp right now. Upgrade any time: $1/month per cube, each cube adds 8 pooled agent sessions + 1000 req/hr.
16
- `));const{subscribeMethod:o}=await h({type:"select",name:"subscribeMethod",message:"You're ready on the Free tier. Want to do more?",choices:[{title:"\u25FC Continue on the Free tier (recommended)",value:"skip",description:"Start now \u2014 1 cube, 3 agent sessions, 100 req/hr. No payment required."},{title:"\u25FC Upgrade to Cube tier \u2014 $1/month per cube",value:"web",description:"Each cube adds 8 pooled agent sessions + 1000 req/hr. Opens the subscribe page in your browser."},{title:"\u25FC Quick Stripe checkout",value:"stripe",description:"Fast upgrade checkout in the browser"},{title:"\u25FC I already subscribed \u2014 re-check",value:"recheck",description:"Re-check now \u2014 a just-completed subscription can take a moment to activate"}]});switch(o===void 0&&console.log(e.yellow(`
19
+ `));const{subscribeMethod:o}=await u({type:"select",name:"subscribeMethod",message:"You're ready on the Free tier. Want to do more?",choices:[{title:"\u25FC Continue on the Free tier (recommended)",value:"skip",description:"Start now \u2014 1 cube, 3 agent sessions, 100 req/hr. No payment required."},{title:"\u25FC Upgrade to Cube tier \u2014 $1/month per cube",value:"web",description:"Each cube adds 8 pooled agent sessions + 1000 req/hr. Opens the subscribe page in your browser."},{title:"\u25FC Quick Stripe checkout",value:"stripe",description:"Fast upgrade checkout in the browser"},{title:"\u25FC I already subscribed \u2014 re-check",value:"recheck",description:"Re-check now \u2014 a just-completed subscription can take a moment to activate"}]});switch(o===void 0&&console.log(e.yellow(`
17
20
  \u25FC No subscription option selected \u2014 continuing on the Free tier.
18
21
  `)),o){case"web":console.log(e.blue(`
19
- \u25FC Opening: https://borgmcp.ai/subscribe`));try{await g("https://borgmcp.ai/subscribe"),console.log(e.gray(`\u25FC Waiting for subscription (checking every 5s for 2 min)...
20
- `)),await p()}catch(r){console.error(e.yellow(`
22
+ \u25FC Opening: https://borgmcp.ai/subscribe`));try{await p("https://borgmcp.ai/subscribe"),console.log(e.gray(`\u25FC Waiting for subscription (checking every 5s for 2 min)...
23
+ `)),await m()}catch(r){console.error(e.yellow(`
21
24
  \u25FC ${r.message}`)),console.log(e.green(`\u25FC Continuing on the Free tier. Upgrade any time from https://borgmcp.ai/subscribe.
22
- `))}break;case"stripe":try{const r=await m();console.log(e.blue(`
23
- \u25FC Opening Stripe: ${r}`)),await g(r),console.log(e.gray(`\u25FC Waiting for subscription...
24
- `)),await p()}catch(r){console.error(e.red(`
25
+ `))}break;case"stripe":try{const r=await y();console.log(e.blue(`
26
+ \u25FC Opening Stripe: ${r}`)),await p(r),console.log(e.gray(`\u25FC Waiting for subscription...
27
+ `)),await m()}catch(r){console.error(e.red(`
25
28
  \u25FC Failed to create checkout: ${r.message}
26
29
  `)),console.log(e.green(`\u25FC Continuing on the Free tier. Upgrade any time from https://borgmcp.ai/subscribe.
27
- `))}break;case"recheck":try{let r;try{r=await c()}catch{r={hasAccess:!1}}r=await d(r,{check:c,sleep:a=>new Promise(l=>setTimeout(l,a)),onRetry:(a,l)=>console.log(e.gray(`\u25FC Re-checking subscription... (attempt ${a}/${l})`))}),r.hasAccess?console.log(e.green(`
30
+ `))}break;case"recheck":try{let r;try{r=await i()}catch{r={hasAccess:!1}}r=await h(r,{check:i,sleep:a=>new Promise(l=>setTimeout(l,a)),onRetry:(a,l)=>console.log(e.gray(`\u25FC Re-checking subscription... (attempt ${a}/${l})`))}),r.hasAccess?console.log(e.green(`
28
31
  \u25FC Subscription found!
29
32
  `)):console.log(e.yellow(`
30
33
  \u25FC No subscription found \u2014 continuing on the Free tier.
@@ -36,7 +39,7 @@ import h from"prompts";import e from"chalk";import g from"open";import u from"wh
36
39
  `));break}}console.log(e.green.bold(`Setup complete!
37
40
  `)),console.log(e.yellow(`\u{1F504} Restart Claude Code/Codex (or open a new session) for the changes to take effect.
38
41
  `)),console.log(e.gray("\u25FC Next steps:")),console.log(e.gray('1. cd into your project, then run "borg assimilate" to join a cube')),console.log(e.gray(" (this creates/joins the cube and launches your agent)")),console.log(e.gray(`2. Manage cubes and subscription at https://borgmcp.ai/dashboard
39
- `))}async function p(){for(let n=0;n<24;n++){await new Promise(t=>setTimeout(t,5e3));try{if((await c()).hasAccess){console.log(e.green(`\u25FC Subscription activated!
40
- `));return}}catch{}}throw new Error("Timeout - Run setup again after subscribing")}P().catch(i=>{console.error(e.red(`
41
- \u25FC Setup failed: ${i.message}
42
+ `))}async function m(){for(let t=0;t<24;t++){await new Promise(n=>setTimeout(n,5e3));try{if((await i()).hasAccess){console.log(e.green(`\u25FC Subscription activated!
43
+ `));return}}catch{}}throw new Error("Timeout - Run setup again after subscribing")}I().catch(c=>{console.error(e.red(`
44
+ \u25FC Setup failed: ${c.message}
42
45
  `)),process.exit(1)});
@@ -43,6 +43,12 @@ import type { StreamStatus } from './log-stream.js';
43
43
  * platforms gracefully).
44
44
  */
45
45
  export declare function checkInboxMonitorHealthy(inboxPath: string | null): boolean | null;
46
+ /**
47
+ * gh#822: is the holder heartbeat sidecar present AND stale past the threshold?
48
+ * Absent (old monitor / just-armed) or fresh → NOT stale (false). Only a
49
+ * present-but-stale sidecar → true (the wedged-holder signal).
50
+ */
51
+ export declare function isHeartbeatStale(inboxPath: string): boolean;
46
52
  export interface RenderInputs {
47
53
  status: StreamStatus;
48
54
  /**
@@ -1,3 +1,3 @@
1
- import{spawnSync as p}from"node:child_process";function m(n){if(!n)return null;try{const t=p("pgrep",["-f",n],{encoding:"utf-8",timeout:2e3});return t.error?null:t.status===0&&t.stdout.trim().length>0?!0:t.status===1?!1:null}catch{return null}}function f(n){const{status:t,inboxMonitorHealthy:s,inboxPath:r,droneLabel:l,cubeName:u,humanAgo:i}=n,h=t.reconnectAttempts===0&&t.lastWireActivityAt===null&&!t.connected,c=t.ownership?.state==="owned-by-other-process";let o;c?o="**Stream owned by another Borg MCP process.**":h?o="**Stream not started.**":t.connected?s===!1?o="**Stream connected (no inbox-Monitor \u2014 wake path broken).**":t.lastContentEventAt===null?o="**Stream connected, awaiting first content event.**":o=`**Stream connected, last content ${i(new Date(t.lastContentEventAt))}.**`:o=`**Stream disconnected (reconnect attempt ${t.reconnectAttempts}).**`;const e=[];if(e.push(o),e.push(""),e.push("# Log-stream status"),e.push(""),c?e.push("- **state**: _(stream owner is another local process)_"):h?e.push("- **state**: _(stream not started)_"):e.push(`- **connected**: ${t.connected}`),e.push(`- **last content event**: ${t.lastContentEventAt?`${t.lastContentEventAt} (${i(new Date(t.lastContentEventAt))})`:"_(none yet)_"}`),e.push(`- **last heartbeat at**: ${t.lastHeartbeatAt?`${t.lastHeartbeatAt} (${i(new Date(t.lastHeartbeatAt))})`:"_(none)_"}`),e.push(`- **last wire activity**: ${t.lastWireActivityAt?`${t.lastWireActivityAt} (${i(new Date(t.lastWireActivityAt))})`:"_(none)_"}`),e.push(`- **last persisted event id**: ${t.lastPersistedEventId??"_(none)_"}`),e.push(`- **reconnect attempts**: ${t.reconnectAttempts}`),c){const a=t.ownership;e.push(`- **stream owner pid**: ${a.pid??"_(unknown)_"}`),e.push(`- **stream owner cwd**: ${a.cwd??"_(unknown)_"}`),e.push(`- **stream owner heartbeat age**: ${typeof a.ageMs=="number"&&Number.isFinite(a.ageMs)?`${Math.max(0,Math.round(a.ageMs/1e3))}s`:"_(unknown)_"}`)}return t.connected&&s===!1&&(e.push("- **inbox-monitor**: _(no watcher detected \u2014 wake path broken)_"),r&&l&&u&&(e.push(""),e.push("## Real-time wake-up (do this now)"),e.push("No process is tailing this drone's inbox file \u2014 this session won't wake on real-time cube activity, only on its fallback timer, and will miss live coordination from other drones. Arm an inbox Monitor:"),e.push(""),e.push(`> Monitor command: \`tail -n 0 -F ${r}\` \u2014 persistent, 1h timeout, description "borg inbox for ${l} on cube ${u}".`))),e.join(`
2
- `)}function b(n,t){return n.connected&&t===!1}function w(n){const{inboxPath:t,droneLabel:s,cubeName:r}=n;return!t||!s||!r?"":["## \u26A0 Wake path broken \u2014 arm Monitor NOW","","No process is tailing this drone's inbox file. SSE delivery is healthy (entries reach disk), but Claude Code has no event source to wake on. Until you arm a Monitor, this session only wakes on the /loop fallback heartbeat and will miss live coordination from other drones:","",`> Monitor command: \`tail -n 0 -F ${t}\` \u2014 persistent, 1h timeout, description "borg inbox for ${s} on cube ${r}".`,"","---",""].join(`
3
- `)}export{m as checkInboxMonitorHealthy,w as formatWakePathPrefix,f as renderStreamStatus,b as shouldShowWakePathWarning};
1
+ import{spawnSync as p}from"node:child_process";import{statSync as m}from"node:fs";import{heartbeatPathFor as d,HEARTBEAT_STALE_MS as f}from"./inbox-monitor.js";function y(n){if(!n)return null;try{const t=p("pgrep",["-f",n],{encoding:"utf-8",timeout:2e3});return t.error?null:t.status===0&&t.stdout.trim().length>0?!b(n):t.status===1?!1:null}catch{return null}}function b(n){try{const t=m(d(n)).mtimeMs;return Date.now()-t>f}catch{return!1}}function _(n){const{status:t,inboxMonitorHealthy:s,inboxPath:r,droneLabel:l,cubeName:u,humanAgo:i}=n,h=t.reconnectAttempts===0&&t.lastWireActivityAt===null&&!t.connected,c=t.ownership?.state==="owned-by-other-process";let o;c?o="**Stream owned by another Borg MCP process.**":h?o="**Stream not started.**":t.connected?s===!1?o="**Stream connected (no inbox-Monitor \u2014 wake path broken).**":t.lastContentEventAt===null?o="**Stream connected, awaiting first content event.**":o=`**Stream connected, last content ${i(new Date(t.lastContentEventAt))}.**`:o=`**Stream disconnected (reconnect attempt ${t.reconnectAttempts}).**`;const e=[];if(e.push(o),e.push(""),e.push("# Log-stream status"),e.push(""),c?e.push("- **state**: _(stream owner is another local process)_"):h?e.push("- **state**: _(stream not started)_"):e.push(`- **connected**: ${t.connected}`),e.push(`- **last content event**: ${t.lastContentEventAt?`${t.lastContentEventAt} (${i(new Date(t.lastContentEventAt))})`:"_(none yet)_"}`),e.push(`- **last heartbeat at**: ${t.lastHeartbeatAt?`${t.lastHeartbeatAt} (${i(new Date(t.lastHeartbeatAt))})`:"_(none)_"}`),e.push(`- **last wire activity**: ${t.lastWireActivityAt?`${t.lastWireActivityAt} (${i(new Date(t.lastWireActivityAt))})`:"_(none)_"}`),e.push(`- **last persisted event id**: ${t.lastPersistedEventId??"_(none)_"}`),e.push(`- **reconnect attempts**: ${t.reconnectAttempts}`),c){const a=t.ownership;e.push(`- **stream owner pid**: ${a.pid??"_(unknown)_"}`),e.push(`- **stream owner cwd**: ${a.cwd??"_(unknown)_"}`),e.push(`- **stream owner heartbeat age**: ${typeof a.ageMs=="number"&&Number.isFinite(a.ageMs)?`${Math.max(0,Math.round(a.ageMs/1e3))}s`:"_(unknown)_"}`)}return t.connected&&s===!1&&(e.push("- **inbox-monitor**: _(no watcher detected \u2014 wake path broken)_"),r&&l&&u&&(e.push(""),e.push("## Real-time wake-up (do this now)"),e.push("No process is tailing this drone's inbox file \u2014 this session won't wake on real-time cube activity, only on its fallback timer, and will miss live coordination from other drones. Arm an inbox Monitor:"),e.push(""),e.push(`> Monitor command: \`tail -n 0 -F ${r}\` \u2014 persistent, 1h timeout, description "borg inbox for ${l} on cube ${u}".`))),e.join(`
2
+ `)}function k(n,t){return n.connected&&t===!1}function v(n){const{inboxPath:t,droneLabel:s,cubeName:r}=n;return!t||!s||!r?"":["## \u26A0 Wake path broken \u2014 arm Monitor NOW","","No process is tailing this drone's inbox file. SSE delivery is healthy (entries reach disk), but Claude Code has no event source to wake on. Until you arm a Monitor, this session only wakes on the /loop fallback heartbeat and will miss live coordination from other drones:","",`> Monitor command: \`tail -n 0 -F ${t}\` \u2014 persistent, 1h timeout, description "borg inbox for ${s} on cube ${r}".`,"","---",""].join(`
3
+ `)}export{y as checkInboxMonitorHealthy,v as formatWakePathPrefix,b as isHeartbeatStale,_ as renderStreamStatus,k as shouldShowWakePathWarning};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
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",