airgate 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,7 @@ airgate run "get the failing tests passing" # a fresh pod; the result comes ba
38
38
  airgate skill install # teach Claude Code when and how to use airgate
39
39
  claude mcp add airgate -- airgate mcp # airgate as tools (pod_run, pod_up, pod_down…)
40
40
  airgate models connect claude --laptop # a token from `claude setup-token`, kept on this laptop
41
+ airgate models connect chatgpt --laptop # or Codex on your ChatGPT plan
41
42
  ```
42
43
 
43
44
  The last line lets this laptop sign the pods' model calls while airgate runs, so tasks run without
package/dist/airgate.mjs CHANGED
@@ -301,6 +301,72 @@ function dotenvExample(names) {
301
301
  return names.map((n2) => `${n2}=`).join("\n") + (names.length ? "\n" : "");
302
302
  }
303
303
 
304
+ // ../sandbox/src/chatgpt.ts
305
+ var CHATGPT_CODEX_BASE = "https://chatgpt.com/backend-api/codex";
306
+ var CHATGPT_TOKEN_URL = "https://auth.openai.com/oauth/token";
307
+ var CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
308
+ function jwtClaims(token) {
309
+ const part = token?.split(".")[1];
310
+ if (!part) return null;
311
+ try {
312
+ const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
313
+ const bin = atob(b64 + "=".repeat((4 - b64.length % 4) % 4));
314
+ return JSON.parse(new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0))));
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+ function accountIdOf(t) {
320
+ if (typeof t.account_id === "string" && t.account_id) return t.account_id;
321
+ const auth = jwtClaims(typeof t.id_token === "string" ? t.id_token : void 0)?.["https://api.openai.com/auth"];
322
+ return typeof auth?.chatgpt_account_id === "string" ? auth.chatgpt_account_id : null;
323
+ }
324
+ function parseChatgptLogin(text2) {
325
+ let j;
326
+ try {
327
+ j = JSON.parse(text2);
328
+ } catch {
329
+ throw new Error("not a Codex login: expected the auth.json that `codex login` writes");
330
+ }
331
+ const t = j.tokens && typeof j.tokens === "object" ? j.tokens : j;
332
+ if (typeof t.access_token !== "string" || typeof t.refresh_token !== "string") {
333
+ throw new Error("this Codex login has no ChatGPT tokens: sign in with ChatGPT in `codex login`, not an API key");
334
+ }
335
+ const account = accountIdOf(t);
336
+ if (!account) throw new Error("this Codex login doesn't name a ChatGPT account");
337
+ return { access_token: t.access_token, refresh_token: t.refresh_token, id_token: typeof t.id_token === "string" ? t.id_token : void 0, account_id: account };
338
+ }
339
+ function chatgptNeedsRefresh(t, now = Date.now(), windowMs = 5 * 6e4) {
340
+ const exp = jwtClaims(t.access_token)?.exp;
341
+ return typeof exp !== "number" || exp * 1e3 - now < windowMs;
342
+ }
343
+ async function refreshChatgpt(t, o = {}) {
344
+ const res = await (o.fetch ?? fetch)(o.tokenUrl ?? CHATGPT_TOKEN_URL, {
345
+ method: "POST",
346
+ headers: { "content-type": "application/json" },
347
+ body: JSON.stringify({ client_id: CODEX_CLIENT_ID, grant_type: "refresh_token", refresh_token: t.refresh_token, scope: "openid profile email" }),
348
+ signal: AbortSignal.timeout(45e3)
349
+ });
350
+ if (!res.ok) {
351
+ const why = await res.text().catch(() => "");
352
+ throw new Error(
353
+ res.status === 400 || res.status === 401 ? "the ChatGPT sign-in expired or was revoked: connect it again (airgate models connect chatgpt)" : `couldn't renew the ChatGPT sign-in (${res.status}${why ? `: ${why.slice(0, 120)}` : ""})`
354
+ );
355
+ }
356
+ const r = await res.json();
357
+ if (!r.access_token) throw new Error("renewing the ChatGPT sign-in returned no access token");
358
+ return { ...t, access_token: r.access_token, refresh_token: r.refresh_token ?? t.refresh_token, id_token: r.id_token ?? t.id_token };
359
+ }
360
+ function chatgptUrl(path, base = CHATGPT_CODEX_BASE) {
361
+ return base + path.replace(/^\/?v1(?=\/|$)/, "").replace(/^(?!\/)/, "/");
362
+ }
363
+ function signChatgpt(t, headers) {
364
+ delete headers["x-api-key"];
365
+ headers.authorization = `Bearer ${t.access_token}`;
366
+ headers["chatgpt-account-id"] = t.account_id;
367
+ headers.originator ??= "codex_cli_rs";
368
+ }
369
+
304
370
  // src/api.ts
305
371
  var UPLOAD_PART = 6 * 1024 * 1024;
