airgate 0.2.2 → 0.2.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 +2 -2
- package/dist/airgate.mjs +205 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ macOS and Linux, Node.js 20.10 or newer.
|
|
|
25
25
|
airgate login # approve this terminal in your browser
|
|
26
26
|
airgate # the full-screen terminal app
|
|
27
27
|
airgate launch # a new pod
|
|
28
|
-
airgate up # move the session you're in to a pod; airgate down brings
|
|
28
|
+
airgate up # move the session you're in to a pod (files, conversation, memory); airgate down brings it back
|
|
29
29
|
airgate help # every command
|
|
30
30
|
```
|
|
31
31
|
|
|
@@ -68,7 +68,7 @@ Guide: https://airgate.sh/docs/teams
|
|
|
68
68
|
## For your agent
|
|
69
69
|
|
|
70
70
|
Claude Code or Codex on your laptop can send work to pods on its own: one task in a fresh pod
|
|
71
|
-
with the result back here, or the whole project and
|
|
71
|
+
with the result back here, or the whole project, conversation and memory up and back.
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
74
|
airgate run "get the failing tests passing" # a fresh pod; the result comes back as a patch
|
package/dist/airgate.mjs
CHANGED
|
@@ -130,6 +130,12 @@ var init_pack = __esm({
|
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
// ../sandbox/src/types.ts
|
|
133
|
+
var AIRLOCK_MODES = ["ask", "branches", "off"];
|
|
134
|
+
var AIRLOCK_MODE_INFO = {
|
|
135
|
+
ask: { name: "ask", line: "everything waits for you: pushes, pull requests, email, deploys, outside calls" },
|
|
136
|
+
branches: { name: "work branches go through", line: "pushes to work branches go through on their own; main, force pushes, pull requests, email and deploys still wait" },
|
|
137
|
+
off: { name: "off", line: "everything goes through on its own. it's all still recorded in the airlock, but nothing waits for you" }
|
|
138
|
+
};
|
|
133
139
|
var SIZES = {
|
|
134
140
|
small: { cpu: 8, memoryGb: 4, diskGb: 100 },
|
|
135
141
|
standard: { cpu: 8, memoryGb: 8, diskGb: 100 },
|
|
@@ -813,6 +819,9 @@ var Api = class {
|
|
|
813
819
|
resumeAgentSession(id, body) {
|
|
814
820
|
return this.request("POST", `/v1/pods/${enc(id)}/agent-session/resume`, { body });
|
|
815
821
|
}
|
|
822
|
+
exportAgentMemory(id, dir) {
|
|
823
|
+
return this.request("GET", `/v1/pods/${enc(id)}/agent-memory?${new URLSearchParams({ dir })}`);
|
|
824
|
+
}
|
|
816
825
|
exportAgentSession(id, q) {
|
|
817
826
|
const qs = new URLSearchParams({ agent: q.agent, dir: q.dir, ...q.id ? { id: q.id } : {} });
|
|
818
827
|
return this.request("GET", `/v1/pods/${enc(id)}/agent-session?${qs}`);
|
|
@@ -2911,12 +2920,12 @@ function typeIn(src, options) {
|
|
|
2911
2920
|
};
|
|
2912
2921
|
});
|
|
2913
2922
|
}
|
|
2914
|
-
function level(t, period2,
|
|
2923
|
+
function level(t, period2, mode2) {
|
|
2915
2924
|
const u = mod(t, period2) / period2;
|
|
2916
2925
|
const ease2 = (k) => smoothstep(0, 1, k);
|
|
2917
|
-
if (
|
|
2926
|
+
if (mode2 === "in")
|
|
2918
2927
|
return u < 0.4 ? ease2(u / 0.4) : 1;
|
|
2919
|
-
if (
|
|
2928
|
+
if (mode2 === "out")
|
|
2920
2929
|
return u < 0.6 ? 1 : 1 - ease2((u - 0.6) / 0.4);
|
|
2921
2930
|
return u < 0.3 ? ease2(u / 0.3) : u < 0.6 ? 1 : u < 0.9 ? 1 - ease2((u - 0.6) / 0.3) : 0;
|
|
2922
2931
|
}
|
|
@@ -2926,7 +2935,7 @@ var MODE_WHAT = { in: "in", out: "out", inout: "in and out" };
|
|
|
2926
2935
|
function dissolve(src, options) {
|
|
2927
2936
|
const o = optionsOf2(options, "dissolve", ["period", "mode", "blob", "edge", "seed"]);
|
|
2928
2937
|
const period2 = num(o.period, "dissolve.period", 6, "seconds");
|
|
2929
|
-
const
|
|
2938
|
+
const mode2 = choice(o.mode, "dissolve.mode", MODES, "inout");
|
|
2930
2939
|
const scale2 = 1 / num(o.blob, "dissolve.blob", 7, "atLeast1");
|
|
2931
2940
|
const auto = o.edge === void 0;
|
|
2932
2941
|
const edge = o.edge === "" ? "" : chars2(o.edge, "dissolve.edge", ".:", 1);
|
|
@@ -2944,10 +2953,10 @@ function dissolve(src, options) {
|
|
|
2944
2953
|
return {
|
|
2945
2954
|
moves: true,
|
|
2946
2955
|
period: period2,
|
|
2947
|
-
still: HELD[
|
|
2948
|
-
what: `dissolving ${MODE_WHAT[
|
|
2956
|
+
still: HELD[mode2] * period2,
|
|
2957
|
+
what: `dissolving ${MODE_WHAT[mode2]}`,
|
|
2949
2958
|
setup: () => (t, s2, g) => {
|
|
2950
|
-
const k = level(t, period2,
|
|
2959
|
+
const k = level(t, period2, mode2);
|
|
2951
2960
|
if (k >= 1)
|
|
2952
2961
|
return copy(s2, g);
|
|
2953
2962
|
const front = k * (1 + band);
|
|
@@ -2968,7 +2977,7 @@ function dissolve(src, options) {
|
|
|
2968
2977
|
function fade2(src, options) {
|
|
2969
2978
|
const o = optionsOf2(options, "fade", ["period", "mode", "ramp"]);
|
|
2970
2979
|
const period2 = num(o.period, "fade.period", 6, "seconds");
|
|
2971
|
-
const
|
|
2980
|
+
const mode2 = choice(o.mode, "fade.mode", MODES, "inout");
|
|
2972
2981
|
const auto = o.ramp === void 0 || o.ramp === "auto";
|
|
2973
2982
|
const chars3 = ramp(auto ? "standard" : o.ramp);
|
|
2974
2983
|
if (chars3[0] !== " ")
|
|
@@ -2979,8 +2988,8 @@ function fade2(src, options) {
|
|
|
2979
2988
|
return build2("fade", src, o, () => ({
|
|
2980
2989
|
moves: true,
|
|
2981
2990
|
period: period2,
|
|
2982
|
-
still: HELD[
|
|
2983
|
-
what: `fading ${MODE_WHAT[
|
|
2991
|
+
still: HELD[mode2] * period2,
|
|
2992
|
+
what: `fading ${MODE_WHAT[mode2]}`,
|
|
2984
2993
|
setup: () => {
|
|
2985
2994
|
const steps = /* @__PURE__ */ new Map();
|
|
2986
2995
|
const stepsOf = (c) => {
|
|
@@ -2993,7 +3002,7 @@ function fade2(src, options) {
|
|
|
2993
3002
|
return v;
|
|
2994
3003
|
};
|
|
2995
3004
|
return (t, s2, g) => {
|
|
2996
|
-
const k = level(t, period2,
|
|
3005
|
+
const k = level(t, period2, mode2);
|
|
2997
3006
|
if (k >= 1)
|
|
2998
3007
|
return copy(s2, g);
|
|
2999
3008
|
for (let y = 0, i = 0; y < g.rows; y++)
|
|
@@ -7067,7 +7076,7 @@ function approvalRows(ctx, items, names) {
|
|
|
7067
7076
|
ctx.ui.c(a.state === "pending" ? "warn" : a.state === "denied" || a.state === "failed" ? "bad" : "ok", a.state),
|
|
7068
7077
|
a.kind,
|
|
7069
7078
|
names.get(a.podId) ?? (a.requestedBy ? `${a.requestedBy}'s pod` : a.podId),
|
|
7070
|
-
a.summary + (a.requestedBy ? ctx.ui.c("dim", ` \xB7 from ${a.requestedBy}`) : "") + (a.needsApprover && a.state === "pending" && !a.requestedBy ? ctx.ui.c("warn", " \xB7 waits for an approver") : ""),
|
|
7079
|
+
a.summary + (a.auto ? ctx.ui.c("dim", ` \xB7 went through on its own (${a.auto})`) : "") + (a.requestedBy ? ctx.ui.c("dim", ` \xB7 from ${a.requestedBy}`) : "") + (a.needsApprover && a.state === "pending" && !a.requestedBy ? ctx.ui.c("warn", " \xB7 waits for an approver") : ""),
|
|
7071
7080
|
ago(a.createdAt),
|
|
7072
7081
|
ctx.ui.c("dim", a.id)
|
|
7073
7082
|
]);
|
|
@@ -7080,12 +7089,21 @@ var airlock = {
|
|
|
7080
7089
|
name: "airlock",
|
|
7081
7090
|
aliases: ["approvals"],
|
|
7082
7091
|
group: "airlock \xB7 mask",
|
|
7083
|
-
usage: "[--watch]",
|
|
7084
|
-
summary: "what your pods want to send out: pending first, then recent",
|
|
7085
|
-
example: "airgate airlock
|
|
7086
|
-
notes: [
|
|
7087
|
-
|
|
7092
|
+
usage: "[--watch] | mode [ask|branches|off|account] [--pod p]",
|
|
7093
|
+
summary: "what your pods want to send out: pending first, then recent \xB7 how strict it is",
|
|
7094
|
+
example: "airgate airlock mode branches",
|
|
7095
|
+
notes: [
|
|
7096
|
+
"--watch follows the live stream and prints each new request as it arrives; ctrl-c to stop.",
|
|
7097
|
+
"mode: how much waits for you, like Claude Code's permission modes. ask (the default): everything waits. branches: pushes to work branches go through on their own; main, force pushes, pull requests, email and deploys still wait. off: everything goes through, still recorded.",
|
|
7098
|
+
"mode with --pod sets one pod's own mode (account: back to the account's). a team's approver rules always win."
|
|
7099
|
+
],
|
|
7100
|
+
flags: {
|
|
7101
|
+
watch: { type: "boolean", short: "w", help: "follow new requests" },
|
|
7102
|
+
pod: { type: "string", value: "pod", help: "with mode: this pod only" }
|
|
7103
|
+
},
|
|
7088
7104
|
async run(ctx) {
|
|
7105
|
+
if (ctx.args.positionals[0] === "mode") return mode(ctx);
|
|
7106
|
+
if (ctx.args.positionals.length) throw new UsageError("airgate airlock [--watch] | airgate airlock mode [ask|branches|off]");
|
|
7089
7107
|
if (bool(ctx.args, "watch")) return watch(ctx);
|
|
7090
7108
|
const [items, names] = await Promise.all([ctx.api.approvals(), podNames(ctx)]);
|
|
7091
7109
|
const pending = items.filter((a) => a.state === "pending").sort((a, b2) => a.createdAt - b2.createdAt);
|
|
@@ -7104,6 +7122,30 @@ var airlock = {
|
|
|
7104
7122
|
}
|
|
7105
7123
|
}
|
|
7106
7124
|
};
|
|
7125
|
+
async function mode(ctx) {
|
|
7126
|
+
const want = ctx.args.positionals[1];
|
|
7127
|
+
const podRef = str(ctx.args, "pod");
|
|
7128
|
+
if (want && !AIRLOCK_MODES.includes(want) && !(podRef && want === "account")) {
|
|
7129
|
+
throw new UsageError(`${AIRLOCK_MODES.join(", ")}${podRef ? " or account" : ""}`, "airgate airlock mode branches");
|
|
7130
|
+
}
|
|
7131
|
+
const account = (await ctx.api.me()).user.airlockMode ?? "ask";
|
|
7132
|
+
const say = (who, m2, own) => {
|
|
7133
|
+
ctx.ui.out(`${who}: ${m2 === "off" ? ctx.ui.c("warn", AIRLOCK_MODE_INFO[m2].name) : AIRLOCK_MODE_INFO[m2].name}${own ? "" : ctx.ui.c("dim", " (the account's)")}`);
|
|
7134
|
+
ctx.ui.out(ctx.ui.c("dim", ` ${AIRLOCK_MODE_INFO[m2].line}`));
|
|
7135
|
+
if (m2 === "off") ctx.ui.err(ctx.ui.ce("warn", " nothing waits: an agent's push to main, a pull request or an email goes out on its own"));
|
|
7136
|
+
};
|
|
7137
|
+
if (podRef) {
|
|
7138
|
+
let pod = await resolvePod(ctx, podRef);
|
|
7139
|
+
if (want) pod = await ctx.api.updatePod(pod.id, { airlock: want === "account" ? null : want });
|
|
7140
|
+
const m2 = pod.spec.airlock ?? account;
|
|
7141
|
+
if (ctx.json) return ctx.ui.json({ pod: pod.spec.name, mode: m2, own: Boolean(pod.spec.airlock) });
|
|
7142
|
+
return say(pod.spec.name, m2, Boolean(pod.spec.airlock));
|
|
7143
|
+
}
|
|
7144
|
+
const m = want ? (await ctx.api.updateMe({ airlockMode: want })).user.airlockMode ?? want : account;
|
|
7145
|
+
if (ctx.json) return ctx.ui.json({ mode: m });
|
|
7146
|
+
say("airlock", m, true);
|
|
7147
|
+
if (!want) ctx.ui.out(ctx.ui.c("dim", ` change it: airgate airlock mode ${AIRLOCK_MODES.join("|")} \xB7 one pod: --pod <p>`));
|
|
7148
|
+
}
|
|
7107
7149
|
async function watch(ctx) {
|
|
7108
7150
|
const seen = /* @__PURE__ */ new Set();
|
|
7109
7151
|
let first = true;
|
|
@@ -7559,7 +7601,7 @@ import { rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
|
7559
7601
|
import { resolve as resolve4 } from "node:path";
|
|
7560
7602
|
|
|
7561
7603
|
// src/version.ts
|
|
7562
|
-
var VERSION = "0.2.
|
|
7604
|
+
var VERSION = "0.2.4";
|
|
7563
7605
|
|
|
7564
7606
|
// src/commands/team.ts
|
|
7565
7607
|
async function checkSimilar(ctx, task2) {
|
|
@@ -8133,6 +8175,7 @@ var launch = {
|
|
|
8133
8175
|
env: { type: "string", value: "file", help: "a .env file: its keys become this pod's secrets before it boots" },
|
|
8134
8176
|
"public-repo": { type: "boolean", help: "the repo is public: don't check it against your GitHub App" },
|
|
8135
8177
|
mask: { type: "boolean", default: true, help: "mask model traffic (--no-mask to turn off)" },
|
|
8178
|
+
airlock: { type: "string", value: "mode", choices: AIRLOCK_MODES, help: "this pod's airlock: ask | branches | off (default: your account's, see airgate airlock mode)" },
|
|
8136
8179
|
undo: { type: "boolean", default: true, help: "checkpoint after every agent step (--no-undo to turn off)" },
|
|
8137
8180
|
network: { type: "string", value: "policy", choices: NETWORK_NAMES, default: "allowlist", help: "allowlist | open | off" },
|
|
8138
8181
|
allow: { type: "list", value: "host", help: "extra hosts for the allowlist (repeatable, or comma-separated)" },
|
|
@@ -8252,6 +8295,8 @@ function specFromFlags(ctx, defaults = {}) {
|
|
|
8252
8295
|
if (jobs2.length) spec.jobs = jobs2;
|
|
8253
8296
|
if (builtin) spec.builtinModel = true;
|
|
8254
8297
|
if (builtin && model) spec.model = model;
|
|
8298
|
+
const airlockMode = str(a, "airlock");
|
|
8299
|
+
if (airlockMode) spec.airlock = airlockMode;
|
|
8255
8300
|
return spec;
|
|
8256
8301
|
}
|
|
8257
8302
|
var info = {
|
|
@@ -8275,6 +8320,7 @@ var info = {
|
|
|
8275
8320
|
["network", s2.network + (s2.network === "allowlist" && s2.allow?.length ? ` \xB7 ${s2.allow.join(", ")}` : "")],
|
|
8276
8321
|
["mask", s2.masking ? `on${s2.maskTerms?.length ? ` \xB7 ${s2.maskTerms.length} terms` : ""}` : ctx.ui.c("warn", "off")],
|
|
8277
8322
|
["undo", s2.autoCheckpoint ? "auto checkpoints" : "manual only"],
|
|
8323
|
+
["airlock", s2.airlock ? s2.airlock === "off" ? ctx.ui.c("warn", AIRLOCK_MODE_INFO.off.name) : AIRLOCK_MODE_INFO[s2.airlock].name : "as the account"],
|
|
8278
8324
|
["auto-stop", s2.autoStopMinutes === 0 ? "keep awake (never sleeps: every hour counts, ~720 a month)" : `sleeps after ${s2.autoStopMinutes ?? 10} min idle`]
|
|
8279
8325
|
];
|
|
8280
8326
|
if (s2.repo) rows2.push(["repo", s2.repo]);
|
|
@@ -8754,6 +8800,79 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
|
|
|
8754
8800
|
atomic(file, body);
|
|
8755
8801
|
return { agent: sent.agent, id, file, outcome: "new", sha256: sha256(body) };
|
|
8756
8802
|
}
|
|
8803
|
+
var TEXTISH = /\.(md|markdown|txt|json|jsonl|ya?ml|toml)$/i;
|
|
8804
|
+
function findClaudeMemory(home, projectDir, env = {}) {
|
|
8805
|
+
for (const p of spellings(projectDir)) {
|
|
8806
|
+
const dir = join3(claudeHome(home, env), "projects", claudeKey(p), "memory");
|
|
8807
|
+
try {
|
|
8808
|
+
if (statSync(dir).isDirectory() && listFiles(dir).length) return dir;
|
|
8809
|
+
} catch {
|
|
8810
|
+
}
|
|
8811
|
+
}
|
|
8812
|
+
return null;
|
|
8813
|
+
}
|
|
8814
|
+
function listFiles(dir, rel = "") {
|
|
8815
|
+
const out = [];
|
|
8816
|
+
for (const x of readdirSync2(join3(dir, rel), { withFileTypes: true })) {
|
|
8817
|
+
const r = rel ? `${rel}/${x.name}` : x.name;
|
|
8818
|
+
if (x.isDirectory()) out.push(...listFiles(dir, r));
|
|
8819
|
+
else if (x.isFile()) out.push(r);
|
|
8820
|
+
}
|
|
8821
|
+
return out;
|
|
8822
|
+
}
|
|
8823
|
+
async function packMemory(dir, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
8824
|
+
const froms = spellings(projectDir);
|
|
8825
|
+
const stage = mkdtempSync(join3(tmpdir(), "airgate-memory-"));
|
|
8826
|
+
try {
|
|
8827
|
+
const files2 = {};
|
|
8828
|
+
for (const rel of listFiles(dir)) {
|
|
8829
|
+
const body = readFileSync4(join3(dir, rel));
|
|
8830
|
+
files2[rel] = sha256(body);
|
|
8831
|
+
const out = join3(stage, "memory", rel);
|
|
8832
|
+
mkdirSync2(dirname(out), { recursive: true });
|
|
8833
|
+
writeFileSync3(out, TEXTISH.test(rel) ? rewriteAll(body.toString("utf8"), froms, podPath) : body);
|
|
8834
|
+
}
|
|
8835
|
+
const archive = await runTar(stage, ["-czf", "-", "memory"], null, limit);
|
|
8836
|
+
return { podDir: `.claude/projects/${claudeKey(podPath)}`, archive, sent: { dir, localPath: projectDir, podPath, files: files2 }, count: Object.keys(files2).length };
|
|
8837
|
+
} finally {
|
|
8838
|
+
rmSync3(stage, { recursive: true, force: true });
|
|
8839
|
+
}
|
|
8840
|
+
}
|
|
8841
|
+
function bringBackMemory(sent, pod) {
|
|
8842
|
+
const out = { added: [], updated: [], conflicts: [], sent: { ...sent, files: { ...sent.files } } };
|
|
8843
|
+
const write2 = (file, body) => {
|
|
8844
|
+
mkdirSync2(dirname(file), { recursive: true });
|
|
8845
|
+
const tmp = `${file}.airgate-${process.pid}`;
|
|
8846
|
+
writeFileSync3(tmp, body, { mode: 384 });
|
|
8847
|
+
renameSync2(tmp, file);
|
|
8848
|
+
};
|
|
8849
|
+
for (const f of pod) {
|
|
8850
|
+
const rel = f.path.replace(/\\/g, "/");
|
|
8851
|
+
if (!rel || rel.startsWith("/") || rel.split("/").some((p) => p === ".." || p === "")) continue;
|
|
8852
|
+
const body = TEXTISH.test(rel) ? Buffer.from(rewritePath(f.body.toString("utf8"), sent.podPath, sent.localPath)) : f.body;
|
|
8853
|
+
const file = join3(sent.dir, rel);
|
|
8854
|
+
const local = existsSync2(file) ? readFileSync4(file) : null;
|
|
8855
|
+
if (local && local.equals(body)) {
|
|
8856
|
+
out.sent.files[rel] = sha256(local);
|
|
8857
|
+
continue;
|
|
8858
|
+
}
|
|
8859
|
+
if (!local) {
|
|
8860
|
+
write2(file, body);
|
|
8861
|
+
out.added.push(rel);
|
|
8862
|
+
out.sent.files[rel] = sha256(body);
|
|
8863
|
+
} else if (sent.files[rel] && sha256(local) === sent.files[rel]) {
|
|
8864
|
+
write2(file, body);
|
|
8865
|
+
out.updated.push(rel);
|
|
8866
|
+
out.sent.files[rel] = sha256(body);
|
|
8867
|
+
} else {
|
|
8868
|
+
const dot = rel.lastIndexOf(".");
|
|
8869
|
+
const savedAs = dot > rel.lastIndexOf("/") ? `${rel.slice(0, dot)}.from-pod${rel.slice(dot)}` : `${rel}.from-pod`;
|
|
8870
|
+
write2(join3(sent.dir, savedAs), body);
|
|
8871
|
+
out.conflicts.push({ file: rel, savedAs });
|
|
8872
|
+
}
|
|
8873
|
+
}
|
|
8874
|
+
return out;
|
|
8875
|
+
}
|
|
8757
8876
|
|
|
8758
8877
|
// src/patch.ts
|
|
8759
8878
|
var RULES2 = [
|
|
@@ -8963,7 +9082,7 @@ function podDirName(projectDir) {
|
|
|
8963
9082
|
var up = {
|
|
8964
9083
|
name: "up",
|
|
8965
9084
|
group: "up \xB7 down",
|
|
8966
|
-
usage: "[dir] [--pod p] [--agent a] [--with-config] [--env [.env]] [--session id | --no-session]",
|
|
9085
|
+
usage: "[dir] [--pod p] [--agent a] [--with-config] [--env [.env]] [--session id | --no-session] [--no-memory]",
|
|
8967
9086
|
summary: "move this project (with uncommitted work) into a pod",
|
|
8968
9087
|
example: "airgate up --with-config",
|
|
8969
9088
|
notes: [
|
|
@@ -8972,6 +9091,7 @@ var up = {
|
|
|
8972
9091
|
"--with-config also sends ~/.claude settings.json, CLAUDE.md, skills/, agents/, commands/ (never credentials).",
|
|
8973
9092
|
"remembers the pod in .airgate/state.json (kept out of git via .git/info/exclude) so `airgate down` knows where to look.",
|
|
8974
9093
|
"also sends your latest Claude Code / Codex conversation in this directory and resumes it in the pod (paths rewritten); --no-session to skip, --session <id> to pick one. dropped with a warning if it is over the size limit.",
|
|
9094
|
+
"and Claude Code's memory for this project (~/.claude/projects/<project>/memory); --no-memory to skip. down brings new and changed memories back, never overwriting one you changed here.",
|
|
8975
9095
|
"--env [file] sends the keys of a dotenv file (default .env) as secrets of that pod; the names sent are listed."
|
|
8976
9096
|
],
|
|
8977
9097
|
flags: {
|
|
@@ -8983,6 +9103,7 @@ var up = {
|
|
|
8983
9103
|
env: { type: "string", value: "file", implicit: ".env", help: "send a dotenv file's keys as this pod's secrets (default .env)" },
|
|
8984
9104
|
session: { type: "string", value: "id", help: "send this Claude Code / Codex conversation (default: the latest here)" },
|
|
8985
9105
|
"no-session": { type: "boolean", help: "don't send the agent conversation" },
|
|
9106
|
+
"no-memory": { type: "boolean", help: "don't send Claude Code's memory for this project" },
|
|
8986
9107
|
yes: { type: "boolean", short: "y", help: "don't ask" }
|
|
8987
9108
|
},
|
|
8988
9109
|
async run(ctx) {
|
|
@@ -9026,6 +9147,16 @@ var up = {
|
|
|
9026
9147
|
for (const f of [findClaudeSession(home, projectDir, sessionId, ctx.io.env), findCodexSession(home, projectDir, sessionId, ctx.io.env)]) if (f) found.push(f);
|
|
9027
9148
|
if (sessionId && !found.length) throw new CliError(`no Claude Code or Codex session ${sessionId} for ${projectDir}`, "leave out --session to take the latest one");
|
|
9028
9149
|
}
|
|
9150
|
+
let memory2 = null;
|
|
9151
|
+
const memDir = bool(ctx.args, "no-memory") ? null : findClaudeMemory(home, projectDir, ctx.io.env);
|
|
9152
|
+
if (memDir) {
|
|
9153
|
+
try {
|
|
9154
|
+
memory2 = await packMemory(memDir, projectDir, podPath);
|
|
9155
|
+
} catch (e) {
|
|
9156
|
+
if (!(e instanceof TooLargeError)) throw e;
|
|
9157
|
+
ui2.err(ui2.ce("warn", `Claude Code's memory for this project is over ${bytes(UPLOAD_LIMIT)} packed: not sent`));
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
9029
9160
|
const sessions = [];
|
|
9030
9161
|
for (const f of found) {
|
|
9031
9162
|
try {
|
|
@@ -9072,6 +9203,15 @@ var up = {
|
|
|
9072
9203
|
let envSent = null;
|
|
9073
9204
|
const envFile = str(ctx.args, "env");
|
|
9074
9205
|
if (envFile) envSent = await ui2.spin(`sending ${envFile} as secrets`, sendDotenv(ctx, envFile, pod.id));
|
|
9206
|
+
let memorySent;
|
|
9207
|
+
if (memory2) {
|
|
9208
|
+
try {
|
|
9209
|
+
await ui2.spin(`sending Claude Code's memory (${memory2.count} file${memory2.count === 1 ? "" : "s"})`, ctx.api.upload(pod.id, memory2.podDir, memory2.archive));
|
|
9210
|
+
memorySent = memory2.sent;
|
|
9211
|
+
} catch (e) {
|
|
9212
|
+
ui2.err(ui2.ce("warn", `couldn't send the memory: ${e.message}`));
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9075
9215
|
const sent = [];
|
|
9076
9216
|
const resumed = [];
|
|
9077
9217
|
for (const p of sessions) {
|
|
@@ -9101,7 +9241,7 @@ var up = {
|
|
|
9101
9241
|
} catch {
|
|
9102
9242
|
based = false;
|
|
9103
9243
|
}
|
|
9104
|
-
const state = { gateway: ctx.cfg.gateway, podId: pod.id, podName: pod.spec.name, dir: podDir, at: Date.now(), ...sent.length ? { sessions: sent } : {} };
|
|
9244
|
+
const state = { gateway: ctx.cfg.gateway, podId: pod.id, podName: pod.spec.name, dir: podDir, at: Date.now(), ...sent.length ? { sessions: sent } : {}, ...memorySent ? { memory: memorySent } : {} };
|
|
9105
9245
|
writeState(projectDir, state);
|
|
9106
9246
|
if (ctx.json) {
|
|
9107
9247
|
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 });
|
|
@@ -9114,6 +9254,7 @@ var up = {
|
|
|
9114
9254
|
ui2.out(`secrets for ${pod.spec.name}: ${envSent.sent.length ? envSent.sent.join(", ") : "none"}${envSent.skipped.length ? ui2.c("dim", ` (skipped: ${envSent.skipped.join(", ")})`) : ""}`);
|
|
9115
9255
|
for (const f of envSent.failed) ui2.err(ui2.ce("warn", `not sent: ${f}`));
|
|
9116
9256
|
}
|
|
9257
|
+
if (memorySent) ui2.out(`memory: ${Object.keys(memorySent.files).length} file(s) of Claude Code's memory for this project`);
|
|
9117
9258
|
for (const r of resumed) {
|
|
9118
9259
|
ui2.out(`conversation: ${r.command}${r.running ? ui2.c("dim", ` \xB7 resuming in the pod (cd ~/${podDir})`) : ui2.c("dim", ` \xB7 sent; in the pod: cd ~/${podDir} && ${r.command}`)}`);
|
|
9119
9260
|
}
|
|
@@ -9138,8 +9279,35 @@ async function downSessions(ctx, root, state, pod, quiet2 = false) {
|
|
|
9138
9279
|
}
|
|
9139
9280
|
}
|
|
9140
9281
|
if (state.sessions?.length) writeState(root, { ...state, sessions: next });
|
|
9141
|
-
|
|
9142
|
-
|
|
9282
|
+
let memory2 = null;
|
|
9283
|
+
if (state.memory) {
|
|
9284
|
+
try {
|
|
9285
|
+
const x = await ctx.api.exportAgentMemory(pod.id, state.dir);
|
|
9286
|
+
memory2 = bringBackMemory(state.memory, x.files.map((f) => ({ path: f.path, body: Buffer.from(f.b64, "base64") })));
|
|
9287
|
+
writeState(root, { ...readState(root, state), memory: memory2.sent });
|
|
9288
|
+
} catch (e) {
|
|
9289
|
+
if (!(e instanceof ApiError && e.status === 404)) ctx.ui.err(ctx.ui.ce("warn", `couldn't bring Claude Code's memory back: ${e.message}`));
|
|
9290
|
+
}
|
|
9291
|
+
}
|
|
9292
|
+
if (!quiet2 && !ctx.json) {
|
|
9293
|
+
printSessions(ctx, out);
|
|
9294
|
+
printMemory(ctx, memory2);
|
|
9295
|
+
}
|
|
9296
|
+
return { convo: out, memory: memory2 };
|
|
9297
|
+
}
|
|
9298
|
+
function readState(root, fallback) {
|
|
9299
|
+
try {
|
|
9300
|
+
return JSON.parse(readFileSync5(statePath(root), "utf8"));
|
|
9301
|
+
} catch {
|
|
9302
|
+
return fallback;
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
function printMemory(ctx, m) {
|
|
9306
|
+
if (!m) return;
|
|
9307
|
+
const n2 = m.added.length + m.updated.length;
|
|
9308
|
+
if (!n2 && !m.conflicts.length) return ctx.ui.out(ctx.ui.c("dim", "memory: nothing new in the pod"));
|
|
9309
|
+
if (n2) ctx.ui.out(`memory: ${[m.added.length ? `${m.added.length} new` : "", m.updated.length ? `${m.updated.length} updated` : ""].filter(Boolean).join(", ")} from the pod`);
|
|
9310
|
+
for (const c of m.conflicts) ctx.ui.out(`memory: ${c.file} changed here and in the pod; yours kept, the pod's saved as ${c.savedAs}`);
|
|
9143
9311
|
}
|
|
9144
9312
|
function printSessions(ctx, list2) {
|
|
9145
9313
|
for (const r of list2) {
|
|
@@ -9261,11 +9429,13 @@ ${check.stderr.trim()}`, "commit or stash local edits to those files, or look wi
|
|
|
9261
9429
|
} catch {
|
|
9262
9430
|
acked = false;
|
|
9263
9431
|
}
|
|
9264
|
-
const
|
|
9265
|
-
|
|
9432
|
+
const back = takeSessions && found ? await downSessions(ctx, found.root, found.state, pod, true) : { convo: [], memory: null };
|
|
9433
|
+
const convo = back.convo;
|
|
9434
|
+
if (ctx.json) return ui2.json({ pod: pod.id, base, files: files2, outside, refused, applied: true, acked, sessions: convo, memory: back.memory && { added: back.memory.added, updated: back.memory.updated, conflicts: back.memory.conflicts } });
|
|
9266
9435
|
ui2.out(`${ui2.c("ok", "down")}: applied ${files2.length} file(s). review: git status \xB7 git diff`);
|
|
9267
9436
|
if (!acked) ui2.err(ui2.ce("warn", "couldn't tell the pod what was taken; the next down will show these again"));
|
|
9268
9437
|
printSessions(ctx, convo);
|
|
9438
|
+
printMemory(ctx, back.memory);
|
|
9269
9439
|
}
|
|
9270
9440
|
};
|
|
9271
9441
|
|
|
@@ -10404,7 +10574,7 @@ var TOOLS = [
|
|
|
10404
10574
|
},
|
|
10405
10575
|
{
|
|
10406
10576
|
name: "pod_down",
|
|
10407
|
-
description: "Apply a pod\u2019s work to the local repo as a git patch, with the CLI\u2019s safety checks (refuses symlinks and paths outside the repo; all or nothing). Look with pod_diff first and tell the user about risky files.",
|
|
10577
|
+
description: "Apply a pod\u2019s work to the local repo as a git patch (and bring back the conversation and Claude Code\u2019s memory that up sent), with the CLI\u2019s safety checks (refuses symlinks and paths outside the repo; all or nothing). Look with pod_diff first and tell the user about risky files.",
|
|
10408
10578
|
inputSchema: obj({ pod: s("pod name or id (default: the pod from airgate up in dir)"), dir: s("the local project directory (default: where the server started)") }),
|
|
10409
10579
|
async call(ctx, a) {
|
|
10410
10580
|
const argv = ["--yes"];
|
|
@@ -10447,7 +10617,7 @@ var TOOLS = [
|
|
|
10447
10617
|
},
|
|
10448
10618
|
{
|
|
10449
10619
|
name: "pod_up",
|
|
10450
|
-
description: "Move the local project into a pod: its files (git-tracked and untracked-but-not-ignored), and by default the latest Claude Code / Codex conversation here, which resumes in the pod. Launches a new pod named after the folder unless pod is given. Bring the work back later with pod_diff / pod_down.",
|
|
10620
|
+
description: "Move the local project into a pod: its files (git-tracked and untracked-but-not-ignored), and by default the latest Claude Code / Codex conversation here, which resumes in the pod, plus Claude Code\u2019s memory for the project. Launches a new pod named after the folder unless pod is given. Bring the work back later with pod_diff / pod_down.",
|
|
10451
10621
|
inputSchema: obj({
|
|
10452
10622
|
dir: s("the local project directory (default: where the server started)"),
|
|
10453
10623
|
pod: s("send into this existing pod instead of launching one"),
|
|
@@ -10548,17 +10718,18 @@ var TOOLS = [
|
|
|
10548
10718
|
},
|
|
10549
10719
|
{
|
|
10550
10720
|
name: "pod_settings",
|
|
10551
|
-
description: "Change a pod's network after launch (open | allowlist | off, extra allowlist hosts)
|
|
10552
|
-
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"]),
|
|
10721
|
+
description: "Change a pod's network after launch (open | allowlist | off, extra allowlist hosts), switch its AI off/on (aiOff: no model calls at all), or set its airlock (ask | branches | off | account). Applies immediately. Only when the user asked for it; never loosen the airlock on your own.",
|
|
10722
|
+
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"), airlock: s("ask (everything waits) | branches (pushes to work branches go through) | off (nothing waits) | account (the account's mode)", { enum: ["ask", "branches", "off", "account"] }) }, ["pod"]),
|
|
10553
10723
|
async call(ctx, a) {
|
|
10554
10724
|
const p = await resolvePod(ctx, need(a, "pod"));
|
|
10555
10725
|
const patch = {};
|
|
10556
10726
|
if (typeof a.network === "string") patch.network = a.network;
|
|
10557
10727
|
if (Array.isArray(a.allow)) patch.allow = a.allow.map(String);
|
|
10558
10728
|
if (typeof a.aiOff === "boolean") patch.aiOff = a.aiOff;
|
|
10559
|
-
if (
|
|
10729
|
+
if (typeof a.airlock === "string") patch.airlock = a.airlock === "account" ? null : a.airlock;
|
|
10730
|
+
if (!Object.keys(patch).length) throw new UsageError("nothing to change: network, allow, aiOff or airlock");
|
|
10560
10731
|
const pod = await ctx.api.updatePod(p.id, patch);
|
|
10561
|
-
return { name: pod.spec.name, network: pod.spec.network, allow: pod.spec.allow ?? [], aiOff: Boolean(pod.spec.aiOff) };
|
|
10732
|
+
return { name: pod.spec.name, network: pod.spec.network, allow: pod.spec.allow ?? [], aiOff: Boolean(pod.spec.aiOff), airlock: pod.spec.airlock ?? "account" };
|
|
10562
10733
|
}
|
|
10563
10734
|
},
|
|
10564
10735
|
{
|
|
@@ -12763,16 +12934,16 @@ var network = {
|
|
|
12763
12934
|
remove: { type: "string", value: "host", help: "remove hosts (comma separated)" }
|
|
12764
12935
|
},
|
|
12765
12936
|
async run(ctx) {
|
|
12766
|
-
const [ref,
|
|
12937
|
+
const [ref, mode2] = ctx.args.positionals;
|
|
12767
12938
|
if (!ref) throw new UsageError("airgate network <pod> [open|allowlist|off]");
|
|
12768
|
-
if (
|
|
12939
|
+
if (mode2 && !["open", "allowlist", "off"].includes(mode2)) throw new UsageError("open, allowlist or off");
|
|
12769
12940
|
const pod = await resolvePod(ctx, ref);
|
|
12770
12941
|
const list2 = (v) => (v ?? "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
12771
12942
|
let allow = pod.spec.allow ?? [];
|
|
12772
12943
|
if (str(ctx.args, "allow") !== void 0) allow = list2(str(ctx.args, "allow"));
|
|
12773
12944
|
allow = [.../* @__PURE__ */ new Set([...allow, ...list2(str(ctx.args, "add"))])].filter((h) => !list2(str(ctx.args, "remove")).includes(h));
|
|
12774
|
-
const changed =
|
|
12775
|
-
const p = changed ? await ctx.api.updatePod(pod.id, { ...
|
|
12945
|
+
const changed = mode2 || ["allow", "add", "remove"].some((k) => str(ctx.args, k) !== void 0);
|
|
12946
|
+
const p = changed ? await ctx.api.updatePod(pod.id, { ...mode2 ? { network: mode2 } : {}, allow }) : pod;
|
|
12776
12947
|
if (ctx.json) return ctx.ui.json({ network: p.spec.network, allow: p.spec.allow ?? [], aiOff: Boolean(p.spec.aiOff) });
|
|
12777
12948
|
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)") : ""}`);
|
|
12778
12949
|
}
|