@abuaboud/box 0.7.11 → 0.7.13

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 CHANGED
@@ -1,26 +1,27 @@
1
- # box
1
+ # skyfleet
2
2
 
3
- Compute for your agents: a fresh cloud machine for every task, with Claude Code on it.
3
+ Compute for your agents: a fresh cloud machine, an agent, for every task, with Claude Code on it.
4
4
 
5
5
  ```sh
6
6
  npm i -g @abuaboud/box
7
- box
7
+ skyfleet
8
8
  ```
9
9
 
10
- `box` signs you in (a browser approval, once), makes a box from your team's environment, and opens Claude on your
11
- laptop in a git worktree whose builds, tests and servers run on the box. Every port you open gets a preview link.
10
+ `skyfleet` signs you in (a browser approval, once), makes an agent from your team's environment, and opens Claude on
11
+ your laptop in a git worktree whose builds, tests and servers run on the agent. Every port you open gets a preview link.
12
12
 
13
13
  | Command | What it does |
14
14
  | --- | --- |
15
- | `box` | Pick one of your boxes, or make a new one |
16
- | `box new [-e env] [--size small\|medium\|large] [--always-on]` | Always make a fresh box |
17
- | `box <name>` | Back to a box |
18
- | `box ls` | List your boxes and where Claude runs |
19
- | `box code [env]` | Open every box of an environment in one VS Code window |
20
- | `box rm <name>` | Delete a box; unpushed work goes to `cb/<name>` first |
21
- | `box --remote` | Run Claude on the box instead, so it keeps going with the laptop shut |
22
- | `box upgrade` | Update now (box also updates itself; `CBOX_NO_UPDATE=1` turns that off) |
15
+ | `skyfleet` | Pick one of your agents, or make a new one |
16
+ | `skyfleet new [-e env] [--size small\|medium\|large] [--always-on]` | Always make a fresh agent |
17
+ | `skyfleet <name>` | Back to an agent |
18
+ | `skyfleet ls` | List your agents and where Claude runs |
19
+ | `skyfleet code [env]` | Open every agent of an environment in one VS Code window |
20
+ | `skyfleet rm <name>` | Delete an agent; unpushed work goes to `cb/<name>` first |
21
+ | `skyfleet --remote` | Run Claude on the agent instead, so it keeps going with the laptop shut |
22
+ | `skyfleet upgrade` | Update now (skyfleet also updates itself; `CBOX_NO_UPDATE=1` turns that off) |
23
23
 
24
- Every box is also an SSH host, `<name>.box`, for ssh, scp, rsync and VS Code Remote-SSH. Local boxes live in `~/.box`.
24
+ Every agent is also an SSH host, `<name>.skyfleet`, for ssh, scp, rsync and VS Code Remote-SSH (`<name>.box` still
25
+ works). Local agents live in `~/.box`. `box` still runs the same command.
25
26
 
26
27
  Dashboard: https://box.abuaboud.me · Node 22.18 or newer.
package/dist/cbox.mjs CHANGED
@@ -20,6 +20,7 @@ import { join } from "node:path";
20
20
  import ssh2 from "ssh2";
21
21
  var CONFIG_DIR = join(homedir(), ".config", "box");
22
22
  var BOXES = join(homedir(), ".box");
23
+ var MAX_SETUP_MB = 70;
23
24
  var SERVICES = { 5432: "Postgres", 6379: "Redis", 3306: "MySQL", 27017: "MongoDB", 8123: "ClickHouse" };
