@mifune/agro 0.18.0 → 0.19.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/agro.js +133 -36
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -105,7 +105,7 @@ with `AGRO_EXECUTION_TARGET=local` or `AGRO_EXECUTION_TARGET=docker-compose`.
105
105
  | `agro compose config` | Print the resolved compose configuration. |
106
106
  | `agro harness <list\|install\|status>` | Install and inspect agent CLI harnesses. `install` is the only door: it probes the running sandbox, installs into the persistent home volume, and reports. It reads and writes no `agro.json` field, and needs no rebuild. |
107
107
  | `agro tool <list\|install\|status>` | Install and inspect sandbox tooling that is not an agent CLI. `herdr`, `cloudflared`, `agent-browser`, `microsandbox`, and `tailscale` are `installable`; `gh` and the Docker CLI are `baked-in` and cannot be installed. Nothing installs at boot. A large download is confirmed first, and `--yes` accepts it. |
108
- | `agro gateway <args…>` | Manage a messaging client session (Slack bridge for `pi`/`hermes`). |
108
+ | `agro gateway <args…>` | Manage a messaging client session (Slack bridge for `pi`/`hermes`, or the `openclaw` gateway). |
109
109
  | `agro --version` | Print the CLI version. |
110
110
  | `agro --help` | Show help; every subcommand also accepts `--help`. |
111
111
 
package/dist/agro.js CHANGED
@@ -2251,7 +2251,7 @@ function resolveExecutionTarget(opts) {
2251
2251
  }
2252
2252
 
2253
2253
  // src/lib/version.ts
2254
- var AGRO_VERSION = true ? "0.18.0" : "0.0.0-dev";
2254
+ var AGRO_VERSION = true ? "0.19.0" : "0.0.0-dev";
2255
2255
  var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2256
