@clawops/cli 1.3.0 → 1.5.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 (45) hide show
  1. package/README.md +136 -0
  2. package/dist/{apply-DLNE2JYZ.js → apply-5RREMN3L.js} +3 -3
  3. package/dist/automation-RULUSJBW.js +0 -0
  4. package/dist/{aws-KQBWY43W.js → aws-FRE2JVAZ.js} +9 -4
  5. package/dist/{azure-4EJFVJZL.js → azure-PVC3AQVC.js} +11 -4
  6. package/dist/bootstrap-G4UZ2FKH.js +0 -0
  7. package/dist/chunk-3QJBNAHW.js +0 -0
  8. package/dist/chunk-4U3LTLWZ.js +0 -0
  9. package/dist/chunk-6ZFIFDBJ.js +0 -0
  10. package/dist/chunk-A2I76FTA.js +0 -0
  11. package/dist/chunk-BOPSG2LI.js +0 -0
  12. package/dist/chunk-CX5SL5HP.js +0 -0
  13. package/dist/{chunk-ZDPBBYYV.js → chunk-IVX62LFO.js} +2 -2
  14. package/dist/chunk-KGXPLI7W.js +0 -0
  15. package/dist/chunk-OIGTOLB3.js +0 -0
  16. package/dist/chunk-R5S6IXBE.js +771 -0
  17. package/dist/{chunk-JDN6PLH2.js → chunk-T5EX55GP.js} +3 -3
  18. package/dist/chunk-YTH4L2GN.js +0 -0
  19. package/dist/{chunk-UDNZUSKA.js → chunk-ZFNPM2WG.js} +1 -1
  20. package/dist/{chunk-JCUZU5BH.js → chunk-ZONXY3C6.js} +2 -2
  21. package/dist/chunk-ZVOEQCNW.js +0 -0
  22. package/dist/cli.js +207 -53
  23. package/dist/{context-M2RMDHX6.js → context-PSGEX2D7.js} +1 -1
  24. package/dist/errors-OK47MQFD.js +0 -0
  25. package/dist/firewall-YYDOWDDP.js +0 -0
  26. package/dist/{gcp-SKURG3L6.js → gcp-BXDTC6EK.js} +60 -17
  27. package/dist/{generate-U7PJ5LRG.js → generate-2QGKYR42.js} +2 -2
  28. package/dist/js-yaml-PTEEG4FO.js +0 -0
  29. package/dist/local-DXBEVZ5C.js +0 -0
  30. package/dist/mcp-apps-KY44ED3Y.js +0 -0
  31. package/dist/mcp-wire-ZWIYMLEW.js +45 -0
  32. package/dist/outputs-DJHBY7EE.js +0 -0
  33. package/dist/overlay-store-ADZPD7EU.js +0 -0
  34. package/dist/{package-AFUTH62F.js → package-QLDA65A3.js} +2 -1
  35. package/dist/pool-FBFHATDG.js +0 -0
  36. package/dist/providers-2OABPW2E.js +0 -0
  37. package/dist/{remote-config-JQ77SRC2.js → remote-config-QF5TT7GU.js} +1 -1
  38. package/dist/secrets-SVZWNAPK.js +0 -0
  39. package/dist/{server-I62UES5V.js → server-LOL2NUJ2.js} +92 -19
  40. package/dist/ssh-IQXNME3D.js +0 -0
  41. package/dist/state-PRBIIN7I.js +0 -0
  42. package/dist/store-SDUR52Z5.js +0 -0
  43. package/dist/validate-T5M5EHSJ.js +0 -0
  44. package/package.json +27 -14
  45. package/dist/chunk-LVIIYY27.js +0 -266
