airgate 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +16 -2
  2. package/dist/airgate.mjs +1281 -475
  3. package/package.json +1 -1
package/dist/airgate.mjs CHANGED
@@ -229,7 +229,76 @@ function spaceName(taken = [], random = Math.random) {
229
229
  if (!used.has(name)) return name;
230
230
  }
231
231
  const base = `${pick(FIRST, random)}-${pick(SECOND, random)}`;
232
- for (let n = 2; ; n++) if (!used.has(`${base}-${n}`)) return `${base}-${n}`;
232
+ for (let n2 = 2; ; n2++) if (!used.has(`${base}-${n2}`)) return `${base}-${n2}`;
233
+ }
234
+
235
+ // ../sandbox/src/dotenv.ts
236
+ var SECRET_NAME_RE = /^[A-Z_][A-Z0-9_]{0,63}$/;
237
+ function reservedSecretName(name) {
238
+ return name.startsWith("AIRGATE_") || name.startsWith("LD_") || name.startsWith("DYLD_") || ["PATH", "HOME", "USER", "SHELL", "PWD", "BASH_ENV", "ENV", "PROMPT_COMMAND", "IFS", "TERM", "TMUX"].includes(name);
239
+ }
240
+ function suggestSecretName(raw) {
241
+ const s2 = raw.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_").replace(/^([0-9])/, "_$1").slice(0, 64);
242
+ return SECRET_NAME_RE.test(s2) && !reservedSecretName(s2) ? s2 : null;
243
+ }
244
+ var unescapeDouble = (s2) => s2.replace(/\\([nrt"\\$])/g, (_m, c) => ({ n: "\n", r: "\r", t: " " })[c] ?? c);
245
+ function parseDotenv(input) {
246
+ const lines = input.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").split("\n");
247
+ const entries = [];
248
+ const unparsed = [];
249
+ for (let i = 0; i < lines.length; i++) {
250
+ const lineNo = i + 1;
251
+ const line2 = lines[i].trim();
252
+ if (!line2 || line2.startsWith("#")) continue;
253
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line2);
254
+ if (!m) {
255
+ unparsed.push(lineNo);
256
+ continue;
257
+ }
258
+ const key = m[1];
259
+ let rest = m[2];
260
+ let value;
261
+ const q = rest[0];
262
+ if (q === '"' || q === "'" || q === "`") {
263
+ let body = rest.slice(1);
264
+ let close = findClose(body, q);
265
+ while (close < 0 && i + 1 < lines.length) {
266
+ i++;
267
+ body += "\n" + lines[i];
268
+ close = findClose(body, q);
269
+ }
270
+ const raw = close < 0 ? body : body.slice(0, close);
271
+ value = q === '"' ? unescapeDouble(raw) : raw;
272
+ } else {
273
+ value = rest.replace(/\s+#.*$/, "").trim();
274
+ }
275
+ let name = key;
276
+ let note;
277
+ if (!SECRET_NAME_RE.test(key)) {
278
+ name = suggestSecretName(key);
279
+ note = name ? `saved as ${name}` : "not a usable name";
280
+ } else if (reservedSecretName(key)) {
281
+ name = null;
282
+ note = "reserved (the pod sets it)";
283
+ }
284
+ entries.push({ key, name, value, line: lineNo, ...note ? { note } : {} });
285
+ }
286
+ const values = {};
287
+ for (const e of entries) if (e.name) values[e.name] = e.value;
288
+ return { entries, values, unparsed };
289
+ }
290
+ function findClose(s2, q) {
291
+ for (let i = 0; i < s2.length; i++) {
292
+ if (q === '"' && s2[i] === "\\") {
293
+ i++;
294
+ continue;
295
+ }
296
+ if (s2[i] === q) return i;
297
+ }
298
+ return -1;
299
+ }
300
+ function dotenvExample(names) {
301
+ return names.map((n2) => `${n2}=`).join("\n") + (names.length ? "\n" : "");
233
302
  }
234
303
 
235
304
  // src/api.ts
@@ -356,8 +425,8 @@ var Api = class {
356
425
  pod(id) {
357
426
  return this.request("GET", `/v1/pods/${enc(id)}`).then((r) => r.pod);
358
427
  }
359
- launch(spec, returnUrl) {
360
- return this.request("POST", "/v1/pods", { body: { spec, returnUrl } }).then((r) => r.pod);
428
+ launch(spec, returnUrl, secrets2) {
429
+ return this.request("POST", "/v1/pods", { body: { spec, returnUrl, ...secrets2 && Object.keys(secrets2).length ? { secrets: secrets2 } : {} } }).then((r) => r.pod);
361
430
  }
362
431
  stop(id) {
363
432
  return this.request("POST", `/v1/pods/${enc(id)}/stop`, { body: {} }).then((r) => r.pod);
@@ -436,18 +505,48 @@ var Api = class {
436
505
  githubConnect(returnUrl) {
437
506
  return this.request("POST", "/v1/github/connect", { body: { returnUrl } });
438
507
  }
508
+ inspectRepo(repo) {
509
+ return this.request("GET", `/v1/github/inspect?repo=${encodeURIComponent(repo)}`);
510
+ }
439
511
  githubRepos(fresh = false) {
440
512
  return this.request("GET", `/v1/github/repos${fresh ? "?fresh=1" : ""}`).then((r) => r.repos);
441
513
  }
442
514
  githubDisconnect(id) {
443
515
  return this.request("DELETE", `/v1/github/installations/${id}`);
444
516
  }
517
+ /* model sign-ins: connected to airgate (opt-in), and whether a laptop relay is connected */
518
+ modelsAuth() {
519
+ return this.request("GET", "/v1/models/auth");
520
+ }
521
+ connectModel(vendor, value) {
522
+ return this.request("PUT", `/v1/models/auth/${enc(vendor)}`, { body: { value } });
523
+ }
524
+ disconnectModel(vendor) {
525
+ return this.request("DELETE", `/v1/models/auth/${enc(vendor)}`);
526
+ }
527
+ relayUrl() {
528
+ const q = new URLSearchParams({ token: this.tokenForUrl(), client: this.client });
529
+ return `${this.gateway.replace(/^http/, "ws")}/v1/relay?${q}`;
530
+ }
445
531
  secrets() {
446
532
  return this.request("GET", "/v1/secrets");
447
533
  }
448
534
  setSecret(name, value, podId) {
449
535
  return this.request("PUT", `/v1/secrets/${enc(name)}`, { body: { value, ...podId ? { podId } : {} } });
450
536
  }
537
+ previews(id) {
538
+ return this.request("GET", `/v1/pods/${enc(id)}/previews`);
539
+ }
540
+ ensurePreview(id, body) {
541
+ return this.request("POST", `/v1/pods/${enc(id)}/previews`, { body }).then((r) => r.preview);
542
+ }
543
+ openPreview(slug, path = "/") {
544
+ return this.request("POST", `/v1/previews/${enc(slug)}/open`, { body: { path } }).then((r) => r.url);
545
+ }
546
+ /** many at once (a .env file), saved together or not at all */
547
+ setSecrets(values, podId) {
548
+ return this.request("PUT", "/v1/secrets", { body: { values, ...podId ? { podId } : {} } });
549
+ }
451
550
  deleteSecret(name, podId) {
452
551
  return this.request("DELETE", `/v1/secrets/${enc(name)}${podId ? `?podId=${enc(podId)}` : ""}`);
453
552
  }
@@ -1015,10 +1114,10 @@ function drawable(text2, font = "block") {
1015
1114
  return [...String(text2)].filter((ch) => [...f.cased ? ch : ch.toUpperCase()].some((c) => Object.hasOwn(f.glyphs, c))).join("");
1016
1115
  }
1017
1116
  var shapes = (text2, f) => [...String(text2)].flatMap((ch) => [...f.cased ? ch : ch.toUpperCase()]).map((c) => Object.hasOwn(f.glyphs, c) ? f.glyphs[c] : void 0).filter((g) => Boolean(g)).map((g) => g.split("|"));
1018
- function fade(stops, n) {
1117
+ function fade(stops, n2) {
1019
1118
  const rgb2 = stops.map((s2) => [1, 3, 5].map((i) => parseInt(s2.slice(i, i + 2), 16)));
1020
- return Array.from({ length: n }, (_, i) => {
1021
- const k = n > 1 ? i / (n - 1) * (rgb2.length - 1) : 0;
1119
+ return Array.from({ length: n2 }, (_, i) => {
1120
+ const k = n2 > 1 ? i / (n2 - 1) * (rgb2.length - 1) : 0;
1022
1121
  const a = Math.min(rgb2.length - 2, Math.floor(k)), f = k - a;
1023
1122
  const c = rgb2.length === 1 ? rgb2[0] : rgb2[a].map((v, j) => Math.round(v + (rgb2[a + 1][j] - v) * f));
1024
1123
  return `#${c.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
@@ -1446,14 +1545,14 @@ function merge(parts, extra = [], plain = false) {
1446
1545
  };
1447
1546
  const idx = lists.map((p) => (p ?? []).map(add));
1448
1547
  const inked = lists.some((p) => !p) ? { light: add(INK2.light), dark: add(INK2.dark) } : null;
1449
- const n = all.length;
1548
+ const n2 = all.length;
1450
1549
  const values = all.map(rgb);
1451
1550
  const into = all.map((_, i) => i);
1452
- const alive = new Uint8Array(n).fill(1);
1453
- const near = new Int32Array(n), gap = new Float64Array(n);
1551
+ const alive = new Uint8Array(n2).fill(1);
1552
+ const near = new Int32Array(n2), gap = new Float64Array(n2);
1454
1553
  const look = (i) => {
1455
1554
  let best = Infinity, at4 = -1;
1456
- for (let j = 0; j < n; j++)
1555
+ for (let j = 0; j < n2; j++)
1457
1556
  if (j !== i && alive[j]) {
1458
1557
  const d = apart(values[i], values[j]);
1459
1558
  if (d < best)
@@ -1462,19 +1561,19 @@ function merge(parts, extra = [], plain = false) {
1462
1561
  near[i] = at4;
1463
1562
  gap[i] = best;
1464
1563
  };
1465
- for (let i = 0; i < n; i++)
1564
+ for (let i = 0; i < n2; i++)
1466
1565
  look(i);
1467
- for (let left = n; left > 64; left--) {
1566
+ for (let left = n2; left > 64; left--) {
1468
1567
  let a = -1;
1469
- for (let i = 0; i < n; i++)
1568
+ for (let i = 0; i < n2; i++)
1470
1569
  if (alive[i] && (a < 0 || gap[i] < gap[a]))
1471
1570
  a = i;
1472
1571
  const keep = Math.min(a, near[a]), drop = Math.max(a, near[a]);
1473
1572
  alive[drop] = 0;
1474
- for (let i = 0; i < n; i++)
1573
+ for (let i = 0; i < n2; i++)
1475
1574
  if (into[i] === drop)
1476
1575
  into[i] = keep;
1477
- for (let i = 0; i < n; i++)
1576
+ for (let i = 0; i < n2; i++)
1478
1577
  if (alive[i] && near[i] === drop)
1479
1578
  look(i);
1480
1579
  }
@@ -1679,8 +1778,8 @@ function knockout(s2, g, x, y, h, mask2) {
1679
1778
  }
1680
1779
  }
1681
1780
  var at = (v, t) => {
1682
- const n = typeof v === "function" ? v(t) : v;
1683
- return finite(n) ? Math.floor(n) : 0;
1781
+ const n2 = typeof v === "function" ? v(t) : v;
1782
+ return finite(n2) ? Math.floor(n2) : 0;
1684
1783
  };
1685
1784
  var around = (p, size2, room) => {
1686
1785
  const span = room + size2;
@@ -1870,19 +1969,19 @@ function crop(src, region) {
1870
1969
  fail2(`crop's region, ${cols} by ${rows2} at ${x}, ${y}, misses ${p.meta.name}, which is ${p.cols} by ${p.rows}`);
1871
1970
  return reshape("crop", p, x1 - x0, y1 - y0, (g, s2) => s2.paste(g, -x0, -y0));
1872
1971
  }
1873
- function pad(src, n) {
1972
+ function pad(src, n2) {
1874
1973
  const p = prepare("pad", src);
1875
1974
  let sides;
1876
- if (typeof n === "number")
1877
- sides = Array(4).fill(whole2(n, "pad", 0, 0));
1878
- else if (Array.isArray(n) && n.length === 2) {
1879
- const [r, c] = [whole2(n[0], "pad's rows", 0, 0), whole2(n[1], "pad's columns", 0, 0)];
1975
+ if (typeof n2 === "number")
1976
+ sides = Array(4).fill(whole2(n2, "pad", 0, 0));
1977
+ else if (Array.isArray(n2) && n2.length === 2) {
1978
+ const [r, c] = [whole2(n2[0], "pad's rows", 0, 0), whole2(n2[1], "pad's columns", 0, 0)];
1880
1979
  sides = [r, c, r, c];
1881
- } else if (n !== null && typeof n === "object" && !Array.isArray(n)) {
1882
- const o = n;
1980
+ } else if (n2 !== null && typeof n2 === "object" && !Array.isArray(n2)) {
1981
+ const o = n2;
1883
1982
  sides = ["top", "right", "bottom", "left"].map((k) => whole2(o[k], `pad's ${k}`, 0, 0));
1884
1983
  } else
1885
- return fail2(`pad takes a number, [rows, columns] or { top, right, bottom, left }, not ${shown(n)}`);
1984
+ return fail2(`pad takes a number, [rows, columns] or { top, right, bottom, left }, not ${shown(n2)}`);
1886
1985
  const [top, right, bottom, left] = sides;
1887
1986
  return reshape("pad", p, p.cols + left + right, p.rows + top + bottom, (g, s2) => s2.paste(g, left, top));
1888
1987
  }
@@ -2069,7 +2168,7 @@ function optionsOf2(o, fx, keys) {
2069
2168
  fail2(`${fx}() has no option ${JSON.stringify(k)}${RENAMED[`${fx}.${k}`] ? "" : suggest(k, known)}: ${RENAMED[`${fx}.${k}`] ?? `it takes ${or2(known)}`}`);
2070
2169
  return o;
2071
2170
  }
2072
- var mod = (a, n) => (a % n + n) % n;
2171
+ var mod = (a, n2) => (a % n2 + n2) % n2;
2073
2172
  var gcd2 = (a, b2) => b2 ? gcd2(b2, a % b2) : a;
2074
2173
  function lcm(a, b2) {
2075
2174
  const x = Math.round(a * 100), y = Math.round(b2 * 100);
@@ -2079,14 +2178,14 @@ function lcm(a, b2) {
2079
2178
  return k * y <= 6e3 ? +(k * b2).toFixed(6) : void 0;
2080
2179
  }
2081
2180
  function quiet(t, busy, span) {
2082
- const n = 480;
2181
+ const n2 = 480;
2083
2182
  let from = -1;
2084
- for (let k = 0; k <= n; k++) {
2085
- const u = t + span * k / n;
2183
+ for (let k = 0; k <= n2; k++) {
2184
+ const u = t + span * k / n2;
2086
2185
  if (from < 0 && !busy(u))
2087
2186
  from = u;
2088
2187
  else if (from >= 0 && busy(u))
2089
- return +((from + u) / 2 - span / n / 2).toFixed(3);
2188
+ return +((from + u) / 2 - span / n2 / 2).toFixed(3);
2090
2189
  }
2091
2190
  return from < 0 ? t : +((from + t + span) / 2).toFixed(3);
2092
2191
  }
@@ -2152,7 +2251,7 @@ function mainInk(p, at3, options, map) {
2152
2251
  const most = (paper) => {
2153
2252
  const { text: text2, color } = snapshot(p, at3, { paper, options });
2154
2253
  const count = /* @__PURE__ */ new Map();
2155
- let best = 0, n = 0;
2254
+ let best = 0, n2 = 0;
2156
2255
  for (let k = 0, i = 0; k < text2.length; k++) {
2157
2256
  const c = text2.charCodeAt(k);
2158
2257
  if (c === 10)
@@ -2160,8 +2259,8 @@ function mainInk(p, at3, options, map) {
2160
2259
  if (c !== 32 && color && i < color.length) {
2161
2260
  const v = (count.get(color[i]) ?? 0) + 1;
2162
2261
  count.set(color[i], v);
2163
- if (v > n)
2164
- n = v, best = color[i];
2262
+ if (v > n2)
2263
+ n2 = v, best = color[i];
2165
2264
  }
2166
2265
  i++;
2167
2266
  }
@@ -2380,14 +2479,14 @@ function typeIn(src, options) {
2380
2479
  what: "typed in",
2381
2480
  setup: () => (t, s2, g) => {
2382
2481
  const local = cycle2 ? mod(t, cycle2) : t;
2383
- const n = Math.floor((local - start) * speed2 + 1e-6);
2482
+ const n2 = Math.floor((local - start) * speed2 + 1e-6);
2384
2483
  const all = g.chars.length;
2385
2484
  let shown2 = 0, next = -1, last = -1;
2386
2485
  for (let k = 0; k < all; k++) {
2387
2486
  const i = seq ? seq[k] : k;
2388
2487
  if (g.chars[i] === EMPTY)
2389
2488
  continue;
2390
- if (shown2 >= n) {
2489
+ if (shown2 >= n2) {
2391
2490
  next = i;
2392
2491
  break;
2393
2492
  }
@@ -2432,13 +2531,13 @@ function dissolve(src, options) {
2432
2531
  const edge = o.edge === "" ? "" : chars2(o.edge, "dissolve.edge", ".:", 1);
2433
2532
  const seed = num(o.seed, "dissolve.seed", 1, "whole");
2434
2533
  return build2("dissolve", src, o, ({ meta: m }) => {
2435
- const n = m.cols * m.rows, aspect = m.cell ?? 2;
2436
- const v = new Float64Array(n);
2534
+ const n2 = m.cols * m.rows, aspect = m.cell ?? 2;
2535
+ const v = new Float64Array(n2);
2437
2536
  for (let y = 0, i = 0; y < m.rows; y++)
2438
2537
  for (let x = 0; x < m.cols; x++, i++)
2439
2538
  v[i] = 0.8 * valueNoise(x * scale2, y * scale2 * aspect, seed) + 0.2 * hash(x, y, seed, 3);
2440
- const at3 = new Float32Array(n);
2441
- Int32Array.from({ length: n }, (_, i) => i).sort((a, b2) => v[a] - v[b2]).forEach((i, rank) => at3[i] = (rank + 0.5) / n);
2539
+ const at3 = new Float32Array(n2);
2540
+ Int32Array.from({ length: n2 }, (_, i) => i).sort((a, b2) => v[a] - v[b2]).forEach((i, rank) => at3[i] = (rank + 0.5) / n2);
2442
2541
  const band = edge ? 0.12 : 0;
2443
2542
  const codes = [...edge].map((c) => c.charCodeAt(0)), blocks = auto ? [9617, 9618] : codes;
2444
2543
  return {
@@ -2451,7 +2550,7 @@ function dissolve(src, options) {
2451
2550
  if (k >= 1)
2452
2551
  return copy(s2, g);
2453
2552
  const front = k * (1 + band);
2454
- for (let i = 0; i < n; i++) {
2553
+ for (let i = 0; i < n2; i++) {
2455
2554
  const ch = g.chars[i];
2456
2555
  if (ch === EMPTY)
2457
2556
  continue;
@@ -2625,7 +2724,7 @@ function wave(src, options) {
2625
2724
  const lines = rows2 ? h : w;
2626
2725
  for (let k = 0; k < lines; k++) {
2627
2726
  const off = room + Math.round(amplitude * Math.sin(TAU * (k / wavelength - t / period2)));
2628
- for (let j = 0, n = rows2 ? w : h; j < n; j++) {
2727
+ for (let j = 0, n2 = rows2 ? w : h; j < n2; j++) {
2629
2728
  const i = rows2 ? k * w + j : j * w + k;
2630
2729
  if (g.chars[i] === EMPTY)
2631
2730
  continue;
@@ -2637,8 +2736,8 @@ function wave(src, options) {
2637
2736
  }
2638
2737
  function hsl(h, s2, l) {
2639
2738
  const a = s2 * Math.min(l, 1 - l);
2640
- const f = (n) => {
2641
- const k = (n + h / 30) % 12;
2739
+ const f = (n2) => {
2740
+ const k = (n2 + h / 30) % 12;
2642
2741
  return Math.round(255 * (l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1))));
2643
2742
  };
2644
2743
  return "#" + [f(0), f(8), f(4)].map((v) => v.toString(16).padStart(2, "0")).join("");
@@ -2647,7 +2746,7 @@ var hues = {
2647
2746
  light: Array.from({ length: 12 }, (_, i) => hsl(i * 30, 0.8, 0.4)),
2648
2747
  dark: Array.from({ length: 12 }, (_, i) => hsl(i * 30, 0.9, 0.66))
2649
2748
  };
2650
- var cycle = (stops, n) => stops.length === 1 ? Array(n).fill(stops[0]) : gradient([...stops, stops[0]], n + 1).slice(0, n);
2749
+ var cycle = (stops, n2) => stops.length === 1 ? Array(n2).fill(stops[0]) : gradient([...stops, stops[0]], n2 + 1).slice(0, n2);
2651
2750
  function rainbowOf(fx, src, options, cyclesBy) {
2652
2751
  const o = optionsOf2(options, fx, ["colors", "steps", "period", "cycles", "direction"]);
2653
2752
  const steps = num(o.steps, `${fx}.steps`, 12, "steps");
@@ -2919,14 +3018,14 @@ var mix = (a, b2, k) => {
2919
3018
  const x = rgb(a), y = rgb(b2);
2920
3019
  return hex(x.map((v, i) => lerp(v, y[i], clamp(k))));
2921
3020
  };
2922
- function gradient(stops, n) {
3021
+ function gradient(stops, n2) {
2923
3022
  if (!Array.isArray(stops) || !stops.length)
2924
3023
  fail2("a gradient takes one or more colours as #rrggbb");
2925
- if (!Number.isInteger(n) || n < 1)
2926
- fail2(`a gradient takes a whole number of colours of 1 or more, not ${String(n)}`);
3024
+ if (!Number.isInteger(n2) || n2 < 1)
3025
+ fail2(`a gradient takes a whole number of colours of 1 or more, not ${String(n2)}`);
2927
3026
  const c = stops.map(rgb);
2928
- return Array.from({ length: n }, (_, i) => {
2929
- const k = n > 1 ? i / (n - 1) * (c.length - 1) : 0;
3027
+ return Array.from({ length: n2 }, (_, i) => {
3028
+ const k = n2 > 1 ? i / (n2 - 1) * (c.length - 1) : 0;
2930
3029
  const a = Math.min(c.length - 2, Math.floor(k));
2931
3030
  return c.length === 1 ? hex(c[0]) : hex(c[a].map((v, j) => lerp(v, c[a + 1][j], k - a)));
2932
3031
  });
@@ -3162,7 +3261,7 @@ function colorOf(v, what = "color") {
3162
3261
  function didYouMean(word) {
3163
3262
  const all = [.../* @__PURE__ */ new Set([...Object.keys(schemes), ...Object.keys(materialColors)])];
3164
3263
  const near = nearWords(word, all);
3165
- return near.length ? `did you mean ${near.map((n) => JSON.stringify(n)).join(" or ")}?` : `a colour of your own is #rrggbb, and the names are ${and2(all)}`;
3264
+ return near.length ? `did you mean ${near.map((n2) => JSON.stringify(n2)).join(" or ")}?` : `a colour of your own is #rrggbb, and the names are ${and2(all)}`;
3166
3265
  }
3167
3266
  function lettersApart(a, b2) {
3168
3267
  const d = Array.from({ length: b2.length + 1 }, (_, j) => j);
@@ -3230,14 +3329,14 @@ var SYNONYMS = {
3230
3329
  function nearWords(word, words2) {
3231
3330
  const w = word.toLowerCase().slice(0, 40);
3232
3331
  const same = (SYNONYMS[w] ?? []).filter((s2) => words2.includes(s2) && s2 !== word);
3233
- const close = words2.filter((n) => !same.includes(n) && lettersApart(w, n.toLowerCase()) <= (w.length <= 4 ? 1 : 2));
3332
+ const close = words2.filter((n2) => !same.includes(n2) && lettersApart(w, n2.toLowerCase()) <= (w.length <= 4 ? 1 : 2));
3234
3333
  return [...same, ...close].slice(0, 3);
3235
3334
  }
3236
3335
  function suggest(word, words2) {
3237
3336
  if (typeof word !== "string")
3238
3337
  return "";
3239
3338
  const near = nearWords(word, words2);
3240
- return near.length ? ` (did you mean ${near.map((n) => JSON.stringify(n)).join(" or ")}?)` : "";
3339
+ return near.length ? ` (did you mean ${near.map((n2) => JSON.stringify(n2)).join(" or ")}?)` : "";
3241
3340
  }
3242
3341
  function mergePalettes(parts) {
3243
3342
  const palette = [];
@@ -3426,8 +3525,8 @@ var Surface = class _Surface {
3426
3525
  const color = env.color;
3427
3526
  if (color && this.palette) {
3428
3527
  const ink = this.palette.ink(this.paper);
3429
- const n = Math.min(color.length, chars3.length);
3430
- for (let i = 0; i < n; i++)
3528
+ const n2 = Math.min(color.length, chars3.length);
3529
+ for (let i = 0; i < n2; i++)
3431
3530
  color[i] = colors[i] === NONE ? ink : colors[i];
3432
3531
  }
3433
3532
  const line2 = this.#line;
@@ -3515,8 +3614,8 @@ var Chainable = class {
3515
3614
  freeze(at3) {
3516
3615
  return freeze(this, at3);
3517
3616
  }
3518
- pad(n) {
3519
- return pad(this, n);
3617
+ pad(n2) {
3618
+ return pad(this, n2);
3520
3619
  }
3521
3620
  border(o) {
3522
3621
  return border(this, o);
@@ -3631,11 +3730,11 @@ function asPiece(src, name = "text") {
3631
3730
  const meta3 = checkMeta({ name, category: "type", note: name, cols: grid2.cols, rows: grid2.rows, fps: 0, ...palette ? { palette: palette.colors } : {} });
3632
3731
  const other = grid2.clone();
3633
3732
  if (palette?.themed) {
3634
- const n = palette.size;
3733
+ const n2 = palette.size;
3635
3734
  for (let i = 0; i < other.colors.length; i++) {
3636
3735
  const c = other.colors[i];
3637
3736
  if (c !== NONE)
3638
- other.colors[i] = grid2.paper ? c < n ? c + n : c : c >= n ? c - n : c;
3737
+ other.colors[i] = grid2.paper ? c < n2 ? c + n2 : c : c >= n2 ? c - n2 : c;
3639
3738
  }
3640
3739
  }
3641
3740
  other.paper = !grid2.paper;
@@ -3651,7 +3750,7 @@ function asPiece(src, name = "text") {
3651
3750
  lines.shift();
3652
3751
  while (lines.length > 1 && !lines[lines.length - 1].trim())
3653
3752
  lines.pop();
3654
- const indent = lines.reduce((n, l) => l.trim() ? Math.min(n, l.length - l.trimStart().length) : n, Infinity);
3753
+ const indent = lines.reduce((n2, l) => l.trim() ? Math.min(n2, l.length - l.trimStart().length) : n2, Infinity);
3655
3754
  if (Number.isFinite(indent))
3656
3755
  lines = lines.map((l) => l.slice(indent));
3657
3756
  }
@@ -3695,9 +3794,9 @@ function snapshot(src, t = 0, { paper = false, mono = false, options = {} } = {}
3695
3794
  // ../../node_modules/ascii.rest/dist/kit/math.js
3696
3795
  var said = (v) => typeof v === "string" ? JSON.stringify(v) : Array.isArray(v) ? `[${v.map(said).join(", ")}]` : String(v);
3697
3796
  var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
3698
- function mod2(a, n) {
3699
- const r = a % n;
3700
- return r < 0 ? r + n < n ? r + n : 0 : r + 0;
3797
+ function mod2(a, n2) {
3798
+ const r = a % n2;
3799
+ return r < 0 ? r + n2 < n2 ? r + n2 : 0 : r + 0;
3701
3800
  }
3702
3801
  var tables = [];
3703
3802
  function table(seed) {
@@ -3862,7 +3961,7 @@ var H3 = 1 / 6;
3862
3961
  var F4 = (Math.sqrt(5) - 1) / 4;
3863
3962
  var H4 = (5 - Math.sqrt(5)) / 20;
3864
3963
  var S4 = 27;
3865
- var unit = (n) => n <= -1 ? 0 : n >= 1 ? 1 : 0.5 + 0.5 * n;
3964
+ var unit = (n2) => n2 <= -1 ? 0 : n2 >= 1 ? 1 : 0.5 + 0.5 * n2;
3866
3965
  var dot4 = (g, a, b2, c, d) => G4[g] * a + G4[g + 1] * b2 + G4[g + 2] * c + G4[g + 3] * d;
3867
3966
  function simplex4(x, y, z, w, seed) {
3868
3967
  const { perm } = table(seed);
@@ -3905,26 +4004,26 @@ function simplex4(x, y, z, w, seed) {
3905
4004
  const x3 = x0 - i3 + 3 * H4, y3 = y0 - j3 + 3 * H4, z3 = z0 - l3 + 3 * H4, w3 = w0 - m3 + 3 * H4;
3906
4005
  const x4 = x0 - 1 + 4 * H4, y4 = y0 - 1 + 4 * H4, z4 = z0 - 1 + 4 * H4, w4 = w0 - 1 + 4 * H4;
3907
4006
  const ii = i & 255, jj = j & 255, ll = l & 255, mm = m & 255;
3908
- let n = 0, k;
4007
+ let n2 = 0, k;
3909
4008
  if ((k = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0) > 0)
3910
- k *= k, n += k * k * dot4((perm[ii + perm[jj + perm[ll + perm[mm]]]] & 31) * 4, x0, y0, z0, w0);
4009
+ k *= k, n2 += k * k * dot4((perm[ii + perm[jj + perm[ll + perm[mm]]]] & 31) * 4, x0, y0, z0, w0);
3911
4010
  if ((k = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1) > 0)
3912
- k *= k, n += k * k * dot4((perm[ii + i1 + perm[jj + j1 + perm[ll + l1 + perm[mm + m1]]]] & 31) * 4, x1, y1, z1, w1);
4011
+ k *= k, n2 += k * k * dot4((perm[ii + i1 + perm[jj + j1 + perm[ll + l1 + perm[mm + m1]]]] & 31) * 4, x1, y1, z1, w1);
3913
4012
  if ((k = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2) > 0)
3914
- k *= k, n += k * k * dot4((perm[ii + i2 + perm[jj + j2 + perm[ll + l2 + perm[mm + m2]]]] & 31) * 4, x2, y2, z2, w2);
4013
+ k *= k, n2 += k * k * dot4((perm[ii + i2 + perm[jj + j2 + perm[ll + l2 + perm[mm + m2]]]] & 31) * 4, x2, y2, z2, w2);
3915
4014
  if ((k = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3) > 0)
3916
- k *= k, n += k * k * dot4((perm[ii + i3 + perm[jj + j3 + perm[ll + l3 + perm[mm + m3]]]] & 31) * 4, x3, y3, z3, w3);
4015
+ k *= k, n2 += k * k * dot4((perm[ii + i3 + perm[jj + j3 + perm[ll + l3 + perm[mm + m3]]]] & 31) * 4, x3, y3, z3, w3);
3917
4016
  if ((k = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4) > 0)
3918
- k *= k, n += k * k * dot4((perm[ii + 1 + perm[jj + 1 + perm[ll + 1 + perm[mm + 1]]]] & 31) * 4, x4, y4, z4, w4);
3919
- return S4 * n;
4017
+ k *= k, n2 += k * k * dot4((perm[ii + 1 + perm[jj + 1 + perm[ll + 1 + perm[mm + 1]]]] & 31) * 4, x4, y4, z4, w4);
4018
+ return S4 * n2;
3920
4019
  }
3921
4020
  var OFF = Array.from({ length: 16 }, (_, i) => [i * 0.6180339887 % 1 * 97.3, i * 0.7548776662 % 1 * 89.1]);
3922
4021
  var octN = 4;
3923
4022
  var octLac = 2;
3924
4023
  var octGain = 0.5;
3925
4024
  var octSeed = 0;
3926
- function octaves(o, n = 4) {
3927
- octN = o?.octaves ?? n;
4025
+ function octaves(o, n2 = 4) {
4026
+ octN = o?.octaves ?? n2;
3928
4027
  octLac = o?.lacunarity ?? 2;
3929
4028
  octGain = o?.gain ?? 0.5;
3930
4029
  octSeed = o?.seed ?? 0;
@@ -3939,8 +4038,8 @@ function octaves(o, n = 4) {
3939
4038
  }
3940
4039
  var SPREAD = 1.45;
3941
4040
  var WIDEN4 = 1.35;
3942
- var crest = (n) => {
3943
- const r = 1 - (n < 0 ? -n : n);
4041
+ var crest = (n2) => {
4042
+ const r = 1 - (n2 < 0 ? -n2 : n2);
3944
4043
  return r < 0 ? 0 : r * r;
3945
4044
  };
3946
4045
  var weigh = (r) => r > 0.5 ? 1 : 2 * r;
@@ -3955,10 +4054,10 @@ function loopNoise(x, y, t, period2, o) {
3955
4054
  const z = radius * Math.cos(a), w = radius * Math.sin(a);
3956
4055
  let sum = 0, amp = 1, norm = 0, f = 1, weight = 1;
3957
4056
  for (let i = 0; i < octN; i++) {
3958
- let n = simplex4(x * f + OFF[i][0], y * f + OFF[i][1], z * f, w * f, octSeed) * WIDEN4;
4057
+ let n2 = simplex4(x * f + OFF[i][0], y * f + OFF[i][1], z * f, w * f, octSeed) * WIDEN4;
3959
4058
  if (ridge)
3960
- n = crest(n) * weight, weight = weigh(n);
3961
- sum += amp * n;
4059
+ n2 = crest(n2) * weight, weight = weigh(n2);
4060
+ sum += amp * n2;
3962
4061
  norm += amp;
3963
4062
  amp *= octGain;
3964
4063
  f *= octLac;
@@ -3971,14 +4070,14 @@ var B3 = B1 + 1;
3971
4070
  var E4 = TAU / 3;
3972
4071
  var E5 = TAU / 4.5;
3973
4072
  var bounce = (k) => {
3974
- const n = 7.5625, d = 2.75;
4073
+ const n2 = 7.5625, d = 2.75;
3975
4074
  if (k < 1 / d)
3976
- return n * k * k;
4075
+ return n2 * k * k;
3977
4076
  if (k < 2 / d)
3978
- return n * (k -= 1.5 / d) * k + 0.75;
4077
+ return n2 * (k -= 1.5 / d) * k + 0.75;
3979
4078
  if (k < 2.5 / d)
3980
- return n * (k -= 2.25 / d) * k + 0.9375;
3981
- return n * (k -= 2.625 / d) * k + 0.984375;
4079
+ return n2 * (k -= 2.25 / d) * k + 0.9375;
4080
+ return n2 * (k -= 2.625 / d) * k + 0.984375;
3982
4081
  };
3983
4082
  var curves = {
3984
4083
  linear: (k) => k,
@@ -4149,7 +4248,7 @@ function brush(ch, what) {
4149
4248
  fail2(`${what} takes one character, or "" to clear, not ${show3(ch)}`);
4150
4249
  return code(ch);
4151
4250
  }
4152
- var finite3 = (...v) => v.every((n) => typeof n === "number" && Number.isFinite(n));
4251
+ var finite3 = (...v) => v.every((n2) => typeof n2 === "number" && Number.isFinite(n2));
4153
4252
  function show3(v, depth = 0) {
4154
4253
  if (typeof v === "string")
4155
4254
  return JSON.stringify(v.length > 40 ? v.slice(0, 40) + "\u2026" : v);
@@ -4159,7 +4258,7 @@ function show3(v, depth = 0) {
4159
4258
  return String(v);
4160
4259
  if (depth > 1)
4161
4260
  return Array.isArray(v) ? "[...]" : "{...}";
4162
- const more = (n) => n > 6 ? ", ..." : "";
4261
+ const more = (n2) => n2 > 6 ? ", ..." : "";
4163
4262
  if (Array.isArray(v))
4164
4263
  return `[${Array.from(v.slice(0, 6), (x) => show3(x, depth + 1)).join(", ")}${more(v.length)}]`;
4165
4264
  const entries = Object.entries(v);
@@ -4219,12 +4318,12 @@ function trace(x0, y0, x1, y1, w, h) {
4219
4318
  const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0), sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
4220
4319
  if (XS.length < dx - dy + 1)
4221
4320
  XS = new Int32Array(dx - dy + 1), YS = new Int32Array(dx - dy + 1);
4222
- let err = dx + dy, n = 0;
4321
+ let err = dx + dy, n2 = 0;
4223
4322
  for (; ; ) {
4224
- XS[n] = x0;
4225
- YS[n++] = y0;
4323
+ XS[n2] = x0;
4324
+ YS[n2++] = y0;
4226
4325
  if (x0 === x1 && y0 === y1)
4227
- return n;
4326
+ return n2;
4228
4327
  const e2 = 2 * err;
4229
4328
  if (e2 >= dy)
4230
4329
  err += dy, x0 += sx;
@@ -4261,11 +4360,11 @@ function glyph(a, round, weight) {
4261
4360
  return ROUND.get(a);
4262
4361
  let c = GLYPH.get(a);
4263
4362
  if (c === void 0) {
4264
- let n = 0;
4363
+ let n2 = 0;
4265
4364
  for (let k = 0; k < 8; k += 2)
4266
4365
  if (a >> k & 3)
4267
- n |= weight << k;
4268
- c = GLYPH.get(n);
4366
+ n2 |= weight << k;
4367
+ c = GLYPH.get(n2);
4269
4368
  }
4270
4369
  return c ?? DOT;
4271
4370
  }
@@ -4283,14 +4382,14 @@ function linked(s2, j, b2, ascii) {
4283
4382
  const nx = x + (k === AR ? 1 : k === AL ? -1 : 0), ny = y + (k === AD ? 1 : k === AU ? -1 : 0);
4284
4383
  if (nx < 0 || ny < 0 || nx >= s2.cols || ny >= s2.rows)
4285
4384
  continue;
4286
- const n = s2.chars[ny * s2.cols + nx], nb = ascii ? asciiArms(n) : ARMS.get(n);
4385
+ const n2 = s2.chars[ny * s2.cols + nx], nb = ascii ? asciiArms(n2) : ARMS.get(n2);
4287
4386
  if (nb !== void 0 && nb >> (k ^ 4) & 3)
4288
4387
  m |= b2 & 3 << k;
4289
4388
  }
4290
4389
  return m;
4291
4390
  }
4292
- function place3(s2, j, c, k, join5, a = ARMS.get(c), ascii = false, round = false) {
4293
- if (join5 && a !== void 0) {
4391
+ function place3(s2, j, c, k, join7, a = ARMS.get(c), ascii = false, round = false) {
4392
+ if (join7 && a !== void 0) {
4294
4393
  const old = s2.chars[j], b2 = ascii ? asciiArms(old) : ARMS.get(old);
4295
4394
  if (b2 !== void 0) {
4296
4395
  const kept = linked(s2, j, b2, ascii);
@@ -4408,8 +4507,8 @@ function write(s2, x, y, str2, o, region) {
4408
4507
  let lines = layout(t, bw, wrap2);
4409
4508
  if (lines.length > bh)
4410
4509
  lines = lines.slice(0, bh);
4411
- const n = lines.length;
4412
- const top = valign === "top" ? fy : valign === "middle" ? fy + Math.floor((bh - n) / 2) : fy + bh - n;
4510
+ const n2 = lines.length;
4511
+ const top = valign === "top" ? fy : valign === "middle" ? fy + Math.floor((bh - n2) / 2) : fy + bh - n2;
4413
4512
  const lefts = lines.map((l) => align === "left" ? fx : align === "center" ? fx + Math.floor((bw - l.length) / 2) : fx + bw - l.length);
4414
4513
  return writeLines(s2, lines, lefts, top, k, transparent);
4415
4514
  }
@@ -4422,7 +4521,7 @@ function label(s2, x, y, str2, o) {
4422
4521
  if (!finite3(x, y) || t === "")
4423
4522
  return { x: finite3(x) ? Math.floor(x) : 0, y: finite3(y) ? Math.floor(y) : 0, cols: 0, rows: 0 };
4424
4523
  const lines = t.split("\n");
4425
- const start = (v, n) => Math.floor(v - n / 2 + 0.5);
4524
+ const start = (v, n2) => Math.floor(v - n2 / 2 + 0.5);
4426
4525
  return writeLines(s2, lines, lines.map((l) => start(x, l.length)), start(y, lines.length), k, transparent);
4427
4526
  }
4428
4527
  function around2(on, cx, cy, r, turn) {
@@ -4456,7 +4555,7 @@ function rect(s2, x, y, w, h, o) {
4456
4555
  if (title)
4457
4556
  for (const ch of title)
4458
4557
  code(ch);
4459
- const join5 = yes(o?.join, "join", true);
4558
+ const join7 = yes(o?.join, "join", true);
4460
4559
  const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
4461
4560
  if (!finite3(x, y, w, h))
4462
4561
  return { x: 0, y: 0, cols: 0, rows: 0 };
@@ -4479,10 +4578,10 @@ function rect(s2, x, y, w, h, o) {
4479
4578
  const put = (cx, cy, e, reach = 255) => {
4480
4579
  const j = s2.index(cx, cy), a = ascii ? EDGE_ARMS[e] : ARMS.get(ed[e]);
4481
4580
  if (j >= 0)
4482
- place3(s2, j, ed[e], k, join5, a === void 0 ? a : a & reach, ascii, round);
4581
+ place3(s2, j, ed[e], k, join7, a === void 0 ? a : a & reach, ascii, round);
4483
4582
  };
4484
- const end = (i, n, first, last) => n < 2 ? 255 : i === 0 ? 3 << first : i === n - 1 ? 3 << last : 255;
4485
- const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi, n) => Math.min(hi, n - o2);
4583
+ const end = (i, n2, first, last) => n2 < 2 ? 255 : i === 0 ? 3 << first : i === n2 - 1 ? 3 << last : 255;
4584
+ const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi, n2) => Math.min(hi, n2 - o2);
4486
4585
  if (h === 1)
4487
4586
  for (let i = from(x, 0); i < to(x, w, s2.cols); i++)
4488
4587
  put(x + i, y, 4, end(i, w, AR, AL));
@@ -4530,11 +4629,11 @@ function ellipseCells(w, h, cx, cy, rx, ry, inside, visit, aspect = 0, from = 0,
4530
4629
  const j0 = Math.max(0, Math.floor(cy - ry) - 1), j1 = Math.min(h - 1, Math.ceil(cy + ry));
4531
4630
  if (i0 > i1 || j0 > j1)
4532
4631
  return;
4533
- const bw = i1 - i0 + 1, n = bw * (j1 - j0 + 1);
4534
- if (EDGE.length < n)
4535
- EDGE = new Uint8Array(n);
4632
+ const bw = i1 - i0 + 1, n2 = bw * (j1 - j0 + 1);
4633
+ if (EDGE.length < n2)
4634
+ EDGE = new Uint8Array(n2);
4536
4635
  const edge = EDGE;
4537
- edge.fill(0, 0, n);
4636
+ edge.fill(0, 0, n2);
4538
4637
  const mark = (i2, j2, kind) => {
4539
4638
  if (i2 >= i0 && i2 <= i1 && j2 >= j0 && j2 <= j1)
4540
4639
  edge[(j2 - j0) * bw + i2 - i0] |= kind;
@@ -4558,7 +4657,7 @@ function ellipseCells(w, h, cx, cy, rx, ry, inside, visit, aspect = 0, from = 0,
4558
4657
  mark(Math.floor(cx - xo), j2, steep ? 2 : 8);
4559
4658
  mark(Math.ceil(cx + xo) - 1, j2, steep ? 2 : 8);
4560
4659
  }
4561
- for (let k = 0; k < n; k++)
4660
+ for (let k = 0; k < n2; k++)
4562
4661
  edge[k] = edge[k] & 3 | ((edge[k] & 12) === 12 ? 1 : 0);
4563
4662
  for (let j2 = j0; j2 <= j1; j2++)
4564
4663
  for (let i2 = i0; i2 <= i1; i2++) {
@@ -4615,11 +4714,11 @@ function shape(s2, cx, cy, rx, ry, o, what, from = 0, span = 1) {
4615
4714
  const ch = char === "auto" ? "auto" : brush(char, `${/^[aeiou]/.test(what) ? "an" : "a"} ${what}'s char`);
4616
4715
  const f = fill === void 0 || fill === false ? null : brush(fill, "fill");
4617
4716
  const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
4618
- const join5 = yes(o?.join, "join", true);
4717
+ const join7 = yes(o?.join, "join", true);
4619
4718
  ellipseCells(s2.cols, s2.rows, cx, cy, rx, ry, f !== null, (x, y, edge, slope) => {
4620
4719
  const j = y * s2.cols + x;
4621
4720
  if (edge)
4622
- place3(s2, j, ch === "auto" ? slope : ch, k, join5 && ch !== "auto");
4721
+ place3(s2, j, ch === "auto" ? slope : ch, k, join7 && ch !== "auto");
4623
4722
  else
4624
4723
  s2.put(j, f, f === EMPTY ? NONE : fk);
4625
4724
  }, ch === "auto" ? s2.aspect : 0, from, span);
@@ -4700,8 +4799,8 @@ var BrailleCanvas = class {
4700
4799
  }
4701
4800
  line(x0, y0, x1, y1, color) {
4702
4801
  const k = this.#k(color);
4703
- const n = trace(x0, y0, x1, y1, this.width, this.height);
4704
- for (let i = 0; i < n; i++)
4802
+ const n2 = trace(x0, y0, x1, y1, this.width, this.height);
4803
+ for (let i = 0; i < n2; i++)
4705
4804
  this.#dot(XS[i], YS[i], k);
4706
4805
  }
4707
4806
  ray(x, y, length, turn, color) {
@@ -4750,12 +4849,12 @@ var BrailleCanvas = class {
4750
4849
  y1 ??= 1;
4751
4850
  if (y0 === y1)
4752
4851
  fail2(`plot's y0 and y1 take two different numbers, the values at the bottom and the top, not ${y0} and ${y1}`);
4753
- const w = this.width, h = this.height, n = list2 ? list2.length : w;
4852
+ const w = this.width, h = this.height, n2 = list2 ? list2.length : w;
4754
4853
  const top = fill ? new Int32Array(w).fill(h) : null;
4755
4854
  let px = 0, py = NaN;
4756
- for (let i = 0; i < n; i++) {
4855
+ for (let i = 0; i < n2; i++) {
4757
4856
  const v = list2 ? list2[i] : f(x0 + (x1 - x0) * i / Math.max(1, w - 1));
4758
- const x = list2 ? n > 1 ? Math.round(i * (w - 1) / (n - 1)) : 0 : i;
4857
+ const x = list2 ? n2 > 1 ? Math.round(i * (w - 1) / (n2 - 1)) : 0 : i;
4759
4858
  const y = typeof v === "number" && Number.isFinite(v) ? Math.round((y1 - v) / (y1 - y0) * (h - 1)) : NaN;
4760
4859
  if (Number.isFinite(y)) {
4761
4860
  const m = Number.isFinite(py) ? trace(px, py, x, y, w, h) : trace(x, y, x, y, w, h);
@@ -4854,7 +4953,7 @@ function tones(accent, more = []) {
4854
4953
  var WIDGET = ["color", ...NAMING];
4855
4954
  var naming2 = (o, name, note) => ({ name: o.name ?? name, note: (o.note ?? note).slice(0, 72) });
4856
4955
  var framing = (p, framed) => framed ? Object.assign(p, { frames: true }) : p;
4857
- function amountOf(what, v, min, max, share) {
4956
+ function amountOf(what, v, min, max, share2) {
4858
4957
  if (v === void 0)
4859
4958
  return void 0;
4860
4959
  if (typeof v === "function") {
@@ -4870,10 +4969,10 @@ function amountOf(what, v, min, max, share) {
4870
4969
  return min + +m[1] / 100 * (max - min);
4871
4970
  }
4872
4971
  if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max) {
4873
- const hint = share && typeof v === "number" && v > 1 && v <= 100 ? `: did you mean ${+(v / 100).toFixed(4)}, or "${v}%"?` : "";
4972
+ const hint = share2 && typeof v === "number" && v > 1 && v <= 100 ? `: did you mean ${+(v / 100).toFixed(4)}, or "${v}%"?` : "";
4874
4973
  fail2(`${what}'s value takes a number from ${min} to ${max}, a percentage such as "64%", or a function that reads one, not ${show2(v)}${hint}`);
4875
4974
  }
4876
- if (!share && min === 0 && max === 100 && v > 0 && v < 1)
4975
+ if (!share2 && min === 0 && max === 100 && v > 0 && v < 1)
4877
4976
  fail2(`${what}'s value is a reading from 0 to 100, and ${v} is less than 1: did you mean ${+(v * 100).toFixed(2)}, or "${+(v * 100).toFixed(2)}%"? Give it max: 1 for a reading from 0 to 1`);
4878
4977
  return v;
4879
4978
  }
@@ -4894,24 +4993,24 @@ function progressBar(o) {
4894
4993
  const smooth = ease.inOutSine;
4895
4994
  const loop = loopOf(fill + 1, true);
4896
4995
  const cols = label2.length + width + (percent ? 5 : 0);
4897
- const share = (v) => (v - min) / (max - min);
4996
+ const share2 = (v) => (v - min) / (max - min);
4898
4997
  const timing = live ? { fps: 4, clock: true } : value === void 0 ? { fps: 24, loop, still: fill } : { fps: 0 };
4899
- const note = live ? "a progress bar, live" : value === void 0 ? "a progress bar filling" : `a progress bar at ${Math.round(share(value) * 100)}%`;
4998
+ const note = live ? "a progress bar, live" : value === void 0 ? "a progress bar filling" : `a progress bar at ${Math.round(share2(value) * 100)}%`;
4900
4999
  return piece({ ...naming2(p, label2.trim() || "progress", note), category: "ui", cols, rows: 1, ...timing, palette: tones(themed2("progressBar's color", p.color)) }, (t, s2) => {
4901
- const k = typeof value === "function" ? share(value()) : value !== void 0 ? share(value) : smooth(Math.min(1, (t % loop + loop) % loop / fill));
5000
+ const k = typeof value === "function" ? share2(value()) : value !== void 0 ? share2(value) : smooth(Math.min(1, (t % loop + loop) % loop / fill));
4902
5001
  const color = k >= 1 ? GOOD : ACCENT;
4903
5002
  s2.write(0, 0, label2);
4904
5003
  const x = label2.length;
4905
5004
  if (style === "ascii") {
4906
- const n = Math.round(k * (width - 2));
5005
+ const n2 = Math.round(k * (width - 2));
4907
5006
  s2.write(x, 0, "[", MUTED);
4908
5007
  for (let i = 0; i < width - 2; i++)
4909
- s2.set(x + 1 + i, 0, i < n ? "#" : "-", i < n ? color : MUTED);
5008
+ s2.set(x + 1 + i, 0, i < n2 ? "#" : "-", i < n2 ? color : MUTED);
4910
5009
  s2.write(x + width - 1, 0, "]", MUTED);
4911
5010
  } else if (style === "dots") {
4912
- const n = Math.round(k * width);
5011
+ const n2 = Math.round(k * width);
4913
5012
  for (let i = 0; i < width; i++)
4914
- s2.set(x + i, 0, i < n ? "\u25CF" : "\u25CB", i < n ? color : MUTED);
5013
+ s2.set(x + i, 0, i < n2 ? "\u25CF" : "\u25CB", i < n2 ? color : MUTED);
4915
5014
  } else {
4916
5015
  const eighths = Math.round(k * width * 8);
4917
5016
  for (let i = 0; i < width; i++) {
@@ -4957,7 +5056,7 @@ function sparkline(data, o) {
4957
5056
  [data, o] = [void 0, data];
4958
5057
  }
4959
5058
  const p = optionsOf3("sparkline()", o, ["label", "width", "height", "fill", ...WIDGET]);
4960
- const numbers = (v) => Array.isArray(v) && v.length >= 2 && v.every((n) => typeof n === "number" && Number.isFinite(n));
5059
+ const numbers = (v) => Array.isArray(v) && v.length >= 2 && v.every((n2) => typeof n2 === "number" && Number.isFinite(n2));
4961
5060
  const live = typeof data === "function" ? data : null;
4962
5061
  if (data !== void 0 && !live && !numbers(data))
4963
5062
  fail2(`sparkline() takes a list of two or more numbers, such as [3, 5, 2, 8], or a function that returns one, not ${show2(data)}`);
@@ -5111,7 +5210,7 @@ var meta2 = {
5111
5210
  };
5112
5211
  var QUAD = " \u2597\u2596\u2584\u259D\u2590\u259E\u259F\u2598\u259A\u258C\u2599\u2580\u259C\u259B\u2588";
5113
5212
  var quad = (ul, ur, ll, lr) => QUAD[ul << 3 | ur << 2 | ll << 1 | lr];
5114
- var ping = (n, i) => i % (2 * n - 2) < n ? i % (2 * n - 2) : 2 * n - 2 - i % (2 * n - 2);
5213
+ var ping = (n2, i) => i % (2 * n2 - 2) < n2 ? i % (2 * n2 - 2) : 2 * n2 - 2 - i % (2 * n2 - 2);
5115
5214
  var blank = (w, h) => Array.from({ length: h }, () => new Array(w).fill(" "));
5116
5215
  var rows = (g) => g.map((r) => r.join(""));
5117
5216
  var plot = (w, h, cells) => {
@@ -5143,8 +5242,8 @@ var SPINNERS = [
5143
5242
  }],
5144
5243
  ["arc", 1, (i) => {
5145
5244
  const ring2 = [[0, 0, "\u256D"], [1, 0, "\u2500"], [2, 0, "\u2500"], [3, 0, "\u2500"], [4, 0, "\u256E"], [4, 1, "\u2502"], [4, 2, "\u256F"], [3, 2, "\u2500"], [2, 2, "\u2500"], [1, 2, "\u2500"], [0, 2, "\u2570"], [0, 1, "\u2502"]];
5146
- const n = 3 + Math.round(ping(8, i) * 4 / 7);
5147
- return plot(5, 3, [...Array(n)].map((_, k) => ring2[(i + 12 - k) % 12]));
5245
+ const n2 = 3 + Math.round(ping(8, i) * 4 / 7);
5246
+ return plot(5, 3, [...Array(n2)].map((_, k) => ring2[(i + 12 - k) % 12]));
5148
5247
  }],
5149
5248
  ["bounce", 1, (i) => ["[" + " ".repeat(ping(6, i)) + "o" + " ".repeat(5 - ping(6, i)) + "]"]],
5150
5249
  ["clock", 3, (i) => {
@@ -5176,7 +5275,7 @@ var SPINNERS = [
5176
5275
  }]
5177
5276
  ];
5178
5277
  function spinners2({ names = meta2.options.names, labels = meta2.options.labels, speed: speed2 = meta2.options.speed } = {}) {
5179
- const chosen = names && names.length ? names.map((n) => SPINNERS.find((s2) => s2[0] === n)).filter(Boolean) : SPINNERS;
5278
+ const chosen = names && names.length ? names.map((n2) => SPINNERS.find((s2) => s2[0] === n2)).filter(Boolean) : SPINNERS;
5180
5279
  const all = chosen === SPINNERS;
5181
5280
  const slot2 = 11, per = all ? 6 : Math.min(6, chosen.length);
5182
5281
  const block = labels || all ? 5 : 3;
@@ -5184,8 +5283,8 @@ function spinners2({ names = meta2.options.names, labels = meta2.options.labels,
5184
5283
  const height = all ? meta2.rows : Math.ceil(chosen.length / per) * (block + 1) + 1;
5185
5284
  return (t) => {
5186
5285
  const grid2 = Array.from({ length: height }, () => new Array(cols).fill(" "));
5187
- chosen.forEach(([name, frames, draw2], n) => {
5188
- const x0 = n % per * slot2, y0 = 1 + Math.floor(n / per) * (block + 1);
5286
+ chosen.forEach(([name, frames, draw2], n2) => {
5287
+ const x0 = n2 % per * slot2, y0 = 1 + Math.floor(n2 / per) * (block + 1);
5189
5288
  const art = draw2(Math.floor(t * meta2.fps * speed2 / frames + 1e-3));
5190
5289
  const top = y0 + Math.floor((3 - art.length) / 2);
5191
5290
  art.forEach((line2, j) => {
@@ -5293,8 +5392,8 @@ function cached(cache, key, make) {
5293
5392
  return v;
5294
5393
  }
5295
5394
  var bars = /* @__PURE__ */ new Map();
5296
- function bar(share, width, opts2 = {}) {
5297
- const value = Math.max(0, Math.min(1, Number.isFinite(share) ? share : 0));
5395
+ function bar(share2, width, opts2 = {}) {
5396
+ const value = Math.max(0, Math.min(1, Number.isFinite(share2) ? share2 : 0));
5298
5397
  const w = Math.max(4, Math.round(width));
5299
5398
  const key = `${w}|${opts2.label ?? ""}|${opts2.percent !== false}|${value.toFixed(4)}`;
5300
5399
  const b2 = cached(bars, key, () => {
@@ -5330,12 +5429,12 @@ function dial(value, max, opts2 = {}) {
5330
5429
  return frameOf(g.piece, g.frame, 0);
5331
5430
  }
5332
5431
  var DIAL_SIZE = { cols: 25, rows: 12 };
5333
- function divider(n, label2) {
5334
- if (n <= 0) return "";
5335
- if (!label2) return "\u2500".repeat(n);
5432
+ function divider(n2, label2) {
5433
+ if (n2 <= 0) return "";
5434
+ if (!label2) return "\u2500".repeat(n2);
5336
5435
  const l = ` ${label2} `;
5337
- const side = Math.max(2, Math.floor((n - l.length) / 2));
5338
- return [..."\u2500".repeat(side) + l + "\u2500".repeat(n)].slice(0, n).join("");
5436
+ const side = Math.max(2, Math.floor((n2 - l.length) / 2));
5437
+ return [..."\u2500".repeat(side) + l + "\u2500".repeat(n2)].slice(0, n2).join("");
5339
5438
  }
5340
5439
 
5341
5440
  // src/args.ts
@@ -5526,9 +5625,9 @@ async function resolvePod(ctx, ref) {
5526
5625
  }
5527
5626
  function resolveCheckpoint(cps, ref) {
5528
5627
  if (/^#\d+$/.test(ref)) {
5529
- const n = Number(ref.slice(1));
5628
+ const n2 = Number(ref.slice(1));
5530
5629
  const sorted = [...cps].sort((a, b2) => b2.at - a.at);
5531
- const cp = sorted[n - 1];
5630
+ const cp = sorted[n2 - 1];
5532
5631
  if (!cp) throw new CliError(`no checkpoint ${ref}`, "list them with: airgate checkpoints <pod>");
5533
5632
  return cp;
5534
5633
  }
@@ -5656,13 +5755,13 @@ function duration2(seconds) {
5656
5755
  function hours(h) {
5657
5756
  return `${Math.round(h * 100) / 100}h`;
5658
5757
  }
5659
- function usd(n) {
5660
- return `$${n.toFixed(2)}`;
5758
+ function usd(n2) {
5759
+ return `$${n2.toFixed(2)}`;
5661
5760
  }
5662
- function bytes(n) {
5663
- if (n < 1024) return `${n} B`;
5664
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
5665
- return `${(n / 1024 / 1024).toFixed(1)} MiB`;
5761
+ function bytes(n2) {
5762
+ if (n2 < 1024) return `${n2} B`;
5763
+ if (n2 < 1024 * 1024) return `${(n2 / 1024).toFixed(1)} KiB`;
5764
+ return `${(n2 / 1024 / 1024).toFixed(1)} MiB`;
5666
5765
  }
5667
5766
  function date(ts) {
5668
5767
  if (!ts) return "-";
@@ -6132,8 +6231,8 @@ var US_PHONE_RE = /(?<![\w+.-])(?:1[-. ]?)?(?:\([2-9]\d{2}\)[ ]?|[2-9]\d{2}[-. ]
6132
6231
  var INTL_PHONE_RE = /(?<![\w+])\+\d{1,3}(?:[ .-]?\(?\d{1,5}\)?){1,6}(?![\w-]|\.\d)/g;
6133
6232
  var CUED_PHONE_RE = /\b(?:phone|tel|telephone|cell|mobile|fax|call|ph|whatsapp)\b\.?(?:[ \t]*(?:no\.?|number|#))?[ \t]*[:.]?[ \t]*(\+?\(?\d[\d \t().-]{5,}\d)/gi;
6134
6233
  var phoneDigitsOk = (min) => (s2) => {
6135
- const n = digitsOf(s2).length;
6136
- return n >= min && n <= 15;
6234
+ const n2 = digitsOf(s2).length;
6235
+ return n2 >= min && n2 <= 15;
6137
6236
  };
6138
6237
  var SSN_RE = /(?<![\d-])(?!000|666|9\d\d)\d{3}-(?!00)\d{2}-(?!0000)\d{4}(?![\d-])/g;
6139
6238
  var CUED_SSN_RE = /\b(?:SSN|social security(?:[ \t]+(?:number|no\.?))?)[ \t]*(?:#|no\.?|number)?[ \t]*[:.]?[ \t]*(\d{3}[ -]?\d{2}[ -]?\d{4})(?!\d)/gi;
@@ -6409,7 +6508,7 @@ var DETECTORS = [
6409
6508
  ];
6410
6509
 
6411
6510
  // ../mask/src/index.ts
6412
- var defaultPlaceholder = (kind, n) => `[${kind}_${n}]`;
6511
+ var defaultPlaceholder = (kind, n2) => `[${kind}_${n2}]`;
6413
6512
  var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
6414
6513
  function vaultKey(kind, value) {
6415
6514
  switch (kind) {
@@ -6456,9 +6555,9 @@ function createMasker(options = {}) {
6456
6555
  const key = vaultKey(kind, value);
6457
6556
  const existing = byKey.get(key);
6458
6557
  if (existing) return existing.placeholder;
6459
- const n = (counters.get(kind) ?? 0) + 1;
6460
- counters.set(kind, n);
6461
- const entry = { placeholder: makePlaceholder(kind, n), kind, original: value };
6558
+ const n2 = (counters.get(kind) ?? 0) + 1;
6559
+ counters.set(kind, n2);
6560
+ const entry = { placeholder: makePlaceholder(kind, n2), kind, original: value };
6462
6561
  byKey.set(key, entry);
6463
6562
  byLoose.set(looseKey(coreOf(entry.placeholder)), entry);
6464
6563
  unmaskRe = null;
@@ -6471,8 +6570,8 @@ function createMasker(options = {}) {
6471
6570
  const entry = { placeholder: e.placeholder, kind: e.kind, original: e.original };
6472
6571
  byKey.set(key, entry);
6473
6572
  byLoose.set(looseKey(coreOf(entry.placeholder)), entry);
6474
- const n = Number(/(\d+)\D*$/.exec(e.placeholder)?.[1] ?? 0);
6475
- counters.set(e.kind, Math.max(counters.get(e.kind) ?? 0, n));
6573
+ const n2 = Number(/(\d+)\D*$/.exec(e.placeholder)?.[1] ?? 0);
6574
+ counters.set(e.kind, Math.max(counters.get(e.kind) ?? 0, n2));
6476
6575
  }
6477
6576
  unmaskRe = null;
6478
6577
  }
@@ -6690,7 +6789,7 @@ var mask = {
6690
6789
  if (r.spans.length) {
6691
6790
  const kinds = /* @__PURE__ */ new Map();
6692
6791
  for (const e of masker.entries()) kinds.set(e.kind, (kinds.get(e.kind) ?? 0) + 1);
6693
- ctx.ui.err(ctx.ui.ce("dim", `masked ${r.spans.length} \xB7 ${[...kinds].map(([k, n]) => `${k.toLowerCase()} ${n}`).join(", ")}`));
6792
+ ctx.ui.err(ctx.ui.ce("dim", `masked ${r.spans.length} \xB7 ${[...kinds].map(([k, n2]) => `${k.toLowerCase()} ${n2}`).join(", ")}`));
6694
6793
  } else ctx.ui.err(ctx.ui.ce("dim", "nothing to mask."));
6695
6794
  }
6696
6795
  };
@@ -6724,7 +6823,7 @@ function attachSession(o) {
6724
6823
  return;
6725
6824
  }
6726
6825
  if (i > 0) send({ type: "input", data: s2.slice(0, i) });
6727
- finish({ reason: "detached" });
6826
+ finish2({ reason: "detached" });
6728
6827
  };
6729
6828
  const restore = () => {
6730
6829
  if (restored) return;
@@ -6745,7 +6844,7 @@ function attachSession(o) {
6745
6844
  } catch {
6746
6845
  }
6747
6846
  };
6748
- function finish(end) {
6847
+ function finish2(end) {
6749
6848
  restore();
6750
6849
  try {
6751
6850
  if (ws.readyState === 0 || ws.readyState === 1) ws.close(1e3, end.reason);
@@ -6754,7 +6853,7 @@ function attachSession(o) {
6754
6853
  settle(end);
6755
6854
  }
6756
6855
  function onTerm() {
6757
- finish({ reason: "closed", message: "signal" });
6856
+ finish2({ reason: "closed", message: "signal" });
6758
6857
  }
6759
6858
  try {
6760
6859
  stdin.setRawMode?.(true);
@@ -6782,14 +6881,14 @@ function attachSession(o) {
6782
6881
  return;
6783
6882
  }
6784
6883
  if (msg.type === "output" && typeof msg.data === "string") stdout.write(msg.data);
6785
- else if (msg.type === "exit") finish({ reason: "exit", code: typeof msg.code === "number" ? msg.code : 0 });
6884
+ else if (msg.type === "exit") finish2({ reason: "exit", code: typeof msg.code === "number" ? msg.code : 0 });
6786
6885
  else if (msg.type === "status" && msg.state === "waking") o.status?.("waking the pod\u2026");
6787
6886
  };
6788
6887
  ws.onerror = () => {
6789
- if (!restored) finish({ reason: "error", message: "connection error" });
6888
+ if (!restored) finish2({ reason: "error", message: "connection error" });
6790
6889
  };
6791
6890
  ws.onclose = (ev) => {
6792
- if (!restored) finish({ reason: "closed", code: ev?.code, message: ev?.reason });
6891
+ if (!restored) finish2({ reason: "closed", code: ev?.code, message: ev?.reason });
6793
6892
  };
6794
6893
  return done;
6795
6894
  }
@@ -6934,81 +7033,81 @@ async function readAll2(stream) {
6934
7033
  for await (const c of stream) s2 += c.toString();
6935
7034
  return s2;
6936
7035
  }
6937
- async function readValue(ctx, name) {
7036
+ async function readValue(ctx, name, pipeHint = `printf %s "$VALUE" | airgate secrets set ${name}`) {
6938
7037
  if (ctx.io.stdin.isTTY) {
6939
7038
  const v2 = await ctx.io.prompt(`value for ${name} (hidden): `, { hidden: true });
6940
7039
  if (!v2) throw new CliError("empty value, nothing saved");
6941
7040
  return v2;
6942
7041
  }
6943
7042
  const v = (await readAll2(ctx.io.stdin)).replace(/\r?\n$/, "");
6944
- if (!v) throw new CliError("no value on stdin", `printf %s "$VALUE" | airgate secrets set ${name}`);
7043
+ if (!v) throw new CliError("no value on stdin", pipeHint);
6945
7044
  return v;
6946
7045
  }
6947
- function parseDotenv(text2) {
6948
- const values = {};
6949
- const skipped = [];
6950
- for (const raw of text2.split(/\r?\n/)) {
6951
- const line2 = raw.trim();
6952
- if (!line2 || line2.startsWith("#")) continue;
6953
- const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line2);
6954
- if (!m) continue;
6955
- let v = m[2];
6956
- if (v.startsWith('"')) {
6957
- const end = v.lastIndexOf('"');
6958
- v = v.slice(1, end > 0 ? end : void 0).replace(/\\(["\\n])/g, (_s, c) => c === "n" ? "\n" : c);
6959
- } else if (v.startsWith("'")) {
6960
- const end = v.lastIndexOf("'");
6961
- v = v.slice(1, end > 0 ? end : void 0);
6962
- } else v = v.replace(/\s+#.*$/, "").trim();
6963
- if (NAME_RE.test(m[1]) && !m[1].startsWith("AIRGATE_")) values[m[1]] = v;
6964
- else skipped.push(m[1]);
6965
- }
6966
- return { values, skipped };
7046
+ function parseDotenv2(text2) {
7047
+ const r = parseDotenv(text2);
7048
+ return {
7049
+ values: r.values,
7050
+ skipped: r.entries.filter((e) => !e.name).map((e) => e.key),
7051
+ renamed: r.entries.filter((e) => e.name && e.name !== e.key).map((e) => `${e.key} \u2192 ${e.name}`)
7052
+ };
6967
7053
  }
6968
- async function sendDotenv(ctx, file, podId) {
6969
- const path = resolve3(ctx.io.cwd, file);
6970
- let text2;
7054
+ function readDotenvFile(ctx, file, hint) {
6971
7055
  try {
6972
- text2 = readFileSync2(path, "utf8");
7056
+ return readFileSync2(resolve3(ctx.io.cwd, file), "utf8");
6973
7057
  } catch {
6974
- throw new CliError(`can't read ${file}`, "airgate up --env path/to/.env");
7058
+ throw new CliError(`can't read ${file}`, hint);
6975
7059
  }
6976
- const { values, skipped } = parseDotenv(text2);
6977
- const sent = [];
6978
- const failed = [];
6979
- for (const [k, v] of Object.entries(values)) {
6980
- try {
6981
- await ctx.api.setSecret(k, v, podId);
6982
- sent.push(k);
6983
- } catch (e) {
6984
- if (e instanceof ApiError && e.status < 500) failed.push(`${k} (${e.message})`);
6985
- else throw e;
6986
- }
7060
+ }
7061
+ async function sendDotenv(ctx, file, podId) {
7062
+ const { values, skipped, renamed } = parseDotenv2(readDotenvFile(ctx, file, "airgate up --env path/to/.env"));
7063
+ if (!Object.keys(values).length) return { sent: [], skipped, renamed, failed: [] };
7064
+ try {
7065
+ const r = await ctx.api.setSecrets(values, podId);
7066
+ return { sent: [...r.created, ...r.updated], skipped, renamed, failed: [] };
7067
+ } catch (e) {
7068
+ if (e instanceof ApiError && e.status < 500) return { sent: [], skipped, renamed, failed: [e.message] };
7069
+ throw e;
6987
7070
  }
6988
- return { sent, skipped, failed };
6989
7071
  }
6990
7072
  var secrets = {
6991
7073
  name: "secrets",
6992
7074
  aliases: ["secret"],
6993
7075
  group: "pods",
6994
- usage: "[set NAME | rm NAME] [--pod p]",
7076
+ usage: "[set NAME | rm NAME | import FILE] [--pod p] [--dotenv]",
6995
7077
  summary: "environment variables for pods, stored encrypted, never shown again",
6996
- example: "airgate secrets set STRIPE_KEY --pod api",
7078
+ example: "airgate secrets import .env --pod api",
6997
7079
  notes: [
6998
7080
  "set reads the value from a hidden prompt, or from stdin when piped (never from the command line).",
6999
7081
  "account-wide unless --pod; a pod value overrides an account one. pods load them into ~/.airgate/secrets.env when they start or wake.",
7000
- "every value of 6+ characters is masked before it reaches a model, even if the agent reads it."
7082
+ "every value of 6+ characters is masked before it reaches a model, even if the agent reads it.",
7083
+ "import FILE saves every KEY=value in a .env file at once (quotes, export, multiline values); ls --dotenv prints the names as a .env.example."
7001
7084
  ],
7002
7085
  flags: {
7003
- pod: { type: "string", value: "pod", help: "just this pod (default: every pod)" }
7086
+ pod: { type: "string", value: "pod", help: "just this pod (default: every pod)" },
7087
+ dotenv: { type: "boolean", help: "ls: print the names as a .env.example (no values)" }
7004
7088
  },
7005
7089
  async run(ctx) {
7006
7090
  const [action, name, extra] = ctx.args.positionals;
7007
7091
  if (extra !== void 0) throw new UsageError(`unexpected "${extra}"`, "values are read from a hidden prompt or stdin, never from arguments");
7008
7092
  const podRef = str(ctx.args, "pod");
7009
7093
  const pod = podRef ? await resolvePod(ctx, podRef) : void 0;
7094
+ if (action === "import") {
7095
+ if (!name) throw new UsageError("which file? airgate secrets import .env [--pod p]");
7096
+ const r = await ctx.ui.spin(`saving ${name}`, sendDotenv(ctx, name, pod?.id));
7097
+ if (ctx.json) return ctx.ui.json(r);
7098
+ if (r.failed.length) throw new CliError(`nothing saved: ${r.failed.join("; ")}`);
7099
+ if (!r.sent.length) throw new CliError(`no KEY=value lines in ${name}`);
7100
+ ctx.ui.out(`${ctx.ui.c("ok", "saved")} ${r.sent.length} for ${pod ? pod.spec.name : "every pod"}: ${r.sent.join(", ")}`);
7101
+ if (r.renamed.length) ctx.ui.out(ctx.ui.c("dim", `renamed: ${r.renamed.join(", ")}`));
7102
+ if (r.skipped.length) ctx.ui.err(ctx.ui.ce("warn", `skipped (reserved or unusable names): ${r.skipped.join(", ")}`));
7103
+ return;
7104
+ }
7010
7105
  if (!action || action === "list" || action === "ls") {
7011
7106
  const all = await ctx.api.secrets();
7107
+ if (bool(ctx.args, "dotenv")) {
7108
+ const names = [...all.account.map((s2) => s2.name), ...pod ? (all.pods[pod.id] ?? []).map((s2) => s2.name) : []];
7109
+ return void ctx.io.stdout.write(dotenvExample([...new Set(names)].sort()));
7110
+ }
7012
7111
  if (ctx.json) return ctx.ui.json(pod ? { account: all.account, pod: all.pods[pod.id] ?? [] } : all);
7013
7112
  const podNames2 = new Map((await ctx.api.pods()).map((p) => [p.id, p.spec.name]));
7014
7113
  const rows2 = [];
@@ -7040,11 +7139,14 @@ var secrets = {
7040
7139
  ctx.ui.out(`removed ${name} from ${pod ? pod.spec.name : "the account"}. ${r.note}.`);
7041
7140
  return;
7042
7141
  }
7043
- throw new UsageError(`unknown action "${action}"`, "airgate secrets [set NAME | rm NAME] [--pod p]");
7142
+ throw new UsageError(`unknown action "${action}"`, "airgate secrets [set NAME | rm NAME | import FILE] [--pod p]");
7044
7143
  }
7045
7144
  };
7046
7145
 
7047
7146
  // src/commands/pods.ts
7147
+ import { execFileSync } from "node:child_process";
7148
+ import { readFileSync as readFileSync3 } from "node:fs";
7149
+ import { resolve as resolvePath } from "node:path";
7048
7150
  var AGENT_NAMES = Object.keys(AGENTS);
7049
7151
  var SIZE_NAMES = Object.keys(SIZES);
7050
7152
  var NETWORK_NAMES = ["allowlist", "open", "off"];
@@ -7060,9 +7162,9 @@ function cronProblem(cron) {
7060
7162
  if (!cronValid(cron)) return `"${cron}" isn't a 5-field cron (min hour day month weekday, utc)`;
7061
7163
  let t = cronNext(cron);
7062
7164
  for (let i = 0; i < 12 && t !== null; i++) {
7063
- const n = cronNext(cron, t);
7064
- if (n !== null && n - t < 15 * 6e4) return `"${cron}" runs more often than every 15 minutes`;
7065
- t = n;
7165
+ const n2 = cronNext(cron, t);
7166
+ if (n2 !== null && n2 - t < 15 * 6e4) return `"${cron}" runs more often than every 15 minutes`;
7167
+ t = n2;
7066
7168
  }
7067
7169
  return null;
7068
7170
  }
@@ -7125,7 +7227,7 @@ var pods = {
7125
7227
  var launch = {
7126
7228
  name: "launch",
7127
7229
  group: "pods",
7128
- usage: "[--name n] [--template t] [--agent a] [--memory m] [--browser] [--repo owner/name] [--cron c --task t \u2026] [--model m] [--no-mask] [--no-undo] [--network n]",
7230
+ usage: "[--name n] [--template t] [--agent a] [--memory m] [--browser] [--repo owner/name|.] [--env .env] [--cron c --task t \u2026] [--model m] [--no-mask] [--no-undo] [--network n]",
7129
7231
  summary: "start a new pod",
7130
7232
  example: "airgate launch --name fix-nav --agent codex --repo acme/web",
7131
7233
  notes: [
@@ -7143,7 +7245,8 @@ var launch = {
7143
7245
  "builtin-model": { type: "boolean", help: "autopilot with opencode: use airgate's built-in model (no subscription)" },
7144
7246
  model: { type: "string", value: "id", help: "built-in model: an openrouter id (default: the gateway's); see airgate templates" },
7145
7247
  memory: { type: "string", value: "size", choices: Object.keys(SIZES), default: "standard", help: `standard 8 GB | large 16 GB (pro, ${addOnPrice(ADD_ONS.large.usdPerHour)})` },
7146
- repo: { type: "string", value: "owner/name", help: "GitHub repo to clone (private ones: airgate github connect)" },
7248
+ repo: { type: "string", value: "owner/name", help: "GitHub repo to clone, a github link, or . for this folder's repo (private ones: airgate github connect)" },
7249
+ env: { type: "string", value: "file", help: "a .env file: its keys become this pod's secrets before it boots" },
7147
7250
  "public-repo": { type: "boolean", help: "the repo is public: don't check it against your GitHub App" },
7148
7251
  mask: { type: "boolean", default: true, help: "mask model traffic (--no-mask to turn off)" },
7149
7252
  undo: { type: "boolean", default: true, help: "checkpoint after every agent step (--no-undo to turn off)" },
@@ -7156,16 +7259,48 @@ var launch = {
7156
7259
  if (ctx.args.positionals.length) throw new UsageError(`unexpected "${ctx.args.positionals[0]}"`, "name the pod with --name");
7157
7260
  const spec = specFromFlags(ctx);
7158
7261
  if (spec.repo) await checkRepo(ctx, spec.repo);
7159
- const pod = await ctx.ui.spin(`launching ${spec.name}`, async () => waitReady(ctx, await ctx.api.launch(spec)));
7262
+ const envFile = str(ctx.args, "env");
7263
+ let secrets2;
7264
+ if (envFile) {
7265
+ const { values, skipped, renamed } = parseDotenv2(readDotenv(ctx, envFile));
7266
+ secrets2 = values;
7267
+ if (renamed.length) ctx.ui.err(ctx.ui.ce("dim", `renamed: ${renamed.join(", ")}`));
7268
+ if (skipped.length) ctx.ui.err(ctx.ui.ce("warn", `skipped (reserved or unusable names): ${skipped.join(", ")}`));
7269
+ }
7270
+ const pod = await ctx.ui.spin(`launching ${spec.name}`, async () => waitReady(ctx, await ctx.api.launch(spec, void 0, secrets2)));
7271
+ if (secrets2 && Object.keys(secrets2).length && !ctx.json) ctx.ui.out(`secrets for ${pod.spec.name}: ${Object.keys(secrets2).join(", ")}`);
7160
7272
  if (ctx.json) return ctx.ui.json(pod);
7161
7273
  ctx.ui.out(readyLine(ctx, pod));
7162
7274
  if ((pod.spec.template ?? "code") === "autopilot") {
7163
- const n = pod.spec.jobs?.length ?? 0;
7275
+ const n2 = pod.spec.jobs?.length ?? 0;
7164
7276
  if (!pod.spec.builtinModel) ctx.ui.out(`sign ${AGENTS[pod.spec.agent].label} in once: airgate attach ${pod.spec.name}`);
7165
- ctx.ui.out(n ? `${n} job${n === 1 ? "" : "s"}: airgate jobs ${pod.spec.name}` : `add a job: airgate job add ${pod.spec.name} --cron "0 2 * * *" --task "\u2026"`);
7277
+ ctx.ui.out(n2 ? `${n2} job${n2 === 1 ? "" : "s"}: airgate jobs ${pod.spec.name}` : `add a job: airgate job add ${pod.spec.name} --cron "0 2 * * *" --task "\u2026"`);
7166
7278
  } else ctx.ui.out(`next: airgate attach ${pod.spec.name}`);
7167
7279
  }
7168
7280
  };
7281
+ function repoArg(ctx, raw) {
7282
+ if (raw === void 0) return void 0;
7283
+ let input = raw;
7284
+ if (raw === ".") {
7285
+ try {
7286
+ input = execFileSync("git", ["remote", "get-url", "origin"], { cwd: ctx.io.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7287
+ } catch {
7288
+ throw new UsageError("--repo .: this folder has no git remote called origin");
7289
+ }
7290
+ }
7291
+ const s2 = input.trim().replace(/\.git$/, "").replace(/^git@github\.com:/, "").replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//, "");
7292
+ const [owner, name] = s2.split(/[/#?]/);
7293
+ const repo = owner && name ? `${owner}/${name}` : "";
7294
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) throw new UsageError(raw === "." ? `--repo .: origin isn't a github repo (${input})` : "--repo: owner/name or a github link");
7295
+ return repo;
7296
+ }
7297
+ function readDotenv(ctx, file) {
7298
+ try {
7299
+ return readFileSync3(resolvePath(ctx.io.cwd, file), "utf8");
7300
+ } catch {
7301
+ throw new CliError(`can't read ${file}`, "airgate launch --env path/to/.env");
7302
+ }
7303
+ }
7169
7304
  function specFromFlags(ctx, defaults = {}) {
7170
7305
  const a = ctx.args;
7171
7306
  const network = str(a, "network") ?? "allowlist";
@@ -7176,8 +7311,7 @@ function specFromFlags(ctx, defaults = {}) {
7176
7311
  autoStopMinutes = Number(autoStop);
7177
7312
  if (!Number.isInteger(autoStopMinutes) || autoStopMinutes < 1) throw new UsageError("--auto-stop: whole idle minutes \u2265 1, or keep-awake");
7178
7313
  }
7179
- const repo = str(a, "repo");
7180
- if (repo && !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) throw new UsageError("--repo: owner/name");
7314
+ const repo = repoArg(ctx, str(a, "repo"));
7181
7315
  const template = str(a, "template") ?? "code";
7182
7316
  const t = TEMPLATES[template];
7183
7317
  const agent = str(a, "agent") ?? (defaults.agent && t.agents.includes(defaults.agent) ? defaults.agent : t.agents[0]);
@@ -7307,7 +7441,6 @@ var destroy = {
7307
7441
  };
7308
7442
  var exec = {
7309
7443
  name: "exec",
7310
- aliases: ["run"],
7311
7444
  group: "pods",
7312
7445
  usage: "<pod> -- <command\u2026>",
7313
7446
  summary: "run a command in the pod (bash -c, in ~/workspace) and print its output",
@@ -7330,7 +7463,7 @@ var exec = {
7330
7463
  };
7331
7464
 
7332
7465
  // src/commands/jobs.ts
7333
- var price = (n) => `$${n.toFixed(2)}/h`;
7466
+ var price = (n2) => `$${n2.toFixed(2)}/h`;
7334
7467
  var templates = {
7335
7468
  name: "templates",
7336
7469
  group: "pods",
@@ -7528,199 +7661,16 @@ var log = {
7528
7661
  // src/commands/mcp.ts
7529
7662
  import { createInterface } from "node:readline";
7530
7663
 
7531
- // src/io.ts
7532
- import { spawn } from "node:child_process";
7533
- import { EventEmitter } from "node:events";
7534
- import { PassThrough } from "node:stream";
7535
- import { StringDecoder as StringDecoder2 } from "node:string_decoder";
7536
- var LineReader = class {
7537
- buf = "";
7538
- ended = false;
7539
- waiters = [];
7540
- stream;
7541
- attached = false;
7542
- decoder = new StringDecoder2("utf8");
7543
- constructor(stream) {
7544
- this.stream = stream;
7545
- }
7546
- attach() {
7547
- if (this.attached) return;
7548
- this.attached = true;
7549
- this.stream.on("data", (d) => {
7550
- this.buf += typeof d === "string" ? d : this.decoder.write(d);
7551
- this.wake();
7552
- });
7553
- this.stream.on("end", () => {
7554
- this.ended = true;
7555
- this.wake();
7556
- });
7557
- this.stream.on("error", () => {
7558
- this.ended = true;
7559
- this.wake();
7560
- });
7561
- }
7562
- wake() {
7563
- for (const w of this.waiters.splice(0)) w();
7564
- }
7565
- async line() {
7566
- this.attach();
7567
- this.stream.resume();
7568
- for (; ; ) {
7569
- const i = this.buf.indexOf("\n");
7570
- if (i !== -1) {
7571
- const l = this.buf.slice(0, i).replace(/\r$/, "");
7572
- this.buf = this.buf.slice(i + 1);
7573
- if (!this.buf) this.stream.pause();
7574
- return l;
7575
- }
7576
- if (this.ended) {
7577
- const rest = this.buf;
7578
- this.buf = "";
7579
- return rest ? rest : null;
7580
- }
7581
- await new Promise((r) => this.waiters.push(r));
7582
- }
7583
- }
7584
- };
7585
- function ttyPrompt(stdin, out) {
7586
- return (question, opts2 = {}) => new Promise((resolve5, reject) => {
7587
- out.write(question);
7588
- const decoder = new StringDecoder2("utf8");
7589
- let answer = "";
7590
- const wasRaw = Boolean(stdin.isRaw);
7591
- stdin.setRawMode?.(true);
7592
- stdin.resume();
7593
- const done = (err) => {
7594
- stdin.off("data", onData);
7595
- stdin.setRawMode?.(wasRaw);
7596
- stdin.pause();
7597
- out.write("\n");
7598
- if (err) reject(err);
7599
- else resolve5(answer);
7600
- };
7601
- const onData = (d) => {
7602
- for (const ch of decoder.write(d)) {
7603
- if (ch === "\r" || ch === "\n") return done();
7604
- if (ch === "") return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
7605
- if (ch === "") {
7606
- if (!answer) return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
7607
- continue;
7608
- }
7609
- if (ch === "\x7F" || ch === "\b") {
7610
- if (answer) {
7611
- answer = [...answer].slice(0, -1).join("");
7612
- if (!opts2.hidden) out.write("\b \b");
7613
- }
7614
- continue;
7615
- }
7616
- if (ch < " ") continue;
7617
- answer += ch;
7618
- if (!opts2.hidden) out.write(ch);
7619
- }
7620
- };
7621
- stdin.on("data", onData);
7622
- });
7623
- }
7624
- function openInBrowser(url) {
7625
- const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
7626
- return new Promise((resolve5) => {
7627
- try {
7628
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
7629
- child.on("error", () => resolve5(false));
7630
- child.on("spawn", () => {
7631
- child.unref();
7632
- resolve5(true);
7633
- });
7634
- } catch {
7635
- resolve5(false);
7636
- }
7637
- });
7638
- }
7639
- function processIo() {
7640
- const stdin = process.stdin;
7641
- const lines = new LineReader(stdin);
7642
- const tty = ttyPrompt(stdin, process.stderr);
7643
- return {
7644
- stdin,
7645
- stdout: process.stdout,
7646
- stderr: process.stderr,
7647
- env: process.env,
7648
- // `npm run cli -- up` runs in packages/cli; INIT_CWD is where the user typed it
7649
- cwd: process.env.AIRGATE_CWD || process.env.npm_lifecycle_event === "dev" && process.env.INIT_CWD || process.cwd(),
7650
- async prompt(q, o) {
7651
- if (stdin.isTTY) return tty(q, o);
7652
- process.stderr.write(q);
7653
- const l = await lines.line();
7654
- if (l === null) throw Object.assign(new Error("no input (stdin closed)"), { code: "no_input" });
7655
- process.stderr.write("\n");
7656
- return l;
7657
- },
7658
- openUrl: openInBrowser,
7659
- websocket: (url) => new WebSocket(url),
7660
- signals: process
7661
- };
7662
- }
7663
- function memoryIo(opts2 = {}) {
7664
- const out = new PassThrough();
7665
- const err = new PassThrough();
7666
- let stdout = "";
7667
- let stderr = "";
7668
- out.on("data", (d) => stdout += d.toString());
7669
- err.on("data", (d) => stderr += d.toString());
7670
- const answers = [...opts2.answers ?? []];
7671
- const opened = [];
7672
- const prompts = [];
7673
- const stdin = opts2.stdin ?? Object.assign(new PassThrough(), { isTTY: false });
7674
- if (!opts2.stdin) stdin.end();
7675
- const io = {
7676
- stdin,
7677
- stdout: out,
7678
- stderr: err,
7679
- env: opts2.env ?? {},
7680
- cwd: opts2.cwd ?? process.cwd(),
7681
- async prompt(q) {
7682
- prompts.push(q);
7683
- const a = answers.shift();
7684
- if (a === void 0) throw Object.assign(new Error(`no answer queued for prompt: ${q}`), { code: "no_input" });
7685
- return a;
7686
- },
7687
- async openUrl(u) {
7688
- opened.push(u);
7689
- return true;
7690
- },
7691
- websocket: () => {
7692
- throw new Error("no websocket in memoryIo");
7693
- },
7694
- signals: new EventEmitter()
7695
- };
7696
- return {
7697
- io,
7698
- answers,
7699
- opened,
7700
- prompts,
7701
- get stdout() {
7702
- return stdout;
7703
- },
7704
- get stderr() {
7705
- return stderr;
7706
- },
7707
- clear() {
7708
- stdout = "";
7709
- stderr = "";
7710
- }
7711
- };
7712
- }
7713
-
7714
7664
  // src/version.ts
7715
- var VERSION = "0.1.0";
7665
+ var VERSION = "0.1.3";
7716
7666
 
7717
7667
  // src/commands/updown.ts
7718
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
7668
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
7719
7669
  import { homedir as homedir2 } from "node:os";
7720
7670
  import { basename, dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve4 } from "node:path";
7721
7671
 
7722
7672
  // src/pack.ts
7723
- import { spawn as spawn2, spawnSync } from "node:child_process";
7673
+ import { spawn, spawnSync } from "node:child_process";
7724
7674
  import { existsSync, lstatSync } from "node:fs";
7725
7675
  import { join as join2 } from "node:path";
7726
7676
  var UPLOAD_LIMIT = 32 * 6 * 1024 * 1024;
@@ -7759,8 +7709,8 @@ function gitFiles(dir) {
7759
7709
  return out;
7760
7710
  }
7761
7711
  function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7762
- return new Promise((resolve5, reject) => {
7763
- const child = spawn2("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
7712
+ return new Promise((resolve6, reject) => {
7713
+ const child = spawn("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
7764
7714
  const chunks = [];
7765
7715
  let size2 = 0;
7766
7716
  let err = "";
@@ -7779,7 +7729,7 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7779
7729
  child.on("close", (code3) => {
7780
7730
  if (over2) return reject(new TooLargeError(limit));
7781
7731
  if (code3 !== 0) return reject(new Error(`tar failed: ${err.trim().split("\n").slice(-3).join(" ")}`));
7782
- resolve5(Buffer.concat(chunks));
7732
+ resolve6(Buffer.concat(chunks));
7783
7733
  });
7784
7734
  child.stdin.on("error", () => void 0);
7785
7735
  child.stdin.end(input ?? "");
@@ -7815,7 +7765,7 @@ import { gunzipSync } from "node:zlib";
7815
7765
 
7816
7766
  // src/sessions.ts
7817
7767
  import { createHash, randomUUID } from "node:crypto";
7818
- import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync, readFileSync as readFileSync3, readSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
7768
+ import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync, readFileSync as readFileSync4, readSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
7819
7769
  import { tmpdir } from "node:os";
7820
7770
  import { dirname, join as join3, relative } from "node:path";
7821
7771
  var POD_HOME = "/home/sprite";
@@ -7843,15 +7793,15 @@ function findClaudeSession(home, projectDir, id, env = {}) {
7843
7793
  const dir = join3(claudeHome(home, env), "projects", claudeKey(p));
7844
7794
  let names = [];
7845
7795
  try {
7846
- names = readdirSync(dir).filter((n) => n.endsWith(".jsonl"));
7796
+ names = readdirSync(dir).filter((n2) => n2.endsWith(".jsonl"));
7847
7797
  } catch {
7848
7798
  continue;
7849
7799
  }
7850
- for (const n of names) {
7851
- const sid = n.slice(0, -".jsonl".length);
7800
+ for (const n2 of names) {
7801
+ const sid = n2.slice(0, -".jsonl".length);
7852
7802
  if (id && sid !== id) continue;
7853
7803
  if (!ID_RE.test(sid)) continue;
7854
- const file = join3(dir, n);
7804
+ const file = join3(dir, n2);
7855
7805
  const side = join3(dir, sid);
7856
7806
  best.push({ agent: "claude-code", id: sid, file, mtime: statSync(file).mtimeMs, ...existsSync2(side) && statSync(side).isDirectory() ? { sidecar: side } : {} });
7857
7807
  }
@@ -7863,8 +7813,8 @@ function firstLine(file) {
7863
7813
  const fd = openSync(file, "r");
7864
7814
  try {
7865
7815
  const buf = Buffer.alloc(1024 * 1024);
7866
- const n = readSync(fd, buf, 0, buf.length, 0);
7867
- return buf.subarray(0, n).toString("utf8").split("\n")[0];
7816
+ const n2 = readSync(fd, buf, 0, buf.length, 0);
7817
+ return buf.subarray(0, n2).toString("utf8").split("\n")[0];
7868
7818
  } finally {
7869
7819
  closeSync(fd);
7870
7820
  }
@@ -7920,7 +7870,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
7920
7870
  const froms = spellings(projectDir);
7921
7871
  const stage = mkdtempSync(join3(tmpdir(), "airgate-session-"));
7922
7872
  try {
7923
- const raw = readFileSync3(s2.file);
7873
+ const raw = readFileSync4(s2.file);
7924
7874
  let podDir;
7925
7875
  const names = [];
7926
7876
  if (s2.agent === "claude-code") {
@@ -7936,7 +7886,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
7936
7886
  mkdirSync2(b2, { recursive: true });
7937
7887
  copy2(a, b2);
7938
7888
  } else if (x.isFile()) {
7939
- const body = readFileSync3(a);
7889
+ const body = readFileSync4(a);
7940
7890
  writeFileSync2(b2, /\.(jsonl?|txt|md)$/.test(x.name) ? rewriteAll(body.toString("utf8"), froms, podPath) : body);
7941
7891
  }
7942
7892
  }
@@ -7968,7 +7918,7 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
7968
7918
  let text2 = rewritePath(podContent.toString("utf8"), sent.podPath, sent.localPath);
7969
7919
  const localId = sent.localId ?? sent.id;
7970
7920
  if (localId !== sent.id) text2 = text2.split(sent.id).join(localId);
7971
- const local = existsSync2(sent.file) ? readFileSync3(sent.file) : null;
7921
+ const local = existsSync2(sent.file) ? readFileSync4(sent.file) : null;
7972
7922
  if (local && local.toString("utf8") === text2) return { agent: sent.agent, id: localId, file: sent.file, outcome: "same", sha256: sha256(local) };
7973
7923
  const atomic = (file2, body2) => {
7974
7924
  mkdirSync2(dirname(file2), { recursive: true });
@@ -7985,7 +7935,7 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
7985
7935
  let file;
7986
7936
  if (sent.agent === "claude-code") file = join3(dirname(sent.file), `${id}.jsonl`);
7987
7937
  else {
7988
- const p = (n) => String(n).padStart(2, "0");
7938
+ const p = (n2) => String(n2).padStart(2, "0");
7989
7939
  const root = join3(dirname(sent.file), "..", "..", "..");
7990
7940
  const stamp = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}T${p(now.getHours())}-${p(now.getMinutes())}-${p(now.getSeconds())}`;
7991
7941
  file = join3(root, String(now.getFullYear()), p(now.getMonth() + 1), p(now.getDate()), `rollout-${stamp}-${id}.jsonl`);
@@ -8031,11 +7981,11 @@ function unquote(p) {
8031
7981
  bytes2.push(...Buffer.from(c, "utf8"));
8032
7982
  continue;
8033
7983
  }
8034
- const n = p[++i];
8035
- if (/[0-7]/.test(n)) {
7984
+ const n2 = p[++i];
7985
+ if (/[0-7]/.test(n2)) {
8036
7986
  bytes2.push(parseInt(p.slice(i, i + 3), 8));
8037
7987
  i += 2;
8038
- } else bytes2.push(simple[n] ?? n.charCodeAt(0));
7988
+ } else bytes2.push(simple[n2] ?? n2.charCodeAt(0));
8039
7989
  }
8040
7990
  return Buffer.from(bytes2).toString("utf8");
8041
7991
  }
@@ -8171,7 +8121,7 @@ function findState(from) {
8171
8121
  const p = statePath(dir);
8172
8122
  if (existsSync3(p)) {
8173
8123
  try {
8174
- const s2 = JSON.parse(readFileSync4(p, "utf8"));
8124
+ const s2 = JSON.parse(readFileSync5(p, "utf8"));
8175
8125
  if (s2 && typeof s2.podId === "string" && typeof s2.dir === "string") return { root: dir, state: s2 };
8176
8126
  } catch {
8177
8127
  }
@@ -8187,7 +8137,7 @@ function excludeStateDir(projectDir) {
8187
8137
  if (r.status !== 0) return false;
8188
8138
  const raw = r.stdout.trim();
8189
8139
  const file = isAbsolute(raw) ? raw : join4(projectDir, raw);
8190
- const current = existsSync3(file) ? readFileSync4(file, "utf8") : "";
8140
+ const current = existsSync3(file) ? readFileSync5(file, "utf8") : "";
8191
8141
  if (current.split(/\r?\n/).some((l) => l.trim() === ".airgate/" || l.trim() === "/.airgate/" || l.trim() === ".airgate")) return true;
8192
8142
  mkdirSync3(dirname2(file), { recursive: true });
8193
8143
  appendFileSync(file, `${current && !current.endsWith("\n") ? "\n" : ""}# airgate up/down state
@@ -8196,8 +8146,8 @@ function excludeStateDir(projectDir) {
8196
8146
  return true;
8197
8147
  }
8198
8148
  function podDirName(projectDir) {
8199
- const n = basename(resolve4(projectDir)).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
8200
- return n && n !== "." && n !== ".." ? n.slice(0, 64) : "project";
8149
+ const n2 = basename(resolve4(projectDir)).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
8150
+ return n2 && n2 !== "." && n2 !== ".." ? n2.slice(0, 64) : "project";
8201
8151
  }
8202
8152
  var up = {
8203
8153
  name: "up",
@@ -8507,30 +8457,654 @@ ${check.stderr.trim()}`, "commit or stash local edits to those files, or look wi
8507
8457
  }
8508
8458
  };
8509
8459
 
8510
- // src/commands/mcp.ts
8511
- var PROTOCOL_VERSION = "2025-06-18";
8512
- var obj = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
8513
- var s = (description, extra = {}) => ({ type: "string", description, ...extra });
8514
- var b = (description) => ({ type: "boolean", description });
8515
- var POD = s("pod name or id (a unique prefix is enough)");
8516
- var need = (args, key) => {
8517
- const v = args[key];
8518
- if (typeof v !== "string" || !v.trim()) throw new UsageError(`${key} is required`);
8519
- return v;
8520
- };
8521
- async function runCommand(ctx, cmd, argv, cwd) {
8522
- const m = memoryIo({ env: ctx.io.env, cwd });
8523
- const args = parseArgs([...argv, "--json"], cmd.flags ?? {});
8524
- const sub = { ...makeCtx(m.io, createUi(m.io), args, ctx.fetch), api: ctx.api };
8525
- await cmd.run(sub);
8526
- const out = m.stdout.trim();
8527
- try {
8528
- return out ? JSON.parse(out) : { ok: true };
8529
- } catch {
8530
- return { output: out };
8531
- }
8532
- }
8533
- var jobSchema = obj(
8460
+ // src/io.ts
8461
+ import { spawn as spawn2 } from "node:child_process";
8462
+ import { EventEmitter } from "node:events";
8463
+ import { PassThrough } from "node:stream";
8464
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
8465
+ var LineReader = class {
8466
+ buf = "";
8467
+ ended = false;
8468
+ waiters = [];
8469
+ stream;
8470
+ attached = false;
8471
+ decoder = new StringDecoder2("utf8");
8472
+ constructor(stream) {
8473
+ this.stream = stream;
8474
+ }
8475
+ attach() {
8476
+ if (this.attached) return;
8477
+ this.attached = true;
8478
+ this.stream.on("data", (d) => {
8479
+ this.buf += typeof d === "string" ? d : this.decoder.write(d);
8480
+ this.wake();
8481
+ });
8482
+ this.stream.on("end", () => {
8483
+ this.ended = true;
8484
+ this.wake();
8485
+ });
8486
+ this.stream.on("error", () => {
8487
+ this.ended = true;
8488
+ this.wake();
8489
+ });
8490
+ }
8491
+ wake() {
8492
+ for (const w of this.waiters.splice(0)) w();
8493
+ }
8494
+ async line() {
8495
+ this.attach();
8496
+ this.stream.resume();
8497
+ for (; ; ) {
8498
+ const i = this.buf.indexOf("\n");
8499
+ if (i !== -1) {
8500
+ const l = this.buf.slice(0, i).replace(/\r$/, "");
8501
+ this.buf = this.buf.slice(i + 1);
8502
+ if (!this.buf) this.stream.pause();
8503
+ return l;
8504
+ }
8505
+ if (this.ended) {
8506
+ const rest = this.buf;
8507
+ this.buf = "";
8508
+ return rest ? rest : null;
8509
+ }
8510
+ await new Promise((r) => this.waiters.push(r));
8511
+ }
8512
+ }
8513
+ };
8514
+ function ttyPrompt(stdin, out) {
8515
+ return (question, opts2 = {}) => new Promise((resolve6, reject) => {
8516
+ out.write(question);
8517
+ const decoder = new StringDecoder2("utf8");
8518
+ let answer = "";
8519
+ const wasRaw = Boolean(stdin.isRaw);
8520
+ stdin.setRawMode?.(true);
8521
+ stdin.resume();
8522
+ const done = (err) => {
8523
+ stdin.off("data", onData);
8524
+ stdin.setRawMode?.(wasRaw);
8525
+ stdin.pause();
8526
+ out.write("\n");
8527
+ if (err) reject(err);
8528
+ else resolve6(answer);
8529
+ };
8530
+ const onData = (d) => {
8531
+ for (const ch of decoder.write(d)) {
8532
+ if (ch === "\r" || ch === "\n") return done();
8533
+ if (ch === "") return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
8534
+ if (ch === "") {
8535
+ if (!answer) return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
8536
+ continue;
8537
+ }
8538
+ if (ch === "\x7F" || ch === "\b") {
8539
+ if (answer) {
8540
+ answer = [...answer].slice(0, -1).join("");
8541
+ if (!opts2.hidden) out.write("\b \b");
8542
+ }
8543
+ continue;
8544
+ }
8545
+ if (ch < " ") continue;
8546
+ answer += ch;
8547
+ if (!opts2.hidden) out.write(ch);
8548
+ }
8549
+ };
8550
+ stdin.on("data", onData);
8551
+ });
8552
+ }
8553
+ function openInBrowser(url) {
8554
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
8555
+ return new Promise((resolve6) => {
8556
+ try {
8557
+ const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
8558
+ child.on("error", () => resolve6(false));
8559
+ child.on("spawn", () => {
8560
+ child.unref();
8561
+ resolve6(true);
8562
+ });
8563
+ } catch {
8564
+ resolve6(false);
8565
+ }
8566
+ });
8567
+ }
8568
+ function processIo() {
8569
+ const stdin = process.stdin;
8570
+ const lines = new LineReader(stdin);
8571
+ const tty = ttyPrompt(stdin, process.stderr);
8572
+ return {
8573
+ stdin,
8574
+ stdout: process.stdout,
8575
+ stderr: process.stderr,
8576
+ env: process.env,
8577
+ // `npm run cli -- up` runs in packages/cli; INIT_CWD is where the user typed it
8578
+ cwd: process.env.AIRGATE_CWD || process.env.npm_lifecycle_event === "dev" && process.env.INIT_CWD || process.cwd(),
8579
+ async prompt(q, o) {
8580
+ if (stdin.isTTY) return tty(q, o);
8581
+ process.stderr.write(q);
8582
+ const l = await lines.line();
8583
+ if (l === null) throw Object.assign(new Error("no input (stdin closed)"), { code: "no_input" });
8584
+ process.stderr.write("\n");
8585
+ return l;
8586
+ },
8587
+ openUrl: openInBrowser,
8588
+ websocket: (url) => new WebSocket(url),
8589
+ signals: process
8590
+ };
8591
+ }
8592
+ function memoryIo(opts2 = {}) {
8593
+ const out = new PassThrough();
8594
+ const err = new PassThrough();
8595
+ let stdout = "";
8596
+ let stderr = "";
8597
+ out.on("data", (d) => stdout += d.toString());
8598
+ err.on("data", (d) => stderr += d.toString());
8599
+ const answers = [...opts2.answers ?? []];
8600
+ const opened = [];
8601
+ const prompts = [];
8602
+ const stdin = opts2.stdin ?? Object.assign(new PassThrough(), { isTTY: false });
8603
+ if (!opts2.stdin) stdin.end();
8604
+ const io = {
8605
+ stdin,
8606
+ stdout: out,
8607
+ stderr: err,
8608
+ env: opts2.env ?? {},
8609
+ cwd: opts2.cwd ?? process.cwd(),
8610
+ async prompt(q) {
8611
+ prompts.push(q);
8612
+ const a = answers.shift();
8613
+ if (a === void 0) throw Object.assign(new Error(`no answer queued for prompt: ${q}`), { code: "no_input" });
8614
+ return a;
8615
+ },
8616
+ async openUrl(u) {
8617
+ opened.push(u);
8618
+ return true;
8619
+ },
8620
+ websocket: () => {
8621
+ throw new Error("no websocket in memoryIo");
8622
+ },
8623
+ signals: new EventEmitter()
8624
+ };
8625
+ return {
8626
+ io,
8627
+ answers,
8628
+ opened,
8629
+ prompts,
8630
+ get stdout() {
8631
+ return stdout;
8632
+ },
8633
+ get stderr() {
8634
+ return stderr;
8635
+ },
8636
+ clear() {
8637
+ stdout = "";
8638
+ stderr = "";
8639
+ }
8640
+ };
8641
+ }
8642
+
8643
+ // src/commands/sub.ts
8644
+ async function runCommand(ctx, cmd, argv, cwd) {
8645
+ const m = memoryIo({ env: ctx.io.env, cwd });
8646
+ const args = parseArgs([...argv, "--json"], cmd.flags ?? {});
8647
+ const sub = { ...makeCtx(m.io, createUi(m.io), args, ctx.fetch), api: ctx.api };
8648
+ await cmd.run(sub);
8649
+ const out = m.stdout.trim();
8650
+ try {
8651
+ return out ? JSON.parse(out) : { ok: true };
8652
+ } catch {
8653
+ return { output: out };
8654
+ }
8655
+ }
8656
+
8657
+ // src/commands/run.ts
8658
+ import { existsSync as existsSync5, readFileSync as readFileSync7, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
8659
+ import { basename as basename2, resolve as resolve5 } from "node:path";
8660
+ import { randomBytes } from "node:crypto";
8661
+
8662
+ // src/relay.ts
8663
+ import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
8664
+ import { join as join5 } from "node:path";
8665
+ function vendorOf(name) {
8666
+ const n2 = (name ?? "").toLowerCase();
8667
+ if (n2 === "claude" || n2 === "anthropic" || n2 === "claude-code") return "anthropic";
8668
+ if (n2 === "openai" || n2 === "codex" || n2 === "chatgpt") return "openai";
8669
+ return null;
8670
+ }
8671
+ var vendorLabel = (v) => v === "anthropic" ? "Claude" : "OpenAI";
8672
+ var kindOf = (v, value) => v === "anthropic" && value.startsWith("sk-ant-oat") ? "oauth_token" : "api_key";
8673
+ var modelsPath = (env) => join5(configDir(env), "models.json");
8674
+ function readSaved(env) {
8675
+ try {
8676
+ return JSON.parse(readFileSync6(modelsPath(env), "utf8"));
8677
+ } catch {
8678
+ return {};
8679
+ }
8680
+ }
8681
+ function saveLocal(env, vendor, value) {
8682
+ const all = readSaved(env);
8683
+ if (value) all[vendor] = { kind: kindOf(vendor, value), value };
8684
+ else delete all[vendor];
8685
+ const dir = configDir(env);
8686
+ mkdirSync4(dir, { recursive: true, mode: 448 });
8687
+ const file = modelsPath(env);
8688
+ const tmp = `${file}.${process.pid}.tmp`;
8689
+ writeFileSync4(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
8690
+ chmodSync2(tmp, 384);
8691
+ renameSync3(tmp, file);
8692
+ chmodSync2(file, 384);
8693
+ }
8694
+ function hasSavedFile(env) {
8695
+ return existsSync4(modelsPath(env));
8696
+ }
8697
+ function localAuth(env) {
8698
+ const saved = readSaved(env);
8699
+ const out = {};
8700
+ if (env.CLAUDE_CODE_OAUTH_TOKEN) out.anthropic = { kind: "oauth_token", value: env.CLAUDE_CODE_OAUTH_TOKEN, from: "CLAUDE_CODE_OAUTH_TOKEN" };
8701
+ else if (env.ANTHROPIC_API_KEY) out.anthropic = { kind: "api_key", value: env.ANTHROPIC_API_KEY, from: "ANTHROPIC_API_KEY" };
8702
+ else if (saved.anthropic) out.anthropic = { ...saved.anthropic, from: "this laptop" };
8703
+ if (env.OPENAI_API_KEY) out.openai = { kind: "api_key", value: env.OPENAI_API_KEY, from: "OPENAI_API_KEY" };
8704
+ else if (saved.openai) out.openai = { ...saved.openai, from: "this laptop" };
8705
+ return out;
8706
+ }
8707
+ function sign(vendor, auth, headers) {
8708
+ if (vendor === "openai") {
8709
+ headers.authorization = `Bearer ${auth.value}`;
8710
+ return;
8711
+ }
8712
+ if (auth.kind === "oauth_token") {
8713
+ delete headers["x-api-key"];
8714
+ headers.authorization = `Bearer ${auth.value}`;
8715
+ const beta = (headers["anthropic-beta"] ?? "").split(",").map((s2) => s2.trim()).filter(Boolean);
8716
+ if (!beta.includes("oauth-2025-04-20")) beta.push("oauth-2025-04-20");
8717
+ headers["anthropic-beta"] = beta.join(",");
8718
+ } else {
8719
+ delete headers.authorization;
8720
+ headers["x-api-key"] = auth.value;
8721
+ }
8722
+ }
8723
+ var UPSTREAM = { anthropic: "https://api.anthropic.com", openai: "https://api.openai.com" };
8724
+ function startRelay(o) {
8725
+ const vendors = Object.keys(o.auth).filter((v) => o.auth[v]);
8726
+ const inflight = /* @__PURE__ */ new Map();
8727
+ let ws = null;
8728
+ let stopped = false;
8729
+ let signed = 0;
8730
+ let attempt = 0;
8731
+ let resolveDone;
8732
+ const done = new Promise((r) => resolveDone = r);
8733
+ const base = (v) => o.io.env[v === "anthropic" ? "AIRGATE_RELAY_ANTHROPIC_URL" : "AIRGATE_RELAY_OPENAI_URL"] || UPSTREAM[v];
8734
+ const handle2 = async (sock, m) => {
8735
+ const send = (f) => sock.readyState === 1 && sock.send(JSON.stringify(f));
8736
+ const auth = o.auth[m.vendor];
8737
+ if (!auth) return send({ t: "error", id: m.id, message: `this laptop has no ${vendorLabel(m.vendor)} sign-in` });
8738
+ const headers = { ...m.headers };
8739
+ sign(m.vendor, auth, headers);
8740
+ const ctl = new AbortController();
8741
+ inflight.set(m.id, ctl);
8742
+ try {
8743
+ const res = await o.fetch(`${base(m.vendor)}${m.path}`, {
8744
+ method: m.method,
8745
+ headers,
8746
+ body: m.body ? Buffer.from(m.body, "base64") : void 0,
8747
+ signal: ctl.signal
8748
+ });
8749
+ const out = {};
8750
+ res.headers.forEach((v, k) => {
8751
+ if (!["content-encoding", "content-length", "transfer-encoding", "connection", "keep-alive"].includes(k)) out[k] = v;
8752
+ });
8753
+ send({ t: "head", id: m.id, status: res.status, headers: out });
8754
+ signed++;
8755
+ if (res.body) for await (const chunk of res.body) send({ t: "data", id: m.id, b64: Buffer.from(chunk).toString("base64") });
8756
+ send({ t: "end", id: m.id });
8757
+ } catch (e) {
8758
+ if (!ctl.signal.aborted) send({ t: "error", id: m.id, message: e instanceof Error ? e.message : String(e) });
8759
+ } finally {
8760
+ inflight.delete(m.id);
8761
+ }
8762
+ };
8763
+ const connect = () => {
8764
+ if (stopped) return;
8765
+ const sock = o.io.websocket(o.url);
8766
+ ws = sock;
8767
+ sock.onopen = () => {
8768
+ attempt = 0;
8769
+ sock.send(JSON.stringify({ t: "hello", vendors, client: `airgate ${VERSION}` }));
8770
+ };
8771
+ sock.onmessage = (ev) => {
8772
+ let m;
8773
+ try {
8774
+ m = JSON.parse(String(ev.data));
8775
+ } catch {
8776
+ return;
8777
+ }
8778
+ if (m.t === "req" && typeof m.id === "string") void handle2(sock, m);
8779
+ else if (m.t === "cancel" && typeof m.id === "string") inflight.get(m.id)?.abort();
8780
+ };
8781
+ sock.onerror = () => void 0;
8782
+ sock.onclose = (ev) => {
8783
+ for (const c of inflight.values()) c.abort();
8784
+ inflight.clear();
8785
+ if (stopped) return resolveDone();
8786
+ if (ev.code === 4401 || ev.code === 1008) {
8787
+ o.say("the gateway refused this laptop (sign in again: airgate login)");
8788
+ stopped = true;
8789
+ return resolveDone();
8790
+ }
8791
+ attempt++;
8792
+ setTimeout(connect, Math.min(15e3, 500 * 2 ** Math.min(attempt, 5)));
8793
+ };
8794
+ };
8795
+ connect();
8796
+ return {
8797
+ vendors,
8798
+ done,
8799
+ count: () => signed,
8800
+ stop() {
8801
+ stopped = true;
8802
+ try {
8803
+ ws?.close(1e3, "done");
8804
+ } catch {
8805
+ }
8806
+ resolveDone();
8807
+ }
8808
+ };
8809
+ }
8810
+
8811
+ // src/commands/models.ts
8812
+ var HOW = {
8813
+ anthropic: "a Claude subscription token (run `claude setup-token` and paste what it prints) or an Anthropic API key",
8814
+ openai: "an OpenAI API key"
8815
+ };
8816
+ function relayIfSignedIn(ctx, say) {
8817
+ const auth = localAuth(ctx.io.env);
8818
+ if (!Object.keys(auth).length || !ctx.api.signedIn) return null;
8819
+ try {
8820
+ return startRelay({ io: ctx.io, url: ctx.api.relayUrl(), fetch: ctx.fetch, auth, say: say ?? (() => void 0) });
8821
+ } catch {
8822
+ return null;
8823
+ }
8824
+ }
8825
+ var models = {
8826
+ name: "models",
8827
+ group: "account",
8828
+ usage: "[connect|disconnect <claude|openai> [--laptop]]",
8829
+ summary: "how pods' agents sign in to their model: in the pod, through this laptop, or connected to airgate",
8830
+ example: "airgate models connect claude --laptop",
8831
+ notes: [
8832
+ "three ways, pick per your comfort: sign in inside the pod (nothing stored with airgate); this laptop signs (airgate relay: airgate holds nothing, works while the laptop is connected); or connect a sign-in to airgate (encrypted on the gateway, never sent to a pod).",
8833
+ "connect <claude|openai>: saves it on airgate. with --laptop: saves it only on this laptop (~/.config/airgate/models.json), for airgate relay.",
8834
+ "claude takes a subscription token (claude setup-token) or an API key; openai takes an API key. the value is read from a hidden prompt or stdin, never from the command line.",
8835
+ "this laptop also uses CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY or OPENAI_API_KEY from its environment."
8836
+ ],
8837
+ flags: { laptop: { type: "boolean", help: "keep the sign-in on this laptop only (for airgate relay)" } },
8838
+ async run(ctx) {
8839
+ const { ui: ui2 } = ctx;
8840
+ const [action, who, extra] = ctx.args.positionals;
8841
+ if (extra) throw new UsageError(`unexpected "${extra}"`, "values are read from a hidden prompt or stdin, never from arguments");
8842
+ const laptop = bool(ctx.args, "laptop");
8843
+ if (action === "connect" || action === "disconnect") {
8844
+ const vendor = vendorOf(who);
8845
+ if (!vendor) throw new UsageError("which one? claude or openai", `airgate models ${action} claude`);
8846
+ if (action === "disconnect") {
8847
+ if (laptop) {
8848
+ saveLocal(ctx.io.env, vendor, null);
8849
+ if (ctx.json) return ui2.json({ ok: true, vendor, where: "laptop" });
8850
+ return void ui2.out(`${vendorLabel(vendor)}: removed from this laptop.`);
8851
+ }
8852
+ const r2 = await ctx.api.disconnectModel(vendor);
8853
+ if (ctx.json) return ui2.json({ ...r2, vendor, where: "airgate" });
8854
+ return void ui2.out(r2.ok ? `${vendorLabel(vendor)}: disconnected from airgate. airgate no longer holds it.` : `${vendorLabel(vendor)} wasn't connected to airgate.`);
8855
+ }
8856
+ if (!ctx.json && ctx.io.stdin.isTTY) ui2.err(ui2.ce("dim", `paste ${HOW[vendor]}.`));
8857
+ const value = (await readValue(ctx, vendor === "anthropic" ? "your Claude sign-in" : "your OpenAI key", `printf %s "$KEY" | airgate models connect ${who}${laptop ? " --laptop" : ""}`)).trim();
8858
+ if (value.length < 20 || /\s/.test(value)) throw new CliError("that doesn't look like a key or token", HOW[vendor]);
8859
+ if (laptop) {
8860
+ saveLocal(ctx.io.env, vendor, value);
8861
+ if (ctx.json) return ui2.json({ ok: true, vendor, kind: kindOf(vendor, value), where: "laptop" });
8862
+ ui2.out(`${ui2.c("ok", "saved")} on this laptop only (${kindOf(vendor, value) === "oauth_token" ? "subscription token" : "API key"}). airgate doesn't get it.`);
8863
+ return void ui2.out(ui2.c("dim", "pods use it while this laptop is connected: airgate relay (airgate run and airgate mcp do this on their own)."));
8864
+ }
8865
+ const r = await ctx.api.connectModel(vendor, value);
8866
+ if (ctx.json) return ui2.json({ ...r, where: "airgate" });
8867
+ ui2.out(`${ui2.c("ok", "connected")}: ${vendorLabel(vendor)} (${r.kind === "oauth_token" ? "subscription token" : "API key"}), kept encrypted on airgate and never sent to a pod.`);
8868
+ return void ui2.out(ui2.c("dim", `every pod's ${vendor === "anthropic" ? "Claude Code" : "Codex"} can use it now, unless signed in inside the pod. undo: airgate models disconnect ${who}`));
8869
+ }
8870
+ if (action) throw new UsageError(`unknown "${action}"`, "airgate models \xB7 airgate models connect claude [--laptop] \xB7 airgate models disconnect claude [--laptop]");
8871
+ const st = await ctx.api.modelsAuth();
8872
+ const local = localAuth(ctx.io.env);
8873
+ if (ctx.json) return ui2.json({ ...st, thisLaptop: Object.fromEntries(Object.entries(local).map(([v, a]) => [v, { kind: a.kind, from: a.from }])) });
8874
+ const rows2 = ["anthropic", "openai"].map((v) => {
8875
+ const c = st.connected.find((x) => x.vendor === v);
8876
+ const l = local[v];
8877
+ return [
8878
+ vendorLabel(v),
8879
+ c ? ui2.c("ok", `connected to airgate (${c.kind === "oauth_token" ? "subscription" : "API key"})`) : ui2.c("dim", "not on airgate"),
8880
+ l ? `this laptop: ${l.from}${st.laptop.connected && st.laptop.vendors.includes(v) ? ui2.c("ok", " \xB7 relaying") : ""}` : ui2.c("dim", "not on this laptop")
8881
+ ];
8882
+ });
8883
+ ui2.table(rows2, { head: ["model", "airgate", "laptop"] });
8884
+ ui2.out();
8885
+ ui2.out(ui2.c("dim", "a pod uses, in order: a sign-in made inside it \xB7 a connected laptop (airgate relay) \xB7 one connected to airgate."));
8886
+ if (!st.connected.length && !Object.keys(local).length) ui2.out(ui2.c("dim", "set one up: airgate models connect claude --laptop (airgate holds nothing) or airgate models connect claude."));
8887
+ if (!hasSavedFile(ctx.io.env) && Object.keys(local).length) ui2.out(ui2.c("dim", "this laptop signs from its environment variables."));
8888
+ }
8889
+ };
8890
+ var relay = {
8891
+ name: "relay",
8892
+ group: "account",
8893
+ usage: "",
8894
+ summary: "sign your pods' model calls from this laptop while it runs: airgate never holds your sign-in",
8895
+ example: "airgate relay",
8896
+ notes: [
8897
+ "uses this laptop's Claude / OpenAI sign-in (see airgate models). only pods with no sign-in of their own use it, and only while this runs.",
8898
+ "airgate run and airgate mcp start it on their own when this laptop has a sign-in."
8899
+ ],
8900
+ async run(ctx) {
8901
+ const { ui: ui2 } = ctx;
8902
+ const auth = localAuth(ctx.io.env);
8903
+ if (!Object.keys(auth).length) throw new CliError("no model sign-in on this laptop", "airgate models connect claude --laptop (or set CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY / OPENAI_API_KEY)");
8904
+ const r = startRelay({ io: ctx.io, url: ctx.api.relayUrl(), fetch: ctx.fetch, auth, say: (l) => ui2.err(ui2.ce("warn", l)) });
8905
+ const stop = () => r.stop();
8906
+ ctx.io.signals.on("SIGINT", stop);
8907
+ ctx.io.signal?.addEventListener("abort", stop);
8908
+ if (!ctx.json) {
8909
+ ui2.out(`relaying ${r.vendors.map(vendorLabel).join(" and ")} for your pods from this laptop. ${ui2.c("dim", "ctrl-c to stop.")}`);
8910
+ for (const v of r.vendors) ui2.out(ui2.c("dim", ` ${vendorLabel(v)}: ${auth[v].from}`));
8911
+ }
8912
+ try {
8913
+ await r.done;
8914
+ } finally {
8915
+ ctx.io.signals.off("SIGINT", stop);
8916
+ ctx.io.signal?.removeEventListener("abort", stop);
8917
+ }
8918
+ if (ctx.json) return ui2.json({ signed: r.count() });
8919
+ ui2.out(`stopped. ${r.count()} model call(s) signed.`);
8920
+ }
8921
+ };
8922
+
8923
+ // src/commands/run.ts
8924
+ var MODEL = {
8925
+ "claude-code": { vendor: "anthropic", secrets: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"] },
8926
+ codex: { vendor: "openai", secrets: ["OPENAI_API_KEY"] }
8927
+ };
8928
+ var shortId = () => randomBytes(2).toString("hex");
8929
+ var runName = (base) => `${base.slice(0, 52)}-run-${shortId()}`;
8930
+ async function signInWarning(ctx, agent, relaying) {
8931
+ const m = MODEL[agent];
8932
+ if (!m || relaying.includes(m.vendor)) return null;
8933
+ const st = await ctx.api.modelsAuth().catch(() => null);
8934
+ if (st?.connected.some((c) => c.vendor === m.vendor)) return null;
8935
+ const list2 = await ctx.api.secrets().catch(() => null);
8936
+ if (list2?.account.some((s2) => m.secrets.includes(s2.name))) return null;
8937
+ if (!st && !list2) return null;
8938
+ const who = m.vendor === "anthropic" ? "claude" : "openai";
8939
+ return `${AGENTS[agent].label} has no sign-in for a fresh pod. pick one: airgate models connect ${who} --laptop (this laptop signs; airgate holds nothing), or airgate models connect ${who} (kept on airgate)`;
8940
+ }
8941
+ async function finish(ctx, pod, keep) {
8942
+ if (keep === "destroy") await ctx.ui.spin(`deleting ${pod.spec.name}`, ctx.api.destroy(pod.id));
8943
+ else if (keep === "sleep") await ctx.ui.spin(`putting ${pod.spec.name} to sleep`, ctx.api.stop(pod.id)).catch(() => void 0);
8944
+ }
8945
+ var runTask = {
8946
+ name: "run",
8947
+ group: "up \xB7 down",
8948
+ usage: '"<task>" [dir] [--repo owner/name] [--agent a] [--env [.env]] [--keep] [--no-apply] [--pr] [--timeout min]',
8949
+ summary: "one task in a fresh pod: send the project, run it, bring the changes back, delete the pod",
8950
+ example: 'airgate run "get the failing tests passing"',
8951
+ notes: [
8952
+ "this folder (default): launches a pod, sends the project like `airgate up` (no conversation), hands the pod's agent the task, waits, applies the result here like `airgate down`, then deletes the pod.",
8953
+ "--no-apply prints the patch instead of applying it (the pod sleeps, so `airgate down --pod <it>` can still apply it); --keep keeps the pod (asleep).",
8954
+ "--repo owner/name (or a github link): runs on the repo itself instead (private repos need GitHub connected: airgate github connect). the work stays on a branch in the pod; --pr asks the agent to open a pull request, which waits in your airlock. the pod sleeps afterwards (free).",
8955
+ "if the task fails, the patch does not apply, or it runs past --timeout (default 60 minutes), the pod is kept so nothing is lost.",
8956
+ "the pod's agent needs a model sign-in: while run runs, this laptop signs for it if it has one (airgate models connect claude --laptop); or connect one to airgate (airgate models connect claude). see: airgate help models"
8957
+ ],
8958
+ flags: {
8959
+ repo: { type: "string", value: "owner/name", help: "run on a GitHub repo instead of this folder" },
8960
+ agent: { type: "string", value: "agent", choices: Object.keys(AGENTS), help: "the pod's agent (default claude-code)" },
8961
+ memory: { type: "string", value: "size", choices: Object.keys(SIZES), help: "pod size (default standard)" },
8962
+ env: { type: "string", value: "file", implicit: ".env", help: "send a dotenv file's keys as the pod's secrets (default .env)" },
8963
+ keep: { type: "boolean", help: "keep the pod afterwards (asleep) instead of deleting it" },
8964
+ "no-apply": { type: "boolean", help: "print the patch, change nothing here (the pod is kept, asleep)" },
8965
+ pr: { type: "boolean", help: "with --repo: have the agent open a pull request (it waits in your airlock)" },
8966
+ timeout: { type: "string", value: "minutes", default: "60", help: "stop waiting after this long (the pod keeps working)" },
8967
+ yes: { type: "boolean", short: "y", help: "don't ask" }
8968
+ },
8969
+ async run(ctx) {
8970
+ const { ui: ui2 } = ctx;
8971
+ const [task2, dirArg, extra] = ctx.args.positionals;
8972
+ if (!task2?.trim()) throw new UsageError("what should the agent do?", 'airgate run "get the failing tests passing"');
8973
+ if (extra) throw new UsageError(`unexpected "${extra}" (put the task in quotes)`);
8974
+ if (task2.length > 3500) throw new UsageError("the task is 3500 characters at most");
8975
+ const agent = str(ctx.args, "agent") ?? "claude-code";
8976
+ const size2 = str(ctx.args, "memory") ?? "standard";
8977
+ const minutes = Number(str(ctx.args, "timeout") ?? "60");
8978
+ if (!Number.isFinite(minutes) || minutes <= 0) throw new UsageError("--timeout: minutes, e.g. 30");
8979
+ const repo = str(ctx.args, "repo");
8980
+ const warnings = [];
8981
+ const relayer = relayIfSignedIn(ctx);
8982
+ if (relayer && !ctx.json) ui2.err(ui2.ce("dim", `this laptop signs the pod's ${relayer.vendors.map(vendorLabel).join(" / ")} calls while it runs (airgate holds nothing)`));
8983
+ const warn = await signInWarning(ctx, agent, relayer?.vendors ?? []);
8984
+ if (warn) {
8985
+ warnings.push(warn);
8986
+ if (!ctx.json) ui2.err(ui2.ce("warn", warn));
8987
+ }
8988
+ try {
8989
+ return await go();
8990
+ } finally {
8991
+ relayer?.stop();
8992
+ }
8993
+ async function go() {
8994
+ const work = async (pod2, text3) => {
8995
+ let r2 = await ui2.spin(`handing ${pod2.spec.name} the task`, ctx.api.startTask(pod2.id, text3));
8996
+ r2 = await ui2.spin(`${pod2.spec.name} is working (${AGENTS[agent].label})`, () => waitRun(ctx, pod2.id, r2, minutes * 6e4));
8997
+ const log2 = await ctx.api.runLog(pod2.id, r2.id).catch(() => "");
8998
+ return { run: r2, log: log2, done: r2.endedAt !== void 0, ok: r2.endedAt !== void 0 && r2.exitCode === 0 };
8999
+ };
9000
+ if (repo) {
9001
+ if (dirArg) throw new UsageError("a folder or --repo, not both");
9002
+ const look = await ctx.api.inspectRepo(repo).catch((e) => {
9003
+ if (e instanceof ApiError && e.status === 404) return null;
9004
+ throw e;
9005
+ });
9006
+ if (look && !look.reachable) {
9007
+ const gh = await ctx.api.github().catch(() => null);
9008
+ if (gh && !gh.configured) throw new CliError(`${look.repo} is private, and private repos need the Airgate GitHub App, which isn't set up on this gateway yet`, 'run it from a local clone instead: cd into it, then airgate run "\u2026"');
9009
+ throw new CliError(`airgate can't reach ${look.repo} (private, and not shared with Airgate)`, "connect GitHub and install the app on it: airgate github connect");
9010
+ }
9011
+ const name2 = runName((look?.repo ?? repo).split("/").pop() ?? "repo");
9012
+ const spec = { name: name2, agent, size: size2, repo: look?.repo ?? repo, masking: true, network: "allowlist", allow: DEFAULT_ALLOW, autoStopMinutes: null, autoCheckpoint: true };
9013
+ const pod2 = await ui2.spin(`launching ${name2} on ${spec.repo}`, async () => waitReady(ctx, await ctx.api.launch(spec)));
9014
+ const envFile2 = str(ctx.args, "env");
9015
+ if (envFile2) await ui2.spin(`sending ${envFile2} as secrets`, sendDotenv(ctx, envFile2, pod2.id));
9016
+ const text3 = task2 + (bool(ctx.args, "pr") ? '\n\nWhen you are done: commit on a new branch and open a pull request with `airgate pr --title "\u2026"` (it waits for my approval).' : "\n\nWhen you are done: commit your work on a new branch here and say which branch.");
9017
+ const r2 = await work(pod2, text3);
9018
+ await finish(ctx, pod2, r2.done ? "sleep" : "running");
9019
+ if (ctx.json) return ui2.json({ pod: { id: pod2.id, name: name2 }, repo: spec.repo, run: r2.run, ok: r2.ok, log: r2.log, pod_state: r2.done ? "sleeping" : "running", warnings });
9020
+ ctx.io.stdout.write(r2.log.endsWith("\n") || !r2.log ? r2.log : r2.log + "\n");
9021
+ if (!r2.done) ui2.err(ui2.ce("warn", `still working after ${minutes} min; it carries on. follow it: airgate log ${name2}`));
9022
+ ui2.out(`${r2.ok ? ui2.c("ok", "done") : ui2.c("bad", "failed")} on ${name2}${r2.done ? " (asleep now, free)" : ""}. look: airgate attach ${name2}${bool(ctx.args, "pr") ? " \xB7 the pull request: airgate airlock" : ""}`);
9023
+ return r2.ok ? 0 : 1;
9024
+ }
9025
+ const projectDir = resolve5(ctx.io.cwd, dirArg ?? ".");
9026
+ if (!existsSync5(projectDir) || !statSync3(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
9027
+ const noApply = bool(ctx.args, "no-apply");
9028
+ if (!noApply && !gitTopLevel(projectDir)) {
9029
+ throw new CliError(`${projectDir} isn't a git repo, so the result can't come back as a patch`, "git init && git add -A && git commit -m base, or use --no-apply");
9030
+ }
9031
+ const sp = statePath(projectDir);
9032
+ const before = existsSync5(sp) ? readFileSync7(sp, "utf8") : null;
9033
+ const restoreState = () => {
9034
+ try {
9035
+ if (before !== null) writeFileSync5(sp, before);
9036
+ else rmSync3(sp, { force: true });
9037
+ } catch {
9038
+ }
9039
+ };
9040
+ const name = runName(podDirName(projectDir));
9041
+ const upArgv = [projectDir, "--name", name, "--agent", agent, "--memory", size2, "--no-session", "--yes"];
9042
+ const envFile = str(ctx.args, "env");
9043
+ if (envFile) upArgv.push("--env", envFile);
9044
+ const sent = await ui2.spin(`sending ${basename2(projectDir)} to a fresh pod`, runCommand(ctx, up, upArgv, ctx.io.cwd));
9045
+ const pod = sent.pod;
9046
+ const text2 = `Work in ~/${sent.dir} (a project sent from my laptop). ${task2}
9047
+
9048
+ When you are done, leave your changes in the files there (no need to commit or push): they are brought back to my laptop as a patch.`;
9049
+ let r;
9050
+ try {
9051
+ r = await work(pod, text2);
9052
+ } catch (e) {
9053
+ await finish(ctx, pod, "sleep");
9054
+ throw e;
9055
+ }
9056
+ const keepNote = (why) => `${why}. the pod is kept (asleep): airgate attach ${name} \xB7 bring it back: airgate down --pod ${name} \xB7 delete it: airgate destroy ${name}`;
9057
+ const out = (o) => ui2.json({ pod: { id: pod.id, name }, run: r.run, ok: r.ok, log: r.log, warnings, ...o });
9058
+ if (!r.ok) {
9059
+ await finish(ctx, pod, r.done ? "sleep" : "running");
9060
+ if (ctx.json) return out({ applied: false, pod_state: r.done ? "sleeping" : "running" });
9061
+ ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
9062
+ ui2.err(ui2.ce("bad", r.done ? keepNote(`the task failed (exit ${r.run.exitCode ?? "?"})`) : `still working after ${minutes} min; it carries on. follow it: airgate log ${name}, then: airgate down --pod ${name}`));
9063
+ return 1;
9064
+ }
9065
+ let result;
9066
+ try {
9067
+ result = await ui2.spin(`bringing the changes back from ${name}`, runCommand(ctx, down2, ["--pod", name, "--no-session", ...noApply ? ["--dry-run"] : ["--yes"]], projectDir));
9068
+ } catch (e) {
9069
+ await finish(ctx, pod, "sleep");
9070
+ const why = e instanceof Error ? e.message.split("\n")[0] : String(e);
9071
+ if (ctx.json) return out({ applied: false, error: why, pod_state: "sleeping" });
9072
+ ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
9073
+ ui2.err(ui2.ce("bad", keepNote(`the task finished, but its changes didn't come back: ${why}`)));
9074
+ return 1;
9075
+ }
9076
+ const files = result.files ?? [];
9077
+ const keep = noApply || bool(ctx.args, "keep");
9078
+ await finish(ctx, pod, keep ? "sleep" : "destroy");
9079
+ if (!keep) restoreState();
9080
+ if (ctx.json) return out({ applied: !noApply && files.length > 0, files, ...noApply ? { patch: result.patch ?? "" } : {}, pod_state: keep ? "sleeping" : "deleted" });
9081
+ ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
9082
+ if (noApply) {
9083
+ ctx.io.stdout.write(result.patch ?? "");
9084
+ ui2.err(`${files.length} file(s) changed, not applied. apply: airgate down --pod ${name} \xB7 delete the pod: airgate destroy ${name}`);
9085
+ return 0;
9086
+ }
9087
+ const risky = files.filter((f) => f.risky).length;
9088
+ ui2.out(
9089
+ `${ui2.c("ok", "done")}: ${files.length ? `${files.length} file(s) changed here${risky ? `, ${risky} risky` : ""}. review: git status \xB7 git diff` : "no changes came back"}` + (keep ? ` \xB7 the pod is kept (asleep): ${name}` : " \xB7 the pod is deleted")
9090
+ );
9091
+ return 0;
9092
+ }
9093
+ }
9094
+ };
9095
+
9096
+ // src/commands/mcp.ts
9097
+ var PROTOCOL_VERSION = "2025-06-18";
9098
+ var obj = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
9099
+ var s = (description, extra = {}) => ({ type: "string", description, ...extra });
9100
+ var b = (description) => ({ type: "boolean", description });
9101
+ var POD = s("pod name or id (a unique prefix is enough)");
9102
+ var need = (args, key) => {
9103
+ const v = args[key];
9104
+ if (typeof v !== "string" || !v.trim()) throw new UsageError(`${key} is required`);
9105
+ return v;
9106
+ };
9107
+ var jobSchema = obj(
8534
9108
  {
8535
9109
  id: s("keep an existing job: its id"),
8536
9110
  name: s("a name to show"),
@@ -8540,6 +9114,9 @@ var jobSchema = obj(
8540
9114
  },
8541
9115
  ["cron", "task"]
8542
9116
  );
9117
+ var OWN = /* @__PURE__ */ new Map();
9118
+ var n = (description) => ({ type: "number", description });
9119
+ var dirOf = (ctx, a) => typeof a.dir === "string" && a.dir ? a.dir : ctx.io.cwd;
8543
9120
  var TOOLS = [
8544
9121
  {
8545
9122
  name: "pods_list",
@@ -8589,6 +9166,7 @@ var TOOLS = [
8589
9166
  }
8590
9167
  const spec = specFromFlags({ ...ctx, args: parseArgs(argv, launch.flags ?? {}) });
8591
9168
  const pod = await waitReady(ctx, await ctx.api.launch(spec));
9169
+ OWN.set(pod.id, "launch");
8592
9170
  return { name: pod.spec.name, id: pod.id, state: pod.state, readyMs: pod.readyMs, spec: pod.spec };
8593
9171
  }
8594
9172
  },
@@ -8709,6 +9287,119 @@ var TOOLS = [
8709
9287
  if (typeof a.pod === "string" && a.pod) argv.push("--pod", a.pod);
8710
9288
  return runCommand(ctx, down2, argv, typeof a.dir === "string" && a.dir ? a.dir : ctx.io.cwd);
8711
9289
  }
9290
+ },
9291
+ {
9292
+ name: "pod_run",
9293
+ description: "One task in a fresh pod, start to finish: sends the local project (like pod_up, without the conversation), the pod's agent does the task, the changes come back into the local repo as a patch (pod_down's checks), and the pod is deleted. With repo: runs on a GitHub repo instead (private ones need GitHub connected); the work stays on a branch (or a PR with pr, which waits in the airlock) and the pod sleeps. On failure the pod is kept, asleep. Can take many minutes: for long work prefer pod_up or pod_launch + task_start and check back with run_status.",
9294
+ inputSchema: obj(
9295
+ {
9296
+ task: s("what the agent should do"),
9297
+ dir: s("the local project directory (default: where the server started)"),
9298
+ repo: s("run on this GitHub repo (owner/name) instead of the local project"),
9299
+ agent: s("claude-code | codex | opencode | pi (default claude-code)"),
9300
+ apply: b("false: return the patch without applying it (the pod is kept, asleep)"),
9301
+ keep: b("keep the pod afterwards (asleep) instead of deleting it"),
9302
+ pr: b("with repo: open a pull request at the end (it waits in the airlock)"),
9303
+ env: s("a dotenv file whose keys become the pod's secrets, e.g. .env"),
9304
+ timeoutMinutes: n("stop waiting after this many minutes (default 60); the pod keeps working")
9305
+ },
9306
+ ["task"]
9307
+ ),
9308
+ async call(ctx, a) {
9309
+ const argv = [need(a, "task")];
9310
+ const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
9311
+ opt("repo", a.repo);
9312
+ opt("agent", a.agent);
9313
+ opt("env", a.env);
9314
+ if (a.apply === false) argv.push("--no-apply");
9315
+ if (a.keep === true) argv.push("--keep");
9316
+ if (a.pr === true) argv.push("--pr");
9317
+ if (typeof a.timeoutMinutes === "number") argv.push("--timeout", String(a.timeoutMinutes));
9318
+ const out = await runCommand(ctx, runTask, argv, dirOf(ctx, a));
9319
+ if (out.pod?.id && out.pod_state !== "deleted") OWN.set(out.pod.id, a.repo ? "launch" : "project");
9320
+ return out;
9321
+ }
9322
+ },
9323
+ {
9324
+ name: "pod_up",
9325
+ description: "Move the local project into a pod: its files (git-tracked and untracked-but-not-ignored), and by default the latest Claude Code / Codex conversation here, which resumes in the pod. Launches a new pod named after the folder unless pod is given. Bring the work back later with pod_diff / pod_down.",
9326
+ inputSchema: obj({
9327
+ dir: s("the local project directory (default: where the server started)"),
9328
+ pod: s("send into this existing pod instead of launching one"),
9329
+ name: s("name for a new pod"),
9330
+ agent: s("agent for a new pod: claude-code | codex | opencode | pi"),
9331
+ env: s("a dotenv file whose keys become the pod's secrets, e.g. .env"),
9332
+ conversation: b("false: don't send the agent conversation")
9333
+ }),
9334
+ async call(ctx, a) {
9335
+ const argv = ["--yes"];
9336
+ const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
9337
+ opt("pod", a.pod);
9338
+ opt("name", a.name);
9339
+ opt("agent", a.agent);
9340
+ opt("env", a.env);
9341
+ if (a.conversation === false) argv.push("--no-session");
9342
+ const out = await runCommand(ctx, up, argv, dirOf(ctx, a));
9343
+ if (!a.pod && out.pod?.id) OWN.set(out.pod.id, "project");
9344
+ return out;
9345
+ }
9346
+ },
9347
+ {
9348
+ name: "pod_sleep",
9349
+ description: "Put a pod to sleep: it stops, keeps its files and checkpoints, and costs nothing until woken. Do this when you are done with a pod you launched.",
9350
+ inputSchema: obj({ pod: POD }, ["pod"]),
9351
+ async call(ctx, a) {
9352
+ const p = await resolvePod(ctx, need(a, "pod"));
9353
+ return ctx.api.stop(p.id);
9354
+ }
9355
+ },
9356
+ {
9357
+ name: "pod_wake",
9358
+ description: "Wake a sleeping pod (seconds).",
9359
+ inputSchema: obj({ pod: POD }, ["pod"]),
9360
+ async call(ctx, a) {
9361
+ const p = await resolvePod(ctx, need(a, "pod"));
9362
+ return ctx.api.resume(p.id);
9363
+ }
9364
+ },
9365
+ {
9366
+ name: "pod_destroy",
9367
+ description: "Delete a pod for good (its files and checkpoints). Allowed on your own for pods you launched in this session (pod_launch, pod_up, pod_run) once their work is safe: a pod holding a local project refuses while it has changes not brought back (pod_down first, or discard_changes); for a pod_launch pod, make sure its work is pushed or not needed. Any other pod: only when the user explicitly asked to delete that pod; then set user_confirmed.",
9368
+ inputSchema: obj({ pod: POD, user_confirmed: b("the user explicitly asked to delete this pod"), discard_changes: b("delete even though it has changes not brought back") }, ["pod"]),
9369
+ async call(ctx, a) {
9370
+ const p = await resolvePod(ctx, need(a, "pod"));
9371
+ if (!OWN.has(p.id) && a.user_confirmed !== true) {
9372
+ throw new CliError(`${p.spec.name} wasn't launched by you in this session: ask the user before deleting it, then call again with user_confirmed`);
9373
+ }
9374
+ if (a.discard_changes !== true && (OWN.get(p.id) ?? "project") === "project") {
9375
+ const pending = await ctx.api.diff(p.id, "up").catch(() => null);
9376
+ if (pending && pending.files.length) throw new CliError(`${p.spec.name} has ${pending.files.length} changed file(s) not brought back: pod_down first, or set discard_changes if the user doesn't want them`);
9377
+ }
9378
+ await ctx.api.destroy(p.id);
9379
+ OWN.delete(p.id);
9380
+ return { deleted: p.spec.name, id: p.id };
9381
+ }
9382
+ },
9383
+ {
9384
+ name: "pod_ports",
9385
+ description: "What is listening in a pod (dev servers on ports 1024+) and its preview links.",
9386
+ inputSchema: obj({ pod: POD }, ["pod"]),
9387
+ async call(ctx, a) {
9388
+ const p = await resolvePod(ctx, need(a, "pod"));
9389
+ return ctx.api.previews(p.id);
9390
+ }
9391
+ },
9392
+ {
9393
+ name: "pod_preview",
9394
+ description: "A link to a port in a pod, for the user to open: private (signed in as them) by default; public makes a link anyone can open (tell the user it is public).",
9395
+ inputSchema: obj({ pod: POD, port: n("the port, 1024+"), public: b("a public link anyone can open") }, ["pod", "port"]),
9396
+ async call(ctx, a) {
9397
+ const p = await resolvePod(ctx, need(a, "pod"));
9398
+ const port = Number(a.port);
9399
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new UsageError("port: 1024\u201365535");
9400
+ const pv = await ctx.api.ensurePreview(p.id, { port, public: a.public === true });
9401
+ return pv.public ? { url: pv.url, public: true } : { url: await ctx.api.openPreview(pv.slug, "/"), public: false, note: "a private link: it signs the user in; give it to them, not to anyone else" };
9402
+ }
8712
9403
  }
8713
9404
  ];
8714
9405
  function errorText(e) {
@@ -8732,7 +9423,7 @@ async function handle(ctx, msg) {
8732
9423
  protocolVersion: asked,
8733
9424
  capabilities: { tools: { listChanged: false } },
8734
9425
  serverInfo: { name: "airgate", title: "Airgate", version: VERSION },
8735
- instructions: "Airgate pods are private cloud computers for coding agents. Use pods_list first; task_start hands a pod a task and run_status/run_log follow it. Outbound actions wait in the airlock for the user: never approve on your own initiative."
9426
+ instructions: "Airgate pods are private cloud computers for coding agents. pod_run does one task in a fresh pod and brings the result back; pod_up / pod_down move the local project and conversation there and back; task_start hands an existing pod a task (run_status / run_log follow it). Sleep pods you are done with; delete only pods you launched (or the user asked). Outbound actions wait in the airlock for the user: never approve on your own initiative."
8736
9427
  });
8737
9428
  }
8738
9429
  case "ping":
@@ -8774,6 +9465,10 @@ var mcp = {
8774
9465
  const rl = createInterface({ input: ctx.io.stdin, crlfDelay: Infinity, terminal: false });
8775
9466
  const pending = /* @__PURE__ */ new Set();
8776
9467
  if (!ctx.api.signedIn) ctx.io.stderr.write("airgate mcp: not signed in; tools will say so. run: airgate login\n");
9468
+ const relayer = relayIfSignedIn(ctx, (l) => ctx.io.stderr.write(`airgate mcp: ${l}
9469
+ `));
9470
+ if (relayer) ctx.io.stderr.write(`airgate mcp: signing pods' ${relayer.vendors.join(" / ")} calls from this laptop while this runs
9471
+ `);
8777
9472
  for await (const line2 of rl) {
8778
9473
  if (!line2.trim()) continue;
8779
9474
  let msg;
@@ -8791,10 +9486,14 @@ var mcp = {
8791
9486
  void job2.finally(() => pending.delete(job2));
8792
9487
  }
8793
9488
  await Promise.all(pending);
9489
+ relayer?.stop();
8794
9490
  }
8795
9491
  };
8796
9492
 
8797
9493
  // src/commands/misc.ts
9494
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
9495
+ import { homedir as homedir3 } from "node:os";
9496
+ import { join as join6 } from "node:path";
8798
9497
  var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
8799
9498
  var open = {
8800
9499
  name: "open",
@@ -8853,6 +9552,106 @@ var version = {
8853
9552
  ctx.ui.out(`airgate ${VERSION}`);
8854
9553
  }
8855
9554
  };
9555
+ var skill = {
9556
+ name: "skill",
9557
+ group: "other",
9558
+ usage: "[install [--project] | show]",
9559
+ summary: "teach Claude Code airgate: install the airgate skill (~/.claude/skills, or this project with --project)",
9560
+ example: "airgate skill install",
9561
+ public: true,
9562
+ flags: { project: { type: "boolean", help: "install into ./.claude/skills (shared with the project) instead of ~/.claude/skills" } },
9563
+ async run(ctx) {
9564
+ const action = ctx.args.positionals[0] ?? "show";
9565
+ const url = `${ctx.cfg.app}/skills/airgate/SKILL.md`;
9566
+ const res = await ctx.ui.spin("fetching the airgate skill", ctx.fetch(url));
9567
+ if (!res.ok) throw new CliError(`couldn't fetch ${url} (${res.status})`);
9568
+ const text2 = await res.text();
9569
+ if (!text2.startsWith("---")) throw new CliError(`${url} isn't a skill file`);
9570
+ if (action === "show") return void ctx.io.stdout.write(text2);
9571
+ if (action !== "install") throw new UsageError(`unknown action "${action}"`, "airgate skill install [--project] | airgate skill show");
9572
+ const base = bool(ctx.args, "project") ? join6(ctx.io.cwd, ".claude") : join6(ctx.io.env.HOME ?? homedir3(), ".claude");
9573
+ const dir = join6(base, "skills", "airgate");
9574
+ mkdirSync5(dir, { recursive: true });
9575
+ writeFileSync6(join6(dir, "SKILL.md"), text2);
9576
+ if (ctx.json) return ctx.ui.json({ installed: join6(dir, "SKILL.md") });
9577
+ ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join6(dir, "SKILL.md")}`);
9578
+ ctx.ui.out(ctx.ui.c("dim", 'claude code picks it up in its next session. ask it to "move this to an airgate pod".'));
9579
+ }
9580
+ };
9581
+
9582
+ // src/commands/previews.ts
9583
+ var portArg = (v) => {
9584
+ const n2 = Number(v);
9585
+ if (!v || !Number.isInteger(n2) || n2 < 1 || n2 > 65535) throw new UsageError("a port, like 3000");
9586
+ return n2;
9587
+ };
9588
+ async function podAndPort(ctx) {
9589
+ const [ref, port] = ctx.args.positionals;
9590
+ if (!ref) throw new UsageError("which pod?");
9591
+ return { pod: await resolvePod(ctx, ref), port: portArg(port) };
9592
+ }
9593
+ var ports = {
9594
+ name: "ports",
9595
+ aliases: ["previews"],
9596
+ group: "pods",
9597
+ usage: "<pod>",
9598
+ summary: "what's listening in a pod, and its preview links",
9599
+ example: "airgate ports web",
9600
+ async run(ctx) {
9601
+ const ref = ctx.args.positionals[0];
9602
+ if (!ref) throw new UsageError("which pod? airgate ports <pod>");
9603
+ const pod = await resolvePod(ctx, ref);
9604
+ const r = await ctx.api.previews(pod.id);
9605
+ if (ctx.json) return ctx.ui.json(r);
9606
+ if (!r.enabled) return void ctx.ui.out("previews are not set up on this gateway.");
9607
+ const byPort = new Map(r.previews.map((p) => [p.port, p]));
9608
+ const all = [.../* @__PURE__ */ new Set([...r.ports, ...r.previews.map((p) => p.port)])].sort((a, b2) => a - b2);
9609
+ if (!all.length) return void ctx.ui.out(`nothing listening in ${pod.spec.name} yet. start a dev server (any port 1024+), then: airgate preview ${pod.spec.name} <port>`);
9610
+ ctx.ui.table(
9611
+ all.map((port) => {
9612
+ const p = byPort.get(port);
9613
+ return [`:${port}`, r.ports.includes(port) ? ctx.ui.c("ok", "listening") : ctx.ui.c("dim", "not listening"), p ? p.public ? ctx.ui.c("warn", "public") : "private" : ctx.ui.c("dim", "-"), p?.public ? p.url : ""];
9614
+ }),
9615
+ { head: ["port", "", "preview", "link"] }
9616
+ );
9617
+ ctx.ui.out(ctx.ui.c("dim", `open one: airgate preview ${pod.spec.name} <port> \xB7 share it: airgate share ${pod.spec.name} <port>`));
9618
+ }
9619
+ };
9620
+ var preview = {
9621
+ name: "preview",
9622
+ group: "pods",
9623
+ usage: "<pod> <port> [/path] [--no-open]",
9624
+ summary: "open a port of a pod in your browser (private to you)",
9625
+ example: "airgate preview web 3000",
9626
+ flags: { "no-open": { type: "boolean", help: "print the link instead (it signs you in, once, within a minute)" } },
9627
+ async run(ctx) {
9628
+ const { pod, port } = await podAndPort(ctx);
9629
+ const path = ctx.args.positionals[2] ?? "/";
9630
+ const p = await ctx.api.ensurePreview(pod.id, { port });
9631
+ const url = await ctx.api.openPreview(p.slug, path.startsWith("/") ? path : `/${path}`);
9632
+ if (ctx.json) return ctx.ui.json({ preview: p, open: url });
9633
+ await openOrPrint(ctx, url, `${pod.spec.name}:${port}`);
9634
+ }
9635
+ };
9636
+ var share = {
9637
+ name: "share",
9638
+ group: "pods",
9639
+ usage: "<pod> <port> [--off]",
9640
+ summary: "a public link to a port of a pod (anyone with it can open it); --off makes it private again",
9641
+ example: "airgate share web 3000",
9642
+ flags: { off: { type: "boolean", help: "stop sharing: private to you again" } },
9643
+ async run(ctx) {
9644
+ const { pod, port } = await podAndPort(ctx);
9645
+ const off = bool(ctx.args, "off");
9646
+ const p = await ctx.api.ensurePreview(pod.id, { port, public: !off });
9647
+ if (ctx.json) return ctx.ui.json(p);
9648
+ if (off) ctx.ui.out(`${pod.spec.name}:${port} is private again: only you can open it (airgate preview ${pod.spec.name} ${port}).`);
9649
+ else {
9650
+ ctx.ui.out(p.url);
9651
+ ctx.ui.err(ctx.ui.ce("warn", `public: anyone with this link reaches ${pod.spec.name}:${port}. stop with: airgate share ${pod.spec.name} ${port} --off`));
9652
+ }
9653
+ }
9654
+ };
8856
9655
 
8857
9656
  // src/commands/undo.ts
8858
9657
  var newestFirst = (cps) => [...cps].sort((a, b2) => b2.at - a.at);
@@ -9033,15 +9832,15 @@ var Canvas = class {
9033
9832
  /** write text at (x, y), clipped to maxWidth and the canvas; returns columns written */
9034
9833
  text(x, y, s2, st = {}, maxWidth = Infinity) {
9035
9834
  if (y < 0 || y >= this.rows) return 0;
9036
- let n = 0;
9835
+ let n2 = 0;
9037
9836
  for (const ch of s2.replace(/[\u0000-\u001f\u007f]/g, " ")) {
9038
- if (n >= maxWidth) break;
9039
- const cx = x + n;
9837
+ if (n2 >= maxWidth) break;
9838
+ const cx = x + n2;
9040
9839
  if (cx >= this.cols) break;
9041
9840
  if (cx >= 0) this.cells[y][cx] = { ch, st };
9042
- n++;
9841
+ n2++;
9043
9842
  }
9044
- return n;
9843
+ return n2;
9045
9844
  }
9046
9845
  fill(x, y, w, h, st = {}) {
9047
9846
  for (let r = y; r < y + h; r++) for (let c = x; c < x + w; c++) if (r >= 0 && r < this.rows && c >= 0 && c < this.cols) this.cells[r][c] = { ch: " ", st };
@@ -9246,7 +10045,7 @@ function formKey(s2, form, key) {
9246
10045
  }
9247
10046
  return { state: { ...s2, modal: { kind: "form", form: next } }, effects: [] };
9248
10047
  };
9249
- const cycle2 = (n, len, d2) => (n + d2 + len) % len;
10048
+ const cycle2 = (n2, len, d2) => (n2 + d2 + len) % len;
9250
10049
  switch (key) {
9251
10050
  case "tab":
9252
10051
  case "down":
@@ -9338,10 +10137,10 @@ function modalKey(s2, m, key) {
9338
10137
  return none;
9339
10138
  }
9340
10139
  case "timeline": {
9341
- const n = m.checkpoints?.length ?? 0;
10140
+ const n2 = m.checkpoints?.length ?? 0;
9342
10141
  if (key === "esc" || key === "q" || key === "u") return close;
9343
- if ((key === "down" || key === "j") && n) return { state: { ...s2, modal: { ...m, sel: Math.min(n - 1, m.sel + 1) } }, effects: [] };
9344
- if ((key === "up" || key === "k") && n) return { state: { ...s2, modal: { ...m, sel: Math.max(0, m.sel - 1) } }, effects: [] };
10142
+ if ((key === "down" || key === "j") && n2) return { state: { ...s2, modal: { ...m, sel: Math.min(n2 - 1, m.sel + 1) } }, effects: [] };
10143
+ if ((key === "up" || key === "k") && n2) return { state: { ...s2, modal: { ...m, sel: Math.max(0, m.sel - 1) } }, effects: [] };
9345
10144
  const cp = m.checkpoints?.[m.sel];
9346
10145
  if (key === "enter" && cp) {
9347
10146
  return {
@@ -9649,16 +10448,16 @@ function draw(s2, cols, rows2) {
9649
10448
  }
9650
10449
  function header(c, s2) {
9651
10450
  let x = c.text(0, 0, " \u22A0 airgate", { bold: true }) + 2;
9652
- const n = pendingApprovals(s2).length;
10451
+ const n2 = pendingApprovals(s2).length;
9653
10452
  const tabs = [
9654
10453
  ["pods", "pods"],
9655
- ["airlock", n ? `airlock [${n}]` : "airlock"],
10454
+ ["airlock", n2 ? `airlock [${n2}]` : "airlock"],
9656
10455
  ["usage", "usage"]
9657
10456
  ];
9658
10457
  for (const [id, label2] of tabs) {
9659
10458
  if (x + label2.length + 2 > c.cols) break;
9660
10459
  const active = s2.screen === id;
9661
- x += c.text(x, 0, ` ${label2} `, { inverse: active, bold: active, fg: id === "airlock" && n && !active ? "warn" : void 0 });
10460
+ x += c.text(x, 0, ` ${label2} `, { inverse: active, bold: active, fg: id === "airlock" && n2 && !active ? "warn" : void 0 });
9662
10461
  x += 1;
9663
10462
  }
9664
10463
  const right = s2.connected ? "\u25CF live" : s2.loaded ? "\u25CB offline" : "\u25CB connecting";
@@ -9704,9 +10503,9 @@ function splash(c, s2) {
9704
10503
  }
9705
10504
  function statusLine(c, s2) {
9706
10505
  const y = c.rows - 1;
9707
- const n = pendingApprovals(s2).length;
10506
+ const n2 = pendingApprovals(s2).length;
9708
10507
  const parts = [];
9709
- if (n) parts.push(`airlock [${n}]`);
10508
+ if (n2) parts.push(`airlock [${n2}]`);
9710
10509
  if (s2.me) parts.push(s2.me.plan === "none" ? "no plan" : s2.me.plan, `${hrs(s2.me.usedHours)} this month`);
9711
10510
  const right = parts.join(" \xB7 ");
9712
10511
  let left;
@@ -9725,8 +10524,8 @@ function statusLine(c, s2) {
9725
10524
  c.text(0, y, " " + clip(left, Math.max(0, c.cols - rw - 2)), { fg: leftFg });
9726
10525
  if (right && c.cols - rw > 10) {
9727
10526
  const x = c.cols - rw;
9728
- if (n) {
9729
- const badge = `airlock [${n}]`;
10527
+ if (n2) {
10528
+ const badge = `airlock [${n2}]`;
9730
10529
  c.text(x, y, badge, { fg: "warn", bold: true });
9731
10530
  c.text(x + badge.length, y, right.slice(badge.length), { fg: "dim" });
9732
10531
  } else c.text(x, y, right, { fg: "dim" });
@@ -10029,11 +10828,11 @@ function modal(c, s2, m) {
10029
10828
  return;
10030
10829
  }
10031
10830
  case "timeline": {
10032
- const n = m.checkpoints?.length ?? 0;
10033
- const b2 = centered(c, 64, Math.max(6, Math.min(n, 14) + 4));
10831
+ const n2 = m.checkpoints?.length ?? 0;
10832
+ const b2 = centered(c, 64, Math.max(6, Math.min(n2, 14) + 4));
10034
10833
  c.box(b2.x, b2.y, b2.w, b2.h, { double: true, title: `undo \xB7 ${m.name}`, clear: true });
10035
10834
  if (!m.checkpoints) c.text(b2.x + 2, b2.y + 1, "loading\u2026", { fg: "dim" });
10036
- else if (!n) c.text(b2.x + 2, b2.y + 1, "no checkpoints yet. c takes one.", { fg: "dim" }, b2.w - 4);
10835
+ else if (!n2) c.text(b2.x + 2, b2.y + 1, "no checkpoints yet. c takes one.", { fg: "dim" }, b2.w - 4);
10037
10836
  const rowsAvail = b2.h - 3;
10038
10837
  const start = Math.max(0, m.sel - rowsAvail + 1);
10039
10838
  (m.checkpoints ?? []).slice(start, start + rowsAvail).forEach((cp, i) => {
@@ -10080,7 +10879,7 @@ function formModal(c, s2, f) {
10080
10879
  const t = TEMPLATES[formTemplate(f)];
10081
10880
  const builtin = formBuiltin(f);
10082
10881
  const plan2 = s2.me?.plan === "pro" ? "pro" : "payg";
10083
- const models = s2.catalog?.models ?? [];
10882
+ const models2 = s2.catalog?.models ?? [];
10084
10883
  fields.forEach((id, i) => {
10085
10884
  const y = b2.y + 1 + i;
10086
10885
  if (y >= b2.y + b2.h - 3) return;
@@ -10118,7 +10917,7 @@ function formModal(c, s2, f) {
10118
10917
  v = `[${f.builtin ? "x" : " "}] built-in model, no subscription`;
10119
10918
  break;
10120
10919
  case "model":
10121
- v = `\u2039 ${models[f.model]?.id ?? `default${s2.catalog?.defaultModel ? ` (${s2.catalog.defaultModel})` : ""}`} \u203A`;
10920
+ v = `\u2039 ${models2[f.model]?.id ?? `default${s2.catalog?.defaultModel ? ` (${s2.catalog.defaultModel})` : ""}`} \u203A`;
10122
10921
  break;
10123
10922
  case "schedule": {
10124
10923
  const p = SCHEDULE_LIST[f.schedule];
@@ -10236,7 +11035,7 @@ function startTui(d) {
10236
11035
  scheduleDraw();
10237
11036
  }
10238
11037
  };
10239
- const onSignal = () => finish(130);
11038
+ const onSignal = () => finish2(130);
10240
11039
  const onCrash = (e) => crash(e);
10241
11040
  function listenKeys(on) {
10242
11041
  if (on) {
@@ -10256,7 +11055,7 @@ function startTui(d) {
10256
11055
  d.stdin.pause();
10257
11056
  d.stdout.write(LEAVE_ALT);
10258
11057
  }
10259
- function finish(code3, message) {
11058
+ function finish2(code3, message) {
10260
11059
  if (finished) return;
10261
11060
  finished = true;
10262
11061
  controller.abort();
@@ -10271,7 +11070,7 @@ function startTui(d) {
10271
11070
  resolveDone(code3);
10272
11071
  }
10273
11072
  function crash(e) {
10274
- finish(1, `airgate ui crashed: ${e instanceof Error ? e.message : String(e)}`);
11073
+ finish2(1, `airgate ui crashed: ${e instanceof Error ? e.message : String(e)}`);
10275
11074
  }
10276
11075
  const errText = (e) => {
10277
11076
  if (e instanceof ApiError) {
@@ -10300,7 +11099,7 @@ function startTui(d) {
10300
11099
  try {
10301
11100
  switch (e.type) {
10302
11101
  case "quit":
10303
- return finish(0);
11102
+ return finish2(0);
10304
11103
  case "attach":
10305
11104
  return await handoff(e.podId, e.name);
10306
11105
  case "loadUsage": {
@@ -10538,6 +11337,9 @@ var COMMANDS = [
10538
11337
  task,
10539
11338
  runs,
10540
11339
  log,
11340
+ ports,
11341
+ preview,
11342
+ share,
10541
11343
  checkpoints,
10542
11344
  checkpoint,
10543
11345
  rewind,
@@ -10548,6 +11350,9 @@ var COMMANDS = [
10548
11350
  mask,
10549
11351
  up,
10550
11352
  down2,
11353
+ runTask,
11354
+ models,
11355
+ relay,
10551
11356
  usage,
10552
11357
  plan,
10553
11358
  upgrade,
@@ -10556,6 +11361,7 @@ var COMMANDS = [
10556
11361
  ui,
10557
11362
  mcp,
10558
11363
  open,
11364
+ skill,
10559
11365
  status,
10560
11366
  help,
10561
11367
  version
@@ -10598,7 +11404,7 @@ function printCommandHelp(ui2, c) {
10598
11404
  ui2.out(`usage: airgate ${c.name}${c.usage ? " " + c.usage : ""}`);
10599
11405
  ui2.out();
10600
11406
  ui2.out(c.summary + ".");
10601
- for (const n of c.notes ?? []) ui2.out(n);
11407
+ for (const n2 of c.notes ?? []) ui2.out(n2);
10602
11408
  const flags = flagLines({ ...c.flags ?? {}, json: GLOBAL_FLAGS.json });
10603
11409
  ui2.out();
10604
11410
  for (const l of formatTable(flags, void 0, 2)) ui2.out(l);
@@ -10733,7 +11539,7 @@ async function run(argv, io, opts2 = {}) {
10733
11539
  }
10734
11540
  }
10735
11541
  function suggest2(name) {
10736
- const near = COMMANDS.filter((c) => [c.name, ...c.aliases ?? []].some((n) => n.startsWith(name.slice(0, 2)))).map((c) => c.name);
11542
+ const near = COMMANDS.filter((c) => [c.name, ...c.aliases ?? []].some((n2) => n2.startsWith(name.slice(0, 2)))).map((c) => c.name);
10737
11543
  return near.length ? `did you mean: ${near.slice(0, 3).join(", ")}? (airgate help)` : "see: airgate help";
10738
11544
  }
10739
11545