borgmcp 1.0.6 → 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.
- package/dist/assimilate-cmd.js +39 -511
- package/dist/assimilate-deps.js +3 -177
- package/dist/assimilate-welcome.js +2 -24
- package/dist/auth-env.js +1 -107
- package/dist/auth.js +23 -612
- package/dist/claude.js +11 -281
- package/dist/cli-help.js +29 -50
- package/dist/cli-platform.js +4 -94
- package/dist/codex-app-server.js +4 -228
- package/dist/codex-app-wake.js +2 -122
- package/dist/codex-launch.js +1 -81
- package/dist/codex-remote.js +1 -250
- package/dist/config-utils.js +3 -385
- package/dist/config.js +1 -190
- package/dist/console-prefix.js +1 -86
- package/dist/cube-name.js +1 -65
- package/dist/cubes.js +4 -269
- package/dist/debug.js +1 -71
- package/dist/device-auth.js +1 -167
- package/dist/direct-log.js +1 -11
- package/dist/health-beat.js +1 -168
- package/dist/inbox-monitor.js +1 -129
- package/dist/index.js +26 -1378
- package/dist/lifecycle-log-guard.js +2 -93
- package/dist/list-roles-render.js +6 -39
- package/dist/log-audit.js +3 -186
- package/dist/log-stream.js +9 -848
- package/dist/name-validator.js +1 -22
- package/dist/parse-assimilate-args.js +1 -82
- package/dist/postinstall.js +8 -22
- package/dist/regen-format.js +11 -337
- package/dist/regen.js +5 -83
- package/dist/remote-client.js +1 -695
- package/dist/role-resolver.js +1 -36
- package/dist/role-section.js +8 -208
- package/dist/roster-render.js +3 -96
- package/dist/setup.js +36 -251
- package/dist/shell-escape.js +1 -22
- package/dist/spawn.js +10 -29
- package/dist/stale-version-check.js +1 -102
- package/dist/stream-owner.js +2 -202
- package/dist/stream-status.js +3 -211
- package/dist/subscription-retry.js +1 -23
- package/dist/sync-roles-render.js +3 -118
- package/dist/sync.js +22 -286
- package/dist/templates.js +120 -626
- package/dist/terminal-title.js +1 -68
- package/dist/token-crypto.js +1 -91
- package/dist/token-store.js +1 -222
- package/dist/types.js +0 -5
- package/dist/version.js +2 -78
- package/dist/worktree-lifecycle.js +2 -173
- package/package.json +11 -2
- package/dist/assimilate-cmd.d.ts.map +0 -1
- package/dist/assimilate-cmd.js.map +0 -1
- package/dist/assimilate-deps.d.ts.map +0 -1
- package/dist/assimilate-deps.js.map +0 -1
- package/dist/assimilate-welcome.d.ts.map +0 -1
- package/dist/assimilate-welcome.js.map +0 -1
- package/dist/auth-env.d.ts.map +0 -1
- package/dist/auth-env.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/claude.d.ts.map +0 -1
- package/dist/claude.js.map +0 -1
- package/dist/cli-help.d.ts.map +0 -1
- package/dist/cli-help.js.map +0 -1
- package/dist/cli-platform.d.ts.map +0 -1
- package/dist/cli-platform.js.map +0 -1
- package/dist/codex-app-server.d.ts.map +0 -1
- package/dist/codex-app-server.js.map +0 -1
- package/dist/codex-app-wake.d.ts.map +0 -1
- package/dist/codex-app-wake.js.map +0 -1
- package/dist/codex-launch.d.ts.map +0 -1
- package/dist/codex-launch.js.map +0 -1
- package/dist/codex-remote.d.ts.map +0 -1
- package/dist/codex-remote.js.map +0 -1
- package/dist/config-utils.d.ts.map +0 -1
- package/dist/config-utils.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/console-prefix.d.ts.map +0 -1
- package/dist/console-prefix.js.map +0 -1
- package/dist/cube-name.d.ts.map +0 -1
- package/dist/cube-name.js.map +0 -1
- package/dist/cubes.d.ts.map +0 -1
- package/dist/cubes.js.map +0 -1
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js.map +0 -1
- package/dist/device-auth.d.ts.map +0 -1
- package/dist/device-auth.js.map +0 -1
- package/dist/direct-log.d.ts.map +0 -1
- package/dist/direct-log.js.map +0 -1
- package/dist/health-beat.d.ts.map +0 -1
- package/dist/health-beat.js.map +0 -1
- package/dist/inbox-monitor.d.ts.map +0 -1
- package/dist/inbox-monitor.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle-log-guard.d.ts.map +0 -1
- package/dist/lifecycle-log-guard.js.map +0 -1
- package/dist/list-roles-render.d.ts.map +0 -1
- package/dist/list-roles-render.js.map +0 -1
- package/dist/log-audit.d.ts.map +0 -1
- package/dist/log-audit.js.map +0 -1
- package/dist/log-stream.d.ts.map +0 -1
- package/dist/log-stream.js.map +0 -1
- package/dist/name-validator.d.ts.map +0 -1
- package/dist/name-validator.js.map +0 -1
- package/dist/parse-assimilate-args.d.ts.map +0 -1
- package/dist/parse-assimilate-args.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/regen-format.d.ts.map +0 -1
- package/dist/regen-format.js.map +0 -1
- package/dist/regen.d.ts.map +0 -1
- package/dist/regen.js.map +0 -1
- package/dist/remote-client.d.ts.map +0 -1
- package/dist/remote-client.js.map +0 -1
- package/dist/role-resolver.d.ts.map +0 -1
- package/dist/role-resolver.js.map +0 -1
- package/dist/role-section.d.ts.map +0 -1
- package/dist/role-section.js.map +0 -1
- package/dist/roster-render.d.ts.map +0 -1
- package/dist/roster-render.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/shell-escape.d.ts.map +0 -1
- package/dist/shell-escape.js.map +0 -1
- package/dist/spawn.d.ts.map +0 -1
- package/dist/spawn.js.map +0 -1
- package/dist/stale-version-check.d.ts.map +0 -1
- package/dist/stale-version-check.js.map +0 -1
- package/dist/stream-owner.d.ts.map +0 -1
- package/dist/stream-owner.js.map +0 -1
- package/dist/stream-status.d.ts.map +0 -1
- package/dist/stream-status.js.map +0 -1
- package/dist/subscription-retry.d.ts.map +0 -1
- package/dist/subscription-retry.js.map +0 -1
- package/dist/sync-roles-render.d.ts.map +0 -1
- package/dist/sync-roles-render.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js.map +0 -1
- package/dist/terminal-title.d.ts.map +0 -1
- package/dist/terminal-title.js.map +0 -1
- package/dist/token-crypto.d.ts.map +0 -1
- package/dist/token-crypto.js.map +0 -1
- package/dist/token-store.d.ts.map +0 -1
- package/dist/token-store.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/worktree-lifecycle.d.ts.map +0 -1
- package/dist/worktree-lifecycle.js.map +0 -1
package/dist/remote-client.js
CHANGED
|
@@ -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};
|