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.
- package/README.md +4 -2
- package/dist/airgate.mjs +418 -122
- 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
|
|
40
|
+
airgate models connect claude --laptop # a token from `claude setup-token`, kept on this laptop
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The last line
|
|
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,
|
|
4378
|
-
if (
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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",
|
|
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.
|
|
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
|
|
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
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
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
|
|
8655
|
-
if (!
|
|
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 (
|
|
8658
|
-
if (
|
|
8659
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
}
|
|
8718
|
-
if (
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
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
|
|
8724
|
-
|
|
8725
|
-
const
|
|
8726
|
-
|
|
8727
|
-
|
|
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
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
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
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
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
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
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
|
|
9494
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9201
9495
|
import { homedir as homedir3 } from "node:os";
|
|
9202
|
-
import { join as
|
|
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") ?
|
|
9279
|
-
const dir =
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
if (ctx.json) return ctx.ui.json({ installed:
|
|
9283
|
-
ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${
|
|
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
|
|
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 ${
|
|
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,
|