@abuaboud/box 0.7.12 → 0.7.14

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
@@ -63,7 +63,7 @@ function localClaudeHome() {
63
63
  const paths = [...dirs, ...Object.values(plugins).flat().map((e) => e.installPath), ...Object.values(marketplaces).map((m) => m.installLocation)].map((p) => p.startsWith(root) ? p.slice(root.length + 1) : p);
64
64
  const tar = !paths.length ? Promise.resolve(void 0) : new Promise((done) => execFile("tar", ["-chf", "-", "--no-xattrs", "--exclude=.git", "--exclude=node_modules", "--exclude=.DS_Store", "-C", root, ...new Set(paths)], { encoding: "buffer", maxBuffer: 256 << 20, env: { ...process.env, COPYFILE_DISABLE: "1" } }, (err, out) => err ? done(void 0) : gzip(out, (e, gz) => done(e ? void 0 : gz))));
65
65
  const memory = existsSync(join(root, "CLAUDE.md")) ? readFileSync(join(root, "CLAUDE.md"), "utf8") : "";
66
- return { from: root, pending: paths.length > 0, tar, plugins, marketplaces, enabled: Object.keys(plugins), memory, skills: existsSync(join(root, "skills")) ? readdirSync(join(root, "skills")).filter((f) => !f.startsWith(".")).length : 0 };
66
+ return { from: root, pending: paths.length > 0, tar, plugins, marketplaces, enabled: Object.keys(plugins), memory, skills: existsSync(join(root, "skills")) ? readdirSync(join(root, "skills")).filter((f) => !f.startsWith(".")) : [] };
67
67
  }
