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 +1 -0
- package/dist/airgate.mjs +511 -86
- package/package.json +1 -1
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,
|
|
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,
|
|
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,
|
|
4392
|
-
if (
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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((
|
|
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
|
-
|
|
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
|
|
7741
|
-
|
|
7742
|
-
const
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
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 =
|
|
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
|
|
7947
|
+
const files2 = [];
|
|
7824
7948
|
const walk = (d, depth) => {
|
|
7825
7949
|
let list2 = [];
|
|
7826
7950
|
try {
|
|
7827
|
-
list2 =
|
|
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))
|
|
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
|
|
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
|
|
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(
|
|
8090
|
-
return [...
|
|
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
|
|
8344
|
-
const width = Math.max(...
|
|
8345
|
-
for (const f of
|
|
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
|
|
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 =
|
|
8408
|
-
const head = `${
|
|
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 ${
|
|
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 ${
|
|
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((
|
|
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
|
|
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((
|
|
8680
|
+
return new Promise((resolve7) => {
|
|
8556
8681
|
try {
|
|
8557
8682
|
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
8558
|
-
child.on("error", () =>
|
|
8683
|
+
child.on("error", () => resolve7(false));
|
|
8559
8684
|
child.on("spawn", () => {
|
|
8560
8685
|
child.unref();
|
|
8561
|
-
|
|
8686
|
+
resolve7(true);
|
|
8562
8687
|
});
|
|
8563
8688
|
} catch {
|
|
8564
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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
|
|
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" : "
|
|
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) ?
|
|
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
|
|
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
|
|
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 &&
|
|
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(`${
|
|
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 =
|
|
9276
|
+
const risky = files2.filter((f) => f.risky).length;
|
|
9088
9277
|
ui2.out(
|
|
9089
|
-
`${ui2.c("ok", "done")}: ${
|
|
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
|
|
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") ?
|
|
9573
|
-
const dir =
|
|
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(
|
|
9576
|
-
if (ctx.json) return ctx.ui.json({ installed:
|
|
9577
|
-
ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${
|
|
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
|
|
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,
|
|
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 ${
|
|
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
|
|
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 (
|
|
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,
|