borgmcp 1.0.6 → 1.0.8

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.
Files changed (160) hide show
  1. package/README.md +5 -3
  2. package/dist/assimilate-cmd.js +39 -511
  3. package/dist/assimilate-deps.js +3 -177
  4. package/dist/assimilate-welcome.js +2 -24
  5. package/dist/auth-env.js +1 -107
  6. package/dist/auth.js +23 -612
  7. package/dist/claude.js +11 -281
  8. package/dist/cli-help.js +29 -50
  9. package/dist/cli-platform.js +4 -94
  10. package/dist/codex-app-server.js +4 -228
  11. package/dist/codex-app-wake.js +2 -122
  12. package/dist/codex-launch.js +1 -81
  13. package/dist/codex-remote.js +1 -250
  14. package/dist/config-utils.js +3 -385
  15. package/dist/config.js +1 -190
  16. package/dist/console-prefix.js +1 -86
  17. package/dist/cube-name.js +1 -65
  18. package/dist/cubes.js +4 -269
  19. package/dist/debug.js +1 -71
  20. package/dist/device-auth.js +1 -167
  21. package/dist/direct-log.js +1 -11
  22. package/dist/health-beat.js +1 -168
  23. package/dist/inbox-monitor.js +1 -129
  24. package/dist/index.js +26 -1378
  25. package/dist/lifecycle-log-guard.js +2 -93
  26. package/dist/list-roles-render.js +6 -39
  27. package/dist/log-audit.js +3 -186
  28. package/dist/log-stream.js +9 -848
  29. package/dist/name-validator.js +1 -22
  30. package/dist/parse-assimilate-args.js +1 -82
  31. package/dist/postinstall.js +8 -22
  32. package/dist/regen-format.js +11 -337
  33. package/dist/regen.js +5 -83
  34. package/dist/remote-client.d.ts +4 -7
  35. package/dist/remote-client.js +1 -695
  36. package/dist/role-resolver.js +1 -36
  37. package/dist/role-section.js +8 -208
  38. package/dist/roster-render.js +3 -96
  39. package/dist/setup.js +41 -251
  40. package/dist/shell-escape.js +1 -22
  41. package/dist/spawn.js +10 -29
  42. package/dist/stale-version-check.js +1 -102
  43. package/dist/stream-owner.js +2 -202
  44. package/dist/stream-status.js +3 -211
  45. package/dist/subscription-retry.js +1 -23
  46. package/dist/sync-roles-render.js +3 -118
  47. package/dist/sync.js +22 -286
  48. package/dist/templates.js +120 -626
  49. package/dist/terminal-title.js +1 -68
  50. package/dist/token-crypto.js +1 -91
  51. package/dist/token-store.js +1 -222
  52. package/dist/types.d.ts +0 -5
  53. package/dist/types.js +0 -5
  54. package/dist/version.js +2 -78
  55. package/dist/worktree-lifecycle.js +2 -173
  56. package/package.json +12 -2
  57. package/dist/assimilate-cmd.d.ts.map +0 -1
  58. package/dist/assimilate-cmd.js.map +0 -1
  59. package/dist/assimilate-deps.d.ts.map +0 -1
  60. package/dist/assimilate-deps.js.map +0 -1
  61. package/dist/assimilate-welcome.d.ts.map +0 -1
  62. package/dist/assimilate-welcome.js.map +0 -1
  63. package/dist/auth-env.d.ts.map +0 -1
  64. package/dist/auth-env.js.map +0 -1
  65. package/dist/auth.d.ts.map +0 -1
  66. package/dist/auth.js.map +0 -1
  67. package/dist/claude.d.ts.map +0 -1
  68. package/dist/claude.js.map +0 -1
  69. package/dist/cli-help.d.ts.map +0 -1
  70. package/dist/cli-help.js.map +0 -1
  71. package/dist/cli-platform.d.ts.map +0 -1
  72. package/dist/cli-platform.js.map +0 -1
  73. package/dist/codex-app-server.d.ts.map +0 -1
  74. package/dist/codex-app-server.js.map +0 -1
  75. package/dist/codex-app-wake.d.ts.map +0 -1
  76. package/dist/codex-app-wake.js.map +0 -1
  77. package/dist/codex-launch.d.ts.map +0 -1
  78. package/dist/codex-launch.js.map +0 -1
  79. package/dist/codex-remote.d.ts.map +0 -1
  80. package/dist/codex-remote.js.map +0 -1
  81. package/dist/config-utils.d.ts.map +0 -1
  82. package/dist/config-utils.js.map +0 -1
  83. package/dist/config.d.ts.map +0 -1
  84. package/dist/config.js.map +0 -1
  85. package/dist/console-prefix.d.ts.map +0 -1
  86. package/dist/console-prefix.js.map +0 -1
  87. package/dist/cube-name.d.ts.map +0 -1
  88. package/dist/cube-name.js.map +0 -1
  89. package/dist/cubes.d.ts.map +0 -1
  90. package/dist/cubes.js.map +0 -1
  91. package/dist/debug.d.ts.map +0 -1
  92. package/dist/debug.js.map +0 -1
  93. package/dist/device-auth.d.ts.map +0 -1
  94. package/dist/device-auth.js.map +0 -1
  95. package/dist/direct-log.d.ts.map +0 -1
  96. package/dist/direct-log.js.map +0 -1
  97. package/dist/health-beat.d.ts.map +0 -1
  98. package/dist/health-beat.js.map +0 -1
  99. package/dist/inbox-monitor.d.ts.map +0 -1
  100. package/dist/inbox-monitor.js.map +0 -1
  101. package/dist/index.d.ts.map +0 -1
  102. package/dist/index.js.map +0 -1
  103. package/dist/lifecycle-log-guard.d.ts.map +0 -1
  104. package/dist/lifecycle-log-guard.js.map +0 -1
  105. package/dist/list-roles-render.d.ts.map +0 -1
  106. package/dist/list-roles-render.js.map +0 -1
  107. package/dist/log-audit.d.ts.map +0 -1
  108. package/dist/log-audit.js.map +0 -1
  109. package/dist/log-stream.d.ts.map +0 -1
  110. package/dist/log-stream.js.map +0 -1
  111. package/dist/name-validator.d.ts.map +0 -1
  112. package/dist/name-validator.js.map +0 -1
  113. package/dist/parse-assimilate-args.d.ts.map +0 -1
  114. package/dist/parse-assimilate-args.js.map +0 -1
  115. package/dist/postinstall.d.ts.map +0 -1
  116. package/dist/postinstall.js.map +0 -1
  117. package/dist/regen-format.d.ts.map +0 -1
  118. package/dist/regen-format.js.map +0 -1
  119. package/dist/regen.d.ts.map +0 -1
  120. package/dist/regen.js.map +0 -1
  121. package/dist/remote-client.d.ts.map +0 -1
  122. package/dist/remote-client.js.map +0 -1
  123. package/dist/role-resolver.d.ts.map +0 -1
  124. package/dist/role-resolver.js.map +0 -1
  125. package/dist/role-section.d.ts.map +0 -1
  126. package/dist/role-section.js.map +0 -1
  127. package/dist/roster-render.d.ts.map +0 -1
  128. package/dist/roster-render.js.map +0 -1
  129. package/dist/setup.d.ts.map +0 -1
  130. package/dist/setup.js.map +0 -1
  131. package/dist/shell-escape.d.ts.map +0 -1
  132. package/dist/shell-escape.js.map +0 -1
  133. package/dist/spawn.d.ts.map +0 -1
  134. package/dist/spawn.js.map +0 -1
  135. package/dist/stale-version-check.d.ts.map +0 -1
  136. package/dist/stale-version-check.js.map +0 -1
  137. package/dist/stream-owner.d.ts.map +0 -1
  138. package/dist/stream-owner.js.map +0 -1
  139. package/dist/stream-status.d.ts.map +0 -1
  140. package/dist/stream-status.js.map +0 -1
  141. package/dist/subscription-retry.d.ts.map +0 -1
  142. package/dist/subscription-retry.js.map +0 -1
  143. package/dist/sync-roles-render.d.ts.map +0 -1
  144. package/dist/sync-roles-render.js.map +0 -1
  145. package/dist/sync.d.ts.map +0 -1
  146. package/dist/sync.js.map +0 -1
  147. package/dist/templates.d.ts.map +0 -1
  148. package/dist/templates.js.map +0 -1
  149. package/dist/terminal-title.d.ts.map +0 -1
  150. package/dist/terminal-title.js.map +0 -1
  151. package/dist/token-crypto.d.ts.map +0 -1
  152. package/dist/token-crypto.js.map +0 -1
  153. package/dist/token-store.d.ts.map +0 -1
  154. package/dist/token-store.js.map +0 -1
  155. package/dist/types.d.ts.map +0 -1
  156. package/dist/types.js.map +0 -1
  157. package/dist/version.d.ts.map +0 -1
  158. package/dist/version.js.map +0 -1
  159. package/dist/worktree-lifecycle.d.ts.map +0 -1
  160. package/dist/worktree-lifecycle.js.map +0 -1
