agental 0.2.0 → 0.3.0

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 (2) hide show
  1. package/dist/cli.js +444 -102
  2. package/package.json +6 -1
package/dist/cli.js CHANGED
@@ -46,7 +46,7 @@ import { parseArgs } from "node:util";
46
46
 
47
47
  // ../src/cli-build.ts
48
48
  var PUBLISHED = true;
49
- var CLI_VERSION = true ? "0.2.0" : "dev";
49
+ var CLI_VERSION = true ? "0.3.0" : "dev";
50
50
  var DEFAULT_API_URL = true ? "https://agental.io" : "http://127.0.0.1:8741";
51
51
  var PACKAGE_NAME = "agental";
52
52
  var USER_AGENT = `agental-cli/${CLI_VERSION}`;
@@ -560,13 +560,13 @@ function expiryFrom(given, now = Date.now()) {
560
560
  }
561
561
  return new Date(moment).toISOString();
562
562
  }
563
- async function templateIdsOf(client, words) {
564
- if (words.every((word) => word.startsWith("tpl_"))) {
565
- return words;
563
+ async function templateIdsOf(client, words2) {
564
+ if (words2.every((word) => word.startsWith("tpl_"))) {
565
+ return words2;
566
566
  }
567
567
  const body = await ask2(client, "GET", "/v1/templates");
568
568
  const templates = Array.isArray(body?.templates) ? body.templates : [];
569
- return words.map((word) => {
569
+ return words2.map((word) => {
570
570
  if (word.startsWith("tpl_")) {
571
571
  return word;
572
572
  }
@@ -1238,8 +1238,8 @@ async function sshConnect(client, args) {
1238
1238
  // ../src/cli-onboard.ts
1239
1239
  var SKILLS_CLI = "skills@1.7.2";
1240
1240
  var READY_MS = 3 * 60 * 1e3;
1241
- var PLAN_MS = 15 * 60 * 1e3;
1242
- var PLAN_LOOK_MS = 3e3;
1241
+ var CARD_MS = 15 * 60 * 1e3;
1242
+ var CARD_LOOK_MS = 3e3;
1243
1243
  async function api(base, key2, method, pathname, body, headers = {}) {
1244
1244
  let response;
1245
1245
  try {
@@ -1436,34 +1436,34 @@ async function atTheLimit(base, key2, term, message) {
1436
1436
  process.exitCode = 1;
1437
1437
  return null;
1438
1438
  }
1439
- async function startPlan(base, key2, term, message) {
1439
+ async function saveCard(base, key2, term, message) {
1440
1440
  const policy = await api(base, key2, "GET", "/v1/policy");
1441
- const plans = String(policy.body["consoleUrl"] ?? "");
1441
+ const page = String(policy.body["consoleUrl"] ?? "");
1442
1442
  term.say(`
1443
1443
  ${bold("No new sandbox:")} ${message}`);
1444
1444
  if (!term.interactive) {
1445
- term.say(` Plans: ${plans}
1445
+ term.say(` Save a card: ${page}
1446
1446
  `);
1447
1447
  return false;
1448
1448
  }
1449
- if (!await yes(` Choose a plan in your browser now? [Y/n] `)) {
1450
- term.say(` Choose one when you like: ${plans}
1449
+ if (!await yes(` Save a card in your browser now? Nothing is charged, and the free plan gives $10 of credit. [Y/n] `)) {
1450
+ term.say(` Save one when you like: ${page}
1451
1451
  `);
1452
1452
  return false;
1453
1453
  }
1454
- openBrowser(plans);
1455
- term.say(` ${dim("If it did not open:")} ${plans}`);
1456
- term.say(` ${dim("waiting for your plan to start\u2026 (Ctrl-C to stop)")}`);
1457
- const until = Date.now() + PLAN_MS;
1454
+ openBrowser(page);
1455
+ term.say(` ${dim("If it did not open:")} ${page}`);
1456
+ term.say(` ${dim("waiting for your card to be saved\u2026 (Ctrl-C to stop)")}`);
1457
+ const until = Date.now() + CARD_MS;
1458
1458
  while (Date.now() < until) {
1459
- await sleep(PLAN_LOOK_MS);
1459
+ await sleep(CARD_LOOK_MS);
1460
1460
  const billing2 = await api(base, key2, "GET", "/v1/billing").catch(() => null);
1461
1461
  if (billing2?.status === 200 && billing2.body["needsPlan"] === false) {
1462
- term.say(` ${done("your plan has started")}`);
1462
+ term.say(` ${done("your card is saved")}`);
1463
1463
  return true;
1464
1464
  }
1465
1465
  }
1466
- term.say(` No plan started in ${PLAN_MS / 6e4} minutes. Run the command again once it has.
1466
+ term.say(` No card saved in ${CARD_MS / 6e4} minutes. Run the command again once it is.
1467
1467
  `);
1468
1468
  return false;
1469
1469
  }
@@ -1483,7 +1483,7 @@ async function newSandbox(base, key2, term) {
1483
1483
  return await atTheLimit(base, key2, term, errorOf(made).message);
1484
1484
  }
1485
1485
  if (made.status === 402 && errorOf(made).code === "billing_required") {
1486
- if (await startPlan(base, key2, term, errorOf(made).message)) {
1486
+ if (await saveCard(base, key2, term, errorOf(made).message)) {
1487
1487
  return await newSandbox(base, key2, term);
1488
1488
  }
1489
1489
  process.exitCode = 1;
@@ -1654,9 +1654,9 @@ async function ask4(client, method, pathname, body) {
1654
1654
  function pad(text, width) {
1655
1655
  return text.length >= width ? `${text.slice(0, width - 1)}\u2026 ` : text.padEnd(width + 1);
1656
1656
  }
1657
- function print2(json, body, words) {
1657
+ function print2(json, body, words2) {
1658
1658
  process.stdout.write(json ? `${JSON.stringify(body, null, 2)}
1659
- ` : words);
1659
+ ` : words2);
1660
1660
  }
1661
1661
  async function organizations(client) {
1662
1662
  const listed = (await ask4(client, "GET", "/v1/organizations"))["organizations"];
@@ -1734,8 +1734,8 @@ agental org use ${organization.id} signs this command in to it.
1734
1734
  async function actingTeam(client) {
1735
1735
  const { organizations: list3, acting } = await organizations(client);
1736
1736
  const team = list3.find((organization) => organization.id === acting);
1737
- if (!team || team.personal) {
1738
- throw new Error("This command's key acts in your own organization, which has no other members. agental org use <team> signs it in to a team.");
1737
+ if (!team) {
1738
+ throw new Error("This command's key acts in no organization you are in. agental org use <organization> signs it in to one.");
1739
1739
  }
1740
1740
  return team;
1741
1741
  }
@@ -1850,11 +1850,11 @@ function runOf(run2) {
1850
1850
  const code = run2.exitCode === null ? "" : ` (exit ${run2.exitCode})`;
1851
1851
  return `${run2.outcome}${code} ${at2(run2.dueAt)}`;
1852
1852
  }
1853
- function commandLine(words) {
1854
- if (words.length === 1) {
1855
- return words[0] ?? "";
1853
+ function commandLine(words2) {
1854
+ if (words2.length === 1) {
1855
+ return words2[0] ?? "";
1856
1856
  }
1857
- return words.map((word) => /^[A-Za-z0-9_./:=@%+,-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`).join(" ");
1857
+ return words2.map((word) => /^[A-Za-z0-9_./:=@%+,-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`).join(" ");
1858
1858
  }
1859
1859
  function timeoutMsOf(text) {
1860
1860
  const match = /^([1-9][0-9]*)(s|m|h|d)$/.exec(text.trim());
@@ -1884,6 +1884,9 @@ function fieldsOf(options) {
1884
1884
  if (options["keep-memory"] && options["disk-only"]) {
1885
1885
  throw new Error("Give --keep-memory or --disk-only, not both.");
1886
1886
  }
1887
+ if (options["delete-after"] && options["keep-sandbox"]) {
1888
+ throw new Error("Give --delete-after or --keep-sandbox, not both.");
1889
+ }
1887
1890
  if (options.overlap !== void 0 && !OVERLAPS.includes(options.overlap)) {
1888
1891
  throw new Error(`--overlap is skip, replace or parallel, not ${options.overlap}.`);
1889
1892
  }
@@ -1900,7 +1903,8 @@ function fieldsOf(options) {
1900
1903
  ...options.timeout === void 0 ? {} : { timeoutMs: timeoutMsOf(options.timeout) },
1901
1904
  ...options.overlap === void 0 ? {} : { overlap: options.overlap },
1902
1905
  ...ifStopped === void 0 ? {} : { ifStopped },
1903
- ...options["keep-memory"] ? { keepMemory: true } : options["disk-only"] ? { keepMemory: false } : {}
1906
+ ...options["keep-memory"] ? { keepMemory: true } : options["disk-only"] ? { keepMemory: false } : {},
1907
+ ...options["delete-after"] ? { deleteAfter: true } : options["keep-sandbox"] ? { deleteAfter: false } : {}
1904
1908
  };
1905
1909
  }
1906
1910
  function print3(schedule2, json) {
@@ -1915,7 +1919,7 @@ function print3(schedule2, json) {
1915
1919
  ["command", schedule2.command],
1916
1920
  ["when", whenOf(schedule2)],
1917
1921
  ["if stopped", ifStoppedOf(schedule2)],
1918
- ["overlap", schedule2.overlap],
1922
+ ...schedule2.at ? [["after", schedule2.deleteAfter ? "deletes the sandbox, if the command exits 0" : "keeps the sandbox"]] : [["overlap", schedule2.overlap]],
1919
1923
  ["timeout", `${Math.round(schedule2.timeoutMs / 1e3)}s`],
1920
1924
  ["next", schedule2.enabled ? schedule2.nextRunAt === null ? afterOf(schedule2) : at2(schedule2.nextRunAt) : schedule2.pausedReason ? `paused: ${schedule2.pausedReason}` : "paused"],
1921
1925
  ["last run", runOf(schedule2.lastRun)]
@@ -2018,7 +2022,7 @@ async function schedule(client, positionals, options) {
2018
2022
  at2(run2.dueAt),
2019
2023
  run2.outcome,
2020
2024
  run2.exitCode === null ? "-" : String(run2.exitCode),
2021
- run2.startedSandbox ? run2.stoppedSandbox === null ? "started, left running" : `started, stopped (${run2.stoppedSandbox})` : "-",
2025
+ run2.deletedSandbox ? run2.startedSandbox ? "started, deleted" : "deleted" : run2.startedSandbox ? run2.stoppedSandbox === null ? "started, left running" : `started, stopped (${run2.stoppedSandbox})` : "-",
2022
2026
  run2.commandId ?? (run2.reason ?? "-")
2023
2027
  ])
2024
2028
  ])
@@ -2043,7 +2047,7 @@ async function schedule(client, positionals, options) {
2043
2047
  } else {
2044
2048
  const changes = { ...fieldsOf(options), ...command.length > 0 ? { command: commandLine(command) } : {} };
2045
2049
  if (Object.keys(changes).length === 0) {
2046
- throw new Error("schedule update needs something to change: --every, --cron, --at, --tz, --name, --timeout, --overlap, --if-stopped, --keep-memory or --disk-only, or a new command after --.");
2050
+ throw new Error("schedule update needs something to change: --every, --cron, --at, --tz, --name, --timeout, --overlap, --if-stopped, --keep-memory or --disk-only, --delete-after or --keep-sandbox, or a new command after --.");
2047
2051
  }
2048
2052
  print3(await ask5(client, "PATCH", path4, changes), options.json);
2049
2053
  }
@@ -2066,7 +2070,8 @@ var LIFECYCLE_EVENTS = [
2066
2070
  "sandbox.auto_stop_set",
2067
2071
  "sandbox.labels_changed",
2068
2072
  "sandbox.scheduled_run",
2069
- "sandbox.schedule_paused"
2073
+ "sandbox.schedule_paused",
2074
+ "sandbox.low_cpu"
2070
2075
  ];
2071
2076
  var WEBHOOK_EVENTS = [...LIFECYCLE_EVENTS, "prompt.created"];
2072
2077
  var EMAIL_EVENTS = ["email.received", "email.delivered", "email.bounced"];
@@ -2367,12 +2372,19 @@ var MICROS_PER_DOLLAR = 1e6;
2367
2372
  function plan(id, dollarsPerMonth, sizes, runningUnits, freeBurstUnits) {
2368
2373
  return { id, dollarsPerMonth, includedMicros: dollarsPerMonth * MICROS_PER_DOLLAR, sizes, runningUnits, freeBurstUnits };
2369
2374
  }
2370
- var PLANS = [
2371
- plan("plan20", 20, ["tiny", "small"], 20, 20),
2375
+ var FREE_PLAN = plan("free", 0, ["tiny", "small"], 20, 20);
2376
+ var PAID_PLANS = [
2372
2377
  plan("plan100", 100, ["tiny", "small", "medium"], 100, 100),
2373
2378
  plan("plan500", 500, MACHINE_SIZE_NAMES, 600, 200),
2374
2379
  plan("plan2000", 2e3, MACHINE_SIZE_NAMES, 2e3, 400)
2375
2380
  ];
2381
+ var PLANS = [FREE_PLAN, ...PAID_PLANS];
2382
+ var FREE = {
2383
+ /** The credit given once, in millionths of a US dollar. It does not expire. */
2384
+ creditMicros: 10 * MICROS_PER_DOLLAR,
2385
+ /** Small sandboxes an account on the free plan runs and starts at once before it has paid for anything. */
2386
+ unpaidUnits: 2
2387
+ };
2376
2388
  var TOP_UP_DOLLARS = { min: 15, max: 1e5 };
2377
2389
  var TOP_UP_BONUS = { fromDollars: 200, fromPercent: 2, toDollars: 1e4, toPercent: 25 };
2378
2390
  function topUpBonusPercent(dollars) {
@@ -2394,6 +2406,95 @@ function topUpByDollars(dollars) {
2394
2406
  }
2395
2407
  var TOP_UPS = [20, 50, 100, 250, 500, 1e3, 5e3, 1e4].map((dollars) => topUpByDollars(dollars));
2396
2408
 
2409
+ // ../src/sandbox-where.ts
2410
+ var TERMS = /* @__PURE__ */ new Set(["status", "size", "region", "template", "tag", "archived", "memory", "outbound", "stops", "made-by"]);
2411
+ function words(text) {
2412
+ const found = [];
2413
+ for (const match of text.matchAll(/(?:[^\s"]+|"[^"]*")+/g)) {
2414
+ found.push(match[0].replace(/"/g, ""));
2415
+ }
2416
+ return found;
2417
+ }
2418
+ function whereQuery(text, query = new URLSearchParams()) {
2419
+ const plain2 = [];
2420
+ for (const word of words(text)) {
2421
+ const time = /^(made|stopped)([<>])(.+)$/.exec(word);
2422
+ if (time) {
2423
+ const [, what, side, at4] = time;
2424
+ if (what === "made") {
2425
+ query.append(side === ">" ? "createdAfter" : "createdBefore", at4);
2426
+ } else if (side === "<") {
2427
+ query.append("stoppedBefore", at4);
2428
+ } else {
2429
+ throw new Error(`${word}: stopped takes < only, for sandboxes stopped before a time.`);
2430
+ }
2431
+ continue;
2432
+ }
2433
+ const pair = /^(-?)([a-z0-9][a-z0-9_.-]*):(.*)$/.exec(word);
2434
+ if (!pair) {
2435
+ plain2.push(word);
2436
+ continue;
2437
+ }
2438
+ const [, not, key2, value] = pair;
2439
+ if (value === "") {
2440
+ throw new Error(`${word}: say a value after the colon.`);
2441
+ }
2442
+ if (!TERMS.has(key2)) {
2443
+ query.append(`${not ? "notMetadata" : "metadata"}[${key2}]`, value);
2444
+ continue;
2445
+ }
2446
+ if (not && key2 !== "tag") {
2447
+ throw new Error(`${word}: only tags and metadata keys can be left out with -.`);
2448
+ }
2449
+ switch (key2) {
2450
+ case "status":
2451
+ for (const each of value.split(",")) {
2452
+ if (each === "starting") {
2453
+ query.append("status", "creating");
2454
+ query.append("status", "restoring");
2455
+ } else if (each === "failed") {
2456
+ query.append("status", "error");
2457
+ } else if (each === "archived") {
2458
+ query.append("status", "stopped");
2459
+ query.set("archived", "true");
2460
+ } else {
2461
+ query.append("status", each);
2462
+ }
2463
+ }
2464
+ break;
2465
+ case "template":
2466
+ query.append("templateId", value);
2467
+ break;
2468
+ case "tag":
2469
+ for (const each of value.split(",")) {
2470
+ query.append(not ? "notTag" : "tag", each);
2471
+ }
2472
+ break;
2473
+ case "memory":
2474
+ if (value !== "kept" && value !== "lost") {
2475
+ throw new Error(`${word}: memory is kept or lost.`);
2476
+ }
2477
+ query.set("memoryKept", value === "kept" ? "true" : "false");
2478
+ break;
2479
+ case "made-by":
2480
+ query.append("createdBy", value);
2481
+ break;
2482
+ case "archived":
2483
+ query.set("archived", value);
2484
+ break;
2485
+ case "stops":
2486
+ query.set("stops", value);
2487
+ break;
2488
+ default:
2489
+ query.append(key2, value);
2490
+ }
2491
+ }
2492
+ if (plain2.length > 0) {
2493
+ query.set("q", plain2.join(" "));
2494
+ }
2495
+ return query;
2496
+ }
2497
+
2397
2498
  // ../src/cli.ts
2398
2499
  var DEFAULT_URL = DEFAULT_API_URL;
2399
2500
  var SSH_COMMANDS = {
@@ -2506,7 +2607,7 @@ async function main() {
2506
2607
  expires: { type: "string" },
2507
2608
  "keep-streams": { type: "boolean" },
2508
2609
  "keep-memory": { type: "boolean" },
2509
- // agental schedule: when it runs, in what time zone, how long a run may take, and what it does when it is stopped or the last still runs.
2610
+ // agental schedule: when it runs, in what time zone, how long a run may take, what it does when it is stopped or the last still runs, and whether a one-off deletes its sandbox after.
2510
2611
  every: { type: "string" },
2511
2612
  cron: { type: "string" },
2512
2613
  tz: { type: "string" },
@@ -2514,6 +2615,8 @@ async function main() {
2514
2615
  overlap: { type: "string" },
2515
2616
  "if-stopped": { type: "string" },
2516
2617
  "disk-only": { type: "boolean" },
2618
+ "delete-after": { type: "boolean" },
2619
+ "keep-sandbox": { type: "boolean" },
2517
2620
  ttl: { type: "string" },
2518
2621
  "max-burst-fee": { type: "string" },
2519
2622
  in: { type: "string" },
@@ -2545,6 +2648,11 @@ async function main() {
2545
2648
  // Metadata as <key>=<value>, given again for each key.
2546
2649
  meta: { type: "string", multiple: true },
2547
2650
  anywhere: { type: "boolean", default: false },
2651
+ where: { type: "string" },
2652
+ facets: { type: "string" },
2653
+ yes: { type: "boolean", default: false },
2654
+ confirm: { type: "string" },
2655
+ sort: { type: "string" },
2548
2656
  role: { type: "string" },
2549
2657
  "revoke-keys": { type: "boolean", default: false },
2550
2658
  help: { type: "boolean", short: "h", default: false }
@@ -2560,7 +2668,13 @@ async function main() {
2560
2668
  await create({ ...values, template: once(values.template, "--template") });
2561
2669
  return;
2562
2670
  case "list":
2563
- await list2(values.json, labelsFilter(values));
2671
+ await list2(values.json, whereQuery(values.where ?? "", labelsFilter(values)), values.sort, values.limit);
2672
+ return;
2673
+ case "jobs":
2674
+ await jobs(positionals.slice(1), values.json);
2675
+ return;
2676
+ case "summary":
2677
+ await summary(values.json, whereQuery(values.where ?? "", labelsFilter(values)), values.facets);
2564
2678
  return;
2565
2679
  case "tags":
2566
2680
  await labels("tags", "sandbox", positionals.slice(1), values.json);
@@ -2575,15 +2689,30 @@ async function main() {
2575
2689
  await exec(positionals.slice(1));
2576
2690
  return;
2577
2691
  case "delete":
2692
+ if (values.where !== void 0 && positionals[1] !== void 0) {
2693
+ throw new Error("delete takes a sandbox's ID or --where, not both");
2694
+ }
2695
+ if (values.where !== void 0) {
2696
+ await bulk("delete", whereQuery(values.where, labelsFilter(values)), {}, values.yes === true, values.json, values.confirm);
2697
+ return;
2698
+ }
2578
2699
  await remove2(positionals[1]);
2579
2700
  return;
2580
2701
  case "start":
2702
+ if (values.where !== void 0) {
2703
+ await bulk("start", whereQuery(values.where, labelsFilter(values)), {}, values.yes === true, values.json);
2704
+ return;
2705
+ }
2581
2706
  await lifecycle("start", positionals[1], maxBurstFeeHeaders(values["max-burst-fee"]));
2582
2707
  return;
2583
2708
  case "auto-stop":
2584
2709
  await autoStop(positionals[1], values.in, values.at, values.never === true, values.json);
2585
2710
  return;
2586
2711
  case "stop":
2712
+ if (values.where !== void 0) {
2713
+ await bulk("stop", whereQuery(values.where, labelsFilter(values)), values["keep-memory"] === true ? { keepMemory: true } : {}, values.yes === true, values.json);
2714
+ return;
2715
+ }
2587
2716
  await lifecycle("stop", positionals[1], {}, values["keep-memory"] === true ? { keepMemory: true } : void 0);
2588
2717
  return;
2589
2718
  case "recover":
@@ -2888,20 +3017,39 @@ async function regions(asJson) {
2888
3017
  }
2889
3018
  process.stdout.write("* where a sandbox is made when no region is named\n");
2890
3019
  }
2891
- async function list2(asJson, filter) {
2892
- const response = await request(`/v1/sandboxes${filter.size > 0 ? `?${filter}` : ""}`, { method: "GET" });
2893
- const body = await readBody(response);
2894
- if (!response.ok) {
2895
- fail(response.status, body);
2896
- }
2897
- const sandboxes = readSandboxes(body);
3020
+ async function list2(asJson, filter, sort, limit) {
3021
+ const sandboxes = [];
3022
+ let cursor = null;
3023
+ do {
3024
+ const query = new URLSearchParams(filter);
3025
+ if (sort !== void 0) {
3026
+ query.set("sort", sort);
3027
+ }
3028
+ query.set("limit", limit ?? "200");
3029
+ if (cursor !== null) {
3030
+ query.set("cursor", cursor);
3031
+ }
3032
+ const response = await request(`/v1/sandboxes?${query}`, { method: "GET" });
3033
+ const body = await readBody(response);
3034
+ if (!response.ok) {
3035
+ fail(response.status, body);
3036
+ }
3037
+ sandboxes.push(...readSandboxes(body));
3038
+ const next = body && typeof body === "object" ? body["nextCursor"] : null;
3039
+ cursor = typeof next === "string" ? next : null;
3040
+ if (limit !== void 0 && cursor !== null) {
3041
+ process.stderr.write(`There are more. --cursor is not taken here: drop --limit to list them all, or ask GET /v1/sandboxes?${query.toString().replace(/&?cursor=[^&]*/, "")}&cursor=${cursor}
3042
+ `);
3043
+ cursor = null;
3044
+ }
3045
+ } while (cursor !== null);
2898
3046
  if (asJson) {
2899
3047
  process.stdout.write(`${JSON.stringify(sandboxes, null, 2)}
2900
3048
  `);
2901
3049
  return;
2902
3050
  }
2903
3051
  if (sandboxes.length === 0) {
2904
- process.stdout.write(filter.size > 0 ? "No sandboxes with those tags and metadata.\n" : "No sandboxes.\n");
3052
+ process.stdout.write(filter.size > 0 ? "No sandboxes match.\n" : "No sandboxes.\n");
2905
3053
  return;
2906
3054
  }
2907
3055
  const names = sandboxes.some((sandbox) => typeof sandbox["templateId"] === "string") ? await templateNames() : /* @__PURE__ */ new Map();
@@ -2927,6 +3075,165 @@ async function list2(asJson, filter) {
2927
3075
  `);
2928
3076
  }
2929
3077
  }
3078
+ function dollarsOf(micros) {
3079
+ return `$${(micros / 1e6).toFixed(4)}`;
3080
+ }
3081
+ async function summary(asJson, filter, facets) {
3082
+ const query = new URLSearchParams(filter);
3083
+ if (facets !== void 0) {
3084
+ query.set("facets", facets);
3085
+ }
3086
+ const response = await request(`/v1/sandbox-summary${query.size > 0 ? `?${query}` : ""}`, { method: "GET" });
3087
+ const body = await readBody(response);
3088
+ if (!response.ok) {
3089
+ fail(response.status, body);
3090
+ }
3091
+ if (asJson) {
3092
+ process.stdout.write(`${JSON.stringify(body, null, 2)}
3093
+ `);
3094
+ return;
3095
+ }
3096
+ const read2 = body;
3097
+ const line = (name, money2) => `${pad2(name)} ${String(money2.count).padStart(6)} ${dollarsOf(money2.microsPerHour).padStart(11)} an hour ${dollarsOf(money2.microsPerHour * 24).padStart(11)} a day ${dollarsOf(money2.spentMicros).padStart(11)} today
3098
+ `;
3099
+ for (const [state2, money2] of Object.entries(read2.byState)) {
3100
+ if (money2.count > 0) {
3101
+ process.stdout.write(line(state2, money2));
3102
+ }
3103
+ }
3104
+ process.stdout.write(line("total", read2.total));
3105
+ const running = read2.byState["running"];
3106
+ if (running && running.count > 0) {
3107
+ process.stdout.write(`Stopped, the ${running.count} running would cost ${dollarsOf(read2.ifStopped.microsPerHour)} an hour, ${dollarsOf(running.microsPerHour - read2.ifStopped.microsPerHour)} less.
3108
+ `);
3109
+ }
3110
+ process.stdout.write("A day is at the rate of now. Today is the UTC day, and spent is for these sandboxes alone: the account's month is on Billing.\n");
3111
+ for (const [field, facet] of Object.entries(read2.facets ?? {})) {
3112
+ process.stdout.write(`
3113
+ ${field}
3114
+ `);
3115
+ for (const each of facet.values) {
3116
+ process.stdout.write(` ${pad2(each.value)} ${String(each.count).padStart(6)} ${dollarsOf(each.microsPerHour).padStart(11)} an hour
3117
+ `);
3118
+ }
3119
+ if (facet.more > 0) {
3120
+ process.stdout.write(` and ${facet.more} more
3121
+ `);
3122
+ }
3123
+ }
3124
+ }
3125
+ function filterOf(query) {
3126
+ const filter = {};
3127
+ for (const [name, value] of query) {
3128
+ filter[name] = [...filter[name] ?? [], value];
3129
+ }
3130
+ return filter;
3131
+ }
3132
+ async function typedCount(count) {
3133
+ const { createInterface: createInterface2 } = await import("node:readline/promises");
3134
+ const lines = createInterface2({ input: process.stdin, output: process.stderr });
3135
+ try {
3136
+ return (await lines.question(`They cannot be brought back. Type ${count} to delete them: `)).trim();
3137
+ } finally {
3138
+ lines.close();
3139
+ }
3140
+ }
3141
+ async function confirmed(question) {
3142
+ if (!process.stdin.isTTY) {
3143
+ return false;
3144
+ }
3145
+ const { createInterface: createInterface2 } = await import("node:readline/promises");
3146
+ const lines = createInterface2({ input: process.stdin, output: process.stderr });
3147
+ try {
3148
+ return /^y(es)?$/i.test((await lines.question(`${question} [y/N] `)).trim());
3149
+ } finally {
3150
+ lines.close();
3151
+ }
3152
+ }
3153
+ async function bulk(action, filter, options, yes2, asJson, confirm) {
3154
+ const previewed = await request("/v1/sandbox-jobs/preview", { method: "POST", body: JSON.stringify({ action, options, select: { filter: filterOf(filter) } }) });
3155
+ const preview = await readBody(previewed);
3156
+ if (!previewed.ok) {
3157
+ fail(previewed.status, preview);
3158
+ }
3159
+ const read2 = preview;
3160
+ process.stderr.write(`${action === "stop" ? "Stop" : action === "start" ? "Start" : "Delete"} ${read2.count} ${read2.count === 1 ? "sandbox" : "sandboxes"}: ${read2.sample.map((each) => each.name).join(", ")}${read2.count > read2.sample.length ? `, and ${read2.count - read2.sample.length} more` : ""}.
3161
+ `);
3162
+ process.stderr.write(`They cost ${dollarsOf(read2.microsPerHour.now)} an hour now, and ${dollarsOf(read2.microsPerHour.after)} an hour after.
3163
+ `);
3164
+ let count = null;
3165
+ if (action === "delete") {
3166
+ const given = confirm ?? (process.stdin.isTTY ? await typedCount(read2.count) : "");
3167
+ if (given !== String(read2.count)) {
3168
+ process.stderr.write(`Nothing was deleted. Type ${read2.count}, or send --confirm ${read2.count}, to delete them.
3169
+ `);
3170
+ process.exitCode = 1;
3171
+ return;
3172
+ }
3173
+ count = read2.count;
3174
+ } else if (!yes2 && !await confirmed("Go on?")) {
3175
+ process.stderr.write("Nothing was done. --yes goes on without asking.\n");
3176
+ return;
3177
+ }
3178
+ const made = await request("/v1/sandbox-jobs", { method: "POST", headers: { "Idempotency-Key": `cli-${read2.id}` }, body: JSON.stringify({ previewId: read2.id, ...count === null ? {} : { confirm: count } }) });
3179
+ let job = await readBody(made);
3180
+ if (!made.ok) {
3181
+ fail(made.status, job);
3182
+ }
3183
+ while (job.state === "running") {
3184
+ process.stderr.write(`\r${job.id}: ${job.counts["done"] ?? 0} done, ${job.counts["skipped"] ?? 0} skipped, ${job.counts["failed"] ?? 0} failed of ${job.total}`);
3185
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
3186
+ const read3 = await request(`/v1/sandbox-jobs/${job.id}`, { method: "GET" });
3187
+ job = await readBody(read3);
3188
+ if (!read3.ok) {
3189
+ fail(read3.status, job);
3190
+ }
3191
+ }
3192
+ process.stderr.write("\n");
3193
+ if (asJson) {
3194
+ process.stdout.write(`${JSON.stringify(job, null, 2)}
3195
+ `);
3196
+ return;
3197
+ }
3198
+ process.stdout.write(`${job.id} ${job.state}: ${job.counts["done"] ?? 0} done, ${job.counts["skipped"] ?? 0} skipped, ${job.counts["failed"] ?? 0} failed, ${job.counts["uncertain"] ?? 0} uncertain, of ${job.total}.
3199
+ `);
3200
+ for (const problem of job.problems) {
3201
+ process.stdout.write(` ${problem.sandboxId} ${problem.state}${problem.reason ? `: ${problem.reason}` : ""}
3202
+ `);
3203
+ }
3204
+ }
3205
+ async function jobs(positionals, asJson) {
3206
+ const [verb, id] = positionals;
3207
+ const path4 = verb === void 0 || verb === "list" ? "/v1/sandbox-jobs" : verb === "show" && id ? `/v1/sandbox-jobs/${encodeURIComponent(id)}` : verb === "cancel" && id ? `/v1/sandbox-jobs/${encodeURIComponent(id)}/cancel` : null;
3208
+ if (path4 === null) {
3209
+ throw new Error("jobs takes list, show <job-id> or cancel <job-id>");
3210
+ }
3211
+ const response = await request(path4, { method: verb === "cancel" ? "POST" : "GET" });
3212
+ const body = await readBody(response);
3213
+ if (!response.ok) {
3214
+ fail(response.status, body);
3215
+ }
3216
+ if (asJson) {
3217
+ process.stdout.write(`${JSON.stringify(body, null, 2)}
3218
+ `);
3219
+ return;
3220
+ }
3221
+ const listed = verb === void 0 || verb === "list" ? body.jobs ?? [] : [body];
3222
+ if (listed.length === 0) {
3223
+ process.stdout.write("No jobs.\n");
3224
+ return;
3225
+ }
3226
+ for (const job of listed) {
3227
+ process.stdout.write(`${job.id} ${pad2(job.action)} ${pad2(job.state)} ${job.counts["done"] ?? 0}/${job.total} done, ${job.counts["failed"] ?? 0} failed, ${job.counts["skipped"] ?? 0} skipped ${job.createdAt}
3228
+ `);
3229
+ if (listed.length === 1) {
3230
+ for (const problem of job.problems) {
3231
+ process.stdout.write(` ${problem.sandboxId} ${problem.state}${problem.reason ? `: ${problem.reason}` : ""}
3232
+ `);
3233
+ }
3234
+ }
3235
+ }
3236
+ }
2930
3237
  async function exec(positionals) {
2931
3238
  const id = positionals[0];
2932
3239
  const command = positionals.slice(1);
@@ -3371,7 +3678,7 @@ function labelsInWords(body) {
3371
3678
  return [`tags: ${tags.length > 0 ? tags.join(", ") : "none"}`, `metadata: ${metadata.length > 0 ? metadata.join(", ") : "none"}`, ""].join("\n");
3372
3679
  }
3373
3680
  async function labels(what, holder, positionals, asJson) {
3374
- const [named, action, ...words] = positionals;
3681
+ const [named, action, ...words2] = positionals;
3375
3682
  const command = `${holder === "template" ? "template " : ""}${what}`;
3376
3683
  const usage = what === "tags" ? `${command} <${holder}> [add|rm <tag>...]` : `${command} <${holder}> [set <key>=<value>... | unset <key>...]`;
3377
3684
  if (!named) {
@@ -3383,7 +3690,7 @@ async function labels(what, holder, positionals, asJson) {
3383
3690
  if (action === void 0) {
3384
3691
  body = await call2(path4, { method: "GET" });
3385
3692
  } else if (what === "tags" && (action === "add" || action === "rm")) {
3386
- const tags = listedIn(words).map((tag) => tag.toLowerCase());
3693
+ const tags = listedIn(words2).map((tag) => tag.toLowerCase());
3387
3694
  if (tags.length === 0) {
3388
3695
  throw new Error(`${command} ${action} requires <tag>...`);
3389
3696
  }
@@ -3395,10 +3702,10 @@ async function labels(what, holder, positionals, asJson) {
3395
3702
  body = await call2(`${path4}/tags/${encodeURIComponent(tag)}`, { method: action === "add" ? "PUT" : "DELETE" });
3396
3703
  }
3397
3704
  } else if (what === "meta" && (action === "set" || action === "unset")) {
3398
- if (words.length === 0) {
3705
+ if (words2.length === 0) {
3399
3706
  throw new Error(`${command} ${action} requires ${action === "set" ? "<key>=<value>" : "<key>"}...`);
3400
3707
  }
3401
- const metadata = action === "set" ? metadataGiven(words) : Object.fromEntries(words.map((key2) => [key2, null]));
3708
+ const metadata = action === "set" ? metadataGiven(words2) : Object.fromEntries(words2.map((key2) => [key2, null]));
3402
3709
  body = await call2(path4, { method: "PATCH", body: JSON.stringify({ metadata }) });
3403
3710
  } else {
3404
3711
  throw new Error(`${command} takes ${usage}`);
@@ -3513,7 +3820,6 @@ function showTemplate(each) {
3513
3820
  // Said only where some are. An API from before variables could be kept out of the sandbox says nothing of them.
3514
3821
  ...each.hidden && each.hidden.length > 0 ? [`kept out: ${list3(each.hidden)}`] : [],
3515
3822
  `files: ${list3(each.files)}`,
3516
- `repo: ${each.repo ? `${each.repo.name} ${each.repo.url}` : "none"}`,
3517
3823
  // An API from before templates held rules says nothing of one.
3518
3824
  ...each.network ? [`network: ${ruleInWords(each.network)}`] : [],
3519
3825
  // An API from before templates gave SSH says nothing of it.
@@ -3526,10 +3832,10 @@ function showTemplate(each) {
3526
3832
  }
3527
3833
  async function template(positionals, values) {
3528
3834
  const [action, first, ...rest] = positionals;
3529
- const usage = "template takes create, show, rename, delete, start, repo, network, ssh, ssh-keys, tags, meta, sandboxes, apply, of or put. agental help says how.";
3530
- const print4 = (body, words) => {
3835
+ const usage = "template takes create, show, rename, delete, start, network, ssh, ssh-keys, tags, meta, sandboxes, apply, of or put. agental help says how.";
3836
+ const print4 = (body, words2) => {
3531
3837
  process.stdout.write(values.json ? `${JSON.stringify(body, null, 2)}
3532
- ` : words);
3838
+ ` : words2);
3533
3839
  };
3534
3840
  const need = (word, what) => {
3535
3841
  if (!word) {
@@ -3589,20 +3895,6 @@ async function template(positionals, values) {
3589
3895
  `);
3590
3896
  return;
3591
3897
  }
3592
- case "repo": {
3593
- const found = await templateNamed(need(first, "<template>"));
3594
- const path4 = `/v1/templates/${encodeURIComponent(found.id)}/repo`;
3595
- if (values.remove) {
3596
- await call2(path4, { method: "DELETE" });
3597
- process.stdout.write(`${found.name} links to no repository
3598
- `);
3599
- return;
3600
- }
3601
- const repo = rest.length === 0 ? await call2(path4, { method: "GET" }) : await call2(path4, { method: "PUT", body: JSON.stringify({ name: need(rest[0], "<template> <name> <url>"), url: need(rest[1], "<template> <name> <url>") }) });
3602
- print4(repo, repo ? `repo: ${repo.name} ${repo.url}
3603
- ` : "repo: none\n");
3604
- return;
3605
- }
3606
3898
  case "network": {
3607
3899
  const found = await templateNamed(need(first, "<template>"));
3608
3900
  const path4 = `/v1/templates/${encodeURIComponent(found.id)}/network`;
@@ -3728,19 +4020,19 @@ async function keptOutNow(path4) {
3728
4020
  }
3729
4021
  async function secrets(positionals, values) {
3730
4022
  const usage = "secrets requires <sandbox-id>, or --template <template>, and then nothing, get, set, hosts, unset, get-file, put-file or rm-file";
3731
- const words = [...positionals];
4023
+ const words2 = [...positionals];
3732
4024
  let base;
3733
4025
  let sandboxId;
3734
4026
  if (values.template !== void 0) {
3735
4027
  base = `/v1/templates/${encodeURIComponent((await templateNamed(values.template)).id)}/secrets`;
3736
4028
  } else {
3737
- sandboxId = words.shift();
4029
+ sandboxId = words2.shift();
3738
4030
  if (!sandboxId) {
3739
4031
  throw new Error(usage);
3740
4032
  }
3741
4033
  base = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/secrets`;
3742
4034
  }
3743
- const [action, ...args] = words;
4035
+ const [action, ...args] = words2;
3744
4036
  const fileQuery = (path4) => {
3745
4037
  if (!path4) {
3746
4038
  throw new Error(`secrets ${action ?? ""} requires <path>`);
@@ -4015,14 +4307,14 @@ async function ci(positionals, options) {
4015
4307
  return;
4016
4308
  }
4017
4309
  if (body.repositories.length === 0) {
4018
- process.stdout.write(`No repository has a pool yet. Connect GitHub in the console (Settings, GitHub), then ask for runs-on: agental-medium in a workflow. Each pool keeps ${body.defaultPoolMax} sandboxes of each size.
4310
+ process.stdout.write(`No repository has a pool yet. Connect GitHub in the console (Settings, GitHub), then ask for runs-on: agental (its template's size, medium with none) or agental-<size> in a workflow. Each pool keeps ${body.defaultPoolMax} sandboxes of each size.
4019
4311
  `);
4020
4312
  return;
4021
4313
  }
4022
4314
  for (const item of body.repositories) {
4023
4315
  const busy = item.sandboxes.filter((sandbox) => sandbox.busy).length;
4024
4316
  process.stdout.write(
4025
- `${item.repository} pool ${item.poolMax ?? `${body.defaultPoolMax} (default)`} template ${item.templateId ?? "runs-on (empty)"} ${item.sandboxes.length} sandboxes, ${busy} busy
4317
+ `${item.repository} pool ${item.poolMax ?? `${body.defaultPoolMax} (default)`} template ${item.templateId ?? "runs-on (empty)"} agental is ${item.size} ${item.sandboxes.length} sandboxes, ${busy} busy
4026
4318
  `
4027
4319
  );
4028
4320
  }
@@ -4132,7 +4424,7 @@ async function telemetry(positionals, given, asJson) {
4132
4424
  }
4133
4425
  async function billing(positionals, asJson) {
4134
4426
  const [what, amount, ...rest] = positionals;
4135
- const usage = "billing takes nothing, prices, top-up <dollars>, or auto-reload <dollars | off>";
4427
+ const usage = "billing takes nothing, prices, card, top-up <dollars>, or auto-reload <dollars | off>";
4136
4428
  if (rest.length > 0 || what !== "top-up" && what !== "auto-reload" && amount !== void 0) {
4137
4429
  throw new Error(usage);
4138
4430
  }
@@ -4149,6 +4441,17 @@ async function billing(positionals, asJson) {
4149
4441
  print4(body, () => describePrices(body));
4150
4442
  return;
4151
4443
  }
4444
+ case "card": {
4445
+ const body = await asked("/v1/billing/checkout", { method: "POST", body: JSON.stringify({ card: true }) });
4446
+ print4(
4447
+ body,
4448
+ () => body.url ? `Save a card for the free plan at:
4449
+ ${body.url}
4450
+ Nothing is charged. Once it is saved, the account is given its credit, once, and may run sandboxes: agental billing shows it within a minute.
4451
+ ` : "Nothing to save.\n"
4452
+ );
4453
+ return;
4454
+ }
4152
4455
  case "top-up": {
4153
4456
  const dollars = Number(amount);
4154
4457
  if (amount === void 0 || !/^\d+$/.test(amount) || dollars <= 0) {
@@ -4196,19 +4499,27 @@ function dayOf(iso) {
4196
4499
  }
4197
4500
  function describeBilling(body) {
4198
4501
  const lines = [];
4199
- const live = body.status === "trialing" || body.status === "active" || body.status === "past_due";
4200
- if (live && body.plan) {
4201
- const free = body.status === "trialing" ? body.freeMonth ?? { endsAt: body.period?.end ?? null } : null;
4202
- lines.push([
4203
- "Plan",
4204
- free ? `$${body.plan.dollarsPerMonth} a month, free until ${dayOf(free.endsAt)}, ${body.cancelAtPeriodEnd ? "and it ends then" : "then charged"}` : `$${body.plan.dollarsPerMonth} a month, ${body.cancelAtPeriodEnd ? "ends" : "renews"} ${dayOf(body.period?.end ?? null)}`
4205
- ]);
4206
- const parts = [
4207
- ...(body.mailMicros ?? 0) > 0 ? [`${money(body.mailMicros ?? 0)} by email`] : [],
4208
- ...(body.stoppedMicros ?? 0) > 0 ? [`${money(body.stoppedMicros ?? 0)} by stopped sandboxes`] : [],
4209
- ...(body.snapshotMicros ?? 0) > 0 ? [`${money(body.snapshotMicros ?? 0)} by snapshots past the free ones`] : []
4210
- ];
4211
- lines.push(["Used", `${money(body.usedMicros)} this month at list price${parts.length > 0 ? `, ${parts.join(" and ")} of it` : ""}`]);
4502
+ const free = body.free ?? null;
4503
+ if (free && body.plan) {
4504
+ lines.push(["Plan", "free: nothing a month. What runs is paid from your credits, at list price."]);
4505
+ if (!free.cardSaved) {
4506
+ lines.push(["Card", "none saved. Save one, charged nothing, to run sandboxes and be given the free plan's credit: agental billing card."]);
4507
+ } else if (!free.paid) {
4508
+ lines.push(["At once", `${free.unpaidUnits} small sandboxes until you buy a top-up, then ${(body.plan.runningUnits ?? 20).toLocaleString("en-US")}. A tiny counts half.`]);
4509
+ }
4510
+ lines.push(["Used", `${money(body.usedMicros)} this month at list price${usedParts(body)}`]);
4511
+ lines.push(["Credits left", `${money(body.topUpMicros)}, the free credit and top-ups. They do not expire.`]);
4512
+ if ((body.carriedMicros ?? 0) > 0) {
4513
+ lines.push(["Owed", `${money(body.carriedMicros ?? 0)} an earlier month ran past your credits, taken from the next top-up first`]);
4514
+ }
4515
+ if (body.autoReload) {
4516
+ lines.push(["Auto-reload", describeAutoReload(body.autoReload)]);
4517
+ } else if (body.canAutoReload) {
4518
+ lines.push(["Auto-reload", "off. agental billing auto-reload <dollars> charges your saved card for a top-up when your credits run out."]);
4519
+ }
4520
+ } else if (body.plan && (body.status === "active" || body.status === "past_due")) {
4521
+ lines.push(["Plan", `$${body.plan.dollarsPerMonth} a month, ${body.cancelAtPeriodEnd ? "ends" : "renews"} ${dayOf(body.period?.end ?? null)}`]);
4522
+ lines.push(["Used", `${money(body.usedMicros)} this month at list price${usedParts(body)}`]);
4212
4523
  if ((body.carriedMicros ?? 0) > 0) {
4213
4524
  lines.push(["Carried", `${money(body.carriedMicros ?? 0)} an earlier month could not pay, taken from this month's credits first`]);
4214
4525
  }
@@ -4224,15 +4535,18 @@ function describeBilling(body) {
4224
4535
  lines.push(["Auto-reload", "off. agental billing auto-reload <dollars> charges your saved card for a top-up when your credits run out."]);
4225
4536
  }
4226
4537
  } else {
4227
- lines.push(["Plan", body.enforced ? "none. A plan is needed to run a sandbox, and the $20 plan's first month is free: see Billing in the console." : "none. Plans are not required here yet."]);
4538
+ lines.push(["Plan", "none. Plans are not required here yet."]);
4228
4539
  }
4229
4540
  if (!body.canRun && body.reason) {
4230
- lines.push(["Running", body.reason === "out_of_credit" ? "no: the plan's credits and the top-ups are used. agental billing top-up <dollars> buys more." : `no: ${body.reason}`]);
4541
+ lines.push([
4542
+ "Running",
4543
+ body.reason === "out_of_credit" ? `no: ${free ? "your credits are" : "the plan's credits and the top-ups are"} used. agental billing top-up <dollars> buys more.` : body.reason === "billing_required" ? "no: save a card first, charged nothing, with agental billing card." : `no: ${body.reason}`
4544
+ ]);
4231
4545
  }
4232
4546
  if (body.lapse) {
4233
4547
  lines.push([
4234
4548
  "Deleted on",
4235
- body.lapse.reason === "unpaid" ? `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as the account owes for what it used past its plan. agental billing top-up <dollars> buys enough to pay it and keep them.` : `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as the account has no plan. Start a plan from Billing in the console to keep them.`
4549
+ body.lapse.reason === "unpaid" ? `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as the account owes for what it used past its plan. agental billing top-up <dollars> buys enough to pay it and keep them.` : body.lapse.reason === "out_of_credit" ? `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as your credits are used. agental billing top-up <dollars> keeps them.` : `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as the account has no card saved. agental billing card saves one, charged nothing, to keep them.`
4236
4550
  ]);
4237
4551
  }
4238
4552
  if (body.testMode) {
@@ -4241,6 +4555,14 @@ function describeBilling(body) {
4241
4555
  return lines.map(([label, value]) => `${label.padEnd(14)}${value}
4242
4556
  `).join("");
4243
4557
  }
4558
+ function usedParts(body) {
4559
+ const parts = [
4560
+ ...(body.mailMicros ?? 0) > 0 ? [`${money(body.mailMicros ?? 0)} by email`] : [],
4561
+ ...(body.stoppedMicros ?? 0) > 0 ? [`${money(body.stoppedMicros ?? 0)} by stopped sandboxes`] : [],
4562
+ ...(body.snapshotMicros ?? 0) > 0 ? [`${money(body.snapshotMicros ?? 0)} by snapshots past the free ones`] : []
4563
+ ];
4564
+ return parts.length > 0 ? `, ${parts.join(" and ")} of it` : "";
4565
+ }
4244
4566
  function describeAutoReload(autoReload) {
4245
4567
  if (!autoReload) {
4246
4568
  return "off. Nothing is charged by itself when your credits run out.";
@@ -4257,7 +4579,14 @@ function describePrices(body) {
4257
4579
  for (const tier of body.tiers) {
4258
4580
  const sizes = tier.sizes === void 0 ? "" : tier.sizes.length === body.plans.length ? "every size" : tier.sizes.join(", ");
4259
4581
  const speed = tier.runningUnits !== void 0 && tier.freeBurstUnits !== void 0 ? `${tier.runningUnits.toLocaleString("en-US")} running, ${tier.freeBurstUnits.toLocaleString("en-US")} start free` : tier.burst ? `${(tier.burst.perQuarterMb / 1024).toLocaleString("en-US")} GiB per 15 minutes` : typeof tier.sandboxesAtOnce === "number" ? `${tier.sandboxesAtOnce.toLocaleString("en-US")} sandboxes at once` : "";
4260
- out.push(` ${`$${tier.dollarsPerMonth.toLocaleString("en-US")}`.padEnd(8)}${`${wholeMoney(tier.includedMicros)} of credits`.padEnd(28)}${speed.padEnd(34)}${sizes}`.trimEnd());
4582
+ const price = tier.dollarsPerMonth === 0 ? "Free" : `$${tier.dollarsPerMonth.toLocaleString("en-US")}`;
4583
+ const included = tier.dollarsPerMonth === 0 ? "pay as you go" : `${wholeMoney(tier.includedMicros)} of credits`;
4584
+ out.push(` ${price.padEnd(8)}${included.padEnd(28)}${speed.padEnd(34)}${sizes}`.trimEnd());
4585
+ }
4586
+ if (body.free) {
4587
+ out.push(
4588
+ ` Free needs a card saved, charged nothing, and gives ${wholeMoney(body.free.creditMicros)} of credit once. It runs ${body.free.unpaidUnits} small sandboxes at once until you buy a top-up.`
4589
+ );
4261
4590
  }
4262
4591
  if (body.tiers.some((tier) => tier.runningUnits !== void 0)) {
4263
4592
  out.push(" Counted in small sandboxes: a medium is 2, a large 4, a tiny half.");
@@ -4520,7 +4849,8 @@ Usage
4520
4849
  agental create --name <name> [--template <template>] [--size tiny|small|medium|large] [--region <region>[,<region>...]] [--ttl <time> | --ttl default] [--idempotency-key <key>]
4521
4850
  ${operator ? "[--box <name-or-id>] " : ""}[--allow <address>[,<address>...]] [--max-burst-fee <dollars>]
4522
4851
  [--ssh-key <public-key-or-file>]... [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]...
4523
- agental list [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--json]
4852
+ agental summary [--where '<words>'] [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--facets <field>[,<field>...]] [--json]
4853
+ agental list [--where '<words>'] [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--sort=<sort>] [--limit <n>] [--json]
4524
4854
  agental tags <sandbox-id> [add|rm <tag>...] [--json]
4525
4855
  agental meta <sandbox-id> [set <key>=<value>... | unset <key>...] [--json]
4526
4856
  agental regions [--json]
@@ -4528,6 +4858,10 @@ Usage
4528
4858
  agental delete <sandbox-id>
4529
4859
  agental start <sandbox-id> [--max-burst-fee <dollars>]
4530
4860
  agental stop <sandbox-id> [--keep-memory]
4861
+ agental stop --where '<words>' [--keep-memory] [--yes] every sandbox the words find, as one job
4862
+ agental start --where '<words>' [--yes]
4863
+ agental delete --where '<words>' [--confirm <count>] asks for the count to be typed
4864
+ agental jobs [list | show <job-id> | cancel <job-id>] [--json]
4531
4865
  agental recover <sandbox-id> [--max-burst-fee <dollars>]
4532
4866
  agental auto-stop <sandbox-id> [--in <time> | --at <date-time> | --never] [--json]
4533
4867
  agental network <sandbox-id> [--allow <address>[,<address>...] | --none] [--json]
@@ -4537,7 +4871,6 @@ Usage
4537
4871
  agental template rename <template> <new-name>
4538
4872
  agental template delete <template>
4539
4873
  agental template start <template> [--size tiny|small|medium|large] [--snapshot <snapshot-id> | --new-machine] [--ttl <time> | --ttl default]
4540
- agental template repo <template> [<name> <url> | --remove]
4541
4874
  agental template network <template> [open | blocked | allowlist <address>...] [--json]
4542
4875
  agental template ssh <template> [on | off] [--json]
4543
4876
  agental template ssh-keys <template> [<public-key-or-file>... | --remove <key-id>] [--json]
@@ -4560,11 +4893,12 @@ Usage
4560
4893
  agental data-retention [1 | 7 | 30 | 90 | never] [--json]
4561
4894
  agental schedules <sandbox-id> [--json]
4562
4895
  agental schedule add <sandbox-id> (--every 15m|1h|1d | --cron "<m h dom mon dow>" [--tz <zone>] | --at <date-time>) [--name <name>] [--timeout <time>]
4563
- [--if-stopped start|skip] [--keep-memory] [--overlap skip|replace|parallel] [--json] -- <command> [args...]
4896
+ [--if-stopped start|skip] [--keep-memory] [--overlap skip|replace|parallel] [--delete-after] [--json] -- <command> [args...]
4564
4897
  agental schedule show|run|pause|resume|delete <sandbox-id> <schedule> [--json]
4565
4898
  agental schedule runs <sandbox-id> <schedule> [--limit <n>] [--json]
4566
4899
  agental schedule update <sandbox-id> <schedule> [--every <time> | --cron "<expr>" | --at <date-time>] [--tz <zone>] [--name <name>] [--timeout <time>]
4567
- [--if-stopped start|skip] [--keep-memory | --disk-only] [--overlap skip|replace|parallel] [--json] [-- <command> [args...]]
4900
+ [--if-stopped start|skip] [--keep-memory | --disk-only] [--overlap skip|replace|parallel] [--delete-after | --keep-sandbox] [--json]
4901
+ [-- <command> [args...]]
4568
4902
  agental webhooks [--json]
4569
4903
  agental webhook add <url> (--events <kind>[,<kind>...] | --all) [--description <text>] [--json]
4570
4904
  agental webhook show <webhook-id> [--json]
@@ -4584,7 +4918,7 @@ Usage
4584
4918
  agental key show <key> [--json]
4585
4919
  agental key allow-from <key> (<address-or-range>... | --anywhere) [--json]
4586
4920
  agental key delete <key>
4587
- agental billing [prices | top-up <dollars> | auto-reload <dollars | off>] [--json]
4921
+ agental billing [prices | card | top-up <dollars> | auto-reload <dollars | off>] [--json]
4588
4922
  agental burst [budget <dollars> | budget default] [--json]
4589
4923
  agental account export [--out <file>]
4590
4924
  agental ssh <sandbox-id> [--tmux] [ssh options] [command...]
@@ -4647,7 +4981,7 @@ sandbox is not restarted, and what is sent to it, commands, files and SSH, is no
4647
4981
  create --allow gives a new sandbox its own from its first boot. These work where GET
4648
4982
  /v1/policy says networkPolicy.
4649
4983
  A template is what a sandbox is made as and held to: its start (a size, and a snapshot or a new
4650
- machine), its variables and files, and a repository link. Every account has one named default,
4984
+ machine), its variables and files, and its outbound rule. Every account has one named default,
4651
4985
  which a sandbox is on when create names none. <template> is a template's id or its name. A
4652
4986
  sandbox's commands get its template's variables and files as they were when it last started,
4653
4987
  with its own on top: secrets <sandbox-id> sets its own, and secrets --template sets a
@@ -4700,8 +5034,10 @@ schedules lists the commands a sandbox runs on a schedule, with when each runs n
4700
5034
  last run ended. schedule add makes one: what comes after -- is its command, run by sh -c, and
4701
5035
  --every 5m, 15m, 1h or 1d, counted from now, never more often than every 5 minutes, or --cron
4702
5036
  with five fields in the time zone --tz names, UTC unless it does, says when; --at a date and time
4703
- runs it once, as for a check to come back to. When a run finds the sandbox stopped it starts it,
4704
- runs, and stops it again when the command ends, keeping its disk, or its memory too with
5037
+ runs it once, as for a check to come back to. With --at, --delete-after deletes the sandbox after
5038
+ the run, if the command exits 0, with a key that may delete it; a run that fails leaves it to be
5039
+ looked at, and update --keep-sandbox takes it back. When a run finds the sandbox stopped it
5040
+ starts it, runs, and stops it again when the command ends, keeping its disk, or its memory too with
4705
5041
  --keep-memory; with --if-stopped skip it runs only while the sandbox runs. A sandbox someone
4706
5042
  started again, sent a command or moved the stop of meanwhile is left running. While a run still
4707
5043
  goes the next is skipped; --overlap replace stops it instead, and parallel runs both. A run gets
@@ -4725,8 +5061,10 @@ tries.
4725
5061
  org lists the organizations you are in, your own and your teams, and marks the one this command
4726
5062
  acts in. A key acts in the organization it was made in, for good, so org use signs in again in
4727
5063
  the browser for a key made in the one given, by its id or name, and deletes the key the command
4728
- had. org create makes a team with you as its owner; it has members besides its owners on the
4729
- $500 or $2000 plan. members lists the team the command acts in, and members remove takes one
5064
+ had. Your own organization takes members on the $500 or $2000 plan, with you its only owner.
5065
+ org create makes a team besides, with you as its owner and a plan of its own; it has members
5066
+ besides its owners on the $500 or $2000 plan. members lists the organization the command acts
5067
+ in, your own or a team, and members remove takes one
4730
5068
  out by id or email; keys they made in it go on working, as a member's, until they are revoked,
4731
5069
  and --revoke-keys revokes them in the same step. invite asks an email address in, as a member or
4732
5070
  with --role admin; the invitation is open for seven days.
@@ -4754,8 +5092,12 @@ server it is on, which changes when it moves. On a network with IPv6 the API see
4754
5092
  address, so a key limited to IPv4 addresses alone is refused there.
4755
5093
  billing says where your account stands: its plan, what it has used this month at list price,
4756
5094
  what is left of the month's credits, and what is left of the top-ups, which are used
4757
- after it and do not expire. prices lists the plans with the sizes each makes, the top-ups with
4758
- their bonus and the hourly price of each size. top-up <dollars> starts buying credits for
5095
+ after it and do not expire. The free plan costs nothing a month and pays for what runs from
5096
+ credits alone; it makes tiny and small sandboxes. prices lists the plans with the sizes each
5097
+ makes, the top-ups with their bonus and the hourly price of each size. card prints the address
5098
+ of a page to save a card for the free plan: nothing is charged, and once it is saved the account
5099
+ is given $10 of credit, once. Until it buys a top-up it runs 2 small sandboxes at once, then 20.
5100
+ top-up <dollars> starts buying credits for
4759
5101
  any whole number of dollars from 15 to 100,000, and prints the address of the page to pay on.
4760
5102
  From $200 a top-up adds a bonus, 2% there and growing to 25% at $10,000: $500 adds $537.
4761
5103
  Nothing is charged until it is paid there. auto-reload <dollars> has the card saved at your
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "agental",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The agental command: Linux sandboxes for your agents, from the terminal. npx agental@latest signs you in and installs the agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "VERTIXER LTD",
7
7
  "homepage": "https://agental.io/docs/cli",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/honzabit/agental.git",
11
+ "directory": "cli"
12
+ },
8
13
  "bugs": {
9
14
  "email": "squad@agental.io"
10
15
  },