airgate 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/airgate.mjs +148 -19
- 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
|
|
|
@@ -60,7 +60,7 @@ airgate share web # teammates open, watch and fork
|
|
|
60
60
|
airgate recall "rate limiting" # what a teammate already did
|
|
61
61
|
airgate recall --patch <id> --apply # reuse it here instead of paying again
|
|
62
62
|
airgate note "tests need DATABASE_URL=sqlite::memory:"
|
|
63
|
-
airgate team trigger add acme/web pr --
|
|
63
|
+
airgate team trigger add acme/web pr --mission squad # a crew on every pull request
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Guide: https://airgate.sh/docs/teams
|
|
@@ -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
|
@@ -813,6 +813,9 @@ var Api = class {
|
|
|
813
813
|
resumeAgentSession(id, body) {
|
|
814
814
|
return this.request("POST", `/v1/pods/${enc(id)}/agent-session/resume`, { body });
|
|
815
815
|
}
|
|
816
|
+
exportAgentMemory(id, dir) {
|
|
817
|
+
return this.request("GET", `/v1/pods/${enc(id)}/agent-memory?${new URLSearchParams({ dir })}`);
|
|
818
|
+
}
|
|
816
819
|
exportAgentSession(id, q) {
|
|
817
820
|
const qs = new URLSearchParams({ agent: q.agent, dir: q.dir, ...q.id ? { id: q.id } : {} });
|
|
818
821
|
return this.request("GET", `/v1/pods/${enc(id)}/agent-session?${qs}`);
|
|
@@ -7559,7 +7562,7 @@ import { rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
|
7559
7562
|
import { resolve as resolve4 } from "node:path";
|
|
7560
7563
|
|
|
7561
7564
|
// src/version.ts
|
|
7562
|
-
var VERSION = "0.2.
|
|
7565
|
+
var VERSION = "0.2.3";
|
|
7563
7566
|
|
|
7564
7567
|
// src/commands/team.ts
|
|
7565
7568
|
async function checkSimilar(ctx, task2) {
|
|
@@ -7634,7 +7637,7 @@ var team = {
|
|
|
7634
7637
|
"airgate team audit [--csv] who did what (admins)",
|
|
7635
7638
|
"airgate team billing \xB7 upgrade [--seats n] \xB7 seats <n> the team plan: seats and its pooled hours",
|
|
7636
7639
|
"airgate team secrets \xB7 secret set NAME (value from a hidden prompt or stdin) \xB7 secret rm NAME reach every member's pods",
|
|
7637
|
-
"airgate team triggers \xB7 trigger add <owner/repo> pr|issue [--label l] [--
|
|
7640
|
+
"airgate team triggers \xB7 trigger add <owner/repo> pr|issue [--label l] [--mission m] \xB7 trigger rm <id> crews started by GitHub"
|
|
7638
7641
|
],
|
|
7639
7642
|
flags: {
|
|
7640
7643
|
admin: { type: "boolean", help: "invite: as an admin" },
|
|
@@ -7651,7 +7654,8 @@ var team = {
|
|
|
7651
7654
|
csv: { type: "boolean", help: "audit: as CSV" },
|
|
7652
7655
|
seats: { type: "string", value: "n", help: "upgrade: how many seats (at least everyone in it)" },
|
|
7653
7656
|
label: { type: "string", value: "label", help: "trigger: the issue label that starts a crew (default airgate)" },
|
|
7654
|
-
|
|
7657
|
+
mission: { type: "string", value: "mission", help: "trigger: the crew's mission: race, contest, council, squad or split" },
|
|
7658
|
+
recipe: { type: "string", value: "mission", help: "trigger: the old name for --mission" },
|
|
7655
7659
|
yes: { type: "boolean", short: "y", help: "don't ask" },
|
|
7656
7660
|
"no-open": { type: "boolean", help: "print the link instead of opening the browser" }
|
|
7657
7661
|
},
|
|
@@ -7851,14 +7855,15 @@ var team = {
|
|
|
7851
7855
|
case "triggers": {
|
|
7852
7856
|
const list2 = await ctx.api.triggers();
|
|
7853
7857
|
if (ctx.json) return ui2.json(list2);
|
|
7854
|
-
if (!list2.length) return void ui2.out(ui2.c("dim", "no triggers. add one: airgate team trigger add <owner/repo> pr|issue [--
|
|
7858
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "no triggers. add one: airgate team trigger add <owner/repo> pr|issue [--mission squad]"));
|
|
7855
7859
|
return void ui2.table(list2.map((t) => [t.repo, t.event === "pr_opened" ? "a pull request opens" : `an issue gets "${t.label}"`, `${t.recipe} crew`, ui2.c("dim", t.id)]), { head: ["repo", "when", "starts", "id"] });
|
|
7856
7860
|
}
|
|
7857
7861
|
case "trigger": {
|
|
7858
7862
|
if (a1 === "add") {
|
|
7859
7863
|
const [, , repo, when] = ctx.args.positionals;
|
|
7860
|
-
if (!repo || when !== "pr" && when !== "issue") throw new UsageError("airgate team trigger add <owner/repo> pr|issue [--label l] [--
|
|
7861
|
-
const
|
|
7864
|
+
if (!repo || when !== "pr" && when !== "issue") throw new UsageError("airgate team trigger add <owner/repo> pr|issue [--label l] [--mission m]");
|
|
7865
|
+
const mission = str(ctx.args, "mission") ?? str(ctx.args, "recipe");
|
|
7866
|
+
const t = await ctx.api.addTrigger({ repo, event: when === "pr" ? "pr_opened" : "issue_labeled", ...str(ctx.args, "label") ? { label: str(ctx.args, "label") } : {}, ...mission ? { recipe: mission } : {} });
|
|
7862
7867
|
if (ctx.json) return ui2.json(t);
|
|
7863
7868
|
ui2.out(`${ui2.c("ok", "added")}: ${t.event === "pr_opened" ? "each pull request opened on" : `each issue labelled "${t.label}" on`} ${t.repo} starts a ${t.recipe} crew.`);
|
|
7864
7869
|
return void ui2.out(ui2.c("dim", "it runs as you; what it pushes or opens waits in the airlock. the GitHub App must be on the repo: airgate github"));
|
|
@@ -8752,6 +8757,79 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
|
|
|
8752
8757
|
atomic(file, body);
|
|
8753
8758
|
return { agent: sent.agent, id, file, outcome: "new", sha256: sha256(body) };
|
|
8754
8759
|
}
|
|
8760
|
+
var TEXTISH = /\.(md|markdown|txt|json|jsonl|ya?ml|toml)$/i;
|
|
8761
|
+
function findClaudeMemory(home, projectDir, env = {}) {
|
|
8762
|
+
for (const p of spellings(projectDir)) {
|
|
8763
|
+
const dir = join3(claudeHome(home, env), "projects", claudeKey(p), "memory");
|
|
8764
|
+
try {
|
|
8765
|
+
if (statSync(dir).isDirectory() && listFiles(dir).length) return dir;
|
|
8766
|
+
} catch {
|
|
8767
|
+
}
|
|
8768
|
+
}
|
|
8769
|
+
return null;
|
|
8770
|
+
}
|
|
8771
|
+
function listFiles(dir, rel = "") {
|
|
8772
|
+
const out = [];
|
|
8773
|
+
for (const x of readdirSync2(join3(dir, rel), { withFileTypes: true })) {
|
|
8774
|
+
const r = rel ? `${rel}/${x.name}` : x.name;
|
|
8775
|
+
if (x.isDirectory()) out.push(...listFiles(dir, r));
|
|
8776
|
+
else if (x.isFile()) out.push(r);
|
|
8777
|
+
}
|
|
8778
|
+
return out;
|
|
8779
|
+
}
|
|
8780
|
+
async function packMemory(dir, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
8781
|
+
const froms = spellings(projectDir);
|
|
8782
|
+
const stage = mkdtempSync(join3(tmpdir(), "airgate-memory-"));
|
|
8783
|
+
try {
|
|
8784
|
+
const files2 = {};
|
|
8785
|
+
for (const rel of listFiles(dir)) {
|
|
8786
|
+
const body = readFileSync4(join3(dir, rel));
|
|
8787
|
+
files2[rel] = sha256(body);
|
|
8788
|
+
const out = join3(stage, "memory", rel);
|
|
8789
|
+
mkdirSync2(dirname(out), { recursive: true });
|
|
8790
|
+
writeFileSync3(out, TEXTISH.test(rel) ? rewriteAll(body.toString("utf8"), froms, podPath) : body);
|
|
8791
|
+
}
|
|
8792
|
+
const archive = await runTar(stage, ["-czf", "-", "memory"], null, limit);
|
|
8793
|
+
return { podDir: `.claude/projects/${claudeKey(podPath)}`, archive, sent: { dir, localPath: projectDir, podPath, files: files2 }, count: Object.keys(files2).length };
|
|
8794
|
+
} finally {
|
|
8795
|
+
rmSync3(stage, { recursive: true, force: true });
|
|
8796
|
+
}
|
|
8797
|
+
}
|
|
8798
|
+
function bringBackMemory(sent, pod) {
|
|
8799
|
+
const out = { added: [], updated: [], conflicts: [], sent: { ...sent, files: { ...sent.files } } };
|
|
8800
|
+
const write2 = (file, body) => {
|
|
8801
|
+
mkdirSync2(dirname(file), { recursive: true });
|
|
8802
|
+
const tmp = `${file}.airgate-${process.pid}`;
|
|
8803
|
+
writeFileSync3(tmp, body, { mode: 384 });
|
|
8804
|
+
renameSync2(tmp, file);
|
|
8805
|
+
};
|
|
8806
|
+
for (const f of pod) {
|
|
8807
|
+
const rel = f.path.replace(/\\/g, "/");
|
|
8808
|
+
if (!rel || rel.startsWith("/") || rel.split("/").some((p) => p === ".." || p === "")) continue;
|
|
8809
|
+
const body = TEXTISH.test(rel) ? Buffer.from(rewritePath(f.body.toString("utf8"), sent.podPath, sent.localPath)) : f.body;
|
|
8810
|
+
const file = join3(sent.dir, rel);
|
|
8811
|
+
const local = existsSync2(file) ? readFileSync4(file) : null;
|
|
8812
|
+
if (local && local.equals(body)) {
|
|
8813
|
+
out.sent.files[rel] = sha256(local);
|
|
8814
|
+
continue;
|
|
8815
|
+
}
|
|
8816
|
+
if (!local) {
|
|
8817
|
+
write2(file, body);
|
|
8818
|
+
out.added.push(rel);
|
|
8819
|
+
out.sent.files[rel] = sha256(body);
|
|
8820
|
+
} else if (sent.files[rel] && sha256(local) === sent.files[rel]) {
|
|
8821
|
+
write2(file, body);
|
|
8822
|
+
out.updated.push(rel);
|
|
8823
|
+
out.sent.files[rel] = sha256(body);
|
|
8824
|
+
} else {
|
|
8825
|
+
const dot = rel.lastIndexOf(".");
|
|
8826
|
+
const savedAs = dot > rel.lastIndexOf("/") ? `${rel.slice(0, dot)}.from-pod${rel.slice(dot)}` : `${rel}.from-pod`;
|
|
8827
|
+
write2(join3(sent.dir, savedAs), body);
|
|
8828
|
+
out.conflicts.push({ file: rel, savedAs });
|
|
8829
|
+
}
|
|
8830
|
+
}
|
|
8831
|
+
return out;
|
|
8832
|
+
}
|
|
8755
8833
|
|
|
8756
8834
|
// src/patch.ts
|
|
8757
8835
|
var RULES2 = [
|
|
@@ -8961,7 +9039,7 @@ function podDirName(projectDir) {
|
|
|
8961
9039
|
var up = {
|
|
8962
9040
|
name: "up",
|
|
8963
9041
|
group: "up \xB7 down",
|
|
8964
|
-
usage: "[dir] [--pod p] [--agent a] [--with-config] [--env [.env]] [--session id | --no-session]",
|
|
9042
|
+
usage: "[dir] [--pod p] [--agent a] [--with-config] [--env [.env]] [--session id | --no-session] [--no-memory]",
|
|
8965
9043
|
summary: "move this project (with uncommitted work) into a pod",
|
|
8966
9044
|
example: "airgate up --with-config",
|
|
8967
9045
|
notes: [
|
|
@@ -8970,6 +9048,7 @@ var up = {
|
|
|
8970
9048
|
"--with-config also sends ~/.claude settings.json, CLAUDE.md, skills/, agents/, commands/ (never credentials).",
|
|
8971
9049
|
"remembers the pod in .airgate/state.json (kept out of git via .git/info/exclude) so `airgate down` knows where to look.",
|
|
8972
9050
|
"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.",
|
|
9051
|
+
"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.",
|
|
8973
9052
|
"--env [file] sends the keys of a dotenv file (default .env) as secrets of that pod; the names sent are listed."
|
|
8974
9053
|
],
|
|
8975
9054
|
flags: {
|
|
@@ -8981,6 +9060,7 @@ var up = {
|
|
|
8981
9060
|
env: { type: "string", value: "file", implicit: ".env", help: "send a dotenv file's keys as this pod's secrets (default .env)" },
|
|
8982
9061
|
session: { type: "string", value: "id", help: "send this Claude Code / Codex conversation (default: the latest here)" },
|
|
8983
9062
|
"no-session": { type: "boolean", help: "don't send the agent conversation" },
|
|
9063
|
+
"no-memory": { type: "boolean", help: "don't send Claude Code's memory for this project" },
|
|
8984
9064
|
yes: { type: "boolean", short: "y", help: "don't ask" }
|
|
8985
9065
|
},
|
|
8986
9066
|
async run(ctx) {
|
|
@@ -9024,6 +9104,16 @@ var up = {
|
|
|
9024
9104
|
for (const f of [findClaudeSession(home, projectDir, sessionId, ctx.io.env), findCodexSession(home, projectDir, sessionId, ctx.io.env)]) if (f) found.push(f);
|
|
9025
9105
|
if (sessionId && !found.length) throw new CliError(`no Claude Code or Codex session ${sessionId} for ${projectDir}`, "leave out --session to take the latest one");
|
|
9026
9106
|
}
|
|
9107
|
+
let memory2 = null;
|
|
9108
|
+
const memDir = bool(ctx.args, "no-memory") ? null : findClaudeMemory(home, projectDir, ctx.io.env);
|
|
9109
|
+
if (memDir) {
|
|
9110
|
+
try {
|
|
9111
|
+
memory2 = await packMemory(memDir, projectDir, podPath);
|
|
9112
|
+
} catch (e) {
|
|
9113
|
+
if (!(e instanceof TooLargeError)) throw e;
|
|
9114
|
+
ui2.err(ui2.ce("warn", `Claude Code's memory for this project is over ${bytes(UPLOAD_LIMIT)} packed: not sent`));
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9027
9117
|
const sessions = [];
|
|
9028
9118
|
for (const f of found) {
|
|
9029
9119
|
try {
|
|
@@ -9070,6 +9160,15 @@ var up = {
|
|
|
9070
9160
|
let envSent = null;
|
|
9071
9161
|
const envFile = str(ctx.args, "env");
|
|
9072
9162
|
if (envFile) envSent = await ui2.spin(`sending ${envFile} as secrets`, sendDotenv(ctx, envFile, pod.id));
|
|
9163
|
+
let memorySent;
|
|
9164
|
+
if (memory2) {
|
|
9165
|
+
try {
|
|
9166
|
+
await ui2.spin(`sending Claude Code's memory (${memory2.count} file${memory2.count === 1 ? "" : "s"})`, ctx.api.upload(pod.id, memory2.podDir, memory2.archive));
|
|
9167
|
+
memorySent = memory2.sent;
|
|
9168
|
+
} catch (e) {
|
|
9169
|
+
ui2.err(ui2.ce("warn", `couldn't send the memory: ${e.message}`));
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9073
9172
|
const sent = [];
|
|
9074
9173
|
const resumed = [];
|
|
9075
9174
|
for (const p of sessions) {
|
|
@@ -9099,7 +9198,7 @@ var up = {
|
|
|
9099
9198
|
} catch {
|
|
9100
9199
|
based = false;
|
|
9101
9200
|
}
|
|
9102
|
-
const state = { gateway: ctx.cfg.gateway, podId: pod.id, podName: pod.spec.name, dir: podDir, at: Date.now(), ...sent.length ? { sessions: sent } : {} };
|
|
9201
|
+
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 } : {} };
|
|
9103
9202
|
writeState(projectDir, state);
|
|
9104
9203
|
if (ctx.json) {
|
|
9105
9204
|
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 });
|
|
@@ -9112,6 +9211,7 @@ var up = {
|
|
|
9112
9211
|
ui2.out(`secrets for ${pod.spec.name}: ${envSent.sent.length ? envSent.sent.join(", ") : "none"}${envSent.skipped.length ? ui2.c("dim", ` (skipped: ${envSent.skipped.join(", ")})`) : ""}`);
|
|
9113
9212
|
for (const f of envSent.failed) ui2.err(ui2.ce("warn", `not sent: ${f}`));
|
|
9114
9213
|
}
|
|
9214
|
+
if (memorySent) ui2.out(`memory: ${Object.keys(memorySent.files).length} file(s) of Claude Code's memory for this project`);
|
|
9115
9215
|
for (const r of resumed) {
|
|
9116
9216
|
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}`)}`);
|
|
9117
9217
|
}
|
|
@@ -9136,8 +9236,35 @@ async function downSessions(ctx, root, state, pod, quiet2 = false) {
|
|
|
9136
9236
|
}
|
|
9137
9237
|
}
|
|
9138
9238
|
if (state.sessions?.length) writeState(root, { ...state, sessions: next });
|
|
9139
|
-
|
|
9140
|
-
|
|
9239
|
+
let memory2 = null;
|
|
9240
|
+
if (state.memory) {
|
|
9241
|
+
try {
|
|
9242
|
+
const x = await ctx.api.exportAgentMemory(pod.id, state.dir);
|
|
9243
|
+
memory2 = bringBackMemory(state.memory, x.files.map((f) => ({ path: f.path, body: Buffer.from(f.b64, "base64") })));
|
|
9244
|
+
writeState(root, { ...readState(root, state), memory: memory2.sent });
|
|
9245
|
+
} catch (e) {
|
|
9246
|
+
if (!(e instanceof ApiError && e.status === 404)) ctx.ui.err(ctx.ui.ce("warn", `couldn't bring Claude Code's memory back: ${e.message}`));
|
|
9247
|
+
}
|
|
9248
|
+
}
|
|
9249
|
+
if (!quiet2 && !ctx.json) {
|
|
9250
|
+
printSessions(ctx, out);
|
|
9251
|
+
printMemory(ctx, memory2);
|
|
9252
|
+
}
|
|
9253
|
+
return { convo: out, memory: memory2 };
|
|
9254
|
+
}
|
|
9255
|
+
function readState(root, fallback) {
|
|
9256
|
+
try {
|
|
9257
|
+
return JSON.parse(readFileSync5(statePath(root), "utf8"));
|
|
9258
|
+
} catch {
|
|
9259
|
+
return fallback;
|
|
9260
|
+
}
|
|
9261
|
+
}
|
|
9262
|
+
function printMemory(ctx, m) {
|
|
9263
|
+
if (!m) return;
|
|
9264
|
+
const n2 = m.added.length + m.updated.length;
|
|
9265
|
+
if (!n2 && !m.conflicts.length) return ctx.ui.out(ctx.ui.c("dim", "memory: nothing new in the pod"));
|
|
9266
|
+
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`);
|
|
9267
|
+
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}`);
|
|
9141
9268
|
}
|
|
9142
9269
|
function printSessions(ctx, list2) {
|
|
9143
9270
|
for (const r of list2) {
|
|
@@ -9259,11 +9386,13 @@ ${check.stderr.trim()}`, "commit or stash local edits to those files, or look wi
|
|
|
9259
9386
|
} catch {
|
|
9260
9387
|
acked = false;
|
|
9261
9388
|
}
|
|
9262
|
-
const
|
|
9263
|
-
|
|
9389
|
+
const back = takeSessions && found ? await downSessions(ctx, found.root, found.state, pod, true) : { convo: [], memory: null };
|
|
9390
|
+
const convo = back.convo;
|
|
9391
|
+
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 } });
|
|
9264
9392
|
ui2.out(`${ui2.c("ok", "down")}: applied ${files2.length} file(s). review: git status \xB7 git diff`);
|
|
9265
9393
|
if (!acked) ui2.err(ui2.ce("warn", "couldn't tell the pod what was taken; the next down will show these again"));
|
|
9266
9394
|
printSessions(ctx, convo);
|
|
9395
|
+
printMemory(ctx, back.memory);
|
|
9267
9396
|
}
|
|
9268
9397
|
};
|
|
9269
9398
|
|
|
@@ -10062,10 +10191,10 @@ var crew = {
|
|
|
10062
10191
|
aliases: ["crews"],
|
|
10063
10192
|
group: "pods",
|
|
10064
10193
|
usage: '<race|contest|council|squad|split> "<task>" [dir] [--pod p | --from snapshot | --repo owner/name] [--as role=agent] \u2026 | show|say|stop|down|rm <id>',
|
|
10065
|
-
summary: "several agents, each in its own pod, on one task: race, contest, council, squad, split",
|
|
10194
|
+
summary: "several agents, each in its own pod, on one task. missions: race, contest, council, squad, split",
|
|
10066
10195
|
example: 'airgate crew contest "add OAuth with GitHub"',
|
|
10067
10196
|
notes: [
|
|
10068
|
-
"race: builders try the same task, a judge tests each and picks one. contest: a builder builds, a reviewer reviews and tries to break it, until it holds. council: members propose plans and critique each other, a chair decides (--build to build it). squad: a builder builds, testers write tests and hunt bugs, the builder fixes. split: a planner splits the work, workers build the parts, an integrator merges.",
|
|
10197
|
+
"the mission says how the crew works. race: builders try the same task, a judge tests each and picks one. contest: a builder builds, a reviewer reviews and tries to break it, until it holds. council: members propose plans and critique each other, a chair decides (--build to build it). squad: a builder builds, testers write tests and hunt bugs, the builder fixes. split: a planner splits the work, workers build the parts, an integrator merges.",
|
|
10069
10198
|
"starting point: this folder (sent up to a pod made for the crew, deleted after), --pod (an existing pod), --from (a snapshot) or --repo. members fork from it; each has its own pod.",
|
|
10070
10199
|
"--as role=agent[:model] picks a member's agent: --as reviewer=codex --as builder-3=opencode:qwen/qwen3-coder (opencode with a model id uses airgate's built-in models).",
|
|
10071
10200
|
"airgate runs the crew, so it keeps going with this laptop closed. this command follows the room until it ends (--detach to return at once). while it follows, this laptop signs the members' model calls if it has a sign-in (airgate models).",
|
|
@@ -10095,7 +10224,7 @@ var crew = {
|
|
|
10095
10224
|
if (!list2.length) return void ui2.out(ui2.c("dim", 'no crews yet. start one: airgate crew contest "<task>" (in a project folder)'));
|
|
10096
10225
|
ui2.table(
|
|
10097
10226
|
list2.map((c2) => [c2.id, c2.recipe, tone(ctx, c2.state), c2.task.length > 48 ? `${c2.task.slice(0, 47)}\u2026` : c2.task]),
|
|
10098
|
-
{ head: ["crew", "
|
|
10227
|
+
{ head: ["crew", "mission", "", "task"] }
|
|
10099
10228
|
);
|
|
10100
10229
|
return;
|
|
10101
10230
|
}
|
|
@@ -10130,7 +10259,7 @@ var crew = {
|
|
|
10130
10259
|
return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`deleted ${id}.`);
|
|
10131
10260
|
}
|
|
10132
10261
|
}
|
|
10133
|
-
if (!KINDS.includes(first)) throw new UsageError(`"${first}" isn't a
|
|
10262
|
+
if (!KINDS.includes(first)) throw new UsageError(`"${first}" isn't a mission or an action`, `missions: ${KINDS.join(", ")} \xB7 actions: ${ACTIONS.join(", ")}`);
|
|
10134
10263
|
const kind = first;
|
|
10135
10264
|
const task2 = (second ?? "").trim();
|
|
10136
10265
|
if (!task2) throw new UsageError(`what should the crew do? airgate crew ${kind} "<task>"`);
|
|
@@ -10402,7 +10531,7 @@ var TOOLS = [
|
|
|
10402
10531
|
},
|
|
10403
10532
|
{
|
|
10404
10533
|
name: "pod_down",
|
|
10405
|
-
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.",
|
|
10534
|
+
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.",
|
|
10406
10535
|
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)") }),
|
|
10407
10536
|
async call(ctx, a) {
|
|
10408
10537
|
const argv = ["--yes"];
|
|
@@ -10445,7 +10574,7 @@ var TOOLS = [
|
|
|
10445
10574
|
},
|
|
10446
10575
|
{
|
|
10447
10576
|
name: "pod_up",
|
|
10448
|
-
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.",
|
|
10577
|
+
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.",
|
|
10449
10578
|
inputSchema: obj({
|
|
10450
10579
|
dir: s("the local project directory (default: where the server started)"),
|
|
10451
10580
|
pod: s("send into this existing pod instead of launching one"),
|
|
@@ -10561,7 +10690,7 @@ var TOOLS = [
|
|
|
10561
10690
|
},
|
|
10562
10691
|
{
|
|
10563
10692
|
name: "crew_start",
|
|
10564
|
-
description: "Start a crew: several agents, each in its own pod, on one task, run by airgate (it keeps going without you). kind: race (builders try the same task, a judge tests each and picks one), contest (a builder builds, a reviewer reviews and tries to break it, until it holds), council (members propose plans and critique each other, a chair decides; build: true also builds it), squad (a builder builds, testers write tests and hunt bugs, the builder fixes), split (a planner splits the work, workers build parts, an integrator merges). Starting point: the local project in dir (default), or pod, snapshot or repo. Returns at once with the crew id: follow with crew_status, bring the result back with crew_down. Tell the user it started and roughly what it costs (one pod per member while they work).",
|
|
10693
|
+
description: "Start a crew: several agents, each in its own pod, on one task, run by airgate (it keeps going without you). kind is the mission: race (builders try the same task, a judge tests each and picks one), contest (a builder builds, a reviewer reviews and tries to break it, until it holds), council (members propose plans and critique each other, a chair decides; build: true also builds it), squad (a builder builds, testers write tests and hunt bugs, the builder fixes), split (a planner splits the work, workers build parts, an integrator merges). Starting point: the local project in dir (default), or pod, snapshot or repo. Returns at once with the crew id: follow with crew_status, bring the result back with crew_down. Tell the user it started and roughly what it costs (one pod per member while they work).",
|
|
10565
10694
|
inputSchema: obj(
|
|
10566
10695
|
{
|
|
10567
10696
|
kind: s("race | contest | council | squad | split", { enum: ["race", "contest", "council", "squad", "split"] }),
|