agental 0.2.0 → 0.4.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 +536 -103
  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.4.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,100 @@ 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", "activity"]);
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" && key2 !== "activity") {
2447
+ throw new Error(`${word}: only tags, activity 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 "activity":
2483
+ for (const each of value.split(",")) {
2484
+ query.append(not ? "notActivity" : "activity", each);
2485
+ }
2486
+ break;
2487
+ case "archived":
2488
+ query.set("archived", value);
2489
+ break;
2490
+ case "stops":
2491
+ query.set("stops", value);
2492
+ break;
2493
+ default:
2494
+ query.append(key2, value);
2495
+ }
2496
+ }
2497
+ if (plain2.length > 0) {
2498
+ query.set("q", plain2.join(" "));
2499
+ }
2500
+ return query;
2501
+ }
2502
+
2397
2503
  // ../src/cli.ts
2398
2504
  var DEFAULT_URL = DEFAULT_API_URL;
2399
2505
  var SSH_COMMANDS = {
@@ -2506,7 +2612,7 @@ async function main() {
2506
2612
  expires: { type: "string" },
2507
2613
  "keep-streams": { type: "boolean" },
2508
2614
  "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.
2615
+ // 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
2616
  every: { type: "string" },
2511
2617
  cron: { type: "string" },
2512
2618
  tz: { type: "string" },
@@ -2514,12 +2620,16 @@ async function main() {
2514
2620
  overlap: { type: "string" },
2515
2621
  "if-stopped": { type: "string" },
2516
2622
  "disk-only": { type: "boolean" },
2623
+ "delete-after": { type: "boolean" },
2624
+ "keep-sandbox": { type: "boolean" },
2517
2625
  ttl: { type: "string" },
2518
2626
  "max-burst-fee": { type: "string" },
2519
2627
  in: { type: "string" },
2520
2628
  never: { type: "boolean", default: false },
2521
2629
  outbound: { type: "string" },
2522
2630
  allow: { type: "string", multiple: true },
2631
+ // Where a sandbox's web traffic leaves, as agental create takes it: box, or residential:<country>[:sticky|rotating].
2632
+ egress: { type: "string" },
2523
2633
  "ssh-key": { type: "string", multiple: true },
2524
2634
  none: { type: "boolean", default: false },
2525
2635
  host: { type: "string", multiple: true },
@@ -2545,6 +2655,11 @@ async function main() {
2545
2655
  // Metadata as <key>=<value>, given again for each key.
2546
2656
  meta: { type: "string", multiple: true },
2547
2657
  anywhere: { type: "boolean", default: false },
2658
+ where: { type: "string" },
2659
+ facets: { type: "string" },
2660
+ yes: { type: "boolean", default: false },
2661
+ confirm: { type: "string" },
2662
+ sort: { type: "string" },
2548
2663
  role: { type: "string" },
2549
2664
  "revoke-keys": { type: "boolean", default: false },
2550
2665
  help: { type: "boolean", short: "h", default: false }
@@ -2560,7 +2675,13 @@ async function main() {
2560
2675
  await create({ ...values, template: once(values.template, "--template") });
2561
2676
  return;
2562
2677
  case "list":
2563
- await list2(values.json, labelsFilter(values));
2678
+ await list2(values.json, whereQuery(values.where ?? "", labelsFilter(values)), values.sort, values.limit);
2679
+ return;
2680
+ case "jobs":
2681
+ await jobs(positionals.slice(1), values.json);
2682
+ return;
2683
+ case "summary":
2684
+ await summary(values.json, whereQuery(values.where ?? "", labelsFilter(values)), values.facets);
2564
2685
  return;
2565
2686
  case "tags":
2566
2687
  await labels("tags", "sandbox", positionals.slice(1), values.json);
@@ -2575,15 +2696,30 @@ async function main() {
2575
2696
  await exec(positionals.slice(1));
2576
2697
  return;
2577
2698
  case "delete":
2699
+ if (values.where !== void 0 && positionals[1] !== void 0) {
2700
+ throw new Error("delete takes a sandbox's ID or --where, not both");
2701
+ }
2702
+ if (values.where !== void 0) {
2703
+ await bulk("delete", whereQuery(values.where, labelsFilter(values)), {}, values.yes === true, values.json, values.confirm);
2704
+ return;
2705
+ }
2578
2706
  await remove2(positionals[1]);
2579
2707
  return;
2580
2708
  case "start":
2709
+ if (values.where !== void 0) {
2710
+ await bulk("start", whereQuery(values.where, labelsFilter(values)), {}, values.yes === true, values.json);
2711
+ return;
2712
+ }
2581
2713
  await lifecycle("start", positionals[1], maxBurstFeeHeaders(values["max-burst-fee"]));
2582
2714
  return;
2583
2715
  case "auto-stop":
2584
2716
  await autoStop(positionals[1], values.in, values.at, values.never === true, values.json);
2585
2717
  return;
2586
2718
  case "stop":
2719
+ if (values.where !== void 0) {
2720
+ await bulk("stop", whereQuery(values.where, labelsFilter(values)), values["keep-memory"] === true ? { keepMemory: true } : {}, values.yes === true, values.json);
2721
+ return;
2722
+ }
2587
2723
  await lifecycle("stop", positionals[1], {}, values["keep-memory"] === true ? { keepMemory: true } : void 0);
2588
2724
  return;
2589
2725
  case "recover":
@@ -2641,6 +2777,9 @@ async function main() {
2641
2777
  case "network":
2642
2778
  await network(positionals.slice(1), values);
2643
2779
  return;
2780
+ case "egress":
2781
+ await egress(positionals.slice(1), values);
2782
+ return;
2644
2783
  case "templates":
2645
2784
  await listTemplates(values.json);
2646
2785
  return;
@@ -2746,6 +2885,7 @@ async function create(values) {
2746
2885
  );
2747
2886
  }
2748
2887
  const allow = values.allow === void 0 ? void 0 : listedIn(values.allow);
2888
+ const egressAsked = values.egress === void 0 ? void 0 : egressFrom(values.egress.split(":"), "--egress takes box, or residential:<country>, as residential:de or residential:us:rotating.");
2749
2889
  const sshKeys2 = values["ssh-key"] === void 0 ? void 0 : publicKeysFrom(values["ssh-key"]);
2750
2890
  const box = boxNamed(values.box);
2751
2891
  const templateId = values.template === void 0 ? void 0 : (await templateNamed(values.template)).id;
@@ -2759,6 +2899,7 @@ async function create(values) {
2759
2899
  ...templateId ? { templateId } : {},
2760
2900
  ...regionsOption(values.region),
2761
2901
  ...allow ? { network: { allow } } : {},
2902
+ ...egressAsked ? { egress: egressAsked } : {},
2762
2903
  ...ttlSeconds === void 0 ? {} : { ttlSeconds },
2763
2904
  ...sshKeys2 === void 0 ? {} : { sshKeys: sshKeys2 },
2764
2905
  ...given
@@ -2888,20 +3029,39 @@ async function regions(asJson) {
2888
3029
  }
2889
3030
  process.stdout.write("* where a sandbox is made when no region is named\n");
2890
3031
  }
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);
3032
+ async function list2(asJson, filter, sort, limit) {
3033
+ const sandboxes = [];
3034
+ let cursor = null;
3035
+ do {
3036
+ const query = new URLSearchParams(filter);
3037
+ if (sort !== void 0) {
3038
+ query.set("sort", sort);
3039
+ }
3040
+ query.set("limit", limit ?? "200");
3041
+ if (cursor !== null) {
3042
+ query.set("cursor", cursor);
3043
+ }
3044
+ const response = await request(`/v1/sandboxes?${query}`, { method: "GET" });
3045
+ const body = await readBody(response);
3046
+ if (!response.ok) {
3047
+ fail(response.status, body);
3048
+ }
3049
+ sandboxes.push(...readSandboxes(body));
3050
+ const next = body && typeof body === "object" ? body["nextCursor"] : null;
3051
+ cursor = typeof next === "string" ? next : null;
3052
+ if (limit !== void 0 && cursor !== null) {
3053
+ 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}
3054
+ `);
3055
+ cursor = null;
3056
+ }
3057
+ } while (cursor !== null);
2898
3058
  if (asJson) {
2899
3059
  process.stdout.write(`${JSON.stringify(sandboxes, null, 2)}
2900
3060
  `);
2901
3061
  return;
2902
3062
  }
