@clawops/cli 2.1.3 → 2.2.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 (51) hide show
  1. package/README.md +120 -2
  2. package/dist/agent-activity-25FAWLAI.js +16 -0
  3. package/dist/{apply-JARPVA5Y.js → apply-F5SQ2ZOS.js} +7 -7
  4. package/dist/backup-flows-NDP5JTR3.js +29 -0
  5. package/dist/{bootstrap-3PBVY7MA.js → bootstrap-32TZXVDF.js} +3 -3
  6. package/dist/bootstrap.sh.tmpl +4 -0
  7. package/dist/chunk-27335SIH.js +51 -0
  8. package/dist/{chunk-E4NJSJ54.js → chunk-4PY6PN4G.js} +51 -7
  9. package/dist/{chunk-PTCVFLZ3.js → chunk-66R52R7K.js} +7 -11
  10. package/dist/{chunk-7T4CIT7C.js → chunk-AX65JMDZ.js} +15 -15
  11. package/dist/{chunk-TYHTISI5.js → chunk-CQOYUWR7.js} +610 -104
  12. package/dist/chunk-E6BRQK67.js +289 -0
  13. package/dist/chunk-LV7UO5CG.js +82 -0
  14. package/dist/{chunk-GIEVAGEK.js → chunk-N5ZYUTZS.js} +16 -1
  15. package/dist/{chunk-CPCO6UJK.js → chunk-OUQVKIIA.js} +368 -30
  16. package/dist/{chunk-XFN3YDZE.js → chunk-PASAVVNS.js} +1 -1
  17. package/dist/chunk-PCSX6Y3U.js +297 -0
  18. package/dist/chunk-POYS3EVQ.js +14 -0
  19. package/dist/chunk-SX75L2T3.js +46 -0
  20. package/dist/{chunk-2Z7J3ERQ.js → chunk-TYNMOYVM.js} +1 -1
  21. package/dist/{chunk-2I7KCMXZ.js → chunk-UAJJEDCJ.js} +2 -2
  22. package/dist/{chunk-NUDIHZEH.js → chunk-WPZIJONN.js} +14 -12
  23. package/dist/{chunk-CHNSL3TC.js → chunk-YPVWF7IA.js} +1 -1
  24. package/dist/{chunk-3OLUJPCB.js → chunk-Z3XLWZWF.js} +1 -1
  25. package/dist/cli.js +303 -698
  26. package/dist/conn-M2V5HBPJ.js +9 -0
  27. package/dist/{context-WOKSKAIA.js → context-EHSLWKSH.js} +2 -2
  28. package/dist/{docker-3PDO4TS7.js → docker-IKM5GRMC.js} +2 -2
  29. package/dist/{flows-HGJIA6EL.js → flows-6B2I6TRW.js} +5 -5
  30. package/dist/{generate-NE54BATS.js → generate-4YYNW3IU.js} +5 -4
  31. package/dist/{harden-RUOUA5BV.js → harden-UHAOVQMZ.js} +3 -3
  32. package/dist/{mcp-wire-FIO5DFBY.js → mcp-wire-BXF4DJFT.js} +21 -14
  33. package/dist/migrate-flow-24XA4GZL.js +25 -0
  34. package/dist/{package-MF5LPITL.js → package-HYGHRX35.js} +3 -1
  35. package/dist/{pool-PIIKJ4L2.js → pool-NT4XBSUT.js} +2 -2
  36. package/dist/{privileged-JFA6LLTV.js → privileged-5Y6O4H7Z.js} +1 -1
  37. package/dist/{ready-KDNRBNSU.js → ready-IJEUBFVX.js} +2 -2
  38. package/dist/{remote-config-3HZLUWEA.js → remote-config-OZYBMBSK.js} +3 -3
  39. package/dist/{runtime-X3KW5TCP.js → runtime-VW53TU2W.js} +5 -1
  40. package/dist/{server-OXWHBTXK.js → server-E3YA32ND.js} +21 -14
  41. package/dist/ssh-HOM7GF3M.js +13 -0
  42. package/dist/stack-delete-NTIGKWUF.js +13 -0
  43. package/dist/{tailscale-cutover-EKAQSDXP.js → tailscale-cutover-V42HO5UM.js} +1 -1
  44. package/dist/{validate-JXKN5WD6.js → validate-DZE6LFEB.js} +1 -1
  45. package/dist/{wait-WO72TRRQ.js → wait-DOUNNJNN.js} +2 -2
  46. package/package.json +3 -1
  47. package/spec/mcp-tools.yaml +213 -7
  48. package/dist/logs-5A7YYA53.js +0 -15
  49. package/dist/ssh-VAI5DU5M.js +0 -11
  50. package/dist/upgrade-ULD4KFYC.js +0 -88
  51. /package/dist/{chunk-NUL5F6YV.js → chunk-SOZ62ZXW.js} +0 -0
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@clawops/cli)](https://www.npmjs.com/package/@clawops/cli)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@clawops/cli)](https://www.npmjs.com/package/@clawops/cli)
5
5
  [![clawops MCP server – quality and maintenance score on Glama](https://glama.ai/mcp/servers/dfridkin/clawops/badges/score.svg)](https://glama.ai/mcp/servers/dfridkin/clawops)
6
+ [![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/dfridkin/clawops)
6
7
 
7
8
  MCP-native infrastructure ops for OpenClaw, with read-only mode, destructive-action confirmation, and audit logs built in.
8
9
 
@@ -193,6 +194,27 @@ Either way, pass the arguments. `mcp serve` is what speaks the protocol, and an
193
194
  is one that still reads clearly a year later. clawops does not strand a client that omits them:
194
195
  run with no command at all and a pipe on stdin — how every MCP client starts a server — and it
195
196
  starts `mcp serve`, saying so on stderr. Typed at a terminal, `clawops` still prints help.
197
+
198
+ **Pass cloud credentials in `env`.** A client starts the server with the environment its config
199
+ gives it, not your shell's, so an `AWS_PROFILE` exported in a terminal does not reach it, and
200
+ every tool that reads a stack fails with "Could not read the state of stack". Name the profile,
201
+ or the variables your state backend needs, in the server's `env` block:
202
+
203
+ ```json
204
+ {
205
+ "mcpServers": {
206
+ "clawops": {
207
+ "command": "npx",
208
+ "args": ["-y", "@clawops/cli", "mcp", "serve"],
209
+ "env": { "AWS_PROFILE": "my-profile" }
210
+ }
211
+ }
212
+ }
213
+ ```
214
+
215
+ It holds a profile name, not a key: the credentials themselves stay where the cloud CLI keeps
216
+ them (R6).
217
+
196
218
  Config file locations:
197
219
 
198
220
  | App | Path |
@@ -254,7 +276,7 @@ clawops down --yes # Destroy local-provider stack
254
276
  | `config` | Get/set remote OpenClaw config values (`--dry-run` shows would-write JSON) |
255
277
  | `agents` | List OpenClaw agents, or stream one agent's logs |
256
278
  | `gateway` | Restart the OpenClaw gateway service |
257
- | `backup` | Create and restore OpenClaw state backups (`restore` expands into a staging directory, never in place) |
279
+ | `backup` | Create and restore OpenClaw state backups (`restore` expands into a staging directory, never in place; `--activate` swaps it in and rolls back if the gateway does not come up) |
258
280
  | `stacks` | List named stacks and their state |
259
281
  | `doctor` | Check the local machine; with `--stack`, the deployment's health too; with `--provider`, one cloud's credentials and account setup whether or not a stack exists; with `--instance-type`, account checks ask about that size rather than the provider default. `--json` for the report. Exits 1 on any failure |
260
282
  | `secret` | Manage secrets: `list`, `set`, `delete`, `rotate`, `audit` |
@@ -354,6 +376,10 @@ Do not bind to a non-loopback address without additional authentication controls
354
376
  | `clawops_stacks_list` | admin | List all stacks and their state |
355
377
  | `clawops_config_get` | cli | Read a remote config value |
356
378
  | `clawops_agents_list` | cli | List running agents |
379
+ | `clawops_agents_logs` | cli | One agent's recent activity, paginated |
380
+ | `clawops_gateway_status` | cli | Whether the gateway container is running, and its image |
381
+ | `clawops_secret_list` | cli | List stored secrets by name and status (never values) |
382
+ | `clawops_secret_audit` | cli | Report missing secrets and unresolvable `$secret:` references |
357
383
  | `clawops_up` | cli | Provision or update a stack |
358
384
  | `clawops_destroy` | cli | Destroy a stack (elicits confirmation) |
359
385
  | `clawops_apply` | cli | Apply a plan file |
@@ -362,6 +388,12 @@ Do not bind to a non-loopback address without additional authentication controls
362
388
  | `clawops_config_unset` | cli | Remove a remote config key |
363
389
  | `clawops_config_validate` | cli | Validate the deployed config against the OpenClaw schema |
364
390
  | `clawops_gateway_restart` | cli | Restart the gateway (elicits confirmation) |
391
+ | `clawops_gateway_update` | cli | Move the gateway to a concrete OpenClaw version (elicits confirmation) |
392
+ | `clawops_backup_create` | cli | Back up gateway state to a local archive |
393
+ | `clawops_backup_restore` | cli | Restore a backup to staging, or with `activate` into service (elicits confirmation) |
394
+ | `clawops_migrate` | cli | Migrate a 1.x deployment onto the 2.0 runtime (elicits confirmation) |
395
+ | `clawops_stacks_delete` | admin | Forget a stack in config; does not destroy infrastructure (elicits confirmation) |
396
+ | `clawops_secret_delete` | cli | Delete a stored secret (elicits confirmation) |
365
397
  | `clawops_harden` | cli | Apply hardening modules; join or leave a tailnet (elicits confirmation) |
366
398
  | `clawops_init` | cli | Register a stack and write `~/.clawops/config.json` (no cloud resources) |
367
399
  | `clawops_workflow_deploy_app` | workflow | End-to-end deploy: plan → confirm → apply → status |
@@ -544,9 +576,10 @@ pnpm dev doctor # verify toolchain
544
576
  ```bash
545
577
  pnpm dev # run CLI from src/ via tsx
546
578
  pnpm build # tsup → dist/
547
- pnpm test # vitest (1897 tests, ~13s)
579
+ pnpm test # vitest (2090 tests, ~13s)
548
580
  pnpm test:changed # vitest --changed (fast edit loop)
549
581
  pnpm test:integration # Docker-based SSH integration tests
582
+ pnpm test:e2e:local # builds, then bootstrap, the host-touching MCP tools and a 1.x migration, in systemd containers
550
583
  pnpm typecheck # tsc --noEmit
551
584
  pnpm lint # eslint src/ tests/ scripts/ (--max-warnings=0)
552
585
  pnpm gen:schemas # regenerate src/providers/types.ts + src/mcp/tools/_generated.ts
@@ -602,6 +635,80 @@ Use `pnpm changeset` to record a release note before merging a `feat` or `fix`.
602
635
 
603
636
  ---
604
637
 
638
+ ## What's new in 2.2
639
+
640
+ Recovery is a flag, agents can reach every command they need, and SSH failures say what failed.
641
+
642
+ ### Restore puts a backup into service
643
+
644
+ - `clawops backup restore --file <archive> --activate` swaps the restored state in, restarts
645
+ the gateway on it, and checks that it answers. Without `--activate`, a restore still
646
+ verifies and expands into a staging directory and touches nothing live.
647
+ - The state it replaces is kept, not deleted. If the gateway does not come up, clawops puts the
648
+ old state back and restarts again; the state that failed is kept as
649
+ `.failed-restore-<timestamp>`, as evidence.
650
+ - The restored state lands on the host, beside the directory it replaces. Before, it stayed in
651
+ the container's `/tmp`, where the next `gateway restart` destroyed it.
652
+ - Free space is checked before an archive is expanded.
653
+ - `clawops backup --help` no longer says restore is unavailable. It has worked since 2.0.
654
+
655
+ ### Every command an agent needs is an MCP tool
656
+
657
+ - Ten new tools bring the catalog to 30: backup create and restore (with `activate`), gateway
658
+ status and update, one agent's logs, migrate, stacks delete, and secret list, audit and
659
+ delete. Each calls the same code as its command, so both refuse the same things in the same
660
+ words.
661
+ - `clawops ssh` and `clawops secret set` / `rotate` stay CLI-only on purpose: a root shell or a
662
+ secret value does not belong in an agent's hands or its transcript. The secret tools never
663
+ return values.
664
+
665
+ ### SSH failures name the cause
666
+
667
+ - A timeout says nothing came back, which on a clawops stack usually means the security group no
668
+ longer admits your address. The message prints the re-plan and apply that fix it.
669
+ - A refused connection says the host is up and sshd is not. A rejected key names the key clawops
670
+ offered. A name that does not resolve, and a handshake with no algorithm in common, each say
671
+ what they are. ssh2's own words are kept in every message.
672
+ - `clawops doctor --stack <name>` prints the same advice on its own line.
673
+
674
+ ### Fixes found by running every tool against a real host
675
+
676
+ The new tools were driven end to end over MCP against a systemd host, a 1.x deployment made by
677
+ clawops 1.7.9, and AWS. That found defects the unit tests could not, several in the CLI too:
678
+
679
+ - **`clawops gateway update` works.** Every update since the upgrade gate shipped refused before
680
+ touching anything, because the state snapshot was given a host path inside a container that
681
+ cannot create it.
682
+ - **Local-provider hosts no longer race systemd.** The gateway runs there as the `openclaw` unit
683
+ with `Restart=always`, which undid a restart, an update, a migration or a restore about five
684
+ seconds after clawops made it. clawops now changes the gateway through the unit.
685
+ - **`clawops gateway`, `agents`, `config` and `logs` work on local stacks.** Each failed with "The
686
+ local provider does not use Pulumi stacks" while its MCP tool worked.
687
+ - **`clawops migrate` refuses a stack already on 2.x**, which it would have given a fresh minimal
688
+ config; **asks before it runs**, as `--yes` always claimed; and **pulls the 2.x image before
689
+ stopping 1.x**, so the downtime is a restart rather than a multi-gigabyte download.
690
+ - **A secret name can no longer reach outside the secrets directory.** `clawops secret delete
691
+ ../config.json` deleted the clawops config.
692
+ - **`clawops_gateway_restart` keeps a gateway published on every interface** where it was,
693
+ instead of moving it to loopback.
694
+ - **A stack whose state cannot be read says why**, instead of Pulumi's `code: -2`, and names the
695
+ usual cause for an MCP server: credentials missing from its client config's `env` block.
696
+
697
+ ### Other fixes
698
+
699
+ - Streaming Docker commands, such as a backup upload or a log follow, no longer reuse a sudo
700
+ decision learned from an unrelated command. On AWS, where the login user is not in the `docker`
701
+ group, a backup upload failed with "permission denied" after the backup had been taken.
702
+ - The backup and upgrade-rollback guides describe `--activate` instead of a manual procedure, and
703
+ CI follows every in-repo Markdown link and anchor (`pnpm verify:docs`).
704
+ - The local provider's bootstrap runs for real in its e2e suite, against a container with systemd
705
+ as PID 1, instead of being mocked. The same suite now drives the host-touching MCP tools and a
706
+ 1.x migration (`pnpm test:e2e:local`, nightly and on PRs labelled `e2e`), and
707
+ `E2E_MCP=1 pnpm test:cloud <cloud>` backs up and restores over MCP on a real cloud host.
708
+ - clawops.fyi carries structured data, an `/llms.txt`, a FAQ and a comparison page.
709
+
710
+ ---
711
+
605
712
  ## What's new in 2.1
606
713
 
607
714
  Private networking, hardening on every cloud, and two fixes to commands that could not start.
@@ -667,6 +774,17 @@ Private networking, hardening on every cloud, and two fixes to commands that cou
667
774
  does not create its destination. `pnpm verify:docker` builds it and speaks MCP to the running
668
775
  container, both through its entrypoint and as a bare binary, in CI.
669
776
 
777
+ ### 2.1.2 and 2.1.3
778
+
779
+ - Every MCP tool sends its description, and every one of its parameters documents itself. Clients
780
+ had been seeing 19 tools with a name, a schema and no description.
781
+ - `clawops_init` registers a stack over MCP, so a client can bootstrap from nothing. It writes the
782
+ config and an SSH key and provisions nothing.
783
+ - A client that cannot show a confirmation is told to ask the user and call again with
784
+ `yes: true`, instead of getting a `Client does not support form elicitation` crash.
785
+ - The Docker image carries `ssh-keygen` and a CA bundle; without them it could not make a key or
786
+ reach any HTTPS endpoint.
787
+
670
788
  ---
671
789
 
672
790
  ## What's new in 2.0.1
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_AGENT_LOG_LIMIT,
4
+ parseAgentActivity,
5
+ readAgentActivity,
6
+ readAgentActivityRaw
7
+ } from "./chunk-27335SIH.js";
8
+ import "./chunk-SOZ62ZXW.js";
9
+ import "./chunk-WPZIJONN.js";
10
+ import "./chunk-EI7CQQ7D.js";
11
+ export {
12
+ DEFAULT_AGENT_LOG_LIMIT,
13
+ parseAgentActivity,
14
+ readAgentActivity,
15
+ readAgentActivityRaw
16
+ };
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  applyPlan
4
- } from "./chunk-7T4CIT7C.js";
4
+ } from "./chunk-AX65JMDZ.js";
5
5
  import "./chunk-BOPSG2LI.js";
6
- import "./chunk-6ZFIFDBJ.js";
7
- import "./chunk-2I7KCMXZ.js";
8
- import "./chunk-XFN3YDZE.js";
6
+ import "./chunk-UAJJEDCJ.js";
7
+ import "./chunk-PASAVVNS.js";
9
8
  import "./chunk-55UMO37O.js";
10
9
  import "./chunk-AEEMRFZV.js";
11
10
  import "./chunk-AVERNLUY.js";
12
- import "./chunk-NUDIHZEH.js";
13
- import "./chunk-CHNSL3TC.js";
11
+ import "./chunk-6ZFIFDBJ.js";
12
+ import "./chunk-WPZIJONN.js";
13
+ import "./chunk-YPVWF7IA.js";
14
14
  import "./chunk-JW5A7E3X.js";
15
15
  import "./chunk-VUFMZEIM.js";
16
- import "./chunk-GIEVAGEK.js";
16
+ import "./chunk-N5ZYUTZS.js";
17
17
  import "./chunk-VOEJYRIK.js";
18
18
  import "./chunk-EI7CQQ7D.js";
19
19
  import "./chunk-FEK7VJAY.js";
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ACTIVATED,
4
+ ARCHIVE_IS_A_CREDENTIAL,
5
+ PLUGINS_NOT_CARRIED,
6
+ activateStaged,
7
+ activatedLines,
8
+ activationFailureDetail,
9
+ createBackup,
10
+ defaultBackupFilename,
11
+ nothingChanged,
12
+ stageRestore
13
+ } from "./chunk-PCSX6Y3U.js";
14
+ import "./chunk-WPZIJONN.js";
15
+ import "./chunk-N5ZYUTZS.js";
16
+ import "./chunk-VOEJYRIK.js";
17
+ import "./chunk-EI7CQQ7D.js";
18
+ export {
19
+ ACTIVATED,
20
+ ARCHIVE_IS_A_CREDENTIAL,
21
+ PLUGINS_NOT_CARRIED,
22
+ activateStaged,
23
+ activatedLines,
24
+ activationFailureDetail,
25
+ createBackup,
26
+ defaultBackupFilename,
27
+ nothingChanged,
28
+ stageRestore
29
+ };
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  acquireSession
4
- } from "./chunk-2Z7J3ERQ.js";
5
- import "./chunk-E4NJSJ54.js";
4
+ } from "./chunk-TYNMOYVM.js";
5
+ import "./chunk-4PY6PN4G.js";
6
6
  import "./chunk-MC5ATCXS.js";
7
7
  import {
8
8
  gatewayRunArgs,
9
9
  gatewayRunCommand
10
- } from "./chunk-GIEVAGEK.js";
10
+ } from "./chunk-N5ZYUTZS.js";
11
11
  import {
12
12
  GATEWAY_PORT
13
13
  } from "./chunk-VOEJYRIK.js";
@@ -211,6 +211,10 @@ ExecStop=/usr/bin/docker stop openclaw
211
211
  [Install]
212
212
  WantedBy=multi-user.target
213
213
  UNIT
214
+ # A restart, update or migration since the last bootstrap pins its run command in a drop-in
215
+ # (see gatewayRunCommand). This unit is the new source of truth; a stale drop-in would
216
+ # override its ExecStart and quietly keep the old image.
217
+ rm -f /etc/systemd/system/openclaw.service.d/clawops-run.conf
214
218
  systemctl daemon-reload
215
219
  systemctl enable openclaw
216
220
  systemctl restart openclaw
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ agentAuditCommand
4
+ } from "./chunk-SOZ62ZXW.js";
5
+ import {
6
+ execPrivileged
7
+ } from "./chunk-WPZIJONN.js";
8
+ import {
9
+ err,
10
+ ok
11
+ } from "./chunk-EI7CQQ7D.js";
12
+
13
+ // src/openclaw/agent-activity.ts
14
+ var DEFAULT_AGENT_LOG_LIMIT = 50;
15
+ async function readAgentActivityRaw(session, opts, signal) {
16
+ const result = await execPrivileged(
17
+ session,
18
+ agentAuditCommand({
19
+ agentId: opts.agentId,
20
+ limit: opts.limit ?? DEFAULT_AGENT_LOG_LIMIT,
21
+ ...opts.cursor ? { cursor: opts.cursor } : {}
22
+ }),
23
+ signal
24
+ );
25
+ if (result.code !== 0) {
26
+ const why = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`;
27
+ return err(`Cannot read activity for "${opts.agentId}": ${why}`);
28
+ }
29
+ return ok(result.stdout);
30
+ }
31
+ function parseAgentActivity(agentId, stdout) {
32
+ let page;
33
+ try {
34
+ page = JSON.parse(stdout.trim() || "{}");
35
+ } catch {
36
+ return err(`Cannot read activity for "${agentId}": unexpected output: ${stdout.trim().slice(0, 200)}`);
37
+ }
38
+ return ok({ records: page.records ?? [], ...page.cursor ? { cursor: page.cursor } : {} });
39
+ }
40
+ async function readAgentActivity(session, opts, signal) {
41
+ const raw = await readAgentActivityRaw(session, opts, signal);
42
+ if (!raw.ok) return raw;
43
+ return parseAgentActivity(opts.agentId, raw.value);
44
+ }
45
+
46
+ export {
47
+ DEFAULT_AGENT_LOG_LIMIT,
48
+ readAgentActivityRaw,
49
+ parseAgentActivity,
50
+ readAgentActivity
51
+ };
@@ -153,14 +153,57 @@ var Ssh2Session = class {
153
153
  this.client.end();
154
154
  }
155
155
  };
156
- function describeConnectError(message, opts) {
157
- if (!/host.*(denied|verification)/i.test(message)) {
158
- return `SSH connection failed: ${message}`;
156
+ function diagnoseConnectError(message, opts) {
157
+ if (/host.*(denied|verification)/i.test(message)) {
158
+ const entry = opts.port === 22 ? opts.host : `[${opts.host}]:${opts.port}`;
159
+ return {
160
+ summary: `the host key for ${opts.host}:${opts.port} does not match the one recorded in ${opts.knownHostsPath}. If your cloud reassigned this address \u2014 the usual cause \u2014 drop the stale entry and retry:`,
161
+ remedy: ` ssh-keygen -R ${entry} -f ${opts.knownHostsPath}
162
+ If you did not expect this address to change hands, do not connect.`
163
+ };
164
+ }
165
+ if (opts.awaitingBoot) return { summary: `SSH connection failed: ${message}` };
166
+ if (message.includes("ECONNREFUSED")) {
167
+ return {
168
+ summary: `${opts.host}:${opts.port} refused the connection \u2014 the host is reachable and nothing is listening on that port (${message}).`,
169
+ remedy: "The instance is up and sshd is not. Check the instance state with `clawops status --stack <name>`, and that the port matches the one the stack was deployed with."
170
+ };
171
+ }
172
+ if (/ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|Timed out while waiting for handshake/.test(message)) {
173
+ return {
174
+ // ssh2's readyTimeout (30s) usually fires before the kernel gives up on the TCP connect,
175
+ // so a dropping firewall is reported as a handshake timeout rather than ETIMEDOUT. Both
176
+ // mean the same thing to the operator: nothing came back.
177
+ summary: `${opts.host}:${opts.port} never answered \u2014 nothing came back at all, which is what a firewall dropping the packets looks like (${message}).`,
178
+ remedy: "On a clawops stack the usual cause is that the security group no longer admits this machine: `--ssh-cidr auto` pins the address you planned from, and home, office and VPN addresses change. Re-plan from where you are now and apply:\n clawops plan --stack <name> --ssh-cidr auto --out <plan>.json\n clawops apply <plan>.json\nIf the address has not changed, the instance may be stopped or still booting."
179
+ };
180
+ }
181
+ if (message.includes("ENOTFOUND")) {
182
+ return {
183
+ summary: `${opts.host} does not resolve (${message}).`,
184
+ remedy: "Check the address recorded for this stack with `clawops stacks list`. An instance replaced outside clawops keeps the old address in `~/.clawops/config.json`."
185
+ };
186
+ }
187
+ if (message.includes("All configured authentication methods failed")) {
188
+ const key = opts.privateKeyPath;
189
+ const asUser = opts.user ? ` as ${opts.user}` : "";
190
+ return {
191
+ summary: `${opts.host}:${opts.port} rejected every authentication method clawops offered${asUser} (${message}).`,
192
+ remedy: key ? `clawops offered the key at ${key}. Its public half may not be installed on the host \u2014 an instance rebuilt outside clawops, or a key regenerated since this stack was created. Compare what clawops offers, \`ssh-keygen -y -f ${key}\`, against the authorized_keys on the host.` : "The key clawops offered may not be installed on the host \u2014 an instance rebuilt outside clawops, or a key regenerated since this stack was created."
193
+ };
159
194
  }
160
- const entry = opts.port === 22 ? opts.host : `[${opts.host}]:${opts.port}`;
161
- return `the host key for ${opts.host}:${opts.port} does not match the one recorded in ${opts.knownHostsPath}. If your cloud reassigned this address \u2014 the usual cause \u2014 drop the stale entry and retry:
162
- ssh-keygen -R ${entry} -f ${opts.knownHostsPath}
163
- If you did not expect this address to change hands, do not connect.`;
195
+ if (/no matching|Handshake failed/i.test(message)) {
196
+ return {
197
+ summary: `no algorithm the SSH handshake needs is accepted by both ends (${message}).`,
198
+ remedy: "The host is running an OpenSSH older than the algorithms ssh2 offers. clawops does not deploy hosts that old, so this is usually a pre-existing machine registered with the local provider; upgrading its OpenSSH is the fix."
199
+ };
200
+ }
201
+ return { summary: `SSH connection failed: ${message}` };
202
+ }
203
+ function describeConnectError(message, opts) {
204
+ const { summary, remedy } = diagnoseConnectError(message, opts);
205
+ return remedy ? `${summary}
206
+ ${remedy}` : summary;
164
207
  }
165
208
  async function connect(opts) {
166
209
  let privateKey;
@@ -221,6 +264,7 @@ function verifyHostKey(host, port, keyHash, knownHostsPath) {
221
264
  }
222
265
 
223
266
  export {
267
+ diagnoseConnectError,
224
268
  describeConnectError,
225
269
  connect
226
270
  };
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ pulumiCause
4
+ } from "./chunk-POYS3EVQ.js";
2
5
  import {
3
6
  validatePlan
4
- } from "./chunk-XFN3YDZE.js";
7
+ } from "./chunk-PASAVVNS.js";
5
8
  import {
6
9
  buildContext,
7
10
  loadAdapterModule
@@ -71,7 +74,7 @@ async function generatePlan(intent, _opts) {
71
74
  "plan/apply is not supported for the local provider. Use `clawops up` directly."
72
75
  );
73
76
  }
74
- const { version } = await import("./package-MF5LPITL.js");
77
+ const { version } = await import("./package-HYGHRX35.js");
75
78
  const config = getConfig();
76
79
  const instanceType = await resolveInstanceType(intent);
77
80
  const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
@@ -118,7 +121,7 @@ async function generatePlan(intent, _opts) {
118
121
  } catch (err) {
119
122
  if (err instanceof UsageError) throw err;
120
123
  throw new StateError(
121
- `Cannot open the state backend for stack "${stackName}": ${messageOf(err)}
124
+ `Cannot open the state backend for stack "${stackName}": ${pulumiCause(err)}
122
125
  A plan is only as good as the state it was computed against, so this is not a plan clawops will write. Check the stateUrl in ~/.clawops/config.json, and that the bucket or container exists and your credentials can read it.`
123
126
  );
124
127
  }
@@ -151,7 +154,7 @@ A plan is only as good as the state it was computed against, so this is not a pl
151
154
  throw new Error(`Generated plan failed schema validation:
152
155
  ${validation.errors.join("\n")}`);
153
156
  }
154
- const { validatePlanConfig, validatePlanNetwork } = await import("./validate-JXKN5WD6.js");
157
+ const { validatePlanConfig, validatePlanNetwork } = await import("./validate-DZE6LFEB.js");
155
158
  const cfgCheck = await validatePlanConfig(plan);
156
159
  for (const w of cfgCheck.warnings) process.stderr.write(`[clawops] warning: ${w}
157
160
  `);
@@ -179,13 +182,6 @@ function planId() {
179
182
  function expandHome(p) {
180
183
  return p.replace(/^~/, process.env["HOME"] ?? "~");
181
184
  }
182
- function messageOf(err) {
183
- const raw = err instanceof Error ? err.message : String(err);
184
- const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
185
- const explained = lines.find((l) => /(^|\s)error:/i.test(l));
186
- if (explained) return explained.replace(/^.*?error:\s*/i, "");
187
- return lines[0] ?? raw;
188
- }
189
185
 
190
186
  export {
191
187
  INSTANCE_ALIASES,
@@ -2,24 +2,24 @@
2
2
  import {
3
3
  resolveSecrets
4
4
  } from "./chunk-BOPSG2LI.js";
5
- import {
6
- saveOverlay
7
- } from "./chunk-6ZFIFDBJ.js";
8
5
  import {
9
6
  atomicWriteConfig,
10
7
  deepMerge,
11
8
  readRemoteConfig,
12
9
  restartGateway
13
- } from "./chunk-2I7KCMXZ.js";
10
+ } from "./chunk-UAJJEDCJ.js";
14
11
  import {
15
12
  validatePlan
16
- } from "./chunk-XFN3YDZE.js";
13
+ } from "./chunk-PASAVVNS.js";
17
14
  import {
18
15
  writeStackConfig
19
16
  } from "./chunk-55UMO37O.js";
20
17
  import {
21
18
  buildContext
22
19
  } from "./chunk-AVERNLUY.js";
20
+ import {
21
+ saveOverlay
22
+ } from "./chunk-6ZFIFDBJ.js";
23
23
  import {
24
24
  GATEWAY_PORT
25
25
  } from "./chunk-VOEJYRIK.js";
@@ -54,7 +54,7 @@ ${validation.errors.join("\n")}`
54
54
  });
55
55
  const stack = await ctx.getStack();
56
56
  const { guardPrivateOnlyApply } = await import("./private-only-DIA26I2E.js");
57
- const { probeSsh } = await import("./tailscale-cutover-EKAQSDXP.js");
57
+ const { probeSsh } = await import("./tailscale-cutover-V42HO5UM.js");
58
58
  await guardPrivateOnlyApply(plan, ctx, opts?.probeTailnet ?? ((conn2) => probeSsh(conn2, opts?.signal)));
59
59
  await writeStackConfig(stack, plan);
60
60
  if (plan.metadata.stackVersion !== void 0) {
@@ -84,7 +84,7 @@ The diff you reviewed may no longer reflect what will be applied.
84
84
  changeSummary[op] = count;
85
85
  }
86
86
  }
87
- const { waitForSsh } = await import("./wait-WO72TRRQ.js");
87
+ const { waitForSsh } = await import("./wait-DOUNNJNN.js");
88
88
  const conn = await connectionInfoFor(ctx, outputs);
89
89
  if (opts?.skipReadiness) {
90
90
  return { outputs, changeSummary, durationMs: Date.now() - start };
@@ -93,7 +93,7 @@ The diff you reviewed may no longer reflect what will be applied.
93
93
  signal: opts?.signal,
94
94
  onProgress: (line) => reportProgress(opts, line)
95
95
  });