24
25
  function localMcp() {
25
26
  const claudeJson = join(homedir(), ".claude.json");
@@ -202,6 +203,13 @@ function label(b) {
202
203
  return styleText("yellow", `\u263E sleeps in ${span(Math.max(6e4, left))}`);
203
204
  }
204
205
 
206
+ // src/brand.ts
207
+ var BRANDS = {
208
+ box: { name: "Box", command: "box" },
209
+ skyfleet: { name: "Skyfleet", command: "skyfleet" }
210
+ };
211
+ var brand = process.env.CB_BRAND === "skyfleet" ? BRANDS.skyfleet : BRANDS.box;
212
+
205
213
  // src/api.ts
206
214
  var file = join2(CONFIG_DIR, "config.json");
207
215
  var load = () => {
@@ -252,7 +260,7 @@ async function login(server) {
252
260
  }
253
261
  async function findBox(name) {
254
262
  const box = (await api("/api/boxes")).find((b) => b.name === name || b.id === name);
255
- return box ?? fail(`No box named ${name}. box ls lists them.`);
263
+ return box ?? fail(`No agent named ${name}. ${brand.command} ls lists them.`);
256
264
  }
257
265
 
258
266
  // src/environments.ts
@@ -288,7 +296,7 @@ async function waitReady(env) {
288
296
  if (now.progress) bar.say(now.progress);
289
297
  if (now.status === "ready") {
290
298
  const secs = Math.round((Date.now() - t0) / 1e3);
291
- bar(`Snapshot ready ${dim(`${secs >= 60 ? `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s` : `${secs}s`} \xB7 next boxes start in about 2s`)}`);
299
+ bar(`Snapshot ready ${dim(`${secs >= 60 ? `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s` : `${secs}s`} \xB7 next agents start in about 2s`)}`);
292
300
  return now;
293
301
  }
294
302
  if (now.status === "failed") {
@@ -358,11 +366,11 @@ async function setUpEnvironment(repo, dir, envs) {
358
366
  console.log(` ${dim("Name ")} ${name}`);
359
367
  console.log(` ${dim("Repo ")} ${repo}`);
360
368
  console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${sizes[size]}${size === sized.size ? ` (${sized.why})` : ""}`)}`);
361
- console.log(` ${dim("Setup ")} ${setup2 ? styleText3("cyan", setup2) : dim("none")} ${dim(detected && setup2 === detected.setup ? `\xB7 from ${detected.from}` : setup2 ? "" : "\xB7 boxes start from a plain clone")}`);
369
+ console.log(` ${dim("Setup ")} ${setup2 ? styleText3("cyan", setup2) : dim("none")} ${dim(detected && setup2 === detected.setup ? `\xB7 from ${detected.from}` : setup2 ? "" : "\xB7 agents start from a plain clone")}`);
362
370
  if (names.length) console.log(` ${dim("Secrets ")} ${names.join(", ")} ${dim(secrets ? "\xB7 values copied from .env" : "\xB7 names from .env.example, values empty")}`);
363
371
  console.log();
364
372
  const choice = await select({ message: "Create it?", choices: [
365
- { name: "Create and start a box", value: "go" },
373
+ { name: "Create and start an agent", value: "go" },
366
374
  ...names.length && !secrets && dir && existsSync3(join3(dir, ".env")) ? [{ name: "Copy the secret values from my .env first", value: "secrets" }] : [],
367
375
  { name: "Change something", value: "edit" },
368
376
  { name: "Cancel", value: "cancel" }
@@ -397,14 +405,14 @@ async function ensureGitHub(repo) {
397
405
  if (!r.linked) {
398
406
  const url = new URL("/connect/github", load().server).toString();
399
407
  console.log(`
400
- ${styleText3("yellow", "!")} Connect GitHub so Box can clone ${repo} and your boxes can push as you.`);
408
+ ${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} and your agents can push as you.`);
401
409
  console.log(dim(` Opening ${url}`));
402
410
  open(url);
403
411
  const bar = progress([[0, "Waiting for you to connect GitHub in the browser"]], { pace: 6e4 });
404
412
  for (const end = Date.now() + 10 * 6e4; !r.linked; r = await status()) {
405
413
  if (Date.now() > end) {
406
414
  bar();
407
- fail("Gave up waiting after 10 minutes. Run box again when it's done.");
415
+ fail(`Gave up waiting after 10 minutes. Run ${brand.command} again when it's done.`);
408
416
  }
409
417
  await new Promise((resolve) => setTimeout(resolve, 2500));
410
418
  }
@@ -435,7 +443,7 @@ function ensureSshConfig() {
435
443
  mkdirSync3(sshDir, { recursive: true, mode: 448 });
436
444
  laptopKey();
437
445
  writeFileSync3(join4(sshDir, "box"), [
438
- "Host *.box",
446
+ "Host *.box *.skyfleet",
439
447
  " User user",
440
448
  ` IdentityFile "${join4(CONFIG_DIR, "id_ed25519")}"`,
441
449
  ` ProxyCommand ${selfCommand("proxy", "%h")}`,
@@ -452,7 +460,7 @@ function ensureSshConfig() {
452
460
  ${now}`, { mode: 384 });
453
461
  }
454
462
  async function proxy(host) {
455
- const name = host.replace(/\.box$/, "");
463
+ const name = host.replace(/\.(box|skyfleet)$/, "");
456
464
  const got = await api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) });
457
465
  const targets = [...got.ssh.hosts].reverse();
458
466
  for (let attempt = 0; attempt < 20; attempt++) {
@@ -482,15 +490,15 @@ async function proxy(host) {
482
490
  async function newLocalBox(env, branch, name, o) {
483
491
  const stop = progress([[0, "Finding a machine"], [900, `Unpacking the ${env} snapshot`], [2500, "Connecting GitHub"], [9e3, "Bigger machines take a few extra seconds"]]);
484
492
  const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, agent: "local", size: o.size, idleMinutes: o.alwaysOn ? 0 : void 0, sshKey: sshKeys(laptopKey().publicKey) }) }).finally(() => stop());
485
- ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.box`)}`);
493
+ ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.${brand.command}`)}`);
486
494
  await openLocal({ name: box.name, env, here: localCopy(), branch, fresh: true });
487
495
  }
488
496
  var CLONE = ".clone";
489
497
  var MIRROR_SKIP = [".git", "node_modules", "dist", "build", ".turbo", ".nx", ".next", ".cache", "coverage", ".DS_Store"];
490
498
  function claudeMd(name, here) {
491
499
  const lines = [
492
- `# Machine: ${name}.box`,
493
- `Run every command there with \`ssh ${name}.box '<command>'\`, never on this laptop. ./repo is a git worktree on this laptop and its files are mirrored to ~/repo on the machine: read and edit files in ./repo.`,
500
+ `# Machine: ${name}.${brand.command}`,
501
+ `Run every command there with \`ssh ${name}.${brand.command} '<command>'\`, never on this laptop. ./repo is a git worktree on this laptop and its files are mirrored to ~/repo on the machine: read and edit files in ./repo.`,
494
502
  `Run git here, never on the machine: \`git -C repo status\`, commit and push from ./repo.`
495
503
  ];
496
504
  if (here) {
@@ -535,8 +543,8 @@ function prepareFolder(name, env, here, mcp) {
535
543
  const next = { ...state, name, env, owner: load().email, local };
536
544
  writeFileSync4(join5(folder, "CLAUDE.md"), claudeMd(name, local));
537
545
  const hook = (direction) => [{ matcher: "Bash", hooks: [{ type: "command", command: selfCommand("sync", name, direction, folder), timeout: 600 }] }];
538
- writeFileSync4(join5(folder, ".claude", "settings.json"), JSON.stringify({ enableAllProjectMcpServers: true, permissions: { allow: [`Bash(ssh ${name}.box:*)`, `Bash(scp:*)`, `Bash(rsync:*)`, "mcp__box"] }, hooks: { PreToolUse: hook("push"), PostToolUse: hook("pull") } }, null, 2));
539
- if (mcp) writeFileSync4(join5(folder, ".mcp.json"), JSON.stringify({ mcpServers: { box: { type: "http", url: mcp.url, headers: { authorization: `Box ${mcp.token}` } } } }, null, 2), { mode: 384 });
546
+ writeFileSync4(join5(folder, ".claude", "settings.json"), JSON.stringify({ enableAllProjectMcpServers: true, permissions: { allow: [`Bash(ssh ${name}.box:*)`, `Bash(ssh ${name}.skyfleet:*)`, `Bash(scp:*)`, `Bash(rsync:*)`, "mcp__box", "mcp__skyfleet"] }, hooks: { PreToolUse: hook("push"), PostToolUse: hook("pull") } }, null, 2));
547
+ if (mcp) writeFileSync4(join5(folder, ".mcp.json"), JSON.stringify({ mcpServers: { [brand.command]: { type: "http", url: mcp.url, headers: { authorization: `Box ${mcp.token}` } } } }, null, 2), { mode: 384 });
540
548
  writeFileSync4(stateFile, JSON.stringify(next, null, 2));
541
549
  return folder;
542
550
  }
@@ -551,7 +559,7 @@ async function ensureClone(env) {
551
559
  const partial = `${clone}.partial`;
552
560
  rmSync(partial, { recursive: true, force: true });
553
561
  mkdirSync4(dirname(clone), { recursive: true });
554
- const bar = progress([[0, `Cloning ${env.repo} once for every ${env.name} box`]], { pace: 6e4 });
562
+ const bar = progress([[0, `Cloning ${env.repo} once for every ${env.name} agent`]], { pace: 6e4 });
555
563
  const cloned = await git([...auth, "clone", "-q", "--no-checkout", "--filter=blob:none", token ? `https://github.com/${env.repo}.git` : `git@github.com:${env.repo}.git`, partial]);
556
564
  if (!cloned.ok) {
557
565
  bar();
@@ -585,7 +593,7 @@ async function setUpWorktree({ clone, folder, name, branch, fresh }) {
585
593
  for (let attempt = 0; attempt < 4 && !copied; attempt++) copied = await (fresh ? push(name, folder, false, ["--delete"]) : pull(name, folder)) || (await new Promise((r) => setTimeout(r, 3e3)), false);
586
594
  if (copied && fresh) await alignBox(name, repo);
587
595
  bar(copied ? `${tilde(repo)} is a worktree of ${tilde(clone)}` : void 0);
588
- if (!copied) fail(`Couldn't sync ${tilde(repo)} with ${name}. Check that ssh ${name}.box works.`);
596
+ if (!copied) fail(`Couldn't sync ${tilde(repo)} with ${name}. Check that ssh ${name}.${brand.command} works.`);
589
597
  rmSync(`${repo}.mirror`, { recursive: true, force: true });
590
598
  return true;
591
599
  }
@@ -776,7 +784,7 @@ var pull = (name, folder, quiet2 = false) => rsync(`${name}.box:repo/`, join5(fo
776
784
  var push = (name, folder, quiet2 = false, extra = []) => rsync(join5(folder, "repo") + "/", `${name}.box:repo/`, extra, quiet2);
777
785
  async function openLocal({ name, env, here, branch, fresh = false }) {
778
786
  ensureSshConfig();
779
- const stop = progress([[0, `Opening ${name}`], [1500, "Waking the box up"]]);
787
+ const stop = progress([[0, `Opening ${name}`], [1500, "Waking the agent up"]]);
780
788
  const [got, envs] = await Promise.all([
781
789
  api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) }),
782
790
  api("/api/environments")
@@ -801,18 +809,18 @@ async function openLocal({ name, env, here, branch, fresh = false }) {
801
809
  clearInterval(beat);
802
810
  await stopWatching();
803
811
  await push(name, folder);
804
- ok(`Claude closed \xB7 repo synced \xB7 ${name} sleeps once it's quiet ${dim(`\xB7 come back with: box ${name}`)}`);
812
+ ok(`Claude closed \xB7 repo synced \xB7 ${name} sleeps once it's quiet ${dim(`\xB7 come back with: ${brand.command} ${name}`)}`);
805
813
  process.exit(code2);
806
814
  }
807
815
  async function sendSetup() {
808
816
  const { tar, skills, enabled, memory, ...home2 } = localClaudeHome();
809
817
  const buf = await tar;
810
- const sha = buf && buf.length <= 30 << 20 ? createHash("sha256").update(buf).digest("hex") : void 0;
818
+ const sha = buf && buf.length <= MAX_SETUP_MB << 20 ? createHash("sha256").update(buf).digest("hex") : void 0;
811
819
  const put = (body) => api("/api/laptop-setup", { method: "PUT", body: JSON.stringify({ home: { ...home2, memory }, sha, ...body }) });
812
820
  if ((await put({})).need && buf) await put({ tgz: buf.toString("base64") });
813
821
  return { skills, plugins: enabled.length, memory: !!memory.trim(), tooBig: !!buf && !sha };
814
822
  }
815
- var claude = (folder, args) => new Promise((done) => spawn3("claude", args, { cwd: folder, stdio: "inherit", env: { ...process.env, ...process.getuid?.() === 0 && { IS_SANDBOX: "1" } } }).on("error", () => fail("Claude Code is not installed on this laptop. npm i -g @anthropic-ai/claude-code, or use box --remote.")).on("close", (c) => done(c ?? 0)));
823
+ var claude = (folder, args) => new Promise((done) => spawn3("claude", args, { cwd: folder, stdio: "inherit", env: { ...process.env, ...process.getuid?.() === 0 && { IS_SANDBOX: "1" } } }).on("error", () => fail(`Claude Code is not installed on this laptop. npm i -g @anthropic-ai/claude-code, or use ${brand.command} --remote.`)).on("close", (c) => done(c ?? 0)));
816
824
  function trustFolder(folder) {
817
825
  const file2 = join5(homedir4(), ".claude.json");
818
826
  try {
@@ -831,7 +839,7 @@ async function sync(name, direction, folder) {
831
839
  command = JSON.parse(Buffer.concat(chunks).toString("utf8")).tool_input?.command ?? "";
832
840
  } catch {
833
841
  }
834
- if (!command.includes(`${name}.box`) || !existsSync5(join5(folder, "repo"))) return;
842
+ if (![`${name}.box`, `${name}.skyfleet`].some((host) => command.includes(host)) || !existsSync5(join5(folder, "repo"))) return;
835
843
  await (direction === "push" ? push(name, folder) : pull(name, folder));
836
844
  }
837
845
  async function code(envArg) {
@@ -839,10 +847,10 @@ async function code(envArg) {
839
847
  await prune(boxes);
840
848
  const here = localBoxes().filter((b) => existsSync5(join5(b.folder, "repo")));
841
849
  const envs = [...new Set(here.map((b) => b.env))].sort();
842
- if (!envs.length) return console.log(dim("No boxes on this laptop yet. Run box first."));
843
- const env = envArg ?? (envs.length === 1 ? envs[0] : await select2({ message: "Open the boxes of which environment?", choices: envs.map((e) => ({ name: `${e.padEnd(18)}${dim(`${here.filter((b) => b.env === e).length} boxes`)}`, value: e })) }));
850
+ if (!envs.length) return console.log(dim(`No agents on this laptop yet. Run ${brand.command} first.`));
851
+ const env = envArg ?? (envs.length === 1 ? envs[0] : await select2({ message: "Open the agents of which environment?", choices: envs.map((e) => ({ name: `${e.padEnd(18)}${dim(`${here.filter((b) => b.env === e).length} agents`)}`, value: e })) }));
844
852
  const group = here.filter((b) => b.env === env);
845
- if (!group.length) fail(`No boxes from ${env} on this laptop.`);
853
+ if (!group.length) fail(`No agents from ${env} on this laptop.`);
846
854
  const awake = new Set(boxes.filter((b) => b.state === "running").map((b) => b.name));
847
855
  const environment = (await api("/api/environments")).find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
848
856
  const clone = await ensureClone(environment);
@@ -853,10 +861,10 @@ async function code(envArg) {
853
861
  const workspace = join5(BOXES, env, `${env}.code-workspace`);
854
862
  writeFileSync4(workspace, JSON.stringify({ folders: group.map((b) => ({ name: basename(b.folder), path: `${basename(b.folder)}/repo` })), settings: { "files.exclude": { "**/node_modules": true } } }, null, 2));
855
863
  spawn3("code", [workspace], { stdio: "ignore", detached: true }).on("error", () => miss(`VS Code's code command isn't installed. In VS Code run: Shell Command: Install 'code' command in PATH, or open ${tilde(workspace)}.`)).unref();
856
- ok(`${group.length} ${env} box${group.length === 1 ? "" : "es"} in one window ${dim(`\xB7 ${tilde(workspace)}`)}`);
864
+ ok(`${group.length} ${env} agent${group.length === 1 ? "" : "s"} in one window ${dim(`\xB7 ${tilde(workspace)}`)}`);
857
865
  await Promise.all(group.filter((b) => awake.has(b.name) && !migrated.includes(b.name)).map((b) => pull(b.name, b.folder, true)));
858
866
  const stops = group.map((b) => watchRepo(b.name, b.folder));
859
- console.log(dim("Syncing your saves to the boxes. Leave this open; ctrl+c stops syncing."));
867
+ console.log(dim("Syncing your saves to the agents. Leave this open; ctrl+c stops syncing."));
860
868
  await new Promise((resolve) => process.once("SIGINT", resolve));
861
869
  await Promise.all(stops.map((s) => s()));
862
870
  ok("Stopped syncing");
@@ -975,7 +983,7 @@ async function newRemoteBox(env, branch, name, o) {
975
983
  const agent = o.agent;
976
984
  const idle = o.alwaysOn ? "0" : void 0;
977
985
  const { mcpServers, mcpOAuth, skipped } = localMcp();
978
- ok(`${mcpCount(mcpServers)} will be copied into the box ${dim(skipped.length ? `(skipping local-only: ${skipped.join(", ")})` : "")}`);
986
+ ok(`${mcpCount(mcpServers)} will be copied into the agent ${dim(skipped.length ? `(skipping local-only: ${skipped.join(", ")})` : "")}`);
979
987
  const claudeHome = localClaudeHome();
980
988
  const stop = progress([
981
989
  [0, "Finding a machine"],
@@ -994,7 +1002,7 @@ async function newRemoteBox(env, branch, name, o) {
994
1002
  }
995
1003
  async function sendHome(name, tar) {
996
1004
  const buf = await tar;
997
- if (!buf || buf.length > 30 << 20) return api(`/api/boxes/${name}/home`, { method: "POST", body: "{}" }).catch(() => void 0);
1005
+ if (!buf || buf.length > MAX_SETUP_MB << 20) return api(`/api/boxes/${name}/home`, { method: "POST", body: "{}" }).catch(() => void 0);
998
1006
  const sha = createHash2("sha256").update(buf).digest("hex");
999
1007
  const post = (body) => api(`/api/boxes/${name}/home`, { method: "POST", body: JSON.stringify(body) });
1000
1008
  if ((await post({ sha }).catch(() => ({ need: false }))).need) await post({ sha, tgz: buf.toString("base64") }).catch(() => void 0);
@@ -1002,7 +1010,7 @@ async function sendHome(name, tar) {
1002
1010
  async function dial({ user, hosts }, privateKey, attempts) {
1003
1011
  const once = () => new Promise((resolve, reject) => {
1004
1012
  let left = hosts.length, won = false;
1005
- if (!left) return reject(new Error("The box has no address yet."));
1013
+ if (!left) return reject(new Error("The agent has no address yet."));
1006
1014
  for (const [host, port] of hosts) {
1007
1015
  const c = new ssh22.Client();
1008
1016
  c.on("ready", () => {
@@ -1026,7 +1034,7 @@ async function dial({ user, hosts }, privateKey, attempts) {
1026
1034
  async function attach(name, ready) {
1027
1035
  if (!name) {
1028
1036
  const boxes = await api("/api/boxes");
1029
- if (!boxes.length) return console.log(dim("No boxes yet. Run box."));
1037
+ if (!boxes.length) return console.log(dim(`No agents yet. Run ${brand.command}.`));
1030
1038
  name = await select3({ message: "Attach to", choices: boxes.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}`, value: b.name })) });
1031
1039
  }
1032
1040
  const boxName = name;
@@ -1056,7 +1064,7 @@ async function attach(name, ready) {
1056
1064
  process.stdout.write(TERMINAL_RESET);
1057
1065
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1058
1066
  console.log(dim(`
1059
- Detached. ${boxName} sleeps once Claude is done. box ${boxName} to return.`));
1067
+ Detached. ${boxName} sleeps once Claude is done. ${brand.command} ${boxName} to return.`));
1060
1068
  process.exit(0);
1061
1069
  }
1062
1070
  let ssh = ready?.ssh, attempts = 4, announced = false;
@@ -1064,7 +1072,7 @@ Detached. ${boxName} sleeps once Claude is done. box ${boxName} to return.`));
1064
1072
  if (!ssh) {
1065
1073
  attempts = 4;
1066
1074
  const box = await findBox(boxName);
1067
- if (box.state === "gone") fail(`${boxName} is gone. Make a new one with box.`);
1075
+ if (box.state === "gone") fail(`${boxName} is gone. Make a new one with ${brand.command}.`);
1068
1076
  if (box.state === "paused" || box.state === "pausing") {
1069
1077
  sleepScreen(box);
1070
1078
  if (!keys.length) await new Promise((r) => {
@@ -1073,12 +1081,12 @@ Detached. ${boxName} sleeps once Claude is done. box ${boxName} to return.`));
1073
1081
  wakingScreen(box, keys);
1074
1082
  pressed = () => wakingScreen(box, keys);
1075
1083
  }
1076
- const stop = keys.length ? () => void 0 : progress([[0, `Opening ${boxName}`], [1200, "Waking the box up"], [4e3, "Starting Claude"]]);
1084
+ const stop = keys.length ? () => void 0 : progress([[0, `Opening ${boxName}`], [1200, "Waking the agent up"], [4e3, "Starting Claude"]]);
1077
1085
  ssh = (await api(`/api/boxes/${boxName}/attach`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(key.publicKey) }) }).finally(() => stop())).ssh;
1078
1086
  }
1079
1087
  conn = await dial(ssh, key.privateKey, attempts).catch(() => void 0);
1080
1088
  attempts = 1;
1081
- if (conn && !announced && !keys.length) console.log(dim(`Attaching to ${boxName}. Detach with ctrl-], come back with box ${boxName}.`));
1089
+ if (conn && !announced && !keys.length) console.log(dim(`Attaching to ${boxName}. Detach with ctrl-], come back with ${brand.command} ${boxName}.`));
1082
1090
  announced = true;
1083
1091
  if (!conn) {
1084
1092
  ssh = void 0;
@@ -1185,11 +1193,11 @@ async function updateFirst() {
1185
1193
  if (!prefix || process.env.CBOX_NO_UPDATE || process.env.CBOX_UPDATED) return;
1186
1194
  const latest = await fetch(LATEST, { signal: AbortSignal.timeout(2500) }).then((r) => r.json()).then((j) => j.version, () => void 0);
1187
1195
  if (!latest || !newer(latest, VERSION)) return;
1188
- const stop = progress([[0, `Updating box ${VERSION} \u2192 ${latest}`]]);
1196
+ const stop = progress([[0, `Updating ${brand.command} ${VERSION} \u2192 ${latest}`]]);
1189
1197
  const done = spawnSync(NPM, installArgs(latest, prefix), { stdio: "ignore" }).status === 0;
1190
1198
  stop();
1191
- if (!done) return miss(`Couldn't update box to ${latest}, so carrying on with ${VERSION}`);
1192
- ok(`Updated box ${dim(VERSION)} \u2192 ${latest}`);
1199
+ if (!done) return miss(`Couldn't update ${brand.command} to ${latest}, so carrying on with ${VERSION}`);
1200
+ ok(`Updated ${brand.command} ${dim(VERSION)} \u2192 ${latest}`);
1193
1201
  try {
1194
1202
  writeFileSync5(join6(CONFIG_DIR, "update.json"), JSON.stringify({ version: latest, checkedAt: Date.now() }));
1195
1203
  } catch {
@@ -1206,7 +1214,7 @@ function autoUpdate() {
1206
1214
  state = JSON.parse(readFileSync6(file2, "utf8"));
1207
1215
  } catch {
1208
1216
  }
1209
- if (state.version && state.version !== VERSION) ok(`Updated box ${dim(state.version)} \u2192 ${VERSION}`);
1217
+ if (state.version && state.version !== VERSION) ok(`Updated ${brand.command} ${dim(state.version)} \u2192 ${VERSION}`);
1210
1218
  const check = !state.checkedAt || Date.now() - state.checkedAt > 12 * 36e5;
1211
1219
  try {
1212
1220
  mkdirSync5(CONFIG_DIR, { recursive: true });
@@ -1225,14 +1233,14 @@ function autoUpdate() {
1225
1233
  }
1226
1234
  async function upgrade() {
1227
1235
  const latest = await fetch(LATEST).then((r) => r.json()).then((j) => j.version).catch(() => fail("Could not reach the npm registry."));
1228
- if (!newer(latest, VERSION)) return ok(`box ${VERSION} is the latest`);
1229
- console.log(dim(`Updating box ${VERSION} \u2192 ${latest}\u2026`));
1236
+ if (!newer(latest, VERSION)) return ok(`${brand.command} ${VERSION} is the latest`);
1237
+ console.log(dim(`Updating ${brand.command} ${VERSION} \u2192 ${latest}\u2026`));
1230
1238
  try {
1231
1239
  execFileSync4(NPM, installArgs(latest, npmPrefix()), { stdio: "inherit" });
1232
1240
  } catch {
1233
- fail(`npm could not update box. Try: npm i -g ${PACKAGE}@latest`);
1241
+ fail(`npm could not update ${brand.command}. Try: npm i -g ${PACKAGE}@latest`);
1234
1242
  }
1235
- ok(`Updated box to ${latest}`);
1243
+ ok(`Updated ${brand.command} to ${latest}`);
1236
1244
  }
1237
1245
 
1238
1246
  // src/index.ts
@@ -1241,7 +1249,7 @@ var NAME = /^[a-z0-9][a-z0-9-]{0,30}$/;
1241
1249
  async function newBox(o = {}) {
1242
1250
  await updateFirst();
1243
1251
  if (o.size && !["small", "medium", "large"].includes(o.size)) fail("Pick --size small, medium or large.");
1244
- if (o.name !== void 0 && !NAME.test(o.name)) fail("Name the box with lowercase letters, digits and dashes, like webhook-retry.");
1252
+ if (o.name !== void 0 && !NAME.test(o.name)) fail("Name the agent with lowercase letters, digits and dashes, like webhook-retry.");
1245
1253
  const here = gitRepo();
1246
1254
  const picked = await chooseEnvironment(o.env, here);
1247
1255
  const branch = here && picked.repo.toLowerCase() === here.repo.toLowerCase() ? here.branch : void 0;
@@ -1253,7 +1261,7 @@ async function askName(branch) {
1253
1261
  const suggestion = fromBranch || `${ANIMALS[Math.floor(Math.random() * ANIMALS.length)]}-${Math.random().toString(16).slice(2, 5)}`;
1254
1262
  if (!process.stdin.isTTY) return void 0;
1255
1263
  const picked = await input2({
1256
- message: `Name this box ${dim("(optional \xB7 you will see it in its folder, ssh name and the dashboard)")}`,
1264
+ message: `Name this agent ${dim("(optional \xB7 you will see it in its folder, ssh name and the dashboard)")}`,
1257
1265
  default: suggestion,
1258
1266
  validate: (v) => !v.trim() || NAME.test(v.trim()) || "Lowercase letters, digits and dashes"
1259
1267
  });
@@ -1261,22 +1269,22 @@ async function askName(branch) {
1261
1269
  }
1262
1270
  async function home(name, o) {
1263
1271
  const making = o.env || o.remote || o.agent || o.size || o.alwaysOn;
1264
- if (name && making) fail(`--env, --remote, --agent, --size and --always-on only shape a new box. Open ${name} with box ${name}, or make a new one with box new --name <name> and the options.`);
1272
+ if (name && making) fail(`--env, --remote, --agent, --size and --always-on only shape a new agent. Open ${name} with ${brand.command} ${name}, or make a new one with ${brand.command} new --name <name> and the options.`);
1265
1273
  if (name) return openBox(name);
1266
1274
  const email = load().email;
1267
1275
  const all = await api("/api/boxes");
1268
1276
  await prune(all);
1269
1277
  const mine = all.filter((b) => b.state !== "gone" && (!email || b.owner === email));
1270
1278
  if (!mine.length || !process.stdin.isTTY || making) return newBox(o);
1271
- const pick = await select4({ message: "Open a box", choices: [
1279
+ const pick = await select4({ message: "Open an agent", choices: [
1272
1280
  ...mine.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}${dim(b.agent === "local" ? "" : " \xB7 remote")}`, value: b.name })),
1273
- { name: styleText5("cyan", "+ new box"), value: "" }
1281
+ { name: styleText5("cyan", "+ new agent"), value: "" }
1274
1282
  ] });
1275
1283
  return pick ? openBox(pick) : newBox(o);
1276
1284
  }
1277
1285
  async function openBox(name) {
1278
1286
  const box = await findBox(name);
1279
- if (box.state === "gone") fail(`${name} is gone. Make a new one with box new.`);
1287
+ if (box.state === "gone") fail(`${name} is gone. Make a new one with ${brand.command} new.`);
1280
1288
  return box.agent === "local" ? openLocal({ name: box.name, env: box.env ?? "boxes", here: localCopy(), branch: box.branch ?? void 0 }) : attach(box.name);
1281
1289
  }
1282
1290
  async function ls() {
@@ -1284,9 +1292,9 @@ async function ls() {
1284
1292
  const all = await api("/api/boxes");
1285
1293
  await prune(all);
1286
1294
  const mine = all.filter((b) => !email || b.owner === email);
1287
- if (!mine.length) return console.log(dim("No boxes of yours. Run box."));
1295
+ if (!mine.length) return console.log(dim(`No agents of yours. Run ${brand.command}.`));
1288
1296
  const width = Math.max(...mine.map((b) => b.name.length)) + 2;
1289
- console.log(dim(`${"BOX".padEnd(width)}${"STATE".padEnd(20)}WHERE`));
1297
+ console.log(dim(`${"AGENT".padEnd(width)}${"STATE".padEnd(20)}WHERE`));
1290
1298
  for (const b of mine) {
1291
1299
  const l = label(b);
1292
1300
  console.log(`${b.name.padEnd(width)}${l}${" ".repeat(Math.max(1, 20 - l.replace(/\x1b\[[0-9;]*m/g, "").length))}${dim(b.agent === "local" ? "laptop" : "remote")}`);
@@ -1295,30 +1303,30 @@ async function ls() {
1295
1303
  async function setup() {
1296
1304
  const sent = await sendSetup();
1297
1305
  const n = (k, word) => `${k} ${word}${k === 1 ? "" : "s"}`;
1298
- ok(`Sent ${[n(sent.skills, "skill"), n(sent.plugins, "plugin"), ...sent.memory ? ["CLAUDE.md"] : []].join(", ")} ${dim("\xB7 @Box and webhooks start boxes with these")}`);
1299
- if (sent.tooBig) miss("Your skills and plugins are over 30 MB, so only CLAUDE.md and the plugin list were sent.");
1306
+ ok(`Sent ${[n(sent.skills, "skill"), n(sent.plugins, "plugin"), ...sent.memory ? ["CLAUDE.md"] : []].join(", ")} ${dim(`\xB7 @${brand.name} and webhooks start agents with these`)}`);
1307
+ if (sent.tooBig) miss(`Your skills and plugins are over ${MAX_SETUP_MB} MB, so only CLAUDE.md and the plugin list were sent.`);
1300
1308
  const { data } = await api("/api/accounts");
1301
1309
  const missing = ["claude", "github"].filter((id) => data.find((a) => a.id === id)?.status !== "ready").map((id) => id === "claude" ? "sign in to Claude" : "connect GitHub");
1302
- if (missing.length) miss(`Before @Box can start boxes as you, ${missing.join(" and ")}: ${new URL("/boxes?new", load().server)}`);
1310
+ if (missing.length) miss(`Before @${brand.name} can start agents as you, ${missing.join(" and ")}: ${new URL("/boxes?new", load().server)}`);
1303
1311
  else ok("Claude and GitHub are connected");
1304
1312
  }
1305
1313
  var quiet = ["proxy", "sync"].includes(process.argv[2] ?? "");
1306
1314
  if (quiet) console.log = console.error;
1307
- if (!quiet && !process.env.CBOX_UPDATED && !process.argv.slice(2).some((a) => ["-V", "--version", "-h", "--help", "help"].includes(a))) console.log(dim(`box ${VERSION}`));
1315
+ if (!quiet && !process.env.CBOX_UPDATED && !process.argv.slice(2).some((a) => ["-V", "--version", "-h", "--help", "help"].includes(a))) console.log(dim(`${brand.command} ${VERSION}`));
1308
1316
  if (!quiet) {
1309
1317
  autoUpdate();
1310
1318
  ensureSshConfig();
1311
1319
  }
1312
- var boxOptions = (c) => c.option("-e, --env <name>", "environment").option("-n, --name <name>", "name the box, like webhook-retry (default: asked, or generated)").option("--remote", "run Claude on the machine instead of this laptop").option("--agent <name>", "claude (default) or codex; codex implies --remote").option("--size <size>", "small, medium or large (default: the environment's)").option("--always-on", "never sleep, for example for a demo");
1313
- var cli = boxOptions(new Command("box").enablePositionalOptions().description("Dev machines for Claude: Claude on your laptop, the work on a box").version(VERSION, "-v, --version").argument("[box]", "open this box")).action((name, o) => home(name, o));
1314
- boxOptions(cli.command("new").description("always make a fresh box")).action((o) => newBox(o));
1315
- cli.command("ls").description("list your boxes").action(ls);
1316
- cli.command("code [env]").description("open an environment's boxes in one VS Code window, each a git worktree, and sync your saves").action((env) => code(env));
1317
- cli.command("rm <box>").option("-f, --force", "delete even while someone is attached or Claude is working").description("delete a box, pushing unpushed work to cb/<box>").action(rm);
1318
- cli.command("setup").description("send your skills, plugins and CLAUDE.md for @Box and webhooks, and check Claude and GitHub").action(setup);
1319
- cli.command("login").option("--server <url>", "box server").description("sign in from the browser").action((o) => login(o.server));
1320
- cli.command("upgrade").description("update box to the latest version now").action(upgrade);
1321
- cli.command("attach [box]", { hidden: true }).action((name) => name ? openBox(name) : home(void 0, {}));
1320
+ var boxOptions = (c) => c.option("-e, --env <name>", "environment").option("-n, --name <name>", "name the agent, like webhook-retry (default: asked, or generated)").option("--remote", "run Claude on the machine instead of this laptop").option("--agent <name>", "claude (default) or codex; codex implies --remote").option("--size <size>", "small, medium or large (default: the environment's)").option("--always-on", "never sleep, for example for a demo");
1321
+ var cli = boxOptions(new Command(brand.command).enablePositionalOptions().description("Dev machines for Claude: Claude on your laptop, the work in the cloud").version(VERSION, "-v, --version").argument("[agent]", "open this agent")).action((name, o) => home(name, o));
1322
+ boxOptions(cli.command("new").description("always make a fresh agent")).action((o) => newBox(o));
1323
+ cli.command("ls").description("list your agents").action(ls);
1324
+ cli.command("code [env]").description("open an environment's agents in one VS Code window, each a git worktree, and sync your saves").action((env) => code(env));
1325
+ cli.command("rm <agent>").option("-f, --force", "delete even while someone is attached or Claude is working").description("delete an agent, pushing unpushed work to cb/<agent>").action(rm);
1326
+ cli.command("setup").description(`send your skills, plugins and CLAUDE.md for @${brand.name} and webhooks, and check Claude and GitHub`).action(setup);
1327
+ cli.command("login").option("--server <url>", `${brand.name} server`).description("sign in from the browser").action((o) => login(o.server));
1328
+ cli.command("upgrade").description(`update ${brand.command} to the latest version now`).action(upgrade);
1329
+ cli.command("attach [agent]", { hidden: true }).action((name) => name ? openBox(name) : home(void 0, {}));
1322
1330
  cli.command("proxy <host>", { hidden: true }).action(proxy);
1323
- cli.command("sync <box> <direction> <folder>", { hidden: true }).action(sync);
1331
+ cli.command("sync <agent> <direction> <folder>", { hidden: true }).action(sync);
1324
1332
  await cli.parseAsync();
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ process.env.CB_BRAND = 'skyfleet'
3
+ await import('./cbox.mjs')
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@abuaboud/box",
3
- "version": "0.7.11",
4
- "description": "box: Claude Code on your laptop, the work on a cloud dev machine you reach as <name>.box, with your team databases through MCP.",
3
+ "version": "0.7.13",
4
+ "description": "skyfleet: Claude Code on your laptop, the work on a cloud dev machine (a ship) you reach as <name>.skyfleet, with your team databases through MCP.",
5
5
  "type": "module",
6
6
  "bin": {
7
+ "skyfleet": "dist/skyfleet.mjs",
7
8
  "box": "dist/box.mjs"
8
9
  },
9
10
  "files": [
10
11
  "dist"
11
12
  ],
12
13
  "scripts": {
13
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --packages=external --outfile=dist/cbox.mjs && cp src/box.mjs dist/box.mjs && chmod +x dist/box.mjs",
14
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --packages=external --outfile=dist/cbox.mjs && cp src/box.mjs src/skyfleet.mjs dist/ && chmod +x dist/box.mjs dist/skyfleet.mjs",
14
15
  "prepublishOnly": "npm run build"
15
16
  },
16
17
  "engines": {