airgate 0.1.2 → 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 +4 -2
  2. package/dist/airgate.mjs +418 -122
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,10 +37,12 @@ with the result back here, or the whole project and conversation up and back.
37
37
  airgate run "get the failing tests passing" # a fresh pod; the result comes back as a patch
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
- claude setup-token # then: airgate secrets set CLAUDE_CODE_OAUTH_TOKEN
40
+ airgate models connect claude --laptop # a token from `claude setup-token`, kept on this laptop
41
41
  ```
42
42
 
43
- The last line signs Claude Code in for every pod, so tasks run without a login each time.
43
+ The last line lets this laptop sign the pods' model calls while airgate runs, so tasks run without
44
+ a login in each pod and airgate never holds your sign-in. Or keep one on airgate so pods work while
45
+ the laptop is off: `airgate models connect claude`. Guide: https://airgate.sh/docs/model-sign-in
44
46
  Guide: https://airgate.sh/docs/local-agents
45
47
 
46
48
  ## Links
package/dist/airgate.mjs CHANGED
@@ -514,6 +514,20 @@ var Api = class {
514
514
  githubDisconnect(id) {
515
515
  return this.request("DELETE", `/v1/github/installations/${id}`);
516
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
+ }
517
531
  secrets() {
518
532
  return this.request("GET", "/v1/secrets");
519
533
  }
@@ -4374,8 +4388,8 @@ function linked(s2, j, b2, ascii) {
4374
4388
  }
4375
4389
  return m;
4376
4390
  }
4377
- function place3(s2, j, c, k, join6, a = ARMS.get(c), ascii = false, round = false) {
4378
- if (join6 && 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) {
4379
4393
  const old = s2.chars[j], b2 = ascii ? asciiArms(old) : ARMS.get(old);
4380
4394
  if (b2 !== void 0) {
4381
4395
  const kept = linked(s2, j, b2, ascii);
@@ -4541,7 +4555,7 @@ function rect(s2, x, y, w, h, o) {
4541
4555
  if (title)
4542
4556
  for (const ch of title)
4543
4557
  code(ch);
4544
- const join6 = yes(o?.join, "join", true);
4558
+ const join7 = yes(o?.join, "join", true);
4545
4559
  const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
4546
4560
  if (!finite3(x, y, w, h))
4547
4561
  return { x: 0, y: 0, cols: 0, rows: 0 };
@@ -4564,7 +4578,7 @@ function rect(s2, x, y, w, h, o) {
4564
4578
  const put = (cx, cy, e, reach = 255) => {
4565
4579
  const j = s2.index(cx, cy), a = ascii ? EDGE_ARMS[e] : ARMS.get(ed[e]);
4566
4580
  if (j >= 0)
4567
- place3(s2, j, ed[e], k, join6, a === void 0 ? a : a & reach, ascii, round);
4581
+ place3(s2, j, ed[e], k, join7, a === void 0 ? a : a & reach, ascii, round);
4568
4582
  };
4569
4583
  const end = (i, n2, first, last) => n2 < 2 ? 255 : i === 0 ? 3 << first : i === n2 - 1 ? 3 << last : 255;
4570
4584
  const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi, n2) => Math.min(hi, n2 - o2);
@@ -4700,11 +4714,11 @@ function shape(s2, cx, cy, rx, ry, o, what, from = 0, span = 1) {
4700
4714
  const ch = char === "auto" ? "auto" : brush(char, `${/^[aeiou]/.test(what) ? "an" : "a"} ${what}'s char`);
4701
4715
  const f = fill === void 0 || fill === false ? null : brush(fill, "fill");
4702
4716
  const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
4703
- const join6 = yes(o?.join, "join", true);
4717
+ const join7 = yes(o?.join, "join", true);
4704
4718
  ellipseCells(s2.cols, s2.rows, cx, cy, rx, ry, f !== null, (x, y, edge, slope) => {
4705
4719
  const j = y * s2.cols + x;
4706
4720
  if (edge)
4707
- place3(s2, j, ch === "auto" ? slope : ch, k, join6 && ch !== "auto");
4721
+ place3(s2, j, ch === "auto" ? slope : ch, k, join7 && ch !== "auto");
4708
4722
  else
4709
4723
  s2.put(j, f, f === EMPTY ? NONE : fk);
4710
4724
  }, ch === "auto" ? s2.aspect : 0, from, span);
@@ -7019,14 +7033,14 @@ async function readAll2(stream) {
7019
7033
  for await (const c of stream) s2 += c.toString();
7020
7034
  return s2;
7021
7035
  }