2903
3063
  if (sandboxes.length === 0) {
2904
- process.stdout.write(filter.size > 0 ? "No sandboxes with those tags and metadata.\n" : "No sandboxes.\n");
3064
+ process.stdout.write(filter.size > 0 ? "No sandboxes match.\n" : "No sandboxes.\n");
2905
3065
  return;
2906
3066
  }
2907
3067
  const names = sandboxes.some((sandbox) => typeof sandbox["templateId"] === "string") ? await templateNames() : /* @__PURE__ */ new Map();
@@ -2927,6 +3087,165 @@ async function list2(asJson, filter) {
2927
3087
  `);
2928
3088
  }
2929
3089
  }
3090
+ function dollarsOf(micros) {
3091
+ return `$${(micros / 1e6).toFixed(4)}`;
3092
+ }
3093
+ async function summary(asJson, filter, facets) {
3094
+ const query = new URLSearchParams(filter);
3095
+ if (facets !== void 0) {
3096
+ query.set("facets", facets);
3097
+ }
3098
+ const response = await request(`/v1/sandbox-summary${query.size > 0 ? `?${query}` : ""}`, { method: "GET" });
3099
+ const body = await readBody(response);
3100
+ if (!response.ok) {
3101
+ fail(response.status, body);
3102
+ }
3103
+ if (asJson) {
3104
+ process.stdout.write(`${JSON.stringify(body, null, 2)}
3105
+ `);
3106
+ return;
3107
+ }
3108
+ const read2 = body;
3109
+ 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
3110
+ `;
3111
+ for (const [state2, money2] of Object.entries(read2.byState)) {
3112
+ if (money2.count > 0) {
3113
+ process.stdout.write(line(state2, money2));
3114
+ }
3115
+ }
3116
+ process.stdout.write(line("total", read2.total));
3117
+ const running = read2.byState["running"];
3118
+ if (running && running.count > 0) {
3119
+ process.stdout.write(`Stopped, the ${running.count} running would cost ${dollarsOf(read2.ifStopped.microsPerHour)} an hour, ${dollarsOf(running.microsPerHour - read2.ifStopped.microsPerHour)} less.
3120
+ `);
3121
+ }
3122
+ 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");
3123
+ for (const [field, facet] of Object.entries(read2.facets ?? {})) {
3124
+ process.stdout.write(`
3125
+ ${field}
3126
+ `);
3127
+ for (const each of facet.values) {
3128
+ process.stdout.write(` ${pad2(each.value)} ${String(each.count).padStart(6)} ${dollarsOf(each.microsPerHour).padStart(11)} an hour
3129
+ `);
3130
+ }
3131
+ if (facet.more > 0) {
3132
+ process.stdout.write(` and ${facet.more} more
3133
+ `);
3134
+ }
3135
+ }
3136
+ }
3137
+ function filterOf(query) {
3138
+ const filter = {};
3139
+ for (const [name, value] of query) {
3140
+ filter[name] = [...filter[name] ?? [], value];
3141
+ }
3142
+ return filter;
3143
+ }
3144
+ async function typedCount(count) {
3145
+ const { createInterface: createInterface2 } = await import("node:readline/promises");
3146
+ const lines = createInterface2({ input: process.stdin, output: process.stderr });
3147
+ try {
3148
+ return (await lines.question(`They cannot be brought back. Type ${count} to delete them: `)).trim();
3149
+ } finally {
3150
+ lines.close();
3151
+ }
3152
+ }
3153
+ async function confirmed(question) {
3154
+ if (!process.stdin.isTTY) {
3155
+ return false;
3156
+ }
3157
+ const { createInterface: createInterface2 } = await import("node:readline/promises");
3158
+ const lines = createInterface2({ input: process.stdin, output: process.stderr });
3159
+ try {
3160
+ return /^y(es)?$/i.test((await lines.question(`${question} [y/N] `)).trim());
3161
+ } finally {
3162
+ lines.close();
3163
+ }
3164
+ }
3165
+ async function bulk(action, filter, options, yes2, asJson, confirm) {
3166
+ const previewed = await request("/v1/sandbox-jobs/preview", { method: "POST", body: JSON.stringify({ action, options, select: { filter: filterOf(filter) } }) });
3167
+ const preview = await readBody(previewed);
3168
+ if (!previewed.ok) {
3169
+ fail(previewed.status, preview);
3170
+ }
3171
+ const read2 = preview;
3172
+ 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` : ""}.
3173
+ `);
3174
+ process.stderr.write(`They cost ${dollarsOf(read2.microsPerHour.now)} an hour now, and ${dollarsOf(read2.microsPerHour.after)} an hour after.
3175
+ `);
3176
+ let count = null;
3177
+ if (action === "delete") {
3178
+ const given = confirm ?? (process.stdin.isTTY ? await typedCount(read2.count) : "");
3179
+ if (given !== String(read2.count)) {
3180
+ process.stderr.write(`Nothing was deleted. Type ${read2.count}, or send --confirm ${read2.count}, to delete them.
3181
+ `);
3182
+ process.exitCode = 1;
3183
+ return;
3184
+ }
3185
+ count = read2.count;
3186
+ } else if (!yes2 && !await confirmed("Go on?")) {
3187
+ process.stderr.write("Nothing was done. --yes goes on without asking.\n");
3188
+ return;
3189
+ }
3190
+ 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 } }) });
3191
+ let job = await readBody(made);
3192
+ if (!made.ok) {
3193
+ fail(made.status, job);
3194
+ }
3195
+ while (job.state === "running") {
3196
+ process.stderr.write(`\r${job.id}: ${job.counts["done"] ?? 0} done, ${job.counts["skipped"] ?? 0} skipped, ${job.counts["failed"] ?? 0} failed of ${job.total}`);
3197
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
3198
+ const read3 = await request(`/v1/sandbox-jobs/${job.id}`, { method: "GET" });
3199
+ job = await readBody(read3);
3200
+ if (!read3.ok) {
3201
+ fail(read3.status, job);
3202
+ }
3203
+ }
3204
+ process.stderr.write("\n");
3205
+ if (asJson) {
3206
+ process.stdout.write(`${JSON.stringify(job, null, 2)}
3207
+ `);
3208
+ return;
3209
+ }
3210
+ 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}.
3211
+ `);
3212
+ for (const problem of job.problems) {
3213
+ process.stdout.write(` ${problem.sandboxId} ${problem.state}${problem.reason ? `: ${problem.reason}` : ""}
3214
+ `);
3215
+ }
3216
+ }
3217
+ async function jobs(positionals, asJson) {
3218
+ const [verb, id] = positionals;
3219
+ 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;
3220
+ if (path4 === null) {
3221
+ throw new Error("jobs takes list, show <job-id> or cancel <job-id>");
3222
+ }
3223
+ const response = await request(path4, { method: verb === "cancel" ? "POST" : "GET" });
3224
+ const body = await readBody(response);
3225
+ if (!response.ok) {
3226
+ fail(response.status, body);
3227
+ }
3228
+ if (asJson) {
3229
+ process.stdout.write(`${JSON.stringify(body, null, 2)}
3230
+ `);
3231
+ return;
3232
+ }
3233
+ const listed = verb === void 0 || verb === "list" ? body.jobs ?? [] : [body];
3234
+ if (listed.length === 0) {
3235
+ process.stdout.write("No jobs.\n");
3236
+ return;
3237
+ }
3238
+ for (const job of listed) {
3239
+ 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}
3240
+ `);
3241
+ if (listed.length === 1) {
3242
+ for (const problem of job.problems) {
3243
+ process.stdout.write(` ${problem.sandboxId} ${problem.state}${problem.reason ? `: ${problem.reason}` : ""}
3244
+ `);
3245
+ }
3246
+ }
3247
+ }
3248
+ }
2930
3249
  async function exec(positionals) {
2931
3250
  const id = positionals[0];
2932
3251
  const command = positionals.slice(1);
@@ -3371,7 +3690,7 @@ function labelsInWords(body) {
3371
3690
  return [`tags: ${tags.length > 0 ? tags.join(", ") : "none"}`, `metadata: ${metadata.length > 0 ? metadata.join(", ") : "none"}`, ""].join("\n");
3372
3691
  }
3373
3692
  async function labels(what, holder, positionals, asJson) {
3374
- const [named, action, ...words] = positionals;
3693
+ const [named, action, ...words2] = positionals;
3375
3694
  const command = `${holder === "template" ? "template " : ""}${what}`;
3376
3695
  const usage = what === "tags" ? `${command} <${holder}> [add|rm <tag>...]` : `${command} <${holder}> [set <key>=<value>... | unset <key>...]`;
3377
3696
  if (!named) {
@@ -3383,7 +3702,7 @@ async function labels(what, holder, positionals, asJson) {
3383
3702
  if (action === void 0) {
3384
3703
  body = await call2(path4, { method: "GET" });
3385
3704
  } else if (what === "tags" && (action === "add" || action === "rm")) {
3386
- const tags = listedIn(words).map((tag) => tag.toLowerCase());
3705
+ const tags = listedIn(words2).map((tag) => tag.toLowerCase());
3387
3706
  if (tags.length === 0) {
3388
3707
  throw new Error(`${command} ${action} requires <tag>...`);
3389
3708
  }
@@ -3395,10 +3714,10 @@ async function labels(what, holder, positionals, asJson) {
3395
3714
  body = await call2(`${path4}/tags/${encodeURIComponent(tag)}`, { method: action === "add" ? "PUT" : "DELETE" });
3396
3715
  }
3397
3716
  } else if (what === "meta" && (action === "set" || action === "unset")) {
3398
- if (words.length === 0) {
3717
+ if (words2.length === 0) {
3399
3718
  throw new Error(`${command} ${action} requires ${action === "set" ? "<key>=<value>" : "<key>"}...`);
3400
3719
  }
3401
- const metadata = action === "set" ? metadataGiven(words) : Object.fromEntries(words.map((key2) => [key2, null]));
3720
+ const metadata = action === "set" ? metadataGiven(words2) : Object.fromEntries(words2.map((key2) => [key2, null]));
3402
3721
  body = await call2(path4, { method: "PATCH", body: JSON.stringify({ metadata }) });
3403
3722
  } else {
3404
3723
  throw new Error(`${command} takes ${usage}`);
@@ -3444,6 +3763,72 @@ async function network(positionals, values) {
3444
3763
  ].join("\n")
3445
3764
  );
3446
3765
  }
3766
+ function egressFrom(words2, how) {
3767
+ const [via, country, session, ...more] = words2.map((word) => word.trim());
3768
+ if (via === "box" && country === void 0) {
3769
+ return { via: "box" };
3770
+ }
3771
+ if (via !== "residential" || country === void 0 || !/^[a-zA-Z]{2}$/.test(country) || more.length > 0 || session !== void 0 && session !== "sticky" && session !== "rotating") {
3772
+ throw new Error(how);
3773
+ }
3774
+ return { via: "residential", country: country.toLowerCase(), ...session === void 0 ? {} : { session } };
3775
+ }
3776
+ function egressInWords(body) {
3777
+ if (body?.via !== "residential") {
3778
+ return "from its server (box)";
3779
+ }
3780
+ return `residential, from ${String(body.country ?? "").toUpperCase()}, ${body.session === "rotating" ? "a new address each connection" : "one address kept (sticky)"}`;
3781
+ }
3782
+ async function egress(positionals, values) {
3783
+ const [first, ...rest] = positionals;
3784
+ const print4 = (body2, words2) => {
3785
+ process.stdout.write(values.json ? `${JSON.stringify(body2, null, 2)}
3786
+ ` : words2);
3787
+ };
3788
+ if (first === void 0 || first === "use-case" || first === "budget") {
3789
+ let body2;
3790
+ if (first === "use-case") {
3791
+ const useCase = rest.join(" ").trim();
3792
+ if (useCase === "") {
3793
+ throw new Error('egress use-case takes what your sandboxes will use residential addresses for, in a few sentences, as agental egress use-case "Our agents test sign-up flows from the countries we sell in."');
3794
+ }
3795
+ body2 = await call2("/v1/egress/use-case", { method: "PUT", body: JSON.stringify({ useCase }) });
3796
+ } else if (first === "budget") {
3797
+ const given = rest[0];
3798
+ const dollars = given === void 0 || given.trim() === "" ? Number.NaN : Number(given.replace(/^\$/, ""));
3799
+ if (rest.length !== 1 || given !== "default" && (!Number.isFinite(dollars) || dollars < 0)) {
3800
+ throw new Error("egress budget takes the most residential traffic may cost in a month, in US dollars, as agental egress budget 20, or default.");
3801
+ }
3802
+ body2 = await call2("/v1/egress/budget", { method: "PUT", body: JSON.stringify({ budgetMicros: given === "default" ? null : Math.round(dollars * 1e6) }) });
3803
+ } else {
3804
+ body2 = await call2("/v1/egress", { method: "GET" });
3805
+ }
3806
+ const account3 = body2 && typeof body2 === "object" ? body2 : {};
3807
+ const number = (name) => typeof account3[name] === "number" ? account3[name] : 0;
3808
+ const missing = Array.isArray(account3["missing"]) ? account3["missing"].map(String) : [];
3809
+ const needs = {
3810
+ plan: "a plan of Fleet ($100 a month) or larger: agental billing",
3811
+ card: "a card saved: agental billing card",
3812
+ use_case: 'what your sandboxes will use it for: agental egress use-case "<what for>"'
3813
+ };
3814
+ const refused = account3["refused"];
3815
+ const lines = !account3["offered"] ? ["Residential egress is not offered here."] : [
3816
+ `residential: ${account3["enabled"] ? "on for this account" : refused ? `refused: ${String(refused.reason ?? "")}` : "not yet"}`,
3817
+ ...missing.map((need) => ` needs ${needs[need] ?? need}`),
3818
+ `price: ${money(number("microsPerGb"))} a GB, both directions counted`,
3819
+ `this month: ${money(number("spentMicros"))} of a ${money(number("budgetMicros"))} budget${account3["budgetSet"] ? "" : " (the default)"}`,
3820
+ ...account3["blocked"] && account3["blocked"] !== "refused" ? [`blocked now: ${String(account3["blocked"]).replaceAll("_", " ")}`] : []
3821
+ ];
3822
+ print4(body2, `${lines.join("\n")}
3823
+ `);
3824
+ return;
3825
+ }
3826
+ const path4 = `/v1/sandboxes/${encodeURIComponent(first)}/egress`;
3827
+ const how = `egress takes <sandbox-id>, and then nothing, box, or residential <country> [sticky|rotating], as agental egress ${first} residential de.`;
3828
+ const body = await call2(path4, rest.length === 0 ? { method: "GET" } : { method: "PUT", body: JSON.stringify(egressFrom(rest, how)) });
3829
+ print4(body, `${first}: ${egressInWords(body)}
3830
+ `);
3831
+ }
3447
3832
  function templateSshInWords(ssh2) {
3448
3833
  const keys = ssh2.keys.length === 1 ? "1 key" : `${ssh2.keys.length} keys`;
3449
3834
  return `${ssh2.enabled ? "on" : "off"} as a sandbox is made, ${keys} let in`;
@@ -3513,7 +3898,6 @@ function showTemplate(each) {
3513
3898
  // Said only where some are. An API from before variables could be kept out of the sandbox says nothing of them.
3514
3899
  ...each.hidden && each.hidden.length > 0 ? [`kept out: ${list3(each.hidden)}`] : [],
3515
3900
  `files: ${list3(each.files)}`,
3516
- `repo: ${each.repo ? `${each.repo.name} ${each.repo.url}` : "none"}`,
3517
3901
  // An API from before templates held rules says nothing of one.
3518
3902
  ...each.network ? [`network: ${ruleInWords(each.network)}`] : [],
3519
3903
  // An API from before templates gave SSH says nothing of it.
@@ -3526,10 +3910,10 @@ function showTemplate(each) {
3526
3910
  }
3527
3911
  async function template(positionals, values) {
3528
3912
  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) => {
3913
+ const usage = "template takes create, show, rename, delete, start, network, egress, ssh, ssh-keys, tags, meta, sandboxes, apply, of or put. agental help says how.";
3914
+ const print4 = (body, words2) => {
3531
3915
  process.stdout.write(values.json ? `${JSON.stringify(body, null, 2)}
3532
- ` : words);
3916
+ ` : words2);
3533
3917
  };
3534
3918
  const need = (word, what) => {
3535
3919
  if (!word) {
@@ -3589,20 +3973,6 @@ async function template(positionals, values) {
3589
3973
  `);
3590
3974
  return;
3591
3975
  }
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
3976
  case "network": {
3607
3977
  const found = await templateNamed(need(first, "<template>"));
3608
3978
  const path4 = `/v1/templates/${encodeURIComponent(found.id)}/network`;
@@ -3630,6 +4000,15 @@ ${mode === void 0 ? "" : "A sandbox on it takes this when it next starts, or now
3630
4000
  );
3631
4001
  return;
3632
4002
  }
4003
+ case "egress": {
4004
+ const found = await templateNamed(need(first, "<template>"));
4005
+ const path4 = `/v1/templates/${encodeURIComponent(found.id)}/egress`;
4006
+ const how = `template egress takes <template>, and then nothing, box, or residential <country> [sticky|rotating], as agental template egress ${first ?? ""} residential de.`;
4007
+ const set = await call2(path4, rest.length === 0 ? { method: "GET" } : { method: "PUT", body: JSON.stringify(egressFrom(rest, how)) });
4008
+ print4(set, `${found.name}: ${egressInWords(set)}${rest.length === 0 ? "" : ", for sandboxes made from it from now on"}
4009
+ `);
4010
+ return;
4011
+ }
3633
4012
  case "ssh": {
3634
4013
  const found = await templateNamed(need(first, "<template>"));
3635
4014
  const path4 = `/v1/templates/${encodeURIComponent(found.id)}/ssh`;
@@ -3728,19 +4107,19 @@ async function keptOutNow(path4) {
3728
4107
  }
3729
4108
  async function secrets(positionals, values) {
3730
4109
  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];
4110
+ const words2 = [...positionals];
3732
4111
  let base;
3733
4112
  let sandboxId;
3734
4113
  if (values.template !== void 0) {
3735
4114
  base = `/v1/templates/${encodeURIComponent((await templateNamed(values.template)).id)}/secrets`;
3736
4115
  } else {
3737
- sandboxId = words.shift();
4116
+ sandboxId = words2.shift();
3738
4117
  if (!sandboxId) {
3739
4118
  throw new Error(usage);
3740
4119
  }
3741
4120
  base = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/secrets`;
3742
4121
  }
3743
- const [action, ...args] = words;
4122
+ const [action, ...args] = words2;
3744
4123
  const fileQuery = (path4) => {
3745
4124
  if (!path4) {
3746
4125
  throw new Error(`secrets ${action ?? ""} requires <path>`);
@@ -4015,14 +4394,14 @@ async function ci(positionals, options) {
4015
4394
  return;
4016
4395
  }
4017
4396
  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.
4397
+ 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
4398
  `);
4020
4399
  return;
4021
4400
  }
4022
4401
  for (const item of body.repositories) {
4023
4402
  const busy = item.sandboxes.filter((sandbox) => sandbox.busy).length;
4024
4403
  process.stdout.write(
4025
- `${item.repository} pool ${item.poolMax ?? `${body.defaultPoolMax} (default)`} template ${item.templateId ?? "runs-on (empty)"} ${item.sandboxes.length} sandboxes, ${busy} busy
4404
+ `${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
4405
  `
4027
4406
  );
4028
4407
  }
@@ -4132,7 +4511,7 @@ async function telemetry(positionals, given, asJson) {
4132
4511
  }
4133
4512
  async function billing(positionals, asJson) {
4134
4513
  const [what, amount, ...rest] = positionals;
4135
- const usage = "billing takes nothing, prices, top-up <dollars>, or auto-reload <dollars | off>";
4514
+ const usage = "billing takes nothing, prices, card, top-up <dollars>, or auto-reload <dollars | off>";
4136
4515
  if (rest.length > 0 || what !== "top-up" && what !== "auto-reload" && amount !== void 0) {
4137
4516
  throw new Error(usage);
4138
4517
  }
@@ -4149,6 +4528,17 @@ async function billing(positionals, asJson) {
4149
4528
  print4(body, () => describePrices(body));
4150
4529
  return;
4151
4530
  }
4531
+ case "card": {
4532
+ const body = await asked("/v1/billing/checkout", { method: "POST", body: JSON.stringify({ card: true }) });
4533
+ print4(
4534
+ body,
4535
+ () => body.url ? `Save a card for the free plan at:
4536
+ ${body.url}
4537
+ 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.
4538
+ ` : "Nothing to save.\n"
4539
+ );
4540
+ return;
4541
+ }
4152
4542
  case "top-up": {
4153
4543
  const dollars = Number(amount);
4154
4544
  if (amount === void 0 || !/^\d+$/.test(amount) || dollars <= 0) {
@@ -4196,19 +4586,27 @@ function dayOf(iso) {
4196
4586
  }
4197
4587
  function describeBilling(body) {
4198
4588
  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` : ""}`]);
4589
+ const free = body.free ?? null;
4590
+ if (free && body.plan) {
4591
+ lines.push(["Plan", "free: nothing a month. What runs is paid from your credits, at list price."]);
4592
+ if (!free.cardSaved) {
4593
+ lines.push(["Card", "none saved. Save one, charged nothing, to run sandboxes and be given the free plan's credit: agental billing card."]);
4594
+ } else if (!free.paid) {
4595
+ 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.`]);
4596
+ }
4597
+ lines.push(["Used", `${money(body.usedMicros)} this month at list price${usedParts(body)}`]);
4598
+ lines.push(["Credits left", `${money(body.topUpMicros)}, the free credit and top-ups. They do not expire.`]);
4599
+ if ((body.carriedMicros ?? 0) > 0) {
4600
+ lines.push(["Owed", `${money(body.carriedMicros ?? 0)} an earlier month ran past your credits, taken from the next top-up first`]);
4601
+ }
4602
+ if (body.autoReload) {
4603
+ lines.push(["Auto-reload", describeAutoReload(body.autoReload)]);
4604
+ } else if (body.canAutoReload) {
4605
+ lines.push(["Auto-reload", "off. agental billing auto-reload <dollars> charges your saved card for a top-up when your credits run out."]);
4606
+ }
4607
+ } else if (body.plan && (body.status === "active" || body.status === "past_due")) {
4608
+ lines.push(["Plan", `$${body.plan.dollarsPerMonth} a month, ${body.cancelAtPeriodEnd ? "ends" : "renews"} ${dayOf(body.period?.end ?? null)}`]);
4609
+ lines.push(["Used", `${money(body.usedMicros)} this month at list price${usedParts(body)}`]);
4212
4610
  if ((body.carriedMicros ?? 0) > 0) {
4213
4611
  lines.push(["Carried", `${money(body.carriedMicros ?? 0)} an earlier month could not pay, taken from this month's credits first`]);
4214
4612
  }
@@ -4224,15 +4622,18 @@ function describeBilling(body) {
4224
4622
  lines.push(["Auto-reload", "off. agental billing auto-reload <dollars> charges your saved card for a top-up when your credits run out."]);
4225
4623
  }
4226
4624
  } 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."]);
4625
+ lines.push(["Plan", "none. Plans are not required here yet."]);
4228
4626
  }
4229
4627
  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}`]);
4628
+ lines.push([
4629
+ "Running",
4630
+ 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}`
4631
+ ]);
4231
4632
  }
