borgmcp 1.0.5 → 1.0.7

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 (157) hide show
  1. package/dist/assimilate-cmd.js +39 -497
  2. package/dist/assimilate-deps.js +3 -177
  3. package/dist/assimilate-welcome.js +2 -24
  4. package/dist/auth-env.js +1 -107
  5. package/dist/auth.js +23 -612
  6. package/dist/claude.js +11 -281
  7. package/dist/cli-help.js +29 -50
  8. package/dist/cli-platform.js +4 -94
  9. package/dist/codex-app-server.js +4 -228
  10. package/dist/codex-app-wake.js +2 -122
  11. package/dist/codex-launch.js +1 -81
  12. package/dist/codex-remote.js +1 -250
  13. package/dist/config-utils.js +3 -385
  14. package/dist/config.js +1 -190
  15. package/dist/console-prefix.js +1 -86
  16. package/dist/cube-name.js +1 -65
  17. package/dist/cubes.js +4 -269
  18. package/dist/debug.js +1 -71
  19. package/dist/device-auth.js +1 -167
  20. package/dist/direct-log.js +1 -11
  21. package/dist/health-beat.js +1 -168
  22. package/dist/inbox-monitor.js +1 -129
  23. package/dist/index.js +26 -1378
  24. package/dist/lifecycle-log-guard.js +2 -93
  25. package/dist/list-roles-render.js +6 -39
  26. package/dist/log-audit.js +3 -186
  27. package/dist/log-stream.js +9 -848
  28. package/dist/name-validator.js +1 -22
  29. package/dist/parse-assimilate-args.js +1 -82
  30. package/dist/postinstall.js +8 -22
  31. package/dist/regen-format.js +11 -329
  32. package/dist/regen.js +5 -83
  33. package/dist/remote-client.js +1 -695
  34. package/dist/role-resolver.js +1 -36
  35. package/dist/role-section.js +8 -208
  36. package/dist/roster-render.js +3 -96
  37. package/dist/setup.js +36 -251
  38. package/dist/shell-escape.js +1 -22
  39. package/dist/spawn.js +10 -29
  40. package/dist/stale-version-check.js +1 -102
  41. package/dist/stream-owner.js +2 -202
  42. package/dist/stream-status.js +3 -211
  43. package/dist/subscription-retry.js +1 -23
  44. package/dist/sync-roles-render.js +3 -118
  45. package/dist/sync.js +22 -286
  46. package/dist/templates.js +120 -563
  47. package/dist/terminal-title.js +1 -68
  48. package/dist/token-crypto.js +1 -91
  49. package/dist/token-store.js +1 -222
  50. package/dist/types.js +0 -5
  51. package/dist/version.js +2 -78
  52. package/dist/worktree-lifecycle.js +2 -173
  53. package/package.json +11 -2
  54. package/dist/assimilate-cmd.d.ts.map +0 -1
  55. package/dist/assimilate-cmd.js.map +0 -1
  56. package/dist/assimilate-deps.d.ts.map +0 -1
  57. package/dist/assimilate-deps.js.map +0 -1
  58. package/dist/assimilate-welcome.d.ts.map +0 -1
  59. package/dist/assimilate-welcome.js.map +0 -1
  60. package/dist/auth-env.d.ts.map +0 -1
  61. package/dist/auth-env.js.map +0 -1
  62. package/dist/auth.d.ts.map +0 -1
  63. package/dist/auth.js.map +0 -1
  64. package/dist/claude.d.ts.map +0 -1
  65. package/dist/claude.js.map +0 -1
  66. package/dist/cli-help.d.ts.map +0 -1
  67. package/dist/cli-help.js.map +0 -1
  68. package/dist/cli-platform.d.ts.map +0 -1
  69. package/dist/cli-platform.js.map +0 -1
  70. package/dist/codex-app-server.d.ts.map +0 -1
  71. package/dist/codex-app-server.js.map +0 -1
  72. package/dist/codex-app-wake.d.ts.map +0 -1
  73. package/dist/codex-app-wake.js.map +0 -1
  74. package/dist/codex-launch.d.ts.map +0 -1
  75. package/dist/codex-launch.js.map +0 -1
  76. package/dist/codex-remote.d.ts.map +0 -1
  77. package/dist/codex-remote.js.map +0 -1
  78. package/dist/config-utils.d.ts.map +0 -1
  79. package/dist/config-utils.js.map +0 -1
  80. package/dist/config.d.ts.map +0 -1
  81. package/dist/config.js.map +0 -1
  82. package/dist/console-prefix.d.ts.map +0 -1
  83. package/dist/console-prefix.js.map +0 -1
  84. package/dist/cube-name.d.ts.map +0 -1
  85. package/dist/cube-name.js.map +0 -1
  86. package/dist/cubes.d.ts.map +0 -1
  87. package/dist/cubes.js.map +0 -1
  88. package/dist/debug.d.ts.map +0 -1
  89. package/dist/debug.js.map +0 -1
  90. package/dist/device-auth.d.ts.map +0 -1
  91. package/dist/device-auth.js.map +0 -1
  92. package/dist/direct-log.d.ts.map +0 -1
  93. package/dist/direct-log.js.map +0 -1
  94. package/dist/health-beat.d.ts.map +0 -1
  95. package/dist/health-beat.js.map +0 -1
  96. package/dist/inbox-monitor.d.ts.map +0 -1
  97. package/dist/inbox-monitor.js.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.js.map +0 -1
  100. package/dist/lifecycle-log-guard.d.ts.map +0 -1
  101. package/dist/lifecycle-log-guard.js.map +0 -1
  102. package/dist/list-roles-render.d.ts.map +0 -1
  103. package/dist/list-roles-render.js.map +0 -1
  104. package/dist/log-audit.d.ts.map +0 -1
  105. package/dist/log-audit.js.map +0 -1
  106. package/dist/log-stream.d.ts.map +0 -1
  107. package/dist/log-stream.js.map +0 -1
  108. package/dist/name-validator.d.ts.map +0 -1
  109. package/dist/name-validator.js.map +0 -1
  110. package/dist/parse-assimilate-args.d.ts.map +0 -1
  111. package/dist/parse-assimilate-args.js.map +0 -1
  112. package/dist/postinstall.d.ts.map +0 -1
  113. package/dist/postinstall.js.map +0 -1
  114. package/dist/regen-format.d.ts.map +0 -1
  115. package/dist/regen-format.js.map +0 -1
  116. package/dist/regen.d.ts.map +0 -1
  117. package/dist/regen.js.map +0 -1
  118. package/dist/remote-client.d.ts.map +0 -1
  119. package/dist/remote-client.js.map +0 -1
  120. package/dist/role-resolver.d.ts.map +0 -1
  121. package/dist/role-resolver.js.map +0 -1
  122. package/dist/role-section.d.ts.map +0 -1
  123. package/dist/role-section.js.map +0 -1
  124. package/dist/roster-render.d.ts.map +0 -1
  125. package/dist/roster-render.js.map +0 -1
  126. package/dist/setup.d.ts.map +0 -1
  127. package/dist/setup.js.map +0 -1
  128. package/dist/shell-escape.d.ts.map +0 -1
  129. package/dist/shell-escape.js.map +0 -1
  130. package/dist/spawn.d.ts.map +0 -1
  131. package/dist/spawn.js.map +0 -1
  132. package/dist/stale-version-check.d.ts.map +0 -1
  133. package/dist/stale-version-check.js.map +0 -1
  134. package/dist/stream-owner.d.ts.map +0 -1
  135. package/dist/stream-owner.js.map +0 -1
  136. package/dist/stream-status.d.ts.map +0 -1
  137. package/dist/stream-status.js.map +0 -1
  138. package/dist/subscription-retry.d.ts.map +0 -1
  139. package/dist/subscription-retry.js.map +0 -1
  140. package/dist/sync-roles-render.d.ts.map +0 -1
  141. package/dist/sync-roles-render.js.map +0 -1
  142. package/dist/sync.d.ts.map +0 -1
  143. package/dist/sync.js.map +0 -1
  144. package/dist/templates.d.ts.map +0 -1
  145. package/dist/templates.js.map +0 -1
  146. package/dist/terminal-title.d.ts.map +0 -1
  147. package/dist/terminal-title.js.map +0 -1
  148. package/dist/token-crypto.d.ts.map +0 -1
  149. package/dist/token-crypto.js.map +0 -1
  150. package/dist/token-store.d.ts.map +0 -1
  151. package/dist/token-store.js.map +0 -1
  152. package/dist/types.d.ts.map +0 -1
  153. package/dist/types.js.map +0 -1
  154. package/dist/version.d.ts.map +0 -1
  155. package/dist/version.js.map +0 -1
  156. package/dist/worktree-lifecycle.d.ts.map +0 -1
  157. package/dist/worktree-lifecycle.js.map +0 -1