7022
- async function readValue(ctx, name) {
7036
+ async function readValue(ctx, name, pipeHint = `printf %s "$VALUE" | airgate secrets set ${name}`) {
7023
7037
  if (ctx.io.stdin.isTTY) {
7024
7038
  const v2 = await ctx.io.prompt(`value for ${name} (hidden): `, { hidden: true });
7025
7039
  if (!v2) throw new CliError("empty value, nothing saved");
7026
7040
  return v2;
7027
7041
  }
7028
7042
  const v = (await readAll2(ctx.io.stdin)).replace(/\r?\n$/, "");
7029
- 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);
7030
7044
  return v;
7031
7045
  }
7032
7046
  function parseDotenv2(text2) {
@@ -7648,7 +7662,7 @@ var log = {
7648
7662
  import { createInterface } from "node:readline";
7649
7663
 
7650
7664
  // src/version.ts
7651
- var VERSION = "0.1.2";
7665
+ var VERSION = "0.1.3";
7652
7666
 
7653
7667
  // src/commands/updown.ts
7654
7668
  import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
@@ -8641,22 +8655,288 @@ async function runCommand(ctx, cmd, argv, cwd) {
8641
8655
  }
8642
8656
 
8643
8657
  // src/commands/run.ts
8644
- import { existsSync as existsSync4, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
8658
+ import { existsSync as existsSync5, readFileSync as readFileSync7, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
8645
8659
  import { basename as basename2, resolve as resolve5 } from "node:path";
8646
8660
  import { randomBytes } from "node:crypto";
8647
- var SIGN_IN = {
8648
- "claude-code": { names: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"], fix: "claude setup-token (on this laptop), then: airgate secrets set CLAUDE_CODE_OAUTH_TOKEN" },
8649
- codex: { names: ["OPENAI_API_KEY"], fix: "airgate secrets set OPENAI_API_KEY (or sign in to codex once in a pod you keep)" }
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"] }
8650
8927
  };
8651
8928
  var shortId = () => randomBytes(2).toString("hex");
8652
8929
  var runName = (base) => `${base.slice(0, 52)}-run-${shortId()}`;
8653
- async function signInWarning(ctx, agent) {
8654
- const need2 = SIGN_IN[agent];
8655
- if (!need2) return null;
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;
8656
8935
  const list2 = await ctx.api.secrets().catch(() => null);
8657
- if (!list2) return null;
8658
- if (list2.account.some((s2) => need2.names.includes(s2.name))) return null;
8659
- return `no ${need2.names.join(" or ")} secret: ${AGENTS[agent].label} may not be signed in inside the pod. fix once for every pod: ${need2.fix}`;
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)`;
8660
8940
  }
8661
8941
  async function finish(ctx, pod, keep) {
8662
8942
  if (keep === "destroy") await ctx.ui.spin(`deleting ${pod.spec.name}`, ctx.api.destroy(pod.id));
@@ -8673,7 +8953,7 @@ var runTask = {
8673
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).",
8674
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).",
8675
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.",
8676
- "the agent needs a sign-in that works in any pod: for Claude Code run `claude setup-token`, then `airgate secrets set CLAUDE_CODE_OAUTH_TOKEN`."
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"
8677
8957
  ],
8678
8958
  flags: {
8679
8959
  repo: { type: "string", value: "owner/name", help: "run on a GitHub repo instead of this folder" },
@@ -8698,109 +8978,118 @@ var runTask = {
8698
8978
  if (!Number.isFinite(minutes) || minutes <= 0) throw new UsageError("--timeout: minutes, e.g. 30");
8699
8979
  const repo = str(ctx.args, "repo");
8700
8980
  const warnings = [];
8701
- const warn = await signInWarning(ctx, agent);
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 ?? []);
8702
8984
  if (warn) {
8703
8985
  warnings.push(warn);
8704
8986
  if (!ctx.json) ui2.err(ui2.ce("warn", warn));
8705
8987
  }
8706
- const work = async (pod2, text3) => {
8707
- let r2 = await ui2.spin(`handing ${pod2.spec.name} the task`, ctx.api.startTask(pod2.id, text3));
8708
- r2 = await ui2.spin(`${pod2.spec.name} is working (${AGENTS[agent].label})`, () => waitRun(ctx, pod2.id, r2, minutes * 6e4));
8709
- const log2 = await ctx.api.runLog(pod2.id, r2.id).catch(() => "");
8710
- return { run: r2, log: log2, done: r2.endedAt !== void 0, ok: r2.endedAt !== void 0 && r2.exitCode === 0 };
8711
- };
8712
- if (repo) {
8713
- if (dirArg) throw new UsageError("a folder or --repo, not both");
8714
- const look = await ctx.api.inspectRepo(repo).catch((e) => {
8715
- if (e instanceof ApiError && e.status === 404) return null;
8716
- throw e;
8717
- });
8718
- if (look && !look.reachable) {
8719
- const gh = await ctx.api.github().catch(() => null);
8720
- 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"');
8721
- 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");
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;
8722
9024
  }
8723
- const name2 = runName((look?.repo ?? repo).split("/").pop() ?? "repo");
8724
- const spec = { name: name2, agent, size: size2, repo: look?.repo ?? repo, masking: true, network: "allowlist", allow: DEFAULT_ALLOW, autoStopMinutes: null, autoCheckpoint: true };
8725
- const pod2 = await ui2.spin(`launching ${name2} on ${spec.repo}`, async () => waitReady(ctx, await ctx.api.launch(spec)));
8726
- const envFile2 = str(ctx.args, "env");
8727
- if (envFile2) await ui2.spin(`sending ${envFile2} as secrets`, sendDotenv(ctx, envFile2, pod2.id));
8728
- 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.");
8729
- const r2 = await work(pod2, text3);
8730
- await finish(ctx, pod2, r2.done ? "sleep" : "running");
8731
- 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 });
8732
- ctx.io.stdout.write(r2.log.endsWith("\n") || !r2.log ? r2.log : r2.log + "\n");
8733
- if (!r2.done) ui2.err(ui2.ce("warn", `still working after ${minutes} min; it carries on. follow it: airgate log ${name2}`));
8734
- 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" : ""}`);
8735
- return r2.ok ? 0 : 1;
8736
- }
8737
- const projectDir = resolve5(ctx.io.cwd, dirArg ?? ".");
8738
- if (!existsSync4(projectDir) || !statSync3(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
8739
- const noApply = bool(ctx.args, "no-apply");
8740
- if (!noApply && !gitTopLevel(projectDir)) {
8741
- 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");
8742
- }
8743
- const sp = statePath(projectDir);
8744
- const before = existsSync4(sp) ? readFileSync6(sp, "utf8") : null;
8745
- const restoreState = () => {
8746
- try {
8747
- if (before !== null) writeFileSync4(sp, before);
8748
- else rmSync3(sp, { force: true });
8749
- } catch {
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");
8750
9030
  }
8751
- };
8752
- const name = runName(podDirName(projectDir));
8753
- const upArgv = [projectDir, "--name", name, "--agent", agent, "--memory", size2, "--no-session", "--yes"];
8754
- const envFile = str(ctx.args, "env");
8755
- if (envFile) upArgv.push("--env", envFile);
8756
- const sent = await ui2.spin(`sending ${basename2(projectDir)} to a fresh pod`, runCommand(ctx, up, upArgv, ctx.io.cwd));
8757
- const pod = sent.pod;
8758
- const text2 = `Work in ~/${sent.dir} (a project sent from my laptop). ${task2}
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}
8759
9047
 
8760
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.`;
8761
- let r;
8762
- try {
8763
- r = await work(pod, text2);
8764
- } catch (e) {
8765
- await finish(ctx, pod, "sleep");
8766
- throw e;
8767
- }
8768
- 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}`;
8769
- const out = (o) => ui2.json({ pod: { id: pod.id, name }, run: r.run, ok: r.ok, log: r.log, warnings, ...o });
8770
- if (!r.ok) {
8771
- await finish(ctx, pod, r.done ? "sleep" : "running");
8772
- if (ctx.json) return out({ applied: false, pod_state: r.done ? "sleeping" : "running" });
8773
- ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
8774
- 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}`));
8775
- return 1;
8776
- }
8777
- let result;
8778
- try {
8779
- result = await ui2.spin(`bringing the changes back from ${name}`, runCommand(ctx, down2, ["--pod", name, "--no-session", ...noApply ? ["--dry-run"] : ["--yes"]], projectDir));
8780
- } catch (e) {
8781
- await finish(ctx, pod, "sleep");
8782
- const why = e instanceof Error ? e.message.split("\n")[0] : String(e);
8783
- if (ctx.json) return out({ applied: false, error: why, pod_state: "sleeping" });
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" });
8784
9081
  ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
8785
- ui2.err(ui2.ce("bad", keepNote(`the task finished, but its changes didn't come back: ${why}`)));
8786
- return 1;
8787
- }
8788
- const files = result.files ?? [];
8789
- const keep = noApply || bool(ctx.args, "keep");
8790
- await finish(ctx, pod, keep ? "sleep" : "destroy");
8791
- if (!keep) restoreState();
8792
- if (ctx.json) return out({ applied: !noApply && files.length > 0, files, ...noApply ? { patch: result.patch ?? "" } : {}, pod_state: keep ? "sleeping" : "deleted" });
8793
- ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
8794
- if (noApply) {
8795
- ctx.io.stdout.write(result.patch ?? "");
8796
- ui2.err(`${files.length} file(s) changed, not applied. apply: airgate down --pod ${name} \xB7 delete the pod: airgate destroy ${name}`);
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
+ );
8797
9091
  return 0;
8798
9092
  }
