@clawops/cli 1.7.8 → 2.0.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 (57) hide show
  1. package/README.md +412 -70
  2. package/dist/{apply-F3KY2HTB.js → apply-AM5HERE7.js} +8 -6
  3. package/dist/{aws-L4RR2LSE.js → aws-AMON7DA7.js} +20 -9
  4. package/dist/{azure-7OXQENQL.js → azure-T4G6OBJQ.js} +19 -8
  5. package/dist/{bootstrap-HL4DNUNG.js → bootstrap-DMRPWJGN.js} +28 -19
  6. package/dist/bootstrap.sh.tmpl +51 -31
  7. package/dist/channels-KBUR42IU.js +53 -0
  8. package/dist/{chunk-DQDAWNGB.js → chunk-3WYA2QDM.js} +1 -1
  9. package/dist/chunk-6TIYVHPX.js +152 -0
  10. package/dist/{chunk-3XK66DKK.js → chunk-AHAAEXWC.js} +41 -0
  11. package/dist/{chunk-QQNKBTYZ.js → chunk-AX74VXTV.js} +292 -94
  12. package/dist/{chunk-C5X375RO.js → chunk-CKSPROD5.js} +59 -15
  13. package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
  14. package/dist/{chunk-FNS56UQY.js → chunk-GYV4KNW7.js} +319 -252
  15. package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
  16. package/dist/chunk-LT4XMO5D.js +50 -0
  17. package/dist/{chunk-P5LKWRPL.js → chunk-LU2V3LXA.js} +3 -3
  18. package/dist/chunk-N44K3LHE.js +140 -0
  19. package/dist/chunk-NUDIHZEH.js +55 -0
  20. package/dist/chunk-SBLQH2TJ.js +56 -0
  21. package/dist/chunk-UFRO4H5W.js +228 -0
  22. package/dist/{chunk-RAAONHW3.js → chunk-WHJMKDV7.js} +24 -4
  23. package/dist/chunk-YR3O2I3S.js +101 -0
  24. package/dist/cli.js +866 -577
  25. package/dist/config-validate-K2N6XLDX.js +76 -0
  26. package/dist/{context-5LCUG2QR.js → context-KHKWJJMC.js} +1 -1
  27. package/dist/{firewall-XQGO7HWK.js → firewall-T3NTPZJD.js} +10 -0
  28. package/dist/{gcp-UG6AWPIZ.js → gcp-2JBTD7ZM.js} +19 -8
  29. package/dist/{generate-ELD7QF5Z.js → generate-2XV2GSP2.js} +4 -4
  30. package/dist/{harden-URPXLSVF.js → harden-PCDK3UQ5.js} +48 -11
  31. package/dist/health-5CCOXYGG.js +11 -0
  32. package/dist/logs-P7YCKVF4.js +15 -0
  33. package/dist/{mcp-wire-ZMIVFAUC.js → mcp-wire-BFEY2Y6H.js} +23 -17
  34. package/dist/{package-QYZFN3FP.js → package-3HUW57XH.js} +3 -3
  35. package/dist/plugins-DTST3OER.js +49 -0
  36. package/dist/{pool-QGFQRMGU.js → pool-EX4ZTA5P.js} +2 -2
  37. package/dist/privileged-JFA6LLTV.js +17 -0
  38. package/dist/{remote-config-QDLN4UMG.js → remote-config-Z6IIZR3D.js} +3 -3
  39. package/dist/runtime-LZ7NA53V.js +42 -0
  40. package/dist/{server-FWTOSSZP.js → server-A5AB67GQ.js} +16 -12
  41. package/dist/spec-path-KQOOIQCA.js +9 -0
  42. package/dist/{ssh-CNJNLM3E.js → ssh-B3NZAU4I.js} +1 -1
  43. package/dist/upgrade-ULD4KFYC.js +88 -0
  44. package/dist/validate-BOQRWX4F.js +14 -0
  45. package/dist/{version-guard-VUCNZXQS.js → version-guard-GPSTCPT7.js} +3 -3
  46. package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
  47. package/package.json +3 -3
  48. package/spec/deploy-plan.schema.json +114 -25
  49. package/spec/integrations.yaml +135 -28
  50. package/spec/mcp-tools.yaml +68 -35
  51. package/spec/models.yaml +9 -0
  52. package/spec/openclaw-2.0.config.schema.json +1 -0
  53. package/spec/openclaw-versions.yaml +116 -27
  54. package/dist/chunk-35WVQY3X.js +0 -122
  55. package/dist/chunk-6X45DDGH.js +0 -42
  56. package/dist/chunk-SEZMVVAS.js +0 -156
  57. package/dist/validate-CFQ4FH2T.js +0 -10
@@ -1,24 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  makeStartupScript
4
- } from "./chunk-C5X375RO.js";
4
+ } from "./chunk-CKSPROD5.js";
5
+ import {
6
+ GATEWAY_PORT
7
+ } from "./chunk-6TIYVHPX.js";
8
+ import "./chunk-EI7CQQ7D.js";
5
9
 
