@abuaboud/box 0.7.20 → 0.7.22

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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/cbox.mjs +46 -40
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -13,10 +13,11 @@ your laptop in a git worktree whose builds, tests and servers run on the agent.
13
13
  | Command | What it does |
14
14
  | --- | --- |
15
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 |
16
+ | `skyfleet new [-e env] [--size small\|medium\|large] [--awake [days]]` | Always make a fresh agent |
17
17
  | `skyfleet <name>` | Back to an agent |
18
18
  | `skyfleet ls` | List your agents and where Claude runs |
19
19
  | `skyfleet code [env]` | Open every agent of an environment in one VS Code window |
20
+ | `skyfleet awake <name> [days]` | Keep an agent awake for days (default 7), or renew it; `0` lets it sleep |
20
21
  | `skyfleet rm <name>` | Delete an agent; unpushed work goes to `cb/<name>` first |
21
22
  | `skyfleet --remote` | Run Claude on the agent instead, so it keeps going with the laptop shut |
22
23
  | `skyfleet upgrade` | Update now (skyfleet also updates itself; `CBOX_NO_UPDATE=1` turns that off) |
@@ -24,4 +25,4 @@ your laptop in a git worktree whose builds, tests and servers run on the agent.
24
25
  Every agent is also an SSH host, `<name>.skyfleet`, for ssh, scp, rsync and VS Code Remote-SSH (`<name>.box` still
25
26
  works). Local agents live in `~/.box`. `box` still runs the same command.
26
27
 
27
- Dashboard: https://skyfleet.run · Node 22.18 or newer.
28
+ Node 22.18 or newer.
package/dist/cbox.mjs CHANGED
@@ -199,13 +199,13 @@ function label(b) {
199
199
  if (b.state !== "running") return styleText("yellow", b.state);
200
200
  if (b.busy) return styleText("green", "\u25CF working");
201
201
  if (!b.idleMinutes) return styleText("green", "awake");
202
- const left = b.idleMinutes * 6e4 - (Date.now() - new Date(b.lastActiveAt ?? Date.now()).getTime());
202
+ const left = b.idleMinutes * 6e4 - (Date.now() - new Date(b.lastActiveAt).getTime());
203
203
  return styleText("yellow", `\u263E sleeps in ${span(Math.max(6e4, left))}`);
204
204
  }
205
205
 
206
206
  // src/brand.ts
207
207
  var BRANDS = {
208
- box: { name: "Box", command: "box", server: "https://box.abuaboud.me" },
208
+ box: { name: "Box", command: "box", server: "https://box.activepieces.dev" },
209
209
  skyfleet: { name: "Skyfleet", command: "skyfleet", server: "https://skyfleet.run" }
210
210
  };
211
211
  var brand = process.env.CB_BRAND === "skyfleet" ? BRANDS.skyfleet : BRANDS.box;
@@ -230,7 +230,7 @@ async function api(path, init = {}, retried = false) {
230
230
  save({ ...c, token: void 0 });
231
231
  return api(path, init, true);
232
232
  }
233
- if (!res.ok) fail(body.error ?? `Request failed (${res.status})`);
233
+ if (!res.ok) fail(body.error?.message ?? body.error ?? `Request failed (${res.status})`);
234
234
  return body;
235
235
  }