68
68
  function laptopKey() {
69
69
  const priv = join(CONFIG_DIR, "id_ed25519");
@@ -203,6 +203,13 @@ function label(b) {
203
203
  return styleText("yellow", `\u263E sleeps in ${span(Math.max(6e4, left))}`);
204
204
  }
205
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
+
206
213
  // src/api.ts
207
214
  var file = join2(CONFIG_DIR, "config.json");
208
215
  var load = () => {
@@ -253,7 +260,7 @@ async function login(server) {
253
260
  }
254
261
  async function findBox(name) {
255
262
  const box = (await api("/api/boxes")).find((b) => b.name === name || b.id === name);
256
- return box ?? fail(`No box named ${name}. box ls lists them.`);
263
+ return box ?? fail(`No agent named ${name}. ${brand.command} ls lists them.`);
257
264
  }
258
265
 
259
266
  // src/environments.ts
@@ -289,7 +296,7 @@ async function waitReady(env) {
289
296
  if (now.progress) bar.say(now.progress);
290
297
  if (now.status === "ready") {
291
298
  const secs = Math.round((Date.now() - t0) / 1e3);
292
- 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`)}`);
293
300
  return now;
294
301
  }
295
302
  if (now.status === "failed") {
@@ -359,11 +366,11 @@ async function setUpEnvironment(repo, dir, envs) {
359
366
  console.log(` ${dim("Name ")} ${name}`);
360
367
  console.log(` ${dim("Repo ")} ${repo}`);
361
368
  console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${sizes[size]}${size === sized.size ? ` (${sized.why})` : ""}`)}`);
362
- 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")}`);
363
370
  if (names.length) console.log(` ${dim("Secrets ")} ${names.join(", ")} ${dim(secrets ? "\xB7 values copied from .env" : "\xB7 names from .env.example, values empty")}`);
364
371
  console.log();
365
372
  const choice = await select({ message: "Create it?", choices: [
366
- { name: "Create and start a box", value: "go" },
373
+ { name: "Create and start an agent", value: "go" },
367
374
  ...names.length && !secrets && dir && existsSync3(join3(dir, ".env")) ? [{ name: "Copy the secret values from my .env first", value: "secrets" }] : [],
368
375
  { name: "Change something", value: "edit" },
369
376
  { name: "Cancel", value: "cancel" }
@@ -398,14 +405,14 @@ async function ensureGitHub(repo) {
398
405
  if (!r.linked) {
399
406
  const url = new URL("/connect/github", load().server).toString();
400
407
  console.log(`
401
- ${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.`);
402
409
  console.log(dim(` Opening ${url}`));
403
410
  open(url);
404
411
  const bar = progress([[0, "Waiting for you to connect GitHub in the browser"]], { pace: 6e4 });
405
412
  for (const end = Date.now() + 10 * 6e4; !r.linked; r = await status()) {
406
413
  if (Date.now() > end) {
407
414
  bar();
408
- 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.`);
409
416
  }
410
417
  await new Promise((resolve) => setTimeout(resolve, 2500));
411
418
  }
@@ -436,7 +443,7 @@ function ensureSshConfig() {
436
443
  mkdirSync3(sshDir, { recursive: true, mode: 448 });
437
444
  laptopKey();
438
445
  writeFileSync3(join4(sshDir, "box"), [
439
- "Host *.box",
446
+ "Host *.box *.skyfleet",
440
447
  " User user",
441
448
  ` IdentityFile "${join4(CONFIG_DIR, "id_ed25519")}"`,
442
449
  ` ProxyCommand ${selfCommand("proxy", "%h")}`,
@@ -453,7 +460,7 @@ function ensureSshConfig() {
453
460
  ${now}`, { mode: 384 });
454
461
  }
455
462
  async function proxy(host) {
456
- const name = host.replace(/\.box$/, "");
463
+ const name = host.replace(/\.(box|skyfleet)$/, "");
457
464
  const got = await api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) });
458
465
  const targets = [...got.ssh.hosts].reverse();
459
466
  for (let attempt = 0; attempt < 20; attempt++) {
@@ -483,15 +490,15 @@ async function proxy(host) {
483
490
  async function newLocalBox(env, branch, name, o) {
484
491
  const stop = progress([[0, "Finding a machine"], [900, `Unpacking the ${env} snapshot`], [2500, "Connecting GitHub"], [9e3, "Bigger machines take a few extra seconds"]]);
485
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());
486
- ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.box`)}`);
493
+ ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.${brand.command}`)}`);
487
494
  await openLocal({ name: box.name, env, here: localCopy(), branch, fresh: true });
488
495
  }
489
496
  var CLONE = ".clone";
490
497
  var MIRROR_SKIP = [".git", "node_modules", "dist", "build", ".turbo", ".nx", ".next", ".cache", "coverage", ".DS_Store"];
491
498
  function claudeMd(name, here) {
492
499
  const lines = [
493
- `# Machine: ${name}.box`,
494
- `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.`,
495
502
  `Run git here, never on the machine: \`git -C repo status\`, commit and push from ./repo.`
496
503
  ];
497
504
  if (here) {
@@ -536,8 +543,8 @@ function prepareFolder(name, env, here, mcp) {
536
543
  const next = { ...state, name, env, owner: load().email, local };
537
544
  writeFileSync4(join5(folder, "CLAUDE.md"), claudeMd(name, local));
538
545
  const hook = (direction) => [{ matcher: "Bash", hooks: [{ type: "command", command: selfCommand("sync", name, direction, folder), timeout: 600 }] }];
539
- 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));
540
- 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 });
541
548
  writeFileSync4(stateFile, JSON.stringify(next, null, 2));
542
549
  return folder;
543
550
  }
@@ -552,7 +559,7 @@ async function ensureClone(env) {
552
559
  const partial = `${clone}.partial`;
553
560
  rmSync(partial, { recursive: true, force: true });
554
561
  mkdirSync4(dirname(clone), { recursive: true });
555
- 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 });
556
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]);
557
564
  if (!cloned.ok) {
558
565
  bar();
@@ -586,7 +593,7 @@ async function setUpWorktree({ clone, folder, name, branch, fresh }) {
586
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);
587
594
  if (copied && fresh) await alignBox(name, repo);
588
595
  bar(copied ? `${tilde(repo)} is a worktree of ${tilde(clone)}` : void 0);
589
- 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.`);
590
597
  rmSync(`${repo}.mirror`, { recursive: true, force: true });
591
598
  return true;
592
599
  }
@@ -777,7 +784,7 @@ var pull = (name, folder, quiet2 = false) => rsync(`${name}.box:repo/`, join5(fo
777
784
  var push = (name, folder, quiet2 = false, extra = []) => rsync(join5(folder, "repo") + "/", `${name}.box:repo/`, extra, quiet2);
778
785
  async function openLocal({ name, env, here, branch, fresh = false }) {
779
786
  ensureSshConfig();
780
- const stop = progress([[0, `Opening ${name}`], [1500, "Waking the box up"]]);
787
+ const stop = progress([[0, `Opening ${name}`], [1500, "Waking the agent up"]]);
781
788
  const [got, envs] = await Promise.all([
782
789
  api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) }),
783
790
  api("/api/environments")
@@ -802,18 +809,18 @@ async function openLocal({ name, env, here, branch, fresh = false }) {
802
809
  clearInterval(beat);
803
810
  await stopWatching();
804
811
  await push(name, folder);
805
- 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}`)}`);
806
813
  process.exit(code2);
807
814
  }
808
815
  async function sendSetup() {
809
816
  const { tar, skills, enabled, memory, ...home2 } = localClaudeHome();
810
817
  const buf = await tar;
811
818
  const sha = buf && buf.length <= MAX_SETUP_MB << 20 ? createHash("sha256").update(buf).digest("hex") : void 0;
812
- const put = (body) => api("/api/laptop-setup", { method: "PUT", body: JSON.stringify({ home: { ...home2, memory }, sha, ...body }) });
819
+ const put = (body) => api("/api/laptop-setup", { method: "PUT", body: JSON.stringify({ home: { ...home2, memory, skills }, sha, ...body }) });
813
820
  if ((await put({})).need && buf) await put({ tgz: buf.toString("base64") });
814
- return { skills, plugins: enabled.length, memory: !!memory.trim(), tooBig: !!buf && !sha };
821
+ return { skills: skills.length, plugins: enabled.length, memory: !!memory.trim(), tooBig: !!buf && !sha };
815
822
  }
816
- 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)));
817
824
  function trustFolder(folder) {
818
825
  const file2 = join5(homedir4(), ".claude.json");
819
826
  try {
@@ -832,7 +839,7 @@ async function sync(name, direction, folder) {
832
839
  command = JSON.parse(Buffer.concat(chunks).toString("utf8")).tool_input?.command ?? "";
833
840
  } catch {
834
841
  }
835
- 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;
836
843
  await (direction === "push" ? push(name, folder) : pull(name, folder));
837
844
  }
838
845
  async function code(envArg) {
@@ -840,10 +847,10 @@ async function code(envArg) {
840
847
  await prune(boxes);
841
848
  const here = localBoxes().filter((b) => existsSync5(join5(b.folder, "repo")));
842
849
  const envs = [...new Set(here.map((b) => b.env))].sort();
843
- if (!envs.length) return console.log(dim("No boxes on this laptop yet. Run box first."));
844
- 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 })) }));
845
852
  const group = here.filter((b) => b.env === env);
846
- if (!group.length) fail(`No boxes from ${env} on this laptop.`);
853
+ if (!group.length) fail(`No agents from ${env} on this laptop.`);
847
854
  const awake = new Set(boxes.filter((b) => b.state === "running").map((b) => b.name));
848
855
  const environment = (await api("/api/environments")).find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
849
856
  const clone = await ensureClone(environment);
@@ -854,10 +861,10 @@ async function code(envArg) {
854
861
  const workspace = join5(BOXES, env, `${env}.code-workspace`);
855
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));
856
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();
857
- 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)}`)}`);
858
865
  await Promise.all(group.filter((b) => awake.has(b.name) && !migrated.includes(b.name)).map((b) => pull(b.name, b.folder, true)));
859
866
  const stops = group.map((b) => watchRepo(b.name, b.folder));
860
- 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."));
861
868
  await new Promise((resolve) => process.once("SIGINT", resolve));
862
869
  await Promise.all(stops.map((s) => s()));
863
870
  ok("Stopped syncing");
@@ -976,14 +983,14 @@ async function newRemoteBox(env, branch, name, o) {
976
983
  const agent = o.agent;
977
984
  const idle = o.alwaysOn ? "0" : void 0;
978
985
  const { mcpServers, mcpOAuth, skipped } = localMcp();
979
- 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(", ")})` : "")}`);
980
987
  const claudeHome = localClaudeHome();
981
988
  const stop = progress([
982
989
  [0, "Finding a machine"],
983
990
  [900, `Unpacking the ${env} snapshot`],
984
991
  [2e3, "Signing Claude Code in"],
985
992
  [2900, `Copying ${mcpCount(mcpServers)}`],
986
- [3300, `Copying ${claudeHome.skills} skill${claudeHome.skills === 1 ? "" : "s"} and ${claudeHome.enabled.length} plugin${claudeHome.enabled.length === 1 ? "" : "s"}`],
993
+ [3300, `Copying ${claudeHome.skills.length} skill${claudeHome.skills.length === 1 ? "" : "s"} and ${claudeHome.enabled.length} plugin${claudeHome.enabled.length === 1 ? "" : "s"}`],
987
994
  [3700, "Connecting GitHub"],
988
995
  [4600, "Waking Claude up"],
989
996
  [9e3, "Bigger machines take a few extra seconds"]
@@ -1003,7 +1010,7 @@ async function sendHome(name, tar) {
1003
1010
  async function dial({ user, hosts }, privateKey, attempts) {
1004
1011
  const once = () => new Promise((resolve, reject) => {
1005
1012
  let left = hosts.length, won = false;
1006
- 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."));
1007
1014
  for (const [host, port] of hosts) {
1008
1015
  const c = new ssh22.Client();
1009
1016
  c.on("ready", () => {
@@ -1027,7 +1034,7 @@ async function dial({ user, hosts }, privateKey, attempts) {
1027
1034
  async function attach(name, ready) {
1028
1035
  if (!name) {
1029
1036
  const boxes = await api("/api/boxes");
1030
- 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}.`));
1031
1038
  name = await select3({ message: "Attach to", choices: boxes.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}`, value: b.name })) });
1032
1039
  }
1033
1040
  const boxName = name;
@@ -1057,7 +1064,7 @@ async function attach(name, ready) {
1057
1064
  process.stdout.write(TERMINAL_RESET);
1058
1065
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1059
1066
  console.log(dim(`
1060
- Detached. ${boxName} sleeps once Claude is done. box ${boxName} to return.`));
1067
+ Detached. ${boxName} sleeps once Claude is done. ${brand.command} ${boxName} to return.`));
1061
1068
  process.exit(0);