4232
4633
  if (body.lapse) {
4233
4634
  lines.push([
4234
4635
  "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.`
4636
+ 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
4637
  ]);
4237
4638
  }
4238
4639
  if (body.testMode) {
@@ -4241,6 +4642,14 @@ function describeBilling(body) {
4241
4642
  return lines.map(([label, value]) => `${label.padEnd(14)}${value}
4242
4643
  `).join("");
4243
4644
  }
4645
+ function usedParts(body) {
4646
+ const parts = [
4647
+ ...(body.mailMicros ?? 0) > 0 ? [`${money(body.mailMicros ?? 0)} by email`] : [],
4648
+ ...(body.stoppedMicros ?? 0) > 0 ? [`${money(body.stoppedMicros ?? 0)} by stopped sandboxes`] : [],
4649
+ ...(body.snapshotMicros ?? 0) > 0 ? [`${money(body.snapshotMicros ?? 0)} by snapshots past the free ones`] : []
4650
+ ];
4651
+ return parts.length > 0 ? `, ${parts.join(" and ")} of it` : "";
4652
+ }
4244
4653
  function describeAutoReload(autoReload) {
4245
4654
  if (!autoReload) {
4246
4655
  return "off. Nothing is charged by itself when your credits run out.";
@@ -4257,7 +4666,14 @@ function describePrices(body) {
4257
4666
  for (const tier of body.tiers) {
4258
4667
  const sizes = tier.sizes === void 0 ? "" : tier.sizes.length === body.plans.length ? "every size" : tier.sizes.join(", ");
4259
4668
  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());
4669
+ const price = tier.dollarsPerMonth === 0 ? "Free" : `$${tier.dollarsPerMonth.toLocaleString("en-US")}`;
4670
+ const included = tier.dollarsPerMonth === 0 ? "pay as you go" : `${wholeMoney(tier.includedMicros)} of credits`;
4671
+ out.push(` ${price.padEnd(8)}${included.padEnd(28)}${speed.padEnd(34)}${sizes}`.trimEnd());
4672
+ }
4673
+ if (body.free) {
4674
+ out.push(
4675
+ ` 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.`
4676
+ );
4261
4677
  }
4262
4678
  if (body.tiers.some((tier) => tier.runningUnits !== void 0)) {
4263
4679
  out.push(" Counted in small sandboxes: a medium is 2, a large 4, a tiny half.");
@@ -4518,9 +4934,10 @@ Setting up
4518
4934
 
4519
4935
  Usage
4520
4936
  agental create --name <name> [--template <template>] [--size tiny|small|medium|large] [--region <region>[,<region>...]] [--ttl <time> | --ttl default] [--idempotency-key <key>]
4521
- ${operator ? "[--box <name-or-id>] " : ""}[--allow <address>[,<address>...]] [--max-burst-fee <dollars>]
4937
+ ${operator ? "[--box <name-or-id>] " : ""}[--allow <address>[,<address>...]] [--egress box | residential:<country>[:sticky|rotating]] [--max-burst-fee <dollars>]
4522
4938
  [--ssh-key <public-key-or-file>]... [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]...
4523
- agental list [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--json]
4939
+ agental summary [--where '<words>'] [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--facets <field>[,<field>...]] [--json]
4940
+ agental list [--where '<words>'] [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--sort=<sort>] [--limit <n>] [--json]
4524
4941
  agental tags <sandbox-id> [add|rm <tag>...] [--json]
4525
4942
  agental meta <sandbox-id> [set <key>=<value>... | unset <key>...] [--json]
4526
4943
  agental regions [--json]
@@ -4528,17 +4945,22 @@ Usage
4528
4945
  agental delete <sandbox-id>
4529
4946
  agental start <sandbox-id> [--max-burst-fee <dollars>]
4530
4947
  agental stop <sandbox-id> [--keep-memory]
4948
+ agental stop --where '<words>' [--keep-memory] [--yes] every sandbox the words find, as one job
4949
+ agental start --where '<words>' [--yes]
4950
+ agental delete --where '<words>' [--confirm <count>] asks for the count to be typed
4951
+ agental jobs [list | show <job-id> | cancel <job-id>] [--json]
4531
4952
  agental recover <sandbox-id> [--max-burst-fee <dollars>]
4532
4953
  agental auto-stop <sandbox-id> [--in <time> | --at <date-time> | --never] [--json]
4533
4954
  agental network <sandbox-id> [--allow <address>[,<address>...] | --none] [--json]
4955
+ agental egress <sandbox-id> [box | residential <country> [sticky | rotating]] [--json]
4534
4956
  agental templates [--json]
4535
4957
  agental template create <name> [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]...
4536
4958
  agental template show <template> [--json]
4537
4959
  agental template rename <template> <new-name>
4538
4960
  agental template delete <template>
4539
4961
  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
4962
  agental template network <template> [open | blocked | allowlist <address>...] [--json]
4963
+ agental template egress <template> [box | residential <country> [sticky | rotating]] [--json]
4542
4964
  agental template ssh <template> [on | off] [--json]
4543
4965
  agental template ssh-keys <template> [<public-key-or-file>... | --remove <key-id>] [--json]
4544
4966
  agental template tags <template> [add|rm <tag>...] [--json]
@@ -4560,11 +4982,12 @@ Usage
4560
4982
  agental data-retention [1 | 7 | 30 | 90 | never] [--json]
4561
4983
  agental schedules <sandbox-id> [--json]
4562
4984
  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...]
4985
+ [--if-stopped start|skip] [--keep-memory] [--overlap skip|replace|parallel] [--delete-after] [--json] -- <command> [args...]
4564
4986
  agental schedule show|run|pause|resume|delete <sandbox-id> <schedule> [--json]
4565
4987
  agental schedule runs <sandbox-id> <schedule> [--limit <n>] [--json]
4566
4988
  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...]]
4989
+ [--if-stopped start|skip] [--keep-memory | --disk-only] [--overlap skip|replace|parallel] [--delete-after | --keep-sandbox] [--json]
4990
+ [-- <command> [args...]]
4568
4991
  agental webhooks [--json]
4569
4992
  agental webhook add <url> (--events <kind>[,<kind>...] | --all) [--description <text>] [--json]
4570
4993
  agental webhook show <webhook-id> [--json]
@@ -4584,8 +5007,10 @@ Usage
4584
5007
  agental key show <key> [--json]
4585
5008
  agental key allow-from <key> (<address-or-range>... | --anywhere) [--json]
4586
5009
  agental key delete <key>
4587
- agental billing [prices | top-up <dollars> | auto-reload <dollars | off>] [--json]
5010
+ agental billing [prices | card | top-up <dollars> | auto-reload <dollars | off>] [--json]
4588
5011
  agental burst [budget <dollars> | budget default] [--json]
5012
+ agental egress [budget <dollars> | budget default] [--json]
5013
+ agental egress use-case "<what your sandboxes will use it for>" [--json]
4589
5014
  agental account export [--out <file>]
4590
5015
  agental ssh <sandbox-id> [--tmux] [ssh options] [command...]
4591
5016
  agental scp [scp options] <source>... <target>
@@ -4647,7 +5072,7 @@ sandbox is not restarted, and what is sent to it, commands, files and SSH, is no
4647
5072
  create --allow gives a new sandbox its own from its first boot. These work where GET
4648
5073
  /v1/policy says networkPolicy.
4649
5074
  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,
5075
+ machine), its variables and files, and its outbound rule. Every account has one named default,
4651
5076
  which a sandbox is on when create names none. <template> is a template's id or its name. A