236
236
  async function login(server) {
@@ -258,10 +258,8 @@ async function login(server) {
258
258
  ok(`Signed in as ${me.user.email} ${dim(`\xB7 ${me.org.name}`)}`);
259
259
  console.log();
260
260
  }
261
- async function findBox(name) {
262
- const box = (await api("/api/boxes")).find((b) => b.name === name || b.id === name);
263
- return box ?? fail(`No agent named ${name}. ${brand.command} ls lists them.`);
264
- }
261
+ var findBox = (name) => api(`/api/boxes/${encodeURIComponent(name)}`);
262
+ var awakeOf = (o) => o.awake === void 0 ? {} : { idleMinutes: 0, awakeDays: o.awake === true ? 7 : Number(o.awake) };
265
263
 
266
264
  // src/environments.ts
267
265
  import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
@@ -269,14 +267,15 @@ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:f
269
267
  import { join as join3 } from "node:path";
270
268
  import { styleText as styleText3 } from "node:util";
271
269
  import { input, select } from "@inquirer/prompts";
270
+ var MACHINES = { small: "2 vCPU, 4 GB", medium: "4 vCPU, 8 GB", large: "8 vCPU, 16 GB" };
272
271
  async function chooseEnvironment(envName, here) {
273
- const envs = await api("/api/environments");
272
+ const { data: envs } = await api("/api/environments");
274
273
  if (envName) return waitReady(envs.find((e) => e.name === envName) ?? fail(`No environment named ${envName}.`));
275
274
  const mine = here ? envs.filter((e) => e.repo.toLowerCase() === here.repo.toLowerCase()) : [];
276
275
  const setUp = async () => setUpEnvironment(here?.repo ?? await askRepo(), here?.dir, envs);
277
276
  if (!envs.length) return waitReady(await setUp());
278
277
  if (!process.stdin.isTTY && mine.length === 1) return waitReady(mine[0]);
279
- const label2 = (e) => `${e.name.padEnd(16)}${dim(`${mine.includes(e) ? "this repo" : e.repo} \xB7 ${e.size ?? "small"}${e.status === "ready" ? "" : ` \xB7 ${e.status}`}`)}`;
278
+ const label2 = (e) => `${e.name.padEnd(16)}${dim(`${mine.includes(e) ? "this repo" : e.repo} \xB7 ${e.size}${e.status === "ready" ? "" : ` \xB7 ${e.status}`}`)}`;
280
279
  const create = { name: styleText3("cyan", here ? `+ Set up a new environment for ${here.repo}` : "+ Set up a new environment"), value: "" };
281
280
  const pick = await select({
282
281
  message: here ? `Environment for ${here.repo}${here.branch ? dim(` \xB7 branch ${here.branch}`) : ""}` : `Environment ${dim("(you're not in a repo)")}`,
@@ -360,12 +359,11 @@ async function setUpEnvironment(repo, dir, envs) {
360
359
  let name = repo.split("/")[1].toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").slice(0, 28) || "env";
361
360
  for (let i = 2, base = name; envs.some((e) => e.name === name); i++) name = `${base}-${i}`;
362
361
  let setup2 = detected?.setup ?? "", size = sized.size, secrets = "";
363
- const sizes = { small: "2 vCPU, 4 GB", medium: "4 vCPU, 8 GB", large: "8 vCPU, 16 GB" };
364
362
  for (; ; ) {
365
363
  console.log();
366
364
  console.log(` ${dim("Name ")} ${name}`);
367
365
  console.log(` ${dim("Repo ")} ${repo}`);
368
- console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${sizes[size]}${size === sized.size ? ` (${sized.why})` : ""}`)}`);
366
+ console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${MACHINES[size]}${size === sized.size ? ` (${sized.why})` : ""}`)}`);
369
367
  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")}`);
370
368
  if (names.length) console.log(` ${dim("Secrets ")} ${names.join(", ")} ${dim(secrets ? "\xB7 values copied from .env" : "\xB7 names from .env.example, values empty")}`);
371
369
  console.log();
@@ -384,32 +382,33 @@ async function setUpEnvironment(repo, dir, envs) {
384
382
  }
385
383
  if (choice === "edit") {
386
384
  name = await input({ message: "Name", default: name, validate: (v) => /^[a-z0-9][a-z0-9-]{0,30}$/.test(v) || "Lowercase letters, digits and dashes" });
387
- size = await select({ message: "Machine", default: size, choices: Object.entries(sizes).map(([value, spec]) => ({ name: `${value.padEnd(8)}${dim(spec)}`, value })) });
385
+ size = await select({ message: "Machine", default: size, choices: machineChoices });
388
386
  setup2 = (await input({ message: "Setup script (runs once in the snapshot; empty for none)", default: setup2 })).trim();
389
387
  continue;
390
388
  }
391
389
  break;
392
390
  }
393
391
  await ensureGitHub(repo);
394
- const { id } = await api("/api/environments", { method: "POST", body: JSON.stringify({ name, repo, size, setup: setup2, secrets }) });
392
+ const made = await api("/api/environments", { method: "POST", body: JSON.stringify({ name, repo, size, setup: setup2, secrets }) });
395
393
  ok(`Environment ${styleText3("cyan", name)} created`);
396
- return { id, name, repo, status: "building", size, progress: "Starting a build machine" };
394
+ return made;
397
395
  }
396
+ var SIZES = ["small", "medium", "large"];
397
+ var machineChoices = SIZES.map((value) => ({ name: `${value.padEnd(8)}${dim(MACHINES[value])}`, value }));
398
398
  async function ensureGitHub(repo) {
399
399
  const open = (url) => {
400
400
  if (process.stdout.isTTY) spawn2(process.platform === "darwin" ? "open" : "xdg-open", [url], { stdio: "ignore", detached: true }).on("error", () => {
401
401
  }).unref();
402
402
  };
403
- const status = () => api("/api/github/repos");
404
- let r = await status();
405
- if (!r.linked) {
403
+ const linked = async () => (await api("/api/accounts")).data.some((a) => a.id === "github" && a.status === "ready");
404
+ if (!await linked()) {
406
405
  const url = new URL("/connect/github", load().server).toString();
407
406
  console.log(`
408
407
  ${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} and your agents can push as you.`);
409
408
  console.log(dim(` Opening ${url}`));
410
409
  open(url);
411
410
  const bar = progress([[0, "Waiting for you to connect GitHub in the browser"]], { pace: 6e4 });
412
- for (const end = Date.now() + 10 * 6e4; !r.linked; r = await status()) {
411
+ for (const end = Date.now() + 10 * 6e4; !await linked(); ) {
413
412
  if (Date.now() > end) {
414
413
  bar();
415
414
  fail(`Gave up waiting after 10 minutes. Run ${brand.command} again when it's done.`);
@@ -420,7 +419,7 @@ ${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} a
420
419
  ok("GitHub connected");
421
420
  }
422
421
  const isPublic = await fetch(`https://api.github.com/repos/${repo}`, { signal: AbortSignal.timeout(5e3) }).then((res) => res.ok ? res.json() : null).then((j) => j && !j.private).catch(() => false);
423
- if (!isPublic && !r.repos.some((p) => p.fullName.toLowerCase() === repo.toLowerCase())) fail(`Your GitHub login can't reach ${repo}.`);
422
+ if (!isPublic && !(await api("/api/github/repos")).data.some((p) => p.fullName.toLowerCase() === repo.toLowerCase())) fail(`Your GitHub login can't reach ${repo}.`);
424
423
  }
425
424
 
426
425
  // src/local.ts
@@ -489,7 +488,7 @@ async function proxy(host) {
489
488
  // src/local.ts
490
489
  async function newLocalBox(env, branch, name, o) {
491
490
  const stop = progress([[0, "Finding a machine"], [900, `Unpacking the ${env} snapshot`], [2500, "Connecting GitHub"], [9e3, "Bigger machines take a few extra seconds"]]);
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());
491
+ const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, agent: "local", size: o.size, ...awakeOf(o), sshKey: sshKeys(laptopKey().publicKey) }) }).finally(() => stop());
493
492
  ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.${brand.command}`)}`);
494
493
  await openLocal({ name: box.name, env, here: localCopy(), branch, fresh: true });
495
494
  }
@@ -761,7 +760,7 @@ async function prune(boxes) {
761
760
  const email = load().email;
762
761
  const mine = localBoxes().filter((b) => b.owner && b.owner === email);
763
762
  if (!mine.length) return;
764
- const alive = new Set((boxes ?? await api("/api/boxes")).filter((b) => b.state !== "gone").map((b) => b.name));
763
+ const alive = new Set((boxes ?? (await api("/api/boxes")).data).filter((b) => b.state !== "gone").map((b) => b.name));
765
764
  for (const b of mine) {
766
765
  if (alive.has(b.name)) continue;
767
766
  await dropFolder(b);
@@ -792,7 +791,7 @@ async function openLocal({ name, env, here, branch, fresh = false }) {
792
791
  const stop = progress([[0, `Opening ${name}`], [1500, "Waking the agent up"]]);
793
792
  const [got, envs] = await Promise.all([
794
793
  api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) }),
795
- api("/api/environments")
794
+ api("/api/environments").then((r) => r.data)
796
795
  ]).finally(() => stop());
797
796
  const environment = envs.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
798
797
  void sendSetup().catch(() => void 0);
@@ -848,7 +847,7 @@ async function sync(name, direction, folder) {
848
847
  await (direction === "push" ? push(name, folder) : pull(name, folder));
849
848
  }
850
849
  async function code(envArg) {
851
- const boxes = await api("/api/boxes");
850
+ const { data: boxes } = await api("/api/boxes");
852
851
  await prune(boxes);
853
852
  const here = localBoxes().filter((b) => existsSync5(join5(b.folder, "repo")));
854
853
  const envs = [...new Set(here.map((b) => b.env))].sort();
@@ -856,18 +855,18 @@ async function code(envArg) {
856
855
  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 })) }));