1062
1069
  }
1063
1070
  let ssh = ready?.ssh, attempts = 4, announced = false;
@@ -1065,7 +1072,7 @@ Detached. ${boxName} sleeps once Claude is done. box ${boxName} to return.`));
1065
1072
  if (!ssh) {
1066
1073
  attempts = 4;
1067
1074
  const box = await findBox(boxName);
1068
- 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}.`);
1069
1076
  if (box.state === "paused" || box.state === "pausing") {
1070
1077
  sleepScreen(box);
1071
1078
  if (!keys.length) await new Promise((r) => {
@@ -1074,12 +1081,12 @@ Detached. ${boxName} sleeps once Claude is done. box ${boxName} to return.`));
1074
1081
  wakingScreen(box, keys);
1075
1082
  pressed = () => wakingScreen(box, keys);
1076
1083
  }
1077
- 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"]]);
1078
1085
  ssh = (await api(`/api/boxes/${boxName}/attach`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(key.publicKey) }) }).finally(() => stop())).ssh;
1079
1086
  }
1080
1087
  conn = await dial(ssh, key.privateKey, attempts).catch(() => void 0);
1081
1088
  attempts = 1;
1082
- 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}.`));
1083
1090
  announced = true;
1084
1091
  if (!conn) {
1085
1092
  ssh = void 0;
@@ -1186,11 +1193,11 @@ async function updateFirst() {
1186
1193
  if (!prefix || process.env.CBOX_NO_UPDATE || process.env.CBOX_UPDATED) return;
1187
1194
  const latest = await fetch(LATEST, { signal: AbortSignal.timeout(2500) }).then((r) => r.json()).then((j) => j.version, () => void 0);
1188
1195
  if (!latest || !newer(latest, VERSION)) return;
1189
- const stop = progress([[0, `Updating box ${VERSION} \u2192 ${latest}`]]);
1196
+ const stop = progress([[0, `Updating ${brand.command} ${VERSION} \u2192 ${latest}`]]);
1190
1197
  const done = spawnSync(NPM, installArgs(latest, prefix), { stdio: "ignore" }).status === 0;
1191
1198
  stop();
1192
- if (!done) return miss(`Couldn't update box to ${latest}, so carrying on with ${VERSION}`);
1193
- 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}`);
1194
1201
  try {
1195
1202
  writeFileSync5(join6(CONFIG_DIR, "update.json"), JSON.stringify({ version: latest, checkedAt: Date.now() }));
1196
1203
  } catch {
@@ -1207,7 +1214,7 @@ function autoUpdate() {
1207
1214
  state = JSON.parse(readFileSync6(file2, "utf8"));
1208
1215
  } catch {
1209
1216
  }
1210
- 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}`);
1211
1218
  const check = !state.checkedAt || Date.now() - state.checkedAt > 12 * 36e5;
1212
1219
  try {
1213
1220
  mkdirSync5(CONFIG_DIR, { recursive: true });
@@ -1226,14 +1233,14 @@ function autoUpdate() {
1226
1233
  }
1227
1234
  async function upgrade() {
1228
1235
  const latest = await fetch(LATEST).then((r) => r.json()).then((j) => j.version).catch(() => fail("Could not reach the npm registry."));
1229
- if (!newer(latest, VERSION)) return ok(`box ${VERSION} is the latest`);
1230
- 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`));
1231
1238
  try {
1232
1239
  execFileSync4(NPM, installArgs(latest, npmPrefix()), { stdio: "inherit" });
1233
1240
  } catch {
1234
- 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`);
1235
1242
  }
1236
- ok(`Updated box to ${latest}`);
1243
+ ok(`Updated ${brand.command} to ${latest}`);
1237
1244
  }
