airgate 0.1.3 → 0.1.4
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 +180 -40
- 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 {
|
|
@@ -518,8 +584,8 @@ var Api = class {
|
|
|
518
584
|
modelsAuth() {
|
|
519
585
|
return this.request("GET", "/v1/models/auth");
|
|
520
586
|
}
|
|
521
|
-
connectModel(vendor, value) {
|
|
522
|
-
return this.request("PUT", `/v1/models/auth/${enc(vendor)}`, { body: { value } });
|
|
587
|
+
connectModel(vendor, value, kind) {
|
|
588
|
+
return this.request("PUT", `/v1/models/auth/${enc(vendor)}`, { body: { value, ...kind ? { kind } : {} } });
|
|
523
589
|
}
|
|
524
590
|
disconnectModel(vendor) {
|
|
525
591
|
return this.request("DELETE", `/v1/models/auth/${enc(vendor)}`);
|
|
@@ -4388,8 +4454,8 @@ function linked(s2, j, b2, ascii) {
|
|
|
4388
4454
|
}
|
|
4389
4455
|
return m;
|
|
4390
4456
|
}
|
|
4391
|
-
function place3(s2, j, c, k,
|
|
4392
|
-
if (
|
|
4457
|
+
function place3(s2, j, c, k, join8, a = ARMS.get(c), ascii = false, round = false) {
|
|
4458
|
+
if (join8 && a !== void 0) {
|
|
4393
4459
|
const old = s2.chars[j], b2 = ascii ? asciiArms(old) : ARMS.get(old);
|
|
4394
4460
|
if (b2 !== void 0) {
|
|
4395
4461
|
const kept = linked(s2, j, b2, ascii);
|
|
@@ -4555,7 +4621,7 @@ function rect(s2, x, y, w, h, o) {
|
|
|
4555
4621
|
if (title)
|
|
4556
4622
|
for (const ch of title)
|
|
4557
4623
|
code(ch);
|
|
4558
|
-
const
|
|
4624
|
+
const join8 = yes(o?.join, "join", true);
|
|
4559
4625
|
const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
|
|
4560
4626
|
if (!finite3(x, y, w, h))
|
|
4561
4627
|
return { x: 0, y: 0, cols: 0, rows: 0 };
|
|
@@ -4578,7 +4644,7 @@ function rect(s2, x, y, w, h, o) {
|
|
|
4578
4644
|
const put = (cx, cy, e, reach = 255) => {
|
|
4579
4645
|
const j = s2.index(cx, cy), a = ascii ? EDGE_ARMS[e] : ARMS.get(ed[e]);
|
|
4580
4646
|
if (j >= 0)
|
|
4581
|
-
place3(s2, j, ed[e], k,
|
|
4647
|
+
place3(s2, j, ed[e], k, join8, a === void 0 ? a : a & reach, ascii, round);
|
|
4582
4648
|
};
|
|
4583
4649
|
const end = (i, n2, first, last) => n2 < 2 ? 255 : i === 0 ? 3 << first : i === n2 - 1 ? 3 << last : 255;
|
|
4584
4650
|
const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi, n2) => Math.min(hi, n2 - o2);
|
|
@@ -4714,11 +4780,11 @@ function shape(s2, cx, cy, rx, ry, o, what, from = 0, span = 1) {
|
|
|
4714
4780
|
const ch = char === "auto" ? "auto" : brush(char, `${/^[aeiou]/.test(what) ? "an" : "a"} ${what}'s char`);
|
|
4715
4781
|
const f = fill === void 0 || fill === false ? null : brush(fill, "fill");
|
|
4716
4782
|
const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
|
|
4717
|
-
const
|
|
4783
|
+
const join8 = yes(o?.join, "join", true);
|
|
4718
4784
|
ellipseCells(s2.cols, s2.rows, cx, cy, rx, ry, f !== null, (x, y, edge, slope) => {
|
|
4719
4785
|
const j = y * s2.cols + x;
|
|
4720
4786
|
if (edge)
|
|
4721
|
-
place3(s2, j, ch === "auto" ? slope : ch, k,
|
|
4787
|
+
place3(s2, j, ch === "auto" ? slope : ch, k, join8 && ch !== "auto");
|
|
4722
4788
|
else
|
|
4723
4789
|
s2.put(j, f, f === EMPTY ? NONE : fk);
|
|
4724
4790
|
}, ch === "auto" ? s2.aspect : 0, from, span);
|
|
@@ -7662,7 +7728,7 @@ var log = {
|
|
|
7662
7728
|
import { createInterface } from "node:readline";
|
|
7663
7729
|
|
|
7664
7730
|
// src/version.ts
|
|
7665
|
-
var VERSION = "0.1.
|
|
7731
|
+
var VERSION = "0.1.4";
|
|
7666
7732
|
|
|
7667
7733
|
// src/commands/updown.ts
|
|
7668
7734
|
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 +7737,7 @@ import { basename, dirname as dirname2, isAbsolute, join as join4, relative as r
|
|
|
7671
7737
|
|
|
7672
7738
|
// src/pack.ts
|
|
7673
7739
|
import { spawn, spawnSync } from "node:child_process";
|
|
7674
|
-
import { existsSync, lstatSync } from "node:fs";
|
|
7740
|
+
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
7675
7741
|
import { join as join2 } from "node:path";
|
|
7676
7742
|
var UPLOAD_LIMIT = 32 * 6 * 1024 * 1024;
|
|
7677
7743
|
var TooLargeError = class extends Error {
|
|
@@ -7681,6 +7747,11 @@ var TooLargeError = class extends Error {
|
|
|
7681
7747
|
this.limit = limit;
|
|
7682
7748
|
}
|
|
7683
7749
|
};
|
|
7750
|
+
function isEnvFile(path) {
|
|
7751
|
+
const name = path.split("/").pop() ?? "";
|
|
7752
|
+
return /^\.env($|\.)/.test(name) && !/\.(example|sample|template|dist|defaults?|schema)$/.test(name);
|
|
7753
|
+
}
|
|
7754
|
+
var ENV_EXCLUDES = [".env", ".env.local", ".env.*.local", ".env.development", ".env.production", ".env.test", ".env.staging", ".envrc"].map((p) => `--exclude=${p}`);
|
|
7684
7755
|
function git(cwd, args, input) {
|
|
7685
7756
|
const r = spawnSync("git", args, { cwd, input, maxBuffer: 256 * 1024 * 1024 });
|
|
7686
7757
|
return { status: r.status ?? (r.error ? 127 : 1), stdout: r.stdout?.toString("utf8") ?? "", stderr: r.stderr?.toString("utf8") ?? "", error: r.error };
|
|
@@ -7737,13 +7808,17 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
|
7737
7808
|
}
|
|
7738
7809
|
async function packProject(dir, limit = UPLOAD_LIMIT) {
|
|
7739
7810
|
if (isGitRepo(dir)) {
|
|
7740
|
-
const
|
|
7811
|
+
const tracked = new Set(git(dir, ["ls-files", "-c", "-z"]).stdout.split("\0"));
|
|
7812
|
+
const all = gitFiles(dir).filter((f) => !f.startsWith(".airgate/"));
|
|
7813
|
+
const skipped2 = all.filter((f) => (isEnvFile(f) || f.split("/").pop() === ".envrc") && !tracked.has(f));
|
|
7814
|
+
const files = all.filter((f) => !skipped2.includes(f));
|
|
7741
7815
|
if (!files.length) throw new Error("no files to send (git sees nothing tracked or untracked here)");
|
|
7742
7816
|
const archive2 = await runTar(dir, ["--null", "-T", "-", "-czf", "-"], files.join("\0") + "\0", limit);
|
|
7743
|
-
return { archive: archive2, files: files.length, git: true };
|
|
7817
|
+
return { archive: archive2, files: files.length, git: true, skipped: skipped2 };
|
|
7744
7818
|
}
|
|
7745
|
-
const
|
|
7746
|
-
|
|
7819
|
+
const skipped = readdirSync(dir).filter((f) => isEnvFile(f) || f === ".envrc");
|
|
7820
|
+
const archive = await runTar(dir, ["--exclude=node_modules", "--exclude=.git", "--exclude=.airgate", ...ENV_EXCLUDES, "-czf", "-", "."], null, limit);
|
|
7821
|
+
return { archive, files: -1, git: false, skipped };
|
|
7747
7822
|
}
|
|
7748
7823
|
var CLAUDE_CONFIG = ["settings.json", "CLAUDE.md", "skills", "agents", "commands"];
|
|
7749
7824
|
async function packClaudeConfig(home, limit = UPLOAD_LIMIT) {
|
|
@@ -7765,7 +7840,7 @@ import { gunzipSync } from "node:zlib";
|
|
|
7765
7840
|
|
|
7766
7841
|
// src/sessions.ts
|
|
7767
7842
|
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";
|
|
7843
|
+
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
7844
|
import { tmpdir } from "node:os";
|
|
7770
7845
|
import { dirname, join as join3, relative } from "node:path";
|
|
7771
7846
|
var POD_HOME = "/home/sprite";
|
|
@@ -7793,7 +7868,7 @@ function findClaudeSession(home, projectDir, id, env = {}) {
|
|
|
7793
7868
|
const dir = join3(claudeHome(home, env), "projects", claudeKey(p));
|
|
7794
7869
|
let names = [];
|
|
7795
7870
|
try {
|
|
7796
|
-
names =
|
|
7871
|
+
names = readdirSync2(dir).filter((n2) => n2.endsWith(".jsonl"));
|
|
7797
7872
|
} catch {
|
|
7798
7873
|
continue;
|
|
7799
7874
|
}
|
|
@@ -7824,7 +7899,7 @@ function codexRollouts(root, limit = 3e3) {
|
|
|
7824
7899
|
const walk = (d, depth) => {
|
|
7825
7900
|
let list2 = [];
|
|
7826
7901
|
try {
|
|
7827
|
-
list2 =
|
|
7902
|
+
list2 = readdirSync2(d, { withFileTypes: true });
|
|
7828
7903
|
} catch {
|
|
7829
7904
|
return;
|
|
7830
7905
|
}
|
|
@@ -7879,7 +7954,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
7879
7954
|
names.push(`${s2.id}.jsonl`);
|
|
7880
7955
|
if (s2.sidecar) {
|
|
7881
7956
|
const copy2 = (src, dst) => {
|
|
7882
|
-
for (const x of
|
|
7957
|
+
for (const x of readdirSync2(src, { withFileTypes: true })) {
|
|
7883
7958
|
const a = join3(src, x.name);
|
|
7884
7959
|
const b2 = join3(dst, x.name);
|
|
7885
7960
|
if (x.isDirectory()) {
|
|
@@ -8293,10 +8368,11 @@ var up = {
|
|
|
8293
8368
|
const state = { gateway: ctx.cfg.gateway, podId: pod.id, podName: pod.spec.name, dir: podDir, at: Date.now(), ...sent.length ? { sessions: sent } : {} };
|
|
8294
8369
|
writeState(projectDir, state);
|
|
8295
8370
|
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 });
|
|
8371
|
+
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
8372
|
}
|
|
8298
8373
|
ui2.out(`${ui2.c("ok", "up")}: ${name} \u2192 ${pod.spec.name}:~/${podDir} \xB7 ${packed.files >= 0 ? `${packed.files} files, ` : ""}${bytes(packed.archive.length)}`);
|
|
8299
8374
|
if (config) ui2.out(`config: ~/.claude/{${config.parts.join(",")}}`);
|
|
8375
|
+
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
8376
|
if (!based) ui2.err(ui2.ce("warn", "couldn't set the down base; the first down will include the files you sent"));
|
|
8301
8377
|
if (envSent) {
|
|
8302
8378
|
ui2.out(`secrets for ${pod.spec.name}: ${envSent.sent.length ? envSent.sent.join(", ") : "none"}${envSent.skipped.length ? ui2.c("dim", ` (skipped: ${envSent.skipped.join(", ")})`) : ""}`);
|
|
@@ -8655,7 +8731,7 @@ async function runCommand(ctx, cmd, argv, cwd) {
|
|
|
8655
8731
|
}
|
|
8656
8732
|
|
|
8657
8733
|
// src/commands/run.ts
|
|
8658
|
-
import { existsSync as existsSync5, readFileSync as
|
|
8734
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8659
8735
|
import { basename as basename2, resolve as resolve5 } from "node:path";
|
|
8660
8736
|
import { randomBytes } from "node:crypto";
|
|
8661
8737
|
|
|
@@ -8678,9 +8754,9 @@ function readSaved(env) {
|
|
|
8678
8754
|
return {};
|
|
8679
8755
|
}
|
|
8680
8756
|
}
|
|
8681
|
-
function saveLocal(env, vendor, value) {
|
|
8757
|
+
function saveLocal(env, vendor, value, kind = value ? kindOf(vendor, value) : "api_key") {
|
|
8682
8758
|
const all = readSaved(env);
|
|
8683
|
-
if (value) all[vendor] = { kind
|
|
8759
|
+
if (value) all[vendor] = { kind, value };
|
|
8684
8760
|
else delete all[vendor];
|
|
8685
8761
|
const dir = configDir(env);
|
|
8686
8762
|
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
@@ -8721,6 +8797,14 @@ function sign(vendor, auth, headers) {
|
|
|
8721
8797
|
}
|
|
8722
8798
|
}
|
|
8723
8799
|
var UPSTREAM = { anthropic: "https://api.anthropic.com", openai: "https://api.openai.com" };
|
|
8800
|
+
async function chatgptReady(io, auth, f) {
|
|
8801
|
+
const t = JSON.parse(auth.value);
|
|
8802
|
+
if (!chatgptNeedsRefresh(t)) return t;
|
|
8803
|
+
const fresh = await refreshChatgpt(t, { fetch: f, tokenUrl: io.env.AIRGATE_RELAY_CHATGPT_TOKEN_URL || void 0 });
|
|
8804
|
+
auth.value = JSON.stringify(fresh);
|
|
8805
|
+
if (auth.from === "this laptop") saveLocal(io.env, "openai", auth.value, "chatgpt");
|
|
8806
|
+
return fresh;
|
|
8807
|
+
}
|
|
8724
8808
|
function startRelay(o) {
|
|
8725
8809
|
const vendors = Object.keys(o.auth).filter((v) => o.auth[v]);
|
|
8726
8810
|
const inflight = /* @__PURE__ */ new Map();
|
|
@@ -8736,11 +8820,15 @@ function startRelay(o) {
|
|
|
8736
8820
|
const auth = o.auth[m.vendor];
|
|
8737
8821
|
if (!auth) return send({ t: "error", id: m.id, message: `this laptop has no ${vendorLabel(m.vendor)} sign-in` });
|
|
8738
8822
|
const headers = { ...m.headers };
|
|
8739
|
-
sign(m.vendor, auth, headers);
|
|
8740
8823
|
const ctl = new AbortController();
|
|
8741
8824
|
inflight.set(m.id, ctl);
|
|
8742
8825
|
try {
|
|
8743
|
-
|
|
8826
|
+
let url = `${base(m.vendor)}${m.path}`;
|
|
8827
|
+
if (auth.kind === "chatgpt") {
|
|
8828
|
+
signChatgpt(await chatgptReady(o.io, auth, o.fetch), headers);
|
|
8829
|
+
url = chatgptUrl(m.path, o.io.env.AIRGATE_RELAY_CHATGPT_URL || CHATGPT_CODEX_BASE);
|
|
8830
|
+
} else sign(m.vendor, auth, headers);
|
|
8831
|
+
const res = await o.fetch(url, {
|
|
8744
8832
|
method: m.method,
|
|
8745
8833
|
headers,
|
|
8746
8834
|
body: m.body ? Buffer.from(m.body, "base64") : void 0,
|
|
@@ -8809,9 +8897,13 @@ function startRelay(o) {
|
|
|
8809
8897
|
}
|
|
8810
8898
|
|
|
8811
8899
|
// src/commands/models.ts
|
|
8900
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8901
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync3 } from "node:fs";
|
|
8902
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
8903
|
+
import { join as join6 } from "node:path";
|
|
8812
8904
|
var HOW = {
|
|
8813
8905
|
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"
|
|
8906
|
+
openai: "an OpenAI API key (for a ChatGPT plan: airgate models connect chatgpt)"
|
|
8815
8907
|
};
|
|
8816
8908
|
function relayIfSignedIn(ctx, say) {
|
|
8817
8909
|
const auth = localAuth(ctx.io.env);
|
|
@@ -8822,19 +8914,66 @@ function relayIfSignedIn(ctx, say) {
|
|
|
8822
8914
|
return null;
|
|
8823
8915
|
}
|
|
8824
8916
|
}
|
|
8917
|
+
var KIND = { oauth_token: "subscription token", api_key: "API key", chatgpt: "ChatGPT plan" };
|
|
8918
|
+
async function connectChatgpt(ctx, laptop) {
|
|
8919
|
+
const { ui: ui2 } = ctx;
|
|
8920
|
+
let text2;
|
|
8921
|
+
if (!ctx.io.stdin.isTTY) {
|
|
8922
|
+
text2 = await readValue(ctx, "a Codex auth.json", "cat auth.json | airgate models connect chatgpt");
|
|
8923
|
+
} else {
|
|
8924
|
+
if (spawnSync2("codex", ["--version"], { stdio: "ignore" }).status !== 0) {
|
|
8925
|
+
throw new CliError("Codex is not installed on this laptop", "npm i -g @openai/codex, then: airgate models connect chatgpt");
|
|
8926
|
+
}
|
|
8927
|
+
const home = mkdtempSync2(join6(tmpdir2(), "airgate-codex-"));
|
|
8928
|
+
try {
|
|
8929
|
+
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."));
|
|
8930
|
+
const args = ["login", ...bool(ctx.args, "device") ? ["--device-auth"] : [], "-c", 'cli_auth_credentials_store="file"'];
|
|
8931
|
+
const r2 = spawnSync2("codex", args, { stdio: "inherit", env: { ...process.env, ...ctx.io.env, CODEX_HOME: home } });
|
|
8932
|
+
if (r2.status !== 0) throw new CliError("the ChatGPT sign-in did not finish", "try again, or with --device");
|
|
8933
|
+
try {
|
|
8934
|
+
text2 = readFileSync7(join6(home, "auth.json"), "utf8");
|
|
8935
|
+
} catch {
|
|
8936
|
+
throw new CliError("Codex didn't write a login file", "update Codex (npm i -g @openai/codex) and try again");
|
|
8937
|
+
}
|
|
8938
|
+
} finally {
|
|
8939
|
+
rmSync3(home, { recursive: true, force: true });
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
let tokens2;
|
|
8943
|
+
try {
|
|
8944
|
+
tokens2 = parseChatgptLogin(text2);
|
|
8945
|
+
} catch (e) {
|
|
8946
|
+
throw new CliError(e.message);
|
|
8947
|
+
}
|
|
8948
|
+
const value = JSON.stringify(tokens2);
|
|
8949
|
+
if (laptop) {
|
|
8950
|
+
saveLocal(ctx.io.env, "openai", value, "chatgpt");
|
|
8951
|
+
if (ctx.json) return ui2.json({ ok: true, vendor: "openai", kind: "chatgpt", where: "laptop" });
|
|
8952
|
+
ui2.out(`${ui2.c("ok", "saved")} on this laptop only (ChatGPT plan). airgate doesn't get it.`);
|
|
8953
|
+
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)."));
|
|
8954
|
+
}
|
|
8955
|
+
const r = await ctx.api.connectModel("openai", value, "chatgpt");
|
|
8956
|
+
if (ctx.json) return ui2.json({ ...r, where: "airgate" });
|
|
8957
|
+
ui2.out(`${ui2.c("ok", "connected")}: ChatGPT plan for Codex, kept encrypted on airgate (renewed there) and never sent to a pod.`);
|
|
8958
|
+
ui2.out(ui2.c("dim", "undo: airgate models disconnect chatgpt"));
|
|
8959
|
+
}
|
|
8825
8960
|
var models = {
|
|
8826
8961
|
name: "models",
|
|
8827
8962
|
group: "account",
|
|
8828
|
-
usage: "[connect|disconnect <claude|openai> [--laptop]]",
|
|
8963
|
+
usage: "[connect|disconnect <claude|openai|chatgpt> [--laptop]]",
|
|
8829
8964
|
summary: "how pods' agents sign in to their model: in the pod, through this laptop, or connected to airgate",
|
|
8830
8965
|
example: "airgate models connect claude --laptop",
|
|
8831
8966
|
notes: [
|
|
8832
8967
|
"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.",
|
|
8968
|
+
"connect <claude|openai|chatgpt>: saves it on airgate. with --laptop: saves it only on this laptop (~/.config/airgate/models.json), for airgate relay.",
|
|
8834
8969
|
"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.",
|
|
8970
|
+
"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
8971
|
"this laptop also uses CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY or OPENAI_API_KEY from its environment."
|
|
8836
8972
|
],
|
|
8837
|
-
flags: {
|
|
8973
|
+
flags: {
|
|
8974
|
+
laptop: { type: "boolean", help: "keep the sign-in on this laptop only (for airgate relay)" },
|
|
8975
|
+
device: { type: "boolean", help: "chatgpt: sign in with a device code instead of this browser" }
|
|
8976
|
+
},
|
|
8838
8977
|
async run(ctx) {
|
|
8839
8978
|
const { ui: ui2 } = ctx;
|
|
8840
8979
|
const [action, who, extra] = ctx.args.positionals;
|
|
@@ -8853,6 +8992,7 @@ var models = {
|
|
|
8853
8992
|
if (ctx.json) return ui2.json({ ...r2, vendor, where: "airgate" });
|
|
8854
8993
|
return void ui2.out(r2.ok ? `${vendorLabel(vendor)}: disconnected from airgate. airgate no longer holds it.` : `${vendorLabel(vendor)} wasn't connected to airgate.`);
|
|
8855
8994
|
}
|
|
8995
|
+
if (who?.toLowerCase() === "chatgpt") return connectChatgpt(ctx, laptop);
|
|
8856
8996
|
if (!ctx.json && ctx.io.stdin.isTTY) ui2.err(ui2.ce("dim", `paste ${HOW[vendor]}.`));
|
|
8857
8997
|
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
8998
|
if (value.length < 20 || /\s/.test(value)) throw new CliError("that doesn't look like a key or token", HOW[vendor]);
|
|
@@ -8864,7 +9004,7 @@ var models = {
|
|
|
8864
9004
|
}
|
|
8865
9005
|
const r = await ctx.api.connectModel(vendor, value);
|
|
8866
9006
|
if (ctx.json) return ui2.json({ ...r, where: "airgate" });
|
|
8867
|
-
ui2.out(`${ui2.c("ok", "connected")}: ${vendorLabel(vendor)} (${r.kind
|
|
9007
|
+
ui2.out(`${ui2.c("ok", "connected")}: ${vendorLabel(vendor)} (${KIND[r.kind] ?? r.kind}), kept encrypted on airgate and never sent to a pod.`);
|
|
8868
9008
|
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
9009
|
}
|
|
8870
9010
|
if (action) throw new UsageError(`unknown "${action}"`, "airgate models \xB7 airgate models connect claude [--laptop] \xB7 airgate models disconnect claude [--laptop]");
|
|
@@ -8876,8 +9016,8 @@ var models = {
|
|
|
8876
9016
|
const l = local[v];
|
|
8877
9017
|
return [
|
|
8878
9018
|
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")
|
|
9019
|
+
c ? ui2.c("ok", `connected to airgate (${KIND[c.kind] ?? c.kind})`) : ui2.c("dim", "not on airgate"),
|
|
9020
|
+
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
9021
|
];
|
|
8882
9022
|
});
|
|
8883
9023
|
ui2.table(rows2, { head: ["model", "airgate", "laptop"] });
|
|
@@ -8935,7 +9075,7 @@ async function signInWarning(ctx, agent, relaying) {
|
|
|
8935
9075
|
const list2 = await ctx.api.secrets().catch(() => null);
|
|
8936
9076
|
if (list2?.account.some((s2) => m.secrets.includes(s2.name))) return null;
|
|
8937
9077
|
if (!st && !list2) return null;
|
|
8938
|
-
const who = m.vendor === "anthropic" ? "claude" : "
|
|
9078
|
+
const who = m.vendor === "anthropic" ? "claude" : "chatgpt";
|
|
8939
9079
|
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
9080
|
}
|
|
8941
9081
|
async function finish(ctx, pod, keep) {
|
|
@@ -9029,11 +9169,11 @@ var runTask = {
|
|
|
9029
9169
|
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
9170
|
}
|
|
9031
9171
|
const sp = statePath(projectDir);
|
|
9032
|
-
const before = existsSync5(sp) ?
|
|
9172
|
+
const before = existsSync5(sp) ? readFileSync8(sp, "utf8") : null;
|
|
9033
9173
|
const restoreState = () => {
|
|
9034
9174
|
try {
|
|
9035
9175
|
if (before !== null) writeFileSync5(sp, before);
|
|
9036
|
-
else
|
|
9176
|
+
else rmSync4(sp, { force: true });
|
|
9037
9177
|
} catch {
|
|
9038
9178
|
}
|
|
9039
9179
|
};
|
|
@@ -9493,7 +9633,7 @@ var mcp = {
|
|
|
9493
9633
|
// src/commands/misc.ts
|
|
9494
9634
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9495
9635
|
import { homedir as homedir3 } from "node:os";
|
|
9496
|
-
import { join as
|
|
9636
|
+
import { join as join7 } from "node:path";
|
|
9497
9637
|
var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
|
|
9498
9638
|
var open = {
|
|
9499
9639
|
name: "open",
|
|
@@ -9569,12 +9709,12 @@ var skill = {
|
|
|
9569
9709
|
if (!text2.startsWith("---")) throw new CliError(`${url} isn't a skill file`);
|
|
9570
9710
|
if (action === "show") return void ctx.io.stdout.write(text2);
|
|
9571
9711
|
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 =
|
|
9712
|
+
const base = bool(ctx.args, "project") ? join7(ctx.io.cwd, ".claude") : join7(ctx.io.env.HOME ?? homedir3(), ".claude");
|
|
9713
|
+
const dir = join7(base, "skills", "airgate");
|
|
9574
9714
|
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: ${
|
|
9715
|
+
writeFileSync6(join7(dir, "SKILL.md"), text2);
|
|
9716
|
+
if (ctx.json) return ctx.ui.json({ installed: join7(dir, "SKILL.md") });
|
|
9717
|
+
ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join7(dir, "SKILL.md")}`);
|
|
9578
9718
|
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
9719
|
}
|
|
9580
9720
|
};
|