@abuaboud/box 0.7.21 → 0.7.23

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 +49 -41
  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,22 +199,24 @@ 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;
212
212
 
213
213
  // src/api.ts
214
214
  var file = join2(CONFIG_DIR, "config.json");
215
+ var MOVED = { "https://box.abuaboud.me": "https://box.activepieces.dev" };
215
216
  var load = () => {
216
217
  if (!existsSync2(file)) return { server: process.env.CB_SERVER ?? brand.server };
217
- return JSON.parse(readFileSync2(file, "utf8"));
218
+ const config = JSON.parse(readFileSync2(file, "utf8"));
219
+ return { ...config, server: MOVED[config.server] ?? config.server };
218
220
  };
219
221
  var save = (c) => {
220
222
  mkdirSync2(CONFIG_DIR, { recursive: true });
@@ -230,7 +232,7 @@ async function api(path, init = {}, retried = false) {
230
232
  save({ ...c, token: void 0 });
231
233
  return api(path, init, true);
232
234
  }
233
- if (!res.ok) fail(body.error ?? `Request failed (${res.status})`);
235
+ if (!res.ok) fail(body.error?.message ?? body.error ?? `Request failed (${res.status})`);
234
236
  return body;
235
237
  }
236
238
  async function login(server) {
@@ -258,10 +260,8 @@ async function login(server) {
258
260
  ok(`Signed in as ${me.user.email} ${dim(`\xB7 ${me.org.name}`)}`);
259
261
  console.log();
260
262
  }
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
- }
263
+ var findBox = (name) => api(`/api/boxes/${encodeURIComponent(name)}`);
264
+ var awakeOf = (o) => o.awake === void 0 ? {} : { idleMinutes: 0, awakeDays: o.awake === true ? 7 : Number(o.awake) };
265
265
 
266
266
  // src/environments.ts
267
267
  import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
@@ -269,14 +269,15 @@ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:f
269
269
  import { join as join3 } from "node:path";
270
270
  import { styleText as styleText3 } from "node:util";
271
271
  import { input, select } from "@inquirer/prompts";
272
+ var MACHINES = { small: "2 vCPU, 4 GB", medium: "4 vCPU, 8 GB", large: "8 vCPU, 16 GB" };
272
273
  async function chooseEnvironment(envName, here) {
273
- const envs = await api("/api/environments");
274
+ const { data: envs } = await api("/api/environments");
274
275
  if (envName) return waitReady(envs.find((e) => e.name === envName) ?? fail(`No environment named ${envName}.`));
275
276
  const mine = here ? envs.filter((e) => e.repo.toLowerCase() === here.repo.toLowerCase()) : [];
276
277
  const setUp = async () => setUpEnvironment(here?.repo ?? await askRepo(), here?.dir, envs);
277
278
  if (!envs.length) return waitReady(await setUp());
278
279
  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}`}`)}`;
280
+ 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
281
  const create = { name: styleText3("cyan", here ? `+ Set up a new environment for ${here.repo}` : "+ Set up a new environment"), value: "" };
281
282
  const pick = await select({
282
283
  message: here ? `Environment for ${here.repo}${here.branch ? dim(` \xB7 branch ${here.branch}`) : ""}` : `Environment ${dim("(you're not in a repo)")}`,
@@ -360,12 +361,11 @@ async function setUpEnvironment(repo, dir, envs) {
360
361
  let name = repo.split("/")[1].toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").slice(0, 28) || "env";
361
362
  for (let i = 2, base = name; envs.some((e) => e.name === name); i++) name = `${base}-${i}`;
362
363
  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
364
  for (; ; ) {
365
365
  console.log();
366
366
  console.log(` ${dim("Name ")} ${name}`);
367
367
  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})` : ""}`)}`);
368
+ console.log(` ${dim("Machine ")} ${size[0].toUpperCase() + size.slice(1)} ${dim(`\xB7 ${MACHINES[size]}${size === sized.size ? ` (${sized.why})` : ""}`)}`);
369
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")}`);
370
370
  if (names.length) console.log(` ${dim("Secrets ")} ${names.join(", ")} ${dim(secrets ? "\xB7 values copied from .env" : "\xB7 names from .env.example, values empty")}`);
371
371
  console.log();