6
10
  // src/providers/aws/index.ts
7
11
  import process2 from "process";
8
12
 
9
13
  // src/providers/aws/program.ts
10
- var GATEWAY_PORT = 18789;
11
14
  var SSH_PORT = 22;
12
15
  var awsProgram = async () => {
13
- const [pulumi, aws, { resolveIngressCidrs, detectEgressIp }] = await Promise.all([
16
+ const [pulumi, aws, { resolveIngressCidrs, resolveGatewayIngressCidrs, resolveGatewayPort, detectEgressIp }] = await Promise.all([
14
17
  import("@pulumi/pulumi"),
15
18
  import("@pulumi/aws"),
16
- import("./firewall-XQGO7HWK.js")
19
+ import("./firewall-T3NTPZJD.js")
17
20
  ]);
18
21
  const cfg = new pulumi.Config();
19
22
  const instanceType = cfg.get("instanceType") ?? "t3.small";
20
23
  const region = cfg.get("region") ?? "us-east-1";
21
24
  const openclawVersion = cfg.get("openclawVersion") ?? "latest";
25
+ const publishGateway = cfg.get("publishGateway") === "all" ? "all" : "loopback";
26
+ const gatewayPort = resolveGatewayPort(cfg.get("gatewayPort"), GATEWAY_PORT);
22
27
  const accessMode = cfg.get("accessMode") ?? "restricted";
23
28
  const allowedCidrs = cfg.get("allowedCidrs") ?? "";
24
29
  const sshCidrs = cfg.get("sshCidrs") ?? "";
@@ -38,7 +43,13 @@ var awsProgram = async () => {
38
43
  );
39
44
  }
40
45
  const sshIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, sshCidrs, egressResult);
41
- const gatewayIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, gatewayCidrs, egressResult);
46
+ const gatewayIngressCidrs = resolveGatewayIngressCidrs(
47
+ publishGateway,
48
+ accessMode,
49
+ allowedCidrs,
50
+ gatewayCidrs,
51
+ egressResult
52
+ );
42
53
  const vpc = new aws.ec2.Vpc("clawops-vpc", {
43
54
  cidrBlock: "10.0.0.0/16",
44
55
  enableDnsHostnames: true,
@@ -91,8 +102,8 @@ var awsProgram = async () => {
91
102
  new aws.vpc.SecurityGroupIngressRule(`clawops-sg-gw-${i}`, {
92
103
  securityGroupId: sg.id,
93
104
  ipProtocol: "tcp",
94
- fromPort: GATEWAY_PORT,
95
- toPort: GATEWAY_PORT,
105
+ fromPort: gatewayPort,
106
+ toPort: gatewayPort,
96
107
  cidrIpv4: cidr,
97
108
  tags: { Name: `clawops-gateway-${i}` }
98
109
  });
@@ -159,7 +170,7 @@ var awsProgram = async () => {
159
170
  vpcSecurityGroupIds: [sg.id],
160
171
  iamInstanceProfile: instanceProfile.name,
161
172
  keyName: keyPair.keyName,
162
- userData: makeStartupScript({ openclawVersion, os: "ubuntu", bedrockEnabled }),
173
+ userData: makeStartupScript({ openclawVersion, os: "ubuntu", bedrockEnabled, publishGateway, gatewayPort }),
163
174
  // IMDSv2 with hopLimit=2 so Docker containers on this host can reach IMDS
164
175
  // and obtain the instance role credentials (required for Bedrock access).
165
176
  metadataOptions: {
@@ -179,7 +190,7 @@ var awsProgram = async () => {
179
190
  return {
180
191
  instanceId: instance.id,
181
192
  publicIp: eip.publicIp,
182
- gatewayUrl: pulumi.interpolate`https://${eip.publicIp}:${GATEWAY_PORT}`,
193
+ gatewayUrl: pulumi.interpolate`https://${eip.publicIp}:${gatewayPort}`,
183
194
  sshHost: eip.publicIp,
184
195
  sshPort: SSH_PORT,
185
196
  sshUser: "ubuntu",
@@ -1,27 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  makeStartupScript
4
- } from "./chunk-C5X375RO.js";
4
+ } from "./chunk-CKSPROD5.js";
5
+ import {
6
+ GATEWAY_PORT
7
+ } from "./chunk-6TIYVHPX.js";
8
+ import "./chunk-EI7CQQ7D.js";
5
9
 
6
10
  // src/providers/azure/index.ts
7
11
  import process2 from "process";
8
12
 
9
13
  // src/providers/azure/program.ts
10
14
  import { createHash } from "crypto";
11
- var GATEWAY_PORT = 18789;
12
15
  var SSH_PORT = 22;
13
16
  var azureProgram = async () => {
14
- const [pulumi, azure, random, { resolveIngressCidrs, detectEgressIp }] = await Promise.all([
17
+ const [pulumi, azure, random, { resolveIngressCidrs, resolveGatewayIngressCidrs, resolveGatewayPort, detectEgressIp }] = await Promise.all([
15
18
  import("@pulumi/pulumi"),
16
19
  import("@pulumi/azure-native"),
17
20
  import("@pulumi/random"),
18
- import("./firewall-XQGO7HWK.js")
21
+ import("./firewall-T3NTPZJD.js")
19
22
  ]);
20
23
  const cfg = new pulumi.Config();
21
24
  const stackName = pulumi.getStack();
22
25
  const instanceType = cfg.get("instanceType") ?? "Standard_B2s";
23
26
  const location = cfg.get("region") ?? "eastus";
24
27
  const openclawVersion = cfg.get("openclawVersion") ?? "latest";
28
+ const publishGateway = cfg.get("publishGateway") === "all" ? "all" : "loopback";
29
+ const gatewayPort = resolveGatewayPort(cfg.get("gatewayPort"), GATEWAY_PORT);
25
30
  const accessMode = cfg.get("accessMode") ?? "restricted";
26
31
  const allowedCidrs = cfg.get("allowedCidrs") ?? "";
27
32
  const sshCidrs = cfg.get("sshCidrs") ?? "";
@@ -42,7 +47,13 @@ var azureProgram = async () => {
42
47
  );
43
48
  }
44
49
  const sshIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, sshCidrs, egressResult);
45
- const gatewayIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, gatewayCidrs, egressResult);
50
+ const gatewayIngressCidrs = resolveGatewayIngressCidrs(
51
+ publishGateway,
52
+ accessMode,
53
+ allowedCidrs,
54
+ gatewayCidrs,
55
+ egressResult
56
+ );
46
57
  const rg = new azure.resources.ResourceGroup("clawops-rg", {
47
58
  resourceGroupName,
48
59
  location
@@ -82,7 +93,7 @@ var azureProgram = async () => {
82
93
  sourceAddressPrefix: cidr,
83
94
  sourcePortRange: "*",
84
95
  destinationAddressPrefix: "*",
85
- destinationPortRange: String(GATEWAY_PORT)
96
+ destinationPortRange: String(gatewayPort)
86
97
  });
87
98
  }
88
99
  const nsg = new azure.network.NetworkSecurityGroup("clawops-nsg", {
@@ -115,7 +126,7 @@ var azureProgram = async () => {
115
126
  osProfile: {
116
127
  adminUsername: "clawops",
117
128
  computerName: "clawops",
118
- customData: Buffer.from(makeStartupScript({ openclawVersion, os: "ubuntu" })).toString("base64"),
129
+ customData: Buffer.from(makeStartupScript({ openclawVersion, os: "ubuntu", publishGateway, gatewayPort })).toString("base64"),
119
130
  linuxConfiguration: {
120
131
  disablePasswordAuthentication: true,
121
132
  ssh: {
@@ -181,7 +192,7 @@ var azureProgram = async () => {
181
192
  return {
182
193
  instanceId: vm.id,
183
194
  publicIp: resolvedIp,
184
- gatewayUrl: pulumi.interpolate`https://${resolvedIp}:${GATEWAY_PORT}`,
195
+ gatewayUrl: pulumi.interpolate`https://${resolvedIp}:${gatewayPort}`,
185
196
  sshHost: resolvedIp,
186
197
  sshPort: SSH_PORT,
187
198
  sshUser: "clawops",
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  acquireSession
4
- } from "./chunk-DQDAWNGB.js";
5
- import "./chunk-3XK66DKK.js";
4
+ } from "./chunk-3WYA2QDM.js";
5
+ import "./chunk-AHAAEXWC.js";
6
6
  import {
7
7
  writeLocalState
8
8
  } from "./chunk-A2I76FTA.js";
@@ -10,12 +10,17 @@ import "./chunk-CX5SL5HP.js";
10
10
  import {
11
11
  ProviderError
12
12
  } from "./chunk-KGXPLI7W.js";
13
+ import {
14
+ GATEWAY_PORT,
15
+ gatewayRunArgs,
16
+ gatewayRunCommand
17
+ } from "./chunk-6TIYVHPX.js";
18
+ import "./chunk-EI7CQQ7D.js";
13
19
 
14
20
  // src/providers/local/bootstrap.ts
15
21
  import { readFileSync, existsSync } from "fs";
16
22
  import path from "path";
17
23
  import { fileURLToPath } from "url";
18
- var GATEWAY_PORT = 18789;
19
24
  var HEALTH_POLL_INTERVAL_MS = 3e3;
20
25
  var HEALTH_TIMEOUT_MS = 12e4;
21
26
  function loadTemplate() {
@@ -31,11 +36,18 @@ function loadTemplate() {
31
36
  `Cannot find bootstrap.sh.tmpl (looked in: ${candidates.join(", ")}). If this is an installed package, the template may be missing from the build output.`
32
37
  );
33
38
  }
34
- function renderScript(openclawVersion) {
35
- return loadTemplate().replace(/\{\{OPENCLAW_VERSION\}\}/g, openclawVersion);
39
+ function renderScript(openclawVersion, gatewayPort = GATEWAY_PORT) {
40
+ const shared = {
41
+ image: "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}",
42
+ stateDir: '"${OPENCLAW_STATE_DIR}"',
43
+ envFilePath: '"${OPENCLAW_ENV_FILE}"',
44
+ port: "${OPENCLAW_PORT}"
45
+ };
46
+ return loadTemplate().replace(/\{\{GATEWAY_RUN_SYSTEMD\}\}/g, gatewayRunArgs({ ...shared, supervisor: "systemd" })).replace(/\{\{GATEWAY_RUN_DARWIN\}\}/g, gatewayRunCommand(shared)).replace(/\{\{OPENCLAW_VERSION\}\}/g, openclawVersion).replace(/\{\{OPENCLAW_PORT\}\}/g, String(gatewayPort));
36
47
  }
37
48
  async function localBootstrap(opts) {
38
- const script = renderScript(opts.openclawVersion);
49
+ const gatewayPort = opts.gatewayPort ?? GATEWAY_PORT;
50
+ const script = renderScript(opts.openclawVersion, gatewayPort);
39
51
  const b64 = Buffer.from(script, "utf-8").toString("base64");
40
52
  const command = opts.sudoPassword ? `printf '%s
41
53
  ' ${shellQuote(opts.sudoPassword)} | sudo -S -p '' bash -c "echo '${b64}' | base64 -d | bash"` : `echo '${b64}' | base64 -d | sudo bash`;
@@ -59,13 +71,14 @@ ${result.stderr || result.stdout}`
59
71
  );
60
72
  }
61
73
  }
74
+ if (!opts.noWait) await waitForGateway(session, gatewayPort, opts.signal);
62
75
  } finally {
63
76
  release();
64
77
  }
65
78
  const state = {
66
79
  instanceId: `local:${opts.host}`,
67
80
  publicIp: opts.host,
68
- gatewayUrl: `http://${opts.host}:${GATEWAY_PORT}`,
81
+ gatewayUrl: `http://${opts.host}:${gatewayPort}`,
69
82
  sshHost: opts.host,
70
83
  sshPort: opts.port,
71
84
  sshUser: opts.user,
@@ -75,27 +88,22 @@ ${result.stderr || result.stdout}`
75
88
  knownHostsPath: opts.knownHostsPath
76
89
  };
77
90
  writeLocalState(opts.stackName, state);
78
- if (!opts.noWait) {
79
- await waitForGateway(opts.host, GATEWAY_PORT, opts.signal);
80
- }
81
91
  return state;
82
92
  }
83
- async function waitForGateway(host, port, signal) {
84
- const url = `http://${host}:${port}/health`;
93
+ async function waitForGateway(session, gatewayPort, signal) {
94
+ const { probeCommand, interpretProbe } = await import("./health-5CCOXYGG.js");
95
+ const probe = probeCommand("started", gatewayPort);
85
96
  const deadline = Date.now() + HEALTH_TIMEOUT_MS;
86
97
  while (Date.now() < deadline) {
87
98
  if (signal?.aborted) {
88
99
  throw new ProviderError("Bootstrap aborted while waiting for gateway");
89
100
  }
90
- try {
91
- const res = await fetch(url, { signal });
92
- if (res.ok) return;
93
- } catch {
94
- }
101
+ const result = await session.exec(probe, signal);
102
+ if (interpretProbe("started", result.stdout).ok) return;
95
103
  await sleep(HEALTH_POLL_INTERVAL_MS);
96
104
  }
97
105
  throw new ProviderError(
98
- `Gateway at ${url} did not become healthy within ${HEALTH_TIMEOUT_MS / 1e3}s`
106
+ `Gateway did not finish starting within ${HEALTH_TIMEOUT_MS / 1e3}s. Probed /startupz on the host's loopback; check \`clawops logs\`.`
99
107
  );
100
108
  }
101
109
  function sleep(ms) {
@@ -132,5 +140,6 @@ ${stderr}`);
132
140
  }
133
141
  }
134
142
  export {
135
- localBootstrap
143
+ localBootstrap,
144
+ renderScript
136
145
  };
@@ -5,7 +5,7 @@
5
5
  set -euo pipefail
6
6
 
7
7
  OPENCLAW_VERSION="{{OPENCLAW_VERSION}}"
8
- OPENCLAW_PORT=18789
8
+ OPENCLAW_PORT={{OPENCLAW_PORT}}
9
9
 
10
10
  # ── OS detection ──────────────────────────────────────────────────────────────
11
11
  OS="$(uname -s)"
@@ -25,17 +25,20 @@ case "$OS" in
25
25
  exit 1
26
26
  fi
27
27
  OPENCLAW_USER=clawops
28
- OPENCLAW_CONFIG=/home/${OPENCLAW_USER}/openclaw.json
28
+ OPENCLAW_STATE_DIR=/var/lib/clawops/openclaw
29
+ OPENCLAW_CONFIG="${OPENCLAW_STATE_DIR}/openclaw.json"
30
+ OPENCLAW_LEGACY_CONFIG=/home/${OPENCLAW_USER}/openclaw.json
29
31
  ;;
30
32
  Darwin)
31
33
  PKG_MANAGER=darwin
32
34
  # When invoked via sudo the script runs as root but SUDO_USER is the real caller.
33
35
  # Fall back to id -un for passwordless (non-sudo) invocations.
34
36
  OPENCLAW_USER="${SUDO_USER:-$(id -un)}"
35
- OPENCLAW_CONFIG="${HOME}/.config/openclaw/config.json"
36
- mkdir -p "${HOME}/.config/openclaw"
37
+ OPENCLAW_STATE_DIR="${HOME}/.clawops/openclaw"
38
+ OPENCLAW_CONFIG="${OPENCLAW_STATE_DIR}/openclaw.json"
39
+ OPENCLAW_LEGACY_CONFIG="${HOME}/.config/openclaw/config.json"
37
40
  # Fix ownership so subsequent non-sudo SSH writes (atomicWriteConfig) can overwrite.
38
- chown "${OPENCLAW_USER}" "${HOME}/.config/openclaw" 2>/dev/null || true
41
+ chown "${OPENCLAW_USER}" "${OPENCLAW_STATE_DIR}" 2>/dev/null || true
39
42
  # sudo resets PATH; add common Docker install locations so `command -v docker` works.
40
43
  # Covers: Docker Desktop (Intel + AS), Homebrew (AS), Colima.
41
44
  export PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH"
@@ -140,17 +143,51 @@ if [ ! -s "${OPENCLAW_ENV_FILE}" ]; then
140
143
  chown "${OPENCLAW_USER}" "${OPENCLAW_ENV_FILE}" 2>/dev/null || true
141
144
  fi
142
145
 
146
+ # ── State directory ───────────────────────────────────────────────────────────
147
+ # One directory holds config, the SQLite state and any plugins installed into it, so a
148
+ # container replacement no longer destroys sessions and credentials.
149
+ mkdir -p "${OPENCLAW_STATE_DIR}"
150
+
151
+ # Migrate a pre-2.0 deployment. The config used to be a bare file elsewhere; without this
152
+ # an in-place upgrade comes up with no configuration at all. Guarded on the target being
153
+ # absent, so re-running the bootstrap never clobbers a later edit.
154
+ if [ -f "${OPENCLAW_LEGACY_CONFIG}" ] && [ ! -f "${OPENCLAW_CONFIG}" ]; then
155
+ cp -p "${OPENCLAW_LEGACY_CONFIG}" "${OPENCLAW_CONFIG}"
156
+ mv "${OPENCLAW_LEGACY_CONFIG}" "${OPENCLAW_LEGACY_CONFIG}.migrated"
157
+ echo "clawops: migrated config from ${OPENCLAW_LEGACY_CONFIG}"
158
+ OPENCLAW_MIGRATED=1
159
+ fi
160
+
143
161
  # ── Write default openclaw.json if absent ─────────────────────────────────────
144
162
  # clawops up --config will overwrite this with the plan config overlay.
145
163
  if [ ! -f "${OPENCLAW_CONFIG}" ]; then
146
- cat > "${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
147
- {"meta":{"lastTouchedVersion":"2026.4"},"gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":{}}
164
+ # Unquoted heredoc, so ${OPENCLAW_PORT} expands. gateway.port and the -p mapping have to
165
+ # be the same number: a container that publishes one port while the gateway listens on
166
+ # another starts, looks healthy to Docker, and answers nothing.
167
+ cat > "${OPENCLAW_CONFIG}" <<OPENCLAWJSON
168
+ {"meta":{"lastTouchedVersion":"2026.9"},"gateway":{"mode":"local","port":${OPENCLAW_PORT},"auth":{"mode":"token"}},"models":{},"channels":{}}
148
169
  OPENCLAWJSON
149
- if [ "$OS" = "Linux" ]; then
150
- chown "${OPENCLAW_USER}:${OPENCLAW_USER}" "${OPENCLAW_CONFIG}"
151
- else
152
- chown "${OPENCLAW_USER}" "${OPENCLAW_CONFIG}" 2>/dev/null || true
153
- fi
170
+ fi
171
+
172
+ # Ownership last, so it covers the config and anything migrated above.
173
+ if [ "$OS" = "Linux" ]; then
174
+ # NUMERIC, never clawops:clawops. useradd gets uid 1001 on Ubuntu 24.04 because the
175
+ # ubuntu user already holds 1000, and the container runs as 1000 — a 1001-owned state
176
+ # dir makes the gateway exit 1 with EACCES on its own SQLite WAL.
177
+ chown -R 1000:1000 "${OPENCLAW_STATE_DIR}"
178
+ else
179
+ chown -R "${OPENCLAW_USER}" "${OPENCLAW_STATE_DIR}" 2>/dev/null || true
180
+ fi
181
+
182
+ # A 1.x config has no gateway.mode, and clawops no longer passes --allow-unconfigured, so a
183
+ # migrated deployment would exit 78. Normalised with OpenClaw's own tooling — schema-aware,
184
+ # atomic, and it applies OpenClaw's internal config migrations too. After the chown, so the
185
+ # container can write.
186
+ if [ "${OPENCLAW_MIGRATED:-0}" = "1" ]; then
187
+ docker run --rm -v "${OPENCLAW_STATE_DIR}":/home/node/.openclaw \
188
+ "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}" \
189
+ openclaw config set gateway.mode local || \
190
+ echo "clawops: WARNING could not set gateway.mode; gateway may refuse to start" >&2
154
191
  fi
155
192
 
156
193
  # ── Start service ─────────────────────────────────────────────────────────────
@@ -168,13 +205,7 @@ Restart=always
168
205
  RestartSec=5
169
206
  ExecStartPre=-/usr/bin/docker stop openclaw
170
207
  ExecStartPre=-/usr/bin/docker rm openclaw
171
- ExecStart=/usr/bin/docker run --name openclaw --rm \
172
- -p ${OPENCLAW_PORT}:${OPENCLAW_PORT} \
173
- -e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \
174
- --env-file ${OPENCLAW_ENV_FILE} \
175
- -v ${OPENCLAW_CONFIG}:/app/config.json:ro \
176
- ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} \
177
- node openclaw.mjs gateway run --allow-unconfigured --port ${OPENCLAW_PORT}
208
+ ExecStart=/usr/bin/{{GATEWAY_RUN_SYSTEMD}}
178
209
  ExecStop=/usr/bin/docker stop openclaw
179
210
 
180
211
  [Install]
@@ -187,18 +218,7 @@ UNIT
187
218
  elif [ "$OS" = "Darwin" ]; then
188
219
  # macOS: no systemd; run as foreground-detached container.
189
220
  # LaunchAgent support is a future enhancement.
190
- docker stop openclaw 2>/dev/null || true
191
- docker rm openclaw 2>/dev/null || true
192
- docker run -d \
193
- --name openclaw \
194
- --restart unless-stopped \
195
- -p "${OPENCLAW_PORT}:${OPENCLAW_PORT}" \
196
- -e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \
197
- --env-file "${OPENCLAW_ENV_FILE}" \
198
- --env-file ${OPENCLAW_ENV_FILE} \
199
- -v "${OPENCLAW_CONFIG}":/app/config.json:ro \
200
- "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}" \
201
- node openclaw.mjs gateway run --allow-unconfigured --port ${OPENCLAW_PORT}
221
+ {{GATEWAY_RUN_DARWIN}}
202
222
  fi
203
223
 
204
224
  echo "clawops: bootstrap complete"
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/openclaw/channels.ts
4
+ function requiredChannelPlugins(cfg, catalog) {
5
+ const configured = configuredChannelKeys(cfg);
6
+ const out = [];
7
+ for (const entry of catalog.integrations) {
8
+ const plugin = entry.plugin;
9
+ if (!plugin || plugin.source === "bundled" || !plugin.package) continue;
10
+ if (configured.has(entry.channelKey)) {
11
+ out.push({
12
+ channelKey: entry.channelKey,
13
+ package: plugin.package,
14
+ version: plugin.version ?? ""
15
+ });
16
+ }
17
+ }
18
+ return out;
19
+ }
20
+ function channelInstallCommand(plugin, image, stateDir) {
21
+ const spec = plugin.version ? `${plugin.package}@${plugin.version}` : plugin.package;
22
+ return `docker run --rm -v ${stateDir}:/home/node/.openclaw ${image} openclaw plugins install '${spec}' --accept-capabilities`;
23
+ }
24
+ var CHANNELS_LIST_CMD = "docker exec openclaw openclaw channels list --all --json";
25
+ function missingChannels(cfg, channelsListJson, catalog) {
26
+ let installed;
27
+ try {
28
+ const parsed = JSON.parse(channelsListJson);
29
+ const chat = parsed.chat ?? {};
30
+ installed = new Set(
31
+ Object.entries(chat).filter(([, v]) => v.installed === true).map(([k]) => k)
32
+ );
33
+ } catch {
34
+ return [];
35
+ }
36
+ const bundled = new Set(
37
+ catalog.integrations.filter((i) => i.plugin?.source === "bundled").map((i) => i.channelKey)
38
+ );
39
+ return [...configuredChannelKeys(cfg)].filter((k) => !installed.has(k) && !bundled.has(k));
40
+ }
41
+ function configuredChannelKeys(cfg) {
42
+ if (cfg === null || typeof cfg !== "object") return /* @__PURE__ */ new Set();
43
+ const channels = cfg["channels"];
44
+ if (channels === null || typeof channels !== "object") return /* @__PURE__ */ new Set();
45
+ const notAChannel = /* @__PURE__ */ new Set(["defaults", "modelByChannel"]);
46
+ return new Set(Object.keys(channels).filter((k) => !notAChannel.has(k)));
47
+ }
48
+ export {
49
+ CHANNELS_LIST_CMD,
50
+ channelInstallCommand,
51
+ missingChannels,
52
+ requiredChannelPlugins
53
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  connect
4
- } from "./chunk-3XK66DKK.js";
4
+ } from "./chunk-AHAAEXWC.js";
5
5
  import {
6
6
  NetworkError
7
7
  } from "./chunk-KGXPLI7W.js";
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ err,
4
+ ok
5
+ } from "./chunk-EI7CQQ7D.js";
6
+
7
+ // src/openclaw/run-flags.ts
8
+ var CONFIG_MOUNT_PATH = "/app/config.json";
9
+ var GATEWAY_PORT = 18789;
10
+ var CONFIG_PATH_ENV = `-e OPENCLAW_CONFIG_PATH=${CONFIG_MOUNT_PATH}`;
11
+ var ADD_HOST_FLAG = "--add-host=host.docker.internal:host-gateway";
12
+ var PORT_PIN = `--port ${GATEWAY_PORT}`;
13
+ var COMMON_RUN_FLAGS = `${CONFIG_PATH_ENV} ${ADD_HOST_FLAG}`;
14
+ var IMAGE_INSPECT_CMD = `docker inspect openclaw --format '{{.Config.Image}}'`;
15
+ function imageForRestart(inspectStdout) {
16
+ const image = inspectStdout.trim();
17
+ if (!image || image.includes("Error") || !image.includes(":")) {
18
+ return err(
19
+ "No running OpenClaw container found, so there is no image version to reuse. Deploy one with `clawops up --openclaw-version <version>`. clawops does not fall back to `latest` or `stable` here: both now point at OpenClaw 2.0, which this release line does not support."
20
+ );
21
+ }
22
+ return ok(image);
23
+ }
24
+ function versionOf(image) {
25
+ return image.split(":")[1] ?? "unknown";
26
+ }
27
+
28
+ // src/openclaw/runtime.ts
29
+ var STATE_DIR_CONTAINER = "/home/node/.openclaw";
30
+ var STATE_DIR_HOST_LINUX = "/var/lib/clawops/openclaw";
31
+ var STATE_DIR_HOST_MACOS = "${HOME}/.clawops/openclaw";
32
+ var CONFIG_FILENAME = "openclaw.json";
33
+ var CONTAINER_UID = 1e3;
34
+ var CONFIG_MOUNT_PATH2 = `${STATE_DIR_CONTAINER}/${CONFIG_FILENAME}`;
35
+ function stateDirForOS(os) {
36
+ return os === "Darwin" ? STATE_DIR_HOST_MACOS : STATE_DIR_HOST_LINUX;
37
+ }
38
+ function configPathForOS(os) {
39
+ return `${stateDirForOS(os)}/${CONFIG_FILENAME}`;
40
+ }
41
+ var ENV_FILE_PATH = "/home/clawops/openclaw.env";
42
+ var SECURITY_FLAGS = [
43
+ "--cap-drop=ALL",
44
+ "--security-opt no-new-privileges",
45
+ "--init",
46
+ "--pids-limit 512"
47
+ ].join(" ");
48
+ var SUPERVISOR_ENV = "-e OPENCLAW_SUPERVISOR_MODE=external";
49
+ var ADD_HOST_FLAG2 = "--add-host=host.docker.internal:host-gateway";
50
+ function publishFlag(port, scope) {
51
+ return scope === "loopback" ? `-p 127.0.0.1:${port}:${port}` : `-p ${port}:${port}`;
52
+ }
53
+ function gatewayRunArgs(spec) {
54
+ const {
55
+ image,
56
+ stateDir,
57
+ port = GATEWAY_PORT,
58
+ envFilePath = ENV_FILE_PATH,
59
+ supervisor = "docker",
60
+ publish = "loopback",
61
+ limits
62
+ } = spec;
63
+ const envFileArg = `$([ -s ${envFilePath} ] && echo --env-file ${envFilePath})`;
64
+ const lifecycle = supervisor === "systemd" ? "--rm" : "-d --restart unless-stopped";
65
+ const capacity = [
66
+ limits?.memory ? `--memory ${limits.memory}` : "",
67
+ limits?.cpus ? `--cpus ${limits.cpus}` : ""
68
+ ].filter(Boolean).join(" ");
69
+ return [
70
+ `docker run ${lifecycle} --name openclaw`,
71
+ publishFlag(port, publish),
72
+ SECURITY_FLAGS,
73
+ capacity,
74
+ // No OPENCLAW_CONFIG_PATH: STATE_DIR_CONTAINER is OpenClaw's own default, so mounting
75
+ // it there *is* the configuration. One fewer thing to keep in sync.
76
+ ADD_HOST_FLAG2,
77
+ SUPERVISOR_ENV,
78
+ envFileArg,
79
+ // The directory, writable. Never the file, and never :ro — see GatewayRunSpec.stateDir.
80
+ `-v ${stateDir}:${STATE_DIR_CONTAINER}`,
81
+ spec.extraArgs?.trim() ?? "",
82
+ image,
83
+ // No --allow-unconfigured. That flag bypasses a check upstream describes as detecting
84
+ // "suspicious or clobbered config", and clawops passed it permanently — meaning a
85
+ // clobbered config would have started silently on defaults instead of failing.
86
+ // Provisioning writes `gateway.mode: "local"`, which is what the check actually wants.
87
+ // Measured on 2026.9.2: with the mode present the gateway starts without the flag; a
88
+ // config lacking it exits 78, which is the signal we want rather than one we suppress.
89
+ `node openclaw.mjs gateway run --port ${port}`
90
+ ].filter(Boolean).join(" ");
91
+ }
92
+ function gatewayRunCommand(spec) {
93
+ const { pathPrefix = "" } = spec;
94
+ return pathPrefix + [
95
+ "docker stop openclaw 2>/dev/null || true",
96
+ "docker rm openclaw 2>/dev/null || true",
97
+ gatewayRunArgs(spec)
98
+ ].join(" && ");
99
+ }
100
+ var PUBLISH_INSPECT_CMD = `docker inspect openclaw --format '{{json .HostConfig.PortBindings}}'`;
101
+ function publishedGatewayPort(inspectStdout) {
102
+ try {
103
+ const bindings = JSON.parse(inspectStdout.trim());
104
+ if (!bindings) return void 0;
105
+ for (const hostPorts of Object.values(bindings)) {
106
+ for (const binding of hostPorts ?? []) {
107
+ const port = Number(binding.HostPort);
108
+ if (Number.isInteger(port) && port > 0 && port < 65536) return port;
109
+ }
110
+ }
111
+ } catch {
112
+ }
113
+ return void 0;
114
+ }
115
+ function publishForRestart(inspectStdout) {
116
+ try {
117
+ const bindings = JSON.parse(inspectStdout.trim());
118
+ if (!bindings) return "loopback";
119
+ for (const hostPorts of Object.values(bindings)) {
120
+ for (const binding of hostPorts ?? []) {
121
+ const ip = binding.HostIp ?? "";
122
+ if (ip === "" || ip === "0.0.0.0" || ip === "::") return "all";
123
+ }
124
+ }
125
+ } catch {
126
+ }
127
+ return "loopback";
128
+ }
129
+
130
+ export {
131
+ GATEWAY_PORT,
132
+ IMAGE_INSPECT_CMD,
133
+ imageForRestart,
134
+ versionOf,
135
+ STATE_DIR_CONTAINER,
136
+ STATE_DIR_HOST_LINUX,
137
+ STATE_DIR_HOST_MACOS,
138
+ CONFIG_FILENAME,
139
+ CONTAINER_UID,
140
+ CONFIG_MOUNT_PATH2 as CONFIG_MOUNT_PATH,
141
+ stateDirForOS,
142
+ configPathForOS,
143
+ ENV_FILE_PATH,
144
+ SECURITY_FLAGS,
145
+ SUPERVISOR_ENV,
146
+ ADD_HOST_FLAG2 as ADD_HOST_FLAG,
147
+ gatewayRunArgs,
148
+ gatewayRunCommand,
149
+ PUBLISH_INSPECT_CMD,
150
+ publishedGatewayPort,
151
+ publishForRestart
152
+ };
@@ -140,6 +140,47 @@ var Ssh2Session = class {
140
140
  });
141
141
  });
142
142
  }
143
+ execWithInput(command, input, signal) {
144
+ return new Promise((resolve, reject) => {
145
+ if (signal?.aborted) {
146
+ reject(new NetworkError("Operation aborted"));
147
+ return;
148
+ }
149
+ this.client.exec(command, (err, channel) => {
150
+ if (err) {
151
+ reject(new NetworkError(`SSH exec failed: ${err.message}`));
152
+ return;
153
+ }
154
+ let stdout = "";
155
+ let stderr = "";
156
+ const onAbort = () => {
157
+ channel.destroy();
158
+ reject(new NetworkError("Operation aborted"));
159
+ };
160
+ signal?.addEventListener("abort", onAbort, { once: true });
161
+ channel.on("data", (data) => {
162
+ stdout += data.toString("utf-8");
163
+ });
164
+ channel.stderr.on("data", (data) => {
165
+ stderr += data.toString("utf-8");
166
+ });
167
+ channel.on("close", (code) => {
168
+ signal?.removeEventListener("abort", onAbort);
169
+ resolve({ stdout, stderr, code: code ?? 0 });
170
+ });
171
+ channel.on("error", (chanErr) => {
172
+ signal?.removeEventListener("abort", onAbort);
173
+ reject(new NetworkError(`SSH channel error: ${chanErr.message}`));
174
+ });
175
+ input.on("error", (readErr) => {
176
+ channel.destroy();
177
+ signal?.removeEventListener("abort", onAbort);
178
+ reject(new NetworkError(`Failed reading input: ${readErr.message}`));
179
+ });
180
+ input.pipe(channel);
181
+ });
182
+ });
183
+ }
143
184
  stream(command, signal) {
144
185
  return new Promise((resolve, reject) => {
145
186
  if (signal?.aborted) {