@clawops/cli 1.1.0 → 1.2.1

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 (39) hide show
  1. package/README.md +115 -44
  2. package/dist/apply-KBF7OPWL.js +14 -0
  3. package/dist/{automation-JG2YBEL7.js → automation-RULUSJBW.js} +2 -2
  4. package/dist/{aws-A3323GNM.js → aws-KQBWY43W.js} +12 -10
  5. package/dist/{azure-LNWBK2ZI.js → azure-4EJFVJZL.js} +4 -3
  6. package/dist/{bootstrap-7ZS2GLFJ.js → bootstrap-UYCOEJQX.js} +48 -12
  7. package/dist/{chunk-LT24GUUO.js → chunk-3QJBNAHW.js} +1 -1
  8. package/dist/{chunk-BA6MDFZK.js → chunk-4U3LTLWZ.js} +2 -83
  9. package/dist/{chunk-PERSDQMT.js → chunk-ACYJBSLJ.js} +7 -7
  10. package/dist/chunk-BOPSG2LI.js +77 -0
  11. package/dist/{chunk-PRYLTCS4.js → chunk-BRPU7AQC.js} +1 -1
  12. package/dist/{chunk-ALSUDYA7.js → chunk-CX5SL5HP.js} +1 -1
  13. package/dist/{chunk-ZSE4QRKE.js → chunk-KGXPLI7W.js} +19 -1
  14. package/dist/chunk-LU63NZD3.js +116 -0
  15. package/dist/chunk-OIGTOLB3.js +99 -0
  16. package/dist/chunk-QJ6ERXHN.js +266 -0
  17. package/dist/{chunk-D4UAHAKI.js → chunk-SSB6SGPQ.js} +10 -6
  18. package/dist/chunk-UDNZUSKA.js +97 -0
  19. package/dist/chunk-ZVOEQCNW.js +92 -0
  20. package/dist/cli.js +1340 -115
  21. package/dist/context-VUAYRAJY.js +10 -0
  22. package/dist/errors-OK47MQFD.js +21 -0
  23. package/dist/{gcp-OHWCTFLL.js → gcp-SKURG3L6.js} +4 -3
  24. package/dist/generate-PZPLG3ZM.js +14 -0
  25. package/dist/js-yaml-PTEEG4FO.js +2647 -0
  26. package/dist/mcp-apps-KY44ED3Y.js +11 -0
  27. package/dist/{outputs-6DAVEEAZ.js → outputs-DJHBY7EE.js} +2 -2
  28. package/dist/{package-FA6UOG2I.js → package-SE3M4NJA.js} +1 -1
  29. package/dist/{pool-FAVU7WCW.js → pool-FBFHATDG.js} +3 -2
  30. package/dist/remote-config-JQ77SRC2.js +21 -0
  31. package/dist/secrets-SVZWNAPK.js +7 -0
  32. package/dist/{server-V5ZFL76Y.js → server-4DNHLGEG.js} +64 -150
  33. package/dist/ssh-IQXNME3D.js +8 -0
  34. package/dist/{store-ARJ2EO6L.js → store-SDUR52Z5.js} +2 -2
  35. package/package.json +1 -1
  36. package/dist/apply-DPS23GLL.js +0 -12
  37. package/dist/chunk-UCIVU24B.js +0 -56
  38. package/dist/context-T52JWL3P.js +0 -10
  39. package/dist/generate-55MITRXU.js +0 -14
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/plan/secrets.ts
4
+ import { readFileSync } from "fs";
5
+ var UNRESOLVED_SOURCES = /* @__PURE__ */ new Set(["aws-sm", "aws-ssm", "gcp-sm", "azure-kv"]);
6
+ function resolveSecrets(config, secrets) {
7
+ const resolved = buildSecretMap(secrets);
8
+ return walkResolve(config, resolved);
9
+ }
10
+ function buildSecretMap(secrets) {
11
+ const map = /* @__PURE__ */ new Map();
12
+ for (const secret of secrets) {
13
+ if (UNRESOLVED_SOURCES.has(secret.source)) {
14
+ process.stderr.write(
15
+ `[clawops] Warning: secret "${secret.name}" uses source "${secret.source}" which is not yet resolved automatically. Set the value manually after deployment with: clawops config set
16
+ `
17
+ );
18
+ continue;
19
+ }
20
+ if (secret.source === "env") {
21
+ const ref = secret.ref ?? secret.name;
22
+ const val = process.env[ref];
23
+ if (val === void 0) {
24
+ process.stderr.write(
25
+ `[clawops] Warning: secret "${secret.name}" references env var "${ref}" which is not set. The $secret: ref will remain unresolved in the config.
26
+ `
27
+ );
28
+ continue;
29
+ }
30
+ map.set(secret.name, val);
31
+ } else if (secret.source === "file") {
32
+ if (!secret.ref) {
33
+ process.stderr.write(
34
+ `[clawops] Warning: secret "${secret.name}" has source "file" but no ref path.
35
+ `
36
+ );
37
+ continue;
38
+ }
39
+ try {
40
+ map.set(secret.name, readFileSync(secret.ref, "utf-8").trim());
41
+ } catch (err) {
42
+ process.stderr.write(
43
+ `[clawops] Warning: cannot read secret "${secret.name}" from file "${secret.ref}": ${err.message}
44
+ `
45
+ );
46
+ }
47
+ }
48
+ }
49
+ return map;
50
+ }
51
+ function walkResolve(value, secrets) {
52
+ if (typeof value === "string") {
53
+ const match = /^\$secret:(.+)$/.exec(value);
54
+ if (match) {
55
+ const name = match[1];
56
+ const resolved = secrets.get(name);
57
+ if (resolved !== void 0) return resolved;
58
+ return value;
59
+ }
60
+ return value;
61
+ }
62
+ if (Array.isArray(value)) {
63
+ return value.map((item) => walkResolve(item, secrets));
64
+ }
65
+ if (value !== null && typeof value === "object") {
66
+ const result = {};
67
+ for (const [k, v] of Object.entries(value)) {
68
+ result[k] = walkResolve(v, secrets);
69
+ }
70
+ return result;
71
+ }
72
+ return value;
73
+ }
74
+
75
+ export {
76
+ resolveSecrets
77
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getConfigDir
4
- } from "./chunk-ALSUDYA7.js";
4
+ } from "./chunk-CX5SL5HP.js";
5
5
 