package/dist/debug.js CHANGED
@@ -1,71 +1 @@
1
- /**
2
- * Opt-in debug logging for the borg CLI (Queen observability ask — surface
3
- * exactly what failed on errors like the cross-account assimilate 404).
4
- *
5
- * Enabled by `--debug` on the command line OR a truthy `BORG_DEBUG` env var,
6
- * wired at the CLI entry points (top-level `borg` dispatcher + `borg setup`
7
- * + `borg assimilate`). When on, `authedFetch` (remote-client.ts) emits one
8
- * line per HTTP request + the server error body on failure.
9
- *
10
- * Output goes to STDERR with a `[borg:debug]` prefix so it never contaminates
11
- * STDOUT (the MCP / tool output stream a drone session proxies).
12
- *
13
- * HARD INVARIANT: debug output must NEVER contain token material — no
14
- * Authorization header, id_token, or refresh_token. Call sites log only
15
- * method / path / status / server-error-body; the Bearer token is omitted.
16
- */
17
- let debugEnabled = false;
18
- /** Enable or disable debug logging for the lifetime of the process. */
19
- export function setDebug(on) {
20
- debugEnabled = on;
21
- }
22
- /** Whether debug logging is currently enabled. */
23
- export function isDebug() {
24
- return debugEnabled;
25
- }
26
- /**
27
- * Emit a debug line to STDERR with the `[borg:debug]` prefix. No-op when
28
- * debug is disabled, so call sites need not guard. STDOUT is left clean for
29
- * MCP / tool output.
30
- */
31
- export function debugLog(...args) {
32
- if (!debugEnabled)
33
- return;
34
- if (args.length > 0 && typeof args[0] === 'string') {
35
- console.error(`[borg:debug] ${args[0]}`, ...args.slice(1));
36
- }
37
- else {
38
- console.error('[borg:debug]', ...args);
39
- }
40
- }
41
- /**
42
- * Resolve debug state from a process argv array + `BORG_DEBUG` at a CLI entry
43
- * point. Enables debug when `--debug` is present OR `BORG_DEBUG` is truthy,
44
- * then STRIPS every `--debug` token from the array IN PLACE so downstream
45
- * subcommand parsers (which reject unknown flags) never see it. Idempotent —
46
- * safe to call from more than one entry point in the same process.
47
- */
48
- export function initDebugFromArgv(argv) {
49
- const hasFlag = argv.includes('--debug');
50
- if (hasFlag || isTruthyEnv(process.env.BORG_DEBUG)) {
51
- setDebug(true);
52
- }
53
- if (hasFlag) {
54
- for (let i = argv.length - 1; i >= 0; i -= 1) {
55
- if (argv[i] === '--debug')
56
- argv.splice(i, 1);
57
- }
58
- }
59
- }
60
- /** A non-empty env value other than the common falsy spellings is truthy. */
61
- function isTruthyEnv(value) {
62
- if (!value)
63
- return false;
64
- const v = value.trim().toLowerCase();
65
- return v !== '' && v !== '0' && v !== 'false' && v !== 'no' && v !== 'off';
66
- }
67
- /** Test hook: reset module-level debug state between tests. */
68
- export function _resetDebugForTests() {
69
- debugEnabled = false;
70
- }
71
- //# sourceMappingURL=debug.js.map
1
+ let n=!1;function r(e){n=e}function u(){return n}function f(...e){n&&(e.length>0&&typeof e[0]=="string"?console.error(`[borg:debug] ${e[0]}`,...e.slice(1)):console.error("[borg:debug]",...e))}function s(e){const o=e.includes("--debug");if((o||i(process.env.BORG_DEBUG))&&r(!0),o)for(let t=e.length-1;t>=0;t-=1)e[t]==="--debug"&&e.splice(t,1)}function i(e){if(!e)return!1;const o=e.trim().toLowerCase();return o!==""&&o!=="0"&&o!=="false"&&o!=="no"&&o!=="off"}function l(){n=!1}export{l as _resetDebugForTests,f as debugLog,s as initDebugFromArgv,u as isDebug,r as setDebug};
@@ -1,167 +1 @@
1
- /**
2
- * gh#557 — Google OAuth 2.0 Device Authorization Grant (RFC 8628).
3
- *
4
- * The no-browser counterpart to the loopback flow in auth.ts. Instead of
5
- * opening a browser and listening on localhost, the device flow:
6
- * 1. asks Google for a device_code + a short human-typable user_code,
7
- * 2. prints a verification URL + the user_code for the human to open on
8
- * ANY device (their phone, a laptop with a browser), and
9
- * 3. polls Google's token endpoint until the human authorizes (or the
10
- * code expires / is denied).
11
- *
12
- * This module is decoupled from the live Google client: `fetch`, `sleep`,
13
- * and `now` are injected, and the client_id / client_secret / endpoints
14
- * come from the caller. The live device flow needs a Google OAuth client
15
- * of type "TVs & Limited Input devices" (a separate GOOGLE_DEVICE_CLIENT_ID
16
- * — Desktop/loopback clients reject /device/code with invalid_client); the
17
- * wiring layer supplies those credentials. Everything here is unit-tested
18
- * against a mocked Google.
19
- */
20
- const GOOGLE_DEVICE_CODE_URL = 'https://oauth2.googleapis.com/device/code';
21
- const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
22
- const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
23
- const DEFAULT_INTERVAL_SECONDS = 5;
24
- const SLOW_DOWN_INCREMENT_SECONDS = 5;
25
- /**
26
- * Failure of the device-grant flow. `code` is Google's OAuth error code
27
- * where one exists (`access_denied`, `expired_token`, `invalid_client`,
28
- * `slow_down`, `authorization_pending`) or a synthetic code for
29
- * transport/shape failures (`device_code_request_failed`,
30
- * `device_token_request_failed`, `malformed_token_response`).
31
- *
32
- * Token material is never placed in the message — only Google's error
33
- * code + description, mirroring RefreshTokenInvalidError's discipline.
34
- */
35
- export class DeviceAuthError extends Error {
36
- code;
37
- constructor(code, message) {
38
- super(message ?? code);
39
- this.code = code;
40
- this.name = 'DeviceAuthError';
41
- }
42
- }
43
- /**
44
- * Step 1 — request a device_code + user_code from Google.
45
- */
46
- export async function requestDeviceCode(config, deps) {
47
- const url = config.deviceCodeUrl ?? GOOGLE_DEVICE_CODE_URL;
48
- let response;
49
- try {
50
- response = await deps.fetch(url, {
51
- method: 'POST',
52
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
53
- body: new URLSearchParams({
54
- client_id: config.clientId,
55
- scope: config.scopes.join(' '),
56
- }),
57
- });
58
- }
59
- catch (err) {
60
- throw new DeviceAuthError('device_code_request_failed', `Could not reach Google device endpoint: ${err?.message ?? 'unknown'}`);
61
- }
62
- if (!response.ok) {
63
- const code = await readErrorCode(response);
64
- throw new DeviceAuthError(code ?? 'device_code_request_failed', `Device-code request failed (HTTP ${response.status}${code ? `, ${code}` : ''})`);
65
- }
66
- let data;
67
- try {
68
- data = (await response.json());
69
- }
70
- catch {
71
- throw new DeviceAuthError('malformed_token_response', 'Device-code response was not JSON');
72
- }
73
- if (!data.device_code || !data.user_code || !data.verification_url) {
74
- throw new DeviceAuthError('malformed_token_response', 'Device-code response missing device_code/user_code/verification_url');
75
- }
76
- return {
77
- device_code: data.device_code,
78
- user_code: data.user_code,
79
- verification_url: data.verification_url,
80
- expires_in: typeof data.expires_in === 'number' ? data.expires_in : 1800,
81
- interval: typeof data.interval === 'number' ? data.interval : DEFAULT_INTERVAL_SECONDS,
82
- };
83
- }
84
- /**
85
- * Step 2 — poll Google's token endpoint until the user authorizes the
86
- * device_code, honoring the RFC 8628 poll semantics.
87
- *
88
- * Sleeps `interval` BEFORE each poll (never hammers immediately). A local
89
- * deadline derived from `expires_in` bounds the loop so a code the user
90
- * abandons can't poll forever even if Google never returns expired_token.
91
- */
92
- export async function pollForDeviceToken(deviceCode, config, deps) {
93
- const tokenUrl = config.tokenUrl ?? GOOGLE_TOKEN_URL;
94
- const now = deps.now ?? Date.now;
95
- const deadline = now() + deviceCode.expires_in * 1000;
96
- let intervalSeconds = deviceCode.interval > 0 ? deviceCode.interval : DEFAULT_INTERVAL_SECONDS;
97
- // eslint-disable-next-line no-constant-condition
98
- while (true) {
99
- if (now() >= deadline) {
100
- throw new DeviceAuthError('expired_token', 'Device code expired before the authorization was completed');
101
- }
102
- await deps.sleep(intervalSeconds * 1000);
103
- let response;
104
- try {
105
- response = await deps.fetch(tokenUrl, {
106
- method: 'POST',
107
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
108
- body: new URLSearchParams({
109
- client_id: config.clientId,
110
- ...(config.clientSecret ? { client_secret: config.clientSecret } : {}),
111
- device_code: deviceCode.device_code,
112
- grant_type: DEVICE_GRANT_TYPE,
113
- }),
114
- });
115
- }
116
- catch (err) {
117
- throw new DeviceAuthError('device_token_request_failed', `Could not reach Google token endpoint: ${err?.message ?? 'unknown'}`);
118
- }
119
- if (response.ok) {
120
- let data;
121
- try {
122
- data = (await response.json());
123
- }
124
- catch {
125
- throw new DeviceAuthError('malformed_token_response', 'Token response was not JSON');
126
- }
127
- if (!data.id_token || typeof data.expires_in !== 'number') {
128
- throw new DeviceAuthError('malformed_token_response', 'Token response missing id_token or expires_in');
129
- }
130
- return {
131
- id_token: data.id_token,
132
- refresh_token: data.refresh_token,
133
- expires_in: data.expires_in,
134
- };
135
- }
136
- const code = await readErrorCode(response);
137
- switch (code) {
138
- case 'authorization_pending':
139
- // The user hasn't finished yet — keep polling at the same interval.
140
- continue;
141
- case 'slow_down':
142
- // Google asks us to back off; RFC 8628 §3.5 → bump the interval.
143
- intervalSeconds += SLOW_DOWN_INCREMENT_SECONDS;
144
- continue;
145
- case 'access_denied':
146
- throw new DeviceAuthError('access_denied', 'Authorization was denied by the user');
147
- case 'expired_token':
148
- throw new DeviceAuthError('expired_token', 'Device code expired before authorization');
149
- default:
150
- throw new DeviceAuthError(code ?? 'device_token_request_failed', `Device token poll failed (HTTP ${response.status}${code ? `, ${code}` : ''})`);
151
- }
152
- }
153
- }
154
- /**
155
- * Extract Google's OAuth `error` code from a non-2xx response body without
156
- * throwing. Returns null when the body isn't JSON or has no error field.
157
- */
158
- async function readErrorCode(response) {
159
- try {
160
- const body = (await response.json());
161
- return typeof body?.error === 'string' ? body.error : null;
162
- }
163
- catch {
164
- return null;
165
- }
166
- }
167
- //# sourceMappingURL=device-auth.js.map
1
+ const u="https://oauth2.googleapis.com/device/code",p="https://oauth2.googleapis.com/token",w="urn:ietf:params:oauth:grant-type:device_code",h=5,f=5;class n extends Error{code;constructor(o,i){super(i??o),this.code=o,this.name="DeviceAuthError"}}async function v(t,o){const i=t.deviceCodeUrl??u;let c;try{c=await o.fetch(i,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:t.clientId,scope:t.scopes.join(" ")})})}catch(s){throw new n("device_code_request_failed",`Could not reach Google device endpoint: ${s?.message??"unknown"}`)}if(!c.ok){const s=await l(c);throw new n(s??"device_code_request_failed",`Device-code request failed (HTTP ${c.status}${s?`, ${s}`:""})`)}let e;try{e=await c.json()}catch{throw new n("malformed_token_response","Device-code response was not JSON")}if(!e.device_code||!e.user_code||!e.verification_url)throw new n("malformed_token_response","Device-code response missing device_code/user_code/verification_url");return{device_code:e.device_code,user_code:e.user_code,verification_url:e.verification_url,expires_in:typeof e.expires_in=="number"?e.expires_in:1800,interval:typeof e.interval=="number"?e.interval:5}}async function E(t,o,i){const c=o.tokenUrl??p,e=i.now??Date.now,s=e()+t.expires_in*1e3;let _=t.interval>0?t.interval:5;for(;;){if(e()>=s)throw new n("expired_token","Device code expired before the authorization was completed");await i.sleep(_*1e3);let a;try{a=await i.fetch(c,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:o.clientId,...o.clientSecret?{client_secret:o.clientSecret}:{},device_code:t.device_code,grant_type:w})})}catch(r){throw new n("device_token_request_failed",`Could not reach Google token endpoint: ${r?.message??"unknown"}`)}if(a.ok){let r;try{r=await a.json()}catch{throw new n("malformed_token_response","Token response was not JSON")}if(!r.id_token||typeof r.expires_in!="number")throw new n("malformed_token_response","Token response missing id_token or expires_in");return{id_token:r.id_token,refresh_token:r.refresh_token,expires_in:r.expires_in}}const d=await l(a);switch(d){case"authorization_pending":continue;case"slow_down":_+=5;continue;case"access_denied":throw new n("access_denied","Authorization was denied by the user");case"expired_token":throw new n("expired_token","Device code expired before authorization");default:throw new n(d??"device_token_request_failed",`Device token poll failed (HTTP ${a.status}${d?`, ${d}`:""})`)}}}async function l(t){try{const o=await t.json();return typeof o?.error=="string"?o.error:null}catch{return null}}export{n as DeviceAuthError,E as pollForDeviceToken,v as requestDeviceCode};
@@ -1,11 +1 @@
1
- export function normalizeDirectLogRecipients(value) {
2
- if (value == null)
3
- return [];
4
- const raw = Array.isArray(value) ? value : [value];
5
- const recipients = raw
6
- .filter((item) => typeof item === 'string')
7
- .map((item) => item.trim())
8
- .filter((item) => item.length > 0);
9
- return [...new Set(recipients)];
10
- }
11
- //# sourceMappingURL=direct-log.js.map
1
+ function e(t){if(t==null)return[];const i=(Array.isArray(t)?t:[t]).filter(r=>typeof r=="string").map(r=>r.trim()).filter(r=>r.length>0);return[...new Set(i)]}export{e as normalizeDirectLogRecipients};
@@ -1,168 +1 @@
1
- /**
2
- * gh#541 WU-2 — client health beat (the Part B "receipt watermark" producer).
3
- *
4
- * The MCP-client child process emits a periodic + event-driven health BEAT to
5
- * the server's `POST /api/drone/health` (WU-1). The beat reports:
6
- * - `last_event_at` — when this client last RECEIVED an inbound cube
7
- * event (the wake-path RECEIPT evidence). This is
8
- * produced BELOW the agent classifier (WU-0 proved
9
- * child-process HTTP is independent of the agent
10
- * tool-call path), so it stays fresh even during a
11
- * classifier outage and even when the agent /loop
12
- * never wakes — which is exactly what lets the
13
- * watchdog (WU-3) tell DEAF (Monitor down) apart
14
- * from POST-BLOCKED.
15
- * - `sse_connected` — the SSE wire state.
16
- * - `inbox_monitor_armed` — whether a tail-Monitor is watching the inbox.
17
- *
18
- * The beat carries NO token material in the body (auth is the X-Drone-Session
19
- * header, same as every other drone endpoint). It is strictly BEST-EFFORT: a
20
- * failed POST is swallowed and must never crash the SSE stream.
21
- *
22
- * Side-effecting deps (fetch, token, cube/status/monitor probes, clock) are
23
- * injected so the producer is unit-tested without real network/keychain/pgrep.
24
- */
25
- // ─── Module state (process-singleton, like the SSE stream state) ─────────
26
- let lastEventReceivedAt = null;
27
- // Cached monitor-health, refreshed by the ~60s tick so per-event beats don't
28
- // have to spawn pgrep on every inbound entry. `null` = unknown (treated as
29
- // armed by the payload mapping below).
30
- let cachedMonitorHealthy = null;
31
- // gh#633: cached transport-agnostic wake-armed (claude monitor OR codex bridge
32
- // liveness), refreshed by the ~60s tick so per-event beats reuse it without
33
- // re-probing. `null` = unknown (treated as armed by the payload mapping below).
34
- let cachedWakeArmed = null;
35
- /** Record that an inbound cube event was just received (the receipt evidence). */
36
- export function recordEventReceipt(now = new Date()) {
37
- lastEventReceivedAt = now;
38
- }
39
- export function getLastEventReceivedAt() {
40
- return lastEventReceivedAt;
41
- }
42
- export function getCachedMonitorHealthy() {
43
- return cachedMonitorHealthy;
44
- }
45
- /** Cached transport-agnostic wake-armed (gh#633), for cheap per-event beats. */
46
- export function getCachedWakeArmed() {
47
- return cachedWakeArmed;
48
- }
49
- /**
50
- * Reset module state. TEST-ONLY — the beat state is a process singleton in
51
- * normal operation; nothing in production code should call this.
52
- * @internal
53
- */
54
- export function __resetHealthBeatStateForTest() {
55
- lastEventReceivedAt = null;
56
- cachedMonitorHealthy = null;
57
- cachedWakeArmed = null;
58
- }
59
- /**
60
- * Build the beat payload from the current receipt watermark + the supplied
61
- * wire/monitor state.
62
- *
63
- * `inbox_monitor_armed` maps the tri-state monitor probe to the boolean the
64
- * server schema requires: ONLY a POSITIVELY-broken probe (`false`) reports
65
- * `false`; both healthy (`true`) and unknown (`null`) report `true`. This
66
- * mirrors `shouldShowWakePathWarning` (which fires only on `=== false`) and is
67
- * the design's false-deaf-avoidance posture — an undeterminable probe must not
68
- * masquerade as a dead Monitor.
69
- */
70
- export function buildHealthPayload(sseConnected, inboxMonitorHealthy, wakeArmed, agentKind, hostname, version) {
71
- return {
72
- sse_connected: sseConnected,
73
- inbox_monitor_armed: inboxMonitorHealthy !== false,
74
- // gh#633: same false-deaf-avoidance map as inbox_monitor_armed — only a
75
- // POSITIVELY-false probe reports false; healthy (true) and indeterminate
76
- // (null) both report armed. Prevents a transient/undeterminable wake-path
77
- // probe from masquerading as a dead wake path.
78
- wake_armed: wakeArmed !== false,
79
- // gh#634: live runtime agent_kind (caller passes resolveSessionAgentKind()).
80
- agent_kind: agentKind,
81
- hostname,
82
- version,
83
- last_event_at: lastEventReceivedAt ? lastEventReceivedAt.toISOString() : null,
84
- };
85
- }
86
- /**
87
- * Best-effort POST of a beat. Swallows ALL errors (token fetch, network,
88
- * non-2xx) — a beat that can't be delivered must never propagate into the
89
- * stream loop. Body carries NO token material; auth is via headers.
90
- */
91
- export async function postHealthBeat(active, payload, deps) {
92
- try {
93
- const token = await deps.getToken();
94
- await deps.fetchImpl(`${active.apiUrl}/api/drone/health`, {
95
- method: 'POST',
96
- headers: {
97
- Authorization: `Bearer ${token}`,
98
- 'X-Drone-Session': active.sessionToken,
99
- 'Content-Type': 'application/json',
100
- },
101
- body: JSON.stringify(payload),
102
- });
103
- }
104
- catch {
105
- // Best-effort: never crash the stream on a beat failure.
106
- }
107
- }
108
- /** Build the current payload and post it (best-effort). */
109
- export async function emitHealthBeat(active, opts) {
110
- const payload = buildHealthPayload(opts.sseConnected, opts.inboxMonitorHealthy, opts.wakeArmed, opts.agentKind, opts.hostname, opts.version);
111
- await postHealthBeat(active, payload, opts);
112
- }
113
- /**
114
- * One tick of the periodic beat: resolve the active cube, probe the live
115
- * wire + Monitor state, cache the monitor result (so cheap per-event beats can
116
- * reuse it without re-spawning pgrep), and emit the beat. No active cube → no
117
- * beat. Best-effort: never throws.
118
- */
119
- export async function runHealthBeatOnce(deps) {
120
- try {
121
- const active = await deps.getActiveCube();
122
- if (!active)
123
- return;
124
- const connected = deps.getStreamConnected();
125
- const healthy = deps.checkMonitor(deps.getInboxPath(active));
126
- cachedMonitorHealthy = healthy;
127
- // gh#633: wake_armed = the running drone's OWN wake-transport health.
128
- // codex (remote-wake) → app-server bridge liveness; claude → the same
129
- // tail-F Monitor health (its wake path IS the Monitor). Transport-agnostic:
130
- // HOP-2 reads one boolean, never branches on (mis-recordable) agent_kind.
131
- const wakeArmed = deps.isCodexRemoteWake()
132
- ? await deps.probeBridgeArmed(active)
133
- : healthy;
134
- cachedWakeArmed = wakeArmed;
135
- // gh#634: live runtime agent_kind, beated every cycle to self-heal the
136
- // recorded column (frozen at first-join; relaunch never re-assimilates).
137
- const agentKind = deps.resolveAgentKind();
138
- const hostname = deps.resolveHostname();
139
- const version = deps.resolveVersion();
140
- await emitHealthBeat(active, {
141
- sseConnected: connected,
142
- inboxMonitorHealthy: healthy,
143
- wakeArmed,
144
- agentKind,
145
- hostname,
146
- version,
147
- fetchImpl: deps.fetchImpl,
148
- getToken: deps.getToken,
149
- });
150
- }
151
- catch {
152
- // Best-effort tick: a probe/beat failure must not kill the interval.
153
- }
154
- }
155
- /** Default periodic cadence for the health beat. */
156
- export const HEALTH_BEAT_INTERVAL_MS = 60_000;
157
- /**
158
- * Start the periodic beat. Fire-and-forget; the timer is `unref`'d so it never
159
- * keeps the process alive on its own. Returns the timer handle (tests clear it).
160
- */
161
- export function startHealthBeatTick(deps, intervalMs = HEALTH_BEAT_INTERVAL_MS) {
162
- const handle = setInterval(() => {
163
- void runHealthBeatOnce(deps);
164
- }, intervalMs);
165
- handle.unref?.();
166
- return handle;
167
- }
168
- //# sourceMappingURL=health-beat.js.map
1
+ let a=null,i=null,l=null;function x(e=new Date){a=e}function y(){return a}function k(){return i}function v(){return l}function A(){a=null,i=null,l=null}function u(e,t,n,o,r,c){return{sse_connected:e,inbox_monitor_armed:t!==!1,wake_armed:n!==!1,agent_kind:o,hostname:r,version:c,last_event_at:a?a.toISOString():null}}async function d(e,t,n){try{const o=await n.getToken();await n.fetchImpl(`${e.apiUrl}/api/drone/health`,{method:"POST",headers:{Authorization:`Bearer ${o}`,"X-Drone-Session":e.sessionToken,"Content-Type":"application/json"},body:JSON.stringify(t)})}catch{}}async function f(e,t){const n=u(t.sseConnected,t.inboxMonitorHealthy,t.wakeArmed,t.agentKind,t.hostname,t.version);await d(e,n,t)}async function m(e){try{const t=await e.getActiveCube();if(!t)return;const n=e.getStreamConnected(),o=e.checkMonitor(e.getInboxPath(t));i=o;const r=e.isCodexRemoteWake()?await e.probeBridgeArmed(t):o;l=r;const c=e.resolveAgentKind(),h=e.resolveHostname(),s=e.resolveVersion();await f(t,{sseConnected:n,inboxMonitorHealthy:o,wakeArmed:r,agentKind:c,hostname:h,version:s,fetchImpl:e.fetchImpl,getToken:e.getToken})}catch{}}const g=6e4;function H(e,t=g){const n=setInterval(()=>{m(e)},t);return n.unref?.(),n}export{g as HEALTH_BEAT_INTERVAL_MS,A as __resetHealthBeatStateForTest,u as buildHealthPayload,f as emitHealthBeat,k as getCachedMonitorHealthy,v as getCachedWakeArmed,y as getLastEventReceivedAt,d as postHealthBeat,x as recordEventReceipt,m as runHealthBeatOnce,H as startHealthBeatTick};
@@ -1,130 +1,2 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * borg-inbox-monitor — per-entry pretty-printer for borgmcp inbox files.
4
- *
5
- * Per gh#8: Claude Code's task-notification title is the Monitor's
6
- * `description`, set once at arm-time. When the Monitor command is
7
- * `tail -F <inbox>`, every event's notification title is the same
8
- * static "Monitor event: ..." string regardless of which drone posted
9
- * what. Recipients have to read the body to triage.
10
- *
11
- * Replacement: tail the inbox file and emit one stdout line per cube
12
- * log entry, summarizing drone label + role + first ~80 chars of the
13
- * message body. Claude Code's Monitor batching then uses that single
14
- * line as the per-event task-notification title.
15
- *
16
- * Inbox file format (per client/src/log-stream.ts formatInboxLine):
17
- * <iso-ts> <drone-label> (<role-name>): <message>
18
- *
19
- * Multi-line messages are appended as a single fs.appendFile() call
20
- * with embedded `\n` characters, so they become multiple physical
21
- * lines in the file. Continuation lines (those that don't start with
22
- * an ISO-8601 timestamp) are dropped — only the first line of each
23
- * entry surfaces, which is the part that summarizes the entry.
24
- *
25
- * Usage:
26
- * borg-inbox-monitor <inbox-file-path>
27
- */
28
- import { spawn } from 'node:child_process';
29
- import { realpathSync } from 'node:fs';
30
- import { createInterface } from 'node:readline';
31
- import { fileURLToPath } from 'node:url';
32
- const ENTRY_LINE_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/;
33
- /**
34
- * Pure: parse one inbox-file line and produce the pretty summary line
35
- * (or null if the line is a continuation or unrecognized shape).
36
- *
37
- * Pass-through — no truncation. Claude Code does not impose a hard cap
38
- * on task-notification title length. The 200-char `MAX_SUMMARY_LEN` cap
39
- * removed here (and the 80-char predecessor) were borg-mcp conventions
40
- * built on a misunderstanding of the renderer's limits. Drones now see
41
- * the full first line of every entry; multi-signal batched posts no
42
- * longer have their second signal hidden.
43
- *
44
- * Exported so tests can exercise the parsing without spawning tail.
45
- */
46
- export function formatEventLine(inboxLine) {
47
- const match = ENTRY_LINE_RE.exec(inboxLine);
48
- if (!match)
49
- return null;
50
- const [, , label, role, body] = match;
51
- const summary = body.trim();
52
- return `${label} (${role}): ${summary}`;
53
- }
54
- function main() {
55
- const inboxPath = process.argv[2];
56
- if (!inboxPath) {
57
- console.error('borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>');
58
- process.exit(2);
59
- }
60
- // `tail -F` for rotation/truncation resilience on macOS + Linux.
61
- // Node's fs.watch is unreliable across file rotation; subprocess
62
- // tail matches the prior kickoff-Monitor shape (`tail -F`) so the
63
- // wire behavior is identical — only the per-line projection changes.
64
- // `-n 0` skips backfilling history so fresh sessions don't replay
65
- // old entries on every restart.
66
- const tail = spawn('tail', ['-F', '-n', '0', inboxPath], {
67
- stdio: ['ignore', 'pipe', 'inherit'],
68
- });
69
- if (!tail.stdout) {
70
- console.error('borg-inbox-monitor: tail subprocess has no stdout');
71
- process.exit(1);
72
- }
73
- const rl = createInterface({ input: tail.stdout, crlfDelay: Infinity });
74
- let shuttingDown = false;
75
- rl.on('line', (line) => {
76
- const pretty = formatEventLine(line);
77
- if (pretty !== null) {
78
- console.log(pretty);
79
- }
80
- });
81
- tail.on('error', (err) => {
82
- console.error(`borg-inbox-monitor: tail failed: ${err.message}`);
83
- process.exit(1);
84
- });
85
- tail.on('exit', (code, signal) => {
86
- if (shuttingDown)
87
- process.exit(0);
88
- if (signal)
89
- process.exit(0);
90
- process.exit(code ?? 0);
91
- });
92
- const shutdown = (signal) => {
93
- if (shuttingDown)
94
- return;
95
- shuttingDown = true;
96
- rl.close();
97
- if (!tail.killed && !tail.kill(signal)) {
98
- process.exit(0);
99
- }
100
- setTimeout(() => process.exit(0), 1000).unref();
101
- };
102
- process.once('SIGTERM', () => shutdown('SIGTERM'));
103
- process.once('SIGINT', () => shutdown('SIGINT'));
104
- }
105
- /**
106
- * Is this module being invoked as the bin entry point?
107
- *
108
- * gh#114: under `npm install`, `process.argv[1]` is the npm-bin symlink
109
- * path while `fileURLToPath(import.meta.url)` is the realpath of the
110
- * installed file. A naive `===` check never matches → `main()` never
111
- * runs → the documented `borg-inbox-monitor` Monitor command silently
112
- * no-ops and drones go deaf without the wake-path-self-heal (gh#43)
113
- * triggering. Resolve the symlink before comparing.
114
- *
115
- * Exported for unit testing.
116
- */
117
- export function isEntryInvocation(argv1, importMetaUrl) {
118
- try {
119
- return realpathSync(argv1) === fileURLToPath(importMetaUrl);
120
- }
121
- catch {
122
- return false;
123
- }
124
- }
125
- // Only run main() when invoked as the bin entry — allow importing the
126
- // pure formatEventLine for unit testing without spawning tail.
127
- if (isEntryInvocation(process.argv[1], import.meta.url)) {
128
- main();
129
- }
130
- //# sourceMappingURL=inbox-monitor.js.map
2
+ import{spawn as c}from"node:child_process";import{realpathSync as l}from"node:fs";import{createInterface as a}from"node:readline";import{fileURLToPath as p}from"node:url";const f=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/;function u(r){const o=f.exec(r);if(!o)return null;const[,,n,e,i]=o,t=i.trim();return`${n} (${e}): ${t}`}function m(){const r=process.argv[2];r||(console.error("borg-inbox-monitor: usage: borg-inbox-monitor <inbox-path>"),process.exit(2));const o=c("tail",["-F","-n","0",r],{stdio:["ignore","pipe","inherit"]});o.stdout||(console.error("borg-inbox-monitor: tail subprocess has no stdout"),process.exit(1));const n=a({input:o.stdout,crlfDelay:1/0});let e=!1;n.on("line",t=>{const s=u(t);s!==null&&console.log(s)}),o.on("error",t=>{console.error(`borg-inbox-monitor: tail failed: ${t.message}`),process.exit(1)}),o.on("exit",(t,s)=>{e&&process.exit(0),s&&process.exit(0),process.exit(t??0)});const i=t=>{e||(e=!0,n.close(),!o.killed&&!o.kill(t)&&process.exit(0),setTimeout(()=>process.exit(0),1e3).unref())};process.once("SIGTERM",()=>i("SIGTERM")),process.once("SIGINT",()=>i("SIGINT"))}function x(r,o){try{return l(r)===p(o)}catch{return!1}}x(process.argv[1],import.meta.url)&&m();export{u as formatEventLine,x as isEntryInvocation};