@mifune/agro 0.21.0 → 0.23.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/agro.js +493 -88
  2. package/package.json +1 -1
package/dist/agro.js CHANGED
@@ -1424,10 +1424,16 @@ async function askSecretPiped(question) {
1424
1424
  process.on("SIGINT", onSigint);
1425
1425
  try {
1426
1426
  return await new Promise((resolve14) => {
1427
- rl.question(` ${question} `, (answer) => {
1427
+ let settled = false;
1428
+ const settle = (answer) => {
1429
+ if (settled) return;
1430
+ settled = true;
1428
1431
  process.stdout.write("\n");
1429
1432
  resolve14(answer.trim());
1430
- });
1433
+ };
1434
+ rl.question(` ${question} `, settle);
1435
+ rl.once("line", settle);
1436
+ rl.once("close", () => settle(rl.line));
1431
1437
  });
1432
1438
  } finally {
1433
1439
  process.removeListener("SIGINT", onSigint);
@@ -1471,7 +1477,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as read
1471
1477
  import { dirname as dirname4, join as join4, resolve as resolve5, sep as sep2 } from "node:path";
1472
1478
 
1473
1479
  // agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.yml
1474
- var docker_compose_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-sandbox-${SANDBOX_NAME:-agro}}\n pull_policy: ${AGRO_PULL_POLICY:-missing}\n container_name: ${SANDBOX_NAME:-agro}\n build:\n context: ${AGRO_REPO_DIR:-..}\n dockerfile: .devcontainer/Dockerfile\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n - ${AGRO_REPO_DIR:-..}:/home/sandbox/harness\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n - TYPESAFE_API_KEY=${TYPESAFE_API_KEY:-}\n stdin_open: true\n tty: true\n init: true\n entrypoint: ["/usr/local/bin/sandbox-main.sh"]\n stop_signal: SIGTERM\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
1480
+ var docker_compose_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-sandbox-${SANDBOX_NAME:-agro}}\n pull_policy: ${AGRO_PULL_POLICY:-missing}\n container_name: ${SANDBOX_NAME:-agro}\n build:\n context: ${AGRO_REPO_DIR:-..}\n dockerfile: .devcontainer/Dockerfile\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n - ${AGRO_REPO_DIR:-..}:/home/sandbox/harness\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n - TYPESAFE_API_KEY=${TYPESAFE_API_KEY:-}\n - XAI_API_KEY=${XAI_API_KEY:-}\n stdin_open: true\n tty: true\n init: true\n entrypoint: ["/usr/local/bin/sandbox-main.sh"]\n stop_signal: SIGTERM\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
1475
1481
 
1476
1482
  // agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.image-only.yml
1477
1483
  var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-ghcr.io/mifunedev/agro:latest}\n pull_policy: ${AGRO_PULL_POLICY:-always}\n container_name: ${SANDBOX_NAME:-agro}\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n - TYPESAFE_API_KEY=${TYPESAFE_API_KEY:-}\n stdin_open: true\n tty: true\n init: true\n entrypoint: ["/usr/local/bin/sandbox-main.sh"]\n stop_signal: SIGTERM\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
@@ -2034,7 +2040,15 @@ var SECRET_KEYS = [
2034
2040
  "PI_SLACK_BOT_TOKEN",
2035
2041
  "LANGFUSE_PUBLIC_KEY",
2036
2042
  "LANGFUSE_SECRET_KEY",
2037
- "TYPESAFE_API_KEY"
2043
+ "TYPESAFE_API_KEY",
2044
+ "AGRO_CONSOLE_URL",
2045
+ "AGRO_CONSOLE_TOKEN"
2046
+ ];
2047
+ var COMPOSE_SECRET_KEYS = [
2048
+ "GH_TOKEN",
2049
+ "SANDBOX_PASSWORD",
2050
+ "TYPESAFE_API_KEY",
2051
+ "XAI_API_KEY"
2038
2052
  ];
2039
2053
  var ALLOWED = new Set(SECRET_KEYS);