2256
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2257
2257
  function parseReleaseVersion(value) {
@@ -3145,6 +3145,25 @@ var HARNESS_CATALOG = [
3145
3145
  docsPath: "docs/harnesses/opencode.md",
3146
3146
  kind: "installable"
3147
3147
  },
3148
+ {
3149
+ id: "openclaw",
3150
+ title: "OpenClaw",
3151
+ binary: "openclaw",
3152
+ installArgv: [
3153
+ "npm",
3154
+ "--prefix",
3155
+ HARNESS_PREFIX_TOKEN,
3156
+ "install",
3157
+ "-g",
3158
+ "--allow-scripts=openclaw",
3159
+ "openclaw@2026.9.9"
3160
+ ],
3161
+ installUser: "sandbox",
3162
+ verifyArgv: ["openclaw", "--version"],
3163
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "openclaw"],
3164
+ docsPath: "docs/harnesses/openclaw.md",
3165
+ kind: "installable"
3166
+ },
3148
3167
  {
3149
3168
  id: "grok-build",
3150
3169
  title: "Grok Build",
@@ -3197,7 +3216,7 @@ var HARNESS_CATALOG = [
3197
3216
  installArgv: [
3198
3217
  "bash",
3199
3218
  "-lc",
3200
- `set -o pipefail; curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir "${HARNESS_PREFIX_TOKEN}/bin"`
3219
+ `set -o pipefail; curl -fsSL https://antigravity.google/cli/install.sh | gzip -cdf | bash -s -- --dir "${HARNESS_PREFIX_TOKEN}/bin"`
3201
3220
  ],
3202
3221
  installUser: "sandbox",
3203
3222
  verifyArgv: ["agy", "--version"],
@@ -4740,8 +4759,15 @@ var PROBE_TIMEOUT_MS = 15e3;
4740
4759
  function isReachable(status) {
4741
4760
  return status === "ready" || status === "starting";
4742
4761
  }
4743
- function targetFor(root, run, env) {
4744
- const name = configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
4762
+ function registeredContainerName(cwd) {
4763
+ try {
4764
+ return configuredContainerName(resolveSandboxRoot(cwd === void 0 ? {} : { cwd }));
4765
+ } catch {
4766
+ return void 0;
4767
+ }
4768
+ }
4769
+ function targetFor(root, cwd, run, env) {
4770
+ const name = configuredContainerName(root) ?? registeredContainerName(cwd) ?? DEFAULT_CONTAINER_NAME;
4745
4771
  return resolveExecutionTarget({
4746
4772
  projectRoot: root,
4747
4773
  container: name,
@@ -4821,9 +4847,9 @@ function probeableHost(env, home) {
4821
4847
  if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
4822
4848
  return { root, prefix };
4823
4849
  }
4824
- async function collectStates(root, run, home, env, only) {
4850
+ async function collectStates(root, cwd, run, home, env, only) {
4825
4851
  const entries = only ? [...only] : [...HARNESS_CATALOG];
4826
- const target = targetFor(root, run, env);
4852
+ const target = targetFor(root, cwd, run, env);
4827
4853
  const reachable = isReachable(await resolveTargetStatus(target));
4828
4854
  if (reachable) {
4829
4855
  const states2 = [];
@@ -4876,7 +4902,7 @@ INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} san
4876
4902
  async function runHarnessList(opts, io) {
4877
4903
  const run = opts.run ?? spawnRunner;
4878
4904
  const root = resolveProjectRoot(opts.cwd);
4879
- const collected = await collectStates(root, run, homeOf(opts), opts.env);
4905
+ const collected = await collectStates(root, opts.cwd, run, homeOf(opts), opts.env);
4880
4906
  if (opts.json) {
4881
4907
  io.stdout(`${JSON.stringify(collected.states, null, 2)}
4882
4908
  `);
@@ -4902,7 +4928,7 @@ async function runHarnessStatus(name, opts, io) {
4902
4928
  only = findHarness(name);
4903
4929
  if (!only) return unknownHarness(name, io, opts.bin);
4904
4930
  }
4905
- const collected = await collectStates(root, run, homeOf(opts), opts.env, only ? [only] : void 0);
4931
+ const collected = await collectStates(root, opts.cwd, run, homeOf(opts), opts.env, only ? [only] : void 0);
4906
4932
  if (opts.json) {
4907
4933
  io.stdout(`${JSON.stringify(only ? collected.states[0] : collected.states, null, 2)}
4908
4934
  `);
@@ -4911,7 +4937,7 @@ async function runHarnessStatus(name, opts, io) {
4911
4937
  }
4912
4938
  return 0;
4913
4939
  }
4914
- function hermesTargetRoot(target) {
4940
+ function sandboxTargetRoot(target) {
4915
4941
  return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
4916
4942
  }
4917
4943
  function shellPath(path4) {
@@ -4924,7 +4950,7 @@ function hermesLaunch(io, root) {
4924
4950
  function hermesEnv(root) {
4925
4951
  return { ...agroEnvPair("PROJECT_ROOT", root), HERMES_HOME: `${root}/.hermes` };
4926
4952
  }
4927
- async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
4953
+ async function reconcileHermes(target, io, bin, user, root = sandboxTargetRoot(target)) {
4928
4954
  const result = await target.exec({
4929
4955
  argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
4930
4956
  env: hermesEnv(root),
@@ -4937,7 +4963,7 @@ async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(ta
4937
4963
  }
4938
4964
  return result.exitCode;
4939
4965
  }
4940
- async function validateHermesHome(target, io, bin, user, inheritedHome, root = hermesTargetRoot(target)) {
4966
+ async function validateHermesHome(target, io, bin, user, inheritedHome, root = sandboxTargetRoot(target)) {
4941
4967
  const result = await target.exec({
4942
4968
  argv: remoteControlDirScript(root, "scripts/hermes-workspace.sh", [
4943
4969
  "check",
@@ -4955,7 +4981,7 @@ async function validateHermesHome(target, io, bin, user, inheritedHome, root = h
4955
4981
  }
4956
4982
  return result.exitCode;
4957
4983
  }
4958
- async function configureHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
4984
+ async function configureHermes(target, io, bin, user, root = sandboxTargetRoot(target)) {
4959
4985
  const result = await target.exec({
4960
4986
  argv: remoteControlDirScript(root, "scripts/hermes-workspace.sh", [
4961
4987
  "configure",
@@ -4974,6 +5000,55 @@ async function configureHermes(target, io, bin, user, root = hermesTargetRoot(ta
4974
5000
  }
4975
5001
  return result.exitCode;
4976
5002
  }
5003
+ function openclawStateDir(root) {
5004
+ return `${root}/.openclaw`;
5005
+ }
5006
+ function shellWord(value) {
5007
+ return /^[\w./-]+$/.test(value) ? value : shellPath(value);
5008
+ }
5009
+ function openclawLaunch(io, root) {
5010
+ io.stdout(`Run OpenClaw in the AGRO workspace: OPENCLAW_STATE_DIR=${shellWord(openclawStateDir(root))} openclaw
5011
+ `);
5012
+ }
5013
+ function openclawEnv(root) {
5014
+ return { ...agroEnvPair("PROJECT_ROOT", root), OPENCLAW_STATE_DIR: openclawStateDir(root) };
5015
+ }
5016
+ async function validateOpenclawState(target, io, bin, user, inheritedDir, root = sandboxTargetRoot(target)) {
5017
+ const result = await target.exec({
5018
+ argv: remoteControlDirScript(root, "scripts/openclaw-workspace.sh", [
5019
+ "check",
5020
+ root,
5021
+ openclawStateDir(root),
5022
+ inheritedDir ?? ""
5023
+ ]),
5024
+ env: openclawEnv(root),
5025
+ ...user ? { user } : {},
5026
+ stdio: "capture"
5027
+ });
5028
+ if (result.exitCode !== 0) {
5029
+ io.stderr(result.stderr || `${bin} harness: could not validate the OpenClaw state directory; select OPENCLAW_STATE_DIR=${shellWord(openclawStateDir(root))} before retrying.
5030
+ `);
5031
+ }
5032
+ return result.exitCode;
5033
+ }
5034
+ async function configureOpenclaw(target, io, bin, user, root = sandboxTargetRoot(target)) {
5035
+ const result = await target.exec({
5036
+ argv: remoteControlDirScript(root, "scripts/openclaw-workspace.sh", [
5037
+ "configure",
5038
+ root,
5039
+ openclawStateDir(root),
5040
+ "openclaw"
5041
+ ]),
5042
+ env: openclawEnv(root),
5043
+ ...user ? { user } : {},
5044
+ stdio: "inherit"
5045
+ });
5046
+ if (result.exitCode !== 0) {
5047
+ io.stderr(`${bin} harness: OpenClaw workspace configuration failed (exit ${result.exitCode}); no installation success reported.
5048
+ `);
5049
+ }
5050
+ return result.exitCode;
5051
+ }
4977
5052
  function unreachableReason(status) {
4978
5053
  return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
4979
5054
  }
@@ -5050,10 +5125,15 @@ async function installOnHost(entry, opts, io, run, status) {
5050
5125
  const prefix = hostPrefix(home);
5051
5126
  const target = hostTargetFor(root, prefix, run, env);
5052
5127
  const hermes = entry.id === "hermes";
5128
+ const openclaw = entry.id === "openclaw";
5053
5129
  if (hermes) {
5054
5130
  const code = await validateHermesHome(target, io, bin, void 0, env.HERMES_HOME, root);
5055
5131
  if (code !== 0) return code;
5056
5132
  }
5133
+ if (openclaw) {
5134
+ const code = await validateOpenclawState(target, io, bin, void 0, env.OPENCLAW_STATE_DIR, root);
5135
+ if (code !== 0) return code;
5136
+ }
5057
5137
  const linked = await target.exec({
5058
5138
  argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init"]),
5059
5139
  env: agroEnvPair("PROJECT_ROOT", root),
@@ -5067,7 +5147,7 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5067
5147
  );
5068
5148
  return 1;
5069
5149
  }
5070
- const installEnv = hermes ? hermesEnv(root) : void 0;
5150
+ const installEnv = hermes ? hermesEnv(root) : openclaw ? openclawEnv(root) : void 0;
5071
5151
  if (hermes) {
5072
5152
  const code = await reconcileHermes(target, io, bin, void 0, root);
5073
5153
  if (code !== 0) return code;
@@ -5080,11 +5160,16 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5080
5160
  `);
5081
5161
  return 1;
5082
5162
  }
5083
- if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true && (hasReceipt || !hostBinaryUnderPrefix(entry, prefix))) {
5163
+ const present = await probeInstalled(target, entry, prefix, void 0, installEnv) === true;
5164
+ if (present && !hasReceipt && !hostBinaryUnderPrefix(entry, prefix)) {
5084
5165
  if (hermes) {
5085
5166
  const code = await configureHermes(target, io, bin, void 0, root);
5086
5167
  if (code !== 0) return code;
5087
5168
  }
5169
+ if (openclaw) {
5170
+ const code = await configureOpenclaw(target, io, bin, void 0, root);
5171
+ if (code !== 0) return code;
5172
+ }
5088
5173
  io.stdout(`${entry.id}: already installed (${entry.binary})
5089
5174
  `);
5090
5175
  try {
@@ -5095,9 +5180,10 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5095
5180
  return 1;
5096
5181
  }
5097
5182
  if (hermes) hermesLaunch(io, root);
5183
+ if (openclaw) openclawLaunch(io, root);
5098
5184
  return 0;
5099
5185
  }
5100
- io.stdout(`installing ${entry.title} on the host\u2026
5186
+ io.stdout(`${present && hasReceipt ? "updating" : "installing"} ${entry.title} on the host\u2026
5101
5187
  `);
5102
5188
  const r = await target.exec({
5103
5189
  argv: resolveInstallArgv(entry, prefix),
@@ -5120,6 +5206,10 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5120
5206
  const configured = await configureHermes(target, io, bin, void 0, root);
5121
5207
  if (configured !== 0) return configured;
5122
5208
  }
5209
+ if (openclaw) {
5210
+ const configured = await configureOpenclaw(target, io, bin, void 0, root);
5211
+ if (configured !== 0) return configured;
5212
+ }
5123
5213
  const receipt = {
5124
5214
  prefix,
5125
5215
  binary: entry.binary,
@@ -5150,6 +5240,7 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5150
5240
  `);
5151
5241
  }
5152
5242
  if (hermes) hermesLaunch(io, root);
5243
+ else if (openclaw) openclawLaunch(io, root);
5153
5244
  else io.stdout(`Run ${entry.title} in the AGRO workspace: cd ${root} && ${harnessLaunchCommand(entry)}
5154
5245
  `);
5155
5246
  return 0;
@@ -5248,7 +5339,7 @@ async function runHarnessUninstall(name, opts, io) {
5248
5339
  const root = resolveProjectRoot(opts.cwd);
5249
5340
  const entry = findHarness(name);
5250
5341
  if (!entry) return unknownHarness(name, io, opts.bin);
5251
- const target = targetFor(root, run, opts.env);
5342
+ const target = targetFor(root, opts.cwd, run, opts.env);
5252
5343
  if (!isReachable(await resolveTargetStatus(target))) {
5253
5344
  return await uninstallOnHost(entry, opts, io, run);
5254
5345
  }
@@ -5272,40 +5363,41 @@ async function runHarnessInstall(name, opts, io) {
5272
5363
  const env = opts.env ?? process.env;
5273
5364
  const projectRoot = agroEnvValue(env, "PROJECT_ROOT");
5274
5365
  const root = resolveProjectRoot(
5275
- name === "hermes" && runningInsideSandbox(env) && projectRoot !== void 0 ? projectRoot : opts.cwd
5366
+ (name === "hermes" || name === "openclaw") && runningInsideSandbox(env) && projectRoot !== void 0 ? projectRoot : opts.cwd
5276
5367
  );
5277
5368
  const entry = findHarness(name);
5278
5369
  if (!entry) return unknownHarness(name, io, opts.bin);
5279
- const target = targetFor(root, run, opts.env);
5370
+ const target = targetFor(root, opts.cwd, run, opts.env);
5280
5371
  const status = await resolveTargetStatus(target);
5281
5372
  if (!isReachable(status)) {
5282
5373
  return await installOnHost(entry, opts, io, run, status);
5283
5374
  }
5284
5375
  const hermes = entry.id === "hermes";
5285
- const installEnv = hermes ? hermesEnv(hermesTargetRoot(target)) : void 0;
5376
+ const openclaw = entry.id === "openclaw";
5377
+ const targetRoot = sandboxTargetRoot(target);
5378
+ const installEnv = hermes ? hermesEnv(targetRoot) : openclaw ? openclawEnv(targetRoot) : void 0;
5286
5379
  if (hermes) {
5287
5380
  const checked = await validateHermesHome(target, io, opts.bin, "sandbox", env.HERMES_HOME);
5288
5381
  if (checked !== 0) return checked;
5289
5382
  const code = await reconcileHermes(target, io, opts.bin, "sandbox");
5290
5383
  if (code !== 0) return code;
5291
5384
  }
5292
- const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
5293
- if (already === true && await sandboxMarkerExists(target, entry)) {
5294
- if (hermes) {
5295
- const code = await configureHermes(target, io, opts.bin, "sandbox");
5296
- if (code !== 0) return code;
5297
- }
5298
- io.stdout(`${entry.id}: already installed (${entry.binary})
5299
- `);
5300
- if (hermes) hermesLaunch(io, hermesTargetRoot(target));
5301
- return 0;
5385
+ if (openclaw) {
5386
+ const checked = await validateOpenclawState(target, io, opts.bin, "sandbox", env.OPENCLAW_STATE_DIR);
5387
+ if (checked !== 0) return checked;
5302
5388
  }
5389
+ const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
5303
5390
  if (already === null) {
5304
5391
  io.stderr("docker is required to install into the running sandbox but was not found on PATH\n");
5305
5392
  return 1;
5306
5393
  }
5307
- io.stdout(`installing ${entry.title} into the sandbox\u2026
5394
+ if (already && await sandboxMarkerExists(target, entry)) {
5395
+ io.stdout(`updating ${entry.title} in the sandbox\u2026
5396
+ `);
5397
+ } else {
5398
+ io.stdout(`installing ${entry.title} into the sandbox\u2026
5308
5399
  `);
5400
+ }
5309
5401
  const r = await target.exec({
5310
5402
  argv: resolveInstallArgv(entry, SANDBOX_HARNESS_PREFIX),
5311
5403
  user: entry.installUser,
@@ -5328,6 +5420,10 @@ async function runHarnessInstall(name, opts, io) {
5328
5420
  const configured = await configureHermes(target, io, opts.bin, "sandbox");
5329
5421
  if (configured !== 0) return configured;
5330
5422
  }
5423
+ if (openclaw) {
5424
+ const configured = await configureOpenclaw(target, io, opts.bin, "sandbox");
5425
+ if (configured !== 0) return configured;
5426
+ }
5331
5427
  const marker = sandboxMarkerPath(entry);
5332
5428
  const marked = await target.exec({
5333
5429
  argv: ["sh", "-c", `mkdir -p "$(dirname "$1")" && : > "$1"`, "sh", marker],
@@ -5341,7 +5437,8 @@ async function runHarnessInstall(name, opts, io) {
5341
5437
  }
5342
5438
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
5343
5439
  `);
5344
- if (hermes) hermesLaunch(io, hermesTargetRoot(target));
5440
+ if (hermes) hermesLaunch(io, targetRoot);
5441
+ if (openclaw) openclawLaunch(io, targetRoot);
5345
5442
  return 0;
5346
5443
  }
5347
5444
 
@@ -6605,7 +6702,7 @@ Usage:
6605
6702
  ${bin} workspace <args...> Create and list host AGRO workspaces (create|list)
6606
6703
  ${bin} harness <args...> Install and inspect agent CLI harnesses
6607
6704
  ${bin} tool <args...> Install and inspect sandbox tooling
6608
- ${bin} gateway <args...> Manage a messaging client session (pi|hermes)
6705
+ ${bin} gateway <args...> Manage a messaging client session (pi|hermes|openclaw)
6609
6706
  ${bin} --version Print version
6610
6707
  ${bin} --help Show this help
6611
6708
 
@@ -6813,10 +6910,10 @@ function printGatewayHelp(bin = AGRO_PRODUCT.bin) {
6813
6910
  process.stdout.write(`${bin} gateway \u2014 Manage a messaging client session (Slack bridge)
6814
6911
 
6815
6912
  Usage:
6816
- ${bin} gateway <pi|hermes> [--attach] start the client session (--attach after)
6817
- ${bin} gateway <pi|hermes> --restart restart the session
6818
- ${bin} gateway <pi|hermes> --stop stop the session
6819
- ${bin} gateway status show both sessions
6913
+ ${bin} gateway <pi|hermes|openclaw> [--attach] start the client session (--attach after)
6914
+ ${bin} gateway <pi|hermes|openclaw> --restart restart the session
6915
+ ${bin} gateway <pi|hermes|openclaw> --stop stop the session
6916
+ ${bin} gateway status show all sessions
6820
6917
 
6821
6918
  Only a LEADING --help/-h is intercepted here; everything else passes through
6822
6919
  verbatim to the vendored ${stateNames(bin).controlDir}/scripts/gateway.sh with ${stateNames(bin).envPrefix}PROJECT_ROOT set to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",
@@ -34,7 +34,7 @@
34
34
  "prepublishOnly": "npm run typecheck:build"
35
35
  },
36
36
  "devDependencies": {
37
- "@types/node": "^22.0.0",
37
+ "@types/node": "^24.0.0",
38
38
  "esbuild": "^0.28.1",
39
39
  "typescript": "^5.6.0"
40
40
  }