@@ -0,0 +1,771 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildContext
4
+ } from "./chunk-T5EX55GP.js";
5
+ import {
6
+ acquireSession,
7
+ drainPool
8
+ } from "./chunk-ZVOEQCNW.js";
9
+ import {
10
+ OPENCLAW_CONFIG,
11
+ atomicWriteConfig,
12
+ restartGateway
13
+ } from "./chunk-ZFNPM2WG.js";
14
+ import {
15
+ StateError
16
+ } from "./chunk-KGXPLI7W.js";
17
+
18
+ // src/cli/commands/monitor.ts
19
+ import { defineCommand } from "citty";
20
+ import process2 from "process";
21
+
22
+ // src/output/human.ts
23
+ import chalk from "chalk";
24
+ import ora from "ora";
25
+ function success(msg) {
26
+ console.log(chalk.green("\u2713") + " " + msg);
27
+ }
28
+ function failure(msg) {
29
+ console.error(chalk.red("\u2717") + " " + msg);
30
+ }
31
+ function warn(msg) {
32
+ console.warn(chalk.yellow("\u26A0") + " " + msg);
33
+ }
34
+ function info(msg) {
35
+ console.log(chalk.blue("\u2139") + " " + msg);
36
+ }
37
+ function spinner(text) {
38
+ return ora(text).start();
39
+ }
40
+ var REPO_URL = "https://github.com/dfridkin/clawops";
41
+ function printCta() {
42
+ process.stdout.write(
43
+ "\n" + chalk.dim(" Thank you for using clawops! If it has been useful, star the project:") + "\n" + chalk.dim(" " + REPO_URL) + "\n\n" + chalk.dim(" Found a bug? Open an issue:") + "\n" + chalk.dim(" " + REPO_URL + "/issues") + "\n\n"
44
+ );
45
+ }
46
+
47
+ // src/cli/commands/monitor.ts
48
+ var GATEWAY_PORT = 18789;
49
+ function formatUptime(startedAt) {
50
+ if (!startedAt) return "\u2014";
51
+ const ms = Date.now() - new Date(startedAt).getTime();
52
+ if (ms < 0 || isNaN(ms)) return "\u2014";
53
+ const totalSecs = Math.floor(ms / 1e3);
54
+ const mins = Math.floor(totalSecs / 60);
55
+ const hours = Math.floor(mins / 60);
56
+ const days = Math.floor(hours / 24);
57
+ if (days > 0) return `${days}d ${hours % 24}h ${mins % 60}m`;
58
+ if (hours > 0) return `${hours}h ${mins % 60}m`;
59
+ return `${mins}m ${totalSecs % 60}s`;
60
+ }
61
+ async function gatherSnapshot(session, signal, tailLines = 10) {
62
+ const DOCKER = "PATH=/usr/local/bin:/opt/homebrew/bin:$PATH docker";
63
+ const [inspectRaw, statsRaw, healthRaw, configRaw, diskRaw, logsRaw] = await Promise.all([
64
+ session.exec(
65
+ `${DOCKER} inspect openclaw --format '{{.State.Status}}|{{.Config.Image}}|{{.State.StartedAt}}|{{.RestartCount}}' 2>/dev/null || echo 'not found|||0'`,
66
+ signal
67
+ ),
68
+ session.exec(
69
+ `${DOCKER} stats openclaw --no-stream --format '{{.MemUsage}}|{{.CPUPerc}}' 2>/dev/null || echo '\u2014|\u2014'`,
70
+ signal
71
+ ),
72
+ session.exec(
73
+ `curl -sf --connect-timeout 2 http://localhost:${GATEWAY_PORT}/health >/dev/null 2>&1 && echo ok || echo unreachable`,
74
+ signal
75
+ ),
76
+ session.exec(
77
+ `cat /home/clawops/openclaw.json 2>/dev/null || echo '{}'`,
78
+ signal
79
+ ),
80
+ session.exec(
81
+ `df -h /home/clawops 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
82
+ signal
83
+ ),
84
+ session.exec(
85
+ `${DOCKER} logs openclaw -n ${tailLines} 2>&1 || echo '(no logs)'`,
86
+ signal
87
+ )
88
+ ]);
89
+ const inspectParts = inspectRaw.stdout.trim().split("|");
90
+ const status = inspectParts[0] ?? "not found";
91
+ const image = inspectParts[1] ?? "";
92
+ const startedAt = inspectParts[2] ?? "";
93
+ const restartCount = parseInt(inspectParts[3] ?? "0", 10) || 0;
94
+ const statsParts = statsRaw.stdout.trim().split("|");
95
+ const memUsage = statsParts[0] ?? "\u2014";
96
+ const cpuPct = statsParts[1] ?? "\u2014";
97
+ let version = "unknown";
98
+ let authMode = "unknown";
99
+ try {
100
+ const cfg = JSON.parse(configRaw.stdout.trim());
101
+ version = cfg.meta?.lastTouchedVersion ?? "unknown";
102
+ authMode = cfg.gateway?.auth?.mode ?? "unknown";
103
+ } catch {
104
+ }
105
+ return {
106
+ container: { status, image, startedAt, restartCount, memUsage, cpuPct },
107
+ gateway: {
108
+ reachable: healthRaw.stdout.trim() === "ok",
109
+ version,
110
+ authMode
111
+ },
112
+ disk: diskRaw.stdout.trim() || "\u2014",
113
+ logLines: logsRaw.stdout.split("\n").filter((l) => l.trim().length > 0),
114
+ capturedAt: /* @__PURE__ */ new Date()
115
+ };
116
+ }
117
+ function renderSnapshot(snap, opts) {
118
+ const noColor = opts.noColor ?? false;
119
+ const g = noColor ? { green: (s) => s, red: (s) => s, yellow: (s) => s, dim: (s) => s, bold: (s) => s } : chalk;
120
+ const LINE = "\u2500".repeat(68);
121
+ const timeStr = snap.capturedAt.toLocaleTimeString();
122
+ const header = `clawops monitor \u2014 ${opts.stackName}`;
123
+ const padding = Math.max(1, 68 - header.length - `updated ${timeStr}`.length);
124
+ const gatewayStr = snap.gateway.reachable ? g.green("\u2713 healthy") : g.red("\u2717 unreachable");
125
+ const containerStr = snap.container.status === "running" ? g.green("\u2713 running") : snap.container.status === "not found" ? g.red("\u2717 not found") : g.yellow(`\u26A0 ${snap.container.status}`);
126
+ const uptime = formatUptime(snap.container.startedAt);
127
+ const shortImage = snap.container.image.replace("ghcr.io/openclaw/openclaw:", "") || "\u2014";
128
+ const lines = [
129
+ g.bold(header) + " ".repeat(padding) + g.dim(`updated ${timeStr}`),
130
+ LINE,
131
+ "",
132
+ ` Stack ${g.bold(opts.stackName)}`,
133
+ ` Gateway ${gatewayStr} version ${g.bold(snap.gateway.version)} auth ${snap.gateway.authMode}`,
134
+ ` Container ${containerStr} ${g.dim(snap.container.image)}`,
135
+ ` Image tag ${shortImage} uptime ${uptime}`,
136
+ ` Resources CPU ${snap.container.cpuPct} Memory ${snap.container.memUsage}`,
137
+ ` Restarts ${snap.container.restartCount} Disk ${snap.disk}`,
138
+ "",
139
+ LINE,
140
+ ""
141
+ ];
142
+ if (opts.showLogs) {
143
+ lines.push(` Logs (last ${snap.logLines.length} lines)`);
144
+ for (const l of snap.logLines) {
145
+ lines.push(" " + g.dim(l));
146
+ }
147
+ lines.push("");
148
+ lines.push(LINE);
149
+ lines.push("");
150
+ }
151
+ const logToggle = opts.showLogs ? "hide logs" : "show logs";
152
+ const backHint = opts.menuMode ? " [s] back" : "";
153
+ lines.push(g.dim(` [r] refresh [l] ${logToggle} [q] quit${backHint} interval: ${opts.intervalSec}s`));
154
+ return lines.join("\n");
155
+ }
156
+ async function probeEntries() {
157
+ const { buildContext: buildContext2 } = await import("./context-PSGEX2D7.js");
158
+ const { getConfig } = await import("./store-SDUR52Z5.js");
159
+ const config = getConfig();
160
+ if (!config) return [];
161
+ return Promise.all(
162
+ Object.entries(config.stacks).map(async ([name, stackCfg]) => {
163
+ const base = {
164
+ name,
165
+ provider: stackCfg.provider,
166
+ region: stackCfg.region ?? "\u2014",
167
+ deployed: false
168
+ };
169
+ try {
170
+ const ctx = buildContext2({ stack: name });
171
+ if (ctx.adapter.name === "local") {
172
+ return { ...base, deployed: !!ctx.localState };
173
+ }
174
+ const stack = await ctx.getStack();
175
+ const outputMap = await stack.outputs();
176
+ const outputs = Object.fromEntries(
177
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
178
+ );
179
+ return { ...base, deployed: !!outputs["publicIp"] };
180
+ } catch {
181
+ return base;
182
+ }
183
+ })
184
+ );
185
+ }
186
+ function renderMenu(entries, selectedIdx, showAll, noColor, confirmDelete) {
187
+ const g = noColor ? { green: (s) => s, red: (s) => s, dim: (s) => s, bold: (s) => s, yellow: (s) => s } : chalk;
188
+ const visible = showAll ? entries : entries.filter((e) => e.deployed);
189
+ const totalDeployed = entries.filter((e) => e.deployed).length;
190
+ const heading = showAll ? "clawops monitor \u2014 all stacks" : "clawops monitor \u2014 select a stack";
191
+ const countStr = showAll ? `(${totalDeployed} of ${entries.length} running)` : `(${totalDeployed} running \xB7 ${entries.length - totalDeployed} not deployed)`;
192
+ const lines = ["", ` ${g.bold(heading)} ${g.dim(countStr)}`, ""];
193
+ if (visible.length === 0) {
194
+ if (showAll) {
195
+ lines.push(" No stacks configured. Run `clawops init` to set up.");
196
+ } else {
197
+ lines.push(" No deployed stacks found.");
198
+ lines.push("");
199
+ lines.push(g.dim(" [a] show all [q] quit"));
200
+ }
201
+ } else {
202
+ for (let i = 0; i < visible.length; i++) {
203
+ const e = visible[i];
204
+ const cursor = i === selectedIdx ? "\u25B6" : " ";
205
+ const nameStr = i === selectedIdx ? g.bold(e.name.padEnd(24)) : e.name.padEnd(24);
206
+ const provStr = e.provider.padEnd(10);
207
+ const regStr = e.region.padEnd(14);
208
+ const statusStr = showAll ? e.deployed ? g.green("\u2713 running") : g.dim("\u2717 not deployed") : "";
209
+ lines.push(` ${cursor} ${nameStr}${provStr}${regStr}${statusStr}`);
210
+ }
211
+ lines.push("");
212
+ if (confirmDelete !== null) {
213
+ lines.push(` ${g.yellow(`Delete "${confirmDelete}" from registry?`)} [y/n]`);
214
+ } else {
215
+ const sel = visible[selectedIdx];
216
+ const canDelete = showAll && sel !== void 0 && !sel.deployed;
217
+ const toggleLabel = showAll ? "[a] running only" : "[a] show all";
218
+ const deleteHint = canDelete ? " [d] delete" : "";
219
+ lines.push(g.dim(` [\u2191\u2193] navigate [enter] select${deleteHint} ${toggleLabel} [q] quit`));
220
+ }
221
+ }
222
+ lines.push("");
223
+ return lines.join("\n");
224
+ }
225
+ async function deleteFromRegistry(name) {
226
+ const { requireConfig, setConfig } = await import("./store-SDUR52Z5.js");
227
+ const config = requireConfig();
228
+ const newStacks = { ...config.stacks };
229
+ delete newStacks[name];
230
+ const updated = { ...config, stacks: newStacks };
231
+ if (name === config.defaults.stack) {
232
+ updated.defaults = { ...config.defaults, stack: Object.keys(newStacks)[0] };
233
+ }
234
+ setConfig(updated);
235
+ }
236
+ async function runStackMenu(ac, setKeyHandler, noColor) {
237
+ process2.stdout.write("\x1B[2J\x1B[H\n Checking stacks...\n");
238
+ const entries = await probeEntries();
239
+ if (ac.signal.aborted) return null;
240
+ let selectedIdx = 0;
241
+ let showAll = false;
242
+ let confirmDelete = null;
243
+ function visible() {
244
+ return showAll ? entries : entries.filter((e) => e.deployed);
245
+ }
246
+ function redraw() {
247
+ process2.stdout.write("\x1B[2J\x1B[H" + renderMenu(entries, selectedIdx, showAll, noColor, confirmDelete));
248
+ }
249
+ redraw();
250
+ return new Promise((resolve) => {
251
+ if (ac.signal.aborted) {
252
+ resolve(null);
253
+ return;
254
+ }
255
+ ac.signal.addEventListener("abort", () => resolve(null), { once: true });
256
+ setKeyHandler((key) => {
257
+ if (confirmDelete !== null) {
258
+ if (key === "y" || key === "Y") {
259
+ const nameToDelete = confirmDelete;
260
+ confirmDelete = null;
261
+ void deleteFromRegistry(nameToDelete).then(() => {
262
+ const idx = entries.findIndex((e) => e.name === nameToDelete);
263
+ if (idx !== -1) entries.splice(idx, 1);
264
+ const vis2 = visible();
265
+ selectedIdx = Math.min(selectedIdx, Math.max(0, vis2.length - 1));
266
+ redraw();
267
+ }).catch(() => {
268
+ redraw();
269
+ });
270
+ } else {
271
+ confirmDelete = null;
272
+ redraw();
273
+ }
274
+ return;
275
+ }
276
+ if (key === "q" || key === "") {
277
+ resolve(null);
278
+ return;
279
+ }
280
+ const vis = visible();
281
+ if (key === "\x1B[A") {
282
+ if (vis.length > 0) selectedIdx = Math.max(0, selectedIdx - 1);
283
+ redraw();
284
+ } else if (key === "\x1B[B") {
285
+ if (vis.length > 0) selectedIdx = Math.min(vis.length - 1, selectedIdx + 1);
286
+ redraw();
287
+ } else if (key === "\r") {
288
+ const sel = vis[selectedIdx];
289
+ if (sel?.deployed) resolve(sel.name);
290
+ } else if (key === "a") {
291
+ showAll = !showAll;
292
+ selectedIdx = 0;
293
+ redraw();
294
+ } else if (key === "d" && showAll) {
295
+ const sel = vis[selectedIdx];
296
+ if (sel !== void 0 && !sel.deployed) {
297
+ confirmDelete = sel.name;
298
+ redraw();
299
+ }
300
+ }
301
+ });
302
+ });
303
+ }
304
+ async function runDashboard(session, stackName, opts, ac, setKeyHandler, menuMode) {
305
+ let showLogs = true;
306
+ let lastSnapshot = null;
307
+ let refreshTimer = null;
308
+ function scheduleRefresh() {
309
+ refreshTimer = setTimeout(() => void doRefresh(), opts.intervalSec * 1e3);
310
+ }
311
+ function cancelRefresh() {
312
+ if (refreshTimer !== null) {
313
+ clearTimeout(refreshTimer);
314
+ refreshTimer = null;
315
+ }
316
+ }
317
+ function redraw() {
318
+ if (!lastSnapshot) return;
319
+ const out = renderSnapshot(lastSnapshot, {
320
+ stackName,
321
+ intervalSec: opts.intervalSec,
322
+ showLogs,
323
+ noColor: opts.noColor,
324
+ menuMode
325
+ });
326
+ process2.stdout.write("\x1B[2J\x1B[H" + out + "\n");
327
+ }
328
+ async function doRefresh() {
329
+ cancelRefresh();
330
+ if (ac.signal.aborted) return;
331
+ try {
332
+ lastSnapshot = await gatherSnapshot(session, ac.signal, opts.tailLines);
333
+ redraw();
334
+ } catch (err) {
335
+ if (!ac.signal.aborted) {
336
+ process2.stdout.write("\x1B[2J\x1B[H Error gathering snapshot: " + (err instanceof Error ? err.message : String(err)) + "\n");
337
+ }
338
+ }
339
+ if (!ac.signal.aborted) scheduleRefresh();
340
+ }
341
+ return new Promise((resolve) => {
342
+ if (ac.signal.aborted) {
343
+ resolve("quit");
344
+ return;
345
+ }
346
+ ac.signal.addEventListener("abort", () => {
347
+ cancelRefresh();
348
+ resolve("quit");
349
+ }, { once: true });
350
+ setKeyHandler((key) => {
351
+ if (key === "q" || key === "") {
352
+ ac.abort();
353
+ cancelRefresh();
354
+ resolve("quit");
355
+ } else if (key === "s" && menuMode) {
356
+ cancelRefresh();
357
+ resolve("back");
358
+ } else if (key === "r") {
359
+ void doRefresh();
360
+ } else if (key === "l") {
361
+ showLogs = !showLogs;
362
+ redraw();
363
+ }
364
+ });
365
+ void doRefresh();
366
+ });
367
+ }
368
+ var monitor_default = defineCommand({
369
+ meta: {
370
+ name: "monitor",
371
+ description: "Live dashboard: gateway health, container status, resource usage, log tail"
372
+ },
373
+ args: {
374
+ stack: { type: "string", description: "Target stack name (omit to pick from a list)" },
375
+ interval: { type: "string", description: "Refresh interval in seconds (default: 10)" },
376
+ tail: { type: "string", description: "Log lines to show (default: 10)" },
377
+ "no-color": { type: "boolean", description: "Disable ANSI colors (auto-detected when not a TTY)" }
378
+ },
379
+ async run({ args }) {
380
+ const intervalSec = Math.max(2, parseInt(String(args.interval ?? "10"), 10) || 10);
381
+ const tailLines = Math.max(1, parseInt(String(args.tail ?? "10"), 10) || 10);
382
+ const isTTY = Boolean(process2.stdout.isTTY);
383
+ const noColor = Boolean(args["no-color"]) || !isTTY;
384
+ const { buildContext: buildContext2 } = await import("./context-PSGEX2D7.js");
385
+ const { acquireSession: acquireSession2, drainPool: drainPool2 } = await import("./pool-FBFHATDG.js");
386
+ const ac = new AbortController();
387
+ process2.on("SIGINT", () => ac.abort());
388
+ process2.on("SIGTERM", () => ac.abort());
389
+ if (args.stack) {
390
+ const ctx = buildContext2(args);
391
+ let conn;
392
+ if (ctx.adapter.name === "local") {
393
+ if (!ctx.localState) {
394
+ failure("Stack is not bootstrapped. Run `clawops up` first.");
395
+ process2.exit(4);
396
+ }
397
+ const ls = ctx.localState;
398
+ conn = { host: ls.sshHost, port: ls.sshPort, user: ls.sshUser, privateKeyPath: ls.privateKeyPath, knownHostsPath: ls.knownHostsPath };
399
+ } else {
400
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
401
+ const stack = await ctx.getStack();
402
+ const outputMap = await stack.outputs();
403
+ const outputs = Object.fromEntries(
404
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
405
+ );
406
+ if (!outputs["publicIp"]) {
407
+ failure("Stack has no outputs. Run `clawops up` first.");
408
+ process2.exit(4);
409
+ }
410
+ const base = extractBaseOutputs(outputs);
411
+ conn = ctx.adapter.getConnectionInfo({
412
+ ...base,
413
+ privateKeyPath: ctx.config.ssh.keyPath,
414
+ knownHostsPath: ctx.config.ssh.knownHostsPath
415
+ });
416
+ }
417
+ if (!isTTY) {
418
+ const { session: session2, release: release2 } = await acquireSession2({ ...conn, signal: ac.signal });
419
+ try {
420
+ const snap = await gatherSnapshot(session2, ac.signal, tailLines);
421
+ process2.stdout.write(
422
+ renderSnapshot(snap, { stackName: ctx.stackName, intervalSec, showLogs: true, noColor }) + "\n"
423
+ );
424
+ } finally {
425
+ release2();
426
+ drainPool2();
427
+ }
428
+ return;
429
+ }
430
+ const { session, release } = await acquireSession2({ ...conn, signal: ac.signal });
431
+ process2.stdin.setRawMode(true);
432
+ process2.stdin.resume();
433
+ process2.stdin.setEncoding("utf8");
434
+ process2.stdout.write("\x1B[?25l");
435
+ let keyHandler2 = () => {
436
+ };
437
+ process2.stdin.on("data", (key) => keyHandler2(key));
438
+ try {
439
+ await runDashboard(session, ctx.stackName, { intervalSec, tailLines, noColor }, ac, (fn) => {
440
+ keyHandler2 = fn;
441
+ }, false);
442
+ } finally {
443
+ process2.stdin.setRawMode(false);
444
+ process2.stdin.pause();
445
+ process2.stdout.write("\x1B[?25h\n");
446
+ release();
447
+ drainPool2();
448
+ }
449
+ return;
450
+ }
451
+ if (!isTTY) {
452
+ failure("Pass --stack <name> or run in a TTY for interactive stack selection.");
453
+ process2.exit(2);
454
+ }
455
+ process2.stdin.setRawMode(true);
456
+ process2.stdin.resume();
457
+ process2.stdin.setEncoding("utf8");
458
+ process2.stdout.write("\x1B[?25l");
459
+ let keyHandler = () => {
460
+ };
461
+ process2.stdin.on("data", (key) => keyHandler(key));
462
+ try {
463
+ while (!ac.signal.aborted) {
464
+ const stackName = await runStackMenu(ac, (fn) => {
465
+ keyHandler = fn;
466
+ }, noColor);
467
+ if (!stackName || ac.signal.aborted) break;
468
+ const ctx = buildContext2({ stack: stackName });
469
+ let conn = null;
470
+ if (ctx.adapter.name === "local") {
471
+ if (!ctx.localState) continue;
472
+ const ls = ctx.localState;
473
+ conn = { host: ls.sshHost, port: ls.sshPort, user: ls.sshUser, privateKeyPath: ls.privateKeyPath, knownHostsPath: ls.knownHostsPath };
474
+ } else {
475
+ try {
476
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
477
+ const stack = await ctx.getStack();
478
+ const outputMap = await stack.outputs();
479
+ const outputs = Object.fromEntries(
480
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
481
+ );
482
+ if (!outputs["publicIp"]) continue;
483
+ const base = extractBaseOutputs(outputs);
484
+ conn = ctx.adapter.getConnectionInfo({
485
+ ...base,
486
+ privateKeyPath: ctx.config.ssh.keyPath,
487
+ knownHostsPath: ctx.config.ssh.knownHostsPath
488
+ });
489
+ } catch {
490
+ continue;
491
+ }
492
+ }
493
+ if (!conn) continue;
494
+ const { session, release } = await acquireSession2({ ...conn, signal: ac.signal });
495
+ try {
496
+ const result = await runDashboard(session, stackName, { intervalSec, tailLines, noColor }, ac, (fn) => {
497
+ keyHandler = fn;
498
+ }, true);
499
+ if (result === "quit") break;
500
+ } finally {
501
+ release();
502
+ drainPool2();
503
+ }
504
+ }
505
+ } finally {
506
+ process2.stdin.setRawMode(false);
507
+ process2.stdin.pause();
508
+ process2.stdout.write("\x1B[?25h\n");
509
+ }
510
+ }
511
+ });
512
+
513
+ // src/mcp/tools/_conn.ts
514
+ async function resolveConn(ctx) {
515
+ if (ctx.adapter.name === "local") {
516
+ const state = ctx.localState;
517
+ if (!state) throw new StateError("Stack has no local state \u2014 run `clawops up` first.");
518
+ return {
519
+ host: state.sshHost,
520
+ port: state.sshPort,
521
+ user: state.sshUser,
522
+ privateKeyPath: state.privateKeyPath,
523
+ knownHostsPath: state.knownHostsPath
524
+ };
525
+ }
526
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
527
+ const stack = await ctx.getStack();
528
+ const outputMap = await stack.outputs();
529
+ const outputs = Object.fromEntries(
530
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
531
+ );
532
+ if (!outputs["publicIp"]) {
533
+ throw new StateError("Stack has no outputs \u2014 run `clawops up` first.");
534
+ }
535
+ const base = extractBaseOutputs(outputs);
536
+ return ctx.adapter.getConnectionInfo({
537
+ ...base,
538
+ privateKeyPath: ctx.config.ssh.keyPath,
539
+ knownHostsPath: ctx.config.ssh.knownHostsPath
540
+ });
541
+ }
542
+ function errText(message) {
543
+ return { content: [{ type: "text", text: message }], isError: true };
544
+ }
545
+ function okText(t) {
546
+ return { content: [{ type: "text", text: t }] };
547
+ }
548
+
549
+ // src/mcp/tools/cli/config.ts
550
+ var VALID_AUTH_MODES = /* @__PURE__ */ new Set(["none", "token", "password", "trusted-proxy"]);
551
+ async function handleConfigGet(input, _server) {
552
+ const ac = new AbortController();
553
+ const ctx = buildContext({ stack: input.stackName });
554
+ const conn = await resolveConn(ctx);
555
+ const { session, release } = await acquireSession(conn);
556
+ try {
557
+ const result = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
558
+ let cfg;
559
+ try {
560
+ cfg = JSON.parse(result.stdout);
561
+ } catch {
562
+ return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${result.stderr || result.stdout}`);
563
+ }
564
+ const value = input.key ? getPath(cfg, input.key) : cfg;
565
+ return okText(JSON.stringify(value, null, 2));
566
+ } finally {
567
+ release();
568
+ drainPool();
569
+ }
570
+ }
571
+ async function handleConfigSet(input, server) {
572
+ const elicit = await server.server.elicitInput({
573
+ message: `Set ${input.key} = ${input.value} on stack "${input.stackName ?? "default"}"?`,
574
+ requestedSchema: {
575
+ type: "object",
576
+ properties: { confirmed: { type: "boolean", title: "Confirm config change" } },
577
+ required: ["confirmed"]
578
+ }
579
+ });
580
+ if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
581
+ return okText("Config change cancelled.");
582
+ }
583
+ const ac = new AbortController();
584
+ const ctx = buildContext({ stack: input.stackName });
585
+ const conn = await resolveConn(ctx);
586
+ const { session, release } = await acquireSession(conn);
587
+ try {
588
+ const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
589
+ let cfg;
590
+ try {
591
+ cfg = JSON.parse(readResult.stdout);
592
+ } catch {
593
+ return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr}`);
594
+ }
595
+ let parsedValue = input.value;
596
+ try {
597
+ parsedValue = JSON.parse(input.value);
598
+ } catch {
599
+ }
600
+ setPath(cfg, input.key, parsedValue);
601
+ try {
602
+ await atomicWriteConfig(session, cfg, ac.signal);
603
+ } catch (err) {
604
+ return errText(`Failed to write config: ${err.message}`);
605
+ }
606
+ let note = "";
607
+ if (input.restart) {
608
+ try {
609
+ await restartGateway(session, ac.signal);
610
+ note = " (gateway restarted)";
611
+ } catch (err) {
612
+ return errText(`Gateway restart failed: ${err.message}`);
613
+ }
614
+ }
615
+ return okText(`Config set: ${input.key}${note}`);
616
+ } finally {
617
+ release();
618
+ drainPool();
619
+ }
620
+ }
621
+ async function handleConfigUnset(input, server) {
622
+ const elicit = await server.server.elicitInput({
623
+ message: `Remove config key "${input.key}" on stack "${input.stackName ?? "default"}"?`,
624
+ requestedSchema: {
625
+ type: "object",
626
+ properties: { confirmed: { type: "boolean", title: "Confirm key removal" } },
627
+ required: ["confirmed"]
628
+ }
629
+ });
630
+ if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
631
+ return okText("Config unset cancelled.");
632
+ }
633
+ const ac = new AbortController();
634
+ const ctx = buildContext({ stack: input.stackName });
635
+ const conn = await resolveConn(ctx);
636
+ const { session, release } = await acquireSession(conn);
637
+ try {
638
+ const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
639
+ let cfg;
640
+ try {
641
+ cfg = JSON.parse(readResult.stdout);
642
+ } catch {
643
+ return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr}`);
644
+ }
645
+ deletePath(cfg, input.key);
646
+ try {
647
+ await atomicWriteConfig(session, cfg, ac.signal);
648
+ } catch (err) {
649
+ return errText(`Failed to write config: ${err.message}`);
650
+ }
651
+ let note = "";
652
+ if (input.restart) {
653
+ try {
654
+ await restartGateway(session, ac.signal);
655
+ note = " (gateway restarted)";
656
+ } catch (err) {
657
+ return errText(`Gateway restart failed: ${err.message}`);
658
+ }
659
+ }
660
+ return okText(`Config key removed: ${input.key}${note}`);
661
+ } finally {
662
+ release();
663
+ drainPool();
664
+ }
665
+ }
666
+ async function handleConfigValidate(input, _server) {
667
+ const ac = new AbortController();
668
+ const ctx = buildContext({ stack: input.stackName });
669
+ const conn = await resolveConn(ctx);
670
+ const { session, release } = await acquireSession(conn);
671
+ try {
672
+ const result = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
673
+ let cfg;
674
+ try {
675
+ cfg = JSON.parse(result.stdout);
676
+ } catch {
677
+ return okText(JSON.stringify({ valid: false, issues: [`Invalid JSON: ${result.stderr || result.stdout}`] }));
678
+ }
679
+ const issues = validateOpenclawConfig(cfg);
680
+ return okText(JSON.stringify({ valid: issues.length === 0, issues }));
681
+ } finally {
682
+ release();
683
+ drainPool();
684
+ }
685
+ }
686
+ function getPath(obj, dotKey) {
687
+ return dotKey.split(".").reduce((cur, k) => {
688
+ if (cur !== null && typeof cur === "object") return cur[k];
689
+ return void 0;
690
+ }, obj);
691
+ }
692
+ function setPath(obj, dotKey, value) {
693
+ const keys = dotKey.split(".");
694
+ let cur = obj;
695
+ for (let i = 0; i < keys.length - 1; i++) {
696
+ const k = keys[i];
697
+ if (typeof cur[k] !== "object" || cur[k] === null) cur[k] = {};
698
+ cur = cur[k];
699
+ }
700
+ cur[keys[keys.length - 1]] = value;
701
+ }
702
+ function deletePath(obj, dotKey) {
703
+ const keys = dotKey.split(".");
704
+ let cur = obj;
705
+ for (let i = 0; i < keys.length - 1; i++) {
706
+ const k = keys[i];
707
+ if (typeof cur[k] !== "object" || cur[k] === null) return;
708
+ cur = cur[k];
709
+ }
710
+ delete cur[keys[keys.length - 1]];
711
+ }
712
+ function validateOpenclawConfig(cfg) {
713
+ const issues = [];
714
+ if ("version" in cfg) {
715
+ issues.push(
716
+ "Top-level 'version' is not a valid OpenClaw config key. Use 'meta.lastTouchedVersion' (string) instead."
717
+ );
718
+ }
719
+ if ("channels" in cfg && Array.isArray(cfg["channels"])) {
720
+ issues.push(
721
+ `'channels' must be an object keyed by provider name (e.g. {"discord":{...}}), not an array.`
722
+ );
723
+ }
724
+ const meta = cfg["meta"];
725
+ if (meta !== void 0 && (typeof meta !== "object" || Array.isArray(meta) || meta === null)) {
726
+ issues.push("'meta' must be an object.");
727
+ } else if (meta && typeof meta === "object") {
728
+ const ltv = meta["lastTouchedVersion"];
729
+ if (ltv !== void 0 && typeof ltv !== "string") {
730
+ issues.push("'meta.lastTouchedVersion' must be a string.");
731
+ }
732
+ }
733
+ const gateway = cfg["gateway"];
734
+ if (gateway !== void 0 && typeof gateway === "object" && !Array.isArray(gateway) && gateway !== null) {
735
+ const gw = gateway;
736
+ if ("port" in gw && typeof gw["port"] !== "number") {
737
+ issues.push("'gateway.port' must be a number.");
738
+ }
739
+ const auth = gw["auth"];
740
+ if (auth !== void 0 && typeof auth === "object" && !Array.isArray(auth) && auth !== null) {
741
+ const mode = auth["mode"];
742
+ if (mode !== void 0 && !VALID_AUTH_MODES.has(mode)) {
743
+ issues.push(
744
+ `'gateway.auth.mode' must be one of: ${[...VALID_AUTH_MODES].join(", ")}. Got: "${mode}".`
745
+ );
746
+ }
747
+ }
748
+ }
749
+ return issues;
750
+ }
751
+
752
+ export {
753
+ chalk,
754
+ success,
755
+ failure,
756
+ warn,
757
+ info,
758
+ spinner,
759
+ printCta,
760
+ resolveConn,
761
+ errText,
762
+ okText,
763
+ handleConfigGet,
764
+ handleConfigSet,
765
+ handleConfigUnset,
766
+ handleConfigValidate,
767
+ validateOpenclawConfig,
768
+ formatUptime,
769
+ gatherSnapshot,
770
+ monitor_default
771
+ };