306
372
  var ApiError = class extends Error {
@@ -365,6 +431,7 @@ var Api = class {
365
431
  if (e?.name === "AbortError") throw e;
366
432
  throw new NetworkError(this.gateway, e);
367
433
  }
434
+ if (opts2.bytes && res.ok) return Buffer.from(await res.arrayBuffer());
368
435
  const text2 = await res.text();
369
436
  if (opts2.text && res.ok) return text2;
370
437
  let data = {};
@@ -464,11 +531,11 @@ var Api = class {
464
531
  checkpoint(id, label2) {
465
532
  return this.request("POST", `/v1/pods/${enc(id)}/checkpoints`, { body: { label: label2 } }).then((r) => r.pod);
466
533
  }
467
- restore(id, checkpointId) {
468
- return this.request("POST", `/v1/pods/${enc(id)}/restore`, { body: { checkpointId } }).then((r) => r.pod);
534
+ restore(id, checkpointId2) {
535
+ return this.request("POST", `/v1/pods/${enc(id)}/restore`, { body: { checkpointId: checkpointId2 } }).then((r) => r.pod);
469
536
  }
470
- fork(id, checkpointId, overrides) {
471
- return this.request("POST", `/v1/pods/${enc(id)}/fork`, { body: { checkpointId, overrides } }).then((r) => r.pod);
537
+ fork(id, checkpointId2, overrides) {
538
+ return this.request("POST", `/v1/pods/${enc(id)}/fork`, { body: { checkpointId: checkpointId2, overrides } }).then((r) => r.pod);
472
539
  }
473
540
  diff(id, base) {
474
541
  return this.request("GET", `/v1/pods/${enc(id)}/diff?base=${encodeURIComponent(base)}`);
@@ -514,12 +581,44 @@ var Api = class {
514
581
  githubDisconnect(id) {
515
582
  return this.request("DELETE", `/v1/github/installations/${id}`);
516
583
  }
584
+ /* a workspace's files, live or at a checkpoint */
585
+ files(id, path = "", checkpoint2) {
586
+ const q = new URLSearchParams({ path, ...checkpoint2 ? { checkpoint: checkpoint2 } : {} });
587
+ return this.request("GET", `/v1/pods/${enc(id)}/files?${q}`);
588
+ }
589
+ fileRaw(id, path, checkpoint2) {
590
+ const q = new URLSearchParams({ path, ...checkpoint2 ? { checkpoint: checkpoint2 } : {} });
591
+ return this.request("GET", `/v1/pods/${enc(id)}/files/raw?${q}`, { bytes: true });
592
+ }
593
+ /** after launch: the network and the AI switch */
594
+ updatePod(id, patch) {
595
+ return this.request("PATCH", `/v1/pods/${enc(id)}`, { body: patch }).then((r) => r.pod);
596
+ }
597
+ /* saved snapshots */
598
+ snapshots() {
599
+ return this.request("GET", "/v1/snapshots").then((r) => r.snapshots);
600
+ }
601
+ saveSnapshot(id, name, checkpointId2) {
602
+ return this.request("POST", `/v1/pods/${enc(id)}/snapshots`, { body: { name, ...checkpointId2 ? { checkpointId: checkpointId2 } : {} } }).then((r) => r.snapshot);
603
+ }
604
+ sharedSnapshot(code3) {
605
+ return this.request("GET", `/v1/snapshots/shared/${enc(code3)}`).then((r) => r.snapshot);
606
+ }
607
+ launchSnapshot(ref, overrides = {}) {
608
+ return this.request("POST", `/v1/snapshots/${enc(ref)}/launch`, { body: { overrides } }).then((r) => r.pod);
609
+ }
610
+ shareSnapshot(ref, on) {
611
+ return this.request(on ? "POST" : "DELETE", `/v1/snapshots/${enc(ref)}/share`).then((r) => r.snapshot);
612
+ }
613
+ deleteSnapshot(ref) {
614
+ return this.request("DELETE", `/v1/snapshots/${enc(ref)}`);
615
+ }
517
616
  /* model sign-ins: connected to airgate (opt-in), and whether a laptop relay is connected */
518
617
  modelsAuth() {
519
618
  return this.request("GET", "/v1/models/auth");
520
619
  }
521
- connectModel(vendor, value) {
522
- return this.request("PUT", `/v1/models/auth/${enc(vendor)}`, { body: { value } });
620
+ connectModel(vendor, value, kind) {
621
+ return this.request("PUT", `/v1/models/auth/${enc(vendor)}`, { body: { value, ...kind ? { kind } : {} } });
523
622
  }
524
623
  disconnectModel(vendor) {
525
624
  return this.request("DELETE", `/v1/models/auth/${enc(vendor)}`);
@@ -4388,8 +4487,8 @@ function linked(s2, j, b2, ascii) {
4388
4487
  }
4389
4488
  return m;
4390
4489
  }
4391
- function place3(s2, j, c, k, join7, a = ARMS.get(c), ascii = false, round = false) {
4392
- if (join7 && a !== void 0) {
4490
+ function place3(s2, j, c, k, join8, a = ARMS.get(c), ascii = false, round = false) {
4491
+ if (join8 && a !== void 0) {
4393
4492
  const old = s2.chars[j], b2 = ascii ? asciiArms(old) : ARMS.get(old);
4394
4493
  if (b2 !== void 0) {
4395
4494
  const kept = linked(s2, j, b2, ascii);
@@ -4555,7 +4654,7 @@ function rect(s2, x, y, w, h, o) {
4555
4654
  if (title)
4556
4655
  for (const ch of title)
4557
4656
  code(ch);
4558
- const join7 = yes(o?.join, "join", true);
4657
+ const join8 = yes(o?.join, "join", true);
4559
4658
  const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
4560
4659
  if (!finite3(x, y, w, h))
4561
4660
  return { x: 0, y: 0, cols: 0, rows: 0 };
@@ -4578,7 +4677,7 @@ function rect(s2, x, y, w, h, o) {
4578
4677
  const put = (cx, cy, e, reach = 255) => {
4579
4678
  const j = s2.index(cx, cy), a = ascii ? EDGE_ARMS[e] : ARMS.get(ed[e]);
4580
4679
  if (j >= 0)
4581
- place3(s2, j, ed[e], k, join7, a === void 0 ? a : a & reach, ascii, round);
4680
+ place3(s2, j, ed[e], k, join8, a === void 0 ? a : a & reach, ascii, round);
4582
4681
  };
4583
4682
  const end = (i, n2, first, last) => n2 < 2 ? 255 : i === 0 ? 3 << first : i === n2 - 1 ? 3 << last : 255;
4584
4683
  const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi, n2) => Math.min(hi, n2 - o2);
@@ -4714,11 +4813,11 @@ function shape(s2, cx, cy, rx, ry, o, what, from = 0, span = 1) {
4714
4813
  const ch = char === "auto" ? "auto" : brush(char, `${/^[aeiou]/.test(what) ? "an" : "a"} ${what}'s char`);
4715
4814
  const f = fill === void 0 || fill === false ? null : brush(fill, "fill");
4716
4815
  const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
4717
- const join7 = yes(o?.join, "join", true);
4816
+ const join8 = yes(o?.join, "join", true);
4718
4817
  ellipseCells(s2.cols, s2.rows, cx, cy, rx, ry, f !== null, (x, y, edge, slope) => {
4719
4818
  const j = y * s2.cols + x;
4720
4819
  if (edge)
4721
- place3(s2, j, ch === "auto" ? slope : ch, k, join7 && ch !== "auto");
4820
+ place3(s2, j, ch === "auto" ? slope : ch, k, join8 && ch !== "auto");
4722
4821
  else
4723
4822
  s2.put(j, f, f === EMPTY ? NONE : fk);
4724
4823
  }, ch === "auto" ? s2.aspect : 0, from, span);
@@ -7235,6 +7334,7 @@ var launch = {
7235
7334
  'autopilot jobs: --cron "0 2 * * *" --task "update deps, run tests" (repeat both for more; --job-name names them). more later: airgate job add'
7236
7335
  ],
7237
7336
  flags: {
7337
+ from: { type: "string", value: "snapshot", help: "start from a saved snapshot: its name, or a share code (airgate snapshots)" },
7238
7338
  name: { type: "string", value: "name" },
7239
7339
  template: { type: "string", value: "template", choices: TEMPLATE_IDS, help: `${TEMPLATE_IDS.join(" | ")} (default code)` },
7240
7340
  agent: { type: "string", value: "agent", choices: Object.keys(AGENTS), help: `${AGENT_NAMES.join(" | ")} (default: the template's first)` },
@@ -7257,6 +7357,18 @@ var launch = {
7257
7357
  },
7258
7358
  async run(ctx) {
7259
7359
  if (ctx.args.positionals.length) throw new UsageError(`unexpected "${ctx.args.positionals[0]}"`, "name the pod with --name");
7360
+ const from = str(ctx.args, "from");
7361
+ if (from) {
7362
+ const o = {};
7363
+ if (str(ctx.args, "name")) o.name = str(ctx.args, "name");
7364
+ if (str(ctx.args, "agent")) o.agent = str(ctx.args, "agent");
7365
+ if (ctx.args.flags.memory !== "standard") o.size = ctx.args.flags.memory;
7366
+ if (ctx.args.flags.browser === true) o.browser = true;
7367
+ const pod2 = await ctx.ui.spin(`starting a pod from ${from}`, async () => waitReady(ctx, await ctx.api.launchSnapshot(from, o)));
7368
+ if (ctx.json) return ctx.ui.json(pod2);
7369
+ ctx.ui.out(readyLine(ctx, pod2));
7370
+ return void ctx.ui.out(`next: airgate attach ${pod2.spec.name}`);
7371
+ }
7260
7372
  const spec = specFromFlags(ctx);
7261
7373
  if (spec.repo) await checkRepo(ctx, spec.repo);
7262
7374
  const envFile = str(ctx.args, "env");
@@ -7303,7 +7415,7 @@ function readDotenv(ctx, file) {
7303
7415
  }
7304
7416
  function specFromFlags(ctx, defaults = {}) {
7305
7417
  const a = ctx.args;
7306
- const network = str(a, "network") ?? "allowlist";
7418
+ const network2 = str(a, "network") ?? "allowlist";
7307
7419
  const autoStop = str(a, "auto-stop");
7308
7420
  let autoStopMinutes = null;
7309
7421
  if (autoStop === "keep-awake" || autoStop === "never") autoStopMinutes = 0;
@@ -7342,12 +7454,12 @@ function specFromFlags(ctx, defaults = {}) {
7342
7454
  agent,
7343
7455
  size: str(a, "memory") ?? "standard",
7344
7456
  masking: a.flags.mask !== false,
7345
- network,
7457
+ network: network2,
7346
7458
  autoStopMinutes,
7347
7459
  autoCheckpoint: a.flags.undo !== false
7348
7460
  };
7349
7461
  if (repo) spec.repo = repo;
7350
- if (network === "allowlist") spec.allow = [.../* @__PURE__ */ new Set([...DEFAULT_ALLOW, ...list(a, "allow").flatMap((h) => h.split(",")).map((h) => h.trim()).filter(Boolean)])];
7462
+ if (network2 === "allowlist") spec.allow = [.../* @__PURE__ */ new Set([...DEFAULT_ALLOW, ...list(a, "allow").flatMap((h) => h.split(",")).map((h) => h.trim()).filter(Boolean)])];
7351
7463
  const terms = list(a, "mask-term");
7352
7464
  if (terms.length) spec.maskTerms = terms;
7353
7465
  if (template !== "code") spec.template = template;
@@ -7431,7 +7543,10 @@ var destroy = {
7431
7543
  flags: { yes: { type: "boolean", short: "y", help: "don't ask" } },
7432
7544
  async run(ctx) {
7433
7545
  const p = await resolvePod(ctx, ctx.args.positionals[0]);
7434
- if (!await confirm(ctx, `destroy ${p.spec.name} (${p.id})? its files and checkpoints are gone for good.`)) {
7546
+ const pending = await ctx.api.diff(p.id, "up").catch(() => null);
7547
+ const unpulled = pending?.files.length ?? 0;
7548
+ if (unpulled && !ctx.json) ctx.ui.err(ctx.ui.ce("warn", `${p.spec.name} has ${unpulled} changed file(s) not brought back. keep them first: airgate down --pod ${p.spec.name} (or save it: airgate snapshot ${p.spec.name} <name>)`));
7549
+ if (!await confirm(ctx, `destroy ${p.spec.name} (${p.id})? its files and checkpoints are gone for good${unpulled ? `, including ${unpulled} changed file(s)` : ""}.`)) {
7435
7550
  throw new CliError("not destroyed");
7436
7551
  }
7437
7552
  await ctx.ui.spin(`destroying ${p.spec.name}`, ctx.api.destroy(p.id));
@@ -7662,7 +7777,7 @@ var log = {
7662
7777
  import { createInterface } from "node:readline";
7663
7778
 
7664
7779
  // src/version.ts
7665
- var VERSION = "0.1.3";
7780
+ var VERSION = "0.1.5";
7666
7781
 
7667
7782
  // src/commands/updown.ts
7668
7783
  import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
@@ -7671,7 +7786,7 @@ import { basename, dirname as dirname2, isAbsolute, join as join4, relative as r
7671
7786
 
7672
7787
  // src/pack.ts
7673
7788
  import { spawn, spawnSync } from "node:child_process";
7674
- import { existsSync, lstatSync } from "node:fs";
7789
+ import { existsSync, lstatSync, readdirSync } from "node:fs";
7675
7790
  import { join as join2 } from "node:path";
7676
7791
  var UPLOAD_LIMIT = 32 * 6 * 1024 * 1024;
7677
7792
  var TooLargeError = class extends Error {
@@ -7681,6 +7796,11 @@ var TooLargeError = class extends Error {
7681
7796
  this.limit = limit;
7682
7797
  }
7683
7798
  };
7799
+ function isEnvFile(path) {
7800
+ const name = path.split("/").pop() ?? "";
7801
+ return /^\.env($|\.)/.test(name) && !/\.(example|sample|template|dist|defaults?|schema)$/.test(name);
7802
+ }
7803
+ var ENV_EXCLUDES = [".env", ".env.local", ".env.*.local", ".env.development", ".env.production", ".env.test", ".env.staging", ".envrc"].map((p) => `--exclude=${p}`);
7684
7804
  function git(cwd, args, input) {
7685
7805
  const r = spawnSync("git", args, { cwd, input, maxBuffer: 256 * 1024 * 1024 });
7686
7806
  return { status: r.status ?? (r.error ? 127 : 1), stdout: r.stdout?.toString("utf8") ?? "", stderr: r.stderr?.toString("utf8") ?? "", error: r.error };
@@ -7709,7 +7829,7 @@ function gitFiles(dir) {
7709
7829
  return out;
7710
7830
  }
7711
7831
  function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7712
- return new Promise((resolve6, reject) => {
7832
+ return new Promise((resolve7, reject) => {
7713
7833
  const child = spawn("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
7714
7834
  const chunks = [];
7715
7835
  let size2 = 0;
@@ -7729,7 +7849,7 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7729
7849
  child.on("close", (code3) => {
7730
7850
  if (over2) return reject(new TooLargeError(limit));
7731
7851
  if (code3 !== 0) return reject(new Error(`tar failed: ${err.trim().split("\n").slice(-3).join(" ")}`));
7732
- resolve6(Buffer.concat(chunks));
7852
+ resolve7(Buffer.concat(chunks));
7733
7853
  });
7734
7854
  child.stdin.on("error", () => void 0);
7735
7855
  child.stdin.end(input ?? "");
@@ -7737,13 +7857,17 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7737
7857
  }
7738
7858
  async function packProject(dir, limit = UPLOAD_LIMIT) {
7739
7859
  if (isGitRepo(dir)) {
7740
- const files = gitFiles(dir).filter((f) => !f.startsWith(".airgate/"));
7741
- if (!files.length) throw new Error("no files to send (git sees nothing tracked or untracked here)");
7742
- const archive2 = await runTar(dir, ["--null", "-T", "-", "-czf", "-"], files.join("\0") + "\0", limit);
7743
- return { archive: archive2, files: files.length, git: true };
7744
- }
7745
- const archive = await runTar(dir, ["--exclude=node_modules", "--exclude=.git", "--exclude=.airgate", "-czf", "-", "."], null, limit);
7746
- return { archive, files: -1, git: false };
7860
+ const tracked = new Set(git(dir, ["ls-files", "-c", "-z"]).stdout.split("\0"));
7861
+ const all = gitFiles(dir).filter((f) => !f.startsWith(".airgate/"));
7862
+ const skipped2 = all.filter((f) => (isEnvFile(f) || f.split("/").pop() === ".envrc") && !tracked.has(f));
7863
+ const files2 = all.filter((f) => !skipped2.includes(f));
7864
+ if (!files2.length) throw new Error("no files to send (git sees nothing tracked or untracked here)");
7865
+ const archive2 = await runTar(dir, ["--null", "-T", "-", "-czf", "-"], files2.join("\0") + "\0", limit);
7866
+ return { archive: archive2, files: files2.length, git: true, skipped: skipped2 };
7867
+ }
7868
+ const skipped = readdirSync(dir).filter((f) => isEnvFile(f) || f === ".envrc");
7869
+ const archive = await runTar(dir, ["--exclude=node_modules", "--exclude=.git", "--exclude=.airgate", ...ENV_EXCLUDES, "-czf", "-", "."], null, limit);
7870
+ return { archive, files: -1, git: false, skipped };
7747
7871
  }
7748
7872
  var CLAUDE_CONFIG = ["settings.json", "CLAUDE.md", "skills", "agents", "commands"];
7749
7873
  async function packClaudeConfig(home, limit = UPLOAD_LIMIT) {
@@ -7765,7 +7889,7 @@ import { gunzipSync } from "node:zlib";
7765
7889
 
7766
7890
  // src/sessions.ts
7767
7891
  import { createHash, randomUUID } from "node:crypto";
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";
7892
+ import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync as readdirSync2, readFileSync as readFileSync4, readSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
7769
7893
  import { tmpdir } from "node:os";
7770
7894
  import { dirname, join as join3, relative } from "node:path";
7771
7895
  var POD_HOME = "/home/sprite";
@@ -7793,7 +7917,7 @@ function findClaudeSession(home, projectDir, id, env = {}) {
7793
7917
  const dir = join3(claudeHome(home, env), "projects", claudeKey(p));
7794
7918
  let names = [];
7795
7919
  try {
7796
- names = readdirSync(dir).filter((n2) => n2.endsWith(".jsonl"));
7920
+ names = readdirSync2(dir).filter((n2) => n2.endsWith(".jsonl"));
7797
7921
  } catch {
7798
7922
  continue;
7799
7923
  }
@@ -7820,22 +7944,22 @@ function firstLine(file) {
7820
7944
  }
7821
7945
  }
7822
7946
  function codexRollouts(root, limit = 3e3) {
7823
- const files = [];
7947
+ const files2 = [];
7824
7948
  const walk = (d, depth) => {
7825
7949
  let list2 = [];
7826
7950
  try {
7827
- list2 = readdirSync(d, { withFileTypes: true });
7951
+ list2 = readdirSync2(d, { withFileTypes: true });
7828
7952
  } catch {
7829
7953
  return;
7830
7954
  }
7831
7955
  for (const x of list2) {
7832
7956
  const full = join3(d, x.name);
7833
7957
  if (x.isDirectory() && depth < 4) walk(full, depth + 1);
7834
- else if (x.isFile() && /^rollout-.*\.jsonl$/.test(x.name)) files.push({ f: full, m: statSync(full).mtimeMs });
7958
+ else if (x.isFile() && /^rollout-.*\.jsonl$/.test(x.name)) files2.push({ f: full, m: statSync(full).mtimeMs });
7835
7959
  }
7836
7960
  };
7837
7961
  walk(join3(root, "sessions"), 0);
7838
- return files.sort((a, b2) => b2.m - a.m).slice(0, limit).map((x) => x.f);
7962
+ return files2.sort((a, b2) => b2.m - a.m).slice(0, limit).map((x) => x.f);
7839
7963
  }
7840
7964
  var codexId = (file) => /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/.exec(file)?.[1] ?? null;
7841
7965
  function findCodexSession(home, projectDir, id, env = {}) {
@@ -7879,7 +8003,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
7879
8003
  names.push(`${s2.id}.jsonl`);
7880
8004
  if (s2.sidecar) {
7881
8005
  const copy2 = (src, dst) => {
7882
- for (const x of readdirSync(src, { withFileTypes: true })) {
8006
+ for (const x of readdirSync2(src, { withFileTypes: true })) {
7883
8007
  const a = join3(src, x.name);
7884
8008
  const b2 = join3(dst, x.name);
7885
8009
  if (x.isDirectory()) {
@@ -8086,8 +8210,8 @@ function rewritePaths(text2, pre) {
8086
8210
  }
8087
8211
  return lines.join("\n");
8088
8212
  }
8089
- function orderFiles(files) {
8090
- return [...files.filter((f) => f.risky), ...files.filter((f) => !f.risky)];
8213
+ function orderFiles(files2) {
8214
+ return [...files2.filter((f) => f.risky), ...files2.filter((f) => !f.risky)];
8091
8215
  }
8092
8216
  function refusals(blocks) {
8093
8217
  const out = [];
@@ -8293,10 +8417,11 @@ var up = {
8293
8417
  const state = { gateway: ctx.cfg.gateway, podId: pod.id, podName: pod.spec.name, dir: podDir, at: Date.now(), ...sent.length ? { sessions: sent } : {} };
8294
8418
  writeState(projectDir, state);
8295
8419
  if (ctx.json) {
8296
- return ui2.json({ pod, dir: podDir, bytes: packed.archive.length, files: packed.files, config: config?.parts ?? [], baseSet: based, sessions: resumed, secrets: envSent });
8420
+ return ui2.json({ pod, dir: podDir, bytes: packed.archive.length, files: packed.files, skipped: packed.skipped, config: config?.parts ?? [], baseSet: based, sessions: resumed, secrets: envSent });
8297
8421
  }
8298
8422
  ui2.out(`${ui2.c("ok", "up")}: ${name} \u2192 ${pod.spec.name}:~/${podDir} \xB7 ${packed.files >= 0 ? `${packed.files} files, ` : ""}${bytes(packed.archive.length)}`);
8299
8423
  if (config) ui2.out(`config: ~/.claude/{${config.parts.join(",")}}`);
8424
+ if (packed.skipped.length) ui2.out(ui2.c("dim", `kept here: ${packed.skipped.join(", ")} (env files go to a pod only as secrets: --env${packed.skipped.includes(".env") ? "" : " <file>"})`));
8300
8425
  if (!based) ui2.err(ui2.ce("warn", "couldn't set the down base; the first down will include the files you sent"));
8301
8426
  if (envSent) {
8302
8427
  ui2.out(`secrets for ${pod.spec.name}: ${envSent.sent.length ? envSent.sent.join(", ") : "none"}${envSent.skipped.length ? ui2.c("dim", ` (skipped: ${envSent.skipped.join(", ")})`) : ""}`);
@@ -8340,9 +8465,9 @@ function printSessions(ctx, list2) {
8340
8465
  function printFiles(ctx, blocks, toErr) {
8341
8466
  const line2 = toErr ? ctx.ui.err : ctx.ui.out;
8342
8467
  const c = toErr ? ctx.ui.ce : ctx.ui.c;
8343
- const files = orderFiles(blocks.map((b2) => b2.file));
8344
- const width = Math.max(...files.map((f) => f.path.length + (f.from ? f.from.length + 4 : 0)));
8345
- for (const f of files) {
8468
+ const files2 = orderFiles(blocks.map((b2) => b2.file));
8469
+ const width = Math.max(...files2.map((f) => f.path.length + (f.from ? f.from.length + 4 : 0)));
8470
+ for (const f of files2) {
8346
8471
  const name = f.from ? `${f.from} \u2192 ${f.path}` : f.path;
8347
8472
  const mark = f.risky ? c("warn", "!") : " ";
8348
8473
  line2(` ${mark} ${STATUS_MARK[f.status]} ${f.risky ? name.padEnd(width) + " " + c("warn", f.reason ?? "") : name}`.trimEnd());
@@ -8393,9 +8518,9 @@ var down2 = {
8393
8518
  const { blocks, outside } = selectProject(parsePatch(d.patch), prefix);
8394
8519
  const refused = refusals(blocks);
8395
8520
  const patch = blocks.map((b2) => b2.text).join("");
8396
- const files = orderFiles(blocks.map((b2) => b2.file));
8521
+ const files2 = orderFiles(blocks.map((b2) => b2.file));
8397
8522
  if (ctx.json && (dry || !blocks.length)) {
8398
- return ui2.json({ pod: pod.id, base, files, outside, refused, patch, applied: false });
8523
+ return ui2.json({ pod: pod.id, base, files: files2, outside, refused, patch, applied: false });
8399
8524
  }
8400
8525
  const takeSessions = !dry && !bool(ctx.args, "no-session") && found?.state.podId === pod.id;
8401
8526
  if (!blocks.length) {
@@ -8404,8 +8529,8 @@ var down2 = {
8404
8529
  if (takeSessions && found) await downSessions(ctx, found.root, found.state, pod);
8405
8530
  return;
8406
8531
  }
8407
- const risky = files.filter((f) => f.risky).length;
8408
- const head = `${files.length} file(s) from ${pod.spec.name}${risky ? `, ${risky} risky` : ""}:`;
8532
+ const risky = files2.filter((f) => f.risky).length;
8533
+ const head = `${files2.length} file(s) from ${pod.spec.name}${risky ? `, ${risky} risky` : ""}:`;
8409
8534
  if (dry) {
8410
8535
  ui2.err(head);
8411
8536
  printFiles(ctx, blocks, true);
@@ -8440,7 +8565,7 @@ var down2 = {
8440
8565
  throw new CliError(`the patch doesn't apply here, nothing changed:
8441
8566
  ${check.stderr.trim()}`, "commit or stash local edits to those files, or look with: airgate down --dry-run");
8442
8567
  }
8443
- if (!await confirm(ctx, `apply ${files.length} file(s)${risky ? ` (${risky} risky)` : ""}?`)) throw new CliError("nothing applied");
8568
+ if (!await confirm(ctx, `apply ${files2.length} file(s)${risky ? ` (${risky} risky)` : ""}?`)) throw new CliError("nothing applied");
8444
8569
  const applied = git(top, ["apply", ...applyArgs], patch);
8445
8570
  if (applied.status !== 0) throw new CliError(`git apply failed: ${applied.stderr.trim()}`);
8446
8571
  let acked = true;
@@ -8450,8 +8575,8 @@ ${check.stderr.trim()}`, "commit or stash local edits to those files, or look wi
8450
8575
  acked = false;
8451
8576
  }
8452
8577
  const convo = takeSessions && found ? await downSessions(ctx, found.root, found.state, pod, true) : [];
8453
- if (ctx.json) return ui2.json({ pod: pod.id, base, files, outside, refused, applied: true, acked, sessions: convo });
8454
- ui2.out(`${ui2.c("ok", "down")}: applied ${files.length} file(s). review: git status \xB7 git diff`);
8578
+ if (ctx.json) return ui2.json({ pod: pod.id, base, files: files2, outside, refused, applied: true, acked, sessions: convo });
8579
+ ui2.out(`${ui2.c("ok", "down")}: applied ${files2.length} file(s). review: git status \xB7 git diff`);
8455
8580
  if (!acked) ui2.err(ui2.ce("warn", "couldn't tell the pod what was taken; the next down will show these again"));
8456
8581
  printSessions(ctx, convo);
8457
8582
  }
@@ -8512,7 +8637,7 @@ var LineReader = class {
8512
8637
  }
8513
8638
  };
8514
8639
  function ttyPrompt(stdin, out) {
8515
- return (question, opts2 = {}) => new Promise((resolve6, reject) => {
8640
+ return (question, opts2 = {}) => new Promise((resolve7, reject) => {
8516
8641
  out.write(question);
8517
8642
  const decoder = new StringDecoder2("utf8");
8518
8643
  let answer = "";
@@ -8525,7 +8650,7 @@ function ttyPrompt(stdin, out) {
8525
8650
  stdin.pause();
8526
8651
  out.write("\n");
8527
8652
  if (err) reject(err);
8528
- else resolve6(answer);
8653
+ else resolve7(answer);
8529
8654
  };
8530
8655
  const onData = (d) => {
8531
8656
  for (const ch of decoder.write(d)) {
@@ -8552,16 +8677,16 @@ function ttyPrompt(stdin, out) {
8552
8677
  }
8553
8678
  function openInBrowser(url) {
8554
8679
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
8555
- return new Promise((resolve6) => {
8680
+ return new Promise((resolve7) => {
8556
8681
  try {
8557
8682
  const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
8558
- child.on("error", () => resolve6(false));
8683
+ child.on("error", () => resolve7(false));
8559
8684
  child.on("spawn", () => {
8560
8685
  child.unref();
8561
- resolve6(true);
8686
+ resolve7(true);
8562
8687
  });
8563
8688
  } catch {
8564
- resolve6(false);
8689
+ resolve7(false);
8565
8690
  }
8566
8691
  });
8567
8692
  }
@@ -8655,7 +8780,7 @@ async function runCommand(ctx, cmd, argv, cwd) {
8655
8780
  }
8656
8781
 
8657
8782
  // src/commands/run.ts
8658
- import { existsSync as existsSync5, readFileSync as readFileSync7, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
8783
+ import { existsSync as existsSync5, readFileSync as readFileSync8, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
8659
8784
  import { basename as basename2, resolve as resolve5 } from "node:path";
8660
8785
  import { randomBytes } from "node:crypto";
8661
8786
 
@@ -8678,9 +8803,9 @@ function readSaved(env) {
8678
8803
  return {};
8679
8804
  }
8680
8805
  }
8681
- function saveLocal(env, vendor, value) {
8806
+ function saveLocal(env, vendor, value, kind = value ? kindOf(vendor, value) : "api_key") {
8682
8807
  const all = readSaved(env);
8683
- if (value) all[vendor] = { kind: kindOf(vendor, value), value };
8808
+ if (value) all[vendor] = { kind, value };
8684
8809
  else delete all[vendor];
8685
8810
  const dir = configDir(env);
8686
8811
  mkdirSync4(dir, { recursive: true, mode: 448 });
@@ -8721,6 +8846,14 @@ function sign(vendor, auth, headers) {
8721
8846
  }
8722
8847
  }
8723
8848
  var UPSTREAM = { anthropic: "https://api.anthropic.com", openai: "https://api.openai.com" };
8849
+ async function chatgptReady(io, auth, f) {
8850
+ const t = JSON.parse(auth.value);
8851
+ if (!chatgptNeedsRefresh(t)) return t;
8852
+ const fresh = await refreshChatgpt(t, { fetch: f, tokenUrl: io.env.AIRGATE_RELAY_CHATGPT_TOKEN_URL || void 0 });
8853
+ auth.value = JSON.stringify(fresh);
8854
+ if (auth.from === "this laptop") saveLocal(io.env, "openai", auth.value, "chatgpt");
8855
+ return fresh;
8856
+ }
8724
8857
  function startRelay(o) {
8725
8858
  const vendors = Object.keys(o.auth).filter((v) => o.auth[v]);
8726
8859
  const inflight = /* @__PURE__ */ new Map();
@@ -8736,11 +8869,15 @@ function startRelay(o) {
8736
8869
  const auth = o.auth[m.vendor];
8737
8870
  if (!auth) return send({ t: "error", id: m.id, message: `this laptop has no ${vendorLabel(m.vendor)} sign-in` });
8738
8871
  const headers = { ...m.headers };
8739
- sign(m.vendor, auth, headers);
8740
8872
  const ctl = new AbortController();
8741
8873
  inflight.set(m.id, ctl);
8742
8874
  try {
8743
- const res = await o.fetch(`${base(m.vendor)}${m.path}`, {
8875
+ let url = `${base(m.vendor)}${m.path}`;
8876
+ if (auth.kind === "chatgpt") {
8877
+ signChatgpt(await chatgptReady(o.io, auth, o.fetch), headers);
8878
+ url = chatgptUrl(m.path, o.io.env.AIRGATE_RELAY_CHATGPT_URL || CHATGPT_CODEX_BASE);
8879
+ } else sign(m.vendor, auth, headers);
8880
+ const res = await o.fetch(url, {
8744
8881
  method: m.method,
8745
8882
  headers,
8746
8883
  body: m.body ? Buffer.from(m.body, "base64") : void 0,
@@ -8809,9 +8946,13 @@ function startRelay(o) {
8809
8946
  }
8810
8947
 
8811
8948
  // src/commands/models.ts
8949
+ import { spawnSync as spawnSync2 } from "node:child_process";
8950
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync3 } from "node:fs";
8951
+ import { tmpdir as tmpdir2 } from "node:os";
8952
+ import { join as join6 } from "node:path";
8812
8953
  var HOW = {
8813
8954
  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"
8955
+ openai: "an OpenAI API key (for a ChatGPT plan: airgate models connect chatgpt)"
8815
8956
  };
8816
8957
  function relayIfSignedIn(ctx, say) {
8817
8958
  const auth = localAuth(ctx.io.env);
@@ -8822,19 +8963,66 @@ function relayIfSignedIn(ctx, say) {
8822
8963
  return null;
8823
8964
  }
8824
8965
  }
8966
+ var KIND = { oauth_token: "subscription token", api_key: "API key", chatgpt: "ChatGPT plan" };
8967
+ async function connectChatgpt(ctx, laptop) {
8968
+ const { ui: ui2 } = ctx;
8969
+ let text2;
8970
+ if (!ctx.io.stdin.isTTY) {
8971
+ text2 = await readValue(ctx, "a Codex auth.json", "cat auth.json | airgate models connect chatgpt");
8972
+ } else {
8973
+ if (spawnSync2("codex", ["--version"], { stdio: "ignore" }).status !== 0) {
8974
+ throw new CliError("Codex is not installed on this laptop", "npm i -g @openai/codex, then: airgate models connect chatgpt");
8975
+ }
8976
+ const home = mkdtempSync2(join6(tmpdir2(), "airgate-codex-"));
8977
+ try {
8978
+ ui2.err(ui2.ce("dim", "signing in to ChatGPT for airgate: a separate Codex login, so the one on this laptop stays as it is."));
8979
+ const args = ["login", ...bool(ctx.args, "device") ? ["--device-auth"] : [], "-c", 'cli_auth_credentials_store="file"'];
8980
+ const r2 = spawnSync2("codex", args, { stdio: "inherit", env: { ...process.env, ...ctx.io.env, CODEX_HOME: home } });
8981
+ if (r2.status !== 0) throw new CliError("the ChatGPT sign-in did not finish", "try again, or with --device");
8982
+ try {
8983
+ text2 = readFileSync7(join6(home, "auth.json"), "utf8");
8984
+ } catch {
8985
+ throw new CliError("Codex didn't write a login file", "update Codex (npm i -g @openai/codex) and try again");
8986
+ }
8987
+ } finally {
8988
+ rmSync3(home, { recursive: true, force: true });
8989
+ }
8990
+ }
8991
+ let tokens2;
8992
+ try {
8993
+ tokens2 = parseChatgptLogin(text2);
8994
+ } catch (e) {
8995
+ throw new CliError(e.message);
8996
+ }
8997
+ const value = JSON.stringify(tokens2);
8998
+ if (laptop) {
8999
+ saveLocal(ctx.io.env, "openai", value, "chatgpt");
9000
+ if (ctx.json) return ui2.json({ ok: true, vendor: "openai", kind: "chatgpt", where: "laptop" });
9001
+ ui2.out(`${ui2.c("ok", "saved")} on this laptop only (ChatGPT plan). airgate doesn't get it.`);
9002
+ return void ui2.out(ui2.c("dim", "pods' Codex uses it while this laptop is connected: airgate relay (airgate run and airgate mcp do this on their own)."));
9003
+ }
9004
+ const r = await ctx.api.connectModel("openai", value, "chatgpt");
9005
+ if (ctx.json) return ui2.json({ ...r, where: "airgate" });
9006
+ ui2.out(`${ui2.c("ok", "connected")}: ChatGPT plan for Codex, kept encrypted on airgate (renewed there) and never sent to a pod.`);
9007
+ ui2.out(ui2.c("dim", "undo: airgate models disconnect chatgpt"));
9008
+ }
8825
9009
  var models = {
8826
9010
  name: "models",
8827
9011
  group: "account",
8828
- usage: "[connect|disconnect <claude|openai> [--laptop]]",
9012
+ usage: "[connect|disconnect <claude|openai|chatgpt> [--laptop]]",
8829
9013
  summary: "how pods' agents sign in to their model: in the pod, through this laptop, or connected to airgate",
8830
9014
  example: "airgate models connect claude --laptop",
8831
9015
  notes: [
8832
9016
  "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.",
9017
+ "connect <claude|openai|chatgpt>: saves it on airgate. with --laptop: saves it only on this laptop (~/.config/airgate/models.json), for airgate relay.",
8834
9018
  "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.",
9019
+ "chatgpt (Codex on a ChatGPT plan) runs `codex login` into a fresh folder, so airgate gets its own Codex login and never the one on this laptop (sharing one would sign you out here). --device uses the device-code login. or pipe a Codex auth.json in.",
8835
9020
  "this laptop also uses CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY or OPENAI_API_KEY from its environment."
8836
9021
  ],
8837
- flags: { laptop: { type: "boolean", help: "keep the sign-in on this laptop only (for airgate relay)" } },
9022
+ flags: {
9023
+ laptop: { type: "boolean", help: "keep the sign-in on this laptop only (for airgate relay)" },
9024
+ device: { type: "boolean", help: "chatgpt: sign in with a device code instead of this browser" }
9025
+ },
8838
9026
  async run(ctx) {
8839
9027
  const { ui: ui2 } = ctx;
8840
9028
  const [action, who, extra] = ctx.args.positionals;
@@ -8853,6 +9041,7 @@ var models = {
8853
9041
  if (ctx.json) return ui2.json({ ...r2, vendor, where: "airgate" });
8854
9042
  return void ui2.out(r2.ok ? `${vendorLabel(vendor)}: disconnected from airgate. airgate no longer holds it.` : `${vendorLabel(vendor)} wasn't connected to airgate.`);
8855
9043
  }
9044
+ if (who?.toLowerCase() === "chatgpt") return connectChatgpt(ctx, laptop);
8856
9045
  if (!ctx.json && ctx.io.stdin.isTTY) ui2.err(ui2.ce("dim", `paste ${HOW[vendor]}.`));
8857
9046
  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
9047
  if (value.length < 20 || /\s/.test(value)) throw new CliError("that doesn't look like a key or token", HOW[vendor]);
@@ -8864,7 +9053,7 @@ var models = {
8864
9053
  }
8865
9054
  const r = await ctx.api.connectModel(vendor, value);
8866
9055
  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.`);
9056
+ ui2.out(`${ui2.c("ok", "connected")}: ${vendorLabel(vendor)} (${KIND[r.kind] ?? r.kind}), kept encrypted on airgate and never sent to a pod.`);
8868
9057
  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
9058
  }
8870
9059
  if (action) throw new UsageError(`unknown "${action}"`, "airgate models \xB7 airgate models connect claude [--laptop] \xB7 airgate models disconnect claude [--laptop]");
@@ -8876,8 +9065,8 @@ var models = {
8876
9065
  const l = local[v];
8877
9066
  return [
8878
9067
  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")
9068
+ c ? ui2.c("ok", `connected to airgate (${KIND[c.kind] ?? c.kind})`) : ui2.c("dim", "not on airgate"),
9069
+ l ? `this laptop: ${l.kind === "chatgpt" ? "ChatGPT plan" : l.from}${st.laptop.connected && st.laptop.vendors.includes(v) ? ui2.c("ok", " \xB7 relaying") : ""}` : ui2.c("dim", "not on this laptop")
8881
9070
  ];
8882
9071
  });
8883
9072
  ui2.table(rows2, { head: ["model", "airgate", "laptop"] });
@@ -8935,7 +9124,7 @@ async function signInWarning(ctx, agent, relaying) {
8935
9124
  const list2 = await ctx.api.secrets().catch(() => null);
8936
9125
  if (list2?.account.some((s2) => m.secrets.includes(s2.name))) return null;
8937
9126
  if (!st && !list2) return null;
8938
- const who = m.vendor === "anthropic" ? "claude" : "openai";
9127
+ const who = m.vendor === "anthropic" ? "claude" : "chatgpt";
8939
9128
  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
9129
  }
8941
9130
  async function finish(ctx, pod, keep) {
@@ -9029,11 +9218,11 @@ var runTask = {
9029
9218
  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
9219
  }
9031
9220
  const sp = statePath(projectDir);
9032
- const before = existsSync5(sp) ? readFileSync7(sp, "utf8") : null;
9221
+ const before = existsSync5(sp) ? readFileSync8(sp, "utf8") : null;
9033
9222
  const restoreState = () => {
9034
9223
  try {
9035
9224
  if (before !== null) writeFileSync5(sp, before);
9036
- else rmSync3(sp, { force: true });
9225
+ else rmSync4(sp, { force: true });
9037
9226
  } catch {
9038
9227
  }
9039
9228
  };
@@ -9073,20 +9262,20 @@ When you are done, leave your changes in the files there (no need to commit or p
9073
9262
  ui2.err(ui2.ce("bad", keepNote(`the task finished, but its changes didn't come back: ${why}`)));
9074
9263
  return 1;
9075
9264
  }
9076
- const files = result.files ?? [];
9265
+ const files2 = result.files ?? [];
9077
9266
  const keep = noApply || bool(ctx.args, "keep");
9078
9267
  await finish(ctx, pod, keep ? "sleep" : "destroy");
9079
9268
  if (!keep) restoreState();
9080
- if (ctx.json) return out({ applied: !noApply && files.length > 0, files, ...noApply ? { patch: result.patch ?? "" } : {}, pod_state: keep ? "sleeping" : "deleted" });
9269
+ if (ctx.json) return out({ applied: !noApply && files2.length > 0, files: files2, ...noApply ? { patch: result.patch ?? "" } : {}, pod_state: keep ? "sleeping" : "deleted" });
9081
9270
  ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
9082
9271
  if (noApply) {
9083
9272
  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}`);
9273
+ ui2.err(`${files2.length} file(s) changed, not applied. apply: airgate down --pod ${name} \xB7 delete the pod: airgate destroy ${name}`);
9085
9274
  return 0;
9086
9275
  }
9087
- const risky = files.filter((f) => f.risky).length;
9276
+ const risky = files2.filter((f) => f.risky).length;
9088
9277
  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")
9278
+ `${ui2.c("ok", "done")}: ${files2.length ? `${files2.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
9279
  );
9091
9280
  return 0;
9092
9281
  }
@@ -9147,9 +9336,19 @@ var TOOLS = [
9147
9336
  model: s("built-in model: an OpenRouter id, e.g. qwen/qwen3-coder"),
9148
9337
  repo: s("GitHub repo to clone, owner/name"),
9149
9338
  browser: b("add Chromium + Playwright the agent can drive"),
9150
- jobs: { type: "array", items: jobSchema, description: "autopilot: scheduled jobs" }
9339
+ jobs: { type: "array", items: jobSchema, description: "autopilot: scheduled jobs" },
9340
+ fromSnapshot: s("start from a saved snapshot (its name, or a share code): its workspace and settings")
9151
9341
  }),
9152
9342
  async call(ctx, a) {
9343
+ if (typeof a.fromSnapshot === "string" && a.fromSnapshot) {
9344
+ const o = {};
9345
+ if (typeof a.name === "string" && a.name) o.name = a.name;
9346
+ if (typeof a.agent === "string" && a.agent) o.agent = a.agent;
9347
+ if (a.browser === true) o.browser = true;
9348
+ const pod2 = await waitReady(ctx, await ctx.api.launchSnapshot(a.fromSnapshot, o));
9349
+ OWN.set(pod2.id, "launch");
9350
+ return { name: pod2.spec.name, id: pod2.id, state: pod2.state, spec: pod2.spec };
9351
+ }
9153
9352
  const argv = [];
9154
9353
  const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
9155
9354
  opt("name", a.name);
@@ -9380,6 +9579,62 @@ var TOOLS = [
9380
9579
  return { deleted: p.spec.name, id: p.id };
9381
9580
  }
9382
9581
  },
9582
+ {
9583
+ name: "pod_files",
9584
+ description: "List a folder of a pod's workspace (path relative to ~/workspace; '' for the top), live or at a checkpoint. Looking at a checkpoint never rewinds the pod.",
9585
+ inputSchema: obj({ pod: POD, path: s("folder inside the workspace (default: the top)"), checkpoint: s("a checkpoint id or label (default: the live workspace)") }, ["pod"]),
9586
+ async call(ctx, a) {
9587
+ const p = await resolvePod(ctx, need(a, "pod"));
9588
+ const cp = typeof a.checkpoint === "string" && a.checkpoint ? resolveCheckpoint(await ctx.api.checkpoints(p.id), a.checkpoint).id : void 0;
9589
+ return ctx.api.files(p.id, typeof a.path === "string" ? a.path : "", cp);
9590
+ }
9591
+ },
9592
+ {
9593
+ name: "pod_file_read",
9594
+ description: "Read one file of a pod's workspace, live or at a checkpoint (e.g. to compare with how it was before). Text up to 256 KB comes back as text; anything else as base64.",
9595
+ inputSchema: obj({ pod: POD, path: s("file inside the workspace"), checkpoint: s("a checkpoint id or label (default: live)") }, ["pod", "path"]),
9596
+ async call(ctx, a) {
9597
+ const p = await resolvePod(ctx, need(a, "pod"));
9598
+ const cp = typeof a.checkpoint === "string" && a.checkpoint ? resolveCheckpoint(await ctx.api.checkpoints(p.id), a.checkpoint).id : void 0;
9599
+ const body = await ctx.api.fileRaw(p.id, need(a, "path"), cp);
9600
+ const text2 = body.toString("utf8");
9601
+ if (body.length <= 256 * 1024 && !text2.includes("\uFFFD") && !body.includes(0)) return { path: a.path, bytes: body.length, text: text2 };
9602
+ return { path: a.path, bytes: body.length, base64: body.length <= 2 * 1024 * 1024 ? body.toString("base64") : void 0, note: body.length > 2 * 1024 * 1024 ? "too big to return here: airgate pull" : void 0 };
9603
+ }
9604
+ },
9605
+ {
9606
+ name: "snapshot_save",
9607
+ description: "Save a pod's workspace (live, or at a checkpoint) as a named snapshot new pods can start from (pod_launch with fromSnapshot). Only files: never secrets, conversations or the masking vault.",
9608
+ inputSchema: obj({ pod: POD, name: s("a name for it"), checkpoint: s("save it as it was at this checkpoint (default: live)") }, ["pod", "name"]),
9609
+ async call(ctx, a) {
9610
+ const p = await resolvePod(ctx, need(a, "pod"));
9611
+ const cp = typeof a.checkpoint === "string" && a.checkpoint ? resolveCheckpoint(await ctx.api.checkpoints(p.id), a.checkpoint).id : void 0;
9612
+ return ctx.api.saveSnapshot(p.id, need(a, "name"), cp);
9613
+ }
9614
+ },
9615
+ {
9616
+ name: "snapshots_list",
9617
+ description: "Your saved snapshots (name, state, which pod they came from, whether shared).",
9618
+ inputSchema: obj({}),
9619
+ async call(ctx) {
9620
+ return ctx.api.snapshots();
9621
+ }
9622
+ },
9623
+ {
9624
+ name: "pod_settings",
9625
+ description: "Change a pod's network after launch (open | allowlist | off, extra allowlist hosts) or switch its AI off/on (aiOff: no model calls at all). Applies immediately. Only when the user asked for it.",
9626
+ inputSchema: obj({ pod: POD, network: s("open | allowlist | off", { enum: ["open", "allowlist", "off"] }), allow: { type: "array", items: { type: "string" }, description: "extra allowlist hosts (replaces the list)" }, aiOff: b("true switches the pod's AI off; false back on") }, ["pod"]),
9627
+ async call(ctx, a) {
9628
+ const p = await resolvePod(ctx, need(a, "pod"));
9629
+ const patch = {};
9630
+ if (typeof a.network === "string") patch.network = a.network;
9631
+ if (Array.isArray(a.allow)) patch.allow = a.allow.map(String);
9632
+ if (typeof a.aiOff === "boolean") patch.aiOff = a.aiOff;
9633
+ if (!Object.keys(patch).length) throw new UsageError("nothing to change: network, allow or aiOff");
9634
+ const pod = await ctx.api.updatePod(p.id, patch);
9635
+ return { name: pod.spec.name, network: pod.spec.network, allow: pod.spec.allow ?? [], aiOff: Boolean(pod.spec.aiOff) };
9636
+ }
9637
+ },
9383
9638
  {
9384
9639
  name: "pod_ports",
9385
9640
  description: "What is listening in a pod (dev servers on ports 1024+) and its preview links.",
@@ -9493,7 +9748,7 @@ var mcp = {
9493
9748
  // src/commands/misc.ts
9494
9749
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
9495
9750
  import { homedir as homedir3 } from "node:os";
9496
- import { join as join6 } from "node:path";
9751
+ import { join as join7 } from "node:path";
9497
9752
  var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
9498
9753
  var open = {
9499
9754
  name: "open",
@@ -9569,12 +9824,12 @@ var skill = {
9569
9824
  if (!text2.startsWith("---")) throw new CliError(`${url} isn't a skill file`);
9570
9825
  if (action === "show") return void ctx.io.stdout.write(text2);
9571
9826
  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");
9827
+ const base = bool(ctx.args, "project") ? join7(ctx.io.cwd, ".claude") : join7(ctx.io.env.HOME ?? homedir3(), ".claude");
9828
+ const dir = join7(base, "skills", "airgate");
9574
9829
  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")}`);
9830
+ writeFileSync6(join7(dir, "SKILL.md"), text2);
9831
+ if (ctx.json) return ctx.ui.json({ installed: join7(dir, "SKILL.md") });
9832
+ ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join7(dir, "SKILL.md")}`);
9578
9833
  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
9834
  }
9580
9835
  };
@@ -9727,7 +9982,7 @@ var race = {
9727
9982
  async run(ctx) {
9728
9983
  const [ref, cpRef = "current"] = ctx.args.positionals;
9729
9984
  const p = await resolvePod(ctx, ref);
9730
- const checkpointId = cpRef === "current" ? "current" : resolveCheckpoint(await ctx.api.checkpoints(p.id), cpRef).id;
9985
+ const checkpointId2 = cpRef === "current" ? "current" : resolveCheckpoint(await ctx.api.checkpoints(p.id), cpRef).id;
9731
9986
  const overrides = {};
9732
9987
  const agent = str(ctx.args, "agent");
9733
9988
  if (agent) overrides.agent = agent;
@@ -9735,7 +9990,7 @@ var race = {
9735
9990
  if (name) overrides.name = name;
9736
9991
  let pod;
9737
9992
  try {
9738
- pod = await ctx.ui.spin(`forking ${p.spec.name}`, async () => waitReady(ctx, await ctx.api.fork(p.id, checkpointId, overrides)));
9993
+ pod = await ctx.ui.spin(`forking ${p.spec.name}`, async () => waitReady(ctx, await ctx.api.fork(p.id, checkpointId2, overrides)));
9739
9994
  } catch (e) {
9740
9995
  if (e instanceof ApiError && e.code === "checkpoint_not_mounted") {
9741
9996
  throw new CliError("that checkpoint can't be read on the provider right now", `race from the live workspace: airgate race ${p.spec.name} current (or rewind first)`);
@@ -9744,7 +9999,7 @@ var race = {
9744
9999
  }
9745
10000
  if (ctx.json) return ctx.ui.json(pod);
9746
10001
  ctx.ui.out(readyLine(ctx, pod));
9747
- ctx.ui.out(`racing ${p.spec.name} from ${checkpointId === "current" ? "its current state" : `"${cpRef}"`}. compare: airgate down --pod ${pod.spec.name} --dry-run`);
10002
+ ctx.ui.out(`racing ${p.spec.name} from ${checkpointId2 === "current" ? "its current state" : `"${cpRef}"`}. compare: airgate down --pod ${pod.spec.name} --dry-run`);
9748
10003
  }
9749
10004
  };
9750
10005
 
@@ -10008,19 +10263,19 @@ function validateForm(f) {
10008
10263
  return e;
10009
10264
  }
10010
10265
  function formSpec(f, catalog = null) {
10011
- const network = NETWORK_LIST[f.network];
10266
+ const network2 = NETWORK_LIST[f.network];
10012
10267
  const template = formTemplate(f);
10013
10268
  const spec = {
10014
10269
  name: f.name.trim(),
10015
10270
  agent: AGENT_LIST[f.agent],
10016
10271
  size: SIZE_LIST[f.memory],
10017
10272
  masking: f.mask,
10018
- network,
10273
+ network: network2,
10019
10274
  autoStopMinutes: null,
10020
10275
  autoCheckpoint: f.undo
10021
10276
  };
10022
10277
  if (f.repo.trim()) spec.repo = f.repo.trim();
10023
- if (network === "allowlist") spec.allow = DEFAULT_ALLOW2;
10278
+ if (network2 === "allowlist") spec.allow = DEFAULT_ALLOW2;
10024
10279
  if (template !== "code") spec.template = template;
10025
10280
  if (f.browser) spec.browser = true;
10026
10281
  if (template === "autopilot") spec.jobs = [{ cron: formCron(f), task: f.task.trim(), enabled: true }];
@@ -11297,6 +11552,171 @@ var ui = {
11297
11552
  }
11298
11553
  };
11299
11554
 
11555
+ // src/commands/workspace.ts
11556
+ import { writeFileSync as writeFileSync7, existsSync as existsSync6, statSync as statSync4 } from "node:fs";
11557
+ import { basename as basename3, resolve as resolve6 } from "node:path";
11558
+ async function checkpointId(ctx, podId, ref) {
11559
+ if (!ref || ref === "current") return void 0;
11560
+ return resolveCheckpoint(await ctx.api.checkpoints(podId), ref).id;
11561
+ }
11562
+ var files = {
11563
+ name: "files",
11564
+ aliases: ["ls-files"],
11565
+ group: "pods",
11566
+ usage: "<pod> [path] [--at <checkpoint>]",
11567
+ summary: "what's in a pod's workspace, live or at a checkpoint (nothing is rewound)",
11568
+ example: 'airgate files web src --at "#2"',
11569
+ notes: ["--at takes a checkpoint id, its label, or #1 for the newest (airgate checkpoints <pod>). get a file with: airgate pull <pod> <path>"],
11570
+ flags: { at: { type: "string", value: "checkpoint", help: "look at a checkpoint instead of the live workspace" } },
11571
+ async run(ctx) {
11572
+ const [ref, path = ""] = ctx.args.positionals;
11573
+ if (!ref) throw new UsageError("which pod? airgate files <pod> [path]");
11574
+ const pod = await resolvePod(ctx, ref);
11575
+ const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
11576
+ const r = await ctx.api.files(pod.id, path, at3);
11577
+ if (ctx.json) return ctx.ui.json(r);
11578
+ if (!r.entries.length) return void ctx.ui.out(ctx.ui.c("dim", `${r.path || "~/workspace"} is empty`));
11579
+ ctx.ui.table(
11580
+ r.entries.map((e) => [e.type === "dir" ? `${e.name}/` : e.name, e.type === "dir" ? "" : ctx.ui.c("dim", bytes(e.size))]),
11581
+ { head: [`${pod.spec.name}:~/workspace/${r.path}${at3 ? ` @ ${str(ctx.args, "at")}` : ""}`, ""] }
11582
+ );
11583
+ }
11584
+ };
11585
+ var pull = {
11586
+ name: "pull",
11587
+ group: "pods",
11588
+ usage: "<pod> <path> [--at <checkpoint>] [-o <file>]",
11589
+ summary: "copy one file from a pod (live or at a checkpoint) to this laptop",
11590
+ example: 'airgate pull web src/app.ts --at "before the refactor"',
11591
+ notes: ["up to 25 MB. without -o it lands in this folder under its own name (never over an existing file unless --force). -o - prints it."],
11592
+ flags: {
11593
+ at: { type: "string", value: "checkpoint", help: "from a checkpoint instead of the live workspace" },
11594
+ out: { type: "string", short: "o", value: "file", help: "where to save it (- prints it)" },
11595
+ force: { type: "boolean", help: "overwrite an existing file" }
11596
+ },
11597
+ async run(ctx) {
11598
+ const [ref, path] = ctx.args.positionals;
11599
+ if (!ref || !path) throw new UsageError("airgate pull <pod> <path>");
11600
+ const pod = await resolvePod(ctx, ref);
11601
+ const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
11602
+ const body = await ctx.ui.spin(`reading ${path}`, ctx.api.fileRaw(pod.id, path, at3));
11603
+ const out = str(ctx.args, "out") ?? basename3(path);
11604
+ if (out === "-") return void ctx.io.stdout.write(body);
11605
+ const dest = resolve6(ctx.io.cwd, out);
11606
+ if (existsSync6(dest) && !bool(ctx.args, "force")) {
11607
+ if (statSync4(dest).isDirectory()) return void save2(ctx, resolve6(dest, basename3(path)), body);
11608
+ throw new CliError(`${out} exists here`, "pick another name with -o, or --force to overwrite");
11609
+ }
11610
+ save2(ctx, dest, body);
11611
+ }
11612
+ };
11613
+ function save2(ctx, dest, body) {
11614
+ writeFileSync7(dest, body);
11615
+ if (ctx.json) return ctx.ui.json({ saved: dest, bytes: body.length });
11616
+ ctx.ui.out(`${ctx.ui.c("ok", "saved")} ${dest} (${bytes(body.length)})`);
11617
+ }
11618
+ var ai = {
11619
+ name: "ai",
11620
+ group: "pods",
11621
+ usage: "<pod> on|off",
11622
+ summary: "switch a pod's AI off (no model calls at all) or back on",
11623
+ example: "airgate ai web off",
11624
+ notes: ["off: airgate refuses the pod's model calls, and on allowlist or off networks the model hosts are closed too (an open network can still reach them directly)."],
11625
+ async run(ctx) {
11626
+ const [ref, state] = ctx.args.positionals;
11627
+ if (!ref || state !== "on" && state !== "off") throw new UsageError("airgate ai <pod> on|off");
11628
+ const pod = await resolvePod(ctx, ref);
11629
+ const p = await ctx.api.updatePod(pod.id, { aiOff: state === "off" });
11630
+ if (ctx.json) return ctx.ui.json(p);
11631
+ ctx.ui.out(state === "off" ? `${ctx.ui.c("warn", "AI off")} for ${p.spec.name}: no model calls.${p.spec.network === "open" ? ctx.ui.c("dim", " (its network is open, so it could still reach a model directly: airgate network " + p.spec.name + " allowlist)") : ""}` : `${ctx.ui.c("ok", "AI on")} for ${p.spec.name}.`);
11632
+ }
11633
+ };
11634
+ var network = {
11635
+ name: "network",
11636
+ group: "pods",
11637
+ usage: "<pod> [open|allowlist|off] [--allow host,host] [--add host] [--remove host]",
11638
+ summary: "see or change what a pod can reach on the internet, now",
11639
+ example: "airgate network web allowlist --add api.stripe.com",
11640
+ notes: [
11641
+ "allowlist: common package registries and GitHub, plus your hosts; off: only the agent's model and airgate; open: anything.",
11642
+ "the agent's own model and sign-in hosts stay reachable unless its AI is off."
11643
+ ],
11644
+ flags: {
11645
+ allow: { type: "string", value: "hosts", help: "replace the extra hosts (comma separated)" },
11646
+ add: { type: "string", value: "host", help: "add hosts (comma separated)" },
11647
+ remove: { type: "string", value: "host", help: "remove hosts (comma separated)" }
11648
+ },
11649
+ async run(ctx) {
11650
+ const [ref, mode] = ctx.args.positionals;
11651
+ if (!ref) throw new UsageError("airgate network <pod> [open|allowlist|off]");
11652
+ if (mode && !["open", "allowlist", "off"].includes(mode)) throw new UsageError("open, allowlist or off");
11653
+ const pod = await resolvePod(ctx, ref);
11654
+ const list2 = (v) => (v ?? "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
11655
+ let allow = pod.spec.allow ?? [];
11656
+ if (str(ctx.args, "allow") !== void 0) allow = list2(str(ctx.args, "allow"));
11657
+ allow = [.../* @__PURE__ */ new Set([...allow, ...list2(str(ctx.args, "add"))])].filter((h) => !list2(str(ctx.args, "remove")).includes(h));
11658
+ const changed = mode || ["allow", "add", "remove"].some((k) => str(ctx.args, k) !== void 0);
11659
+ const p = changed ? await ctx.api.updatePod(pod.id, { ...mode ? { network: mode } : {}, allow }) : pod;
11660
+ if (ctx.json) return ctx.ui.json({ network: p.spec.network, allow: p.spec.allow ?? [], aiOff: Boolean(p.spec.aiOff) });
11661
+ ctx.ui.out(`${p.spec.name}: ${p.spec.network}${p.spec.network === "allowlist" && (p.spec.allow ?? []).length ? ` + ${(p.spec.allow ?? []).join(", ")}` : ""}${p.spec.aiOff ? ctx.ui.c("warn", " \xB7 AI off") : ""}${changed ? ctx.ui.c("dim", " (applied now)") : ""}`);
11662
+ }
11663
+ };
11664
+ var snapshot2 = {
11665
+ name: "snapshot",
11666
+ aliases: ["snapshots"],
11667
+ group: "undo \xB7 race",
11668
+ usage: "[<pod> <name> [--at <checkpoint>] | share|unshare|rm <name> | show <code>]",
11669
+ summary: "save a pod as a snapshot new pods start from; share it by code",
11670
+ example: 'airgate snapshot web "clean base"',
11671
+ notes: [
11672
+ 'a snapshot is a frozen copy of the workspace (live, or at a checkpoint): it stays when the pod is deleted. start a pod from it: airgate launch --from "<name>" (or a share code).',
11673
+ "only the workspace is copied: never the pod's secrets, its agent's conversations or its masking vault. share gives a code anyone signed in to airgate can launch from; unshare stops it.",
11674
+ "with no arguments: your snapshots."
11675
+ ],
11676
+ flags: { at: { type: "string", value: "checkpoint", help: "save the workspace as it was at a checkpoint" }, yes: { type: "boolean", short: "y", help: "don't ask" } },
11677
+ async run(ctx) {
11678
+ const { ui: ui2 } = ctx;
11679
+ const [a, b2, extra] = ctx.args.positionals;
11680
+ if (extra) throw new UsageError(`unexpected "${extra}" (put names with spaces in quotes)`);
11681
+ if (!a) {
11682
+ const list2 = await ctx.api.snapshots();
11683
+ if (ctx.json) return ui2.json(list2);
11684
+ if (!list2.length) return void ui2.out(ui2.c("dim", "no snapshots yet. save one: airgate snapshot <pod> <name>"));
11685
+ ui2.table(
11686
+ list2.map((s3) => [s3.name, s3.state === "ready" ? ui2.c("ok", "ready") : s3.state === "failed" ? ui2.c("bad", "failed") : ui2.c("warn", "saving"), `from ${s3.from.podName}`, s3.shareCode ? ui2.c("warn", `shared: ${s3.shareCode}`) : ui2.c("dim", "private")]),
11687
+ { head: ["snapshot", "", "", ""] }
11688
+ );
11689
+ return void ui2.out(ui2.c("dim", 'start a pod from one: airgate launch --from "<name>"'));
11690
+ }
11691
+ if (a === "share" || a === "unshare") {
11692
+ if (!b2) throw new UsageError(`airgate snapshot ${a} <name>`);
11693
+ const s3 = await ctx.api.shareSnapshot(b2, a === "share");
11694
+ if (ctx.json) return ui2.json(s3);
11695
+ if (a === "unshare") return void ui2.out(`${s3.name}: private again; its old code no longer works.`);
11696
+ ui2.out(`${s3.name}: share code ${s3.shareCode ?? ""}`);
11697
+ return void ui2.out(ui2.c("dim", `anyone signed in to airgate can start a pod from it: airgate launch --from ${s3.shareCode}. it shares only the workspace files.`));
11698
+ }
11699
+ if (a === "rm" || a === "delete") {
11700
+ if (!b2) throw new UsageError("airgate snapshot rm <name>");
11701
+ if (!await confirm(ctx, `delete the snapshot "${b2}"? pods already started from it keep their files.`)) throw new CliError("nothing deleted");
11702
+ await ctx.api.deleteSnapshot(b2);
11703
+ return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`deleted ${b2}.`);
11704
+ }
11705
+ if (a === "show") {
11706
+ if (!b2) throw new UsageError("airgate snapshot show <code>");
11707
+ const s3 = await ctx.api.sharedSnapshot(b2);
11708
+ if (ctx.json) return ui2.json(s3);
11709
+ return void ui2.out(`${s3.name} \xB7 ${s3.agent} \xB7 ${s3.template}${s3.state !== "ready" ? ` \xB7 ${s3.state}` : ""}. start a pod from it: airgate launch --from ${b2}`);
11710
+ }
11711
+ if (!b2) throw new UsageError("airgate snapshot <pod> <name> (or: airgate snapshots)");
11712
+ const pod = await resolvePod(ctx, a);
11713
+ const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
11714
+ const s2 = await ui2.spin(`saving ${pod.spec.name} as "${b2}"`, ctx.api.saveSnapshot(pod.id, b2, at3));
11715
+ if (ctx.json) return ui2.json(s2);
11716
+ ui2.out(`${ui2.c("ok", "saved")} "${s2.name}" from ${pod.spec.name}${at3 ? ` at ${str(ctx.args, "at")}` : ""}. ${ui2.c("dim", `start a pod from it: airgate launch --from "${s2.name}" \xB7 share it: airgate snapshot share "${s2.name}"`)}`);
11717
+ }
11718
+ };
11719
+
11300
11720
  // src/cli.ts
11301
11721
  var help = {
11302
11722
  name: "help",
@@ -11353,6 +11773,11 @@ var COMMANDS = [
11353
11773
  runTask,
11354
11774
  models,
11355
11775
  relay,
11776
+ files,
11777
+ pull,
11778
+ snapshot2,
11779
+ ai,
11780
+ network,
11356
11781
  usage,
11357
11782
  plan,
11358
11783
  upgrade,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgate",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Give your AI its own computer: the airgate command line.",
5
5
  "type": "module",
6
6
  "bin": {