857
856
  const group = here.filter((b) => b.env === env);
858
857
  if (!group.length) fail(`No agents from ${env} on this laptop.`);
859
- const awake = new Set(boxes.filter((b) => b.state === "running").map((b) => b.name));
860
- const environment = (await api("/api/environments")).find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
858
+ const awake2 = new Set(boxes.filter((b) => b.state === "running").map((b) => b.name));
859
+ const environment = (await api("/api/environments")).data.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
861
860
  const clone = await ensureClone(environment);
862
861
  const migrated = [];
863
- for (const b of group.filter((g) => awake.has(g.name))) {
862
+ for (const b of group.filter((g) => awake2.has(g.name))) {
864
863
  if (await setUpWorktree({ clone, folder: b.folder, name: b.name, branch: boxes.find((x) => x.name === b.name)?.branch ?? void 0, fresh: false })) migrated.push(b.name);
865
864
  }
866
865
  const workspace = join5(BOXES, env, `${env}.code-workspace`);
867
866
  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));
868
867
  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();
869
868
  ok(`${group.length} ${env} agent${group.length === 1 ? "" : "s"} in one window ${dim(`\xB7 ${tilde(workspace)}`)}`);
870
- await Promise.all(group.filter((b) => awake.has(b.name) && !migrated.includes(b.name)).map((b) => pull(b.name, b.folder, true)));
869
+ await Promise.all(group.filter((b) => awake2.has(b.name) && !migrated.includes(b.name)).map((b) => pull(b.name, b.folder, true)));
871
870
  const stops = group.map((b) => watchRepo(b.name, b.folder));