1238
1245
 
1239
1246
  // src/index.ts
@@ -1242,7 +1249,7 @@ var NAME = /^[a-z0-9][a-z0-9-]{0,30}$/;
1242
1249
  async function newBox(o = {}) {
1243
1250
  await updateFirst();
1244
1251
  if (o.size && !["small", "medium", "large"].includes(o.size)) fail("Pick --size small, medium or large.");
1245
- 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.");
1246
1253
  const here = gitRepo();
1247
1254
  const picked = await chooseEnvironment(o.env, here);
1248
1255
  const branch = here && picked.repo.toLowerCase() === here.repo.toLowerCase() ? here.branch : void 0;
@@ -1254,7 +1261,7 @@ async function askName(branch) {
1254
1261
  const suggestion = fromBranch || `${ANIMALS[Math.floor(Math.random() * ANIMALS.length)]}-${Math.random().toString(16).slice(2, 5)}`;
1255
1262
  if (!process.stdin.isTTY) return void 0;
1256
1263
  const picked = await input2({
1257
- 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)")}`,
1258
1265
  default: suggestion,
1259
1266
  validate: (v) => !v.trim() || NAME.test(v.trim()) || "Lowercase letters, digits and dashes"
1260
1267
  });
@@ -1262,22 +1269,22 @@ async function askName(branch) {
1262
1269
  }
1263
1270
  async function home(name, o) {
1264
1271
  const making = o.env || o.remote || o.agent || o.size || o.alwaysOn;
1265
- 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.`);
1266
1273
  if (name) return openBox(name);
1267
1274
  const email = load().email;
1268
1275
  const all = await api("/api/boxes");
1269
1276
  await prune(all);
1270
1277
  const mine = all.filter((b) => b.state !== "gone" && (!email || b.owner === email));
1271
1278
  if (!mine.length || !process.stdin.isTTY || making) return newBox(o);
1272
- const pick = await select4({ message: "Open a box", choices: [
1279
+ const pick = await select4({ message: "Open an agent", choices: [
1273
1280
  ...mine.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}${dim(b.agent === "local" ? "" : " \xB7 remote")}`, value: b.name })),