8799
- const risky = files.filter((f) => f.risky).length;
8800
- ui2.out(
8801
- `${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")
8802
- );
8803
- return 0;
8804
9093
  }
8805
9094
  };
8806
9095
 
@@ -9176,6 +9465,10 @@ var mcp = {
9176
9465
  const rl = createInterface({ input: ctx.io.stdin, crlfDelay: Infinity, terminal: false });
9177
9466
  const pending = /* @__PURE__ */ new Set();
9178
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
+ `);
9179
9472
  for await (const line2 of rl) {
9180
9473
  if (!line2.trim()) continue;
9181
9474
  let msg;
@@ -9193,13 +9486,14 @@ var mcp = {
9193
9486
  void job2.finally(() => pending.delete(job2));
9194
9487
  }
9195
9488
  await Promise.all(pending);
9489
+ relayer?.stop();
9196
9490
  }
9197
9491
  };
9198
9492
 
9199
9493
  // src/commands/misc.ts
9200
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
9494
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
9201
9495
  import { homedir as homedir3 } from "node:os";
9202
- import { join as join5 } from "node:path";
9496
+ import { join as join6 } from "node:path";
9203
9497
  var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
9204
9498
  var open = {
9205
9499
  name: "open",
@@ -9275,12 +9569,12 @@ var skill = {
9275
9569
  if (!text2.startsWith("---")) throw new CliError(`${url} isn't a skill file`);
9276
9570
  if (action === "show") return void ctx.io.stdout.write(text2);
9277
9571
  if (action !== "install") throw new UsageError(`unknown action "${action}"`, "airgate skill install [--project] | airgate skill show");
9278
- const base = bool(ctx.args, "project") ? join5(ctx.io.cwd, ".claude") : join5(ctx.io.env.HOME ?? homedir3(), ".claude");
9279
- const dir = join5(base, "skills", "airgate");
9280
- mkdirSync4(dir, { recursive: true });
9281
- writeFileSync5(join5(dir, "SKILL.md"), text2);
9282
- if (ctx.json) return ctx.ui.json({ installed: join5(dir, "SKILL.md") });
9283
- ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join5(dir, "SKILL.md")}`);
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")}`);
9284
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".'));
9285
9579
  }