4652
5077
  sandbox's commands get its template's variables and files as they were when it last started,
4653
5078
  with its own on top: secrets <sandbox-id> sets its own, and secrets --template sets a
@@ -4700,8 +5125,10 @@ schedules lists the commands a sandbox runs on a schedule, with when each runs n
4700
5125
  last run ended. schedule add makes one: what comes after -- is its command, run by sh -c, and
4701
5126
  --every 5m, 15m, 1h or 1d, counted from now, never more often than every 5 minutes, or --cron
4702
5127
  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
5128
+ runs it once, as for a check to come back to. With --at, --delete-after deletes the sandbox after
5129
+ the run, if the command exits 0, with a key that may delete it; a run that fails leaves it to be
5130
+ looked at, and update --keep-sandbox takes it back. When a run finds the sandbox stopped it
5131
+ starts it, runs, and stops it again when the command ends, keeping its disk, or its memory too with
4705
5132
  --keep-memory; with --if-stopped skip it runs only while the sandbox runs. A sandbox someone
4706
5133
  started again, sent a command or moved the stop of meanwhile is left running. While a run still
4707
5134
  goes the next is skipped; --overlap replace stops it instead, and parallel runs both. A run gets
@@ -4725,8 +5152,10 @@ tries.
4725
5152
  org lists the organizations you are in, your own and your teams, and marks the one this command