6
6
  // src/providers/local/state.ts
7
7
  import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  StateError,
4
4
  UsageError
5
- } from "./chunk-ZSE4QRKE.js";
5
+ } from "./chunk-KGXPLI7W.js";
6
6
 
7
7
  // src/config/store.ts
8
8
  import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
@@ -19,6 +19,11 @@ var UsageError = class extends ClawopsError {
19
19
  super(message, "UsageError", 2, false);
20
20
  }
21
21
  };
22
+ var AuthError = class extends ClawopsError {
23
+ constructor(message) {
24
+ super(message, "AuthError", 3, false);
25
+ }
26
+ };
22
27
  var StateError = class extends ClawopsError {
23
28
  constructor(message) {
24
29
  super(message, "StateError", 4, false);
@@ -34,11 +39,24 @@ var NetworkError = class extends ClawopsError {
34
39
  super(message, "NetworkError", 6, true);
35
40
  }
36
41
  };
42
+ var OperationalError = class extends ClawopsError {
43
+ constructor(message, retryable = false) {
44
+ super(message, "OperationalError", 1, retryable);
45
+ }
46
+ };
47
+ var CancelledError = class extends ClawopsError {
48
+ constructor(message = "Operation cancelled") {
49
+ super(message, "CancelledError", 130, true);
50
+ }
51
+ };
37
52
 
38
53
  export {
39
54
  ClawopsError,
40
55
  UsageError,
56
+ AuthError,
41
57
  StateError,
42
58
  ProviderError,
43
- NetworkError
59
+ NetworkError,
60
+ OperationalError,
61
+ CancelledError
44
62
  };
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ validatePlan
4
+ } from "./chunk-YTH4L2GN.js";
5
+ import {
6
+ resolveSecrets
7
+ } from "./chunk-BOPSG2LI.js";
8
+ import {
9
+ buildContext
10
+ } from "./chunk-ACYJBSLJ.js";
11
+ import {
12
+ atomicWriteConfig,
13
+ deepMerge,
14
+ readRemoteConfig,
15
+ restartGateway
16
+ } from "./chunk-UDNZUSKA.js";
17
+ import {
18
+ UsageError
19
+ } from "./chunk-KGXPLI7W.js";
20
+
21
+ // src/plan/apply.ts
22
+ async function applyPlan(plan, opts) {
23
+ const validation = validatePlan(plan);
24
+ if (!validation.ok) {
25
+ throw new UsageError(
26
+ `Invalid deploy plan:
27
+ ${validation.errors.join("\n")}`
28
+ );
29
+ }
30
+ if (plan.spec.provider === "local") {
31
+ throw new UsageError(
32
+ "plan/apply is not supported for the local provider. Use `clawops up` directly."
33
+ );
34
+ }
35
+ const ctx = buildContext({
36
+ stack: plan.spec.stackName,
37
+ provider: plan.spec.provider
38
+ });
39
+ const stack = await ctx.getStack();
40
+ await stack.setConfig("instanceType", { value: plan.spec.instanceType });
41
+ if (plan.spec.region) {
42
+ await stack.setConfig("region", { value: plan.spec.region });
43
+ }
44
+ await stack.setConfig("openclawVersion", { value: plan.spec.openclaw.version });
45
+ const modelProvider = plan.spec.openclaw.config?.["models"]?.["provider"];
46
+ if (modelProvider === "bedrock") {
47
+ await stack.setConfig("bedrockEnabled", { value: "true" });
48
+ }
49
+ if (plan.metadata.stackVersion !== void 0) {
50
+ const currentInfo = await stack.info();
51
+ if (currentInfo !== void 0 && currentInfo.version !== plan.metadata.stackVersion) {
52
+ process.stderr.write(
53
+ `
54
+ Warning: stack "${plan.spec.stackName}" has changed since this plan was generated (plan version: ${plan.metadata.stackVersion}, current: ${currentInfo.version}).
55
+ The diff you reviewed may no longer reflect what will be applied.
56
+
57
+ `
58
+ );
59
+ if (opts?.confirmDrift) {
60
+ await opts.confirmDrift();
61
+ }
62
+ }
63
+ }
64
+ const start = Date.now();
65
+ const result = await stack.up({ onOutput: opts?.onOutput, signal: opts?.signal });
66
+ const outputs = Object.fromEntries(
67
+ Object.entries(result.outputs).map(([k, v]) => [k, v.value])
68
+ );
69
+ const changeSummary = {};
70
+ if (result.summary.resourceChanges) {
71
+ for (const [op, count] of Object.entries(result.summary.resourceChanges)) {
72
+ changeSummary[op] = count;
73
+ }
74
+ }
75
+ const hasOverlay = plan.spec.openclaw.config !== void 0 || plan.spec.openclaw.channels !== void 0;
76
+ if (hasOverlay) {
77
+ await applyConfigOverlay(plan, outputs, ctx, opts?.signal);
78
+ }
79
+ return {
80
+ outputs,
81
+ changeSummary,
82
+ durationMs: Date.now() - start
83
+ };
84
+ }
85
+ async function applyConfigOverlay(plan, outputs, ctx, signal) {
86
+ const { connect } = await import("./ssh-IQXNME3D.js");
87
+ const connInfo = ctx.adapter.getConnectionInfo(outputs);
88
+ const session = await connect({
89
+ host: connInfo.host,
90
+ port: connInfo.port,
91
+ user: connInfo.user,
92
+ privateKeyPath: connInfo.privateKeyPath,
93
+ knownHostsPath: connInfo.knownHostsPath,
94
+ signal
95
+ });
96
+ try {
97
+ const remote = await readRemoteConfig(session, signal);
98
+ const configOverlay = plan.spec.openclaw.config ?? {};
99
+ const resolvedOverlay = resolveSecrets(
100
+ configOverlay,
101
+ plan.spec.secrets ?? []
102
+ );
103
+ const merged = deepMerge(remote, {
104
+ ...resolvedOverlay,
105
+ ...plan.spec.openclaw.channels !== void 0 ? { channels: plan.spec.openclaw.channels } : {}
106
+ });
107
+ await atomicWriteConfig(session, merged, signal);
108
+ await restartGateway(session, signal);
109
+ } finally {
110
+ session.close();
111
+ }
112
+ }
113
+
114
+ export {
115
+ applyPlan
116
+ };
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/mcp-apps.ts
4
+ import { execSync } from "child_process";
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+ var MCP_APPS = [
9
+ {
10
+ id: "claude-desktop",
11
+ name: "Claude Desktop",
12
+ configPath: () => process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json") : path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json"),
13
+ isInstalled: () => existsSync(
14
+ process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "Claude") : path.join(os.homedir(), ".config", "Claude")
15
+ )
16
+ },
17
+ {
18
+ id: "claude-code",
19
+ name: "Claude Code",
20
+ configPath: () => path.join(os.homedir(), ".claude.json"),
21
+ isInstalled: () => {
22
+ if (existsSync(path.join(os.homedir(), ".claude.json"))) return true;
23
+ try {
24
+ execSync("claude --version", { stdio: "ignore" });
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ },
31
+ {
32
+ id: "cursor",
33
+ name: "Cursor",
34
+ configPath: () => path.join(os.homedir(), ".cursor", "mcp.json"),
35
+ isInstalled: () => existsSync(path.join(os.homedir(), ".cursor"))
36
+ },
37
+ {
38
+ id: "windsurf",
39
+ name: "Windsurf",
40
+ configPath: () => path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"),
41
+ isInstalled: () => existsSync(path.join(os.homedir(), ".codeium", "windsurf"))
42
+ },
43
+ {
44
+ id: "vscode",
45
+ name: "VS Code",
46
+ configPath: () => process.platform === "linux" ? path.join(os.homedir(), ".config", "Code", "User", "mcp.json") : path.join(os.homedir(), "Library", "Application Support", "Code", "User", "mcp.json"),
47
+ isInstalled: () => existsSync(
48
+ process.platform === "linux" ? path.join(os.homedir(), ".config", "Code") : path.join(os.homedir(), "Library", "Application Support", "Code")
49
+ )
50
+ },
51
+ {
52
+ id: "zed",
53
+ name: "Zed",
54
+ configPath: () => path.join(os.homedir(), ".config", "zed", "settings.json"),
55
+ isInstalled: () => existsSync(path.join(os.homedir(), ".config", "zed")),
56
+ configKey: "context_servers",
57
+ entryExtra: { type: "stdio" }
58
+ }
59
+ ];
60
+ function buildMcpEntry() {
61
+ try {
62
+ const bin = execSync("which clawops", {
63
+ encoding: "utf-8",
64
+ stdio: ["ignore", "pipe", "ignore"]
65
+ }).trim();
66
+ if (bin) return { command: bin, args: ["mcp", "serve", "--read-only"], resolved: true };
67
+ } catch {
68
+ }
69
+ return { command: "clawops", args: ["mcp", "serve", "--read-only"], resolved: false };
70
+ }
71
+ function writeAppConfigs(apps, entry) {
72
+ return apps.map((app) => {
73
+ const cfgPath = app.configPath();
74
+ try {
75
+ let config = {};
76
+ if (existsSync(cfgPath)) {
77
+ try {
78
+ config = JSON.parse(readFileSync(cfgPath, "utf-8"));
79
+ } catch {
80
+ }
81
+ }
82
+ mkdirSync(path.dirname(cfgPath), { recursive: true });
83
+ const key = app.configKey ?? "mcpServers";
84
+ const servers = config[key] ?? {};
85
+ servers["clawops"] = { ...entry, ...app.entryExtra ?? {} };
86
+ config[key] = servers;
87
+ writeFileSync(cfgPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
88
+ return { app, configPath: cfgPath, ok: true };
89
+ } catch (err) {
90
+ return { app, configPath: cfgPath, ok: false, error: err.message };
91
+ }
92
+ });
93
+ }
94
+
95
+ export {
96
+ MCP_APPS,
97
+ buildMcpEntry,
98
+ writeAppConfigs
99
+ };
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildContext
4
+ } from "./chunk-ACYJBSLJ.js";
5
+ import {
6
+ acquireSession,
7
+ drainPool
8
+ } from "./chunk-ZVOEQCNW.js";
9
+ import {
10
+ OPENCLAW_CONFIG,
11
+ atomicWriteConfig,
12
+ restartGateway
13
+ } from "./chunk-UDNZUSKA.js";
14
+ import {
15
+ StateError
16
+ } from "./chunk-KGXPLI7W.js";
17
+
18
+ // src/mcp/tools/_conn.ts
19
+ async function resolveConn(ctx) {
20
+ if (ctx.adapter.name === "local") {
21
+ const state = ctx.localState;
22
+ if (!state) throw new StateError("Stack has no local state \u2014 run `clawops up` first.");
23
+ return {
24
+ host: state.sshHost,
25
+ port: state.sshPort,
26
+ user: state.sshUser,
27
+ privateKeyPath: state.privateKeyPath,
28
+ knownHostsPath: state.knownHostsPath
29
+ };
30
+ }
31
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
32
+ const stack = await ctx.getStack();
33
+ const outputMap = await stack.outputs();
34
+ const outputs = Object.fromEntries(
35
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
36
+ );
37
+ if (!outputs["publicIp"]) {
38
+ throw new StateError("Stack has no outputs \u2014 run `clawops up` first.");
39
+ }
40
+ const base = extractBaseOutputs(outputs);
41
+ return ctx.adapter.getConnectionInfo({
42
+ ...base,
43
+ privateKeyPath: ctx.config.ssh.keyPath,
44
+ knownHostsPath: ctx.config.ssh.knownHostsPath
45
+ });
46
+ }
47
+ function errText(message) {
48
+ return { content: [{ type: "text", text: message }], isError: true };
49
+ }
50
+ function okText(t) {
51
+ return { content: [{ type: "text", text: t }] };
52
+ }
53
+
54
+ // src/mcp/tools/cli/config.ts
55
+ var VALID_AUTH_MODES = /* @__PURE__ */ new Set(["none", "token", "password", "trusted-proxy"]);
56
+ async function handleConfigGet(input, _server) {
57
+ const ac = new AbortController();
58
+ const ctx = buildContext({ stack: input.stackName });
59
+ const conn = await resolveConn(ctx);
60
+ const { session, release } = await acquireSession(conn);
61
+ try {
62
+ const result = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
63
+ let cfg;
64
+ try {
65
+ cfg = JSON.parse(result.stdout);
66
+ } catch {
67
+ return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${result.stderr || result.stdout}`);
68
+ }
69
+ const value = input.key ? getPath(cfg, input.key) : cfg;
70
+ return okText(JSON.stringify(value, null, 2));
71
+ } finally {
72
+ release();
73
+ drainPool();
74
+ }
75
+ }
76
+ async function handleConfigSet(input, server) {
77
+ const elicit = await server.server.elicitInput({
78
+ message: `Set ${input.key} = ${input.value} on stack "${input.stackName ?? "default"}"?`,
79
+ requestedSchema: {
80
+ type: "object",
81
+ properties: { confirmed: { type: "boolean", title: "Confirm config change" } },
82
+ required: ["confirmed"]
83
+ }
84
+ });
85
+ if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
86
+ return okText("Config change cancelled.");
87
+ }
88
+ const ac = new AbortController();
89
+ const ctx = buildContext({ stack: input.stackName });
90
+ const conn = await resolveConn(ctx);
91
+ const { session, release } = await acquireSession(conn);
92
+ try {
93
+ const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
94
+ let cfg;
95
+ try {
96
+ cfg = JSON.parse(readResult.stdout);
97
+ } catch {
98
+ return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr}`);
99
+ }
100
+ let parsedValue = input.value;
101
+ try {
102
+ parsedValue = JSON.parse(input.value);
103
+ } catch {
104
+ }
105
+ setPath(cfg, input.key, parsedValue);
106
+ try {
107
+ await atomicWriteConfig(session, cfg, ac.signal);
108
+ } catch (err) {
109
+ return errText(`Failed to write config: ${err.message}`);
110
+ }
111
+ let note = "";
112
+ if (input.restart) {
113
+ try {
114
+ await restartGateway(session, ac.signal);
115
+ note = " (gateway restarted)";
116
+ } catch (err) {
117
+ return errText(`Gateway restart failed: ${err.message}`);
118
+ }
119
+ }
120
+ return okText(`Config set: ${input.key}${note}`);
121
+ } finally {
122
+ release();
123
+ drainPool();
124
+ }
125
+ }
126
+ async function handleConfigUnset(input, server) {
127
+ const elicit = await server.server.elicitInput({
128
+ message: `Remove config key "${input.key}" on stack "${input.stackName ?? "default"}"?`,
129
+ requestedSchema: {
130
+ type: "object",
131
+ properties: { confirmed: { type: "boolean", title: "Confirm key removal" } },
132
+ required: ["confirmed"]
133
+ }
134
+ });
135
+ if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
136
+ return okText("Config unset cancelled.");
137
+ }
138
+ const ac = new AbortController();
139
+ const ctx = buildContext({ stack: input.stackName });
140
+ const conn = await resolveConn(ctx);
141
+ const { session, release } = await acquireSession(conn);
142
+ try {
143
+ const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
144
+ let cfg;
145
+ try {
146
+ cfg = JSON.parse(readResult.stdout);
147
+ } catch {
148
+ return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr}`);
149
+ }
150
+ deletePath(cfg, input.key);
151
+ try {
152
+ await atomicWriteConfig(session, cfg, ac.signal);
153
+ } catch (err) {
154
+ return errText(`Failed to write config: ${err.message}`);
155
+ }
156
+ let note = "";
157
+ if (input.restart) {
158
+ try {
159
+ await restartGateway(session, ac.signal);
160
+ note = " (gateway restarted)";
161
+ } catch (err) {
162
+ return errText(`Gateway restart failed: ${err.message}`);
163
+ }
164
+ }
165
+ return okText(`Config key removed: ${input.key}${note}`);
166
+ } finally {
167
+ release();
168
+ drainPool();
169
+ }
170
+ }
171
+ async function handleConfigValidate(input, _server) {
172
+ const ac = new AbortController();
173
+ const ctx = buildContext({ stack: input.stackName });
174
+ const conn = await resolveConn(ctx);
175
+ const { session, release } = await acquireSession(conn);
176
+ try {
177
+ const result = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
178
+ let cfg;
179
+ try {
180
+ cfg = JSON.parse(result.stdout);
181
+ } catch {
182
+ return okText(JSON.stringify({ valid: false, issues: [`Invalid JSON: ${result.stderr || result.stdout}`] }));
183
+ }
184
+ const issues = validateOpenclawConfig(cfg);
185
+ return okText(JSON.stringify({ valid: issues.length === 0, issues }));
186
+ } finally {
187
+ release();
188
+ drainPool();
189
+ }
190
+ }
191
+ function getPath(obj, dotKey) {
192
+ return dotKey.split(".").reduce((cur, k) => {
193
+ if (cur !== null && typeof cur === "object") return cur[k];
194
+ return void 0;
195
+ }, obj);
196
+ }
197
+ function setPath(obj, dotKey, value) {
198
+ const keys = dotKey.split(".");
199
+ let cur = obj;
200
+ for (let i = 0; i < keys.length - 1; i++) {
201
+ const k = keys[i];
202
+ if (typeof cur[k] !== "object" || cur[k] === null) cur[k] = {};
203
+ cur = cur[k];
204
+ }
205
+ cur[keys[keys.length - 1]] = value;
206
+ }
207
+ function deletePath(obj, dotKey) {
208
+ const keys = dotKey.split(".");
209
+ let cur = obj;
210
+ for (let i = 0; i < keys.length - 1; i++) {
211
+ const k = keys[i];
212
+ if (typeof cur[k] !== "object" || cur[k] === null) return;
213
+ cur = cur[k];
214
+ }
215
+ delete cur[keys[keys.length - 1]];
216
+ }
217
+ function validateOpenclawConfig(cfg) {
218
+ const issues = [];
219
+ if ("version" in cfg) {
220
+ issues.push(
221
+ "Top-level 'version' is not a valid OpenClaw config key. Use 'meta.lastTouchedVersion' (string) instead."
222
+ );
223
+ }
224
+ if ("channels" in cfg && Array.isArray(cfg["channels"])) {
225
+ issues.push(
226
+ `'channels' must be an object keyed by provider name (e.g. {"discord":{...}}), not an array.`
227
+ );
228
+ }
229
+ const meta = cfg["meta"];
230
+ if (meta !== void 0 && (typeof meta !== "object" || Array.isArray(meta) || meta === null)) {
231
+ issues.push("'meta' must be an object.");
232
+ } else if (meta && typeof meta === "object") {
233
+ const ltv = meta["lastTouchedVersion"];
234
+ if (ltv !== void 0 && typeof ltv !== "string") {
235
+ issues.push("'meta.lastTouchedVersion' must be a string.");
236
+ }
237
+ }
238
+ const gateway = cfg["gateway"];
239
+ if (gateway !== void 0 && typeof gateway === "object" && !Array.isArray(gateway) && gateway !== null) {
240
+ const gw = gateway;
241
+ if ("port" in gw && typeof gw["port"] !== "number") {
242
+ issues.push("'gateway.port' must be a number.");
243
+ }
244
+ const auth = gw["auth"];
245
+ if (auth !== void 0 && typeof auth === "object" && !Array.isArray(auth) && auth !== null) {
246
+ const mode = auth["mode"];
247
+ if (mode !== void 0 && !VALID_AUTH_MODES.has(mode)) {
248
+ issues.push(
249
+ `'gateway.auth.mode' must be one of: ${[...VALID_AUTH_MODES].join(", ")}. Got: "${mode}".`
250
+ );
251
+ }
252
+ }
253
+ }
254
+ return issues;
255
+ }
256
+
257
+ export {
258
+ resolveConn,
259
+ errText,
260
+ okText,
261
+ handleConfigGet,
262
+ handleConfigSet,
263
+ handleConfigUnset,
264
+ handleConfigValidate,
265
+ validateOpenclawConfig
266
+ };
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- buildContext
4
- } from "./chunk-PERSDQMT.js";
5
2
  import {
6
3
  validatePlan
7
4
  } from "./chunk-YTH4L2GN.js";
5
+ import {
6
+ buildContext
7
+ } from "./chunk-ACYJBSLJ.js";
8
8
  import {
9
9
  getConfig
10
- } from "./chunk-ALSUDYA7.js";
10
+ } from "./chunk-CX5SL5HP.js";
11
11
  import {
12
12
  UsageError
13
- } from "./chunk-ZSE4QRKE.js";
13
+ } from "./chunk-KGXPLI7W.js";
14
14
 
15
15
  // src/plan/generate.ts
16
16
  import { randomUUID } from "crypto";
@@ -45,7 +45,7 @@ async function generatePlan(intent, _opts) {
45
45
  "plan/apply is not supported for the local provider. Use `clawops up` directly."
46
46
  );
47
47
  }
48
- const { version } = await import("./package-FA6UOG2I.js");
48
+ const { version } = await import("./package-SE3M4NJA.js");
49
49
  const config = getConfig();
50
50
  const instanceType = intent.instanceType ?? "small";
51
51
  const openclawVersion = intent.openclawVersion ?? "latest";
@@ -90,6 +90,10 @@ async function generatePlan(intent, _opts) {
90
90
  diff.totalChanges = (summary["create"] ?? 0) + (summary["update"] ?? 0) + (summary["delete"] ?? 0);
91
91
  }
92
92
  plan.diff = diff;
93
+ const info = await stack.info();
94
+ if (info !== void 0) {
95
+ plan.metadata.stackVersion = info.version;
96
+ }
93
97
  } catch (err) {
94
98
  process.stderr.write(
95
99
  `[clawops] Warning: preview failed, diff section omitted: ${err instanceof Error ? err.message : String(err)}