9286
9580
  };
@@ -10585,7 +10879,7 @@ function formModal(c, s2, f) {
10585
10879
  const t = TEMPLATES[formTemplate(f)];
10586
10880
  const builtin = formBuiltin(f);
10587
10881
  const plan2 = s2.me?.plan === "pro" ? "pro" : "payg";
10588
- const models = s2.catalog?.models ?? [];
10882
+ const models2 = s2.catalog?.models ?? [];
10589
10883
  fields.forEach((id, i) => {
10590
10884
  const y = b2.y + 1 + i;
10591
10885
  if (y >= b2.y + b2.h - 3) return;
@@ -10623,7 +10917,7 @@ function formModal(c, s2, f) {
10623
10917
  v = `[${f.builtin ? "x" : " "}] built-in model, no subscription`;
10624
10918
  break;
10625
10919
  case "model":
10626
- 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`;
10627
10921
  break;
10628
10922
  case "schedule": {
10629
10923
  const p = SCHEDULE_LIST[f.schedule];
@@ -11057,6 +11351,8 @@ var COMMANDS = [
11057
11351
  up,
11058
11352
  down2,
11059
11353
  runTask,
11354
+ models,
11355
+ relay,
11060
11356
  usage,
11061
11357
  plan,
11062
11358
  upgrade,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgate",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Give your AI its own computer: the airgate command line.",
5
5
  "type": "module",
6
6
  "bin": {