@clawops/cli 1.2.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.
package/README.md CHANGED
@@ -45,78 +45,140 @@ and Cursor drive them through typed MCP tools with explicit safety controls.
45
45
 
46
46
  ---
47
47
 
48
- ## Quick Start — local VM (fastest path)
48
+ ## Quick Start
49
49
 
50
- The local provider needs only a Linux host reachable over SSH — no cloud account required.
50
+ ```bash
51
+ npm install -g @clawops/cli
52
+ clawops setup
53
+ ```
54
+
55
+ `clawops setup` is an interactive wizard that gets OpenClaw running in about 2 minutes. It
56
+ handles everything in one flow — no config files to write by hand, no commands to memorize.
57
+
58
+ ### What the wizard does
59
+
60
+ **Step 1 — Choose a deployment target**
61
+
62
+ Pick an existing server you can SSH into (Linux or macOS), or a new cloud VM on AWS, GCP, or
63
+ Azure. Cloud deployments walk you through authenticating with the provider CLI if you aren't
64
+ already signed in.
65
+
66
+ **Step 2 — Pick an LLM provider**
67
+
68
+ Choose from Anthropic, OpenAI, Amazon Bedrock, Ollama, or others. The wizard prompts for your
69
+ API key and saves it locally (in `~/.clawops/secrets/`, chmod 600) — it is never sent anywhere
70
+ except to OpenClaw on the target host when the config is applied.
71
+
72
+ **Step 3 — Add chat integrations (optional)**
73
+
74
+ Select any combination of Discord, Telegram, Slack, WhatsApp, or Teams. The wizard collects each
75
+ integration's bot token the same way as the API key — paste it in, reference an env var, or point
76
+ to a file.
77
+
78
+ **Step 4 — Wire your AI editor**
79
+
80
+ Select which AI apps should have access to clawops — Claude Desktop, Claude Code, Cursor,
81
+ Windsurf, VS Code, and Zed are all supported. The wizard writes an MCP server entry into each
82
+ app's config file using the absolute binary path so the app can launch it independently.
51
83
 
52
- **Prerequisites:** Node.js 22, an SSH key, a Linux host (Ubuntu/Debian/RHEL) you can reach.
84
+ **Step 5 Deploy**
85
+
86
+ The wizard bootstraps OpenClaw on the target host over SSH (installs Docker, pulls the image,
87
+ starts the container), applies your LLM and integration config, generates a gateway auth token,
88
+ and prints a direct dashboard URL:
89
+
90
+ ```
91
+ ✔ All done! OpenClaw is running.
92
+ ℹ Open dashboard: http://192.168.1.50:18789?token=<your-token>
93
+ ℹ Token saved to ~/.clawops/secrets/GATEWAY_TOKEN_my-stack
94
+ ```
95
+
96
+ **Prerequisites:** Node.js ≥ 22, an SSH key, and either an SSH-reachable Linux/macOS host or a
97
+ cloud account with CLI credentials configured (`aws configure`, `gcloud auth login`, or `az login`).
98
+
99
+ For a full narrated walkthrough with example output, see [`docs/demo-script.md`](docs/demo-script.md).
100
+
101
+ ---
102
+
103
+ ### Manual setup — existing server
104
+
105
+ If you prefer step-by-step control, or are adding clawops to an already-running deployment:
53
106
 
54
107
  ```bash
55
108
  npm install -g @clawops/cli
56
109
 
57
- # Check your environment
58
- clawops doctor
110
+ clawops doctor # verify environment
59
111
 
60
- # Configure clawops for a local host
61
112
  clawops init --provider local --host 192.168.1.50 --user ubuntu --key-path ~/.ssh/id_ed25519
62
-
63
- # Bootstrap OpenClaw on the host (installs Docker + OpenClaw over SSH)
64
- clawops up
65
-
66
- # Verify it's running
113
+ clawops up # installs Docker + OpenClaw over SSH
67
114
  clawops status
68
-
69
- # Start the MCP server for Claude Code
70
- clawops mcp serve
71
115
  ```
72
116
 
73
- See [`docs/examples/local-vm.md`](docs/examples/local-vm.md) for the full walkthrough including
74
- SSH prerequisites, firewall setup, and troubleshooting.
117
+ See [`docs/examples/local-vm.md`](docs/examples/local-vm.md) for SSH prerequisites, firewall
118
+ setup, and troubleshooting.
75
119
 
76
- ## Quick Start — cloud (AWS)
120
+ ### Manual setup — cloud (AWS)
77
121
 
78
122
  ```bash
79
123
  npm install -g @clawops/cli
80
- # or: npx @clawops/cli
81
124
 
82
125
  # Requires AWS credentials in your environment (AWS_PROFILE or ~/.aws/credentials)
83
126
  clawops init --provider aws
84
127
 
85
- # Edit ~/.clawops/config.json — set stateUrl to your S3 bucket:
86
- # "stateUrl": "s3://my-clawops-state"
128
+ # Edit ~/.clawops/config.json — set stateUrl to your S3 bucket
87
129
 
88
- # Generate a deploy plan (runs pulumi preview internally)
89
130
  clawops plan --provider aws --stack default --out /tmp/plan.json
90
-
91
- # Review the plan, then apply
92
131
  clawops apply /tmp/plan.json
93
132
  ```
94
133
 
95
134
  ---
96
135
 
97
- ## Connect Claude Code
136
+ ## Connect an AI editor
137
+
138
+ The `setup` wizard handles this automatically (Step 4). To wire or re-wire editors at any time:
139
+
140
+ ```bash
141
+ clawops mcp install
142
+ ```
143
+
144
+ This opens the same interactive checkbox used in the wizard — select Claude Desktop, Claude Code,
145
+ Cursor, Windsurf, VS Code, or Zed and clawops writes the MCP entry into each app's config using
146
+ the correct absolute binary path.
98
147
 
99
- Add to your Claude Code MCP config (`~/.claude.json` or project `.mcp.json`):
148
+ To add the entry manually instead, paste this into your editor's MCP config:
100
149
 
101
150
  ```json
102
151
  {
103
152
  "mcpServers": {
104
153
  "clawops": {
105
- "command": "clawops",
154
+ "command": "/path/to/clawops",
106
155
  "args": ["mcp", "serve", "--read-only"]
107
156
  }
108
157
  }
109
158
  }
110
159
  ```
111
160
 
161
+ Replace `/path/to/clawops` with the output of `which clawops`. Config file locations:
162
+
163
+ | App | Path |
164
+ |---|---|
165
+ | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
166
+ | Claude Desktop (Linux) | `~/.config/Claude/claude_desktop_config.json` |
167
+ | Claude Code | `~/.claude.json` |
168
+ | Cursor | `~/.cursor/mcp.json` |
169
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
170
+ | VS Code (macOS) | `~/Library/Application Support/Code/User/mcp.json` |
171
+ | VS Code (Linux) | `~/.config/Code/User/mcp.json` |
172
+ | Zed | `~/.config/zed/settings.json` (key: `context_servers`) |
173
+
112
174
  **Start with `--read-only`** — it enables status, logs, config reads, and diagnostics while