872
871
  console.log(dim("Syncing your saves to the agents. Leave this open; ctrl+c stops syncing."));
873
872
  await new Promise((resolve) => process.once("SIGINT", resolve));
@@ -986,7 +985,6 @@ var mcpCount = (s) => {
986
985
  };
987
986
  async function newRemoteBox(env, branch, name, o) {
988
987
  const agent = o.agent;
989
- const idle = o.alwaysOn ? "0" : void 0;
990
988
  const { mcpServers, mcpOAuth, skipped } = localMcp();
991
989
  ok(`${mcpCount(mcpServers)} will be copied into the agent ${dim(skipped.length ? `(skipping local-only: ${skipped.join(", ")})` : "")}`);
992
990
  const claudeHome = localClaudeHome();
@@ -1001,9 +999,9 @@ async function newRemoteBox(env, branch, name, o) {
1001
999
  [9e3, "Bigger machines take a few extra seconds"]
1002
1000
  ]);
1003
1001
  const { tar, ...home2 } = claudeHome;
1004
- const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, size: o.size, idleMinutes: idle === void 0 ? void 0 : Number(idle), mcpServers, mcpOAuth, claudeHome: home2, sshKey: sshKeys(laptopKey().publicKey), agent }) }).finally(() => stop());
1002
+ const box = await api("/api/boxes", { method: "POST", body: JSON.stringify({ env, branch, name, size: o.size, ...awakeOf(o), mcpServers, mcpOAuth, claudeHome: home2, sshKey: sshKeys(laptopKey().publicKey), agent }) }).finally(() => stop());
1005
1003
  if (home2.pending) void sendHome(box.name, tar);