4726
5153
  acts in. A key acts in the organization it was made in, for good, so org use signs in again in
4727
5154
  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
5155
+ had. Your own organization takes members on the $500 or $2000 plan, with you its only owner.
5156
+ org create makes a team besides, with you as its owner and a plan of its own; it has members
5157
+ besides its owners on the $500 or $2000 plan. members lists the organization the command acts
5158
+ in, your own or a team, and members remove takes one
4730
5159
  out by id or email; keys they made in it go on working, as a member's, until they are revoked,
4731
5160
  and --revoke-keys revokes them in the same step. invite asks an email address in, as a member or
4732
5161
  with --role admin; the invitation is open for seven days.
@@ -4754,8 +5183,12 @@ server it is on, which changes when it moves. On a network with IPv6 the API see
4754
5183
  address, so a key limited to IPv4 addresses alone is refused there.
4755
5184
  billing says where your account stands: its plan, what it has used this month at list price,
4756
5185
  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
5186
+ after it and do not expire. The free plan costs nothing a month and pays for what runs from
5187
+ credits alone; it makes tiny and small sandboxes. prices lists the plans with the sizes each
5188
+ makes, the top-ups with their bonus and the hourly price of each size. card prints the address
5189
+ of a page to save a card for the free plan: nothing is charged, and once it is saved the account
5190
+ is given $10 of credit, once. Until it buys a top-up it runs 2 small sandboxes at once, then 20.
5191
+ top-up <dollars> starts buying credits for
4759
5192
  any whole number of dollars from 15 to 100,000, and prints the address of the page to pay on.
4760
5193
  From $200 a top-up adds a bonus, 2% there and growing to 25% at $10,000: $500 adds $537.
4761
5194
  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.4.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
  },