113
- blocking any operation that mutates infrastructure. Remove `--read-only` only once you understand
114
- which tools are destructive and have reviewed [`docs/security/mcp-safety.md`](docs/security/mcp-safety.md).
175
+ blocking mutations. Remove it only after reviewing
176
+ [`docs/security/mcp-safety.md`](docs/security/mcp-safety.md).
115
177
 
116
178
  Destructive tools (`clawops_destroy`, `clawops_up`, `clawops_config_set`, etc.) require explicit
117
- confirmation from the agent before executing — they will never run silently.
179
+ confirmation before executing — they will never run silently.
118
180
 
119
- For Cursor, VS Code, or HTTP mode setup see [`docs/mcp/`](docs/mcp/).
181
+ For HTTP mode setup see [`docs/mcp/`](docs/mcp/).
120
182
 
121
183
  ---
122
184
 
@@ -160,7 +222,9 @@ clawops down --yes # Destroy local-provider stack
160
222
  | `backup` | Create or restore an OpenClaw state backup |
161
223
  | `stacks` | List named stacks and their state |
162
224
  | `doctor` | Check Node version, config, SSH key, provider credentials, and Pulumi home |
163
- | `mcp` | Start the embedded MCP server (`mcp serve`) |
225
+ | `mcp serve` | Start the embedded MCP server (stdio or HTTP) |
226
+ | `mcp install` | Interactively wire clawops into AI editors |
227
+ | `help` | List all commands and global flags |
164
228
 
165
229
  Full flag reference: `clawops <command> --help`
166
230
 