1006
- await attach(box.name, { name: box.name, ssh: box.ssh });
1004
+ await attach(box.name, box.ssh);
1007
1005
  }
1008
1006
  async function sendHome(name, tar) {
1009
1007
  const buf = await tar;
@@ -1038,7 +1036,7 @@ async function dial({ user, hosts }, privateKey, attempts) {
1038
1036
  }
1039
1037
  async function attach(name, ready) {
1040
1038
  if (!name) {
1041
- const boxes = await api("/api/boxes");
1039
+ const { data: boxes } = await api("/api/boxes");
1042
1040
  if (!boxes.length) return console.log(dim(`No agents yet. Run ${brand.command}.`));
1043
1041
  name = await select3({ message: "Attach to", choices: boxes.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}`, value: b.name })) });
1044
1042
  }
@@ -1072,7 +1070,7 @@ async function attach(name, ready) {
1072
1070
  Detached. ${boxName} sleeps once Claude is done. ${brand.command} ${boxName} to return.`));
1073
1071
  process.exit(0);
1074
1072
  }
1075
- let ssh = ready?.ssh, attempts = 4, announced = false;
1073
+ let ssh = ready, attempts = 4, announced = false;
1076
1074
  for (; ; ) {
1077
1075
  if (!ssh) {
1078
1076
  attempts = 4;
@@ -1151,12 +1149,12 @@ function centered(lines) {
1151
1149
  }
1152
1150
  function sleepScreen(box) {
1153
1151
  const since = box.asleepSince ? Date.now() - new Date(box.asleepSince).getTime() : 0;
1154
- const saved = since / 36e5 * (RATES[box.size ?? "small"] ?? RATES.small);
1152
+ const saved = since / 36e5 * RATES[box.size];
1155
1153
  centered([
1156
1154
  styleText4("blue", "\u263E"),
1157
1155
  "",
1158
1156
  styleText4("bold", `${box.name} is asleep`),
1159
- dim(`Last used ${span(Date.now() - new Date(box.lastActiveAt ?? Date.now()).getTime())} ago. Your files and the conversation are saved.`),
1157
+ dim(`Last used ${span(Date.now() - new Date(box.lastActiveAt).getTime())} ago. Your files and the conversation are saved.`),
1160
1158
  "",
1161
1159
  `Press any key to wake it ${dim("\xB7")} ctrl+] to leave`,
1162
1160
  "",
@@ -1258,7 +1256,7 @@ var ANIMALS = ["otter", "heron", "lynx", "moth", "yak", "orca", "wren", "newt",
1258
1256
  var NAME = /^[a-z0-9][a-z0-9-]{0,30}$/;
1259
1257
  async function newBox(o = {}) {
1260
1258
  await updateFirst();
1261
- if (o.size && !["small", "medium", "large"].includes(o.size)) fail("Pick --size small, medium or large.");
1259
+ if (o.size && !Object.hasOwn(MACHINES, o.size)) fail("Pick --size small, medium or large.");
1262
1260
  if (o.name !== void 0 && !NAME.test(o.name)) fail("Name the agent with lowercase letters, digits and dashes, like webhook-retry.");
1263
1261
  const here = gitRepo();
1264
1262
  const picked = await chooseEnvironment(o.env, here);
@@ -1278,11 +1276,11 @@ async function askName(branch) {
1278
1276
  return picked.trim() || suggestion;
1279
1277
  }
1280
1278
  async function home(name, o) {
1281
- const making = o.env || o.remote || o.agent || o.size || o.alwaysOn;
1282
- 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.`);
1279
+ const making = o.env || o.remote || o.agent || o.size || o.awake;
1280
+ if (name && making) fail(`--env, --remote, --agent, --size and --awake 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.`);
1283
1281
  if (name) return openBox(name);
1284
1282
  const email = load().email;
1285
- const all = await api("/api/boxes");
1283
+ const { data: all } = await api("/api/boxes");
1286
1284
  await prune(all);
1287
1285
  const mine = all.filter((b) => b.state !== "gone" && (!email || b.owner === email));
1288
1286
  if (!mine.length || !process.stdin.isTTY || making) return newBox(o);
@@ -1299,7 +1297,7 @@ async function openBox(name) {
1299
1297
  }
1300
1298
  async function ls() {
1301
1299
  const email = load().email;
1302
- const all = await api("/api/boxes");
1300
+ const { data: all } = await api("/api/boxes");
1303
1301
  await prune(all);
1304
1302
  const mine = all.filter((b) => !email || b.owner === email);
1305
1303
  if (!mine.length) return console.log(dim(`No agents of yours. Run ${brand.command}.`));
@@ -1310,6 +1308,13 @@ async function ls() {
1310
1308
  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")}`);
1311
1309
  }
1312
1310
  }
1311
+ async function awake(name, days = "7") {
1312
+ const n = Number(days);
1313
+ if (!Number.isInteger(n) || n < 0 || n > 30) fail("Days must be from 1 to 30, or 0 to let it sleep.");
1314
+ await api(`/api/boxes/${(await findBox(name)).id}`, { method: "PATCH", body: JSON.stringify({ awakeDays: n }) });
1315
+ if (n === 0) return ok(`${name} sleeps when idle again`);
1316
+ ok(`${name} stays awake until ${new Date(Date.now() + n * 864e5).toLocaleDateString(void 0, { month: "short", day: "numeric" })} ${dim(`\xB7 run ${brand.command} awake ${name} again to renew`)}`);
1317
+ }
1313
1318
  async function setup() {
1314
1319
  const sent = await sendSetup();
1315
1320
  const n = (k, word) => `${k} ${word}${k === 1 ? "" : "s"}`;
@@ -1337,11 +1342,12 @@ if (!quiet) {
1337
1342
  autoUpdate();
1338
1343
  ensureSshConfig();
1339
1344
  }
1340
- 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");
1345
+ 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("--awake [days]", "stay awake this many days (default 7), for example for a demo; renew with awake <agent>");
1341
1346
  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));
1342
1347
  boxOptions(cli.command("new").description("always make a fresh agent")).action((o) => newBox(o));
1343
1348
  cli.command("ls").description("list your agents").action(ls);
1344
1349
  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));
1350
+ cli.command("awake <agent> [days]").description("keep an agent awake for days (default 7), or renew it; 0 lets it sleep").action(awake);
1345
1351
  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);
1346
1352
  cli.command("submit <issue...>").option("-e, --env <name>", "environment (default: this repo's, or asked)").description("start one agent per issue; the first line of each issue is its title").action(submit);
1347
1353
  cli.command("setup").description(`send your skills, plugins and CLAUDE.md for @${brand.name} and webhooks, and check Claude and GitHub`).action(setup);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abuaboud/box",
3
- "version": "0.7.20",
3
+ "version": "0.7.22",
4
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": {
@@ -25,10 +25,9 @@
25
25
  "cli",
26
26
  "mcp"
27
27
  ],
28
- "homepage": "https://skyfleet.run",
29
28
  "repository": {
30
29
  "type": "git",
31
- "url": "git+https://github.com/abuaboud/box.git",
30
+ "url": "git+https://github.com/abuaboud/skyfleet.git",
32
31
  "directory": "packages/cli"
33
32
  },
34
33
  "author": "Mohammad AbuAboud",
@@ -40,6 +39,7 @@
40
39
  "ssh2": "^1.17.0"
41
40
  },
42
41
  "devDependencies": {
42
+ "@box/common": "*",
43
43
  "esbuild": "^0.25.12"
44
44
  }
45
45
  }