1274
- { name: styleText5("cyan", "+ new box"), value: "" }
1281
+ { name: styleText5("cyan", "+ new agent"), value: "" }
1275
1282
  ] });
1276
1283
  return pick ? openBox(pick) : newBox(o);
1277
1284
  }
1278
1285
  async function openBox(name) {
1279
1286
  const box = await findBox(name);
1280
- 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.`);
1281
1288
  return box.agent === "local" ? openLocal({ name: box.name, env: box.env ?? "boxes", here: localCopy(), branch: box.branch ?? void 0 }) : attach(box.name);
1282
1289
  }
1283
1290
  async function ls() {
@@ -1285,9 +1292,9 @@ async function ls() {
1285
1292
  const all = await api("/api/boxes");
1286
1293
  await prune(all);
1287
1294
  const mine = all.filter((b) => !email || b.owner === email);
1288
- 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}.`));
1289
1296
  const width = Math.max(...mine.map((b) => b.name.length)) + 2;
1290
- console.log(dim(`${"BOX".padEnd(width)}${"STATE".padEnd(20)}WHERE`));
1297
+ console.log(dim(`${"AGENT".padEnd(width)}${"STATE".padEnd(20)}WHERE`));
1291
1298
  for (const b of mine) {
1292
1299
  const l = label(b);
1293
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")}`);
@@ -1296,30 +1303,30 @@ async function ls() {
1296
1303
  async function setup() {
1297
1304
  const sent = await sendSetup();
1298
1305
  const n = (k, word) => `${k} ${word}${k === 1 ? "" : "s"}`;
1299
- ok(`Sent ${[n(sent.skills, "skill"), n(sent.plugins, "plugin"), ...sent.memory ? ["CLAUDE.md"] : []].join(", ")} ${dim("\xB7 @Box and webhooks start boxes with these")}`);
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`)}`);
1300
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.`);
1301
1308
  const { data } = await api("/api/accounts");
1302
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");
1303
- 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)}`);
1304
1311
  else ok("Claude and GitHub are connected");
1305
1312
  }
1306
1313
  var quiet = ["proxy", "sync"].includes(process.argv[2] ?? "");
1307
1314
  if (quiet) console.log = console.error;
1308
- 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}`));
1309
1316
  if (!quiet) {
1310
1317
  autoUpdate();
1311
1318
  ensureSshConfig();
1312
1319
  }
1313
- 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");
1314
- 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));
1315
- boxOptions(cli.command("new").description("always make a fresh box")).action((o) => newBox(o));
1316
- cli.command("ls").description("list your boxes").action(ls);
1317
- 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));
1318
- 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);
1319
- cli.command("setup").description("send your skills, plugins and CLAUDE.md for @Box and webhooks, and check Claude and GitHub").action(setup);
1320
- cli.command("login").option("--server <url>", "box server").description("sign in from the browser").action((o) => login(o.server));
1321
- cli.command("upgrade").description("update box to the latest version now").action(upgrade);
1322
- 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, {}));
1323
1330
  cli.command("proxy <host>", { hidden: true }).action(proxy);
1324
- cli.command("sync <box> <direction> <folder>", { hidden: true }).action(sync);
1331
+ cli.command("sync <agent> <direction> <folder>", { hidden: true }).action(sync);
1325
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.12",
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.14",
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": {