@@ -198,23 +262,29 @@ See [`docs/plan-apply.md`](docs/plan-apply.md) for full semantics, drift guidanc
198
262
  clawops ships an embedded [MCP](https://modelcontextprotocol.io/) server. Claude Code, Cursor, and
199
263
  any MCP-compatible agent can drive deployments without leaving the chat interface.
200
264
 
201
- ### Stdio mode (Claude Code / VS Code)
265
+ ### Wire your editor
202
266
 
203
- ```json
204
- {
205
- "mcpServers": {
206
- "clawops": {
207
- "command": "clawops",
208
- "args": ["mcp", "serve", "--read-only"]
209
- }
210
- }
211
- }
267
+ ```bash
268
+ clawops mcp install # interactive checkbox — writes config for selected apps
269
+ ```
270
+
271
+ The wizard resolves the absolute binary path automatically so app launchers can find `clawops`
272
+ without inheriting your shell's `PATH`. See [Connect an AI editor](#connect-an-ai-editor) above
273
+ for manual config paths.
274
+
275
+ ### Stdio mode (Claude Code / Cursor / VS Code)
276
+
277
+ Start the server manually or confirm your config is correct:
278
+
279
+ ```bash
280
+ clawops mcp serve --read-only # safe for first evaluation
281
+ clawops mcp serve # full mode — enables provisioning, config write, ssh exec
212
282
  ```
213
283
 
214
284
  ### HTTP mode (remote / multi-client)
215
285
 
216
286
  ```bash
217
- clawops mcp serve --http --port 3333 --bind 127.0.0.1
287
+ clawops mcp serve --http 3333 --bind 127.0.0.1
218
288
  # MCP HTTP server listening on 127.0.0.1:3333
219
289
  ```
220
290
 
@@ -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
+ };
@@ -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-3JZ767DN.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";
package/dist/cli.js CHANGED
@@ -2,6 +2,11 @@
2
2
  import {
3
3
  validateOpenclawConfig
4
4
  } from "./chunk-QJ6ERXHN.js";
5
+ import {
6
+ MCP_APPS,
7
+ buildMcpEntry,
8
+ writeAppConfigs
9
+ } from "./chunk-OIGTOLB3.js";
5
10
  import "./chunk-ACYJBSLJ.js";
6
11
  import "./chunk-BRPU7AQC.js";
7
12
  import "./chunk-ZVOEQCNW.js";
@@ -19,7 +24,7 @@ import {
19
24
  } from "./chunk-KGXPLI7W.js";
20
25
 
21
26
  // src/cli/index.ts
22
- import { defineCommand as defineCommand19, runMain } from "citty";
27
+ import { defineCommand as defineCommand20, runMain } from "citty";
23
28
 
24
29
  // src/cli/commands/init.ts
25
30
  import { defineCommand } from "citty";
@@ -495,7 +500,7 @@ var plan_default = defineCommand5({
495
500
  },
496
501
  async run({ args }) {
497
502
  const { buildContext } = await import("./context-VUAYRAJY.js");
498
- const { generatePlan } = await import("./generate-WG5VONSE.js");
503
+ const { generatePlan } = await import("./generate-PZPLG3ZM.js");
499
504
  const ctx = buildContext(args);
500
505
  if (ctx.adapter.name === "local") {
501
506
  throw new UsageError(
@@ -1782,13 +1787,11 @@ var doctor_default = defineCommand16({
1782
1787
 
1783
1788
  // src/cli/commands/mcp.ts
1784
1789
  import { defineCommand as defineCommand17 } from "citty";
1785
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1786
- import path3 from "path";
1787
- import os from "os";
1788
- var mcp_default = defineCommand17({
1790
+ import process19 from "process";
1791
+ var serveCmd = defineCommand17({
1789
1792
  meta: {
1790
- name: "mcp",
1791
- description: "MCP server operations (serve | install)"
1793
+ name: "serve",
1794
+ description: "Start the clawops MCP server"
1792
1795
  },
1793
1796
  args: {
1794
1797
  http: { type: "string", description: "HTTP port for standalone mode" },
@@ -1796,25 +1799,10 @@ var mcp_default = defineCommand17({
1796
1799
  "read-only": { type: "boolean", description: "Only register read toolset" },
1797
1800
  "no-destructive": { type: "boolean", description: "Filter out destructive tools" },
1798
1801
  toolsets: { type: "string", description: "Comma-separated toolsets to enable" },
1799
- inspector: { type: "boolean", description: "Enable MCP inspector" },
1800
- claude: { type: "boolean", description: "Install for Claude Desktop" },
1801
- cursor: { type: "boolean", description: "Install for Cursor" },
1802
- vscode: { type: "boolean", description: "Install for VS Code" },
1803
- windsurf: { type: "boolean", description: "Install for Windsurf" },
1804
- zed: { type: "boolean", description: "Install for Zed" }
1802
+ inspector: { type: "boolean", description: "Enable MCP inspector" }
1805
1803
  },
1806
1804
  async run({ args }) {
1807
- const installFlags = ["claude", "cursor", "vscode", "windsurf", "zed"];
1808
- const requestedClients = installFlags.filter((f) => Boolean(args[f]));
1809
- if (requestedClients.length > 0) {
1810
- for (const client of requestedClients) {
1811
- installMcp(client);
1812
- process.stderr.write(`Installed MCP config for: ${client}
1813
- `);
1814
- }
1815
- return;
1816
- }
1817
- const { serveMcp } = await import("./server-QJRWK2RV.js");
1805
+ const { serveMcp } = await import("./server-4DNHLGEG.js");
1818
1806
  await serveMcp({
1819
1807
  port: args.http ? Number(args.http) : void 0,
1820
1808
  bind: args.bind,
@@ -1825,60 +1813,86 @@ var mcp_default = defineCommand17({
1825
1813
  });
1826
1814
  }
1827
1815
  });
1828
- var CLAWOPS_ENTRY = {
1829
- command: "clawops",
1830
- args: ["mcp", "serve"],
1831
- type: "stdio"
1832
- };
1833
- function getConfigPath(client) {
1834
- const home = os.homedir();
1835
- const isLinux = process.platform === "linux";
1836
- switch (client) {
1837
- case "claude":
1838
- return isLinux ? path3.join(home, ".config", "Claude", "claude_desktop_config.json") : path3.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1839
- case "cursor":
1840
- return path3.join(home, ".cursor", "mcp.json");
1841
- case "vscode":
1842
- return isLinux ? path3.join(home, ".config", "Code", "User", "mcp.json") : path3.join(home, "Library", "Application Support", "Code", "User", "mcp.json");
1843
- case "windsurf":
1844
- return path3.join(home, ".codeium", "windsurf", "mcp_config.json");
1845
- case "zed":
1846
- return path3.join(home, ".config", "zed", "settings.json");
1847
- }
1848
- }
1849
- function installMcp(client) {
1850
- const configPath = getConfigPath(client);
1851
- const dir = path3.dirname(configPath);
1852
- if (!existsSync2(dir)) {
1853
- mkdirSync3(dir, { recursive: true });
1854
- }
1855
- let config = {};
1856
- if (existsSync2(configPath)) {
1857
- try {
1858
- config = JSON.parse(readFileSync3(configPath, "utf-8"));
1859
- } catch {
1816
+ var installCmd = defineCommand17({
1817
+ meta: {
1818
+ name: "install",
1819
+ description: "Interactively wire clawops into AI editors (Claude Desktop, Claude Code, Cursor, \u2026)"
1820
+ },
1821
+ args: {},
1822
+ async run() {
1823
+ const inquirer = (await import("inquirer")).default;
1824
+ const { MCP_APPS: MCP_APPS2, buildMcpEntry: buildMcpEntry2, writeAppConfigs: writeAppConfigs2 } = await import("./mcp-apps-KY44ED3Y.js");
1825
+ const mcpEntry = buildMcpEntry2();
1826
+ if (!mcpEntry.resolved) {
1827
+ info("clawops is not installed globally \u2014 AI apps may not be able to start the MCP server.");
1828
+ info("Install it first: npm install -g @clawops/cli");
1829
+ info('The config will still be written now using "clawops" as the command name.\n');
1830
+ }
1831
+ info("Use \u2191\u2193 to move, Space to select/deselect, Enter to confirm.");
1832
+ const choices = MCP_APPS2.map((app) => ({
1833
+ name: app.isInstalled() ? app.name : `${app.name} (not detected \u2014 config will be written anyway)`,
1834
+ value: app.id,
1835
+ checked: app.isInstalled()
1836
+ }));
1837
+ const { selectedIds } = await inquirer.prompt([{
1838
+ type: "checkbox",
1839
+ name: "selectedIds",
1840
+ message: "Which AI editors should have access to clawops?",
1841
+ choices,
1842
+ pageSize: MCP_APPS2.length + 1
1843
+ }]);
1844
+ const selected = MCP_APPS2.filter((app) => selectedIds.includes(app.id));
1845
+ if (selected.length === 0) {
1846
+ info("No apps selected. Add this to an app's MCP config manually:");
1847
+ process19.stdout.write(JSON.stringify({
1848
+ mcpServers: { clawops: { command: mcpEntry.command, args: mcpEntry.args } }
1849
+ }, null, 2) + "\n\n");
1850
+ info("Config file locations:");
1851
+ for (const app of MCP_APPS2) {
1852
+ info(` ${app.name.padEnd(18)} ${app.configPath()}`);
1853
+ }
1854
+ return;
1855
+ }
1856
+ const results = writeAppConfigs2(selected, { command: mcpEntry.command, args: mcpEntry.args });
1857
+ for (const r of results) {
1858
+ if (r.ok) {
1859
+ success(`${r.app.name} configured (${r.configPath})`);
1860
+ } else {
1861
+ failure(`Could not configure ${r.app.name}: ${r.error ?? "unknown error"}`);
1862
+ }
1863
+ }
1864
+ const needsRestart = selected.filter((app) => app.id !== "claude-code");
1865
+ if (needsRestart.length > 0) {
1866
+ info(`Restart ${needsRestart.map((a) => a.name).join(", ")} to load the clawops tool.`);
1860
1867
  }
1861
1868
  }
1862
- if (client === "zed") {
1863
- const contextServers = config["context_servers"] ?? {};
1864
- contextServers["clawops"] = CLAWOPS_ENTRY;
1865
- config["context_servers"] = contextServers;
1866
- } else {
1867
- const mcpServers = config["mcpServers"] ?? {};
1868
- mcpServers["clawops"] = CLAWOPS_ENTRY;
1869
- config["mcpServers"] = mcpServers;
1869
+ });
1870
+ var mcp_default = defineCommand17({
1871
+ meta: {
1872
+ name: "mcp",
1873
+ description: "MCP server operations"
1874
+ },
1875
+ args: {},
1876
+ subCommands: {
1877
+ serve: serveCmd,
1878
+ install: installCmd
1879
+ },
1880
+ run() {
1881
+ process19.stdout.write("Usage: clawops mcp <serve | install>\n\n");
1882
+ process19.stdout.write(" serve Start the MCP server (stdio or HTTP)\n");
1883
+ process19.stdout.write(" install Wire clawops into AI editors interactively\n\n");
1884
+ process19.stdout.write("Run `clawops mcp <command> --help` for flags.\n");
1870
1885
  }
1871
- writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1872
- }
1886
+ });
1873
1887
 
1874
1888
  // src/cli/commands/setup.ts
1875
1889
  import { defineCommand as defineCommand18 } from "citty";
1876
- import process19 from "process";
1877
- import os2 from "os";
1890
+ import process20 from "process";
1891
+ import os from "os";
1878
1892
  import { randomBytes } from "crypto";
1879
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync3, mkdirSync as mkdirSync4 } from "fs";
1893
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
1880
1894
  import { execSync, spawnSync, spawn as spawn2 } from "child_process";
1881
- import path4 from "path";
1895
+ import path3 from "path";
1882
1896
  import { fileURLToPath } from "url";
1883
1897
  var setup_default = defineCommand18({
1884
1898
  meta: {
@@ -1895,16 +1909,16 @@ var setup_default = defineCommand18({
1895
1909
  const catalogs = loadCatalogs(yaml);
1896
1910
  const dryRun = Boolean(args["dry-run"]);
1897
1911
  const ac = new AbortController();
1898
- process19.on("SIGINT", () => {
1912
+ process20.on("SIGINT", () => {
1899
1913
  ac.abort();
1900
- process19.exit(130);
1914
+ process20.exit(130);
1901
1915
  });
1902
- process19.on("SIGTERM", () => {
1916
+ process20.on("SIGTERM", () => {
1903
1917
  ac.abort();
1904
- process19.exit(143);
1918
+ process20.exit(143);
1905
1919
  });
1906
- process19.stdout.write("\nWelcome to clawops setup. This wizard will help you deploy OpenClaw\n");
1907
- process19.stdout.write("and connect it to your AI tools. It takes about 2 minutes.\n\n");
1920
+ process20.stdout.write("\nWelcome to clawops setup. This wizard will help you deploy OpenClaw\n");
1921
+ process20.stdout.write("and connect it to your AI tools. It takes about 2 minutes.\n\n");
1908
1922
  const { deploymentType } = await inquirer.prompt([{
1909
1923
  type: "list",
1910
1924
  name: "deploymentType",
@@ -1916,7 +1930,7 @@ var setup_default = defineCommand18({
1916
1930
  }]);
1917
1931
  let provider;
1918
1932
  let localHost = "";
1919
- let localUser = os2.userInfo().username;
1933
+ let localUser = os.userInfo().username;
1920
1934
  let localKeyPath = detectSshKey();
1921
1935
  let localPort = 22;
1922
1936
  let localSudoPassword = "";
@@ -1932,12 +1946,12 @@ var setup_default = defineCommand18({
1932
1946
  ]
1933
1947
  }]);
1934
1948
  provider = answer.provider;
1935
- process19.stdout.write("\n");
1949
+ process20.stdout.write("\n");
1936
1950
  await ensureCloudAuth(provider, inquirer);
1937
1951
  } else {
1938
1952
  provider = "local";
1939
1953
  info("We'll connect to your server over SSH to install and configure OpenClaw.\n");
1940
- if (!existsSync3(localKeyPath)) {
1954
+ if (!existsSync2(localKeyPath)) {
1941
1955
  info(`No SSH key found at ${localKeyPath}.`);
1942
1956
  const { genKey } = await inquirer.prompt([{
1943
1957
  type: "confirm",
@@ -1973,7 +1987,7 @@ var setup_default = defineCommand18({
1973
1987
  name: "keyPath",
1974
1988
  message: `SSH private key file: (the key file on your computer used to log in \u2014 e.g. ~/.ssh/id_ed25519)`,
1975
1989
  default: localKeyPath,
1976
- validate: (v) => existsSync3(v.trim()) || `File not found: ${v.trim()}`
1990
+ validate: (v) => existsSync2(v.trim()) || `File not found: ${v.trim()}`
1977
1991
  }
1978
1992
  ]);
1979
1993
  localHost = localAnswers.host;
@@ -1988,7 +2002,7 @@ var setup_default = defineCommand18({
1988
2002
  }]);
1989
2003
  localSudoPassword = sudoPass;
1990
2004
  }
1991
- process19.stdout.write("\n");
2005
+ process20.stdout.write("\n");
1992
2006
  const stackAnswers = await inquirer.prompt([
1993
2007
  {
1994
2008
  type: "input",
@@ -2036,7 +2050,7 @@ var setup_default = defineCommand18({
2036
2050
  default: "latest"
2037
2051
  }
2038
2052
  ]);
2039
- process19.stdout.write("\n");
2053
+ process20.stdout.write("\n");
2040
2054
  info("Choose the AI model that your OpenClaw agent will use.");
2041
2055
  info("You'll need an API key from your chosen provider (except Bedrock and Ollama).\n");
2042
2056
  const { modelProviderId } = await inquirer.prompt([{
@@ -2065,7 +2079,7 @@ var setup_default = defineCommand18({
2065
2079
  secrets.push(entry);
2066
2080
  modelConfig["apiKey"] = `$secret:${secretName}`;
2067
2081
  } else if (modelProvider.credentialSource === "aws-profile") {
2068
- process19.stdout.write("\n");
2082
+ process20.stdout.write("\n");
2069
2083
  info(modelProvider.iamNote ?? "Bedrock uses your AWS server's IAM role \u2014 no API key needed.");
2070
2084
  info("Make sure your EC2 instance has the AmazonBedrockFullAccess IAM policy attached.\n");
2071
2085
  } else if (modelProvider.id === "ollama") {
@@ -2082,7 +2096,7 @@ var setup_default = defineCommand18({
2082
2096
  ...selectedModel.modelId ? { modelId: selectedModel.modelId } : { model: selectedModel.id },
2083
2097
  ...modelConfig
2084
2098
  };
2085
- process19.stdout.write("\n");
2099
+ process20.stdout.write("\n");
2086
2100
  info("Use \u2191\u2193 to move, Space to select/deselect, Enter to confirm.");
2087
2101
  const { selectedIntegrationIds } = await inquirer.prompt([{
2088
2102
  type: "checkbox",
@@ -2098,7 +2112,7 @@ var setup_default = defineCommand18({
2098
2112
  const channelsConfig = {};
2099
2113
  const infraRequired = [];
2100
2114
  for (const integ of catalogs.integrations.filter((i) => selectedIntegrationIds.includes(i.id))) {
2101
- process19.stdout.write("\n");
2115
+ process20.stdout.write("\n");
2102
2116
  info(`Setting up ${integ.displayName}:`);
2103
2117
  const channelConfig = {};
2104
2118
  for (const field of integ.fields) {
@@ -2134,11 +2148,11 @@ var setup_default = defineCommand18({
2134
2148
  }
2135
2149
  }
2136
2150
  const gatewayToken = randomBytes(24).toString("hex");
2137
- const secretsDir = path4.join(os2.homedir(), ".clawops", "secrets");
2138
- mkdirSync4(secretsDir, { recursive: true });
2151
+ const secretsDir = path3.join(os.homedir(), ".clawops", "secrets");
2152
+ mkdirSync3(secretsDir, { recursive: true });
2139
2153
  spawnSync("chmod", ["700", secretsDir], { stdio: "ignore" });
2140
- const tokenPath = path4.join(secretsDir, `GATEWAY_TOKEN_${stackAnswers.stackName}`);
2141
- writeFileSync4(tokenPath, gatewayToken, { encoding: "utf-8", mode: 384 });
2154
+ const tokenPath = path3.join(secretsDir, `GATEWAY_TOKEN_${stackAnswers.stackName}`);
2155
+ writeFileSync3(tokenPath, gatewayToken, { encoding: "utf-8", mode: 384 });
2142
2156
  const openclawConfigOverlay = {
2143
2157
  models: builtModelConfig,
2144
2158
  gateway: { auth: { mode: "token", token: gatewayToken } }
@@ -2148,17 +2162,17 @@ var setup_default = defineCommand18({
2148
2162
  }
2149
2163
  let outputPath;
2150
2164
  if (provider === "local") {
2151
- outputPath = path4.join(outDir, `openclaw-${stackAnswers.stackName}.json`);
2152
- writeFileSync4(outputPath, JSON.stringify(openclawConfigOverlay, null, 2), "utf-8");
2153
- process19.stdout.write("\n");
2165
+ outputPath = path3.join(outDir, `openclaw-${stackAnswers.stackName}.json`);
2166
+ writeFileSync3(outputPath, JSON.stringify(openclawConfigOverlay, null, 2), "utf-8");
2167
+ process20.stdout.write("\n");
2154
2168
  success(`Config file saved to ${outputPath}`);
2155
2169
  } else {
2156
2170
  let sshPublicKey = "";
2157
2171
  try {
2158
- sshPublicKey = readFileSync4(stackAnswers.sshKeyPath ?? "", "utf-8").trim();
2172
+ sshPublicKey = readFileSync3(stackAnswers.sshKeyPath ?? "", "utf-8").trim();
2159
2173
  } catch {
2160
2174
  failure(`Cannot read SSH public key at ${stackAnswers.sshKeyPath ?? ""}`);
2161
- process19.exit(1);
2175
+ process20.exit(1);
2162
2176
  }
2163
2177
  const plan = {
2164
2178
  apiVersion: "clawops.dev/v1",
@@ -2187,12 +2201,12 @@ var setup_default = defineCommand18({
2187
2201
  ...stackAnswers.stateBucket ? { stateBucket: stackAnswers.stateBucket } : {}
2188
2202
  }
2189
2203
  };
2190
- outputPath = path4.join(outDir, `clawops-${stackAnswers.stackName}-plan.json`);
2191
- writeFileSync4(outputPath, JSON.stringify(plan, null, 2), "utf-8");
2192
- process19.stdout.write("\n");
2204
+ outputPath = path3.join(outDir, `clawops-${stackAnswers.stackName}-plan.json`);
2205
+ writeFileSync3(outputPath, JSON.stringify(plan, null, 2), "utf-8");
2206
+ process20.stdout.write("\n");
2193
2207
  success(`Deployment plan saved to ${outputPath}`);
2194
2208
  }
2195
- process19.stdout.write("\n");
2209
+ process20.stdout.write("\n");
2196
2210
  const mcpEntry = buildMcpEntry();
2197
2211
  if (!mcpEntry.resolved) {
2198
2212
  info("Note: clawops is not installed globally \u2014 AI apps will not be able to start");
@@ -2215,21 +2229,12 @@ var setup_default = defineCommand18({
2215
2229
  }]);
2216
2230
  const selectedApps = MCP_APPS.filter((app) => selectedAppIds.includes(app.id));
2217
2231
  if (selectedApps.length > 0) {
2218
- for (const app of selectedApps) {
2219
- const cfgPath = app.configPath();
2220
- try {
2221
- let existing = {};
2222
- if (existsSync3(cfgPath)) {
2223
- existing = JSON.parse(readFileSync4(cfgPath, "utf-8"));
2224
- }
2225
- const mcpServers = existing["mcpServers"] ?? {};
2226
- mcpServers["clawops"] = { command: mcpEntry.command, args: mcpEntry.args };
2227
- existing["mcpServers"] = mcpServers;
2228
- mkdirSync4(path4.dirname(cfgPath), { recursive: true });
2229
- writeFileSync4(cfgPath, JSON.stringify(existing, null, 2), "utf-8");
2230
- success(`${app.name} configured (${cfgPath})`);
2231
- } catch (err) {
2232
- failure(`Could not configure ${app.name}: ${err.message}`);
2232
+ const results = writeAppConfigs(selectedApps, { command: mcpEntry.command, args: mcpEntry.args });
2233
+ for (const r of results) {
2234
+ if (r.ok) {
2235
+ success(`${r.app.name} configured (${r.configPath})`);
2236
+ } else {
2237
+ failure(`Could not configure ${r.app.name}: ${r.error ?? "unknown error"}`);
2233
2238
  info("Add this to its MCP config manually:");
2234
2239
  printMcpSnippet();
2235
2240
  }
@@ -2247,7 +2252,7 @@ var setup_default = defineCommand18({
2247
2252
  }
2248
2253
  }
2249
2254
  if (infraRequired.length > 0) {
2250
- process19.stdout.write("\n");
2255
+ process20.stdout.write("\n");
2251
2256
  info("\u2500\u2500 Action required: webhook registration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2252
2257
  info("These integrations need a webhook URL set up in their developer portal:");
2253
2258
  for (const integ of infraRequired) {
@@ -2258,7 +2263,7 @@ var setup_default = defineCommand18({
2258
2263
  }
2259
2264
  }
2260
2265
  if (modelProvider.id === "ollama" && selectedModel.pullSuffix !== void 0) {
2261
- process19.stdout.write("\n");
2266
+ process20.stdout.write("\n");
2262
2267
  info("\u2500\u2500 Action required: download the Ollama model \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2263
2268
  info("After deployment, run this command on the server (or locally if Ollama is local):");
2264
2269
  info(` ollama pull ${selectedModel.id}${selectedModel.pullSuffix}`);
@@ -2288,8 +2293,8 @@ var setup_default = defineCommand18({
2288
2293
  });
2289
2294
  } else {
2290
2295
  const { getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
2291
- const knownHostsPath = path4.join(getConfigDir2(), "known_hosts");
2292
- process19.stdout.write("\n");
2296
+ const knownHostsPath = path3.join(getConfigDir2(), "known_hosts");
2297
+ process20.stdout.write("\n");
2293
2298
  info("To deploy later, run these two commands:");
2294
2299
  info(` clawops init --provider local --host ${localHost} --port ${localPort} --user ${localUser} --key ${localKeyPath} --stack ${stackAnswers.stackName}`);
2295
2300
  info(` clawops up --stack ${stackAnswers.stackName} --config ${outputPath}`);
@@ -2305,17 +2310,17 @@ var setup_default = defineCommand18({
2305
2310
  }]);
2306
2311
  if (applyNow) {
2307
2312
  const { applyPlan } = await import("./apply-KBF7OPWL.js");
2308
- const plan = JSON.parse(readFileSync4(outputPath, "utf-8"));
2313
+ const plan = JSON.parse(readFileSync3(outputPath, "utf-8"));
2309
2314
  try {
2310
2315
  await applyPlan(plan, {
2311
- onOutput: (line) => process19.stdout.write(line),
2316
+ onOutput: (line) => process20.stdout.write(line),
2312
2317
  signal: ac.signal
2313
2318
  });
2314
2319
  success("Server provisioned and OpenClaw installed.");
2315
2320
  info(`Run: clawops doctor --stack ${stackAnswers.stackName} to verify everything is healthy`);
2316
2321
  } catch (err) {
2317
2322
  failure(err instanceof Error ? err.message : String(err));
2318
- process19.exit(1);
2323
+ process20.exit(1);
2319
2324
  }
2320
2325
  } else {
2321
2326
  info(`
@@ -2330,7 +2335,7 @@ async function runLocalDeploy(opts) {
2330
2335
  const { connect } = await import("./ssh-IQXNME3D.js");
2331
2336
  const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-JQ77SRC2.js");
2332
2337
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
2333
- const knownHostsPath = path4.join(getConfigDir2(), "known_hosts");
2338
+ const knownHostsPath = path3.join(getConfigDir2(), "known_hosts");
2334
2339
  const existing = getConfig2();
2335
2340
  const stackEntry = {
2336
2341
  provider: "local",
@@ -2409,9 +2414,9 @@ async function runLocalDeploy(opts) {
2409
2414
  }
2410
2415
  const { drainPool } = await import("./pool-FBFHATDG.js");
2411
2416
  drainPool();
2412
- const tokenPath = path4.join(os2.homedir(), ".clawops", "secrets", `GATEWAY_TOKEN_${opts.stackName}`);
2417
+ const tokenPath = path3.join(os.homedir(), ".clawops", "secrets", `GATEWAY_TOKEN_${opts.stackName}`);
2413
2418
  const dashboardUrl = `${state.gatewayUrl}?token=${opts.gatewayToken}`;
2414
- process19.stdout.write("\n");
2419
+ process20.stdout.write("\n");
2415
2420
  success("All done! OpenClaw is running.");
2416
2421
  info(`Open dashboard: ${dashboardUrl}`);
2417
2422
  info(` (or enter the token manually in the "Gateway Token" field: ${opts.gatewayToken})`);
@@ -2465,7 +2470,7 @@ ${cfg.loginHint}`);
2465
2470
  default: true
2466
2471
  }]);
2467
2472
  if (runNow) {
2468
- process19.stdout.write("\n");
2473
+ process20.stdout.write("\n");
2469
2474
  const result = spawnSync(cfg.loginCmd[0], cfg.loginCmd.slice(1), { stdio: "inherit" });
2470
2475
  if (result.error) {
2471
2476
  failure(`Could not launch ${cliName} \u2014 is it installed? (${result.error.message})`);
@@ -2475,10 +2480,10 @@ ${cfg.loginHint}`);
2475
2480
  }
2476
2481
  try {
2477
2482
  const identity = cfg.check();
2478
- process19.stdout.write("\n");
2483
+ process20.stdout.write("\n");
2479
2484
  success(`Authenticated: ${cfg.identity(identity)}`);
2480
2485
  } catch {
2481
- process19.stdout.write("\n");
2486
+ process20.stdout.write("\n");
2482
2487
  failure(`Still not authenticated \u2014 the sign-in may have been cancelled or failed.`);
2483
2488
  info("Re-run `clawops setup` after signing in, or continue and authenticate before running `clawops up`.\n");
2484
2489
  }
@@ -2492,7 +2497,7 @@ function cliInstallUrl(provider) {
2492
2497
  return "https://learn.microsoft.com/en-us/cli/azure/install-azure-cli";
2493
2498
  }
2494
2499
  async function promptSecret(name, label, envDefault, inquirer) {
2495
- const secretsDir = path4.join(os2.homedir(), ".clawops", "secrets");
2500
+ const secretsDir = path3.join(os.homedir(), ".clawops", "secrets");
2496
2501
  const { secretSource } = await inquirer.prompt([{
2497
2502
  type: "list",
2498
2503
  name: "secretSource",
@@ -2510,10 +2515,10 @@ async function promptSecret(name, label, envDefault, inquirer) {
2510
2515
  message: `Paste your ${label}: (input is hidden)`,
2511
2516
  validate: (v) => v.trim() !== "" || "Value cannot be empty"
2512
2517
  }]);
2513
- mkdirSync4(secretsDir, { recursive: true });
2518
+ mkdirSync3(secretsDir, { recursive: true });
2514
2519
  spawnSync("chmod", ["700", secretsDir], { stdio: "ignore" });
2515
- const secretPath = path4.join(secretsDir, name);
2516
- writeFileSync4(secretPath, secretValue.trim(), { encoding: "utf-8", mode: 384 });
2520
+ const secretPath = path3.join(secretsDir, name);
2521
+ writeFileSync3(secretPath, secretValue.trim(), { encoding: "utf-8", mode: 384 });
2517
2522
  success(`Secret saved to ${secretPath} (chmod 600)`);
2518
2523
  return { name, source: "file", ref: secretPath };
2519
2524
  }
@@ -2530,7 +2535,7 @@ async function promptSecret(name, label, envDefault, inquirer) {
2530
2535
  type: "input",
2531
2536
  name: "filePath",
2532
2537
  message: `File path: (full path to the file containing your ${label})`,
2533
- validate: (v) => existsSync3(v.trim()) || `File not found: ${v.trim()}`
2538
+ validate: (v) => existsSync2(v.trim()) || `File not found: ${v.trim()}`
2534
2539
  }]);
