@clawops/cli 1.7.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +412 -70
  2. package/dist/{apply-F3KY2HTB.js → apply-AM5HERE7.js} +8 -6
  3. package/dist/{aws-L4RR2LSE.js → aws-AMON7DA7.js} +20 -9
  4. package/dist/{azure-7OXQENQL.js → azure-T4G6OBJQ.js} +19 -8
  5. package/dist/{bootstrap-HL4DNUNG.js → bootstrap-DMRPWJGN.js} +28 -19
  6. package/dist/bootstrap.sh.tmpl +51 -31
  7. package/dist/channels-KBUR42IU.js +53 -0
  8. package/dist/{chunk-DQDAWNGB.js → chunk-3WYA2QDM.js} +1 -1
  9. package/dist/chunk-6TIYVHPX.js +152 -0
  10. package/dist/{chunk-3XK66DKK.js → chunk-AHAAEXWC.js} +41 -0
  11. package/dist/{chunk-GZN7PKP4.js → chunk-AX74VXTV.js} +292 -94
  12. package/dist/{chunk-C5X375RO.js → chunk-CKSPROD5.js} +59 -15
  13. package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
  14. package/dist/{chunk-FNS56UQY.js → chunk-GYV4KNW7.js} +319 -252
  15. package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
  16. package/dist/chunk-LT4XMO5D.js +50 -0
  17. package/dist/{chunk-P5LKWRPL.js → chunk-LU2V3LXA.js} +3 -3
  18. package/dist/chunk-N44K3LHE.js +140 -0
  19. package/dist/chunk-NUDIHZEH.js +55 -0
  20. package/dist/chunk-SBLQH2TJ.js +56 -0
  21. package/dist/chunk-UFRO4H5W.js +228 -0
  22. package/dist/{chunk-A4QR4TXW.js → chunk-WHJMKDV7.js} +24 -4
  23. package/dist/chunk-YR3O2I3S.js +101 -0
  24. package/dist/cli.js +866 -577
  25. package/dist/config-validate-K2N6XLDX.js +76 -0
  26. package/dist/{context-5LCUG2QR.js → context-KHKWJJMC.js} +1 -1
  27. package/dist/{firewall-XQGO7HWK.js → firewall-T3NTPZJD.js} +10 -0
  28. package/dist/{gcp-UG6AWPIZ.js → gcp-2JBTD7ZM.js} +19 -8
  29. package/dist/{generate-OAGYT3VH.js → generate-2XV2GSP2.js} +4 -4
  30. package/dist/{harden-URPXLSVF.js → harden-PCDK3UQ5.js} +48 -11
  31. package/dist/health-5CCOXYGG.js +11 -0
  32. package/dist/logs-P7YCKVF4.js +15 -0
  33. package/dist/{mcp-wire-7E5JQY47.js → mcp-wire-BFEY2Y6H.js} +23 -17
  34. package/dist/{package-5AR4ZPK4.js → package-3HUW57XH.js} +3 -3
  35. package/dist/plugins-DTST3OER.js +49 -0
  36. package/dist/{pool-QGFQRMGU.js → pool-EX4ZTA5P.js} +2 -2
  37. package/dist/privileged-JFA6LLTV.js +17 -0
  38. package/dist/{remote-config-QDLN4UMG.js → remote-config-Z6IIZR3D.js} +3 -3
  39. package/dist/runtime-LZ7NA53V.js +42 -0
  40. package/dist/{server-FIZCOLHL.js → server-A5AB67GQ.js} +16 -12
  41. package/dist/spec-path-KQOOIQCA.js +9 -0
  42. package/dist/{ssh-CNJNLM3E.js → ssh-B3NZAU4I.js} +1 -1
  43. package/dist/upgrade-ULD4KFYC.js +88 -0
  44. package/dist/validate-BOQRWX4F.js +14 -0
  45. package/dist/{version-guard-VUCNZXQS.js → version-guard-GPSTCPT7.js} +3 -3
  46. package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
  47. package/package.json +3 -3
  48. package/spec/deploy-plan.schema.json +114 -25
  49. package/spec/integrations.yaml +135 -28
  50. package/spec/mcp-tools.yaml +68 -35
  51. package/spec/models.yaml +9 -0
  52. package/spec/openclaw-2.0.config.schema.json +1 -0
  53. package/spec/openclaw-versions.yaml +116 -27
  54. package/dist/chunk-35WVQY3X.js +0 -122
  55. package/dist/chunk-6X45DDGH.js +0 -42
  56. package/dist/chunk-SEZMVVAS.js +0 -156
  57. package/dist/validate-CFQ4FH2T.js +0 -10