@@ -384,32 +384,33 @@ async function setUpEnvironment(repo, dir, envs) {
384
384
  }
385
385
  if (choice === "edit") {
386
386
  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 })) });
387
+ size = await select({ message: "Machine", default: size, choices: machineChoices });
388
388
  setup2 = (await input({ message: "Setup script (runs once in the snapshot; empty for none)", default: setup2 })).trim();
389
389
  continue;
390
390
  }
391
391
  break;
392
392
  }
393
393
  await ensureGitHub(repo);
394
- const { id } = await api("/api/environments", { method: "POST", body: JSON.stringify({ name, repo, size, setup: setup2, secrets }) });
394
+ const made = await api("/api/environments", { method: "POST", body: JSON.stringify({ name, repo, size, setup: setup2, secrets }) });
395
395
  ok(`Environment ${styleText3("cyan", name)} created`);
396
- return { id, name, repo, status: "building", size, progress: "Starting a build machine" };
396
+ return made;
397
397
  }
398
+ var SIZES = ["small", "medium", "large"];
399
+ var machineChoices = SIZES.map((value) => ({ name: `${value.padEnd(8)}${dim(MACHINES[value])}`, value }));
398
400
  async function ensureGitHub(repo) {
399
401
  const open = (url) => {
400
402
  if (process.stdout.isTTY) spawn2(process.platform === "darwin" ? "open" : "xdg-open", [url], { stdio: "ignore", detached: true }).on("error", () => {
401
403
  }).unref();
402
404
  };
403
- const status = () => api("/api/github/repos");
404
- let r = await status();
405
- if (!r.linked) {
405
+ const linked = async () => (await api("/api/accounts")).data.some((a) => a.id === "github" && a.status === "ready");
406
+ if (!await linked()) {
406
407
  const url = new URL("/connect/github", load().server).toString();
407
408
  console.log(`
408
409
  ${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} and your agents can push as you.`);
409
410
  console.log(dim(` Opening ${url}`));
410
411
  open(url);
411
412
  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()) {
413
+ for (const end = Date.now() + 10 * 6e4; !await linked(); ) {
413
414
  if (Date.now() > end) {
414
415
  bar();
415
416
  fail(`Gave up waiting after 10 minutes. Run ${brand.command} again when it's done.`);
@@ -420,7 +421,7 @@ ${styleText3("yellow", "!")} Connect GitHub so ${brand.name} can clone ${repo} a
420
421
  ok("GitHub connected");
421
422
  }
422
423
  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}.`);
424
+ if (!isPublic && !(await api("/api/github/repos")).data.some((p) => p.fullName.toLowerCase() === repo.toLowerCase())) fail(`Your GitHub login can't reach ${repo}.`);
424
425
  }
425
426
 
426
427
  // src/local.ts
@@ -489,7 +490,7 @@ async function proxy(host) {
489
490
  // src/local.ts
490
491
  async function newLocalBox(env, branch, name, o) {
491
492
  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());
493
+ 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
494
  ok(`${box.name} is ready ${dim(`\xB7 ssh ${box.name}.${brand.command}`)}`);
494
495
  await openLocal({ name: box.name, env, here: localCopy(), branch, fresh: true });
495
496
  }
@@ -761,7 +762,7 @@ async function prune(boxes) {
761
762
  const email = load().email;
762
763
  const mine = localBoxes().filter((b) => b.owner && b.owner === email);
763
764
  if (!mine.length) return;
764
- const alive = new Set((boxes ?? await api("/api/boxes")).filter((b) => b.state !== "gone").map((b) => b.name));
765
+ const alive = new Set((boxes ?? (await api("/api/boxes")).data).filter((b) => b.state !== "gone").map((b) => b.name));
765
766
  for (const b of mine) {
766
767
  if (alive.has(b.name)) continue;
767
768
  await dropFolder(b);
@@ -792,7 +793,7 @@ async function openLocal({ name, env, here, branch, fresh = false }) {
792
793
  const stop = progress([[0, `Opening ${name}`], [1500, "Waking the agent up"]]);
793
794
  const [got, envs] = await Promise.all([
794
795
  api(`/api/boxes/${name}/connect`, { method: "POST", body: JSON.stringify({ sshKey: sshKeys(laptopKey().publicKey) }) }),
795
- api("/api/environments")
796
+ api("/api/environments").then((r) => r.data)
796
797
  ]).finally(() => stop());
797
798
  const environment = envs.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
798
799
  void sendSetup().catch(() => void 0);
@@ -848,7 +849,7 @@ async function sync(name, direction, folder) {
848
849
  await (direction === "push" ? push(name, folder) : pull(name, folder));
849
850
  }
850
851
  async function code(envArg) {
851
- const boxes = await api("/api/boxes");
852
+ const { data: boxes } = await api("/api/boxes");
852
853
  await prune(boxes);
853
854
  const here = localBoxes().filter((b) => existsSync5(join5(b.folder, "repo")));
854
855
  const envs = [...new Set(here.map((b) => b.env))].sort();
@@ -856,18 +857,18 @@ async function code(envArg) {
856
857
  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
858
  const group = here.filter((b) => b.env === env);
858
859
  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}.`);
860
+ const awake2 = new Set(boxes.filter((b) => b.state === "running").map((b) => b.name));
861
+ const environment = (await api("/api/environments")).data.find((e) => e.name === env) ?? fail(`No environment named ${env}.`);
861
862
  const clone = await ensureClone(environment);
862
863
  const migrated = [];
863
- for (const b of group.filter((g) => awake.has(g.name))) {
864
+ for (const b of group.filter((g) => awake2.has(g.name))) {
864
865
  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
866
  }
866
867
  const workspace = join5(BOXES, env, `${env}.code-workspace`);
867
868
  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
869
  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
870
  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)));
871
+ await Promise.all(group.filter((b) => awake2.has(b.name) && !migrated.includes(b.name)).map((b) => pull(b.name, b.folder, true)));
871
872
  const stops = group.map((b) => watchRepo(b.name, b.folder));
872
873
  console.log(dim("Syncing your saves to the agents. Leave this open; ctrl+c stops syncing."));
873
874
  await new Promise((resolve) => process.once("SIGINT", resolve));
@@ -986,7 +987,6 @@ var mcpCount = (s) => {
986
987
  };
987
988
  async function newRemoteBox(env, branch, name, o) {
988
989
  const agent = o.agent;
989
- const idle = o.alwaysOn ? "0" : void 0;
990
990
  const { mcpServers, mcpOAuth, skipped } = localMcp();
991
991
  ok(`${mcpCount(mcpServers)} will be copied into the agent ${dim(skipped.length ? `(skipping local-only: ${skipped.join(", ")})` : "")}`);
992
992
  const claudeHome = localClaudeHome();
@@ -1001,9 +1001,9 @@ async function newRemoteBox(env, branch, name, o) {
1001
1001
  [9e3, "Bigger machines take a few extra seconds"]
1002
1002
  ]);
1003
1003
  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());
1004
+ 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
1005
  if (home2.pending) void sendHome(box.name, tar);
1006
- await attach(box.name, { name: box.name, ssh: box.ssh });
1006
+ await attach(box.name, box.ssh);
1007
1007
  }
1008
1008
  async function sendHome(name, tar) {
1009
1009
  const buf = await tar;
@@ -1038,7 +1038,7 @@ async function dial({ user, hosts }, privateKey, attempts) {
1038
1038
  }
1039
1039
  async function attach(name, ready) {
1040
1040
  if (!name) {
1041
- const boxes = await api("/api/boxes");
1041
+ const { data: boxes } = await api("/api/boxes");
1042
1042
  if (!boxes.length) return console.log(dim(`No agents yet. Run ${brand.command}.`));
1043
1043
  name = await select3({ message: "Attach to", choices: boxes.map((b) => ({ name: `${b.name.padEnd(28)}${label(b)}`, value: b.name })) });
1044
1044
  }
@@ -1072,7 +1072,7 @@ async function attach(name, ready) {
1072
1072
  Detached. ${boxName} sleeps once Claude is done. ${brand.command} ${boxName} to return.`));
1073
1073
  process.exit(0);
1074
1074
  }
1075
- let ssh = ready?.ssh, attempts = 4, announced = false;
1075
+ let ssh = ready, attempts = 4, announced = false;
1076
1076
  for (; ; ) {
1077
1077
  if (!ssh) {
1078
1078
  attempts = 4;
@@ -1151,12 +1151,12 @@ function centered(lines) {
1151
1151
  }
1152
1152
  function sleepScreen(box) {
1153
1153
  const since = box.asleepSince ? Date.now() - new Date(box.asleepSince).getTime() : 0;
1154
- const saved = since / 36e5 * (RATES[box.size ?? "small"] ?? RATES.small);
1154
+ const saved = since / 36e5 * RATES[box.size];
1155
1155
  centered([
1156
1156
  styleText4("blue", "\u263E"),
1157
1157
  "",
1158
1158
  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.`),
1159
+ dim(`Last used ${span(Date.now() - new Date(box.lastActiveAt).getTime())} ago. Your files and the conversation are saved.`),
1160
1160
  "",
1161
1161
  `Press any key to wake it ${dim("\xB7")} ctrl+] to leave`,
1162
1162
  "",
@@ -1258,7 +1258,7 @@ var ANIMALS = ["otter", "heron", "lynx", "moth", "yak", "orca", "wren", "newt",
1258
1258
  var NAME = /^[a-z0-9][a-z0-9-]{0,30}$/;
1259
1259
  async function newBox(o = {}) {
1260
1260
  await updateFirst();
1261
- if (o.size && !["small", "medium", "large"].includes(o.size)) fail("Pick --size small, medium or large.");
1261
+ if (o.size && !Object.hasOwn(MACHINES, o.size)) fail("Pick --size small, medium or large.");
1262
1262
  if (o.name !== void 0 && !NAME.test(o.name)) fail("Name the agent with lowercase letters, digits and dashes, like webhook-retry.");
1263
1263
  const here = gitRepo();
1264
1264
  const picked = await chooseEnvironment(o.env, here);
@@ -1278,11 +1278,11 @@ async function askName(branch) {
1278
1278
  return picked.trim() || suggestion;
1279
1279
  }
1280
1280
  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.`);
1281
+ const making = o.env || o.remote || o.agent || o.size || o.awake;
1282
+ 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
1283
  if (name) return openBox(name);
1284
1284
  const email = load().email;
1285
- const all = await api("/api/boxes");
1285
+ const { data: all } = await api("/api/boxes");
1286
1286
  await prune(all);
1287
1287
  const mine = all.filter((b) => b.state !== "gone" && (!email || b.owner === email));
1288
1288
  if (!mine.length || !process.stdin.isTTY || making) return newBox(o);
@@ -1299,7 +1299,7 @@ async function openBox(name) {
1299
1299
  }
1300
1300
  async function ls() {
1301
1301
  const email = load().email;
1302
- const all = await api("/api/boxes");
1302
+ const { data: all } = await api("/api/boxes");
1303
1303
  await prune(all);
1304
1304
  const mine = all.filter((b) => !email || b.owner === email);
1305
1305
  if (!mine.length) return console.log(dim(`No agents of yours. Run ${brand.command}.`));
@@ -1310,6 +1310,13 @@ async function ls() {
1310
1310
  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
1311
  }
1312
1312
  }
1313
+ async function awake(name, days = "7") {
1314
+ const n = Number(days);
1315
+ if (!Number.isInteger(n) || n < 0 || n > 30) fail("Days must be from 1 to 30, or 0 to let it sleep.");
1316
+ await api(`/api/boxes/${(await findBox(name)).id}`, { method: "PATCH", body: JSON.stringify({ awakeDays: n }) });
1317
+ if (n === 0) return ok(`${name} sleeps when idle again`);
1318
+ 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`)}`);
1319
+ }
1313
1320
  async function setup() {
1314
1321
  const sent = await sendSetup();
1315
1322
  const n = (k, word) => `${k} ${word}${k === 1 ? "" : "s"}`;
@@ -1337,11 +1344,12 @@ if (!quiet) {
1337
1344
  autoUpdate();
1338
1345
  ensureSshConfig();
1339
1346
  }
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");
1347
+ 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
1348
  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
1349
  boxOptions(cli.command("new").description("always make a fresh agent")).action((o) => newBox(o));
1343
1350
  cli.command("ls").description("list your agents").action(ls);
1344
1351
  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));
1352
+ cli.command("awake <agent> [days]").description("keep an agent awake for days (default 7), or renew it; 0 lets it sleep").action(awake);
1345
1353
  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
1354
  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
1355
  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.21",
3
+ "version": "0.7.23",
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
  }