2535
2540
  return { name, source: "file", ref: filePath };
2536
2541
  }
@@ -2663,7 +2668,7 @@ async function pollUntilDockerReady(label) {
2663
2668
  throw new Error("Docker daemon did not become ready within timeout.");
2664
2669
  }
2665
2670
  async function startDockerAndWait(inquirer) {
2666
- const isLinux = process19.platform === "linux";
2671
+ const isLinux = process20.platform === "linux";
2667
2672
  const { choice } = await inquirer.prompt([{
2668
2673
  type: "list",
2669
2674
  name: "choice",
@@ -2727,12 +2732,12 @@ async function startDockerAndWait(inquirer) {
2727
2732
  var LOCALHOST_ALIASES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
2728
2733
  async function ensureAuthorizedKey(keyPath, host, inquirer) {
2729
2734
  const pubKeyPath = `${keyPath}.pub`;
2730
- if (!existsSync3(pubKeyPath)) return;
2731
- const pubKey = readFileSync4(pubKeyPath, "utf-8").trim();
2735
+ if (!existsSync2(pubKeyPath)) return;
2736
+ const pubKey = readFileSync3(pubKeyPath, "utf-8").trim();
2732
2737
  const isLocal = LOCALHOST_ALIASES.has(host.toLowerCase());
2733
- const authorizedKeysPath = path4.join(os2.homedir(), ".ssh", "authorized_keys");
2738
+ const authorizedKeysPath = path3.join(os.homedir(), ".ssh", "authorized_keys");
2734
2739
  if (isLocal) {
2735
- const existing = existsSync3(authorizedKeysPath) ? readFileSync4(authorizedKeysPath, "utf-8") : "";
2740
+ const existing = existsSync2(authorizedKeysPath) ? readFileSync3(authorizedKeysPath, "utf-8") : "";
2736
2741
  if (existing.includes(pubKey)) {
2737
2742
  success("SSH public key is already in authorized_keys.");
2738
2743
  return;
@@ -2745,9 +2750,9 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
2745
2750
  default: true
2746
2751
  }]);
2747
2752
  if (addKey) {
2748
- mkdirSync4(path4.join(os2.homedir(), ".ssh"), { recursive: true });
2753
+ mkdirSync3(path3.join(os.homedir(), ".ssh"), { recursive: true });
2749
2754
  const entry = existing.endsWith("\n") || existing === "" ? pubKey + "\n" : "\n" + pubKey + "\n";
2750
- writeFileSync4(authorizedKeysPath, existing + entry, { encoding: "utf-8", flag: "a" });
2755
+ writeFileSync3(authorizedKeysPath, existing + entry, { encoding: "utf-8", flag: "a" });
2751
2756
  spawnSync("chmod", ["600", authorizedKeysPath], { stdio: "ignore" });
2752
2757
  success("Public key added to ~/.ssh/authorized_keys.");
2753
2758
  } else {
@@ -2755,15 +2760,15 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
2755
2760
  info(` ${pubKey}`);
2756
2761
  }
2757
2762
  } else {
2758
- process19.stdout.write("\n");
2763
+ process20.stdout.write("\n");
2759
2764
  info("Make sure this public key is in ~/.ssh/authorized_keys on your server:");
2760
2765
  info(` ${pubKey}`);
2761
2766
  info("(If you just created the server, paste the line above into the server's ~/.ssh/authorized_keys file.)\n");
2762
2767
  }
2763
2768
  }
2764
2769
  async function generateSshKey() {
2765
- const keyPath = path4.join(os2.homedir(), ".ssh", "id_ed25519");
2766
- mkdirSync4(path4.join(os2.homedir(), ".ssh"), { recursive: true });
2770
+ const keyPath = path3.join(os.homedir(), ".ssh", "id_ed25519");
2771
+ mkdirSync3(path3.join(os.homedir(), ".ssh"), { recursive: true });
2767
2772
  const result = spawnSync("ssh-keygen", ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops"], {
2768
2773
  stdio: ["ignore", "pipe", "pipe"]
2769
2774
  });
@@ -2772,77 +2777,30 @@ async function generateSshKey() {
2772
2777
  }
2773
2778
  success(`SSH key pair generated: ${keyPath}`);
2774
2779
  info(`Public key (add this to your server's ~/.ssh/authorized_keys if needed):
2775
- ${readFileSync4(`${keyPath}.pub`, "utf-8").trim()}`);
2780
+ ${readFileSync3(`${keyPath}.pub`, "utf-8").trim()}`);
2776
2781
  return keyPath;
2777
2782
  }
2778
2783
  function detectSshKey() {
2779
2784
  const candidates = ["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"];
2780
2785
  for (const name of candidates) {
2781
- const p = path4.join(os2.homedir(), ".ssh", name);
2782
- if (existsSync3(p)) return p;
2783
- }
2784
- return path4.join(os2.homedir(), ".ssh", "id_ed25519");
2785
- }
2786
- function buildMcpEntry() {
2787
- try {
2788
- const bin = execSync("which clawops", {
2789
- encoding: "utf-8",
2790
- stdio: ["ignore", "pipe", "ignore"]
2791
- }).trim();
2792
- if (bin) return { command: bin, args: ["mcp", "serve", "--read-only"], resolved: true };
2793
- } catch {
2786
+ const p = path3.join(os.homedir(), ".ssh", name);
2787
+ if (existsSync2(p)) return p;
2794
2788
  }
2795
- return { command: "clawops", args: ["mcp", "serve", "--read-only"], resolved: false };
2789
+ return path3.join(os.homedir(), ".ssh", "id_ed25519");
2796
2790
  }
2797
- var MCP_APPS = [
2798
- {
2799
- id: "claude-desktop",
2800
- name: "Claude Desktop",
2801
- configPath: () => process19.platform === "darwin" ? path4.join(os2.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json") : path4.join(os2.homedir(), ".config", "Claude", "claude_desktop_config.json"),
2802
- isInstalled: () => existsSync3(
2803
- process19.platform === "darwin" ? path4.join(os2.homedir(), "Library", "Application Support", "Claude") : path4.join(os2.homedir(), ".config", "Claude")
2804
- )
2805
- },
2806
- {
2807
- id: "claude-code",
2808
- name: "Claude Code",
2809
- configPath: () => path4.join(os2.homedir(), ".claude.json"),
2810
- isInstalled: () => {
2811
- if (existsSync3(path4.join(os2.homedir(), ".claude.json"))) return true;
2812
- try {
2813
- execSync("claude --version", { stdio: "ignore" });
2814
- return true;
2815
- } catch {
2816
- return false;
2817
- }
2818
- }
2819
- },
2820
- {
2821
- id: "cursor",
2822
- name: "Cursor",
2823
- configPath: () => path4.join(os2.homedir(), ".cursor", "mcp.json"),
2824
- isInstalled: () => existsSync3(path4.join(os2.homedir(), ".cursor"))
2825
- },
2826
- {
2827
- id: "windsurf",
2828
- name: "Windsurf",
2829
- configPath: () => path4.join(os2.homedir(), ".codeium", "windsurf", "mcp_config.json"),
2830
- isInstalled: () => existsSync3(path4.join(os2.homedir(), ".codeium", "windsurf"))
2831
- }
2832
- ];
2833
2791
  function printMcpSnippet() {
2834
- process19.stdout.write("\n");
2835
- process19.stdout.write(JSON.stringify({
2792
+ process20.stdout.write("\n");
2793
+ process20.stdout.write(JSON.stringify({
2836
2794
  mcpServers: {
2837
2795
  clawops: { command: "clawops", args: ["mcp", "serve", "--read-only"] }
2838
2796
  }
2839
2797
  }, null, 2) + "\n\n");
2840
2798
  }
2841
2799
  function loadCatalogs(yaml) {
2842
- const specDir = path4.join(path4.dirname(fileURLToPath(import.meta.url)), "../../../spec");
2800
+ const specDir = path3.join(path3.dirname(fileURLToPath(import.meta.url)), "../../../spec");
2843
2801
  try {
2844
- const modelsRaw = yaml.load(readFileSync4(path4.join(specDir, "models.yaml"), "utf-8"));
2845
- const integrationsRaw = yaml.load(readFileSync4(path4.join(specDir, "integrations.yaml"), "utf-8"));
2802
+ const modelsRaw = yaml.load(readFileSync3(path3.join(specDir, "models.yaml"), "utf-8"));
2803
+ const integrationsRaw = yaml.load(readFileSync3(path3.join(specDir, "integrations.yaml"), "utf-8"));
2846
2804
  return {
2847
2805
  models: modelsRaw.providers,
2848
2806
  integrations: integrationsRaw.integrations
@@ -2887,26 +2845,96 @@ function instanceChoices(provider) {
2887
2845
  return table[provider] ?? [];
2888
2846
  }
2889
2847
 
2848
+ // src/cli/commands/help.ts
2849
+ import { defineCommand as defineCommand19 } from "citty";
2850
+ var COMMANDS = [
2851
+ ["setup", "Interactive first-run wizard \u2014 configure, deploy, and wire AI apps"],
2852
+ ["init", "Write config and generate SSH key pair (non-interactive)"],
2853
+ ["up", "Provision or update a stack"],
2854
+ ["down", "Destroy a local-provider stack (requires --yes)"],
2855
+ ["destroy", "Destroy a cloud-provider stack with confirmation prompt (requires --yes)"],
2856
+ ["status", "Show stack outputs: IP, gateway URL, region, provisioned time"],
2857
+ ["plan", "Generate a deploy-plan JSON artifact (dry-run safe)"],
2858
+ ["apply", "Apply a previously reviewed plan file"],
2859
+ ["ssh", "Interactive SSH session or run a remote command (--command)"],
2860
+ ["logs", "Stream OpenClaw logs (-f, --tail N, --since 5m)"],
2861
+ ["tunnel", "Port-forward gateway UI to localhost over SSH"],
2862
+ ["config", "Get / set / validate remote OpenClaw config values"],
2863
+ ["agents", "List or restart OpenClaw agents"],
2864
+ ["gateway", "Restart the OpenClaw gateway service"],
2865
+ ["backup", "Create or restore an OpenClaw state backup"],
2866
+ ["stacks", "List named stacks and their state"],
2867
+ ["doctor", "Check Node version, config, SSH key, provider credentials, and Pulumi home"],
2868
+ ["mcp", "MCP server operations (mcp serve | mcp install)"],
2869
+ ["help", "Show this help message"]
2870
+ ];
2871
+ var GLOBAL_FLAGS = [
2872
+ ["--stack <name>", "Target named stack (overrides config default)"],
2873
+ ["--provider <name>", "Override provider (aws | gcp | azure | local)"],
2874
+ ["--json", "Emit JSON to stdout"],
2875
+ ["--quiet", "Suppress non-error output"],
2876
+ ["--dry-run", "Preview without applying (mutating commands)"],
2877
+ ["--yes", "Skip interactive confirmations (CI mode)"]
2878
+ ];
2879
+ var help_default = defineCommand19({
2880
+ meta: {
2881
+ name: "help",
2882
+ description: "Show available commands and global flags"
2883
+ },
2884
+ args: {},
2885
+ run() {
2886
+ const bold = chalk.bold;
2887
+ const dim = chalk.dim;
2888
+ const cyan = chalk.cyan;
2889
+ const cmdWidth = Math.max(...COMMANDS.map(([c]) => c.length));
2890
+ const flagWidth = Math.max(...GLOBAL_FLAGS.map(([f]) => f.length));
2891
+ process.stdout.write(`
2892
+ ${bold("clawops")} \u2014 Deploy and manage self-hosted OpenClaw instances
2893
+
2894
+ `);
2895
+ process.stdout.write(`${bold("Usage:")} clawops <command> [flags]
2896
+
2897
+ `);
2898
+ process.stdout.write(`${bold("Commands:")}
2899
+ `);
2900
+ for (const [cmd, desc] of COMMANDS) {
2901
+ process.stdout.write(` ${cyan(cmd.padEnd(cmdWidth))} ${desc}
2902
+ `);
2903
+ }
2904
+ process.stdout.write(`
2905
+ ${bold("Global flags:")}
2906
+ `);
2907
+ for (const [flag, desc] of GLOBAL_FLAGS) {
2908
+ process.stdout.write(` ${flag.padEnd(flagWidth)} ${dim(desc)}
2909
+ `);
2910
+ }
2911
+ process.stdout.write(`
2912
+ ${dim("Run `clawops <command> --help` for command-specific flags.")}
2913
+
2914
+ `);
2915
+ }
2916
+ });
2917
+
2890
2918
  // src/cli/error-handler.ts
2891
- import process20 from "process";
2919
+ import process21 from "process";
2892
2920
  function handleError(err) {
2893
2921
  if (err instanceof ClawopsError) {
2894
2922
  failure(err.message);
2895
- process20.exit(err.exitCode);
2923
+ process21.exit(err.exitCode);
2896
2924
  }
2897
2925
  if (err instanceof Error) {
2898
2926
  failure(`Unexpected error: ${err.message}`);
2899
- if (process20.env["DEBUG"]) {
2900
- process20.stderr.write(err.stack ?? "\n");
2927
+ if (process21.env["DEBUG"]) {
2928
+ process21.stderr.write(err.stack ?? "\n");
2901
2929
  }
2902
- process20.exit(1);
2930
+ process21.exit(1);
2903
2931
  }
2904
2932
  failure(`Unknown error: ${String(err)}`);
2905
- process20.exit(1);
2933
+ process21.exit(1);
2906
2934
  }
2907
2935
 
2908
2936
  // src/cli/index.ts
2909
- var main = defineCommand19({
2937
+ var main = defineCommand20({
2910
2938
  meta: {
2911
2939
  name: "clawops",
2912
2940
  description: "Deploy and manage self-hosted OpenClaw instances across clouds",
@@ -2939,7 +2967,8 @@ var main = defineCommand19({
2939
2967
  stacks: stacks_default,
2940
2968
  doctor: doctor_default,
2941
2969
  mcp: mcp_default,
2942
- setup: setup_default
2970
+ setup: setup_default,
2971
+ help: help_default
2943
2972
  }
2944
2973
  });
2945
2974
  try {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  generatePlan,
4
4
  planId
5
- } from "./chunk-QMXM2SMO.js";
5
+ } from "./chunk-SSB6SGPQ.js";
6
6
  import "./chunk-YTH4L2GN.js";
7
7
  import "./chunk-ACYJBSLJ.js";
8
8
  import "./chunk-BRPU7AQC.js";
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ MCP_APPS,
4
+ buildMcpEntry,
5
+ writeAppConfigs
6
+ } from "./chunk-OIGTOLB3.js";
7
+ export {
8
+ MCP_APPS,
9
+ buildMcpEntry,
10
+ writeAppConfigs
11
+ };
@@ -2,7 +2,7 @@
2
2
 
3
3
  // package.json
4
4
  var name = "@clawops/cli";
5
- var version = "1.2.0";
5
+ var version = "1.2.1";
6
6
  var description = "Deploy and manage self-hosted OpenClaw instances across clouds";
7
7
  var type = "module";
8
8
  var bin = {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  generatePlan
4
- } from "./chunk-QMXM2SMO.js";
4
+ } from "./chunk-SSB6SGPQ.js";
5
5
  import {
6
6
  applyPlan
7
7
  } from "./chunk-LU63NZD3.js";
@@ -1231,7 +1231,7 @@ If none of the above resolves the issue:
1231
1231
 
1232
1232
  // src/mcp/server.ts
1233
1233
  async function serveMcp(opts) {
1234
- const { version } = await import("./package-3JZ767DN.js");
1234
+ const { version } = await import("./package-SE3M4NJA.js");
1235
1235
  const server = new McpServer({ name: "clawops", version });
1236
1236
  registerTools(server, opts);
1237
1237
  registerResources(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawops/cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Deploy and manage self-hosted OpenClaw instances across clouds",
5
5
  "type": "module",
6
6
  "bin": {