2040
2054
  function secretsFilePath(root) {
@@ -2099,7 +2113,7 @@ import { basename as basename6, join as join6, sep as sep3 } from "node:path";
2099
2113
  import { basename as basename4, resolve as resolve7 } from "node:path";
2100
2114
 
2101
2115
  // src/lib/version.ts
2102
- var AGRO_VERSION = true ? "0.21.0" : "0.0.0-dev";
2116
+ var AGRO_VERSION = true ? "0.23.0" : "0.0.0-dev";
2103
2117
  var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2104
2118
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2105
2119
  function parseReleaseVersion(value) {
@@ -3423,6 +3437,14 @@ Set it with \`${opts.bin} config set ${key}\` instead.
3423
3437
 
3424
3438
  Keys:
3425
3439
  ${secretKeyList()}
3440
+ `
3441
+ );
3442
+ return 1;
3443
+ }
3444
+ if (opts.sandbox !== void 0 && !COMPOSE_SECRET_KEYS.includes(key)) {
3445
+ io.stderr(
3446
+ `${opts.bin} secret set: ${key} does not reach the container from --sandbox ${opts.sandbox} \u2014 Compose passes only ${COMPOSE_SECRET_KEYS.join(", ")}.
3447
+ Run \`${opts.bin} secret set ${key}\` in the project checkout instead.
3426
3448
  `
3427
3449
  );
3428
3450
  return 1;
@@ -3489,6 +3511,9 @@ import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
3489
3511
  import { dirname as dirname7, join as join11, relative } from "node:path";
3490
3512
 
3491
3513
  // src/lib/tracing/harness-writers.ts
3514
+ import { join as join10 } from "node:path";
3515
+
3516
+ // src/lib/tracing/providers/langfuse.ts
3492
3517
  import { join as join9 } from "node:path";
3493
3518
 
3494
3519
  // src/lib/tracing/writer.ts
@@ -3531,16 +3556,66 @@ function isPlainObject(value) {
3531
3556
  return typeof value === "object" && value !== null && !Array.isArray(value);
3532
3557
  }
3533
3558
 
3559
+ // src/lib/tracing/providers/langfuse.ts
3560
+ var LANGFUSE_PLUGIN_MAX_CHARS = 1e5;
3561
+ var LANGFUSE_FRAGMENT_KEYS = [
3562
+ "LANGFUSE_PUBLIC_KEY",
3563
+ "LANGFUSE_SECRET_KEY",
3564
+ "LANGFUSE_BASE_URL",
3565
+ "LANGFUSE_TRACING_ENVIRONMENT",
3566
+ "PI_LANGFUSE_MAX_CHARS"
3567
+ ];
3568
+ function langfuseFragmentPath(home) {
3569
+ return join9(home, ".config", "agro", "langfuse.env");
3570
+ }
3571
+ function loadLangfuseCredentials(root) {
3572
+ const publicKey = readSecret(root, "LANGFUSE_PUBLIC_KEY");
3573
+ const secretKey = readSecret(root, "LANGFUSE_SECRET_KEY");
3574
+ if (publicKey !== void 0 && secretKey !== void 0) return { publicKey, secretKey };
3575
+ const missing = [
3576
+ publicKey === void 0 ? "LANGFUSE_PUBLIC_KEY" : void 0,
3577
+ secretKey === void 0 ? "LANGFUSE_SECRET_KEY" : void 0
3578
+ ].filter((key) => key !== void 0);
3579
+ throw new Error(
3580
+ `${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not set in .env \u2014 run \`${activeBin()} secret set ${missing[0]} <value>\` first`
3581
+ );
3582
+ }
3583
+ var BARE_SAFE_VALUE = /^[A-Za-z0-9_.:/@%+=,-]+$/;
3584
+ var UNCARRIABLE_IN_VALUE = /[\u0000-\u001f\u007f']/;
3585
+ function renderLangfuseFragment(input) {
3586
+ const values = {
3587
+ LANGFUSE_PUBLIC_KEY: input.publicKey,
3588
+ LANGFUSE_SECRET_KEY: input.secretKey,
3589
+ LANGFUSE_BASE_URL: input.baseUrl,
3590
+ LANGFUSE_TRACING_ENVIRONMENT: input.environment,
3591
+ PI_LANGFUSE_MAX_CHARS: String(LANGFUSE_PLUGIN_MAX_CHARS)
3592
+ };
3593
+ return LANGFUSE_FRAGMENT_KEYS.map((key) => `${key}=${quoteFragmentValue(key, values[key])}
3594
+ `).join("");
3595
+ }
3596
+ function writeLangfuseFragment(home, input) {
3597
+ return writeTextIfChanged(langfuseFragmentPath(home), renderLangfuseFragment(input), PRIVATE_FILE_MODE);
3598
+ }
3599
+ function quoteFragmentValue(key, value) {
3600
+ if (value === "") throw new Error(`${key} must not be empty`);
3601
+ if (UNCARRIABLE_IN_VALUE.test(value)) {
3602
+ throw new Error(
3603
+ `${key} must not contain a newline, a control character, or a single quote \u2014 zsh reads the fragment and does not carry that value safely`
3604
+ );
3605
+ }
3606
+ return BARE_SAFE_VALUE.test(value) ? value : `'${value}'`;
3607
+ }
3608
+
3534
3609
  // src/lib/tracing/harness-writers.ts
3535
3610
  var CODEX_TRACING_TAGS = ["codex"];
3536
3611
  function claudeCodeSettingsPath(home) {
3537
- return join9(home, ".claude", "settings.json");
3612
+ return join10(home, ".claude", "settings.json");
3538
3613
  }
3539
3614
  function piTracingConfigPath(home) {
3540
- return join9(home, ".pi", "agent", "langfuse.json");
3615
+ return join10(home, ".pi", "agent", "langfuse.json");
3541
3616
  }
3542
3617
  function codexTracingConfigPath(home) {
3543
- return join9(home, ".codex", "langfuse.json");
3618
+ return join10(home, ".codex", "langfuse.json");
3544
3619
  }
3545
3620
  var claudeCodeTracingWriter = (home, settings) => {
3546
3621
  const path4 = claudeCodeSettingsPath(home);
@@ -3554,7 +3629,8 @@ var claudeCodeTracingWriter = (home, settings) => {
3554
3629
  env: {
3555
3630
  ...env,
3556
3631
  LANGFUSE_BASE_URL: settings.baseUrl,
3557
- LANGFUSE_TRACING_ENVIRONMENT: settings.environment
3632
+ LANGFUSE_TRACING_ENVIRONMENT: settings.environment,
3633
+ CC_LANGFUSE_MAX_CHARS: String(LANGFUSE_PLUGIN_MAX_CHARS)
3558
3634
  }
3559
3635
  };
3560
3636
  return writeTextIfChanged(path4, renderJson(next));
@@ -3783,54 +3859,6 @@ function harnessBinPath(prefix) {
3783
3859
  return `${prefix}/bin`;
3784
3860
  }
3785
3861
 
3786
- // src/lib/tracing/providers/langfuse.ts
3787
- import { join as join10 } from "node:path";
3788
- var LANGFUSE_FRAGMENT_KEYS = [
3789
- "LANGFUSE_PUBLIC_KEY",
3790
- "LANGFUSE_SECRET_KEY",
3791
- "LANGFUSE_BASE_URL",
3792
- "LANGFUSE_TRACING_ENVIRONMENT"
3793
- ];
3794
- function langfuseFragmentPath(home) {
3795
- return join10(home, ".config", "agro", "langfuse.env");
3796
- }
3797
- function loadLangfuseCredentials(root) {
3798
- const publicKey = readSecret(root, "LANGFUSE_PUBLIC_KEY");
3799
- const secretKey = readSecret(root, "LANGFUSE_SECRET_KEY");
3800
- if (publicKey !== void 0 && secretKey !== void 0) return { publicKey, secretKey };
3801
- const missing = [
3802
- publicKey === void 0 ? "LANGFUSE_PUBLIC_KEY" : void 0,
3803
- secretKey === void 0 ? "LANGFUSE_SECRET_KEY" : void 0
3804
- ].filter((key) => key !== void 0);
3805
- throw new Error(
3806
- `${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not set in .env \u2014 run \`${activeBin()} secret set ${missing[0]} <value>\` first`
3807
- );
3808
- }
3809
- var BARE_SAFE_VALUE = /^[A-Za-z0-9_.:/@%+=,-]+$/;
3810
- var UNCARRIABLE_IN_VALUE = /[\u0000-\u001f\u007f']/;
3811
- function renderLangfuseFragment(input) {
3812
- const values = {
3813
- LANGFUSE_PUBLIC_KEY: input.publicKey,
3814
- LANGFUSE_SECRET_KEY: input.secretKey,
3815
- LANGFUSE_BASE_URL: input.baseUrl,
3816
- LANGFUSE_TRACING_ENVIRONMENT: input.environment
3817
- };
3818
- return LANGFUSE_FRAGMENT_KEYS.map((key) => `${key}=${quoteFragmentValue(key, values[key])}
3819
- `).join("");
3820
- }
3821
- function writeLangfuseFragment(home, input) {
3822
- return writeTextIfChanged(langfuseFragmentPath(home), renderLangfuseFragment(input), PRIVATE_FILE_MODE);
3823
- }
3824
- function quoteFragmentValue(key, value) {
3825
- if (value === "") throw new Error(`${key} must not be empty`);
3826
- if (UNCARRIABLE_IN_VALUE.test(value)) {
3827
- throw new Error(
3828
- `${key} must not contain a newline, a control character, or a single quote \u2014 zsh reads the fragment and does not carry that value safely`
3829
- );
3830
- }
3831
- return BARE_SAFE_VALUE.test(value) ? value : `'${value}'`;
3832
- }
3833
-
3834
3862
  // src/commands/langfuse.ts
3835
3863
  var DEFAULT_LANGFUSE_BASE_URL = "https://cloud.langfuse.com";
3836
3864
  var LANGFUSE_FRAGMENT_LABEL = "~/.config/agro/langfuse.env";
@@ -5095,6 +5123,357 @@ function runEgress(verb, opts, io) {
5095
5123
  });
5096
5124
  }
5097
5125
 
5126
+ // src/command-table.ts
5127
+ var CONFIG_VERBS = ["show", "set", "repo"];
5128
+ var CONFIG_INTEGRATIONS = ["langfuse"];
5129
+ var SECRET_VERBS = ["set", "get", "list"];
5130
+ var LANGFUSE_VERBS = ["apply", "status", "disable"];
5131
+ var SANDBOX_SUBCOMMANDS = ["install", "list", "upgrade"];
5132
+ var WORKSPACE_SUBCOMMANDS = ["create", "list"];
5133
+ var HARNESS_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
5134
+ var TOOL_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
5135
+ var EGRESS_VERBS = ["log", "reload"];
5136
+ var NODE_VERBS = ["create", "list", "delete"];
5137
+ function isOneOf(values, value) {
5138
+ return value !== void 0 && values.includes(value);
5139
+ }
5140
+ var AGRO_COMMANDS = Object.freeze({
5141
+ config: [...CONFIG_VERBS, ...CONFIG_INTEGRATIONS],
5142
+ secret: SECRET_VERBS,
5143
+ langfuse: LANGFUSE_VERBS,
5144
+ update: null,
5145
+ "self-upgrade": null,
5146
+ vendor: null,
5147
+ sandbox: SANDBOX_SUBCOMMANDS,
5148
+ shell: null,
5149
+ compose: null,
5150
+ workspace: WORKSPACE_SUBCOMMANDS,
5151
+ harness: HARNESS_SUBCOMMANDS,
5152
+ tool: TOOL_SUBCOMMANDS,
5153
+ gateway: null,
5154
+ egress: EGRESS_VERBS,
5155
+ node: NODE_VERBS,
5156
+ ...Object.fromEntries(composeVerbs().map((verb) => [verb, null]))
5157
+ });
5158
+
5159
+ // src/commands/node.ts
5160
+ var DEFAULT_CONSOLE_URL = "https://console.mifune.dev";
5161
+ var CONSOLE_CONFIG_MISSING = "set AGRO_CONSOLE_TOKEN, or run agro secret set AGRO_CONSOLE_TOKEN";
5162
+ var NODE_READY_TIMEOUT_MS = 15 * 60 * 1e3;
5163
+ var NODE_FAST_POLL_INTERVAL_MS = 1e3;
5164
+ var NODE_FAST_POLL_WINDOW_MS = 30 * 1e3;
5165
+ var NODE_POLL_INTERVAL_MS = 5e3;
5166
+ var FREE_TIER_SPEC = "n4";
5167
+ var FAILURE_STATUSES = /* @__PURE__ */ new Set(["failed", "destroy_requested", "destroying", "destroyed", "retired"]);
5168
+ function resolveConsoleConfig(env, readSecret2) {
5169
+ const lookup = (key) => {
5170
+ const fromEnv = env[key]?.trim();
5171
+ if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
5172
+ const fromSecret = readSecret2(key)?.trim();
5173
+ return fromSecret === void 0 || fromSecret === "" ? void 0 : fromSecret;
5174
+ };
5175
+ const token = lookup("AGRO_CONSOLE_TOKEN");
5176
+ if (token === void 0) return void 0;
5177
+ const url = lookup("AGRO_CONSOLE_URL") ?? DEFAULT_CONSOLE_URL;
5178
+ return { url: url.replace(/\/+$/, ""), token };
5179
+ }
5180
+ function readProjectSecret(key) {
5181
+ try {
5182
+ return readSecret(resolveProjectRoot(), key);
5183
+ } catch {
5184
+ return void 0;
5185
+ }
5186
+ }
5187
+ function nodePollDelayMs(elapsedMs) {
5188
+ return elapsedMs < NODE_FAST_POLL_WINDOW_MS ? NODE_FAST_POLL_INTERVAL_MS : NODE_POLL_INTERVAL_MS;
5189
+ }
5190
+ function defaultDeps2() {
5191
+ return {
5192
+ fetch: (input, init) => globalThis.fetch(input, init),
5193
+ env: process.env,
5194
+ readSecret: readProjectSecret,
5195
+ sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
5196
+ now: () => Date.now(),
5197
+ pollDelayMs: nodePollDelayMs,
5198
+ timeoutMs: NODE_READY_TIMEOUT_MS,
5199
+ confirm: (question) => confirm(question, false)
5200
+ };
5201
+ }
5202
+ function nodeHelp(bin) {
5203
+ return `${bin} node \u2014 Create, list, and delete agro-console sandbox nodes (remote VMs)
5204
+
5205
+ Usage:
5206
+ ${bin} node create --spec <spec> --ssh-key <uuid> [--ssh-key <uuid>] [--name <name>] [--free-tier] [--json]
5207
+ ${bin} node list [--json]
5208
+ ${bin} node delete <id> [--yes]
5209
+
5210
+ \`create\` waits up to 15 minutes for the node to reach \`running\`, then prints
5211
+ its SSH command. \`--free-tier\` requests the console free tier. The spec
5212
+ defaults to n4. The verbs read AGRO_CONSOLE_URL and AGRO_CONSOLE_TOKEN from the
5213
+ environment, then from the project .env (\`${bin} secret set <KEY>\`).
5214
+ AGRO_CONSOLE_URL defaults to ${DEFAULT_CONSOLE_URL}.
5215
+ `;
5216
+ }
5217
+ function asRecord(value) {
5218
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5219
+ }
5220
+ function asText(value) {
5221
+ if (typeof value === "string" && value !== "") return value;
5222
+ if (typeof value === "number") return String(value);
5223
+ return null;
5224
+ }
5225
+ function toNode(value) {
5226
+ const outer = asRecord(value);
5227
+ const record = asRecord(outer?.node) ?? outer;
5228
+ const id = asText(record?.id);
5229
+ if (record === void 0 || id === null) return void 0;
5230
+ return {
5231
+ id,
5232
+ name: asText(record.name),
5233
+ profile: asText(record.profile),
5234
+ status: asText(record.status) ?? "unknown",
5235
+ ipv4: asText(record.ipv4),
5236
+ ssh: asText(record.ssh)
5237
+ };
5238
+ }
5239
+ function sshCommand(node) {
5240
+ if (node.ssh !== null) return node.ssh;
5241
+ return node.ipv4 === null ? null : `ssh sandbox@${node.ipv4}`;
5242
+ }
5243
+ function errorDetail(status, body) {
5244
+ const record = asRecord(body);
5245
+ const nested = asRecord(record?.error);
5246
+ const code = asText(record?.code) ?? asText(nested?.code) ?? "no code";
5247
+ const message = asText(record?.error) ?? asText(nested?.message) ?? asText(record?.message);
5248
+ return `HTTP ${status} ${code}${message === null ? "" : `: ${message}`}`;
5249
+ }
5250
+ async function callApi(deps, config, method, path4, payload) {
5251
+ let response;
5252
+ try {
5253
+ response = await deps.fetch(`${config.url}${path4}`, {
5254
+ method,
5255
+ headers: {
5256
+ Authorization: `Bearer ${config.token}`,
5257
+ Accept: "application/json",
5258
+ ...payload === void 0 ? {} : { "Content-Type": "application/json" }
5259
+ },
5260
+ ...payload === void 0 ? {} : { body: JSON.stringify(payload) }
5261
+ });
5262
+ } catch (error) {
5263
+ return { ok: false, message: `request failed: ${error instanceof Error ? error.message : String(error)}` };
5264
+ }
5265
+ const text = await response.text();
5266
+ let body = null;
5267
+ try {
5268
+ body = text === "" ? null : JSON.parse(text);
5269
+ } catch {
5270
+ body = null;
5271
+ }
5272
+ if (response.status >= 400) {
5273
+ return { ok: false, status: response.status, message: errorDetail(response.status, body) };
5274
+ }
5275
+ return { ok: true, status: response.status, body };
5276
+ }
5277
+ function parseCreate(tokens) {
5278
+ const args = { sshKeyIds: [], freeTier: false, json: false };
5279
+ for (let i = 0; i < tokens.length; i += 1) {
5280
+ const token = tokens[i];
5281
+ if (token === "--json") {
5282
+ args.json = true;
5283
+ continue;
5284
+ }
5285
+ if (token === "--free-tier") {
5286
+ args.freeTier = true;
5287
+ continue;
5288
+ }
5289
+ const [flag, inline] = token.startsWith("--") && token.includes("=") ? [token.slice(0, token.indexOf("=")), token.slice(token.indexOf("=") + 1)] : [token, void 0];
5290
+ if (flag !== "--spec" && flag !== "--ssh-key" && flag !== "--name") {
5291
+ return { ok: false, error: token.startsWith("-") ? `unknown flag "${token}"` : `unexpected argument "${token}"` };
5292
+ }
5293
+ const value = inline ?? tokens[++i];
5294
+ if (value === void 0 || value === "") return { ok: false, error: `${flag} requires a value` };
5295
+ if (flag === "--spec") args.spec = value;
5296
+ else if (flag === "--name") args.name = value;
5297
+ else args.sshKeyIds.push(value);
5298
+ }
5299
+ if (args.spec === void 0 && args.freeTier) args.spec = FREE_TIER_SPEC;
5300
+ if (args.spec === void 0) return { ok: false, error: "--spec is required" };
5301
+ if (args.sshKeyIds.length === 0) return { ok: false, error: "at least one --ssh-key is required" };
5302
+ return { ok: true, args };
5303
+ }
5304
+ function printCreateResult(io, node, json) {
5305
+ const ssh = node.status === "running" ? sshCommand(node) : null;
5306
+ if (json) {
5307
+ io.stdout(`${JSON.stringify({ id: node.id, status: node.status, ssh })}
5308
+ `);
5309
+ return;
5310
+ }
5311
+ io.stdout(`id: ${node.id}
5312
+ status: ${node.status}
5313
+ ${ssh === null ? "" : `${ssh}
5314
+ `}`);
5315
+ }
5316
+ async function runCreate(tokens, io, deps, config, bin) {
5317
+ const prefix = `${bin} node create`;
5318
+ const parsed = parseCreate(tokens);
5319
+ if (!parsed.ok) {
5320
+ io.stderr(`${prefix}: ${parsed.error}
5321
+ `);
5322
+ return 1;
5323
+ }
5324
+ const { spec, sshKeyIds, name, freeTier, json } = parsed.args;
5325
+ const requestedAt = deps.now();
5326
+ const created = await callApi(deps, config, "POST", "/api/nodes", {
5327
+ profile: "sandbox",
5328
+ spec,
5329
+ sshKeyIds,
5330
+ ...name === void 0 ? {} : { name },
5331
+ ...freeTier ? { freeTier: true } : {}
5332
+ });
5333
+ if (!created.ok) {
5334
+ io.stderr(`${prefix}: ${created.message}
5335
+ `);
5336
+ return 1;
5337
+ }
5338
+ let node = toNode(created.body);
5339
+ if (node === void 0) {
5340
+ io.stderr(`${prefix}: the console response has no node id
5341
+ `);
5342
+ return 1;
5343
+ }
5344
+ io.stderr(`${prefix}: node ${node.id} ${node.status}
5345
+ `);
5346
+ const deadline = deps.now() + deps.timeoutMs;
5347
+ while (node.status !== "running" && !FAILURE_STATUSES.has(node.status)) {
5348
+ const remaining = deadline - deps.now();
5349
+ if (remaining <= 0) {
5350
+ printCreateResult(io, node, json);
5351
+ io.stderr(`${prefix}: node ${node.id} is not running after 15 minutes \u2014 last status ${node.status}
5352
+ `);
5353
+ return 1;
5354
+ }
5355
+ await deps.sleep(Math.min(deps.pollDelayMs(deps.now() - requestedAt), remaining));
5356
+ const polled = await callApi(deps, config, "GET", `/api/nodes/${encodeURIComponent(node.id)}`);
5357
+ if (!polled.ok) {
5358
+ io.stderr(`${prefix}: node ${node.id}: ${polled.message}
5359
+ `);
5360
+ return 1;
5361
+ }
5362
+ const previous = node.status;
5363
+ node = toNode(polled.body) ?? { ...node, status: "unknown" };
5364
+ if (node.status !== previous) io.stderr(`${prefix}: node ${node.id} ${node.status}
5365
+ `);
5366
+ }
5367
+ printCreateResult(io, node, json);
5368
+ if (node.status !== "running") {
5369
+ io.stderr(`${prefix}: node ${node.id} failed \u2014 last status ${node.status}
5370
+ `);
5371
+ return 1;
5372
+ }
5373
+ return 0;
5374
+ }
5375
+ function toNodeList(body) {
5376
+ const record = asRecord(body);
5377
+ const items = Array.isArray(body) ? body : record?.nodes ?? record?.data ?? record?.items;
5378
+ if (!Array.isArray(items)) return [];
5379
+ return items.flatMap((item) => toNode(item) ?? []);
5380
+ }
5381
+ async function runList(tokens, io, deps, config, bin) {
5382
+ const prefix = `${bin} node list`;
5383
+ const extra = tokens.find((token) => token !== "--json");
5384
+ if (extra !== void 0) {
5385
+ io.stderr(`${prefix}: ${extra.startsWith("-") ? "unknown flag" : "unexpected argument"} "${extra}"
5386
+ `);
5387
+ return 1;
5388
+ }
5389
+ const listed = await callApi(deps, config, "GET", "/api/nodes");
5390
+ if (!listed.ok) {
5391
+ io.stderr(`${prefix}: ${listed.message}
5392
+ `);
5393
+ return 1;
5394
+ }
5395
+ const rows = toNodeList(listed.body).map(({ id, name, profile, status, ipv4 }) => ({ id, name, profile, status, ipv4 }));
5396
+ if (tokens.includes("--json")) {
5397
+ io.stdout(`${JSON.stringify(rows)}
5398
+ `);
5399
+ return 0;
5400
+ }
5401
+ if (rows.length === 0) {
5402
+ io.stdout("no nodes\n");
5403
+ return 0;
5404
+ }
5405
+ const table = [
5406
+ ["ID", "NAME", "PROFILE", "STATUS", "IPV4"],
5407
+ ...rows.map((row) => [row.id, row.name ?? "-", row.profile ?? "-", row.status, row.ipv4 ?? "-"])
5408
+ ];
5409
+ const widths = table[0].map((_, column) => Math.max(...table.map((cells) => cells[column].length)));
5410
+ for (const cells of table) {
5411
+ io.stdout(`${cells.map((cell3, column) => cell3.padEnd(widths[column])).join(" ").trimEnd()}
5412
+ `);
5413
+ }
5414
+ return 0;
5415
+ }
5416
+ async function runDelete(tokens, io, deps, config, bin) {
5417
+ const prefix = `${bin} node delete`;
5418
+ const flag = tokens.find((token) => token.startsWith("-") && token !== "--yes" && token !== "-y");
5419
+ if (flag !== void 0) {
5420
+ io.stderr(`${prefix}: unknown flag "${flag}"
5421
+ `);
5422
+ return 1;
5423
+ }
5424
+ const ids = tokens.filter((token) => !token.startsWith("-"));
5425
+ if (ids.length !== 1) {
5426
+ io.stderr(`${prefix}: takes exactly one node id
5427
+ `);
5428
+ return 1;
5429
+ }
5430
+ const [id] = ids;
5431
+ const yes = tokens.includes("--yes") || tokens.includes("-y");
5432
+ if (!yes && !await deps.confirm(`Destroy console node ${id}?`)) {
5433
+ io.stderr(`${prefix}: aborted \u2014 node ${id} unchanged
5434
+ `);
5435
+ return 1;
5436
+ }
5437
+ const deleted = await callApi(deps, config, "DELETE", `/api/nodes/${encodeURIComponent(id)}`);
5438
+ if (!deleted.ok) {
5439
+ io.stderr(
5440
+ deleted.status === 403 ? `${prefix}: destroy needs the admin role in agro-console
5441
+ ` : `${prefix}: ${deleted.message}
5442
+ `
5443
+ );
5444
+ return 1;
5445
+ }
5446
+ io.stdout(`deleted ${id}
5447
+ `);
5448
+ return 0;
5449
+ }
5450
+ async function runNode(args, io, overrides = {}, bin = "agro") {
5451
+ const [verb, ...rest] = args;
5452
+ if (verb === void 0 || verb === "--help" || verb === "-h" || verb === "help") {
5453
+ io.stdout(nodeHelp(bin));
5454
+ return verb === void 0 ? 1 : 0;
5455
+ }
5456
+ if (!isOneOf(NODE_VERBS, verb)) {
5457
+ io.stderr(`${bin} node: unknown subcommand "${verb}" \u2014 expected ${NODE_VERBS.join(", ")}
5458
+ `);
5459
+ return 1;
5460
+ }
5461
+ if (rest.some((token) => token === "--help" || token === "-h")) {
5462
+ io.stdout(nodeHelp(bin));
5463
+ return 0;
5464
+ }
5465
+ const deps = { ...defaultDeps2(), ...overrides };
5466
+ const config = resolveConsoleConfig(deps.env, deps.readSecret);
5467
+ if (config === void 0) {
5468
+ io.stderr(`${bin} node ${verb}: ${CONSOLE_CONFIG_MISSING}
5469
+ `);
5470
+ return 1;
5471
+ }
5472
+ if (verb === "list") return runList(rest, io, deps, config, bin);
5473
+ if (verb === "delete") return runDelete(rest, io, deps, config, bin);
5474
+ return runCreate(rest, io, deps, config, bin);
5475
+ }
5476
+
5098
5477
  // src/commands/harness.ts
5099
5478
  import { existsSync as existsSync20 } from "node:fs";
5100
5479
  import { homedir as homedir3 } from "node:os";
@@ -6453,6 +6832,54 @@ install -d -m 0700 "$HOME/.tailscale"`
6453
6832
  ]),
6454
6833
  docsPath: TOOLS_DOC
6455
6834
  }),
6835
+ Object.freeze({
6836
+ id: "ffmpeg",
6837
+ title: "FFmpeg + FFprobe",
6838
+ kind: "installable",
6839
+ binary: "ffmpeg",
6840
+ verifyArgv: Object.freeze(["bash", "-lc", "command -v ffmpeg >/dev/null && command -v ffprobe >/dev/null"]),
6841
+ versionArgv: Object.freeze(["ffmpeg", "-version"]),
6842
+ installArgv: Object.freeze([
6843
+ "bash",
6844
+ "-lc",
6845
+ [
6846
+ "set -e",
6847
+ "release=b6.1.1",
6848
+ 'case "$(dpkg --print-architecture)" in',
6849
+ " amd64) arch=x64; ffmpeg_sha=bfe8a8fc511530457b528c48d77b5737527b504a3797a9bc4866aeca69c2dffa; ffprobe_sha=25d9b6ccb05e3d9de9e04e31e2506d8dd7f9f0418981965ac6df12e8d3afd067 ;;",
6850
+ " arm64) arch=arm64; ffmpeg_sha=754a678672298bc68156adff58aa7385a592c2b30b1d0ae8750c45c915c4bac0; ffprobe_sha=2ab6aba60ee84412dff9188720703376cb4e7aaf7e0b5e43aa8249f2acae5bf8 ;;",
6851
+ ' *) echo "no pinned FFmpeg build for $(dpkg --print-architecture)" >&2; exit 1 ;;',
6852
+ "esac",
6853
+ 'prefix="${NPM_USER_PREFIX:-$HOME/.local}"',
6854
+ 'tmp="$(mktemp -d)"',
6855
+ `trap 'rm -rf "$tmp"' EXIT`,
6856
+ "for name in ffmpeg ffprobe; do",
6857
+ ' curl -fsSL "https://github.com/eugeneware/ffmpeg-static/releases/download/$release/$name-linux-$arch.gz" -o "$tmp/$name.gz"',
6858
+ "done",
6859
+ 'echo "$ffmpeg_sha $tmp/ffmpeg.gz" | sha256sum -c -',
6860
+ 'echo "$ffprobe_sha $tmp/ffprobe.gz" | sha256sum -c -',
6861
+ 'install -d "$prefix/bin"',
6862
+ "for name in ffmpeg ffprobe; do",
6863
+ ' gzip -dc "$tmp/$name.gz" > "$tmp/$name"',
6864
+ ' install -m 0755 "$tmp/$name" "$prefix/bin/$name"',
6865
+ "done",
6866
+ '"$prefix/bin/ffprobe" -version >/dev/null',
6867
+ 'if ! "$prefix/bin/ffmpeg" -hide_banner -encoders | grep -qw libx264; then',
6868
+ ' echo "$prefix/bin/ffmpeg lists no libx264 encoder" >&2',
6869
+ " exit 1",
6870
+ "fi"
6871
+ ].join("\n")
6872
+ ]),
6873
+ installUser: "sandbox",
6874
+ hostCapable: true,
6875
+ uninstallArgv: Object.freeze([
6876
+ "rm",
6877
+ "-rf",
6878
+ `${HARNESS_PREFIX_TOKEN}/bin/ffmpeg`,
6879
+ `${HARNESS_PREFIX_TOKEN}/bin/ffprobe`
6880
+ ]),
6881
+ docsPath: TOOLS_DOC
6882
+ }),
6456
6883
  Object.freeze({
6457
6884
  id: "docker-engine",
6458
6885
  title: "Docker Engine + Compose",
@@ -7246,37 +7673,6 @@ function fetchRemoteSource(opts) {
7246
7673
  return opts.destDir;
7247
7674
  }
7248
7675
 
7249
- // src/command-table.ts
7250
- var CONFIG_VERBS = ["show", "set", "repo"];
7251
- var CONFIG_INTEGRATIONS = ["langfuse"];
7252
- var SECRET_VERBS = ["set", "get", "list"];
7253
- var LANGFUSE_VERBS = ["apply", "status", "disable"];
7254
- var SANDBOX_SUBCOMMANDS = ["install", "list", "upgrade"];
7255
- var WORKSPACE_SUBCOMMANDS = ["create", "list"];
7256
- var HARNESS_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
7257
- var TOOL_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
7258
- var EGRESS_VERBS = ["log", "reload"];
7259
- function isOneOf(values, value) {
7260
- return value !== void 0 && values.includes(value);
7261
- }
7262
- var AGRO_COMMANDS = Object.freeze({
7263
- config: [...CONFIG_VERBS, ...CONFIG_INTEGRATIONS],
7264
- secret: SECRET_VERBS,
7265
- langfuse: LANGFUSE_VERBS,
7266
- update: null,
7267
- "self-upgrade": null,
7268
- vendor: null,
7269
- sandbox: SANDBOX_SUBCOMMANDS,
7270
- shell: null,
7271
- compose: null,
7272
- workspace: WORKSPACE_SUBCOMMANDS,
7273
- harness: HARNESS_SUBCOMMANDS,
7274
- tool: TOOL_SUBCOMMANDS,
7275
- gateway: null,
7276
- egress: EGRESS_VERBS,
7277
- ...Object.fromEntries(composeVerbs().map((verb) => [verb, null]))
7278
- });
7279
-
7280
7676
  // src/cli.ts
7281
7677
  var DEFAULT_SOURCE_CONTROL_DIR = resolve13(
7282
7678
  dirname10(fileURLToPath(import.meta.url)),
@@ -7323,6 +7719,7 @@ Usage:
7323
7719
  ${bin} compose config Print the resolved docker compose configuration
7324
7720
  ${bin} egress <args...> Read the egress proxy log or reload its allowlist (log|reload)
7325
7721
  ${bin} workspace <args...> Create and list host AGRO workspaces (create|list)
7722
+ ${bin} node <args...> Manage remote agro-console sandbox nodes (create|list|delete)
7326
7723
  ${bin} harness <args...> Install and inspect agent CLI harnesses
7327
7724
  ${bin} tool <args...> Install and inspect sandbox tooling
7328
7725
  ${bin} gateway <args...> Manage a messaging client session (pi|hermes|openclaw)
@@ -8420,6 +8817,14 @@ async function main(argv) {
8420
8817
  }
8421
8818
  return runEgress(a.verb, a.name === void 0 ? { bin } : { bin, name: a.name }, lifecycleIo());
8422
8819
  }
8820
+ if (first === "node") {
8821
+ return await runNode(
8822
+ argv.slice(1),
8823
+ { stdout: (s) => process.stdout.write(s), stderr: (s) => process.stderr.write(s) },
8824
+ {},
8825
+ bin
8826
+ );
8827
+ }
8423
8828
  if (first === "workspace") {
8424
8829
  const parsed = parseWorkspaceArgs(argv.slice(1), bin);
8425
8830
  if (!parsed.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "AGRO CLI — create and manage sandboxes for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/mifunedev/agro#readme",