github-router 0.3.121 → 0.3.126

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.
@@ -40,6 +40,7 @@ const state = {
40
40
  showToken: false,
41
41
  extendedBetas: false,
42
42
  browseEnabled: false,
43
+ fleetEnabled: false,
43
44
  powerBrowseEnabled: false,
44
45
  humanlikeForce: "auto",
45
46
  sessionId: randomUUID(),
@@ -306,12 +307,12 @@ async function fetchWithTransientRetry(doFetch, opts = {}) {
306
307
  await res.body.cancel();
307
308
  } catch {}
308
309
  const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
309
- const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
310
+ const delay$1 = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
310
311
  if (label) {
311
312
  const why = res ? `HTTP ${res.status}` : caught?.name ?? "error";
312
- consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay}ms`);
313
+ consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} failed (${why}); retrying in ${delay$1}ms`);
313
314
  }
314
- await abortableSleep(delay, signal);
315
+ await abortableSleep(delay$1, signal);
315
316
  }
316
317
  }
317
318
  /** Extract an HTTP status from a thrown error (HTTPError carries
@@ -352,9 +353,9 @@ async function withTransientRetry(fn, opts = {}) {
352
353
  if (!(status !== void 0 && retryStatuses.includes(status) || isTransientNetworkError(err)) || attempt >= attempts) throw err;
353
354
  const retryAfterMs = parseRetryAfter(err?.response?.headers?.get?.("retry-after") ?? null);
354
355
  const expCap = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
355
- const delay = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
356
- if (label) consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} threw (${status !== void 0 ? `HTTP ${status}` : err?.name ?? "error"}); retrying in ${delay}ms`);
357
- await abortableSleep(delay, signal);
356
+ const delay$1 = Math.min(maxDelayMs, retryAfterMs ?? Math.round(Math.random() * expCap));
357
+ if (label) consola.debug(`[upstream-retry] ${label}: attempt ${attempt}/${attempts} threw (${status !== void 0 ? `HTTP ${status}` : err?.name ?? "error"}); retrying in ${delay$1}ms`);
358
+ await abortableSleep(delay$1, signal);
358
359
  }
359
360
  }
360
361
  }
@@ -1045,6 +1046,1886 @@ function collapsePathKeys(env) {
1045
1046
  return env;
1046
1047
  }
1047
1048
 
1049
+ //#endregion
1050
+ //#region src/lib/artifact/client.ts
1051
+ var ArtifactError = class extends Error {
1052
+ code;
1053
+ retryable;
1054
+ status;
1055
+ detail;
1056
+ constructor(args) {
1057
+ super(args.message);
1058
+ this.name = "ArtifactError";
1059
+ this.code = args.code;
1060
+ this.retryable = args.retryable;
1061
+ this.status = args.status;
1062
+ this.detail = args.detail;
1063
+ }
1064
+ };
1065
+ var ArtifactClient = class {
1066
+ baseUrl;
1067
+ token;
1068
+ sessionId;
1069
+ fetchFn;
1070
+ constructor(options) {
1071
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
1072
+ this.token = options.token;
1073
+ this.sessionId = options.sessionId;
1074
+ this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
1075
+ }
1076
+ open(file, signal) {
1077
+ return this.request("POST", `/api/artifact/${encodeURIComponent(this.sessionId)}/open`, { file }, signal);
1078
+ }
1079
+ poll(timeoutMsHint, signal) {
1080
+ return this.request("GET", `/api/artifact/${encodeURIComponent(this.sessionId)}/poll`, void 0, signal, timeoutMsHint);
1081
+ }
1082
+ agentReply(text, signal) {
1083
+ return this.request("POST", `/api/artifact/${encodeURIComponent(this.sessionId)}/agent-reply`, { text }, signal, void 0, true);
1084
+ }
1085
+ async request(method, pathname, body, signal, timeoutMsHint, allowEmptyJson = false) {
1086
+ let url;
1087
+ try {
1088
+ url = new URL(pathname, `${this.baseUrl}/`);
1089
+ } catch (err) {
1090
+ throw new ArtifactError({
1091
+ code: "UNREACHABLE",
1092
+ message: "artifact API base URL is invalid",
1093
+ retryable: false,
1094
+ detail: err
1095
+ });
1096
+ }
1097
+ const timeout = combineSignalAndTimeout(signal, timeoutMsHint);
1098
+ let response;
1099
+ try {
1100
+ response = await this.fetchFn(url.toString(), {
1101
+ method,
1102
+ headers: {
1103
+ Authorization: `Bearer ${this.token}`,
1104
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
1105
+ },
1106
+ body: body === void 0 ? void 0 : JSON.stringify(body),
1107
+ redirect: "error",
1108
+ signal: timeout.signal
1109
+ });
1110
+ } catch (err) {
1111
+ throw mapNetworkError$1(err);
1112
+ } finally {
1113
+ timeout.cleanup();
1114
+ }
1115
+ if (!response.ok) throw await mapHttpError$1(response);
1116
+ const text = await response.text().catch((err) => {
1117
+ throw new ArtifactError({
1118
+ code: "INVALID_RESPONSE",
1119
+ message: "artifact API response body could not be read",
1120
+ retryable: false,
1121
+ detail: err
1122
+ });
1123
+ });
1124
+ if (!text && allowEmptyJson) return {};
1125
+ try {
1126
+ return JSON.parse(text);
1127
+ } catch (err) {
1128
+ throw new ArtifactError({
1129
+ code: "INVALID_RESPONSE",
1130
+ message: "artifact API returned a non-JSON response",
1131
+ retryable: false,
1132
+ detail: err
1133
+ });
1134
+ }
1135
+ }
1136
+ };
1137
+ function combineSignalAndTimeout(signal, timeoutMsHint) {
1138
+ const timeoutMs = typeof timeoutMsHint === "number" && Number.isFinite(timeoutMsHint) && timeoutMsHint > 0 ? timeoutMsHint : void 0;
1139
+ if (timeoutMs === void 0) return {
1140
+ signal,
1141
+ cleanup: () => {}
1142
+ };
1143
+ const controller = new AbortController();
1144
+ const abortFromCaller = () => {
1145
+ try {
1146
+ controller.abort(signal?.reason);
1147
+ } catch {
1148
+ controller.abort();
1149
+ }
1150
+ };
1151
+ if (signal?.aborted) abortFromCaller();
1152
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
1153
+ const timer = setTimeout(() => {
1154
+ try {
1155
+ controller.abort(new DOMException("artifact API request timed out", "TimeoutError"));
1156
+ } catch {
1157
+ controller.abort();
1158
+ }
1159
+ }, timeoutMs);
1160
+ return {
1161
+ signal: controller.signal,
1162
+ cleanup: () => {
1163
+ clearTimeout(timer);
1164
+ signal?.removeEventListener("abort", abortFromCaller);
1165
+ }
1166
+ };
1167
+ }
1168
+ async function mapHttpError$1(response) {
1169
+ const detail = await readErrorDetail$1(response);
1170
+ const upstreamMessage = detailToMessage$1(detail);
1171
+ const suffix = upstreamMessage ? `: ${upstreamMessage}` : "";
1172
+ if (response.status === 401 || response.status === 403) return new ArtifactError({
1173
+ code: "AUTH_FAILED",
1174
+ message: `artifact API authentication failed (${response.status})${suffix}`,
1175
+ retryable: false,
1176
+ status: response.status,
1177
+ detail
1178
+ });
1179
+ if (response.status === 404) return new ArtifactError({
1180
+ code: "NOT_FOUND",
1181
+ message: `artifact session or resource not found (404)${suffix}`,
1182
+ retryable: false,
1183
+ status: response.status,
1184
+ detail
1185
+ });
1186
+ if (response.status === 408 || response.status === 504) return new ArtifactError({
1187
+ code: "TIMEOUT",
1188
+ message: `artifact API request timed out (${response.status})${suffix}`,
1189
+ retryable: true,
1190
+ status: response.status,
1191
+ detail
1192
+ });
1193
+ return new ArtifactError({
1194
+ code: "UPSTREAM_ERROR",
1195
+ message: `artifact API returned HTTP ${response.status}${suffix}`,
1196
+ retryable: response.status === 429 || response.status >= 500,
1197
+ status: response.status,
1198
+ detail
1199
+ });
1200
+ }
1201
+ function mapNetworkError$1(err) {
1202
+ if (isAbortLike$2(err)) return new ArtifactError({
1203
+ code: "TIMEOUT",
1204
+ message: "artifact API request timed out or was aborted",
1205
+ retryable: true,
1206
+ detail: err
1207
+ });
1208
+ return new ArtifactError({
1209
+ code: "UNREACHABLE",
1210
+ message: `artifact API unreachable: ${err instanceof Error ? err.message : String(err)}`,
1211
+ retryable: true,
1212
+ detail: err
1213
+ });
1214
+ }
1215
+ async function readErrorDetail$1(response) {
1216
+ const text = await response.text().catch(() => "");
1217
+ if (!text) return void 0;
1218
+ try {
1219
+ return JSON.parse(text);
1220
+ } catch {
1221
+ return text;
1222
+ }
1223
+ }
1224
+ function detailToMessage$1(detail) {
1225
+ if (typeof detail === "string") return detail;
1226
+ if (typeof detail !== "object" || detail === null) return void 0;
1227
+ const record = detail;
1228
+ const error = record.error;
1229
+ if (typeof error === "string") return error;
1230
+ if (typeof error === "object" && error !== null) {
1231
+ const errorRecord = error;
1232
+ if (typeof errorRecord.message === "string") return errorRecord.message;
1233
+ if (typeof errorRecord.code === "string") return errorRecord.code;
1234
+ }
1235
+ if (typeof record.message === "string") return record.message;
1236
+ }
1237
+ function isAbortLike$2(err) {
1238
+ return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
1239
+ }
1240
+
1241
+ //#endregion
1242
+ //#region src/lib/artifact/tools.ts
1243
+ const ARTIFACT_GROUP = "peers";
1244
+ const ARTIFACT_POLL_TOOL_BUDGET_MS = 5e4;
1245
+ const ARTIFACT_SINGLE_POLL_TIMEOUT_MS = 25e3;
1246
+ const ARTIFACT_POLL_RETURN_MARGIN_MS = 1e3;
1247
+ const ARTIFACT_MAX_POLLS_PER_TOOL_CALL = 2;
1248
+ function tool(toolNameHttp, description, inputSchema, handler) {
1249
+ return {
1250
+ toolNameHttp,
1251
+ group: ARTIFACT_GROUP,
1252
+ capability: "artifact",
1253
+ description,
1254
+ inputSchema,
1255
+ async handler(args, signal) {
1256
+ try {
1257
+ return await handler(args, signal);
1258
+ } catch (err) {
1259
+ return errorResult$1(err);
1260
+ }
1261
+ }
1262
+ };
1263
+ }
1264
+ const ARTIFACT_TOOLS = Object.freeze([
1265
+ tool("artifact_open", "Open a workspace file in ai-or-die's Artifact review panel for human review. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({ file: stringProp$1("Workspace-relative or absolute file path to show in the Artifact panel.") }, ["file"]), async (args, signal) => {
1266
+ const env = readArtifactEnv();
1267
+ if (!env) return missingEnvResult();
1268
+ const file = requiredString$1(args, "file");
1269
+ return ok$1({
1270
+ viewUrl: (await clientFromEnv(env).open(file, signal)).viewUrl,
1271
+ next_step: "Tell the user to review at the Artifact panel, then call artifact_poll."
1272
+ });
1273
+ }),
1274
+ tool("artifact_poll", "Wait for human Artifact review feedback from ai-or-die and return the prompts/layout warnings/DOM snapshot for the agent to act on. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({}, []), async (_args, signal) => {
1275
+ const env = readArtifactEnv();
1276
+ if (!env) return missingEnvResult();
1277
+ return ok$1(formatPollResponse(await pollUntilReady(clientFromEnv(env), signal)));
1278
+ }),
1279
+ tool("artifact_reply", "Send the agent's reply back to the ai-or-die Artifact review panel after applying or responding to human feedback. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$1({ text: stringProp$1("Agent reply text to deliver to the human Artifact review panel.") }, ["text"]), async (args, signal) => {
1280
+ const env = readArtifactEnv();
1281
+ if (!env) return missingEnvResult();
1282
+ const text = requiredString$1(args, "text");
1283
+ return ok$1({
1284
+ ok: true,
1285
+ ...await clientFromEnv(env).agentReply(text, signal),
1286
+ next_step: "Wait for further human review, or continue if the review loop is complete."
1287
+ });
1288
+ })
1289
+ ]);
1290
+ function readArtifactEnv() {
1291
+ const baseUrl = process.env.AIORDIE_BASE_URL;
1292
+ const token = process.env.AIORDIE_TOKEN;
1293
+ const sessionId = process.env.AIORDIE_SESSION_ID;
1294
+ if (!baseUrl || !token || !sessionId) return void 0;
1295
+ return {
1296
+ baseUrl,
1297
+ token,
1298
+ sessionId
1299
+ };
1300
+ }
1301
+ function clientFromEnv(env) {
1302
+ return new ArtifactClient(env);
1303
+ }
1304
+ async function pollUntilReady(client, signal) {
1305
+ const deadline = Date.now() + ARTIFACT_POLL_TOOL_BUDGET_MS;
1306
+ let last;
1307
+ let attempts = 0;
1308
+ while (!signal?.aborted && attempts < ARTIFACT_MAX_POLLS_PER_TOOL_CALL) {
1309
+ attempts += 1;
1310
+ const remaining = deadline - Date.now();
1311
+ if (remaining <= ARTIFACT_POLL_RETURN_MARGIN_MS) break;
1312
+ const timeoutMsHint = Math.min(ARTIFACT_SINGLE_POLL_TIMEOUT_MS, Math.max(1, remaining - ARTIFACT_POLL_RETURN_MARGIN_MS));
1313
+ last = await client.poll(timeoutMsHint, signal);
1314
+ if (!isWaitingPoll(last)) return last;
1315
+ if (deadline - Date.now() <= ARTIFACT_POLL_RETURN_MARGIN_MS) break;
1316
+ }
1317
+ return {
1318
+ ...last ?? { status: "waiting" },
1319
+ status: "waiting",
1320
+ next_step: "No human feedback is ready yet. Call artifact_poll again."
1321
+ };
1322
+ }
1323
+ function isWaitingPoll(response) {
1324
+ if (hasFeedback(response.prompts)) return false;
1325
+ const status = response.status.toLowerCase();
1326
+ return status === "waiting" || status === "pending" || status === "open" || status === "idle" || status === "timeout" || status === "no_feedback";
1327
+ }
1328
+ function hasFeedback(prompts) {
1329
+ if (Array.isArray(prompts)) return prompts.length > 0;
1330
+ if (typeof prompts === "string") return prompts.trim() !== "";
1331
+ if (typeof prompts === "object" && prompts !== null) return Object.keys(prompts).length > 0;
1332
+ return prompts !== void 0 && prompts !== null;
1333
+ }
1334
+ function formatPollResponse(response) {
1335
+ return definedObject$1({
1336
+ status: response.status,
1337
+ prompts: response.prompts,
1338
+ layout_warnings: response.layout_warnings,
1339
+ dom_snapshot: response.dom_snapshot,
1340
+ next_step: response.next_step ?? defaultPollNextStep(response.status)
1341
+ });
1342
+ }
1343
+ function defaultPollNextStep(status) {
1344
+ return isWaitingStatus(status) ? "No human feedback is ready yet. Call artifact_poll again." : "Apply the human Artifact review feedback, then call artifact_reply with a concise summary.";
1345
+ }
1346
+ function isWaitingStatus(status) {
1347
+ const normalized = status.toLowerCase();
1348
+ return normalized === "waiting" || normalized === "pending" || normalized === "open" || normalized === "idle" || normalized === "timeout" || normalized === "no_feedback";
1349
+ }
1350
+ function requiredString$1(args, key) {
1351
+ const value = args[key];
1352
+ if (typeof value !== "string" || value.trim() === "") throw new ArtifactToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
1353
+ return value;
1354
+ }
1355
+ var ArtifactToolInputError = class extends Error {
1356
+ code;
1357
+ constructor(code, message) {
1358
+ super(message);
1359
+ this.name = "ArtifactToolInputError";
1360
+ this.code = code;
1361
+ }
1362
+ };
1363
+ function missingEnvResult() {
1364
+ return jsonResult$1({ error: {
1365
+ code: "NOT_IN_AIORDIE_TAB",
1366
+ message: "artifact tools only work inside an ai-or-die tab-backed Claude session. Missing AIORDIE_BASE_URL, AIORDIE_TOKEN, or AIORDIE_SESSION_ID."
1367
+ } }, true);
1368
+ }
1369
+ function ok$1(value) {
1370
+ return jsonResult$1(value, false);
1371
+ }
1372
+ function jsonResult$1(value, isError) {
1373
+ return {
1374
+ content: [{
1375
+ type: "text",
1376
+ text: JSON.stringify(value)
1377
+ }],
1378
+ ...isError ? { isError: true } : {}
1379
+ };
1380
+ }
1381
+ function errorResult$1(err) {
1382
+ if (err instanceof ArtifactError) return jsonResult$1({ error: definedObject$1({
1383
+ code: err.code,
1384
+ message: err.message,
1385
+ retryable: err.retryable,
1386
+ status: err.status
1387
+ }) }, true);
1388
+ return jsonResult$1({ error: {
1389
+ code: errorCode$1(err),
1390
+ message: err instanceof Error ? err.message : String(err)
1391
+ } }, true);
1392
+ }
1393
+ function errorCode$1(err) {
1394
+ if (typeof err === "object" && err !== null && "code" in err) {
1395
+ const code = err.code;
1396
+ if (typeof code === "string") return code;
1397
+ }
1398
+ return "ARTIFACT_ERROR";
1399
+ }
1400
+ function definedObject$1(input) {
1401
+ const result = {};
1402
+ for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
1403
+ return result;
1404
+ }
1405
+ function objectSchema$1(properties, required) {
1406
+ return {
1407
+ type: "object",
1408
+ required,
1409
+ additionalProperties: false,
1410
+ properties
1411
+ };
1412
+ }
1413
+ function stringProp$1(description) {
1414
+ return {
1415
+ type: "string",
1416
+ description
1417
+ };
1418
+ }
1419
+
1420
+ //#endregion
1421
+ //#region src/lib/fleet/tunnel-auth.ts
1422
+ var TunnelAuthError = class extends Error {
1423
+ code;
1424
+ constructor(code, message) {
1425
+ super(message);
1426
+ this.name = "TunnelAuthError";
1427
+ this.code = code;
1428
+ }
1429
+ };
1430
+ const REFRESH_MARGIN_MS = 5 * 6e4;
1431
+ const MIN_REMINT_INTERVAL_MS = 3e4;
1432
+ const DEVTUNNEL_TIMEOUT_MS = 1e4;
1433
+ const MINT_FAILURE_BACKOFF_MS = 3e4;
1434
+ const MAX_PLAUSIBLE_TTL_MS = 2880 * 6e4;
1435
+ const MAX_STDOUT_BYTES$2 = 256 * 1024;
1436
+ const TUNNEL_ID_RE$1 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1437
+ const JWT_REDACT_RE = /eyJ[A-Za-z0-9._-]{20,}/g;
1438
+ const SCHEME_TOKEN_RE = /(bearer|tunnel) +[!-~]+/gi;
1439
+ /** Strip credential-shaped substrings from any string before it is logged or surfaced. */
1440
+ function redactTunnelSecrets(s) {
1441
+ return s.replace(JWT_REDACT_RE, "<redacted-token>").replace(SCHEME_TOKEN_RE, "$1 <redacted-token>");
1442
+ }
1443
+ function safeRealpath(p) {
1444
+ try {
1445
+ return realpathSync(p);
1446
+ } catch {
1447
+ return nodePath.resolve(p);
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Guard the resolved `devtunnel` path: it must be a trusted ABSOLUTE path that
1452
+ * is not the current working directory's own binary. `resolveExecutable` already
1453
+ * excludes cwd; this is defense-in-depth against a cwd-local / relative
1454
+ * resolution ever reaching a child-process spawn. Both paths are canonicalized
1455
+ * (realpath, resolving `..` and symlinks) before the cwd-containment check, so a
1456
+ * non-canonical path like `/safe/../cwd/devtunnel` or a symlink cannot evade it.
1457
+ * Returns the (original) path to spawn, or throws.
1458
+ */
1459
+ function assertTrustedDevtunnelPath(resolved, cwd = typeof process.cwd === "function" ? nodePath.resolve(process.cwd()) : null) {
1460
+ if (!resolved) throw new TunnelAuthError("NOT_INSTALLED", "the devtunnel CLI was not found on PATH; install it and run `devtunnel user login` on this (control-plane) machine");
1461
+ if (!nodePath.isAbsolute(resolved)) throw new TunnelAuthError("NOT_INSTALLED", "refusing to run a non-absolute devtunnel binary");
1462
+ const ext = nodePath.extname(resolved).toLowerCase();
1463
+ if (ext === ".cmd" || ext === ".bat" || ext === ".ps1") throw new TunnelAuthError("NOT_INSTALLED", "resolved devtunnel is a script shim (.cmd/.bat/.ps1); github-router runs the native devtunnel(.exe) — ensure the native binary precedes any shim on PATH");
1464
+ const realResolved = safeRealpath(resolved);
1465
+ const realCwd = cwd ? safeRealpath(cwd) : null;
1466
+ if (realCwd && (realResolved === realCwd || realResolved.startsWith(realCwd + nodePath.sep))) throw new TunnelAuthError("NOT_INSTALLED", "refusing to run a cwd-local devtunnel binary");
1467
+ return resolved;
1468
+ }
1469
+ /**
1470
+ * The real runner: resolve `devtunnel` to a trusted absolute path (PATH-resolved,
1471
+ * cwd-excluded) and run it with `shell:false` (native binary).
1472
+ */
1473
+ function realDevtunnelRunner() {
1474
+ return async (args) => {
1475
+ const res = await runManagedExeCapture(assertTrustedDevtunnelPath(resolveExecutable("devtunnel")), args, {
1476
+ timeoutMs: DEVTUNNEL_TIMEOUT_MS,
1477
+ maxStdoutBytes: MAX_STDOUT_BYTES$2
1478
+ });
1479
+ return {
1480
+ stdout: res.stdout,
1481
+ stderr: res.stderr,
1482
+ code: res.code,
1483
+ timedOut: res.timedOut
1484
+ };
1485
+ };
1486
+ }
1487
+ function looksLikeJwt(s) {
1488
+ const parts = s.split(".");
1489
+ if (parts.length !== 3) return false;
1490
+ return parts.every((p) => p.length > 0 && /^[A-Za-z0-9_-]+$/.test(p));
1491
+ }
1492
+ /** Recursively collect JWT-shaped strings from arbitrary parsed JSON. */
1493
+ function collectJwts(value, out) {
1494
+ if (typeof value === "string") {
1495
+ if (value.startsWith("eyJ") && looksLikeJwt(value)) out.add(value);
1496
+ return;
1497
+ }
1498
+ if (Array.isArray(value)) {
1499
+ for (const v of value) collectJwts(v, out);
1500
+ return;
1501
+ }
1502
+ if (value && typeof value === "object") for (const v of Object.values(value)) collectJwts(v, out);
1503
+ }
1504
+ /**
1505
+ * Extract the single access token from `devtunnel token --json` output. Prefers
1506
+ * structured JSON; falls back to a token-shaped scan. Refuses to guess when zero
1507
+ * or more-than-one distinct tokens are present (so we never send a wrong JWT).
1508
+ */
1509
+ function extractToken(stdout) {
1510
+ const found = /* @__PURE__ */ new Set();
1511
+ try {
1512
+ collectJwts(JSON.parse(stdout), found);
1513
+ } catch {}
1514
+ if (found.size === 0) {
1515
+ for (const tok of stdout.split(/[^A-Za-z0-9._-]+/)) if (tok.startsWith("eyJ") && looksLikeJwt(tok)) found.add(tok);
1516
+ }
1517
+ if (found.size === 0) throw new TunnelAuthError("PARSE", "no tunnel access token found in devtunnel output");
1518
+ if (found.size > 1) throw new TunnelAuthError("PARSE", "devtunnel output contained more than one token; refusing to guess");
1519
+ return [...found][0];
1520
+ }
1521
+ /** Parse a JWT `exp` claim (seconds) into epoch milliseconds. Throws on a missing/non-numeric exp. */
1522
+ function parseJwtExpMs(jwt) {
1523
+ const parts = jwt.split(".");
1524
+ if (parts.length !== 3) throw new TunnelAuthError("PARSE", "tunnel token is not a JWT");
1525
+ let payload;
1526
+ try {
1527
+ payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1528
+ } catch {
1529
+ throw new TunnelAuthError("PARSE", "tunnel token payload was not decodable");
1530
+ }
1531
+ const exp = payload?.exp;
1532
+ if (typeof exp !== "number" || !Number.isFinite(exp)) throw new TunnelAuthError("PARSE", "tunnel token has no numeric exp claim");
1533
+ return exp * 1e3;
1534
+ }
1535
+ function classifyMintFailure(res) {
1536
+ if (res.timedOut) return new TunnelAuthError("TIMEOUT", "devtunnel token request timed out");
1537
+ const stderr = (res.stderr || "").toLowerCase();
1538
+ const tail = redactTunnelSecrets((res.stderr || "").trim()).slice(-300);
1539
+ const suffix = tail ? ` [${tail}]` : "";
1540
+ if (/log ?in|sign ?in|not authenticated|unauthor|401/.test(stderr)) return new TunnelAuthError("NOT_LOGGED_IN", `devtunnel is not logged in (or lacks access to this tunnel) on the control-plane machine; run \`devtunnel user login\`${suffix}`);
1541
+ if (/not found|404|does not exist|no such tunnel/.test(stderr)) return new TunnelAuthError("TUNNEL_NOT_FOUND", `devtunnel could not find the tunnel; verify tunnelId with \`devtunnel list\`${suffix}`);
1542
+ return new TunnelAuthError("MINT_FAILED", `devtunnel token failed (exit ${res.code})${suffix}`);
1543
+ }
1544
+ /**
1545
+ * Create a per-process token provider: lazy mint, per-tunnel in-memory cache,
1546
+ * single-flight, and short negative backoff on non-timeout failures.
1547
+ */
1548
+ function createTunnelTokenProvider(runner = realDevtunnelRunner()) {
1549
+ const cache = /* @__PURE__ */ new Map();
1550
+ const inflight = /* @__PURE__ */ new Map();
1551
+ const backoff = /* @__PURE__ */ new Map();
1552
+ async function mint(cfg) {
1553
+ if (!TUNNEL_ID_RE$1.test(cfg.tunnelId)) throw new TunnelAuthError("MINT_FAILED", "invalid tunnelId; must match a devtunnel tunnel name");
1554
+ const args = [
1555
+ "token",
1556
+ cfg.tunnelId,
1557
+ "--scopes",
1558
+ "connect",
1559
+ "--json"
1560
+ ];
1561
+ let res;
1562
+ try {
1563
+ res = await runner(args);
1564
+ } catch (err) {
1565
+ if (err instanceof TunnelAuthError) throw err;
1566
+ throw new TunnelAuthError("MINT_FAILED", redactTunnelSecrets(err instanceof Error ? err.message : String(err)));
1567
+ }
1568
+ if (res.timedOut) throw new TunnelAuthError("TIMEOUT", "devtunnel token request timed out");
1569
+ if (res.code !== 0) throw classifyMintFailure(res);
1570
+ const token = extractToken(res.stdout);
1571
+ const expMs = parseJwtExpMs(token);
1572
+ const now = Date.now();
1573
+ if (expMs <= now) throw new TunnelAuthError("PARSE", "devtunnel minted an already-expired token");
1574
+ if (expMs - now > MAX_PLAUSIBLE_TTL_MS) throw new TunnelAuthError("PARSE", "devtunnel token TTL is implausibly long; refusing");
1575
+ const existing = cache.get(cfg.tunnelId);
1576
+ if (!existing || expMs > existing.expMs) cache.set(cfg.tunnelId, {
1577
+ token,
1578
+ expMs,
1579
+ mintedAt: now
1580
+ });
1581
+ return cache.get(cfg.tunnelId).token;
1582
+ }
1583
+ function mintOnce(cfg) {
1584
+ const key = cfg.tunnelId;
1585
+ return (async () => {
1586
+ try {
1587
+ const token = await mint(cfg);
1588
+ backoff.delete(key);
1589
+ return token;
1590
+ } catch (err) {
1591
+ const e = err instanceof TunnelAuthError ? err : new TunnelAuthError("MINT_FAILED", redactTunnelSecrets(String(err)));
1592
+ if (e.code !== "TIMEOUT") backoff.set(key, {
1593
+ until: Date.now() + MINT_FAILURE_BACKOFF_MS,
1594
+ err: e
1595
+ });
1596
+ const c = cache.get(key);
1597
+ if (c && c.expMs > Date.now()) return c.token;
1598
+ throw e;
1599
+ } finally {
1600
+ inflight.delete(key);
1601
+ }
1602
+ })();
1603
+ }
1604
+ return {
1605
+ async getToken(cfg) {
1606
+ const key = cfg.tunnelId;
1607
+ const now = Date.now();
1608
+ const cached$1 = cache.get(key);
1609
+ if (cached$1 && cached$1.expMs > now) {
1610
+ const comfortablyFresh = cached$1.expMs - now > REFRESH_MARGIN_MS;
1611
+ const recentlyMinted = now - cached$1.mintedAt < MIN_REMINT_INTERVAL_MS;
1612
+ if (comfortablyFresh || recentlyMinted) return cached$1.token;
1613
+ }
1614
+ const inf = inflight.get(key);
1615
+ if (inf) return inf;
1616
+ const bo = backoff.get(key);
1617
+ if (bo && now < bo.until) {
1618
+ if (cached$1 && cached$1.expMs > now) return cached$1.token;
1619
+ throw bo.err;
1620
+ }
1621
+ const p = mintOnce(cfg);
1622
+ inflight.set(key, p);
1623
+ return p;
1624
+ },
1625
+ invalidate(cfg) {
1626
+ cache.delete(cfg.tunnelId);
1627
+ backoff.delete(cfg.tunnelId);
1628
+ }
1629
+ };
1630
+ }
1631
+
1632
+ //#endregion
1633
+ //#region src/lib/fleet/client.ts
1634
+ var FleetError = class extends Error {
1635
+ code;
1636
+ retryable;
1637
+ status;
1638
+ detail;
1639
+ constructor(args) {
1640
+ super(args.message);
1641
+ this.name = "FleetError";
1642
+ this.code = args.code;
1643
+ this.retryable = args.retryable;
1644
+ this.status = args.status;
1645
+ this.detail = args.detail;
1646
+ }
1647
+ };
1648
+ function encodeSessionId(instanceId, localId) {
1649
+ return `${instanceId}:${localId}`;
1650
+ }
1651
+ function decodeSessionId(globalId) {
1652
+ const idx = globalId.indexOf(":");
1653
+ if (idx <= 0 || idx === globalId.length - 1) throw new FleetError({
1654
+ code: "SESSION_NOT_FOUND",
1655
+ message: `invalid fleet sessionId ${JSON.stringify(globalId)}; expected "instanceId:localSessionId"`,
1656
+ retryable: false
1657
+ });
1658
+ return {
1659
+ instanceId: globalId.slice(0, idx),
1660
+ localId: globalId.slice(idx + 1)
1661
+ };
1662
+ }
1663
+ var FleetClient = class {
1664
+ baseUrl;
1665
+ origin;
1666
+ token;
1667
+ fetchFn;
1668
+ getTunnelToken;
1669
+ onTunnelAuthInvalidate;
1670
+ constructor(options) {
1671
+ this.baseUrl = options.url.replace(/\/+$/, "");
1672
+ this.origin = new URL(this.baseUrl).origin;
1673
+ this.token = options.token;
1674
+ this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
1675
+ this.getTunnelToken = options.getTunnelToken;
1676
+ this.onTunnelAuthInvalidate = options.onTunnelAuthInvalidate;
1677
+ }
1678
+ capabilities(signal) {
1679
+ return this.request("GET", "/api/control/capabilities", void 0, void 0, signal);
1680
+ }
1681
+ listSessions(signal) {
1682
+ return this.request("GET", "/api/control/sessions", void 0, void 0, signal);
1683
+ }
1684
+ status(sessionId, signal) {
1685
+ return this.request("GET", `/api/control/sessions/${encodeURIComponent(sessionId)}/status`, void 0, void 0, signal);
1686
+ }
1687
+ readSession(sessionId, lines, signal) {
1688
+ return this.request("GET", `/api/control/sessions/${encodeURIComponent(sessionId)}/read`, lines === void 0 ? void 0 : { lines: String(lines) }, void 0, signal);
1689
+ }
1690
+ createSession(input, signal) {
1691
+ return this.request("POST", "/api/control/sessions/create", void 0, input, signal);
1692
+ }
1693
+ stopSession(sessionId, modeOrInput, idempotencyKeyOrSignal, signal) {
1694
+ const body = {};
1695
+ let requestSignal;
1696
+ if (typeof modeOrInput === "object" && modeOrInput !== null) {
1697
+ if (modeOrInput.mode !== void 0) body.mode = modeOrInput.mode;
1698
+ if (modeOrInput.idempotencyKey !== void 0) body.idempotencyKey = modeOrInput.idempotencyKey;
1699
+ requestSignal = typeof idempotencyKeyOrSignal === "string" ? signal : idempotencyKeyOrSignal;
1700
+ } else {
1701
+ if (modeOrInput !== void 0) body.mode = modeOrInput;
1702
+ if (typeof idempotencyKeyOrSignal === "string") {
1703
+ body.idempotencyKey = idempotencyKeyOrSignal;
1704
+ requestSignal = signal;
1705
+ } else requestSignal = idempotencyKeyOrSignal;
1706
+ }
1707
+ return this.request("POST", `/api/control/sessions/${encodeURIComponent(sessionId)}/stop`, void 0, body, requestSignal);
1708
+ }
1709
+ sendMessage(sessionId, input, signal) {
1710
+ return this.request("POST", `/api/control/sessions/${encodeURIComponent(sessionId)}/message`, void 0, input, signal);
1711
+ }
1712
+ sendKeys(sessionId, input, signal) {
1713
+ return this.request("POST", `/api/control/sessions/${encodeURIComponent(sessionId)}/keys`, void 0, input, signal);
1714
+ }
1715
+ respond(sessionId, input, signal) {
1716
+ return this.request("POST", `/api/control/sessions/${encodeURIComponent(sessionId)}/respond`, void 0, input, signal);
1717
+ }
1718
+ waitEvents(input, signal) {
1719
+ const query = {};
1720
+ if (input.cursor !== void 0) query.cursor = input.cursor;
1721
+ if (input.timeoutMs !== void 0) query.timeoutMs = String(input.timeoutMs);
1722
+ if (input.sessionIds !== void 0) query.sessionIds = input.sessionIds.join(",");
1723
+ if (input.kinds !== void 0) query.kinds = input.kinds.join(",");
1724
+ return this.request("GET", "/api/control/events", query, void 0, signal);
1725
+ }
1726
+ readFile(pathValue, signal) {
1727
+ return this.request("GET", "/api/files/content", { path: pathValue }, void 0, signal);
1728
+ }
1729
+ listDir(pathValue, signal) {
1730
+ return this.request("GET", "/api/files", { path: pathValue }, void 0, signal);
1731
+ }
1732
+ search(queryValue, pathValue, signal) {
1733
+ const query = { q: queryValue };
1734
+ if (pathValue !== void 0) query.path = pathValue;
1735
+ return this.request("GET", "/api/search", query, void 0, signal);
1736
+ }
1737
+ gitShow(input, signal) {
1738
+ const query = {};
1739
+ for (const [key, value] of Object.entries(input)) {
1740
+ if (value === void 0 || value === null) continue;
1741
+ if (key === "instance") continue;
1742
+ query[key] = String(value);
1743
+ }
1744
+ return this.request("GET", "/api/files/git-show", query, void 0, signal);
1745
+ }
1746
+ async request(method, pathname, query, body, signal) {
1747
+ const url = new URL(pathname, `${this.baseUrl}/`);
1748
+ for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);
1749
+ if (url.origin !== this.origin) throw new FleetError({
1750
+ code: "UNREACHABLE",
1751
+ message: "fleet request URL origin did not match the registered instance origin",
1752
+ retryable: false
1753
+ });
1754
+ const devtunnelHost = isDevtunnelHost(url.hostname);
1755
+ const tunnelEligible = this.getTunnelToken !== void 0 && devtunnelHost && url.protocol === "https:";
1756
+ for (let attempt = 0; attempt < 2; attempt++) {
1757
+ let tunnelToken;
1758
+ if (tunnelEligible) try {
1759
+ tunnelToken = await this.getTunnelToken();
1760
+ } catch (err) {
1761
+ throw mapTunnelAuthError(err);
1762
+ }
1763
+ const attachTunnel = tunnelToken !== void 0 && tunnelToken !== "";
1764
+ const canRetry = attachTunnel && !!this.onTunnelAuthInvalidate && attempt === 0;
1765
+ const headers = {
1766
+ Authorization: `Bearer ${this.token}`,
1767
+ ...devtunnelHost ? { "X-Tunnel-Skip-Anti-Phishing-Page": "true" } : {},
1768
+ ...attachTunnel ? { "X-Tunnel-Authorization": `tunnel ${tunnelToken}` } : {},
1769
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
1770
+ };
1771
+ let response;
1772
+ try {
1773
+ response = await this.fetchFn(url.toString(), {
1774
+ method,
1775
+ headers,
1776
+ body: body === void 0 ? void 0 : JSON.stringify(body),
1777
+ redirect: "error",
1778
+ signal
1779
+ });
1780
+ } catch (err) {
1781
+ if (canRetry && method === "GET") {
1782
+ this.onTunnelAuthInvalidate();
1783
+ continue;
1784
+ }
1785
+ throw mapNetworkError(err, devtunnelHost);
1786
+ }
1787
+ if (!response.ok) {
1788
+ if ((response.status === 401 || response.status === 403) && canRetry) {
1789
+ this.onTunnelAuthInvalidate();
1790
+ continue;
1791
+ }
1792
+ throw await mapHttpError(response, url.toString());
1793
+ }
1794
+ return await response.json();
1795
+ }
1796
+ throw new FleetError({
1797
+ code: "AUTH_FAILED",
1798
+ message: "fleet instance tunnel authentication failed after re-mint; verify the tunnel and `devtunnel user login`",
1799
+ retryable: false
1800
+ });
1801
+ }
1802
+ };
1803
+ /** Dev Tunnel access tokens are only ever scoped to the `*.devtunnels.ms` service. */
1804
+ function isDevtunnelHost(hostname) {
1805
+ return hostname === "devtunnels.ms" || hostname.endsWith(".devtunnels.ms");
1806
+ }
1807
+ function mapTunnelAuthError(err) {
1808
+ if (err instanceof TunnelAuthError) return new FleetError({
1809
+ code: "AUTH_FAILED",
1810
+ message: err.message,
1811
+ retryable: err.code === "TIMEOUT",
1812
+ detail: { tunnelAuth: err.code }
1813
+ });
1814
+ return mapNetworkError(err);
1815
+ }
1816
+ async function mapHttpError(response, requestUrl) {
1817
+ const detail = await readErrorDetail(response);
1818
+ const upstreamMessage = detailToMessage(detail);
1819
+ const suffix = upstreamMessage ? `: ${upstreamMessage}` : "";
1820
+ const status = response.status;
1821
+ if (isDevTunnelHost(requestUrl) && detectDevTunnelNoHost(status, detail)) return new FleetError({
1822
+ code: "NO_HOST",
1823
+ message: `dev tunnel relay reports no host connected (${status})${suffix}`,
1824
+ retryable: true,
1825
+ status,
1826
+ detail
1827
+ });
1828
+ if (status === 401 || status === 403) return new FleetError({
1829
+ code: "AUTH_FAILED",
1830
+ message: `fleet instance authentication failed (${status})${suffix}`,
1831
+ retryable: false,
1832
+ status,
1833
+ detail
1834
+ });
1835
+ if (status === 404) return new FleetError({
1836
+ code: "SESSION_NOT_FOUND",
1837
+ message: `fleet session or resource not found (404)${suffix}`,
1838
+ retryable: false,
1839
+ status,
1840
+ detail
1841
+ });
1842
+ if (status === 409 || status === 412) return new FleetError({
1843
+ code: "PRECONDITION_FAILED",
1844
+ message: `fleet instance precondition failed (${status})${suffix}`,
1845
+ retryable: false,
1846
+ status,
1847
+ detail
1848
+ });
1849
+ if (status === 400) return new FleetError({
1850
+ code: "BAD_REQUEST",
1851
+ message: `fleet instance rejected the request (400)${suffix}`,
1852
+ retryable: false,
1853
+ status,
1854
+ detail
1855
+ });
1856
+ if (status === 408 || status === 504) return new FleetError({
1857
+ code: "TIMEOUT",
1858
+ message: `fleet instance request timed out (${status})${suffix}`,
1859
+ retryable: true,
1860
+ status,
1861
+ detail
1862
+ });
1863
+ if ((status === 502 || status === 503) && isDevTunnelHost(requestUrl)) return new FleetError({
1864
+ code: "RELAY_ERROR",
1865
+ message: `dev tunnel relay returned HTTP ${status} (host may be down, restarting, or under load)${suffix}`,
1866
+ retryable: true,
1867
+ status,
1868
+ detail
1869
+ });
1870
+ if (status === 429) return new FleetError({
1871
+ code: "RATE_LIMITED",
1872
+ message: `fleet instance rate-limited the request (429)${suffix}`,
1873
+ retryable: true,
1874
+ status,
1875
+ detail
1876
+ });
1877
+ return new FleetError({
1878
+ code: "UPSTREAM_ERROR",
1879
+ message: `fleet instance returned HTTP ${status}${suffix}`,
1880
+ retryable: status >= 500,
1881
+ status,
1882
+ detail
1883
+ });
1884
+ }
1885
+ const DEVTUNNEL_HOST_RE$1 = /(?:^|\.)devtunnels\.ms$|(?:^|\.)tunnels\.api\.visualstudio\.com$/i;
1886
+ /** F4: only Dev Tunnel relay hosts may be classified NO_HOST / RELAY_ERROR. */
1887
+ function isDevTunnelHost(requestUrl) {
1888
+ try {
1889
+ return DEVTUNNEL_HOST_RE$1.test(new URL(requestUrl).hostname);
1890
+ } catch {
1891
+ return false;
1892
+ }
1893
+ }
1894
+ const DEVTUNNEL_NO_HOST_SIGNALS = [
1895
+ "no host is currently connected",
1896
+ "tunnel is not currently hosted",
1897
+ "host is not accepting connections",
1898
+ "tunnel host is not connected",
1899
+ "no connection to the host",
1900
+ "tunnelporthostnotconnected"
1901
+ ];
1902
+ function detectDevTunnelNoHost(status, detail) {
1903
+ if (status !== 502 && status !== 503 && status !== 404) return false;
1904
+ const haystack = detailToSearchString(detail).toLowerCase();
1905
+ if (haystack === "") return false;
1906
+ return DEVTUNNEL_NO_HOST_SIGNALS.some((signal) => haystack.includes(signal));
1907
+ }
1908
+ function detailToSearchString(detail) {
1909
+ if (detail === void 0 || detail === null) return "";
1910
+ if (typeof detail === "string") return detail;
1911
+ try {
1912
+ return JSON.stringify(detail);
1913
+ } catch {
1914
+ return String(detail);
1915
+ }
1916
+ }
1917
+ function mapNetworkError(err, devtunnelHost = false) {
1918
+ if (isAbortLike$1(err)) return new FleetError({
1919
+ code: "TIMEOUT",
1920
+ message: "fleet instance request timed out or was aborted",
1921
+ retryable: true,
1922
+ detail: err
1923
+ });
1924
+ return new FleetError({
1925
+ code: "UNREACHABLE",
1926
+ message: `fleet instance unreachable: ${err instanceof Error ? err.message : String(err)}${devtunnelHost ? " — if this is a private VS Code Dev Tunnel, an unauthenticated request is redirected to GitHub auth (which we refuse to follow): set a `tunnelId` (auto-mint) / `tunnelToken` in the registry, or make the tunnel anonymous" : ""}`,
1927
+ retryable: true,
1928
+ detail: err
1929
+ });
1930
+ }
1931
+ async function readErrorDetail(response) {
1932
+ const text = await response.text().catch(() => "");
1933
+ if (!text) return void 0;
1934
+ try {
1935
+ return JSON.parse(text);
1936
+ } catch {
1937
+ return text;
1938
+ }
1939
+ }
1940
+ function detailToMessage(detail) {
1941
+ if (typeof detail === "string") return detail;
1942
+ if (typeof detail !== "object" || detail === null) return void 0;
1943
+ const record = detail;
1944
+ const error = record.error;
1945
+ if (typeof error === "string") return error;
1946
+ if (typeof error === "object" && error !== null) {
1947
+ const errorRecord = error;
1948
+ if (typeof errorRecord.message === "string") return errorRecord.message;
1949
+ if (typeof errorRecord.code === "string") return errorRecord.code;
1950
+ }
1951
+ if (typeof record.message === "string") return record.message;
1952
+ }
1953
+ function isAbortLike$1(err) {
1954
+ return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
1955
+ }
1956
+
1957
+ //#endregion
1958
+ //#region src/lib/fleet/registry.ts
1959
+ var FleetRegistryError = class extends Error {
1960
+ code;
1961
+ constructor(code, message) {
1962
+ super(message);
1963
+ this.name = "FleetRegistryError";
1964
+ this.code = code;
1965
+ }
1966
+ };
1967
+ function defaultFleetConfigPath() {
1968
+ return process.env.GH_ROUTER_FLEET_CONFIG || nodePath.join(os.homedir(), ".local", "share", "github-router", "fleet.json");
1969
+ }
1970
+ async function loadFleetRegistryConfig(configPath = defaultFleetConfigPath()) {
1971
+ let stat$1;
1972
+ try {
1973
+ stat$1 = await fs.stat(configPath);
1974
+ } catch (err) {
1975
+ if (isNodeErrorCode(err, "ENOENT")) return { instances: [] };
1976
+ throw err;
1977
+ }
1978
+ if (process.platform !== "win32" && (stat$1.mode & 63) !== 0) console.warn(`[fleet] Registry file ${configPath} is group/other-readable; it contains bearer / tunnel credentials. Consider chmod 600.`);
1979
+ const raw = await fs.readFile(configPath, "utf8");
1980
+ if (raw.trim() === "") return { instances: [] };
1981
+ const parsed = JSON.parse(raw);
1982
+ if (!isObject(parsed)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry must be a JSON object");
1983
+ const instances = parsed.instances;
1984
+ if (instances === void 0) return { instances: [] };
1985
+ if (!Array.isArray(instances)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry instances must be an array");
1986
+ return { instances: instances.map(parseInstance) };
1987
+ }
1988
+ var FleetRegistry = class {
1989
+ loader;
1990
+ loaded;
1991
+ constructor(options = {}) {
1992
+ if (options.config !== void 0) {
1993
+ const config = options.config;
1994
+ this.loader = () => config;
1995
+ } else if (options.loadConfig !== void 0) this.loader = options.loadConfig;
1996
+ else {
1997
+ const configPath = options.configPath;
1998
+ this.loader = () => loadFleetRegistryConfig(configPath);
1999
+ }
2000
+ }
2001
+ async resolveInstance(arg) {
2002
+ const instances = await this.instancesWithTokens();
2003
+ const wanted = typeof arg === "string" ? arg.trim() : "";
2004
+ if (wanted) {
2005
+ const byId = instances.find((instance) => instance.id === wanted);
2006
+ if (byId) return resolvedInstance(byId);
2007
+ const labelMatches = instances.filter((instance) => instance.label.toLocaleLowerCase() === wanted.toLocaleLowerCase());
2008
+ if (labelMatches.length > 1) throw new FleetRegistryError("AMBIGUOUS_LABEL", `fleet instance label ${JSON.stringify(wanted)} matches ${labelMatches.length} instances; use an id`);
2009
+ if (labelMatches.length === 1) return resolvedInstance(labelMatches[0]);
2010
+ throw new FleetRegistryError("INSTANCE_NOT_FOUND", `fleet instance ${JSON.stringify(wanted)} was not found`);
2011
+ }
2012
+ const defaultInstance = instances.find((instance) => instance.default === true);
2013
+ if (defaultInstance) return resolvedInstance(defaultInstance);
2014
+ if (instances.length === 1) return resolvedInstance(instances[0]);
2015
+ throw new FleetRegistryError("INSTANCE_REQUIRED", instances.length === 0 ? "fleet instance is required; registry is empty" : "fleet instance is required; specify an instance id or label");
2016
+ }
2017
+ async listInstances() {
2018
+ return (await this.instancesWithTokens()).map((instance) => ({
2019
+ id: instance.id,
2020
+ label: instance.label,
2021
+ url: instance.url,
2022
+ default: instance.default,
2023
+ allowExec: instance.allowExec
2024
+ }));
2025
+ }
2026
+ instancesWithTokens() {
2027
+ if (!this.loaded) this.loaded = Promise.resolve(this.loader()).then((config) => normalizeConfig(config));
2028
+ return this.loaded;
2029
+ }
2030
+ };
2031
+ function normalizeConfig(config) {
2032
+ if (!isObject(config)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry config must be an object");
2033
+ const instances = config.instances ?? [];
2034
+ if (!Array.isArray(instances)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry instances must be an array");
2035
+ return instances.map(parseInstance);
2036
+ }
2037
+ function parseInstance(raw) {
2038
+ if (!isObject(raw)) throw new FleetRegistryError("INVALID_CONFIG", "fleet registry instance must be an object");
2039
+ const instance = raw;
2040
+ const id = instance.id;
2041
+ const label = instance.label;
2042
+ const url = instance.url;
2043
+ const token = instance.token;
2044
+ if (typeof id !== "string" || id.trim() === "") throw new FleetRegistryError("INVALID_CONFIG", "fleet registry instance id must be a non-empty string");
2045
+ if (typeof label !== "string" || label.trim() === "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} label must be a non-empty string`);
2046
+ if (typeof url !== "string" || url.trim() === "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} url must be a non-empty string`);
2047
+ const trimmedUrl = url.trim();
2048
+ let parsedUrl;
2049
+ try {
2050
+ parsedUrl = new URL(trimmedUrl);
2051
+ } catch {
2052
+ throw invalidInstanceUrlError(id);
2053
+ }
2054
+ if (!isAllowedInstanceUrl(parsedUrl)) throw invalidInstanceUrlError(id);
2055
+ assertDevTunnelUrlShape(id, parsedUrl);
2056
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} url must not contain embedded credentials (userinfo)`);
2057
+ if (typeof token !== "string" || token === "") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} token must be a non-empty string`);
2058
+ const tunnelId = parseTunnelId(id, instance.tunnelId);
2059
+ const tunnelToken = parseTunnelToken(id, instance.tunnelToken);
2060
+ return {
2061
+ id: id.trim(),
2062
+ label: label.trim(),
2063
+ url: trimmedUrl,
2064
+ token,
2065
+ default: instance.default === true ? true : void 0,
2066
+ allowExec: instance.allowExec === true ? true : void 0,
2067
+ tunnelId,
2068
+ tunnelToken
2069
+ };
2070
+ }
2071
+ const TUNNEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
2072
+ function parseTunnelId(id, raw) {
2073
+ if (raw === void 0) return void 0;
2074
+ if (typeof raw !== "string" || !TUNNEL_ID_RE.test(raw.trim())) throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} tunnelId must match ${TUNNEL_ID_RE.source} (a devtunnel tunnel name from \`devtunnel list\`)`);
2075
+ return raw.trim();
2076
+ }
2077
+ function parseTunnelToken(id, raw) {
2078
+ if (raw === void 0) return void 0;
2079
+ if (typeof raw !== "string") throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} tunnelToken must be a string`);
2080
+ let t = raw.trim();
2081
+ if (t.startsWith("\"") && t.endsWith("\"") || t.startsWith("'") && t.endsWith("'")) t = t.slice(1, -1).trim();
2082
+ t = t.replace(/^X-Tunnel-Authorization:\s*/i, "").replace(/^tunnel\s+/i, "").trim();
2083
+ if (t === "" || /\s/.test(t)) throw new FleetRegistryError("INVALID_CONFIG", `fleet registry instance ${id} tunnelToken must be a non-empty single-line token`);
2084
+ return t;
2085
+ }
2086
+ function invalidInstanceUrlError(id) {
2087
+ return new FleetRegistryError("INVALID_CONFIG", `${id.trim()} url must be https (or http://localhost for local testing)`);
2088
+ }
2089
+ function isAllowedInstanceUrl(url) {
2090
+ if (url.protocol === "https:") return true;
2091
+ if (url.protocol !== "http:") return false;
2092
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
2093
+ }
2094
+ const DEVTUNNEL_HOST_RE = /(?:^|\.)devtunnels\.ms$|(?:^|\.)tunnels\.api\.visualstudio\.com$/i;
2095
+ function assertDevTunnelUrlShape(id, url) {
2096
+ if (!DEVTUNNEL_HOST_RE.test(url.hostname)) return;
2097
+ if (url.port === "") return;
2098
+ const firstDot = url.hostname.indexOf(".");
2099
+ const firstLabel = firstDot < 0 ? url.hostname : url.hostname.slice(0, firstDot);
2100
+ const rest = firstDot < 0 ? "" : url.hostname.slice(firstDot + 1);
2101
+ const corrected = rest === "" ? `https://${firstLabel}-${url.port}.devtunnels.ms` : `https://${firstLabel}-${url.port}.${rest}`;
2102
+ throw new FleetRegistryError("INVALID_CONFIG", `${id.trim()} url ${url.href} uses the wrong Dev Tunnel form: the forwarded port must be fused into the hostname, not given as a :port suffix. Use ${corrected} instead (the bare \`<id>.<cluster>.devtunnels.ms:<port>\` host addresses the tunnel-management endpoint, not the relayed service).`);
2103
+ }
2104
+ function resolvedInstance(instance) {
2105
+ return {
2106
+ id: instance.id,
2107
+ label: instance.label,
2108
+ url: instance.url,
2109
+ token: instance.token,
2110
+ allowExec: instance.allowExec,
2111
+ tunnelId: instance.tunnelId,
2112
+ tunnelToken: instance.tunnelToken
2113
+ };
2114
+ }
2115
+ function isObject(value) {
2116
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2117
+ }
2118
+ function isNodeErrorCode(err, code) {
2119
+ return isObject(err) && err.code === code;
2120
+ }
2121
+
2122
+ //#endregion
2123
+ //#region src/lib/fleet/tools.ts
2124
+ const FLEET_GROUP = "fleet";
2125
+ const INSTANCE_PROBE_TIMEOUT_MS = 2e3;
2126
+ const INSTANCE_PROBE_CACHE_TTL_MS = 5e3;
2127
+ const CAPABILITIES_CACHE_TTL_MS = 6e4;
2128
+ const AWAIT_TURN_DEFAULT_TIMEOUT_MS = 3e4;
2129
+ const AWAIT_TURN_TIMEOUT_SLACK_MS = 5e3;
2130
+ const LIST_INSTANCES_FANOUT_CONCURRENCY = 16;
2131
+ const AWAIT_TURN_FANOUT_CONCURRENCY = 256;
2132
+ const INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES = 1;
2133
+ const INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS = 250;
2134
+ const INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS = 1e3;
2135
+ const FLEET_FANOUT_CONCURRENCY_ENV = "GH_ROUTER_FLEET_FANOUT_CONCURRENCY";
2136
+ var FleetToolInputError = class extends Error {
2137
+ code;
2138
+ constructor(code, message) {
2139
+ super(message);
2140
+ this.name = "FleetToolInputError";
2141
+ this.code = code;
2142
+ }
2143
+ };
2144
+ let defaultRegistry;
2145
+ let defaultTunnelProvider;
2146
+ const awaitTurnCursors = /* @__PURE__ */ new Map();
2147
+ const instanceProbeCache = /* @__PURE__ */ new Map();
2148
+ function createFleetTools(options = {}) {
2149
+ const registry = options.registry;
2150
+ const clients = /* @__PURE__ */ new Map();
2151
+ const capabilitiesCache = /* @__PURE__ */ new Map();
2152
+ const tunnelProvider = options.tunnelTokenProvider ?? (defaultTunnelProvider ??= createTunnelTokenProvider());
2153
+ const probeRetryDelay = options.probeRetryDelay ?? delay;
2154
+ const awaitTurnDeadlineSlackMs = nonNegativeNumberOrDefault(options.awaitTurnDeadlineSlackMs, AWAIT_TURN_TIMEOUT_SLACK_MS);
2155
+ function getRegistry() {
2156
+ if (registry) return registry;
2157
+ defaultRegistry ??= new FleetRegistry();
2158
+ return defaultRegistry;
2159
+ }
2160
+ function clientFor(instance) {
2161
+ const key = `${instance.id}\0${instance.url}\0${instance.token}\0${instance.tunnelId ?? ""}\0${instance.tunnelToken ?? ""}`;
2162
+ const existing = clients.get(key);
2163
+ if (existing) return existing;
2164
+ const created = options.createClient ? options.createClient(instance) : new FleetClient({
2165
+ url: instance.url,
2166
+ token: instance.token,
2167
+ fetchFn: options.fetchFn,
2168
+ ...tunnelClientOptions(instance, tunnelProvider)
2169
+ });
2170
+ clients.set(key, created);
2171
+ return created;
2172
+ }
2173
+ async function getInstanceCapabilities(instance, signal) {
2174
+ const now = Date.now();
2175
+ const cached$1 = capabilitiesCache.get(instance.id);
2176
+ if (cached$1 && now - cached$1.at < CAPABILITIES_CACHE_TTL_MS) return cached$1.caps;
2177
+ try {
2178
+ const response = await clientFor(instance).capabilities(signal);
2179
+ const caps = new Set(response.capabilities);
2180
+ capabilitiesCache.set(instance.id, {
2181
+ caps,
2182
+ at: Date.now()
2183
+ });
2184
+ return caps;
2185
+ } catch {
2186
+ capabilitiesCache.set(instance.id, {
2187
+ caps: null,
2188
+ at: Date.now()
2189
+ });
2190
+ return null;
2191
+ }
2192
+ }
2193
+ async function assertCapability(instance, cap, featureName, signal) {
2194
+ const caps = await getInstanceCapabilities(instance, signal);
2195
+ if (caps !== null && !caps.has(cap)) throw new FleetToolInputError("UNSUPPORTED_CAPABILITY", `fleet instance ${instance.id} does not advertise the '${cap}' capability required for ${featureName}; omit it or upgrade the ai-or-die control plane`);
2196
+ }
2197
+ async function resolve(arg) {
2198
+ return getRegistry().resolveInstance(arg);
2199
+ }
2200
+ async function resolveSession(args) {
2201
+ const globalId = requiredString(args, "sessionId");
2202
+ const decoded = decodeSessionId(globalId);
2203
+ const instance = await resolve(decoded.instanceId);
2204
+ const explicitInstance = optionalString(args, "instance");
2205
+ if (explicitInstance !== void 0) {
2206
+ const explicit = await resolve(explicitInstance);
2207
+ if (explicit.id !== decoded.instanceId) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionId is for instance ${JSON.stringify(decoded.instanceId)} but arguments.instance resolved to ${JSON.stringify(explicit.id)}`);
2208
+ }
2209
+ return {
2210
+ instance,
2211
+ localId: decoded.localId,
2212
+ globalId
2213
+ };
2214
+ }
2215
+ async function probeInstance(info) {
2216
+ const cacheKey = `${info.id}\0${info.url}`;
2217
+ const now = Date.now();
2218
+ const cached$1 = instanceProbeCache.get(cacheKey);
2219
+ if (cached$1 && now - cached$1.at < INSTANCE_PROBE_CACHE_TTL_MS) return cached$1.result;
2220
+ for (let attempt = 0; attempt <= INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES; attempt++) {
2221
+ const timeout = createProbeTimeout();
2222
+ try {
2223
+ const response = await clientFor(await resolve(info.id)).listSessions(timeout.signal);
2224
+ const lastSeen = Date.now();
2225
+ const result$1 = {
2226
+ id: info.id,
2227
+ label: info.label,
2228
+ reachable: true,
2229
+ sessionCount: response.sessions.length,
2230
+ lastSeen
2231
+ };
2232
+ instanceProbeCache.set(cacheKey, {
2233
+ result: result$1,
2234
+ at: lastSeen
2235
+ });
2236
+ return result$1;
2237
+ } catch (err) {
2238
+ const code = fleetProbeErrorCode(err);
2239
+ if (code === "RATE_LIMITED" && attempt < INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES) {
2240
+ timeout.cleanup();
2241
+ await probeRetryDelay(probeRateLimitBackoffMs(attempt));
2242
+ continue;
2243
+ }
2244
+ const result$1 = failedProbeResult(info, code);
2245
+ instanceProbeCache.set(cacheKey, {
2246
+ result: result$1,
2247
+ at: Date.now()
2248
+ });
2249
+ return result$1;
2250
+ } finally {
2251
+ timeout.cleanup();
2252
+ }
2253
+ }
2254
+ const result = failedProbeResult(info, "UNREACHABLE");
2255
+ instanceProbeCache.set(cacheKey, {
2256
+ result,
2257
+ at: Date.now()
2258
+ });
2259
+ return result;
2260
+ }
2261
+ function tool$1(toolNameHttp, description, inputSchema, handler) {
2262
+ return {
2263
+ toolNameHttp,
2264
+ group: FLEET_GROUP,
2265
+ description,
2266
+ inputSchema,
2267
+ capability: "fleet",
2268
+ async handler(args, signal) {
2269
+ try {
2270
+ return await handler(args, signal);
2271
+ } catch (err) {
2272
+ return errorResult(err);
2273
+ }
2274
+ }
2275
+ };
2276
+ }
2277
+ return Object.freeze([
2278
+ tool$1("list_instances", "List registered remote ai-or-die instances in the fleet registry. Tokens are never returned.", objectSchema({}, []), async () => {
2279
+ return ok({ instances: await mapWithConcurrency(await getRegistry().listInstances(), fleetFanoutConcurrency(LIST_INSTANCES_FANOUT_CONCURRENCY), (instance) => probeInstance(instance)) });
2280
+ }),
2281
+ tool$1("list_sessions", "List sessions on one fleet instance, returning globally-addressable session ids.", objectSchema({ instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance.") }, []), async (args, signal) => {
2282
+ const instance = await resolve(optionalString(args, "instance"));
2283
+ const response = await clientFor(instance).listSessions(signal);
2284
+ return ok({
2285
+ resolvedInstance: publicInstance(instance),
2286
+ sessions: response.sessions.map((session) => globalizeSession(instance.id, session))
2287
+ });
2288
+ }),
2289
+ tool$1("read_session", "Read recent text output from an addressed fleet session.", objectSchema({
2290
+ sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2291
+ instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2292
+ lines: numberProp("Number of recent lines to read."),
2293
+ format: stringProp("Reserved for future formatting; results are JSON text today.")
2294
+ }, ["sessionId"]), async (args, signal) => {
2295
+ const { instance, localId, globalId } = await resolveSession(args);
2296
+ const lines = optionalNumber(args, "lines");
2297
+ const response = await clientFor(instance).readSession(localId, lines, signal);
2298
+ return ok({
2299
+ resolvedInstance: publicInstance(instance),
2300
+ ...response,
2301
+ sessionId: globalId
2302
+ });
2303
+ }),
2304
+ tool$1("session_status", "Fetch lifecycle and interaction status for an addressed fleet session.", objectSchema({
2305
+ sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2306
+ instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId.")
2307
+ }, ["sessionId"]), async (args, signal) => {
2308
+ const { instance, localId, globalId } = await resolveSession(args);
2309
+ const response = await clientFor(instance).status(localId, signal);
2310
+ return ok({
2311
+ resolvedInstance: publicInstance(instance),
2312
+ ...response,
2313
+ sessionId: globalId
2314
+ });
2315
+ }),
2316
+ tool$1("send_message", "Send a message to a fleet session. isError reflects DELIVERY ONLY: it is true only when the message could not be delivered to the session (transport/precondition failure). A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. Recommended pattern: send with awaitMs:0 for a fast delivery ack that never blocks on confirmation, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema({
2317
+ sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2318
+ instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2319
+ message: stringProp("Message text to deliver to the session."),
2320
+ idempotencyKey: stringProp("Caller-generated idempotency key. Reuse the same key on retry; the upstream dedupes so a retry never re-types."),
2321
+ awaitMs: numberProp("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error.")
2322
+ }, [
2323
+ "sessionId",
2324
+ "message",
2325
+ "idempotencyKey"
2326
+ ]), async (args, signal) => {
2327
+ const { instance, localId, globalId } = await resolveSession(args);
2328
+ const awaitMs = optionalNumber(args, "awaitMs");
2329
+ const response = await clientFor(instance).sendMessage(localId, {
2330
+ message: requiredString(args, "message"),
2331
+ idempotencyKey: requiredString(args, "idempotencyKey"),
2332
+ ...awaitMs === void 0 ? {} : { awaitMs }
2333
+ }, signal);
2334
+ const delivered = !(response.delivered === false || response.delivery?.status === "failed" || response.delivery?.status === "error");
2335
+ const confirmed = delivered && response.confirmed === true;
2336
+ const confirmationTimedOut = delivered && !confirmed && (awaitMs !== void 0 && awaitMs > 0 || response.confirmationTimedOut === true);
2337
+ const isError = !delivered;
2338
+ return jsonResult({
2339
+ resolvedInstance: publicInstance(instance),
2340
+ sessionId: globalId,
2341
+ ...response,
2342
+ delivered,
2343
+ confirmed,
2344
+ ...confirmationTimedOut ? {
2345
+ confirmationPending: true,
2346
+ confirmationTimedOut: true
2347
+ } : {},
2348
+ ...isError ? { message: "message was not delivered to the session by the upstream instance" } : confirmationTimedOut ? { message: "delivered; turn completion not confirmed in the await window. Use await_turn filtered to this sessionId to observe completion (the idempotencyKey makes a retried send safe)." } : {}
2349
+ }, isError);
2350
+ }),
2351
+ tool$1("send_keys", "Send key input to a fleet session.", objectSchema({
2352
+ sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2353
+ instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2354
+ keys: stringProp("Key sequence to send."),
2355
+ idempotencyKey: stringProp("Caller-generated idempotency key."),
2356
+ raw: booleanProp("Pass keys through as raw input when the instance supports it.")
2357
+ }, [
2358
+ "sessionId",
2359
+ "keys",
2360
+ "idempotencyKey"
2361
+ ]), async (args, signal) => {
2362
+ const { instance, localId, globalId } = await resolveSession(args);
2363
+ const raw = optionalBoolean(args, "raw");
2364
+ const response = await clientFor(instance).sendKeys(localId, {
2365
+ keys: requiredString(args, "keys"),
2366
+ idempotencyKey: requiredString(args, "idempotencyKey"),
2367
+ ...raw === void 0 ? {} : { raw }
2368
+ }, signal);
2369
+ return ok({
2370
+ resolvedInstance: publicInstance(instance),
2371
+ sessionId: globalId,
2372
+ ...response
2373
+ });
2374
+ }),
2375
+ tool$1("respond", "Answer an awaited prompt in a fleet session by choice, option value, or explicit key override.", objectSchema({
2376
+ sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2377
+ instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2378
+ choice: stringProp("Named or numbered choice to select."),
2379
+ optionValue: stringProp("Exact option value to select."),
2380
+ keys: stringProp("Explicit key override to send instead of a mapped choice."),
2381
+ idempotencyKey: stringProp("Caller-generated idempotency key.")
2382
+ }, ["sessionId", "idempotencyKey"]), async (args, signal) => {
2383
+ const { instance, localId, globalId } = await resolveSession(args);
2384
+ const input = definedObject({
2385
+ choice: optionalString(args, "choice"),
2386
+ optionValue: optionalString(args, "optionValue"),
2387
+ keys: optionalString(args, "keys"),
2388
+ idempotencyKey: requiredString(args, "idempotencyKey")
2389
+ });
2390
+ const response = await clientFor(instance).respond(localId, input, signal);
2391
+ return ok({
2392
+ resolvedInstance: publicInstance(instance),
2393
+ sessionId: globalId,
2394
+ ...response
2395
+ });
2396
+ }),
2397
+ tool$1("create_session", "Create a new session on a specific fleet instance. The instance argument is required; no default is used.", objectSchema({
2398
+ instance: stringProp("Required instance id or label. Create never uses the registry default."),
2399
+ agent: stringProp("Agent/runtime to create on the instance."),
2400
+ name: stringProp("Optional display name for the session."),
2401
+ workingDir: stringProp("Optional working directory on the remote instance."),
2402
+ idempotencyKey: stringProp("Caller-generated idempotency key."),
2403
+ start: booleanProp("Whether the remote instance should start the session immediately."),
2404
+ readyTimeoutMs: numberProp("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
2405
+ permissionMode: stringProp("F10 (claude only): permission mode the launched agent starts in — one of plan | acceptEdits | default | bypassPermissions. Rejected with BAD_REQUEST if unknown or if agentArgs also sets it."),
2406
+ agentArgs: arrayProp("F10 (claude only): extra launcher args appended after the github-router prefix. Must NOT include --permission-mode or --dangerously-skip-permissions (use permissionMode) — rejected with BAD_REQUEST.")
2407
+ }, [
2408
+ "instance",
2409
+ "agent",
2410
+ "idempotencyKey"
2411
+ ]), async (args, signal) => {
2412
+ const instance = await resolve(requiredString(args, "instance"));
2413
+ const agent = requiredString(args, "agent");
2414
+ const idempotencyKey = requiredString(args, "idempotencyKey");
2415
+ const permissionMode = optionalString(args, "permissionMode");
2416
+ const agentArgs = optionalStringArray(args, "agentArgs");
2417
+ if (permissionMode !== void 0) await assertCapability(instance, "permission_mode", "permissionMode", signal);
2418
+ if (agentArgs !== void 0) await assertCapability(instance, "agent_args", "agentArgs", signal);
2419
+ const response = await clientFor(instance).createSession(definedObject({
2420
+ agent,
2421
+ name: optionalString(args, "name"),
2422
+ workingDir: optionalString(args, "workingDir"),
2423
+ start: optionalBoolean(args, "start"),
2424
+ readyTimeoutMs: optionalNumber(args, "readyTimeoutMs"),
2425
+ permissionMode,
2426
+ agentArgs,
2427
+ idempotencyKey
2428
+ }), signal);
2429
+ const localSessionId = typeof response.sessionId === "string" ? response.sessionId : "";
2430
+ return ok({
2431
+ resolvedInstance: publicInstance(instance),
2432
+ ...response,
2433
+ sessionId: localSessionId ? encodeSessionId(instance.id, localSessionId) : response.sessionId
2434
+ });
2435
+ }),
2436
+ tool$1("stop_session", "Stop a fleet session.", objectSchema({
2437
+ sessionId: stringProp("Global session id in the form instanceId:localSessionId."),
2438
+ instance: stringProp("Optional instance id/label; when supplied it must agree with sessionId."),
2439
+ idempotencyKey: stringProp("Caller-generated idempotency key."),
2440
+ mode: stringProp("Optional stop mode understood by the remote instance.")
2441
+ }, ["sessionId", "idempotencyKey"]), async (args, signal) => {
2442
+ const { instance, localId, globalId } = await resolveSession(args);
2443
+ const idempotencyKey = requiredString(args, "idempotencyKey");
2444
+ const response = await clientFor(instance).stopSession(localId, definedObject({
2445
+ mode: optionalString(args, "mode"),
2446
+ idempotencyKey
2447
+ }), signal);
2448
+ return ok({
2449
+ resolvedInstance: publicInstance(instance),
2450
+ sessionId: globalId,
2451
+ ...response
2452
+ });
2453
+ }),
2454
+ tool$1("await_turn", "Long-poll session events across fleet instances. The server owns per-target opaque cursors, so callers do not pass cursor tokens. Distinct concurrent watchers over the same instance set should pass a distinct watcherId so they do not share a cursor.", objectSchema({
2455
+ instances: arrayProp("Instance ids or labels to poll. Omit with sessionIds to target those session instances; omit both to poll every registered instance."),
2456
+ sessionIds: arrayProp("Global session ids to filter to."),
2457
+ timeoutMs: numberProp("Long-poll timeout per instance in milliseconds."),
2458
+ kinds: arrayProp("Optional event kinds to filter to."),
2459
+ watcherId: stringProp("Optional stable id for this watcher. Use a distinct value for concurrent watchers over the same target set to keep cursors isolated.")
2460
+ }, []), async (args, signal) => {
2461
+ const target = await resolveAwaitTarget(args, getRegistry());
2462
+ const cursorByInstance = takeAwaitTurnCursorMap(awaitTurnCursorKey(optionalString(args, "watcherId")));
2463
+ const timeoutMs = optionalNumber(args, "timeoutMs");
2464
+ const kinds = optionalStringArray(args, "kinds");
2465
+ const results = await mapWithConcurrency(target.instances, fleetFanoutConcurrency(AWAIT_TURN_FANOUT_CONCURRENCY), async (instance) => {
2466
+ const deadline = createAwaitTurnDeadline(timeoutMs, awaitTurnDeadlineSlackMs);
2467
+ const combined = combineAbortSignals([signal, deadline.signal]);
2468
+ try {
2469
+ const response = await clientFor(instance).waitEvents(definedObject({
2470
+ cursor: cursorByInstance.get(instance.id),
2471
+ timeoutMs,
2472
+ sessionIds: target.localSessionIdsByInstance.get(instance.id),
2473
+ kinds
2474
+ }), combined.signal);
2475
+ cursorByInstance.set(instance.id, response.cursor);
2476
+ return {
2477
+ ok: true,
2478
+ instance,
2479
+ response
2480
+ };
2481
+ } catch (err) {
2482
+ const error = fleetProbeErrorCode(err);
2483
+ const hint = fleetProbeHint(error);
2484
+ return {
2485
+ ok: false,
2486
+ instance,
2487
+ error,
2488
+ ...hint ? { hint } : {}
2489
+ };
2490
+ } finally {
2491
+ combined.cleanup();
2492
+ deadline.cleanup();
2493
+ }
2494
+ });
2495
+ const responses = results.filter(isAwaitTurnSuccess);
2496
+ const errors = results.filter(isAwaitTurnFailure).map(({ instance, error, hint }) => ({
2497
+ instance: publicInstance(instance),
2498
+ error,
2499
+ ...hint ? { hint } : {}
2500
+ }));
2501
+ const events$1 = responses.flatMap(({ instance, response }) => response.events.map((event) => stampEvent(instance, event))).sort(compareStampedEvents);
2502
+ const gaps = responses.flatMap(({ instance, response }) => response.gaps.map((gap) => ({
2503
+ instance: publicInstance(instance),
2504
+ ...gap
2505
+ })));
2506
+ return ok({
2507
+ resolvedInstances: target.instances.map(publicInstance),
2508
+ events: events$1,
2509
+ gaps,
2510
+ cursors: responses.map(({ instance, response }) => ({
2511
+ instance: publicInstance(instance),
2512
+ cursor: response.cursor
2513
+ })),
2514
+ more: responses.some(({ response }) => response.more),
2515
+ ...errors.length > 0 ? { errors } : {}
2516
+ });
2517
+ }),
2518
+ tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema({
2519
+ instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2520
+ path: stringProp("Remote file path to read.")
2521
+ }, ["path"]), async (args, signal) => {
2522
+ const instance = await resolve(optionalString(args, "instance"));
2523
+ const response = await clientFor(instance).readFile(requiredString(args, "path"), signal);
2524
+ return ok({
2525
+ resolvedInstance: publicInstance(instance),
2526
+ ...response
2527
+ });
2528
+ }),
2529
+ tool$1("list_dir", "List a directory on one fleet instance via its existing /api/files endpoint.", objectSchema({
2530
+ instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2531
+ path: stringProp("Remote directory path to list.")
2532
+ }, ["path"]), async (args, signal) => {
2533
+ const instance = await resolve(optionalString(args, "instance"));
2534
+ const response = await clientFor(instance).listDir(requiredString(args, "path"), signal);
2535
+ return ok({
2536
+ resolvedInstance: publicInstance(instance),
2537
+ ...response
2538
+ });
2539
+ }),
2540
+ tool$1("search", "Search files on one fleet instance via its existing /api/search endpoint.", objectSchema({
2541
+ instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2542
+ query: stringProp("Search query."),
2543
+ path: stringProp("Optional path scope.")
2544
+ }, ["query"]), async (args, signal) => {
2545
+ const instance = await resolve(optionalString(args, "instance"));
2546
+ const response = await clientFor(instance).search(requiredString(args, "query"), optionalString(args, "path"), signal);
2547
+ return ok({
2548
+ resolvedInstance: publicInstance(instance),
2549
+ ...response
2550
+ });
2551
+ }),
2552
+ tool$1("git_show", "Read a file/revision through one fleet instance's existing /api/files/git-show endpoint.", objectSchema({
2553
+ instance: stringProp("Instance id or label. Defaults to the registry default, or the sole instance."),
2554
+ path: stringProp("Remote repository path or file path for git-show."),
2555
+ ref: stringProp("Optional git ref/revision."),
2556
+ rev: stringProp("Optional git revision alias."),
2557
+ commit: stringProp("Optional commit id.")
2558
+ }, ["path"]), async (args, signal) => {
2559
+ const instance = await resolve(optionalString(args, "instance"));
2560
+ const response = await clientFor(instance).gitShow({
2561
+ ...args,
2562
+ instance: void 0
2563
+ }, signal);
2564
+ return ok({
2565
+ resolvedInstance: publicInstance(instance),
2566
+ ...response
2567
+ });
2568
+ })
2569
+ ]);
2570
+ }
2571
+ const FLEET_TOOLS = createFleetTools();
2572
+ function createProbeTimeout() {
2573
+ const timeout = AbortSignal.timeout;
2574
+ if (typeof timeout === "function") return {
2575
+ signal: timeout(INSTANCE_PROBE_TIMEOUT_MS),
2576
+ cleanup: () => {}
2577
+ };
2578
+ const controller = new AbortController();
2579
+ const timer = setTimeout(() => controller.abort(), INSTANCE_PROBE_TIMEOUT_MS);
2580
+ return {
2581
+ signal: controller.signal,
2582
+ cleanup: () => clearTimeout(timer)
2583
+ };
2584
+ }
2585
+ function createAwaitTurnDeadline(timeoutMs, slackMs) {
2586
+ const deadlineMs = Math.max(0, timeoutMs ?? AWAIT_TURN_DEFAULT_TIMEOUT_MS) + slackMs;
2587
+ const controller = new AbortController();
2588
+ const timer = setTimeout(() => {
2589
+ const err = /* @__PURE__ */ new Error("await_turn per-instance deadline exceeded");
2590
+ err.name = "TimeoutError";
2591
+ controller.abort(err);
2592
+ }, deadlineMs);
2593
+ return {
2594
+ signal: controller.signal,
2595
+ cleanup: () => clearTimeout(timer)
2596
+ };
2597
+ }
2598
+ function combineAbortSignals(signals) {
2599
+ const noop = () => {};
2600
+ const present = signals.filter((signal) => signal !== void 0);
2601
+ if (present.length === 0) return {
2602
+ signal: void 0,
2603
+ cleanup: noop
2604
+ };
2605
+ if (present.length === 1) return {
2606
+ signal: present[0],
2607
+ cleanup: noop
2608
+ };
2609
+ const any = AbortSignal.any;
2610
+ if (typeof any === "function") return {
2611
+ signal: any(present),
2612
+ cleanup: noop
2613
+ };
2614
+ const controller = new AbortController();
2615
+ const listeners = [];
2616
+ const cleanup = () => {
2617
+ for (const { signal, handler } of listeners) signal.removeEventListener("abort", handler);
2618
+ listeners.length = 0;
2619
+ };
2620
+ for (const signal of present) {
2621
+ if (signal.aborted) {
2622
+ if (!controller.signal.aborted) controller.abort(signal.reason);
2623
+ cleanup();
2624
+ return {
2625
+ signal: controller.signal,
2626
+ cleanup: noop
2627
+ };
2628
+ }
2629
+ const handler = () => {
2630
+ if (!controller.signal.aborted) controller.abort(signal.reason);
2631
+ };
2632
+ signal.addEventListener("abort", handler, { once: true });
2633
+ listeners.push({
2634
+ signal,
2635
+ handler
2636
+ });
2637
+ }
2638
+ return {
2639
+ signal: controller.signal,
2640
+ cleanup
2641
+ };
2642
+ }
2643
+ function fleetProbeErrorCode(err) {
2644
+ if (typeof err === "object" && err !== null && "code" in err) {
2645
+ const code = err.code;
2646
+ if (typeof code === "string" && isFleetErrorCode(code)) return code;
2647
+ }
2648
+ if (isAbortLike(err)) return "TIMEOUT";
2649
+ return "UNREACHABLE";
2650
+ }
2651
+ function fleetProbeHint(code) {
2652
+ switch (code) {
2653
+ case "NO_HOST": return "tunnel relay up, no ai-or-die host connected (start the host on that machine)";
2654
+ case "RELAY_ERROR": return "tunnel relay returned an error; the host may be down, restarting, or under load";
2655
+ case "TIMEOUT": return "no response before the probe deadline; the host may be slow or the tunnel may have no host";
2656
+ case "UNREACHABLE": return "could not connect (DNS or connection failure); check the instance url";
2657
+ default: return;
2658
+ }
2659
+ }
2660
+ function isFleetErrorCode(code) {
2661
+ switch (code) {
2662
+ case "UNREACHABLE":
2663
+ case "AUTH_FAILED":
2664
+ case "SESSION_NOT_FOUND":
2665
+ case "PRECONDITION_FAILED":
2666
+ case "TIMEOUT":
2667
+ case "UPSTREAM_ERROR":
2668
+ case "NO_HOST":
2669
+ case "RELAY_ERROR":
2670
+ case "BAD_REQUEST":
2671
+ case "RATE_LIMITED": return true;
2672
+ default: return false;
2673
+ }
2674
+ }
2675
+ async function resolveAwaitTarget(args, registry) {
2676
+ const instanceArgs = optionalStringArray(args, "instances");
2677
+ const sessionIdArgs = optionalStringArray(args, "sessionIds");
2678
+ const localSessionIdsByInstance = /* @__PURE__ */ new Map();
2679
+ for (const sessionId of sessionIdArgs ?? []) {
2680
+ const decoded = decodeSessionId(sessionId);
2681
+ const existing = localSessionIdsByInstance.get(decoded.instanceId) ?? [];
2682
+ existing.push(decoded.localId);
2683
+ localSessionIdsByInstance.set(decoded.instanceId, existing);
2684
+ }
2685
+ let instances;
2686
+ if (instanceArgs !== void 0 && instanceArgs.length > 0) {
2687
+ instances = uniqueInstances(await Promise.all(instanceArgs.map((arg) => registry.resolveInstance(arg))));
2688
+ const ids = new Set(instances.map((instance) => instance.id));
2689
+ for (const instanceId of localSessionIdsByInstance.keys()) if (!ids.has(instanceId)) throw new FleetToolInputError("INSTANCE_MISMATCH", `sessionIds include instance ${JSON.stringify(instanceId)} which is not in arguments.instances`);
2690
+ } else if (localSessionIdsByInstance.size > 0) instances = uniqueInstances(await Promise.all([...localSessionIdsByInstance.keys()].map((instanceId) => registry.resolveInstance(instanceId))));
2691
+ else {
2692
+ const infos = await registry.listInstances();
2693
+ if (infos.length === 0) throw new FleetRegistryError("INSTANCE_REQUIRED", "await_turn requires at least one registered fleet instance");
2694
+ instances = uniqueInstances(await Promise.all(infos.map((info) => registry.resolveInstance(info.id))));
2695
+ }
2696
+ return {
2697
+ instances,
2698
+ localSessionIdsByInstance
2699
+ };
2700
+ }
2701
+ function globalizeSession(instanceId, session) {
2702
+ return {
2703
+ ...session,
2704
+ sessionId: encodeSessionId(instanceId, session.sessionId)
2705
+ };
2706
+ }
2707
+ function stampEvent(instance, event) {
2708
+ return {
2709
+ ...event,
2710
+ instance: publicInstance(instance),
2711
+ ...typeof event.sessionId === "string" ? { sessionId: encodeSessionId(instance.id, event.sessionId) } : {}
2712
+ };
2713
+ }
2714
+ function eventAtMs(value) {
2715
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2716
+ if (typeof value === "string") {
2717
+ const parsed = Date.parse(value);
2718
+ if (!Number.isNaN(parsed)) return parsed;
2719
+ }
2720
+ return 0;
2721
+ }
2722
+ function compareStampedEvents(a, b) {
2723
+ const atA = eventAtMs(a.at);
2724
+ const atB = eventAtMs(b.at);
2725
+ if (atA !== atB) return atA - atB;
2726
+ return (typeof a.seq === "number" ? a.seq : 0) - (typeof b.seq === "number" ? b.seq : 0);
2727
+ }
2728
+ const MAX_WATCHER_ID_LEN = 200;
2729
+ const MAX_AWAIT_TURN_CURSOR_KEYS = 1024;
2730
+ function awaitTurnCursorKey(watcherId) {
2731
+ const id = watcherId ?? "default";
2732
+ return id.length > MAX_WATCHER_ID_LEN ? id.slice(0, MAX_WATCHER_ID_LEN) : id;
2733
+ }
2734
+ function takeAwaitTurnCursorMap(clientKey) {
2735
+ const existing = awaitTurnCursors.get(clientKey);
2736
+ if (existing) {
2737
+ awaitTurnCursors.delete(clientKey);
2738
+ awaitTurnCursors.set(clientKey, existing);
2739
+ return existing;
2740
+ }
2741
+ const created = /* @__PURE__ */ new Map();
2742
+ awaitTurnCursors.set(clientKey, created);
2743
+ while (awaitTurnCursors.size > MAX_AWAIT_TURN_CURSOR_KEYS) {
2744
+ const oldest = awaitTurnCursors.keys().next().value;
2745
+ if (oldest === void 0) break;
2746
+ awaitTurnCursors.delete(oldest);
2747
+ }
2748
+ return created;
2749
+ }
2750
+ function isAwaitTurnSuccess(result) {
2751
+ return result.ok;
2752
+ }
2753
+ function isAwaitTurnFailure(result) {
2754
+ return !result.ok;
2755
+ }
2756
+ function failedProbeResult(info, code) {
2757
+ const hint = fleetProbeHint(code);
2758
+ return {
2759
+ id: info.id,
2760
+ label: info.label,
2761
+ reachable: false,
2762
+ error: code,
2763
+ ...hint ? { hint } : {}
2764
+ };
2765
+ }
2766
+ async function mapWithConcurrency(items, limit, fn) {
2767
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
2768
+ const concurrency = Math.max(1, Math.min(items.length || 1, safeLimit));
2769
+ const results = new Array(items.length);
2770
+ let nextIndex = 0;
2771
+ async function worker() {
2772
+ while (nextIndex < items.length) {
2773
+ const index = nextIndex++;
2774
+ results[index] = await fn(items[index], index);
2775
+ }
2776
+ }
2777
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
2778
+ return results;
2779
+ }
2780
+ function fleetFanoutConcurrency(defaultLimit) {
2781
+ const raw = process.env[FLEET_FANOUT_CONCURRENCY_ENV];
2782
+ const parsed = raw === void 0 ? NaN : Number.parseInt(raw, 10);
2783
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
2784
+ return defaultLimit;
2785
+ }
2786
+ function probeRateLimitBackoffMs(attempt) {
2787
+ return Math.min(INSTANCE_PROBE_RATE_LIMIT_BACKOFF_BASE_MS * 2 ** attempt, INSTANCE_PROBE_RATE_LIMIT_BACKOFF_MAX_MS);
2788
+ }
2789
+ function nonNegativeNumberOrDefault(value, fallback) {
2790
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
2791
+ }
2792
+ async function delay(ms) {
2793
+ if (ms <= 0) return;
2794
+ await new Promise((resolve) => setTimeout(resolve, ms));
2795
+ }
2796
+ function isAbortLike(err) {
2797
+ if (!(err instanceof Error)) return false;
2798
+ return err.name === "AbortError" || err.name === "TimeoutError";
2799
+ }
2800
+ function uniqueInstances(instances) {
2801
+ const seen = /* @__PURE__ */ new Set();
2802
+ const result = [];
2803
+ for (const instance of instances) {
2804
+ if (seen.has(instance.id)) continue;
2805
+ seen.add(instance.id);
2806
+ result.push(instance);
2807
+ }
2808
+ return result;
2809
+ }
2810
+ function publicInstance(instance) {
2811
+ return {
2812
+ id: instance.id,
2813
+ label: instance.label
2814
+ };
2815
+ }
2816
+ /**
2817
+ * Build the FleetClient tunnel-auth options for a resolved instance.
2818
+ * Resolution order: a `tunnelId` enables auto-mint + auto-refresh (and the
2819
+ * evict-on-failure hook); else a static `tunnelToken` is sent directly (no
2820
+ * retry, since it cannot be re-minted); else no tunnel auth.
2821
+ */
2822
+ function tunnelClientOptions(instance, provider) {
2823
+ if (instance.tunnelId) {
2824
+ const cfg = { tunnelId: instance.tunnelId };
2825
+ return {
2826
+ getTunnelToken: () => provider.getToken(cfg),
2827
+ onTunnelAuthInvalidate: () => provider.invalidate(cfg)
2828
+ };
2829
+ }
2830
+ if (instance.tunnelToken) {
2831
+ const token = instance.tunnelToken;
2832
+ return { getTunnelToken: async () => token };
2833
+ }
2834
+ return {};
2835
+ }
2836
+ function ok(value) {
2837
+ return jsonResult(value, false);
2838
+ }
2839
+ function jsonResult(value, isError) {
2840
+ return {
2841
+ content: [{
2842
+ type: "text",
2843
+ text: JSON.stringify(value)
2844
+ }],
2845
+ ...isError ? { isError: true } : {}
2846
+ };
2847
+ }
2848
+ function errorResult(err) {
2849
+ return jsonResult({ error: {
2850
+ code: errorCode(err),
2851
+ message: err instanceof Error ? err.message : String(err)
2852
+ } }, true);
2853
+ }
2854
+ function errorCode(err) {
2855
+ if (typeof err === "object" && err !== null && "code" in err) {
2856
+ const code = err.code;
2857
+ if (typeof code === "string") return code;
2858
+ }
2859
+ return "FLEET_ERROR";
2860
+ }
2861
+ function definedObject(input) {
2862
+ const result = {};
2863
+ for (const [key, value] of Object.entries(input)) if (value !== void 0) result[key] = value;
2864
+ return result;
2865
+ }
2866
+ function requiredString(args, key) {
2867
+ const value = args[key];
2868
+ if (typeof value !== "string" || value.trim() === "") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
2869
+ return value;
2870
+ }
2871
+ function optionalString(args, key) {
2872
+ const value = args[key];
2873
+ if (value === void 0) return void 0;
2874
+ if (typeof value !== "string") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a string`);
2875
+ return value.trim() === "" ? void 0 : value;
2876
+ }
2877
+ function optionalNumber(args, key) {
2878
+ const value = args[key];
2879
+ if (value === void 0) return void 0;
2880
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a finite number`);
2881
+ return value;
2882
+ }
2883
+ function optionalBoolean(args, key) {
2884
+ const value = args[key];
2885
+ if (value === void 0) return void 0;
2886
+ if (typeof value !== "boolean") throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a boolean`);
2887
+ return value;
2888
+ }
2889
+ function optionalStringArray(args, key) {
2890
+ const value = args[key];
2891
+ if (value === void 0) return void 0;
2892
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.${key} must be an array of non-empty strings`);
2893
+ return value;
2894
+ }
2895
+ function objectSchema(properties, required) {
2896
+ return {
2897
+ type: "object",
2898
+ required,
2899
+ additionalProperties: false,
2900
+ properties
2901
+ };
2902
+ }
2903
+ function stringProp(description) {
2904
+ return {
2905
+ type: "string",
2906
+ description
2907
+ };
2908
+ }
2909
+ function numberProp(description) {
2910
+ return {
2911
+ type: "number",
2912
+ description
2913
+ };
2914
+ }
2915
+ function booleanProp(description) {
2916
+ return {
2917
+ type: "boolean",
2918
+ description
2919
+ };
2920
+ }
2921
+ function arrayProp(description) {
2922
+ return {
2923
+ type: "array",
2924
+ items: { type: "string" },
2925
+ description
2926
+ };
2927
+ }
2928
+
1048
2929
  //#endregion
1049
2930
  //#region src/lib/tree-sitter-grammars.ts
1050
2931
  /**
@@ -5046,7 +6927,7 @@ async function runInit(workspace) {
5046
6927
  ];
5047
6928
  const onInactivityCheck = makeIndexProgressProbe(workspace);
5048
6929
  const startMs = Date.now();
5049
- let ok = false;
6930
+ let ok$2 = false;
5050
6931
  let failureClass;
5051
6932
  try {
5052
6933
  const res = await runManagedExeCapture(binary, args, {
@@ -5063,10 +6944,10 @@ async function runInit(workspace) {
5063
6944
  }).catch(() => {});
5064
6945
  }
5065
6946
  });
5066
- ok = !res.stalled && !res.timedOut && res.code === 0;
5067
- if (!ok) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
6947
+ ok$2 = !res.stalled && !res.timedOut && res.code === 0;
6948
+ if (!ok$2) failureClass = res.stalled || res.timedOut ? "stuck" : "error";
5068
6949
  } catch {
5069
- ok = false;
6950
+ ok$2 = false;
5070
6951
  failureClass = "launch";
5071
6952
  } finally {
5072
6953
  releaseInit(workspace);
@@ -5083,9 +6964,9 @@ async function runInit(workspace) {
5083
6964
  finalMeta.lastIndexedDirty = g.dirty;
5084
6965
  }
5085
6966
  } catch {}
5086
- finalMeta.status = ok ? "ready" : "failed";
6967
+ finalMeta.status = ok$2 ? "ready" : "failed";
5087
6968
  finalMeta.lastIndexedAt = (/* @__PURE__ */ new Date()).toISOString();
5088
- if (ok) {
6969
+ if (ok$2) {
5089
6970
  finalMeta.failedAttempts = 0;
5090
6971
  finalMeta.failureClass = void 0;
5091
6972
  } else {
@@ -6164,8 +8045,8 @@ async function isHumanlikeAutoOn(tabId, signal) {
6164
8045
  } catch {}
6165
8046
  return humanlikeAutoCache.tabs.has(tabId);
6166
8047
  }
6167
- async function maybeInjectHumanlikeDelay(tool, signal, tabId) {
6168
- if (!PACED_TOOLS.has(tool)) return;
8048
+ async function maybeInjectHumanlikeDelay(tool$1, signal, tabId) {
8049
+ if (!PACED_TOOLS.has(tool$1)) return;
6169
8050
  let on = state.humanlikeForce === "on";
6170
8051
  if (!on && state.humanlikeForce === "auto") on = await isHumanlikeAutoOn(tabId, signal);
6171
8052
  if (!on) return;
@@ -6270,8 +8151,8 @@ const PER_TOOL_TIMEOUTS = {
6270
8151
  maxMs: 1e4
6271
8152
  }
6272
8153
  };
6273
- function pickTimeout(tool) {
6274
- if (tool in PER_TOOL_TIMEOUTS) return PER_TOOL_TIMEOUTS[tool];
8154
+ function pickTimeout(tool$1) {
8155
+ if (tool$1 in PER_TOOL_TIMEOUTS) return PER_TOOL_TIMEOUTS[tool$1];
6275
8156
  return {
6276
8157
  defaultMs: 1e4,
6277
8158
  maxMs: 3e4
@@ -6284,7 +8165,7 @@ function pickTimeout(tool) {
6284
8165
  * client sends notifications/cancelled, the WS is force-closed and
6285
8166
  * the promise rejects so the slot releases cleanly.
6286
8167
  */
6287
- async function bridgeCall(endpoint, tool, args, timeoutMs, signal) {
8168
+ async function bridgeCall(endpoint, tool$1, args, timeoutMs, signal) {
6288
8169
  return new Promise((resolve, reject) => {
6289
8170
  const id = randomUUID();
6290
8171
  const ws = new WebSocket(`ws://127.0.0.1:${endpoint.port}`, { headers: { authorization: `Bearer ${endpoint.token}` } });
@@ -6318,7 +8199,7 @@ async function bridgeCall(endpoint, tool, args, timeoutMs, signal) {
6318
8199
  }
6319
8200
  ws.send(JSON.stringify({
6320
8201
  id,
6321
- tool,
8202
+ tool: tool$1,
6322
8203
  args
6323
8204
  }));
6324
8205
  });
@@ -6377,8 +8258,8 @@ function blockedUrlEnvelope(reason) {
6377
8258
  * happy-path `ensureBridgeReady()` (and its NMH install) is the accepted
6378
8259
  * cost of keeping the credentials fresh.
6379
8260
  */
6380
- async function browserPreflight(tool, args) {
6381
- const policy = preflightUrlPolicy(tool.startsWith("browser_") ? tool : `browser_${tool}`, args);
8261
+ async function browserPreflight(tool$1, args) {
8262
+ const policy = preflightUrlPolicy(tool$1.startsWith("browser_") ? tool$1 : `browser_${tool$1}`, args);
6382
8263
  if (policy.blocked) return { envelope: blockedUrlEnvelope(policy.reason) };
6383
8264
  const ready = await ensureBridgeReady();
6384
8265
  if (ready.install_required) return { envelope: installRequiredToolResult(ready) };
@@ -6389,23 +8270,23 @@ async function browserPreflight(tool, args) {
6389
8270
  * src/lib/browser-mcp/index.ts. Returns the standard MCP tool-result
6390
8271
  * envelope.
6391
8272
  */
6392
- async function dispatchBrowserTool(tool, args, signal, opts = {}) {
6393
- const policy = preflightUrlPolicy(tool, args);
8273
+ async function dispatchBrowserTool(tool$1, args, signal, opts = {}) {
8274
+ const policy = preflightUrlPolicy(tool$1, args);
6394
8275
  if (policy.blocked) return blockedUrlEnvelope(policy.reason);
6395
8276
  const ready = await ensureBridgeReady();
6396
8277
  if (ready.install_required) return installRequiredToolResult(ready);
6397
- await maybeInjectHumanlikeDelay(tool, signal, typeof args.tabId === "number" ? args.tabId : void 0);
6398
- const { defaultMs, maxMs } = pickTimeout(tool);
8278
+ await maybeInjectHumanlikeDelay(tool$1, signal, typeof args.tabId === "number" ? args.tabId : void 0);
8279
+ const { defaultMs, maxMs } = pickTimeout(tool$1);
6399
8280
  const callerTimeout = typeof opts.timeoutMs === "number" && opts.timeoutMs > 0 ? Math.min(opts.timeoutMs, maxMs) : defaultMs;
6400
8281
  try {
6401
8282
  const resp = await bridgeCall({
6402
8283
  port: ready.port,
6403
8284
  token: ready.token
6404
- }, tool, args, callerTimeout, signal);
8285
+ }, tool$1, args, callerTimeout, signal);
6405
8286
  if (resp.ok) {
6406
8287
  const text = typeof resp.data === "string" ? resp.data : JSON.stringify(resp.data, null, 2);
6407
8288
  logAudit$1({
6408
- tool,
8289
+ tool: tool$1,
6409
8290
  argsBytes: argsByteSize(args),
6410
8291
  durationMs: 0,
6411
8292
  profile: typeof args.profile === "string" ? args.profile : "isolated",
@@ -6417,7 +8298,7 @@ async function dispatchBrowserTool(tool, args, signal, opts = {}) {
6417
8298
  }] };
6418
8299
  }
6419
8300
  logAudit$1({
6420
- tool,
8301
+ tool: tool$1,
6421
8302
  argsBytes: argsByteSize(args),
6422
8303
  durationMs: 0,
6423
8304
  profile: typeof args.profile === "string" ? args.profile : "isolated",
@@ -6427,14 +8308,14 @@ async function dispatchBrowserTool(tool, args, signal, opts = {}) {
6427
8308
  return {
6428
8309
  content: [{
6429
8310
  type: "text",
6430
- text: `${tool} failed: ${resp.error}${resp.code ? ` (${resp.code})` : ""}`
8311
+ text: `${tool$1} failed: ${resp.error}${resp.code ? ` (${resp.code})` : ""}`
6431
8312
  }],
6432
8313
  isError: true
6433
8314
  };
6434
8315
  } catch (err) {
6435
8316
  const message = err instanceof Error ? err.message : String(err);
6436
8317
  logAudit$1({
6437
- tool,
8318
+ tool: tool$1,
6438
8319
  argsBytes: argsByteSize(args),
6439
8320
  durationMs: 0,
6440
8321
  profile: typeof args.profile === "string" ? args.profile : "isolated",
@@ -6444,7 +8325,7 @@ async function dispatchBrowserTool(tool, args, signal, opts = {}) {
6444
8325
  return {
6445
8326
  content: [{
6446
8327
  type: "text",
6447
- text: `${tool} failed: ${message}`
8328
+ text: `${tool$1} failed: ${message}`
6448
8329
  }],
6449
8330
  isError: true
6450
8331
  };
@@ -7405,20 +9286,20 @@ function compressorAvailable() {
7405
9286
  * free-form `message.content` and strip a leading / trailing ```` ``` ````
7406
9287
  * code fence before parsing.
7407
9288
  */
7408
- async function callCompressor(systemPrompt, userMessage, tool, signal) {
9289
+ async function callCompressor(systemPrompt, userMessage, tool$1, signal) {
7409
9290
  const backend = pickBackend();
7410
9291
  if (!backend) throw new Error(`browser-mcp compressor: no backend available in catalog. Checked: ${COMPRESSOR_FALLBACK_CHAIN.join(", ")}`);
7411
9292
  const release = acquireInFlightSlot();
7412
9293
  if (!release) throw new Error("browser-mcp compressor: inflight slot saturated (cap 8); try again shortly");
7413
9294
  try {
7414
- return backend.endpoint === "responses" ? await callViaResponses(backend.id, systemPrompt, userMessage, tool, signal) : await callViaChat(backend.id, systemPrompt, userMessage, tool, signal);
9295
+ return backend.endpoint === "responses" ? await callViaResponses(backend.id, systemPrompt, userMessage, tool$1, signal) : await callViaChat(backend.id, systemPrompt, userMessage, tool$1, signal);
7415
9296
  } finally {
7416
9297
  release();
7417
9298
  }
7418
9299
  }
7419
9300
  /** Forced-tool-call over `/chat/completions`. Parses the function-call
7420
9301
  * arguments, falling back to fenced free-form content. */
7421
- async function callViaChat(model, systemPrompt, userMessage, tool, signal) {
9302
+ async function callViaChat(model, systemPrompt, userMessage, tool$1, signal) {
7422
9303
  const msg = (await createChatCompletions({
7423
9304
  model,
7424
9305
  stream: false,
@@ -7432,14 +9313,14 @@ async function callViaChat(model, systemPrompt, userMessage, tool, signal) {
7432
9313
  tools: [{
7433
9314
  type: "function",
7434
9315
  function: {
7435
- name: tool.name,
7436
- description: tool.description,
7437
- parameters: tool.parameters
9316
+ name: tool$1.name,
9317
+ description: tool$1.description,
9318
+ parameters: tool$1.parameters
7438
9319
  }
7439
9320
  }],
7440
9321
  tool_choice: {
7441
9322
  type: "function",
7442
- function: { name: tool.name }
9323
+ function: { name: tool$1.name }
7443
9324
  }
7444
9325
  }, void 0, signal)).choices?.[0]?.message;
7445
9326
  const toolArgs = msg?.tool_calls?.[0]?.function?.arguments;
@@ -7453,7 +9334,7 @@ async function callViaChat(model, systemPrompt, userMessage, tool, signal) {
7453
9334
  * items of `type: "function_call"` carrying the `arguments` JSON string.
7454
9335
  * Image parts use `input_image` (vs chat's `image_url`) — see
7455
9336
  * `toResponsesContent`. */
7456
- async function callViaResponses(model, systemPrompt, userMessage, tool, signal) {
9337
+ async function callViaResponses(model, systemPrompt, userMessage, tool$1, signal) {
7457
9338
  const resp = await createResponses({
7458
9339
  model,
7459
9340
  stream: false,
@@ -7466,13 +9347,13 @@ async function callViaResponses(model, systemPrompt, userMessage, tool, signal)
7466
9347
  }],
7467
9348
  tools: [{
7468
9349
  type: "function",
7469
- name: tool.name,
7470
- description: tool.description,
7471
- parameters: tool.parameters
9350
+ name: tool$1.name,
9351
+ description: tool$1.description,
9352
+ parameters: tool$1.parameters
7472
9353
  }],
7473
9354
  tool_choice: {
7474
9355
  type: "function",
7475
- name: tool.name
9356
+ name: tool$1.name
7476
9357
  }
7477
9358
  }, void 0, signal);
7478
9359
  const output = Array.isArray(resp.output) ? resp.output : [];
@@ -7528,8 +9409,8 @@ function extractResponsesText$1(output) {
7528
9409
  * directly so the underlying function can change signature without
7529
9410
  * breaking the public surface.
7530
9411
  */
7531
- async function callCompressorPublic(systemPrompt, userMessage, tool, signal) {
7532
- return callCompressor(systemPrompt, userMessage, tool, signal);
9412
+ async function callCompressorPublic(systemPrompt, userMessage, tool$1, signal) {
9413
+ return callCompressor(systemPrompt, userMessage, tool$1, signal);
7533
9414
  }
7534
9415
  /**
7535
9416
  * Strip a single leading / trailing ``` (or ```json) code fence from a
@@ -8643,12 +10524,12 @@ const BROWSER_TOOLS = Object.freeze([
8643
10524
  capability: "browser_power",
8644
10525
  async handler(args, signal) {
8645
10526
  const kind = args.kind === "network" ? "network" : "console";
8646
- const tool = kind === "network" ? "browser_network_log" : "browser_console_logs";
10527
+ const tool$1 = kind === "network" ? "browser_network_log" : "browser_console_logs";
8647
10528
  const tabId = typeof args.tabId === "number" ? args.tabId : void 0;
8648
10529
  const level = typeof args.level === "string" ? args.level : "all";
8649
10530
  const regexStr = typeof args.regex === "string" ? args.regex : void 0;
8650
10531
  const limit = typeof args.limit === "number" ? Math.min(1e3, Math.max(1, args.limit)) : 100;
8651
- const env = await dispatchBrowserTool(tool, {
10532
+ const env = await dispatchBrowserTool(tool$1, {
8652
10533
  tabId,
8653
10534
  level
8654
10535
  }, signal);
@@ -9536,12 +11417,12 @@ function formatValidationPath(error) {
9536
11417
  * @returns The validated (and potentially coerced) arguments
9537
11418
  * @throws Error with formatted message if validation fails
9538
11419
  */
9539
- function validateToolArguments(tool, toolCall) {
11420
+ function validateToolArguments(tool$1, toolCall) {
9540
11421
  const args = structuredClone(toolCall.arguments);
9541
- Value.Convert(tool.parameters, args);
9542
- const validator = getValidator(tool.parameters);
9543
- if (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) {
9544
- const coerced = coerceWithJsonSchema(args, tool.parameters);
11422
+ Value.Convert(tool$1.parameters, args);
11423
+ const validator = getValidator(tool$1.parameters);
11424
+ if (!hasTypeBoxMetadata(tool$1.parameters) && isJsonSchemaObject(tool$1.parameters)) {
11425
+ const coerced = coerceWithJsonSchema(args, tool$1.parameters);
9545
11426
  if (coerced !== args) if (isRecord(args) && isRecord(coerced)) {
9546
11427
  for (const key of Object.keys(args)) delete args[key];
9547
11428
  Object.assign(args, coerced);
@@ -9850,9 +11731,9 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
9850
11731
  function shouldTerminateToolBatch(finalizedCalls) {
9851
11732
  return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
9852
11733
  }
9853
- function prepareToolCallArguments(tool, toolCall) {
9854
- if (!tool.prepareArguments) return toolCall;
9855
- const preparedArguments = tool.prepareArguments(toolCall.arguments);
11734
+ function prepareToolCallArguments(tool$1, toolCall) {
11735
+ if (!tool$1.prepareArguments) return toolCall;
11736
+ const preparedArguments = tool$1.prepareArguments(toolCall.arguments);
9856
11737
  if (preparedArguments === toolCall.arguments) return toolCall;
9857
11738
  return {
9858
11739
  ...toolCall,
@@ -9860,14 +11741,14 @@ function prepareToolCallArguments(tool, toolCall) {
9860
11741
  };
9861
11742
  }
9862
11743
  async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
9863
- const tool = currentContext.tools?.find((t) => t.name === toolCall.name);
9864
- if (!tool) return {
11744
+ const tool$1 = currentContext.tools?.find((t) => t.name === toolCall.name);
11745
+ if (!tool$1) return {
9865
11746
  kind: "immediate",
9866
11747
  result: createErrorToolResult(`Tool ${toolCall.name} not found`),
9867
11748
  isError: true
9868
11749
  };
9869
11750
  try {
9870
- const validatedArgs = validateToolArguments(tool, prepareToolCallArguments(tool, toolCall));
11751
+ const validatedArgs = validateToolArguments(tool$1, prepareToolCallArguments(tool$1, toolCall));
9871
11752
  if (config.beforeToolCall) {
9872
11753
  const beforeResult = await config.beforeToolCall({
9873
11754
  assistantMessage,
@@ -9894,7 +11775,7 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
9894
11775
  return {
9895
11776
  kind: "prepared",
9896
11777
  toolCall,
9897
- tool,
11778
+ tool: tool$1,
9898
11779
  args: validatedArgs
9899
11780
  };
9900
11781
  } catch (error) {
@@ -12192,7 +14073,7 @@ const REPORT_INSUFFICIENT_DESCRIPTION = "Finish by declaring the requested value
12192
14073
  function makeBrowserTool(meta, parameters, dispatch, sessionId) {
12193
14074
  const wireName = `browser_${meta.name}`;
12194
14075
  const policy = tabPolicyFor(meta.name);
12195
- const tool = {
14076
+ const tool$1 = {
12196
14077
  name: meta.name,
12197
14078
  label: meta.label,
12198
14079
  description: meta.description,
@@ -12217,8 +14098,8 @@ function makeBrowserTool(meta, parameters, dispatch, sessionId) {
12217
14098
  return textResult$1(text);
12218
14099
  }
12219
14100
  };
12220
- if (meta.executionMode) tool.executionMode = meta.executionMode;
12221
- return tool;
14101
+ if (meta.executionMode) tool$1.executionMode = meta.executionMode;
14102
+ return tool$1;
12222
14103
  }
12223
14104
  /**
12224
14105
  * Build a synthetic terminal tool. `execute` never touches the browser — it
@@ -12699,9 +14580,9 @@ const calculateParametersTokens = (parameters, encoder, constants) => {
12699
14580
  /**
12700
14581
  * Calculate tokens for a single tool
12701
14582
  */
12702
- const calculateToolTokens = (tool, encoder, constants) => {
14583
+ const calculateToolTokens = (tool$1, encoder, constants) => {
12703
14584
  let tokens = constants.funcInit;
12704
- const func = tool.function;
14585
+ const func = tool$1.function;
12705
14586
  const fName = func.name;
12706
14587
  let fDesc = func.description || "";
12707
14588
  if (fDesc.endsWith(".")) fDesc = fDesc.slice(0, -1);
@@ -12715,7 +14596,7 @@ const calculateToolTokens = (tool, encoder, constants) => {
12715
14596
  */
12716
14597
  const numTokensForTools = (tools, encoder, constants) => {
12717
14598
  let funcTokenCount = 0;
12718
- for (const tool of tools) funcTokenCount += calculateToolTokens(tool, encoder, constants);
14599
+ for (const tool$1 of tools) funcTokenCount += calculateToolTokens(tool$1, encoder, constants);
12719
14600
  funcTokenCount += constants.funcEnd;
12720
14601
  return funcTokenCount;
12721
14602
  };
@@ -13001,6 +14882,28 @@ function browserToolsEnabled() {
13001
14882
  return hasSupportedBrowserInstalled();
13002
14883
  }
13003
14884
  /**
14885
+ * Gate for the fleet session-control MCP tools (`mcp__fleet__*`).
14886
+ *
14887
+ * Returns true iff the operator opted in (`state.fleetEnabled`, set by
14888
+ * `--fleet`, OR `GH_ROUTER_ENABLE_FLEET=1` read directly so non-
14889
+ * `setupAndServe` startup paths — tests, embedded use — can still flip
14890
+ * the gate). Fleet needs no local installed dependency check.
14891
+ */
14892
+ function fleetToolsEnabled() {
14893
+ return state.fleetEnabled || process.env.GH_ROUTER_ENABLE_FLEET === "1";
14894
+ }
14895
+ /**
14896
+ * Gate for ai-or-die Artifact review tools.
14897
+ *
14898
+ * Returns true iff this github-router process was launched inside an
14899
+ * ai-or-die tab and received the tab-scoped API trio. The tools are
14900
+ * otherwise invisible at `tools/list` and rejected at `tools/call`; direct
14901
+ * handler calls still return a friendly isError envelope.
14902
+ */
14903
+ function artifactToolsEnabled() {
14904
+ return !!(process.env.AIORDIE_BASE_URL && process.env.AIORDIE_TOKEN && process.env.AIORDIE_SESSION_ID);
14905
+ }
14906
+ /**
13004
14907
  * Gate for the `browse` worker tool (the Pi-driven autonomous browser
13005
14908
  * agent that delegates a browsing task to its own context).
13006
14909
  *
@@ -13196,6 +15099,8 @@ function toolEntries(scope) {
13196
15099
  if (t.capability === "browse_agent") return browseAgentEnabled();
13197
15100
  if (t.capability === "stand_in") return standInToolEnabled();
13198
15101
  if (t.capability === "browser") return browserToolsEnabled();
15102
+ if (t.capability === "fleet") return fleetToolsEnabled();
15103
+ if (t.capability === "artifact") return artifactToolsEnabled();
13199
15104
  if (t.capability === "browser_compound") return browserToolsEnabled() && browserCompoundToolsEnabled();
13200
15105
  if (t.capability === "browser_power") return browserToolsEnabled() && browserPowerToolsEnabled();
13201
15106
  return true;
@@ -13522,6 +15427,8 @@ async function handleToolsCall(body, scope) {
13522
15427
  if (nonPersonaTool && nonPersonaTool.capability === "browse_agent" && !browseAgentEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
13523
15428
  if (nonPersonaTool && nonPersonaTool.capability === "stand_in" && !standInToolEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
13524
15429
  if (nonPersonaTool && nonPersonaTool.capability === "browser" && !browserToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15430
+ if (nonPersonaTool && nonPersonaTool.capability === "fleet" && !fleetToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
15431
+ if (nonPersonaTool && nonPersonaTool.capability === "artifact" && !artifactToolsEnabled()) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
13525
15432
  if (nonPersonaTool && nonPersonaTool.capability === "browser_compound" && !(browserToolsEnabled() && browserCompoundToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
13526
15433
  if (nonPersonaTool && nonPersonaTool.capability === "browser_power" && !(browserToolsEnabled() && browserPowerToolsEnabled())) return rpcError(body.id, RPC_METHOD_NOT_FOUND, `tools/call: unknown tool "${name}"`);
13527
15434
  let personaPrompt;
@@ -16197,32 +18104,32 @@ function toolbeltTool(workspace) {
16197
18104
  description: "Run a read-only code-analysis CLI in the workspace with NO shell (args are literal — no pipes / redirects / chaining / globbing). Tools: rg, fd, sg (ast-grep), jq, yq, gron, scc, tokei, difft (difftastic), and git (read-only subcommands). Write/exec flags (fd -x, rg --pre, ast-grep --rewrite, yq -i) and mutating git subcommands are rejected. Returns combined stdout (stderr appended on non-zero exit).",
16198
18105
  parameters: TOOLBELT_PARAMS,
16199
18106
  async execute(_toolCallId, params, signal) {
16200
- const tool = params.tool;
18107
+ const tool$1 = params.tool;
16201
18108
  const args = Array.isArray(params.args) ? params.args.map(String) : [];
16202
- if (!TOOLBELT_TOOL_SET.has(tool)) throw new Error(`toolbelt: unknown tool '${tool}'`);
16203
- if (tool === "git") {
18109
+ if (!TOOLBELT_TOOL_SET.has(tool$1)) throw new Error(`toolbelt: unknown tool '${tool$1}'`);
18110
+ if (tool$1 === "git") {
16204
18111
  const sub = args[0];
16205
18112
  if (!sub || !GIT_READONLY_SUBCOMMANDS.has(sub)) throw new Error(`git: only read-only subcommands are allowed and the subcommand must be args[0] (no leading -C/-c). Allowed: ${[...GIT_READONLY_SUBCOMMANDS].join(", ")}. Got: ${sub ? `'${sub}'` : "<none>"}`);
16206
18113
  for (const arg of args) if (gitArgDenied(arg)) throw new Error(`git: flag '${arg}' is not allowed (toolbelt is read-only)`);
16207
18114
  } else {
16208
- if (tool === "sg" && args[0] && SG_DENIED_SUBCOMMANDS.has(args[0])) throw new Error(`sg: subcommand '${args[0]}' is not allowed (toolbelt is read-only)`);
16209
- const denied = TOOLBELT_DENIED_FLAGS[tool];
18115
+ if (tool$1 === "sg" && args[0] && SG_DENIED_SUBCOMMANDS.has(args[0])) throw new Error(`sg: subcommand '${args[0]}' is not allowed (toolbelt is read-only)`);
18116
+ const denied = TOOLBELT_DENIED_FLAGS[tool$1];
16210
18117
  if (denied) {
16211
- for (const arg of args) if (argViolatesDenylist(denied, arg)) throw new Error(`${tool}: arg '${arg}' carries a write/exec flag (toolbelt is read-only)`);
18118
+ for (const arg of args) if (argViolatesDenylist(denied, arg)) throw new Error(`${tool$1}: arg '${arg}' carries a write/exec flag (toolbelt is read-only)`);
16212
18119
  }
16213
18120
  }
16214
18121
  const env = buildEnv();
16215
- if (tool === "git") {
18122
+ if (tool$1 === "git") {
16216
18123
  env.GIT_PAGER = "cat";
16217
18124
  env.PAGER = "cat";
16218
18125
  env.GIT_TERMINAL_PROMPT = "0";
16219
18126
  env.GIT_OPTIONAL_LOCKS = "0";
16220
18127
  }
16221
- const binPath = resolveExecutable(tool, { env });
16222
- if (!binPath) return textResult(`${tool}: not available on this host (not on PATH / toolbelt). rg/fd/jq/yq/sg/gron/scc/difft ship with the toolbelt; git and tokei may require a system install.`);
18128
+ const binPath = resolveExecutable(tool$1, { env });
18129
+ if (!binPath) return textResult(`${tool$1}: not available on this host (not on PATH / toolbelt). rg/fd/jq/yq/sg/gron/scc/difft ship with the toolbelt; git and tokei may require a system install.`);
16223
18130
  const TOOLBELT_TIMEOUT_MS = 6e4;
16224
18131
  const TOOLBELT_STDOUT_CAP = 1024 * 1024;
16225
- const res = await runManagedExeCapture(binPath, tool === "git" ? buildGitExecArgs(args) : args, {
18132
+ const res = await runManagedExeCapture(binPath, tool$1 === "git" ? buildGitExecArgs(args) : args, {
16226
18133
  cwd: workspace,
16227
18134
  env,
16228
18135
  timeoutMs: TOOLBELT_TIMEOUT_MS,
@@ -16232,13 +18139,13 @@ function toolbeltTool(workspace) {
16232
18139
  else signal?.addEventListener("abort", () => killChildTree(child), { once: true });
16233
18140
  }
16234
18141
  });
16235
- if (signal?.aborted) throw new Error(`${tool} aborted`);
16236
- if (res.timedOut) throw new Error(`${tool} timed out after ${TOOLBELT_TIMEOUT_MS}ms`);
18142
+ if (signal?.aborted) throw new Error(`${tool$1} aborted`);
18143
+ if (res.timedOut) throw new Error(`${tool$1} timed out after ${TOOLBELT_TIMEOUT_MS}ms`);
16237
18144
  const parts = [];
16238
18145
  if (res.stdout) parts.push(res.stdout);
16239
18146
  if ((res.code !== 0 || !res.stdout) && res.stderr.trim()) parts.push(`[stderr] ${res.stderr.trim()}`);
16240
18147
  if (res.stdoutTruncated) parts.push(`[truncated at ${TOOLBELT_STDOUT_CAP} bytes — narrow the query]`);
16241
- if (parts.length === 0) parts.push(`(${tool} exited ${res.code} with no output)`);
18148
+ if (parts.length === 0) parts.push(`(${tool$1} exited ${res.code} with no output)`);
16242
18149
  return textResult(parts.join("\n"));
16243
18150
  }
16244
18151
  };
@@ -19233,7 +21140,8 @@ const MCP_GROUPS = Object.freeze([
19233
21140
  "workers",
19234
21141
  "orchestrate",
19235
21142
  "browser",
19236
- "decide"
21143
+ "decide",
21144
+ "fleet"
19237
21145
  ]);
19238
21146
  const GROUP_META = Object.freeze({
19239
21147
  peers: {
@@ -19265,6 +21173,11 @@ const GROUP_META = Object.freeze({
19265
21173
  preferredKey: "decide",
19266
21174
  urlSuffix: "decide",
19267
21175
  serverInfoName: "github-router-decide"
21176
+ },
21177
+ fleet: {
21178
+ preferredKey: "fleet",
21179
+ urlSuffix: "fleet",
21180
+ serverInfoName: "github-router-fleet"
19268
21181
  }
19269
21182
  });
19270
21183
  /** True iff `s` is a registered group name (route `:group` param validation). */
@@ -20376,6 +22289,8 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
20376
22289
  return runStandInToolCall(args, signal);
20377
22290
  }
20378
22291
  },
22292
+ ...ARTIFACT_TOOLS,
22293
+ ...FLEET_TOOLS,
20379
22294
  ...BROWSER_TOOLS.map((t) => ({
20380
22295
  ...t,
20381
22296
  group: "browser",
@@ -20704,5 +22619,5 @@ async function runStandInToolCall(args, signal) {
20704
22619
  }
20705
22620
 
20706
22621
  //#endregion
20707
- export { handleMcpDelete as $, IMPLEMENT_DEFAULT_MODEL as A, setupGitHubToken as At, TOOLBELT_TOOLS$1 as B, getModels as Bt, stopGateEnabledForRepo as C, UPSTREAM_FETCH_TIMEOUT_MS as Ct, liveExec as D, getPackageVersion as Dt, resolveSealedGate as E, pickClaudeDefault as Et, availableToolCommands as F, filterBetaHeader as Ft, buildAdvisorStream as G, copilotBaseUrl as Gt, searchWeb as H, HTTPError as Ht, buildToolbeltAwareness as I, isNullish as It, buildOpenAIErrorEvent as J, state as Jt, injectAdvisorTool as K, copilotHeaders as Kt, toolbeltEnabled as L, resolveCodexModel as Lt, appendPlanReminder as M, cacheCopilotVersion as Mt, runWorkerAgent as N, cacheModels as Nt, BROWSE_DEFAULT_MODEL as O, withInstallLock as Ot, withNoOutputRetry as P, cacheVSCodeVersion as Pt, relayAnthropicStream as Q, toolbeltSkipSet as R, resolveModel as Rt, repoRoot as S, DEFAULT_PORT as St, trustRepo as T, generateRandomPort as Tt, ADVISOR_INTERNAL_TOOL_NAME as U, forwardError as Ut, assetFor as V, fetchWithTransientRetry as Vt, ADVISOR_TOOL_INSTRUCTIONS as W, GITHUB_API_BASE_URL as Wt, logStreamError as X, isControllerClosedError as Y, readIteratorWithTimeout as Z, fileFindingsStore as _, collapsePathKeys as _t, buildPeerAwarenessSnippet as a, createMessages as at, isSubagentContext as b, DEFAULT_CODEX_MODEL as bt, buildStopHookCommand as c, createChatCompletions as ct, fileBlockBudget as d, parseJsonOrDiagnose as dt, handleMcpPost as et, injectStopHookIntoSettingsFile as f, provisionBrowserAssets as ft, fileBaselineStore as g, extractZipMember as gt, stopReviewEnabled as h, extractTarGzMember as ht, buildAgentPrompt as i, countTokens as it, PLAN_DEFAULT_MODEL as j, tryRefreshAndRetry as jt, DEFAULT_MODEL as k, setupCopilotToken as kt, captureLaunchBaseline as l, MAX_RESPONSE_BODY_BYTES as lt, stopGateId as m, provisionAndIndexColbert as mt, MCP_GROUPS as n, standInToolEnabled as nt, personasFor as o, getTokenCount as ot, launchBaselineKey as p, hasSupportedBrowserInstalled as pt, isAdvisorRequested as q, githubHeaders as qt, assertMcpToolSurfaceConsistent as r, workerToolsEnabled as rt, buildSessionBindHookCommand as s, createResponses as st, GROUP_META as t, browserToolsEnabled as tt, decideStopHook as u, readResponseBodyCapped as ut, fileLastPromptStore as v, toolbeltPathOverride as vt, stopReviewStateDir as w, UPSTREAM_INACTIVITY_TIMEOUT_MS as wt, repoFingerprint as x, DEFAULT_CODEX_MODEL_FALLBACKS as xt, fileReviewDebounce as y, DEFAULT_CLAUDE_MODEL_FALLBACKS as yt, vscodeRipgrepPath as z, sleep as zt };
20708
- //# sourceMappingURL=peer-mcp-personas-CoeEliOe.js.map
22622
+ export { handleMcpDelete as $, IMPLEMENT_DEFAULT_MODEL as A, setupCopilotToken as At, TOOLBELT_TOOLS$1 as B, sleep as Bt, stopGateEnabledForRepo as C, DEFAULT_PORT as Ct, liveExec as D, pickClaudeDefault as Dt, resolveSealedGate as E, generateRandomPort as Et, availableToolCommands as F, cacheVSCodeVersion as Ft, buildAdvisorStream as G, GITHUB_API_BASE_URL as Gt, searchWeb as H, fetchWithTransientRetry as Ht, buildToolbeltAwareness as I, filterBetaHeader as It, buildOpenAIErrorEvent as J, githubHeaders as Jt, injectAdvisorTool as K, copilotBaseUrl as Kt, toolbeltEnabled as L, isNullish as Lt, appendPlanReminder as M, tryRefreshAndRetry as Mt, runWorkerAgent as N, cacheCopilotVersion as Nt, BROWSE_DEFAULT_MODEL as O, getPackageVersion as Ot, withNoOutputRetry as P, cacheModels as Pt, relayAnthropicStream as Q, toolbeltSkipSet as R, resolveCodexModel as Rt, repoRoot as S, DEFAULT_CODEX_MODEL_FALLBACKS as St, trustRepo as T, UPSTREAM_INACTIVITY_TIMEOUT_MS as Tt, ADVISOR_INTERNAL_TOOL_NAME as U, HTTPError as Ut, assetFor as V, getModels as Vt, ADVISOR_TOOL_INSTRUCTIONS as W, forwardError as Wt, logStreamError as X, isControllerClosedError as Y, state as Yt, readIteratorWithTimeout as Z, fileFindingsStore as _, extractZipMember as _t, buildPeerAwarenessSnippet as a, countTokens as at, isSubagentContext as b, DEFAULT_CLAUDE_MODEL_FALLBACKS as bt, buildStopHookCommand as c, createResponses as ct, fileBlockBudget as d, readResponseBodyCapped as dt, handleMcpPost as et, injectStopHookIntoSettingsFile as f, parseJsonOrDiagnose as ft, fileBaselineStore as g, extractTarGzMember as gt, stopReviewEnabled as h, provisionAndIndexColbert as ht, buildAgentPrompt as i, workerToolsEnabled as it, PLAN_DEFAULT_MODEL as j, setupGitHubToken as jt, DEFAULT_MODEL as k, withInstallLock as kt, captureLaunchBaseline as l, createChatCompletions as lt, stopGateId as m, hasSupportedBrowserInstalled as mt, MCP_GROUPS as n, fleetToolsEnabled as nt, personasFor as o, createMessages as ot, launchBaselineKey as p, provisionBrowserAssets as pt, isAdvisorRequested as q, copilotHeaders as qt, assertMcpToolSurfaceConsistent as r, standInToolEnabled as rt, buildSessionBindHookCommand as s, getTokenCount as st, GROUP_META as t, browserToolsEnabled as tt, decideStopHook as u, MAX_RESPONSE_BODY_BYTES as ut, fileLastPromptStore as v, collapsePathKeys as vt, stopReviewStateDir as w, UPSTREAM_FETCH_TIMEOUT_MS as wt, repoFingerprint as x, DEFAULT_CODEX_MODEL as xt, fileReviewDebounce as y, toolbeltPathOverride as yt, vscodeRipgrepPath as z, resolveModel as zt };
22623
+ //# sourceMappingURL=peer-mcp-personas-Be4SAgm0.js.map