96
- const { waitForGateway } = await import("./ready-KDNRBNSU.js");
96
+ const { waitForGateway } = await import("./ready-IJEUBFVX.js");
97
97
  try {
98
98
  await waitForGateway(readySession, {
99
99
  signal: opts?.signal,
@@ -129,7 +129,7 @@ function expandHome(p) {
129
129
  return p.replace(/^~/, process.env["HOME"] ?? "~");
130
130
  }
131
131
  async function applyConfigOverlay(plan, outputs, ctx, signal) {
132
- const { connect } = await import("./ssh-VAI5DU5M.js");
132
+ const { connect } = await import("./ssh-HOM7GF3M.js");
133
133
  const session = await connect({ ...await connectionInfoFor(ctx, outputs), signal });
134
134
  try {
135
135
  const remote = await readRemoteConfig(session, signal);
@@ -158,7 +158,7 @@ async function applyConfigOverlay(plan, outputs, ctx, signal) {
158
158
  async function installProviderPlugins(session, cfg, openclawVersion, signal) {
159
159
  const [{ requiredPlugins, installCommand }, { STATE_DIR_HOST_LINUX }, yaml, { readFileSync }, { join }, { resolveSpecDir }] = await Promise.all([
160
160
  import("./plugins-DTST3OER.js"),
161
- import("./runtime-X3KW5TCP.js"),
161
+ import("./runtime-VW53TU2W.js"),
162
162
  import("./js-yaml-PTEEG4FO.js"),
163
163
  import("fs"),
164
164
  import("path"),
@@ -169,7 +169,7 @@ async function installProviderPlugins(session, cfg, openclawVersion, signal) {
169
169
  );
170
170
  const needed = requiredPlugins(cfg, catalog);
171
171
  if (needed.length === 0) return;
172
- const { execPrivileged } = await import("./privileged-JFA6LLTV.js");
172
+ const { execPrivileged } = await import("./privileged-5Y6O4H7Z.js");
173
173
  const image = `ghcr.io/openclaw/openclaw:${openclawVersion}`;
174
174
  for (const plugin of needed) {
175
175
  const result = await execPrivileged(
@@ -187,7 +187,7 @@ async function installProviderPlugins(session, cfg, openclawVersion, signal) {
187
187
  }
188
188
  async function verifyProviders(session, cfg, signal) {
189
189
  const { missingProviders } = await import("./plugins-DTST3OER.js");
190
- const { execPrivileged } = await import("./privileged-JFA6LLTV.js");
190
+ const { execPrivileged } = await import("./privileged-5Y6O4H7Z.js");
191
191
  const result = await execPrivileged(
192
192
  session,
193
193
  "docker exec openclaw openclaw plugins list --json",
@@ -213,10 +213,10 @@ async function loadChannelCatalog() {
213
213
  return yaml.load(readFileSync(join(resolveSpecDir(), "integrations.yaml"), "utf-8"));
214
214
  }
215
215
  async function installChannelPlugins(session, cfg, openclawVersion, signal) {
216
- const [{ requiredChannelPlugins, channelInstallCommand }, { STATE_DIR_HOST_LINUX }] = await Promise.all([import("./channels-KBUR42IU.js"), import("./runtime-X3KW5TCP.js")]);
216
+ const [{ requiredChannelPlugins, channelInstallCommand }, { STATE_DIR_HOST_LINUX }] = await Promise.all([import("./channels-KBUR42IU.js"), import("./runtime-VW53TU2W.js")]);
217
217
  const needed = requiredChannelPlugins(cfg, await loadChannelCatalog());
218
218
  if (needed.length === 0) return;
219
- const { execPrivileged } = await import("./privileged-JFA6LLTV.js");
219
+ const { execPrivileged } = await import("./privileged-5Y6O4H7Z.js");
220
220
  const image = `ghcr.io/openclaw/openclaw:${openclawVersion}`;
221
221
  for (const plugin of needed) {
222
222
  const result = await execPrivileged(
@@ -234,7 +234,7 @@ async function installChannelPlugins(session, cfg, openclawVersion, signal) {
234
234
  }
235
235
  async function verifyChannels(session, cfg, signal) {
236
236
  const { missingChannels, CHANNELS_LIST_CMD } = await import("./channels-KBUR42IU.js");
237
- const { execPrivileged } = await import("./privileged-JFA6LLTV.js");
237
+ const { execPrivileged } = await import("./privileged-5Y6O4H7Z.js");
238
238
  const result = await execPrivileged(session, CHANNELS_LIST_CMD, signal);
239
239
  if (result.code !== 0) return;
240
240
  const missing = missingChannels(cfg, result.stdout, await loadChannelCatalog());