@@ -1,695 +1 @@
1
- /**
2
- * Remote HTTP client for api.borgmcp.ai
3
- *
4
- * Handles:
5
- * - HTTP requests to remote MCP server
6
- * - Automatic token injection
7
- * - Network failure handling with retry + exponential backoff
8
- * - Offline queue for pending operations
9
- */
10
- import { getIdToken, getRefreshToken, clearTokens } from './config.js';
11
- import { refreshIdToken, RefreshTokenInvalidError } from './auth.js';
12
- import { consolePrefix } from './console-prefix.js';
13
- import { debugLog } from './debug.js';
14
- export const API_URL = process.env.BORG_API_URL || 'https://api.borgmcp.ai';
15
- const MAX_RETRIES = 3;
16
- const INITIAL_BACKOFF_MS = 1000; // 1 second
17
- const MAX_BACKOFF_MS = 30000; // 30 seconds
18
- // gh#330: honor the server's Retry-After on 429 instead of failing the
19
- // (often required) coordination signal outright. Bounded so a CLI call
20
- // never blocks unboundedly; capped per attempt so a large window-reset
21
- // retryAfter can't wedge the call.
22
- const RATE_LIMIT_MAX_RETRIES = 3;
23
- const RATE_LIMIT_MAX_WAIT_MS = 60_000; // cap a single Retry-After honor
24
- /**
25
- * Parse a `Retry-After` header (delta-seconds form, which the worker
26
- * emits — mcp-server.ts:382/583) into milliseconds. Returns null when
27
- * absent or not a non-negative integer count of seconds. (The HTTP-date
28
- * form is not emitted by the worker, so it is intentionally unhandled.)
29
- */
30
- export function parseRetryAfterMs(headerValue) {
31
- if (headerValue == null)
32
- return null;
33
- const trimmed = headerValue.trim();
34
- if (!/^\d+$/.test(trimmed))
35
- return null;
36
- return parseInt(trimmed, 10) * 1000;
37
- }
38
- /**
39
- * How long to wait before the next 429 retry. Honors the server's
40
- * Retry-After when present (capped at `capMs` so a full-window reset
41
- * can't wedge a CLI call); falls back to an escalating 1s·(attempt+1)
42
- * when absent. Adds jitter (injected for tests) so co-located sibling
43
- * drones sharing one per-IP bucket don't retry in lockstep.
44
- */
45
- export function rateLimitWaitMs(retryAfterMs, attempt, capMs = RATE_LIMIT_MAX_WAIT_MS, jitter = () => Math.random() * 500) {
46
- const base = retryAfterMs != null ? retryAfterMs : 1000 * (attempt + 1);
47
- return Math.min(base, capMs) + jitter();
48
- }
49
- /**
50
- * Given an ALREADY-OBTAINED response, while it is a 429 and retries
51
- * remain, wait per `rateLimitWaitMs` (honoring the CURRENT response's
52
- * Retry-After) and THEN re-run `doRequest`. Takes `initialResponse`
53
- * (not a first request) because the caller has already made the request
54
- * and read its status — re-fetching first would ignore the first 429's
55
- * Retry-After and double-fire an immediate extra request (CR blocker
56
- * d3a564f5). Returns the last Response (200-class on success, or a final
57
- * 429 if retries exhaust — the caller surfaces that). `sleep` is
58
- * injected for deterministic tests; no fetch-global mocking required.
59
- */
60
- export async function retryOn429(initialResponse, doRequest, opts) {
61
- const maxRetries = opts.maxRetries ?? RATE_LIMIT_MAX_RETRIES;
62
- let response = initialResponse;
63
- let attempt = 0;
64
- while (response.status === 429 && attempt < maxRetries) {
65
- // Honor THIS 429's Retry-After BEFORE issuing the next request.
66
- const waitMs = rateLimitWaitMs(parseRetryAfterMs(response.headers.get('Retry-After')), attempt, opts.capMs, opts.jitter);
67
- opts.log?.(`rate limited (429); retrying in ${Math.round(waitMs)}ms (attempt ${attempt + 1}/${maxRetries})`);
68
- await opts.sleep(waitMs);
69
- attempt++;
70
- response = await doRequest();
71
- }
72
- return response;
73
- }
74
- /**
75
- * Exponential backoff delay
76
- */
77
- function calculateBackoff(retryCount) {
78
- const delay = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, retryCount), MAX_BACKOFF_MS);
79
- // Add jitter to avoid thundering herd
80
- return delay + Math.random() * 1000;
81
- }
82
- /**
83
- * Sleep for specified milliseconds
84
- */
85
- function sleep(ms) {
86
- return new Promise(resolve => setTimeout(resolve, ms));
87
- }
88
- /**
89
- * Get valid auth token (refreshes if expired).
90
- *
91
- * Exported so the SSE log-stream consumer (`client/src/log-stream.ts`)
92
- * can attach the same Bearer header that `authedFetch` uses for REST,
93
- * without duplicating the refresh-token plumbing.
94
- */
95
- export async function getValidToken() {
96
- let token = await getIdToken();
97
- if (!token) {
98
- // Token expired, try to refresh
99
- const refreshToken = await getRefreshToken();
100
- if (refreshToken) {
101
- try {
102
- await refreshIdToken(refreshToken);
103
- token = await getIdToken();
104
- }
105
- catch (error) {
106
- // gh#34: only `clearTokens()` on the canonical revocation
107
- // signal (`RefreshTokenInvalidError`). Transient failures
108
- // (network/DNS/timeout/Google 5xx/parse fail) leave the
109
- // keychain intact so the next call can retry — a single
110
- // transient blip no longer destroys a durable session.
111
- if (error instanceof RefreshTokenInvalidError) {
112
- await clearTokens();
113
- }
114
- // Fall through — token stays null; the throw below surfaces
115
- // the right user-facing message based on whether tokens
116
- // were cleared (Authentication required) or preserved
117
- // (Authentication failed — retry below).
118
- }
119
- }
120
- if (!token) {
121
- throw new Error('Authentication required. Run: borg assimilate');
122
- }
123
- }
124
- return token;
125
- }
126
- /**
127
- * Force-refresh the ID token using the stored refresh token, regardless
128
- * of local expiry timestamp.
129
- *
130
- * Returns the new token on success, or null if no refresh token is stored
131
- * or the refresh failed (in which case stored tokens are cleared so the
132
- * next getValidToken() throws "Authentication required").
133
- *
134
- * Used by the 401-retry path: when the worker rejects a token the client
135
- * thinks is fresh (clock skew, JWKS rotation, transient validation
136
- * glitch), force a refresh and retry once before bothering the user.
137
- */
138
- async function forceRefreshToken() {
139
- const refreshToken = await getRefreshToken();
140
- if (!refreshToken) {
141
- return null;
142
- }
143
- try {
144
- await refreshIdToken(refreshToken);
145
- return await getIdToken();
146
- }
147
- catch (error) {
148
- // gh#34: only `clearTokens()` on the canonical revocation
149
- // signal (`RefreshTokenInvalidError`). Transient failures
150
- // preserve the keychain so subsequent calls (and the next
151
- // session's wake) can retry. The pre-gh#34 shape called
152
- // `clearTokens()` unconditionally — a single network blip
153
- // would destroy the durable session in a way no auto-retry
154
- // could recover from.
155
- if (error instanceof RefreshTokenInvalidError) {
156
- await clearTokens();
157
- }
158
- return null;
159
- }
160
- }
161
- /**
162
- * Call remote MCP tool with retry logic
163
- */
164
- export async function callRemoteTool(toolName, args) {
165
- let lastError = null;
166
- let authRetryUsed = false;
167
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
168
- try {
169
- const token = await getValidToken();
170
- const argsWithAuth = { ...args, auth_token: token };
171
- const response = await fetch(`${API_URL}/mcp`, {
172
- method: 'POST',
173
- headers: { 'Content-Type': 'application/json' },
174
- body: JSON.stringify({
175
- jsonrpc: '2.0',
176
- id: `client-${Date.now()}`,
177
- method: 'tools/call',
178
- params: { name: toolName, arguments: argsWithAuth },
179
- }),
180
- });
181
- // 401 = worker rejected the token (clock skew, JWKS rotation, expired).
182
- // Force a refresh and retry once before giving up.
183
- if (response.status === 401 && !authRetryUsed) {
184
- authRetryUsed = true;
185
- const refreshed = await forceRefreshToken();
186
- if (refreshed) {
187
- continue;
188
- }
189
- throw new Error('Authentication required. Run: borg assimilate');
190
- }
191
- if (!response.ok) {
192
- throw new Error(`HTTP ${response.status}: ${await response.text()}`);
193
- }
194
- const data = (await response.json());
195
- if (data.error) {
196
- // Server-reported auth failure inside a 200 envelope — same recovery
197
- // path as HTTP 401.
198
- const errMsg = (data.error.message || '').toLowerCase();
199
- if ((errMsg.includes('auth') || errMsg.includes('token')) && !authRetryUsed) {
200
- authRetryUsed = true;
201
- const refreshed = await forceRefreshToken();
202
- if (refreshed) {
203
- continue;
204
- }
205
- throw new Error('Authentication required. Run: borg assimilate');
206
- }
207
- throw new Error(data.error.message || 'Remote tool call failed');
208
- }
209
- return { success: true, data: data.result };
210
- }
211
- catch (error) {
212
- lastError = error;
213
- // Stop the retry loop on terminal auth failures (refresh already tried
214
- // above; no point in burning more attempts).
215
- if (error.message?.includes('Authentication required')) {
216
- throw error;
217
- }
218
- if (attempt >= MAX_RETRIES) {
219
- break;
220
- }
221
- const backoff = calculateBackoff(attempt);
222
- console.error(`${consolePrefix()}Retry ${attempt + 1}/${MAX_RETRIES} after ${Math.round(backoff)}ms...`);
223
- await sleep(backoff);
224
- }
225
- }
226
- throw new Error(`Failed after ${MAX_RETRIES} retries: ${lastError?.message}`);
227
- }
228
- /**
229
- * Authenticated fetch helper.
230
- *
231
- * Adds the Bearer token + optional drone-session header, parses errors
232
- * consistently, and surfaces a helpful "run: borg assimilate" message on 401.
233
- *
234
- * Accepts an optional `apiUrl` override so already-assimilated callers can
235
- * route to the worker that issued their drone session token, regardless of
236
- * what BORG_API_URL was set to when this process started.
237
- */
238
- async function authedFetch(path, init = {}) {
239
- let token = await getValidToken();
240
- const { droneSession, apiUrl, headers, ...rest } = init;
241
- const baseUrl = apiUrl ?? API_URL;
242
- const method = (rest.method ?? 'GET').toUpperCase();
243
- const buildRequest = async (tok) => {
244
- const finalHeaders = {
245
- 'Authorization': `Bearer ${tok}`,
246
- ...headers,
247
- };
248
- if (droneSession) {
249
- finalHeaders['X-Drone-Session'] = droneSession;
250
- }
251
- // --debug / BORG_DEBUG: trace every HTTP attempt (initial + 401/429
252
- // retries). Logs method/path/status ONLY — never the Authorization
253
- // header or any token material (debugLog no-ops when debug is off).
254
- debugLog(`→ ${method} ${path}`);
255
- const res = await fetch(`${baseUrl}${path}`, {
256
- ...rest,
257
- headers: finalHeaders,
258
- });
259
- debugLog(`← ${res.status} ${method} ${path}`);
260
- return res;
261
- };
262
- let response = await buildRequest(token);
263
- // 401 on a token getValidToken just handed us means the local expiry
264
- // tracker disagrees with the server. Causes seen in practice: worker
265
- // JWKS cache miss after Google key rotation, clock skew, transient
266
- // validation glitch. Force a refresh and retry once before forcing
267
- // the user back through `borg assimilate`.
268
- if (response.status === 401) {
269
- const refreshed = await forceRefreshToken();
270
- if (refreshed) {
271
- token = refreshed;
272
- response = await buildRequest(token);
273
- }
274
- }
275
- if (response.status === 401) {
276
- throw new Error('Authentication required. Run: borg assimilate');
277
- }
278
- // gh#330: honor the server's Retry-After on 429 instead of failing the
279
- // (often required) coordination signal — borg:log / read-log / regen /
280
- // roster / ack all route through here. Bounded + capped + jittered.
281
- if (response.status === 429) {
282
- response = await retryOn429(response, () => buildRequest(token), {
283
- sleep,
284
- log: (msg) => console.error(`${consolePrefix()}${msg}`),
285
- });
286
- }
287
- if (!response.ok) {
288
- // Read the body ONCE (the stream can only be consumed once) and reuse
289
- // it for both the debug trace and the thrown error. The server error
290
- // body is token-free (it never echoes the Authorization header), so it
291
- // is safe to surface under --debug.
292
- const body = await response.text();
293
- debugLog(`✗ ${response.status} ${method} ${path}: ${body}`);
294
- // Enrich the 429 message with the server's retry guidance so a
295
- // still-exhausted limit surfaces an actionable wait, not a bare code.
296
- if (response.status === 429) {
297
- const retryAfter = response.headers.get('Retry-After');
298
- const hint = retryAfter ? ` (retry after ${retryAfter}s)` : '';
299
- throw new Error(`HTTP 429: rate limited${hint}: ${body}`);
300
- }
301
- throw new Error(`HTTP ${response.status}: ${body}`);
302
- }
303
- return response;
304
- }
305
- /**
306
- * Connect this client as a Drone to a Cube.
307
- *
308
- * Returns the cube definition, the drone's assigned role (with full
309
- * detailed_description), the drone record, and an opaque session token
310
- * the caller is expected to persist via cubes.ts.
311
- */
312
- export async function assimilate(cubeNameOrSelector, apiUrl, hostname, agentKind) {
313
- // String first arg → legacy cube_name-only path (backwards compat).
314
- // Object first arg → orchestrator path with optional cube_id /
315
- // role_id / role_name; assimilate-cmd uses this shape.
316
- const body = { hostname: hostname ?? null };
317
- if (agentKind === 'claude' || agentKind === 'codex')
318
- body.agent_kind = agentKind;
319
- if (typeof cubeNameOrSelector === 'string') {
320
- body.cube_name = cubeNameOrSelector;
321
- }
322
- else {
323
- if (cubeNameOrSelector.cube_id)
324
- body.cube_id = cubeNameOrSelector.cube_id;
325
- if (cubeNameOrSelector.cube_name)
326
- body.cube_name = cubeNameOrSelector.cube_name;
327
- if (cubeNameOrSelector.role_id)
328
- body.role_id = cubeNameOrSelector.role_id;
329
- if (cubeNameOrSelector.role_name)
330
- body.role_name = cubeNameOrSelector.role_name;
331
- }
332
- const response = await authedFetch('/api/assimilate', {
333
- method: 'POST',
334
- headers: { 'Content-Type': 'application/json' },
335
- body: JSON.stringify(body),
336
- apiUrl,
337
- });
338
- return await response.json();
339
- }
340
- /**
341
- * Get the active cube's directive + role registry.
342
- */
343
- export async function getCubeInfo(sessionToken, apiUrl) {
344
- const response = await authedFetch('/api/drone/cube', {
345
- method: 'GET',
346
- droneSession: sessionToken,
347
- apiUrl,
348
- });
349
- return await response.json();
350
- }
351
- /**
352
- * Get this drone's assigned role (with detailed_description).
353
- */
354
- export async function getRoleInfo(sessionToken, apiUrl) {
355
- const response = await authedFetch('/api/drone/role', {
356
- method: 'GET',
357
- droneSession: sessionToken,
358
- apiUrl,
359
- });
360
- return await response.json();
361
- }
362
- export async function whoami(sessionToken, apiUrl) {
363
- const response = await authedFetch('/api/drone/whoami', {
364
- method: 'GET',
365
- droneSession: sessionToken,
366
- apiUrl,
367
- });
368
- return await response.json();
369
- }
370
- /**
371
- * List all currently-connected drones in this cube.
372
- *
373
- * Optional `since` is the T2.1 sender-side liveness probe — pass either
374
- * an activity_log entry id (UUID; server resolves to its `created_at`)
375
- * OR an ISO-8601 timestamp. When provided, the response includes:
376
- * - per-drone `seen_since: boolean` — true iff that drone's
377
- * `last_seen` is strictly after the resolved timestamp
378
- * - top-level `since: ISO-string | null` — the resolved timestamp
379
- * (echoed back so the renderer can label the column accurately
380
- * even when the caller passed an entry-id)
381
- */
382
- export async function getRoster(sessionToken, apiUrl, since) {
383
- const qs = since ? `?since=${encodeURIComponent(since)}` : '';
384
- const response = await authedFetch(`/api/drone/roster${qs}`, {
385
- method: 'GET',
386
- droneSession: sessionToken,
387
- apiUrl,
388
- });
389
- return await response.json();
390
- }
391
- /**
392
- * Read recent log entries for the cube.
393
- */
394
- export async function readLog(sessionToken, apiUrl, opts = {}) {
395
- const params = new URLSearchParams();
396
- if (opts.since)
397
- params.set('since', opts.since);
398
- if (opts.limit !== undefined)
399
- params.set('limit', String(opts.limit));
400
- if (opts.unreadOnly)
401
- params.set('unread_only', 'true');
402
- const qs = params.toString();
403
- const response = await authedFetch(`/api/drone/log${qs ? `?${qs}` : ''}`, {
404
- method: 'GET',
405
- droneSession: sessionToken,
406
- apiUrl,
407
- });
408
- return await response.json();
409
- }
410
- /**
411
- * Sprint 25 log substrate refactor: explicit ack on a log entry.
412
- *
413
- * Replaces in-band `ACK: <dispatch-id>` log entries with a DB-backed
414
- * flag on activity_log_acks. Idempotent — the server INSERT uses ON
415
- * CONFLICT DO NOTHING. 204 No Content on success.
416
- */
417
- export async function ackLogEntry(sessionToken, apiUrl, entryId) {
418
- await authedFetch(`/api/drone/log/${entryId}/ack`, {
419
- method: 'POST',
420
- body: JSON.stringify({ kind: 'ack' }),
421
- droneSession: sessionToken,
422
- apiUrl,
423
- });
424
- }
425
- /**
426
- * Regen: one-shot composite of everything a drone needs to be oriented.
427
- *
428
- * Returns the active cube's directive, the drone's own role with full
429
- * detailed_description, the public role registry (no detailed_description
430
- * leakage for OTHER roles), the drone roster, and recent log entries.
431
- * Use on session start and before each new task to stay in sync.
432
- *
433
- * gh#29 Sprint C / Q3a: optional `since` cursor (entry-id UUID or
434
- * ISO-8601 timestamp) trims the embedded recentLog to entries strictly
435
- * after the anchor — closes the regen-overflow class when drones track
436
- * their last-seen entry across iterations.
437
- */
438
- export async function regen(sessionToken, apiUrl, opts = {}) {
439
- const params = new URLSearchParams();
440
- if (opts.since)
441
- params.set('since', opts.since);
442
- const qs = params.toString();
443
- const response = await authedFetch(`/api/drone/regen${qs ? `?${qs}` : ''}`, {
444
- method: 'GET',
445
- droneSession: sessionToken,
446
- apiUrl,
447
- });
448
- return await response.json();
449
- }
450
- export async function roleRationale(sessionToken, apiUrl, role, section) {
451
- const params = new URLSearchParams({ role, section });
452
- const response = await authedFetch(`/api/drone/role-rationale?${params.toString()}`, {
453
- method: 'GET',
454
- droneSession: sessionToken,
455
- apiUrl,
456
- });
457
- return await response.json();
458
- }
459
- /**
460
- * Append a message to the cube's shared activity log.
461
- */
462
- export async function appendLog(sessionToken, apiUrl, message, opts = {}) {
463
- const body = {
464
- message,
465
- ...(opts.visibility ? { visibility: opts.visibility } : {}),
466
- ...(opts.recipientDroneIds ? { recipientDroneIds: opts.recipientDroneIds } : {}),
467
- ...(opts.class ? { class: opts.class } : {}),
468
- ...(opts.to ? { to: opts.to } : {}),
469
- };
470
- const response = await authedFetch('/api/drone/log', {
471
- method: 'POST',
472
- headers: { 'Content-Type': 'application/json' },
473
- droneSession: sessionToken,
474
- apiUrl,
475
- body: JSON.stringify(body),
476
- });
477
- return await response.json();
478
- }
479
- /**
480
- * List all cubes owned by the authenticated user. Owner-scoped via the
481
- * Bearer token alone; no drone session needed.
482
- */
483
- export async function listCubes() {
484
- const response = await authedFetch('/api/cubes', { method: 'GET' });
485
- return await response.json();
486
- }
487
- /**
488
- * List bundled cube templates. Used by the `borg assimilate` orchestrator
489
- * to surface the interactive template prompt on first-drone bootstrap.
490
- */
491
- export async function listTemplates() {
492
- const response = await authedFetch('/api/templates', { method: 'GET' });
493
- return await response.json();
494
- }
495
- /**
496
- * Create a new cube. Server-side seeds a default "Drone" role atomically
497
- * so the cube is assimilatable immediately, OR applies the named template
498
- * atomically when `opts.template` is set (single-withUserId transaction —
499
- * skips the auto-Drone insert to avoid is_default partial-index conflict).
500
- *
501
- * Returns `{ cube, roles }` — the roles array lets the assimilate
502
- * orchestrator pick a default role without a follow-up `getCube` call.
503
- * Existing callers that read `body.cube` keep working (forward-compat).
504
- */
505
- export async function createCube(name, cubeDirective, opts) {
506
- const body = { cube_directive: cubeDirective };
507
- if (name)
508
- body.name = name;
509
- if (opts?.template)
510
- body.template = opts.template;
511
- if (opts && Object.prototype.hasOwnProperty.call(opts, 'message_taxonomy')) {
512
- body.message_taxonomy = opts.message_taxonomy ?? null;
513
- }
514
- const response = await authedFetch('/api/cubes', {
515
- method: 'POST',
516
- headers: { 'Content-Type': 'application/json' },
517
- body: JSON.stringify(body),
518
- });
519
- // BUG-2 fix (v0.9.2, drone-1 dispatch 2026-05-18T10:48Z): server
520
- // returns `{ cube, roles }` (Phase B wire shape). Unwrap at this
521
- // boundary so callers receive a flat shape `{ id, name, ...cube,
522
- // roles, drones }` consistent with the orchestrator's CubeDetail
523
- // expectation. The `body.cube ?` ternary preserves backwards-compat
524
- // for any future endpoint that might return a non-wrapped shape.
525
- const responseBody = await response.json();
526
- return responseBody.cube
527
- ? { ...responseBody.cube, roles: responseBody.roles ?? [], drones: responseBody.drones ?? [] }
528
- : responseBody;
529
- }
530
- /**
531
- * Update a cube's name and/or cube_directive. Both fields are optional;
532
- * pass only what changes.
533
- */
534
- export async function updateCube(cubeId, updates) {
535
- const response = await authedFetch(`/api/cubes/${cubeId}`, {
536
- method: 'PATCH',
537
- headers: { 'Content-Type': 'application/json' },
538
- body: JSON.stringify(updates),
539
- });
540
- return await response.json();
541
- }
542
- /**
543
- * gh#473 PR1 — granular per-class taxonomy patch. Add / replace-by-name
544
- * / remove a single class within the cube's message_taxonomy, leaving
545
- * other classes unchanged. The worker re-validates the FULL resulting
546
- * array (cross-class invariants) before persist. Owner-scoped via the
547
- * Bearer token.
548
- */
549
- export async function patchTaxonomyClass(cubeId, op) {
550
- const response = await authedFetch(`/api/cubes/${cubeId}/taxonomy-patch`, {
551
- method: 'POST',
552
- headers: { 'Content-Type': 'application/json' },
553
- body: JSON.stringify(op),
554
- });
555
- return await response.json();
556
- }
557
- /**
558
- * Delete a cube. Cascade-deletes all roles, drones, and log entries.
559
- * Owner-scoped via the Bearer token; the worker enforces ownership.
560
- */
561
- export async function deleteCube(cubeId) {
562
- await authedFetch(`/api/cubes/${cubeId}`, { method: 'DELETE' });
563
- }
564
- /**
565
- * Create a role inside a cube. is_default=true demotes the previous
566
- * default role; the cube always has exactly one default.
567
- */
568
- export async function createRole(cubeId, data) {
569
- const response = await authedFetch(`/api/cubes/${cubeId}/roles`, {
570
- method: 'POST',
571
- headers: { 'Content-Type': 'application/json' },
572
- body: JSON.stringify(data),
573
- });
574
- return await response.json();
575
- }
576
- /**
577
- * Update a role. All fields optional; pass only what changes.
578
- */
579
- export async function updateRole(roleId, updates) {
580
- const response = await authedFetch(`/api/roles/${roleId}`, {
581
- method: 'PATCH',
582
- headers: { 'Content-Type': 'application/json' },
583
- body: JSON.stringify(updates),
584
- });
585
- return await response.json();
586
- }
587
- /**
588
- * gh#473 PR1 — granular role-text section patch. Replace / insert /
589
- * delete a single named section of a role's detailed_description,
590
- * leaving the rest of the field byte-identical. Owner-scoped via the
591
- * Bearer token. Sections are delimited by plain-label lines (e.g.
592
- * `Workflow:`), NOT markdown headings.
593
- */
594
- export async function patchRoleSection(roleId, op) {
595
- const response = await authedFetch(`/api/roles/${roleId}/section-patch`, {
596
- method: 'POST',
597
- headers: { 'Content-Type': 'application/json' },
598
- body: JSON.stringify(op),
599
- });
600
- return await response.json();
601
- }
602
- /**
603
- * Delete a role. Worker refuses if any drone is still assigned to it
604
- * (reassign or evict those drones first).
605
- */
606
- export async function deleteRole(roleId) {
607
- await authedFetch(`/api/roles/${roleId}`, { method: 'DELETE' });
608
- }
609
- /**
610
- * Reassign a drone to a different role within the same cube.
611
- * Queen-class seat cardinality is enforced server-side — attempting
612
- * to assign to a queen-class role when another drone already holds
613
- * the seat returns an error. The class-hierarchy guard also rejects
614
- * direct promotion from non-human-seat roles.
615
- */
616
- export async function reassignDrone(droneId, roleId) {
617
- const response = await authedFetch(`/api/drones/${droneId}`, {
618
- method: 'PATCH',
619
- headers: { 'Content-Type': 'application/json' },
620
- body: JSON.stringify({ role_id: roleId }),
621
- });
622
- return await response.json();
623
- }
624
- /**
625
- * Fetch a cube's full detail: directive, roles (with detailed
626
- * descriptions, owner-only), and drones. Owner-scoped via the Bearer
627
- * token; no drone session needed.
628
- */
629
- export async function getCube(cubeId) {
630
- const response = await authedFetch(`/api/cubes/${cubeId}`, { method: 'GET' });
631
- // BUG-2 fix (v0.9.2): same unwrap pattern as createCube — server
632
- // returns `{ cube, roles, drones }`; callers get flat shape.
633
- const responseBody = await response.json();
634
- return responseBody.cube
635
- ? { ...responseBody.cube, roles: responseBody.roles ?? [], drones: responseBody.drones ?? [] }
636
- : responseBody;
637
- }
638
- /**
639
- * gh#473 PR2 — apply a named template to an existing cube via the
640
- * NON-CLOBBERING server route. New roles are inserted; existing
641
- * template-named roles get ADD fragments auto-applied (template
642
- * sections/classes the cube lacks) but their EVOLVED (conflicting)
643
- * fragments are surfaced server-side and KEPT, never overwritten. Returns
644
- * `{ created, updated }` counts. To selectively take template versions of
645
- * conflicting fragments, use `syncRoles` with a `decisions` map instead.
646
- */
647
- export async function applyTemplate(cubeId, templateName) {
648
- const response = await authedFetch(`/api/cubes/${cubeId}/apply-template`, {
649
- method: 'POST',
650
- headers: { 'Content-Type': 'application/json' },
651
- body: JSON.stringify({ template_name: templateName }),
652
- });
653
- return await response.json();
654
- }
655
- /**
656
- * Check subscription status
657
- */
658
- export async function checkSubscriptionStatus() {
659
- const response = await authedFetch('/api/subscription/status', { method: 'GET' });
660
- return await response.json();
661
- }
662
- /**
663
- * gh#473 PR2 — NON-CLOBBERING sync of a cube's roles + message_taxonomy
664
- * against the current built-in template. Dry-run by default classifies
665
- * each fragment (role-text SECTION / short_description / flags / taxonomy
666
- * CLASS) as ADD / UNCHANGED / CONFLICT. Pass apply=true to commit:
667
- * ADD fragments auto-apply (zero clobber risk); CONFLICT fragments apply
668
- * ONLY when their stable key appears in `decisions` as 'accept'.
669
- * Unspecified conflicts DEFAULT TO REJECT — the cube's evolved text is
670
- * never silently overwritten. Custom roles (names not in template) are
671
- * never touched. Returns a NonClobberSyncResult.
672
- */
673
- export async function syncRoles(cubeId, templateName = 'software-dev', apply = false, decisions) {
674
- const response = await authedFetch(`/api/cubes/${cubeId}/sync-roles`, {
675
- method: 'POST',
676
- headers: { 'Content-Type': 'application/json' },
677
- body: JSON.stringify({ template_name: templateName, apply, ...(decisions ? { decisions } : {}) }),
678
- });
679
- return await response.json();
680
- }
681
- /**
682
- * Create subscription (returns checkout URL)
683
- */
684
- export async function createSubscription() {
685
- const response = await authedFetch('/api/subscribe', {
686
- method: 'POST',
687
- headers: { 'Content-Type': 'application/json' },
688
- });
689
- const data = (await response.json());
690
- if (!data.checkout_url) {
691
- throw new Error('No checkout URL in response');
692
- }
693
- return data.checkout_url;
694
- }
695
- //# sourceMappingURL=remote-client.js.map
1
+ import{getIdToken as y,getRefreshToken as b,clearTokens as g}from"./config.js";import{refreshIdToken as $,RefreshTokenInvalidError as x}from"./auth.js";import{consolePrefix as S}from"./console-prefix.js";import{debugLog as m}from"./debug.js";const _=process.env.BORG_API_URL||"https://api.borgmcp.ai",w=3,E=1e3,k=3e4,C=3,A=6e4;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=A,o=()=>Math.random()*500){const s=e??1e3*(n+1);return Math.min(s,t)+o()}async function O(e,n,t){const o=t.maxRetries??C;let s=e,a=0;for(;s.status===429&&a<o;){const c=I(P(s.headers.get("Retry-After")),a,t.capMs,t.jitter);t.log?.(`rate limited (429); retrying in ${Math.round(c)}ms (attempt ${a+1}/${o})`),await t.sleep(c),a++,s=await n()}return s}function M(e){return Math.min(E*Math.pow(2,e),k)+Math.random()*1e3}function j(e){return new Promise(n=>setTimeout(n,e))}async function R(){let e=await y();if(!e){const n=await b();if(n)try{await $(n),e=await y()}catch(t){t instanceof x&&await g()}if(!e)throw new Error("Authentication required. Run: borg assimilate")}return e}async function T(){const e=await b();if(!e)return null;try{return await $(e),await y()}catch(n){return n instanceof x&&await g(),null}}async function q(e,n){let t=null,o=!1;for(let s=0;s<=w;s++)try{const a=await R(),c={...n,auth_token:a},d=await fetch(`${_}/mcp`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`client-${Date.now()}`,method:"tools/call",params:{name:e,arguments:c}})});if(d.status===401&&!o){if(o=!0,await T())continue;throw new Error("Authentication required. Run: borg assimilate")}if(!d.ok)throw new Error(`HTTP ${d.status}: ${await d.text()}`);const u=await d.json();if(u.error){const l=(u.error.message||"").toLowerCase();if((l.includes("auth")||l.includes("token"))&&!o){if(o=!0,await T())continue;throw new Error("Authentication required. Run: borg assimilate")}throw new Error(u.error.message||"Remote tool call failed")}return{success:!0,data:u.result}}catch(a){if(t=a,a.message?.includes("Authentication required"))throw a;if(s>=w)break;const c=M(s);console.error(`${S()}Retry ${s+1}/${w} after ${Math.round(c)}ms...`),await j(c)}throw new Error(`Failed after ${w} retries: ${t?.message}`)}async function r(e,n={}){let t=await R();const{droneSession:o,apiUrl:s,headers:a,...c}=n,d=s??_,u=(c.method??"GET").toUpperCase(),l=async p=>{const f={Authorization:`Bearer ${p}`,...a};o&&(f["X-Drone-Session"]=o),m(`\u2192 ${u} ${e}`);const h=await fetch(`${d}${e}`,{...c,headers:f});return m(`\u2190 ${h.status} ${u} ${e}`),h};let i=await l(t);if(i.status===401){const p=await T();p&&(t=p,i=await l(t))}if(i.status===401)throw new Error("Authentication required. Run: borg assimilate");if(i.status===429&&(i=await O(i,()=>l(t),{sleep:j,log:p=>console.error(`${S()}${p}`)})),!i.ok){const p=await i.text();if(m(`\u2717 ${i.status} ${u} ${e}: ${p}`),i.status===429){const f=i.headers.get("Retry-After"),h=f?` (retry after ${f}s)`:"";throw new Error(`HTTP 429: rate limited${h}: ${p}`)}throw new Error(`HTTP ${i.status}: ${p}`)}return i}async function D(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)),await(await r("/api/assimilate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s),apiUrl:n})).json()}async function v(e,n){return await(await r("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:n})).json()}async function B(e,n){return await(await r("/api/drone/role",{method:"GET",droneSession:e,apiUrl:n})).json()}async function H(e,n){return await(await r("/api/drone/whoami",{method:"GET",droneSession:e,apiUrl:n})).json()}async function F(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 N(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 X(e,n,t){await r(`/api/drone/log/${t}/ack`,{method:"POST",body:JSON.stringify({kind:"ack"}),droneSession:e,apiUrl:n})}async function W(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 z(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 K(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 Q(){return await(await r("/api/cubes",{method:"GET"})).json()}async function V(){return await(await r("/api/templates",{method:"GET"})).json()}async function Y(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 Z(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 await(await r(`/api/drones/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role_id:n})})).json()}async function ie(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 ce(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 pe(){return await(await r("/api/subscription/status",{method:"GET"})).json()}async function ue(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 de(){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}export{_ as API_URL,X as ackLogEntry,K as appendLog,ce as applyTemplate,D as assimilate,q as callRemoteTool,pe as checkSubscriptionStatus,Y as createCube,ne as createRole,de as createSubscription,te as deleteCube,re as deleteRole,ie as getCube,v as getCubeInfo,B as getRoleInfo,F as getRoster,R as getValidToken,Q as listCubes,V as listTemplates,P as parseRetryAfterMs,se as patchRoleSection,ee as patchTaxonomyClass,I as rateLimitWaitMs,N as readLog,ae as reassignDrone,W as regen,O as retryOn429,z as roleRationale,ue as syncRoles,Z as updateCube,oe as updateRole,H as whoami};