@@ -1,28 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- acquireSession,
4
- drainPool
5
- } from "./chunk-DQDAWNGB.js";
3
+ interpretProbe,
4
+ probeCommand
5
+ } from "./chunk-LT4XMO5D.js";
6
6
  import {
7
- OPENCLAW_CONFIG,
8
- atomicWriteConfig,
9
- restartGateway
10
- } from "./chunk-SEZMVVAS.js";
11
- import {
12
- buildContext
13
- } from "./chunk-P5LKWRPL.js";
7
+ execPrivileged
8
+ } from "./chunk-NUDIHZEH.js";
14
9
  import {
15
10
  chalk,
16
11
  failure
17
12
  } from "./chunk-Q7NQY5HV.js";
18
13
  import {
19
- StateError
20
- } from "./chunk-KGXPLI7W.js";
14
+ GATEWAY_PORT,
15
+ PUBLISH_INSPECT_CMD,
16
+ STATE_DIR_HOST_LINUX,
17
+ configPathForOS,
18
+ publishForRestart
19
+ } from "./chunk-6TIYVHPX.js";
21
20
 
22
21
  // src/cli/commands/monitor.ts
23
22
  import { defineCommand } from "citty";
24
23
  import process from "process";
25
- var GATEWAY_PORT = 18789;
26
24
  function formatUptime(startedAt) {
27
25
  if (!startedAt) return "\u2014";
28
26
  const ms = Date.now() - new Date(startedAt).getTime();
@@ -38,27 +36,29 @@ function formatUptime(startedAt) {
38
36
  async function gatherSnapshot(session, signal, tailLines = 10) {
39
37
  const DOCKER = "PATH=/usr/local/bin:/opt/homebrew/bin:$PATH docker";
40
38
  const [inspectRaw, statsRaw, healthRaw, configRaw, diskRaw, logsRaw] = await Promise.all([
41
- session.exec(
39
+ execPrivileged(
40
+ session,
42
41
  `${DOCKER} inspect openclaw --format '{{.State.Status}}|{{.Config.Image}}|{{.State.StartedAt}}|{{.RestartCount}}' 2>/dev/null || echo 'not found|||0'`,
43
42
  signal
44
43
  ),
45
- session.exec(
44
+ execPrivileged(
45
+ session,
46
46
  `${DOCKER} stats openclaw --no-stream --format '{{.MemUsage}}|{{.CPUPerc}}' 2>/dev/null || echo '\u2014|\u2014'`,
47
47
  signal
48
48
  ),
49
+ session.exec(probeCommand("live", GATEWAY_PORT), signal),
49
50
  session.exec(
50
- `curl -sf --connect-timeout 2 http://localhost:${GATEWAY_PORT}/health >/dev/null 2>&1 && echo ok || echo unreachable`,
51
+ `cat ${configPathForOS("Linux")} 2>/dev/null || echo '{}'`,
51
52
  signal
52
53
  ),
53
54
  session.exec(
54
- `cat /home/clawops/openclaw.json 2>/dev/null || echo '{}'`,
55
+ // The state directory, not the service user's home: 2.0 keeps the SQLite database
56
+ // there, so that is the filesystem that fills up.
57
+ `df -h ${STATE_DIR_HOST_LINUX} 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
55
58
  signal
56
59
  ),
57
- session.exec(
58
- `df -h /home/clawops 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
59
- signal
60
- ),
61
- session.exec(
60
+ execPrivileged(
61
+ session,
62
62
  `${DOCKER} logs openclaw -n ${tailLines} 2>&1 || echo '(no logs)'`,
63
63
  signal
64
64
  )
@@ -82,7 +82,9 @@ async function gatherSnapshot(session, signal, tailLines = 10) {
82
82
  return {
83
83
  container: { status, image, startedAt, restartCount, memUsage, cpuPct },
84
84
  gateway: {
85
- reachable: healthRaw.stdout.trim() === "ok",
85
+ // Judged on the payload: a status code cannot distinguish a healthy gateway from
86
+ // a path that fell through to the Control UI. See src/openclaw/health.ts.
87
+ reachable: interpretProbe("live", healthRaw.stdout).ok,
86
88
  version,
87
89
  authMode
88
90
  },
@@ -131,7 +133,7 @@ function renderSnapshot(snap, opts) {
131
133
  return lines.join("\n");
132
134
  }
133
135
  async function probeEntries() {
134
- const { buildContext: buildContext2 } = await import("./context-5LCUG2QR.js");
136
+ const { buildContext } = await import("./context-KHKWJJMC.js");
135
137
  const { getConfig } = await import("./store-SDUR52Z5.js");
136
138
  const config = getConfig();
137
139
  if (!config) return [];
@@ -144,7 +146,7 @@ async function probeEntries() {
144
146
  deployed: false
145
147
  };
146
148
  try {
147
- const ctx = buildContext2({ stack: name });
149
+ const ctx = buildContext({ stack: name });
148
150
  if (ctx.adapter.name === "local") {
149
151
  return { ...base, deployed: !!ctx.localState };
150
152
  }
@@ -358,13 +360,13 @@ var monitor_default = defineCommand({
358
360
  const tailLines = Math.max(1, parseInt(String(args.tail ?? "10"), 10) || 10);
359
361
  const isTTY = Boolean(process.stdout.isTTY);
360
362
  const noColor = Boolean(args["no-color"]) || !isTTY;
361
- const { buildContext: buildContext2 } = await import("./context-5LCUG2QR.js");
362
- const { acquireSession: acquireSession2, drainPool: drainPool2 } = await import("./pool-QGFQRMGU.js");
363
+ const { buildContext } = await import("./context-KHKWJJMC.js");
364
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
363
365
  const ac = new AbortController();
364
366
  process.on("SIGINT", () => ac.abort());
365
367
  process.on("SIGTERM", () => ac.abort());
366
368
  if (args.stack) {
367
- const ctx = buildContext2(args);
369
+ const ctx = buildContext(args);
368
370
  let conn;
369
371
  if (ctx.adapter.name === "local") {
370
372
  if (!ctx.localState) {
@@ -392,7 +394,7 @@ var monitor_default = defineCommand({
392
394
  });
393
395
  }
394
396
  if (!isTTY) {
395
- const { session: session2, release: release2 } = await acquireSession2({ ...conn, signal: ac.signal });
397
+ const { session: session2, release: release2 } = await acquireSession({ ...conn, signal: ac.signal });
396
398
  try {
397
399
  const snap = await gatherSnapshot(session2, ac.signal, tailLines);
398
400
  process.stdout.write(
@@ -400,11 +402,11 @@ var monitor_default = defineCommand({
400
402
  );
401
403
  } finally {
402
404
  release2();
403
- drainPool2();
405
+ drainPool();
404
406
  }
405
407
  return;
406
408
  }
407
- const { session, release } = await acquireSession2({ ...conn, signal: ac.signal });
409
+ const { session, release } = await acquireSession({ ...conn, signal: ac.signal });
408
410
  process.stdin.setRawMode(true);
409
411
  process.stdin.resume();
410
412
  process.stdin.setEncoding("utf8");
@@ -421,7 +423,7 @@ var monitor_default = defineCommand({
421
423
  process.stdin.pause();
422
424
  process.stdout.write("\x1B[?25h\n");
423
425
  release();
424
- drainPool2();
426
+ drainPool();
425
427
  }
426
428
  return;
427
429
  }
@@ -442,7 +444,7 @@ var monitor_default = defineCommand({
442
444
  keyHandler = fn;
443
445
  }, noColor);
444
446
  if (!stackName || ac.signal.aborted) break;
445
- const ctx = buildContext2({ stack: stackName });
447
+ const ctx = buildContext({ stack: stackName });
446
448
  let conn = null;
447
449
  if (ctx.adapter.name === "local") {
448
450
  if (!ctx.localState) continue;
@@ -468,7 +470,7 @@ var monitor_default = defineCommand({
468
470
  }
469
471
  }
470
472
  if (!conn) continue;
471
- const { session, release } = await acquireSession2({ ...conn, signal: ac.signal });
473
+ const { session, release } = await acquireSession({ ...conn, signal: ac.signal });
472
474
  try {
473
475
  const result = await runDashboard(session, stackName, { intervalSec, tailLines, noColor }, ac, (fn) => {
474
476
  keyHandler = fn;
@@ -476,7 +478,7 @@ var monitor_default = defineCommand({
476
478
  if (result === "quit") break;
477
479
  } finally {
478
480
  release();
479
- drainPool2();
481
+ drainPool();
480
482
  }
481
483
  }
482
484
  } finally {
@@ -487,254 +489,319 @@ var monitor_default = defineCommand({
487
489
  }
488
490
  });
489
491
 
490
- // src/mcp/tools/_conn.ts
491
- async function resolveConn(ctx) {
492
- if (ctx.adapter.name === "local") {
493
- const state = ctx.localState;
494
- if (!state) throw new StateError("Stack has no local state \u2014 run `clawops up` first.");
495
- return {
496
- host: state.sshHost,
497
- port: state.sshPort,
498
- user: state.sshUser,
499
- privateKeyPath: state.privateKeyPath,
500
- knownHostsPath: state.knownHostsPath
501
- };
502
- }
503
- const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
504
- const stack = await ctx.getStack();
505
- const outputMap = await stack.outputs();
506
- const outputs = Object.fromEntries(
507
- Object.entries(outputMap).map(([k, v]) => [k, v.value])
508
- );
509
- if (!outputs["publicIp"]) {
510
- throw new StateError("Stack has no outputs \u2014 run `clawops up` first.");
511
- }
512
- const base = extractBaseOutputs(outputs);
513
- return ctx.adapter.getConnectionInfo({
514
- ...base,
515
- privateKeyPath: ctx.config.ssh.keyPath,
516
- knownHostsPath: ctx.config.ssh.knownHostsPath
517
- });
518
- }
519
- function errText(message) {
520
- return { content: [{ type: "text", text: message }], isError: true };
521
- }
522
- function okText(t) {
523
- return { content: [{ type: "text", text: t }] };
524
- }
525
-
526
- // src/mcp/tools/cli/config.ts
527
- var VALID_AUTH_MODES = /* @__PURE__ */ new Set(["none", "token", "password", "trusted-proxy"]);
528
- async function handleConfigGet(input, _server) {
529
- const ac = new AbortController();
530
- const ctx = buildContext({ stack: input.stackName });
531
- const conn = await resolveConn(ctx);
532
- const { session, release } = await acquireSession(conn);
533
- try {
534
- const result = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
535
- let cfg;
536
- try {
537
- cfg = JSON.parse(result.stdout);
538
- } catch {
539
- return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${result.stderr || result.stdout}`);
540
- }
541
- const value = input.key ? getPath(cfg, input.key) : cfg;
542
- return okText(JSON.stringify(value, null, 2));
543
- } finally {
544
- release();
545
- drainPool();
546
- }
547
- }
548
- async function handleConfigSet(input, server) {
549
- const elicit = await server.server.elicitInput({
550
- message: `Set ${input.key} = ${input.value} on stack "${input.stackName ?? "default"}"?`,
551
- requestedSchema: {
552
- type: "object",
553
- properties: { confirmed: { type: "boolean", title: "Confirm config change" } },
554
- required: ["confirmed"]
555
- }
492
+ // src/diagnostics/index.ts
493
+ import process2 from "process";
494
+ import { accessSync, mkdirSync, constants } from "fs";
495
+ import path from "path";
496
+ async function runDiagnostics(opts = {}, deps = {}) {
497
+ const { getConfig, getConfigDir } = await import("./store-SDUR52Z5.js");
498
+ const config = getConfig();
499
+ const sections = [];
500
+ sections.push({ title: "Runtime", checks: runtimeChecks(getConfigDir()) });
501
+ sections.push({
502
+ title: "Config",
503
+ checks: [
504
+ config ? { name: "Config file", status: "pass", detail: path.join(getConfigDir(), "config.json") } : { name: "Config file", status: "warn", detail: "not found", remedy: "run `clawops init`" }
505
+ ]
556
506
  });
557
- if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
558
- return okText("Config change cancelled.");
559
- }
560
- const ac = new AbortController();
561
- const ctx = buildContext({ stack: input.stackName });
562
- const conn = await resolveConn(ctx);
563
- const { session, release } = await acquireSession(conn);
564
- try {
565
- const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
566
- let cfg;
567
- try {
568
- cfg = JSON.parse(readResult.stdout);
569
- } catch {
570
- return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr}`);
571
- }
572
- let parsedValue = input.value;
507
+ sections.push({ title: "SSH", checks: sshChecks(config) });
508
+ sections.push({ title: "Credentials", checks: await credentialChecks(config) });
509
+ sections.push({ title: "OpenClaw", checks: await versionChecks() });
510
+ if (opts.stack) {
511
+ const open = deps.openSession ?? defaultOpenSession;
512
+ let handle;
573
513
  try {
574
- parsedValue = JSON.parse(input.value);
575
- } catch {
576
- }
577
- setPath(cfg, input.key, parsedValue);
578
- try {
579
- await atomicWriteConfig(session, cfg, ac.signal);
514
+ handle = await open(opts.stack, opts.signal);
580
515
  } catch (err) {
581
- return errText(`Failed to write config: ${err.message}`);
516
+ sections.push({
517
+ title: "Remote health",
518
+ checks: [{ name: "Connection", status: "fail", detail: messageOf(err) }]
519
+ });
582
520
  }
583
- let note = "";
584
- if (input.restart) {
521
+ if (handle) {
585
522
  try {
586
- await restartGateway(session, ac.signal);
587
- note = " (gateway restarted)";
588
- } catch (err) {
589
- return errText(`Gateway restart failed: ${err.message}`);
523
+ sections.push({
524
+ title: "Remote health",
525
+ checks: await remoteChecks(handle.session, opts.signal)
526
+ });
527
+ sections.push({
528
+ title: "Hardening",
529
+ checks: await hardeningChecks(handle.conn, opts.stack, opts.signal)
530
+ });
531
+ } finally {
532
+ handle.release();
533
+ const { drainPool } = await import("./pool-EX4ZTA5P.js");
534
+ drainPool();
590
535
  }
591
536
  }
592
- return okText(`Config set: ${input.key}${note}`);
593
- } finally {
594
- release();
595
- drainPool();
596
537
  }
538
+ return summarise(sections);
539
+ }
540
+ function summarise(sections) {
541
+ const counts = { pass: 0, fail: 0, warn: 0, info: 0 };
542
+ for (const s of sections) for (const c of s.checks) counts[c.status]++;
543
+ return { sections, ok: counts.fail === 0, counts };
597
544
  }
598
- async function handleConfigUnset(input, server) {
599
- const elicit = await server.server.elicitInput({
600
- message: `Remove config key "${input.key}" on stack "${input.stackName ?? "default"}"?`,
601
- requestedSchema: {
602
- type: "object",
603
- properties: { confirmed: { type: "boolean", title: "Confirm key removal" } },
604
- required: ["confirmed"]
545
+ function runtimeChecks(configDir) {
546
+ const checks = [];
547
+ const nodeMajor = parseInt(process2.version.slice(1).split(".")[0] ?? "0", 10);
548
+ checks.push(
549
+ nodeMajor >= 22 ? { name: "Node.js", status: "pass", detail: process2.version } : {
550
+ name: "Node.js",
551
+ status: "fail",
552
+ detail: `${process2.version} (requires >=22)`,
553
+ remedy: "install Node.js 22 or later"
605
554
  }
606
- });
607
- if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
608
- return okText("Config unset cancelled.");
555
+ );
556
+ const pulumiHome = path.join(configDir, ".pulumi");
557
+ try {
558
+ mkdirSync(pulumiHome, { recursive: true });
559
+ checks.push({ name: "Pulumi home", status: "pass", detail: pulumiHome });
560
+ } catch {
561
+ checks.push({ name: "Pulumi home", status: "fail", detail: `${pulumiHome} (not writable)` });
562
+ }
563
+ return checks;
564
+ }
565
+ function expandHome(p) {
566
+ return p.replace(/^~/, process2.env["HOME"] ?? "~");
567
+ }
568
+ function sshChecks(config) {
569
+ if (!config) return [{ name: "SSH", status: "info", detail: "skipped \u2014 no config" }];
570
+ const checks = [];
571
+ const keyPath = expandHome(config.ssh.keyPath);
572
+ try {
573
+ accessSync(keyPath, constants.R_OK);
574
+ checks.push({ name: "SSH key", status: "pass", detail: keyPath });
575
+ } catch {
576
+ checks.push({
577
+ name: "SSH key",
578
+ status: "fail",
579
+ detail: `${keyPath} (not found or not readable)`
580
+ });
609
581
  }
610
- const ac = new AbortController();
611
- const ctx = buildContext({ stack: input.stackName });
612
- const conn = await resolveConn(ctx);
613
- const { session, release } = await acquireSession(conn);
582
+ const knownHostsPath = expandHome(config.ssh.knownHostsPath);
614
583
  try {
615
- const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
616
- let cfg;
617
- try {
618
- cfg = JSON.parse(readResult.stdout);
619
- } catch {
620
- return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr}`);
584
+ accessSync(knownHostsPath, constants.F_OK);
585
+ checks.push({ name: "known_hosts", status: "pass", detail: knownHostsPath });
586
+ } catch {
587
+ checks.push({
588
+ name: "known_hosts",
589
+ status: "warn",
590
+ detail: `${knownHostsPath} (does not exist \u2014 will be created on first connect)`
591
+ });
592
+ }
593
+ return checks;
594
+ }
595
+ async function credentialChecks(config) {
596
+ if (!config) return [{ name: "Credentials", status: "info", detail: "skipped \u2014 no config" }];
597
+ const { getProvider } = await import("./providers-2OABPW2E.js");
598
+ await Promise.all([
599
+ import("./aws-AMON7DA7.js"),
600
+ import("./gcp-2JBTD7ZM.js"),
601
+ import("./azure-T4G6OBJQ.js"),
602
+ import("./local-DXBEVZ5C.js")
603
+ ]);
604
+ const checks = [];
605
+ const seen = /* @__PURE__ */ new Set();
606
+ for (const [stackName, stackCfg] of Object.entries(config.stacks)) {
607
+ const providerName = stackCfg.provider;
608
+ if (seen.has(providerName)) continue;
609
+ seen.add(providerName);
610
+ if (providerName === "local") {
611
+ checks.push({
612
+ name: providerName,
613
+ status: "pass",
614
+ detail: `stack "${stackName}" (SSH-only, no cloud credentials required)`
615
+ });
616
+ continue;
621
617
  }
622
- deletePath(cfg, input.key);
623
618
  try {
624
- await atomicWriteConfig(session, cfg, ac.signal);
625
- } catch (err) {
626
- return errText(`Failed to write config: ${err.message}`);
627
- }
628
- let note = "";
629
- if (input.restart) {
630
- try {
631
- await restartGateway(session, ac.signal);
632
- note = " (gateway restarted)";
633
- } catch (err) {
634
- return errText(`Gateway restart failed: ${err.message}`);
619
+ const adapter = getProvider(providerName);
620
+ const result = await adapter.validateConfig();
621
+ if (result.ok) {
622
+ checks.push({ name: providerName, status: "pass", detail: `stack "${stackName}"` });
623
+ } else {
624
+ for (const err of result.errors) {
625
+ checks.push({ name: providerName, status: "fail", detail: `stack "${stackName}" \u2014 ${err}` });
626
+ }
635
627
  }
628
+ } catch (err) {
629
+ checks.push({
630
+ name: providerName,
631
+ status: "fail",
632
+ detail: `stack "${stackName}" \u2014 ${messageOf(err)}`
633
+ });
636
634
  }
637
- return okText(`Config key removed: ${input.key}${note}`);
638
- } finally {
639
- release();
640
- drainPool();
641
635
  }
636
+ if (seen.size === 0) {
637
+ checks.push({ name: "Stacks", status: "warn", detail: "none configured", remedy: "run `clawops init`" });
638
+ }
639
+ return checks;
642
640
  }
643
- async function handleConfigValidate(input, _server) {
644
- const ac = new AbortController();
645
- const ctx = buildContext({ stack: input.stackName });
646
- const conn = await resolveConn(ctx);
647
- const { session, release } = await acquireSession(conn);
641
+ async function versionChecks() {
648
642
  try {
649
- const result = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
650
- let cfg;
651
- try {
652
- cfg = JSON.parse(result.stdout);
653
- } catch {
654
- return okText(JSON.stringify({ valid: false, issues: [`Invalid JSON: ${result.stderr || result.stdout}`] }));
643
+ const yaml = await import("./js-yaml-PTEEG4FO.js");
644
+ const { loadVersionSpec, describeRange } = await import("./versions-W5LRQCAJ.js");
645
+ const support = loadVersionSpec(yaml).support;
646
+ const checks = [
647
+ {
648
+ name: "Supported range",
649
+ status: "pass",
650
+ detail: `${describeRange(support)} (recommended ${support.recommended})`
651
+ }
652
+ ];
653
+ if (!support.max) {
654
+ checks.push({
655
+ name: "Upper bound",
656
+ status: "warn",
657
+ detail: "none declared \u2014 this line would accept any OpenClaw release"
658
+ });
655
659
  }
656
- const issues = validateOpenclawConfig(cfg);
657
- return okText(JSON.stringify({ valid: issues.length === 0, issues }));
658
- } finally {
659
- release();
660
- drainPool();
660
+ return checks;
661
+ } catch (e) {
662
+ return [{ name: "Version matrix", status: "fail", detail: messageOf(e) }];
661
663
  }
662
664
  }
663
- function getPath(obj, dotKey) {
664
- return dotKey.split(".").reduce((cur, k) => {
665
- if (cur !== null && typeof cur === "object") return cur[k];
666
- return void 0;
667
- }, obj);
668
- }
669
- function setPath(obj, dotKey, value) {
670
- const keys = dotKey.split(".");
671
- let cur = obj;
672
- for (let i = 0; i < keys.length - 1; i++) {
673
- const k = keys[i];
674
- if (typeof cur[k] !== "object" || cur[k] === null) cur[k] = {};
675
- cur = cur[k];
665
+ async function remoteChecks(session, signal) {
666
+ const checks = [];
667
+ const containerResult = await execPrivileged(
668
+ session,
669
+ `docker inspect openclaw --format '{{.State.Status}}' 2>/dev/null || echo 'not found'`,
670
+ signal
671
+ );
672
+ const containerStatus = containerResult.stdout.trim();
673
+ checks.push(
674
+ containerStatus === "running" ? { name: "Container", status: "pass", detail: "running" } : { name: "Container", status: "fail", detail: containerStatus || "unknown" }
675
+ );
676
+ const imageResult = await execPrivileged(
677
+ session,
678
+ `docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo ''`,
679
+ signal
680
+ );
681
+ const image = imageResult.stdout.trim();
682
+ const deployedVersion = image.includes(":") ? image.slice(image.lastIndexOf(":") + 1) : "";
683
+ if (deployedVersion) {
684
+ checks.push(await deployedVersionCheck(image, deployedVersion));
676
685
  }
677
- cur[keys[keys.length - 1]] = value;
678
- }
679
- function deletePath(obj, dotKey) {
680
- const keys = dotKey.split(".");
681
- let cur = obj;
682
- for (let i = 0; i < keys.length - 1; i++) {
683
- const k = keys[i];
684
- if (typeof cur[k] !== "object" || cur[k] === null) return;
685
- cur = cur[k];
686
+ const probe = await session.exec(probeCommand("started", GATEWAY_PORT), signal);
687
+ const verdict = interpretProbe("started", probe.stdout);
688
+ checks.push(
689
+ verdict.ok ? { name: "Gateway", status: "pass", detail: `answering on 127.0.0.1:${GATEWAY_PORT}` } : { name: "Gateway", status: "fail", detail: verdict.reason ?? "not answering" }
690
+ );
691
+ const pubResult = await execPrivileged(session, PUBLISH_INSPECT_CMD, signal);
692
+ checks.push(
693
+ publishForRestart(pubResult.stdout) === "all" ? {
694
+ name: "Published",
695
+ status: "warn",
696
+ detail: "0.0.0.0 \u2014 the gateway port is open on every interface, serving plaintext HTTP",
697
+ remedy: 'front it with TLS, or set network.publishGateway to "loopback" and use `clawops tunnel`'
698
+ } : { name: "Published", status: "pass", detail: "127.0.0.1 only (reach it with `clawops tunnel`)" }
699
+ );
700
+ const diskResult = await session.exec(
701
+ // The state directory: 2.0's SQLite lives there, not in the user's home.
702
+ `df -h ${STATE_DIR_HOST_LINUX} 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
703
+ signal
704
+ );
705
+ const diskUsage = diskResult.stdout.trim();
706
+ if (diskUsage) {
707
+ const pct = parseInt(diskUsage.match(/^(\d+)%/)?.[1] ?? "0", 10);
708
+ checks.push({
709
+ name: "Disk",
710
+ status: pct >= 90 ? "fail" : pct >= 75 ? "warn" : "pass",
711
+ detail: diskUsage
712
+ });
713
+ } else {
714
+ checks.push({ name: "Disk", status: "warn", detail: "unable to determine disk usage" });
686
715
  }
687
- delete cur[keys[keys.length - 1]];
716
+ const logrotateResult = await session.exec(
717
+ `test -f /etc/logrotate.d/openclaw && echo 'configured' || echo 'not configured'`,
718
+ signal
719
+ );
720
+ checks.push(
721
+ logrotateResult.stdout.trim() === "configured" ? { name: "Log rotation", status: "pass", detail: "configured" } : { name: "Log rotation", status: "warn", detail: "not configured \u2014 logs may grow unbounded" }
722
+ );
723
+ return checks;
688
724
  }
689
- function validateOpenclawConfig(cfg) {
690
- const issues = [];
691
- if ("version" in cfg) {
692
- issues.push(
693
- "Top-level 'version' is not a valid OpenClaw config key. Use 'meta.lastTouchedVersion' (string) instead."
694
- );
695
- }
696
- if ("channels" in cfg && Array.isArray(cfg["channels"])) {
697
- issues.push(
698
- `'channels' must be an object keyed by provider name (e.g. {"discord":{...}}), not an array.`
699
- );
700
- }
701
- const meta = cfg["meta"];
702
- if (meta !== void 0 && (typeof meta !== "object" || Array.isArray(meta) || meta === null)) {
703
- issues.push("'meta' must be an object.");
704
- } else if (meta && typeof meta === "object") {
705
- const ltv = meta["lastTouchedVersion"];
706
- if (ltv !== void 0 && typeof ltv !== "string") {
707
- issues.push("'meta.lastTouchedVersion' must be a string.");
708
- }
725
+ async function deployedVersionCheck(image, deployedVersion) {
726
+ const yaml = await import("./js-yaml-PTEEG4FO.js");
727
+ const { loadVersionSpec, checkVersion, isMovingTag } = await import("./versions-W5LRQCAJ.js");
728
+ const support = loadVersionSpec(yaml).support;
729
+ if (isMovingTag(deployedVersion)) {
730
+ return {
731
+ name: "Deployed",
732
+ status: "warn",
733
+ detail: `${image} (moving tag)`,
734
+ remedy: "pin an explicit version \u2014 a moving tag resolves to whatever is newest"
735
+ };
709
736
  }
710
- const gateway = cfg["gateway"];
711
- if (gateway !== void 0 && typeof gateway === "object" && !Array.isArray(gateway) && gateway !== null) {
712
- const gw = gateway;
713
- if ("port" in gw && typeof gw["port"] !== "number") {
714
- issues.push("'gateway.port' must be a number.");
715
- }
716
- const auth = gw["auth"];
717
- if (auth !== void 0 && typeof auth === "object" && !Array.isArray(auth) && auth !== null) {
718
- const mode = auth["mode"];
719
- if (mode !== void 0 && !VALID_AUTH_MODES.has(mode)) {
720
- issues.push(
721
- `'gateway.auth.mode' must be one of: ${[...VALID_AUTH_MODES].join(", ")}. Got: "${mode}".`
722
- );
737
+ const check = checkVersion(deployedVersion, support);
738
+ if (check.ok) return { name: "Deployed", status: "pass", detail: `OpenClaw ${deployedVersion}` };
739
+ return {
740
+ name: "Deployed",
741
+ status: "fail",
742
+ detail: `OpenClaw ${deployedVersion} is not supported by this clawops line`,
743
+ remedy: check.error.reason === "too-old" ? "this clawops line requires OpenClaw 2.0 or later \u2014 run `clawops migrate` to move an existing 1.x deployment across, which preserves its state and device identity" : "upgrade clawops, or pin an OpenClaw version this line supports"
744
+ };
745
+ }
746
+ async function hardeningChecks(conn, stack, signal) {
747
+ try {
748
+ const { MODULE_CATALOG, resolveModules, withRemoteExec } = await import("./harden-PCDK3UQ5.js");
749
+ const { buildContext } = await import("./context-KHKWJJMC.js");
750
+ const provider = buildContext({ stack }).adapter.name;
751
+ const modules = resolveModules(MODULE_CATALOG, void 0, provider);
752
+ const checks = [];
753
+ await withRemoteExec(conn, signal, async (exec) => {
754
+ for (const mod of modules) {
755
+ const result = await mod.check(exec);
756
+ if (result.status === "applied") {
757
+ checks.push({ name: mod.label, status: "pass", detail: "applied" });
758
+ } else if (result.status === "drifted") {
759
+ checks.push({ name: mod.label, status: "warn", detail: `drifted \u2014 ${result.detail}` });
760
+ } else if (result.status === "missing") {
761
+ checks.push({ name: mod.label, status: "info", detail: "not applied", remedy: "run `clawops harden`" });
762
+ }
723
763
  }
724
- }
764
+ });
765
+ return checks;
766
+ } catch (err) {
767
+ return [{ name: "Hardening checks", status: "fail", detail: messageOf(err) }];
725
768
  }
726
- return issues;
769
+ }
770
+ async function defaultOpenSession(stack, signal) {
771
+ const { getConfig } = await import("./store-SDUR52Z5.js");
772
+ const { buildContext } = await import("./context-KHKWJJMC.js");
773
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
774
+ const { acquireSession } = await import("./pool-EX4ZTA5P.js");
775
+ const config = getConfig();
776
+ if (!config) throw new Error("no config \u2014 run `clawops init`");
777
+ const ctx = buildContext({ stack });
778
+ const stackObj = await ctx.getStack();
779
+ const outputMap = await stackObj.outputs();
780
+ const outputs = Object.fromEntries(
781
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
782
+ );
783
+ const base = extractBaseOutputs(outputs);
784
+ const conn = ctx.adapter.getConnectionInfo({
785
+ ...base,
786
+ privateKeyPath: config.ssh.keyPath,
787
+ knownHostsPath: config.ssh.knownHostsPath
788
+ });
789
+ const { session, release } = await acquireSession({
790
+ host: conn.host,
791
+ port: conn.port,
792
+ user: conn.user,
793
+ privateKeyPath: conn.privateKeyPath,
794
+ knownHostsPath: conn.knownHostsPath,
795
+ signal
796
+ });
797
+ return { session, release, conn };
798
+ }
799
+ function messageOf(err) {
800
+ return err instanceof Error ? err.message : String(err);
727
801
  }
728
802
 
729
803
  export {
730
- resolveConn,
731
- errText,
732
- okText,
733
- handleConfigGet,
734
- handleConfigSet,
735
- handleConfigUnset,
736
- handleConfigValidate,
737
- validateOpenclawConfig,
804
+ runDiagnostics,
738
805
  formatUptime,
739
806
  gatherSnapshot,
740
807
  monitor_default