@clawops/cli 1.7.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +412 -70
  2. package/dist/{apply-F3KY2HTB.js → apply-AM5HERE7.js} +8 -6
  3. package/dist/{aws-L4RR2LSE.js → aws-AMON7DA7.js} +20 -9
  4. package/dist/{azure-7OXQENQL.js → azure-T4G6OBJQ.js} +19 -8
  5. package/dist/{bootstrap-HL4DNUNG.js → bootstrap-DMRPWJGN.js} +28 -19
  6. package/dist/bootstrap.sh.tmpl +51 -31
  7. package/dist/channels-KBUR42IU.js +53 -0
  8. package/dist/{chunk-DQDAWNGB.js → chunk-3WYA2QDM.js} +1 -1
  9. package/dist/chunk-6TIYVHPX.js +152 -0
  10. package/dist/{chunk-3XK66DKK.js → chunk-AHAAEXWC.js} +41 -0
  11. package/dist/{chunk-QQNKBTYZ.js → chunk-AX74VXTV.js} +292 -94
  12. package/dist/{chunk-C5X375RO.js → chunk-CKSPROD5.js} +59 -15
  13. package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
  14. package/dist/{chunk-FNS56UQY.js → chunk-GYV4KNW7.js} +319 -252
  15. package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
  16. package/dist/chunk-LT4XMO5D.js +50 -0
  17. package/dist/{chunk-P5LKWRPL.js → chunk-LU2V3LXA.js} +3 -3
  18. package/dist/chunk-N44K3LHE.js +140 -0
  19. package/dist/chunk-NUDIHZEH.js +55 -0
  20. package/dist/chunk-SBLQH2TJ.js +56 -0
  21. package/dist/chunk-UFRO4H5W.js +228 -0
  22. package/dist/{chunk-RAAONHW3.js → chunk-WHJMKDV7.js} +24 -4
  23. package/dist/chunk-YR3O2I3S.js +101 -0
  24. package/dist/cli.js +866 -577
  25. package/dist/config-validate-K2N6XLDX.js +76 -0
  26. package/dist/{context-5LCUG2QR.js → context-KHKWJJMC.js} +1 -1
  27. package/dist/{firewall-XQGO7HWK.js → firewall-T3NTPZJD.js} +10 -0
  28. package/dist/{gcp-UG6AWPIZ.js → gcp-2JBTD7ZM.js} +19 -8
  29. package/dist/{generate-ELD7QF5Z.js → generate-2XV2GSP2.js} +4 -4
  30. package/dist/{harden-URPXLSVF.js → harden-PCDK3UQ5.js} +48 -11
  31. package/dist/health-5CCOXYGG.js +11 -0
  32. package/dist/logs-P7YCKVF4.js +15 -0
  33. package/dist/{mcp-wire-ZMIVFAUC.js → mcp-wire-BFEY2Y6H.js} +23 -17
  34. package/dist/{package-QYZFN3FP.js → package-3HUW57XH.js} +3 -3
  35. package/dist/plugins-DTST3OER.js +49 -0
  36. package/dist/{pool-QGFQRMGU.js → pool-EX4ZTA5P.js} +2 -2
  37. package/dist/privileged-JFA6LLTV.js +17 -0
  38. package/dist/{remote-config-QDLN4UMG.js → remote-config-Z6IIZR3D.js} +3 -3
  39. package/dist/runtime-LZ7NA53V.js +42 -0
  40. package/dist/{server-FWTOSSZP.js → server-A5AB67GQ.js} +16 -12
  41. package/dist/spec-path-KQOOIQCA.js +9 -0
  42. package/dist/{ssh-CNJNLM3E.js → ssh-B3NZAU4I.js} +1 -1
  43. package/dist/upgrade-ULD4KFYC.js +88 -0
  44. package/dist/validate-BOQRWX4F.js +14 -0
  45. package/dist/{version-guard-VUCNZXQS.js → version-guard-GPSTCPT7.js} +3 -3
  46. package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
  47. package/package.json +3 -3
  48. package/spec/deploy-plan.schema.json +114 -25
  49. package/spec/integrations.yaml +135 -28
  50. package/spec/mcp-tools.yaml +68 -35
  51. package/spec/models.yaml +9 -0
  52. package/spec/openclaw-2.0.config.schema.json +1 -0
  53. package/spec/openclaw-versions.yaml +116 -27
  54. package/dist/chunk-35WVQY3X.js +0 -122
  55. package/dist/chunk-6X45DDGH.js +0 -42
  56. package/dist/chunk-SEZMVVAS.js +0 -156
  57. package/dist/validate-CFQ4FH2T.js +0 -10
@@ -1,40 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  applyPlan
4
- } from "./chunk-35WVQY3X.js";
4
+ } from "./chunk-UFRO4H5W.js";
5
+ import {
6
+ atomicWriteConfig,
7
+ readRemoteConfig,
8
+ restartGateway
9
+ } from "./chunk-N44K3LHE.js";
5
10
  import {
6
11
  extractBaseOutputs
7
12
  } from "./chunk-3QJBNAHW.js";
8
13
  import {
9
14
  generatePlan
10
- } from "./chunk-RAAONHW3.js";
15
+ } from "./chunk-WHJMKDV7.js";
11
16
  import {
12
17
  validatePlan
13
- } from "./chunk-6X45DDGH.js";
14
- import {
15
- errText,
16
- formatUptime,
17
- gatherSnapshot,
18
- handleConfigGet,
19
- handleConfigSet,
20
- handleConfigUnset,
21
- handleConfigValidate,
22
- okText,
23
- resolveConn
24
- } from "./chunk-FNS56UQY.js";
18
+ } from "./chunk-YR3O2I3S.js";
25
19
  import {
26
20
  acquireSession,
27
21
  drainPool
28
- } from "./chunk-DQDAWNGB.js";
29
- import {
30
- IMAGE_INSPECT_CMD,
31
- OPENCLAW_CONFIG,
32
- gatewayRunCommand,
33
- imageForRestart
34
- } from "./chunk-SEZMVVAS.js";
22
+ } from "./chunk-3WYA2QDM.js";
35
23
  import {
36
24
  buildContext
37
- } from "./chunk-P5LKWRPL.js";
25
+ } from "./chunk-LU2V3LXA.js";
26
+ import {
27
+ formatUptime,
28
+ gatherSnapshot,
29
+ runDiagnostics
30
+ } from "./chunk-GYV4KNW7.js";
31
+ import {
32
+ GATEWAY_LOGS_PROBE,
33
+ chooseLogSource,
34
+ containerLogsCommand,
35
+ gatewayLogsCommand
36
+ } from "./chunk-SBLQH2TJ.js";
37
+ import {
38
+ execPrivileged
39
+ } from "./chunk-NUDIHZEH.js";
38
40
  import {
39
41
  getConfig,
40
42
  getConfigDir
@@ -43,6 +45,12 @@ import {
43
45
  StateError,
44
46
  UsageError
45
47
  } from "./chunk-KGXPLI7W.js";
48
+ import {
49
+ IMAGE_INSPECT_CMD,
50
+ STATE_DIR_HOST_LINUX,
51
+ gatewayRunCommand,
52
+ imageForRestart
53
+ } from "./chunk-6TIYVHPX.js";
46
54
 
47
55
  // src/mcp/server.ts
48
56
  import { createServer } from "http";
@@ -141,6 +149,18 @@ var clawops_statusAnnotations = {
141
149
  openWorldHint: true,
142
150
  toolsets: ["cli", "read"]
143
151
  };
152
+ var clawops_doctorSchema = z.object({
153
+ stackName: z.string().optional(),
154
+ failuresOnly: z.boolean().optional().default(false)
155
+ });
156
+ var clawops_doctorAnnotations = {
157
+ title: "Run Diagnostics",
158
+ readOnlyHint: true,
159
+ destructiveHint: false,
160
+ idempotentHint: true,
161
+ openWorldHint: true,
162
+ toolsets: ["cli", "read"]
163
+ };
144
164
  var clawops_logs_tailSchema = z.object({
145
165
  stackName: z.string().optional(),
146
166
  tailLines: z.number().int().optional().default(100),
@@ -291,18 +311,6 @@ var clawops_config_validateAnnotations = {
291
311
  openWorldHint: true,
292
312
  toolsets: ["cli", "read"]
293
313
  };
294
- var clawops_agents_restartSchema = z.object({
295
- stackName: z.string().optional(),
296
- agentId: z.string()
297
- });
298
- var clawops_agents_restartAnnotations = {
299
- title: "Restart OpenClaw Agent",
300
- readOnlyHint: false,
301
- destructiveHint: true,
302
- idempotentHint: true,
303
- openWorldHint: true,
304
- toolsets: ["cli"]
305
- };
306
314
  var clawops_gateway_restartSchema = z.object({
307
315
  stackName: z.string().optional()
308
316
  });
@@ -353,6 +361,7 @@ var clawops_task_statusAnnotations = {
353
361
  var TOOLSETS = {
354
362
  cli: [
355
363
  "clawops_status",
364
+ "clawops_doctor",
356
365
  "clawops_logs_tail",
357
366
  "clawops_monitor",
358
367
  "clawops_config_get",
@@ -364,7 +373,6 @@ var TOOLSETS = {
364
373
  "clawops_config_set",
365
374
  "clawops_config_unset",
366
375
  "clawops_config_validate",
367
- "clawops_agents_restart",
368
376
  "clawops_gateway_restart",
369
377
  "clawops_task_status"
370
378
  ],
@@ -374,6 +382,7 @@ var TOOLSETS = {
374
382
  ],
375
383
  read: [
376
384
  "clawops_status",
385
+ "clawops_doctor",
377
386
  "clawops_logs_tail",
378
387
  "clawops_monitor",
379
388
  "clawops_stacks_list",
@@ -421,32 +430,102 @@ function text(t) {
421
430
  return { content: [{ type: "text", text: t }] };
422
431
  }
423
432
 
424
- // src/mcp/tools/cli/logs.ts
433
+ // src/mcp/tools/_conn.ts
434
+ async function resolveConn(ctx) {
435
+ if (ctx.adapter.name === "local") {
436
+ const state = ctx.localState;
437
+ if (!state) throw new StateError("Stack has no local state \u2014 run `clawops up` first.");
438
+ return {
439
+ host: state.sshHost,
440
+ port: state.sshPort,
441
+ user: state.sshUser,
442
+ privateKeyPath: state.privateKeyPath,
443
+ knownHostsPath: state.knownHostsPath
444
+ };
445
+ }
446
+ const { extractBaseOutputs: extractBaseOutputs2 } = await import("./outputs-DJHBY7EE.js");
447
+ const stack = await ctx.getStack();
448
+ const outputMap = await stack.outputs();
449
+ const outputs = Object.fromEntries(
450
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
451
+ );
452
+ if (!outputs["publicIp"]) {
453
+ throw new StateError("Stack has no outputs \u2014 run `clawops up` first.");
454
+ }
455
+ const base = extractBaseOutputs2(outputs);
456
+ return ctx.adapter.getConnectionInfo({
457
+ ...base,
458
+ privateKeyPath: ctx.config.ssh.keyPath,
459
+ knownHostsPath: ctx.config.ssh.knownHostsPath
460
+ });
461
+ }
462
+ function errText(message) {
463
+ return { content: [{ type: "text", text: message }], isError: true };
464
+ }
465
+ function okText(t) {
466
+ return { content: [{ type: "text", text: t }] };
467
+ }
468
+
469
+ // src/mcp/tools/_trim.ts
470
+ import { writeFileSync, mkdirSync as mkdirSync2 } from "fs";
471
+ import path2 from "path";
425
472
  var MAX_BYTES = 8 * 1024;
473
+ function trimForMcp(output, stackName) {
474
+ try {
475
+ const dir = path2.join(getConfigDir(), "state");
476
+ mkdirSync2(dir, { recursive: true });
477
+ writeFileSync(path2.join(dir, `${stackName}.last-run.json`), JSON.stringify({ output }), "utf-8");
478
+ } catch {
479
+ }
480
+ if (Buffer.byteLength(output) <= MAX_BYTES) {
481
+ return { content: output, truncated: false };
482
+ }
483
+ const trimmed = output.slice(0, MAX_BYTES);
484
+ return {
485
+ content: trimmed + `
486
+
487
+ [Output truncated at 8KB. Full output available at clawops://stacks/${stackName}/last-run]`,
488
+ truncated: true
489
+ };
490
+ }
491
+
492
+ // src/mcp/tools/cli/doctor.ts
493
+ async function handleDoctor(input, _server) {
494
+ const ac = new AbortController();
495
+ const report = await runDiagnostics({ stack: input.stackName, signal: ac.signal });
496
+ const payload = input.failuresOnly ? failuresOnly(report) : report;
497
+ const { content } = trimForMcp(JSON.stringify(payload, null, 2), input.stackName ?? "local");
498
+ return okText(content);
499
+ }
500
+ function failuresOnly(report) {
501
+ const sections = report.sections.map((s) => ({ ...s, checks: s.checks.filter((c) => c.status === "fail" || c.status === "warn") })).filter((s) => s.checks.length > 0);
502
+ return { sections, ok: report.ok, counts: report.counts };
503
+ }
504
+
505
+ // src/mcp/tools/cli/logs.ts
506
+ var MAX_BYTES2 = 8 * 1024;
426
507
  async function handleLogsTail(input, _server) {
427
508
  const ctx = buildContext({ stack: input.stackName });
428
509
  const conn = await resolveConn(ctx);
429
510
  const { session, release } = await acquireSession(conn);
430
511
  try {
431
512
  const tailLines = input.tailLines ?? 100;
432
- const sinceFlag = input.sinceMin ? `--since "${input.sinceMin} min ago"` : "";
433
- const command = [
434
- "journalctl -u openclaw",
435
- `-n ${tailLines}`,
436
- sinceFlag,
437
- "2>/dev/null",
438
- "|| docker logs openclaw",
439
- `-n ${tailLines}`
440
- ].filter(Boolean).join(" ");
441
- const result = await session.exec(command);
513
+ const since = input.sinceMin ? `${input.sinceMin}m` : void 0;
514
+ const probe = since ? { stdout: "" } : await execPrivileged(session, GATEWAY_LOGS_PROBE);
515
+ const choice = chooseLogSource({ since, gatewayReachable: probe.stdout.trim() === "ok" });
516
+ const opts = { tail: tailLines, follow: false, since, json: choice.source === "gateway" };
517
+ const command = choice.source === "gateway" ? gatewayLogsCommand(opts) : containerLogsCommand(opts);
518
+ const result = await execPrivileged(session, command);
442
519
  if (result.code !== 0 && !result.stdout) {
443
520
  return errText(`Failed to fetch logs: ${result.stderr}`);
444
521
  }
445
- let output = result.stdout;
446
- if (Buffer.byteLength(output) > MAX_BYTES) {
447
- output = output.slice(0, MAX_BYTES) + "\n\n[output truncated at 8KB]";
522
+ let output = `[source: ${choice.source} \u2014 ${choice.reason}]
523
+ ${result.stdout}`;
524
+ if (Buffer.byteLength(output) > MAX_BYTES2) {
525
+ output = output.slice(0, MAX_BYTES2) + "\n\n[output truncated at 8KB]";
448
526
  }
449
- return okText(output || "(no log output)");
527
+ return okText(result.stdout ? output : `[source: ${choice.source}]
528
+ (no log output)`);
450
529
  } finally {
451
530
  release();
452
531
  drainPool();
@@ -469,42 +548,182 @@ async function handleStacksList(_input, _server) {
469
548
  return { content: [{ type: "text", text: JSON.stringify({ stacks }, null, 2) }] };
470
549
  }
471
550
 
472
- // src/mcp/tools/cli/agents.ts
473
- async function handleAgentsList(input, _server) {
551
+ // src/mcp/tools/cli/config.ts
552
+ async function handleConfigGet(input, _server) {
553
+ const ac = new AbortController();
474
554
  const ctx = buildContext({ stack: input.stackName });
475
555
  const conn = await resolveConn(ctx);
476
556
  const { session, release } = await acquireSession(conn);
477
557
  try {
478
- const result = await session.exec('docker exec openclaw openclaw agents list --json 2>&1 || echo "[]"');
479
- return okText(result.stdout.trim());
558
+ let cfg;
559
+ try {
560
+ cfg = await readRemoteConfig(session, ac.signal);
561
+ } catch (err) {
562
+ return errText(err.message);
563
+ }
564
+ const value = input.key ? getPath(cfg, input.key) : cfg;
565
+ return okText(JSON.stringify(value, null, 2));
480
566
  } finally {
481
567
  release();
482
568
  drainPool();
483
569
  }
484
570
  }
485
- async function handleAgentsRestart(input, server) {
571
+ async function handleConfigSet(input, server) {
486
572
  const elicit = await server.server.elicitInput({
487
- message: `Restart agent "${input.agentId}" on stack "${input.stackName ?? "default"}"?`,
573
+ message: `Set ${input.key} = ${input.value} on stack "${input.stackName ?? "default"}"?`,
488
574
  requestedSchema: {
489
575
  type: "object",
490
- properties: { confirmed: { type: "boolean", title: "Confirm restart" } },
576
+ properties: { confirmed: { type: "boolean", title: "Confirm config change" } },
491
577
  required: ["confirmed"]
492
578
  }
493
579
  });
494
580
  if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
495
- return okText("Agent restart cancelled.");
581
+ return okText("Config change cancelled.");
496
582
  }
583
+ const ac = new AbortController();
497
584
  const ctx = buildContext({ stack: input.stackName });
498
585
  const conn = await resolveConn(ctx);
499
586
  const { session, release } = await acquireSession(conn);
500
587
  try {
501
- const result = await session.exec(
502
- `docker exec openclaw openclaw agents restart ${input.agentId} 2>&1`
503
- );
588
+ let cfg;
589
+ try {
590
+ cfg = await readRemoteConfig(session, ac.signal);
591
+ } catch (err) {
592
+ return errText(err.message);
593
+ }
594
+ let parsedValue = input.value;
595
+ try {
596
+ parsedValue = JSON.parse(input.value);
597
+ } catch {
598
+ }
599
+ setPath(cfg, input.key, parsedValue);
600
+ try {
601
+ await atomicWriteConfig(session, cfg, ac.signal);
602
+ } catch (err) {
603
+ return errText(`Failed to write config: ${err.message}`);
604
+ }
605
+ let note = "";
606
+ if (input.restart) {
607
+ try {
608
+ await restartGateway(session, ac.signal);
609
+ note = " (gateway restarted)";
610
+ } catch (err) {
611
+ return errText(`Gateway restart failed: ${err.message}`);
612
+ }
613
+ }
614
+ return okText(`Config set: ${input.key}${note}`);
615
+ } finally {
616
+ release();
617
+ drainPool();
618
+ }
619
+ }
620
+ async function handleConfigUnset(input, server) {
621
+ const elicit = await server.server.elicitInput({
622
+ message: `Remove config key "${input.key}" on stack "${input.stackName ?? "default"}"?`,
623
+ requestedSchema: {
624
+ type: "object",
625
+ properties: { confirmed: { type: "boolean", title: "Confirm key removal" } },
626
+ required: ["confirmed"]
627
+ }
628
+ });
629
+ if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
630
+ return okText("Config unset cancelled.");
631
+ }
632
+ const ac = new AbortController();
633
+ const ctx = buildContext({ stack: input.stackName });
634
+ const conn = await resolveConn(ctx);
635
+ const { session, release } = await acquireSession(conn);
636
+ try {
637
+ let cfg;
638
+ try {
639
+ cfg = await readRemoteConfig(session, ac.signal);
640
+ } catch (err) {
641
+ return errText(err.message);
642
+ }
643
+ deletePath(cfg, input.key);
644
+ try {
645
+ await atomicWriteConfig(session, cfg, ac.signal);
646
+ } catch (err) {
647
+ return errText(`Failed to write config: ${err.message}`);
648
+ }
649
+ let note = "";
650
+ if (input.restart) {
651
+ try {
652
+ await restartGateway(session, ac.signal);
653
+ note = " (gateway restarted)";
654
+ } catch (err) {
655
+ return errText(`Gateway restart failed: ${err.message}`);
656
+ }
657
+ }
658
+ return okText(`Config key removed: ${input.key}${note}`);
659
+ } finally {
660
+ release();
661
+ drainPool();
662
+ }
663
+ }
664
+ async function handleConfigValidate(input, _server) {
665
+ const ac = new AbortController();
666
+ const ctx = buildContext({ stack: input.stackName });
667
+ const conn = await resolveConn(ctx);
668
+ const { session, release } = await acquireSession(conn);
669
+ try {
670
+ let cfg;
671
+ try {
672
+ cfg = await readRemoteConfig(session, ac.signal);
673
+ } catch (err) {
674
+ return okText(JSON.stringify({ valid: false, issues: [err.message] }));
675
+ }
676
+ const { validateConfig } = await import("./config-validate-K2N6XLDX.js");
677
+ const yaml = await import("./js-yaml-PTEEG4FO.js");
678
+ const { loadVersionSpec } = await import("./versions-W5LRQCAJ.js");
679
+ const spec = loadVersionSpec(yaml);
680
+ const { errors, warnings } = await validateConfig(cfg, {
681
+ schemaCapturedFrom: spec.runtime?.configSchemaCapturedFrom
682
+ });
683
+ return okText(JSON.stringify({ valid: errors.length === 0, issues: errors, warnings }));
684
+ } finally {
685
+ release();
686
+ drainPool();
687
+ }
688
+ }
689
+ function getPath(obj, dotKey) {
690
+ return dotKey.split(".").reduce((cur, k) => {
691
+ if (cur !== null && typeof cur === "object") return cur[k];
692
+ return void 0;
693
+ }, obj);
694
+ }
695
+ function setPath(obj, dotKey, value) {
696
+ const keys = dotKey.split(".");
697
+ let cur = obj;
698
+ for (let i = 0; i < keys.length - 1; i++) {
699
+ const k = keys[i];
700
+ if (typeof cur[k] !== "object" || cur[k] === null) cur[k] = {};
701
+ cur = cur[k];
702
+ }
703
+ cur[keys[keys.length - 1]] = value;
704
+ }
705
+ function deletePath(obj, dotKey) {
706
+ const keys = dotKey.split(".");
707
+ let cur = obj;
708
+ for (let i = 0; i < keys.length - 1; i++) {
709
+ const k = keys[i];
710
+ if (typeof cur[k] !== "object" || cur[k] === null) return;
711
+ cur = cur[k];
712
+ }
713
+ delete cur[keys[keys.length - 1]];
714
+ }
715
+
716
+ // src/mcp/tools/cli/agents.ts
717
+ async function handleAgentsList(input, _server) {
718
+ const ctx = buildContext({ stack: input.stackName });
719
+ const conn = await resolveConn(ctx);
720
+ const { session, release } = await acquireSession(conn);
721
+ try {
722
+ const result = await execPrivileged(session, "docker exec openclaw openclaw agents list --json");
504
723
  if (result.code !== 0) {
505
- return errText(`Failed to restart agent: ${result.stderr || result.stdout}`);
724
+ return errText(`Cannot list agents: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
506
725
  }
507
- return okText(`Agent "${input.agentId}" restarted.`);
726
+ return okText(result.stdout.trim() || "[]");
508
727
  } finally {
509
728
  release();
510
729
  drainPool();
@@ -528,11 +747,12 @@ async function handleGatewayRestart(input, server) {
528
747
  const conn = await resolveConn(ctx);
529
748
  const { session, release } = await acquireSession(conn);
530
749
  try {
531
- const imgResult = await session.exec(IMAGE_INSPECT_CMD);
750
+ const imgResult = await execPrivileged(session, IMAGE_INSPECT_CMD);
532
751
  const image = imageForRestart(imgResult.stdout);
533
752
  if (!image.ok) return errText(image.error);
534
- const result = await session.exec(
535
- gatewayRunCommand({ image: image.value, configPath: OPENCLAW_CONFIG })
753
+ const result = await execPrivileged(
754
+ session,
755
+ gatewayRunCommand({ image: image.value, stateDir: STATE_DIR_HOST_LINUX })
536
756
  );
537
757
  if (result.code !== 0) {
538
758
  return errText(`Gateway restart failed: ${result.stderr}`);
@@ -578,29 +798,6 @@ function makeProgressEmitter(server, progressToken) {
578
798
  };
579
799
  }
580
800
 
581
- // src/mcp/tools/_trim.ts
582
- import { writeFileSync, mkdirSync as mkdirSync2 } from "fs";
583
- import path2 from "path";
584
- var MAX_BYTES2 = 8 * 1024;
585
- function trimForMcp(output, stackName) {
586
- try {
587
- const dir = path2.join(getConfigDir(), "state");
588
- mkdirSync2(dir, { recursive: true });
589
- writeFileSync(path2.join(dir, `${stackName}.last-run.json`), JSON.stringify({ output }), "utf-8");
590
- } catch {
591
- }
592
- if (Buffer.byteLength(output) <= MAX_BYTES2) {
593
- return { content: output, truncated: false };
594
- }
595
- const trimmed = output.slice(0, MAX_BYTES2);
596
- return {
597
- content: trimmed + `
598
-
599
- [Output truncated at 8KB. Full output available at clawops://stacks/${stackName}/last-run]`,
600
- truncated: true
601
- };
602
- }
603
-
604
801
  // src/mcp/tools/cli/up.ts
605
802
  var VALID_INSTANCE_TYPES = ["micro", "small", "medium", "large", "gpu"];
606
803
  async function handleUp(input, server) {
@@ -627,7 +824,7 @@ async function handleUp(input, server) {
627
824
  );
628
825
  }
629
826
  const { localOpts } = stackConfig;
630
- const { localBootstrap } = await import("./bootstrap-HL4DNUNG.js");
827
+ const { localBootstrap } = await import("./bootstrap-DMRPWJGN.js");
631
828
  const ac = new AbortController();
632
829
  const state = await localBootstrap({
633
830
  host: localOpts.host,
@@ -850,7 +1047,7 @@ async function handleTaskStatus(input, _server) {
850
1047
 
851
1048
  // src/mcp/tools/cli/monitor.ts
852
1049
  async function handleMonitor(input, _server) {
853
- const { acquireSession: acquireSession2, drainPool: drainPool2 } = await import("./pool-QGFQRMGU.js");
1050
+ const { acquireSession: acquireSession2, drainPool: drainPool2 } = await import("./pool-EX4ZTA5P.js");
854
1051
  const ctx = buildContext({ stack: input.stackName });
855
1052
  const tailLines = input.tailLines ?? 5;
856
1053
  let conn;
@@ -1015,6 +1212,7 @@ function makeEntry(schema, annotations, handler) {
1015
1212
  }
1016
1213
  var TOOL_REGISTRY = {
1017
1214
  clawops_status: makeEntry(clawops_statusSchema, clawops_statusAnnotations, handleStatus),
1215
+ clawops_doctor: makeEntry(clawops_doctorSchema, clawops_doctorAnnotations, handleDoctor),
1018
1216
  clawops_logs_tail: makeEntry(clawops_logs_tailSchema, clawops_logs_tailAnnotations, handleLogsTail),
1019
1217
  clawops_monitor: makeEntry(clawops_monitorSchema, clawops_monitorAnnotations, handleMonitor),
1020
1218
  clawops_stacks_list: makeEntry(clawops_stacks_listSchema, clawops_stacks_listAnnotations, handleStacksList),
@@ -1027,12 +1225,12 @@ var TOOL_REGISTRY = {
1027
1225
  clawops_config_set: makeEntry(clawops_config_setSchema, clawops_config_setAnnotations, handleConfigSet),
1028
1226
  clawops_config_unset: makeEntry(clawops_config_unsetSchema, clawops_config_unsetAnnotations, handleConfigUnset),
1029
1227
  clawops_config_validate: makeEntry(clawops_config_validateSchema, clawops_config_validateAnnotations, handleConfigValidate),
1030
- clawops_agents_restart: makeEntry(clawops_agents_restartSchema, clawops_agents_restartAnnotations, handleAgentsRestart),
1031
1228
  clawops_gateway_restart: makeEntry(clawops_gateway_restartSchema, clawops_gateway_restartAnnotations, handleGatewayRestart),
1032
1229
  clawops_workflow_deploy_app: makeEntry(clawops_workflow_deploy_appSchema, clawops_workflow_deploy_appAnnotations, handleWorkflowDeployApp),
1033
1230
  clawops_workflow_recover: makeEntry(clawops_workflow_recoverSchema, clawops_workflow_recoverAnnotations, handleWorkflowRecover),
1034
1231
  clawops_task_status: makeEntry(clawops_task_statusSchema, clawops_task_statusAnnotations, handleTaskStatus)
1035
1232
  };
1233
+ var TOOL_NAMES = Object.keys(TOOL_REGISTRY);
1036
1234
  function resolveEnabledTools(opts) {
1037
1235
  let names;
1038
1236
  if (opts.readOnly) {
@@ -1258,7 +1456,7 @@ clawops_gateway_restart({ stackName: "${s}" })
1258
1456
  ### Agents not running
1259
1457
  \`\`\`
1260
1458
  clawops_agents_list({ stackName: "${s}" })
1261
- clawops_agents_restart({ stackName: "${s}" })
1459
+ clawops_gateway_restart({ stackName: "${s}" })
1262
1460
  \`\`\`
1263
1461
 
1264
1462
  ### Bootstrap failed or incomplete
@@ -1325,7 +1523,7 @@ function bearerFrom(header) {
1325
1523
  return match?.[1]?.trim();
1326
1524
  }
1327
1525
  async function serveMcp(opts) {
1328
- const { version } = await import("./package-QYZFN3FP.js");
1526
+ const { version } = await import("./package-3HUW57XH.js");
1329
1527
  const makeServer = () => {
1330
1528
  const server = new McpServer({ name: "clawops", version });
1331
1529
  registerTools(server, opts);
@@ -1,8 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ CONFIG_FILENAME,
4
+ CONTAINER_UID,
5
+ GATEWAY_PORT,
6
+ STATE_DIR_HOST_LINUX,
7
+ gatewayRunCommand
8
+ } from "./chunk-6TIYVHPX.js";
2
9
 
3
10
  // src/providers/startup.ts
4
11
  function makeStartupScript(opts) {
5
- const { openclawVersion, os, bedrockEnabled = false } = opts;
12
+ const {
13
+ openclawVersion,
14
+ os,
15
+ bedrockEnabled = false,
16
+ publishGateway = "loopback",
17
+ gatewayPort = GATEWAY_PORT
18
+ } = opts;
6
19
  const dockerDistro = os === "ubuntu" ? "ubuntu" : "debian";
7
20
  const bedrockEnvBlock = bedrockEnabled ? makeBedrockEnvBlock() : "";
8
21
  return `#!/bin/bash
@@ -53,27 +66,58 @@ if [ ! -s "\${OPENCLAW_ENV_FILE}" ]; then
53
66
  chown clawops:clawops "\${OPENCLAW_ENV_FILE}"
54
67
  fi
55
68
 
69
+ # \u2500\u2500 State directory \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\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\u2500\u2500
70
+ # One directory holds config, the SQLite state and any plugins installed below.
71
+ # Owned NUMERICALLY by the uid the container runs as: useradd clawops gets 1001 on
72
+ # Ubuntu 24.04 (the ubuntu user already holds 1000), and a 1001-owned state dir makes the
73
+ # gateway exit 1 with EACCES on its own SQLite WAL. There is no degraded mode.
74
+ OPENCLAW_STATE_DIR=${STATE_DIR_HOST_LINUX}
75
+ OPENCLAW_CONFIG="\${OPENCLAW_STATE_DIR}/${CONFIG_FILENAME}"
76
+ mkdir -p "\${OPENCLAW_STATE_DIR}"
77
+
78
+ # Migrate a pre-2.0 deployment: the config used to be a bare file in the service user's
79
+ # home. Without this an in-place upgrade comes up with no configuration at all. Guarded on
80
+ # the target being absent, so re-running provisioning never clobbers a later edit.
81
+ OPENCLAW_LEGACY_CONFIG=/home/clawops/openclaw.json
82
+ if [ -f "\${OPENCLAW_LEGACY_CONFIG}" ] && [ ! -f "\${OPENCLAW_CONFIG}" ]; then
83
+ cp -p "\${OPENCLAW_LEGACY_CONFIG}" "\${OPENCLAW_CONFIG}"
84
+ mv "\${OPENCLAW_LEGACY_CONFIG}" "\${OPENCLAW_LEGACY_CONFIG}.migrated"
85
+ echo "clawops: migrated config from \${OPENCLAW_LEGACY_CONFIG}"
86
+ OPENCLAW_MIGRATED=1
87
+ fi
88
+
56
89
  # \u2500\u2500 Default config (apply.ts will overwrite with plan overlay) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
57
- OPENCLAW_CONFIG=/home/clawops/openclaw.json
58
90
  if [ ! -f "\${OPENCLAW_CONFIG}" ]; then
59
91
  cat > "\${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
60
- {"meta":{"lastTouchedVersion":"2026.4"},"gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":{}}
92
+ {"meta":{"lastTouchedVersion":"2026.9"},"gateway":{"mode":"local","port":${gatewayPort},"auth":{"mode":"token"}},"models":{},"channels":{}}
61
93
  OPENCLAWJSON
62
- chown clawops:clawops "\${OPENCLAW_CONFIG}"
94
+ fi
95
+
96
+ # Numeric, and last, so it covers the config and anything migrated above.
97
+ chown -R ${CONTAINER_UID}:${CONTAINER_UID} "\${OPENCLAW_STATE_DIR}"
98
+
99
+ # A 1.x config has no gateway.mode, and clawops no longer passes --allow-unconfigured \u2014
100
+ # so a migrated deployment would exit 78 with "existing config is missing gateway.mode".
101
+ # Normalised with OpenClaw's own tooling rather than by editing JSON in shell: it is
102
+ # schema-aware, writes atomically, and applies OpenClaw's internal config migrations at the
103
+ # same time. Runs after the chown so the container can write.
104
+ if [ "\${OPENCLAW_MIGRATED:-0}" = "1" ]; then
105
+ docker run --rm -v "\${OPENCLAW_STATE_DIR}":/home/node/.openclaw \\
106
+ ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION} \\
107
+ openclaw config set gateway.mode local || \\
108
+ echo "clawops: WARNING could not set gateway.mode; gateway may refuse to start" >&2
63
109
  fi
64
110
 
65
111
  # \u2500\u2500 Start OpenClaw container \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
66
- docker stop openclaw 2>/dev/null || true
67
- docker rm openclaw 2>/dev/null || true
68
- docker run -d \\
69
- --name openclaw \\
70
- --restart unless-stopped \\
71
- -p 18789:18789 \\
72
- -e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \\
73
- --env-file "\${OPENCLAW_ENV_FILE}" \\
74
- -v "\${OPENCLAW_CONFIG}":/app/config.json:ro \\
75
- ${bedrockEnvBlock} ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION} \\
76
- node openclaw.mjs gateway run --allow-unconfigured --port 18789
112
+ # Built by src/openclaw/runtime.ts, so this cannot drift from the restart paths.
113
+ ${gatewayRunCommand({
114
+ image: "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}",
115
+ stateDir: '"${OPENCLAW_STATE_DIR}"',
116
+ envFilePath: '"${OPENCLAW_ENV_FILE}"',
117
+ extraArgs: bedrockEnvBlock.trim(),
118
+ publish: publishGateway,
119
+ port: gatewayPort
120
+ })}
77
121
  `;
78
122
  }
79
123
  function makeBedrockEnvBlock() {
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ resolveSpecDir
4
+ } from "./chunk-JW5A7E3X.js";
2
5
  import {
3
6
  err,
4
7
  ok
5
8
  } from "./chunk-EI7CQQ7D.js";
6
- import {
7
- resolveSpecDir
8
- } from "./chunk-PVUVI35Y.js";
9
9
 
10
10
  // src/openclaw/versions.ts
11
11
  import { readFileSync } from "fs";
@@ -28,7 +28,8 @@ function loadVersionSpec(yaml) {
28
28
  min: raw.support.min,
29
29
  max: raw.support.max ?? "",
30
30
  recommended: raw.support.recommended ?? raw.support.min
31
- }
31
+ },
32
+ ...raw.runtime ? { runtime: raw.runtime } : {}
32
33
  };
33
34
  return _spec;
34
35
  } catch (e) {
@@ -77,9 +78,7 @@ function checkVersion(version, support) {
77
78
  reason: "too-new",
78
79
  supportedRange: describeRange(support),
79
80
  message: `OpenClaw ${version} is newer than this clawops release supports (${describeRange(support)}).
80
- OpenClaw 2026.8.1+ changed the container runtime contract: state moved to SQLite, config moved to a writable path, and provider plugins became install-gated.
81
- Use clawops 2.x for OpenClaw 2026.9.1 and later: npm install -g @clawops/cli@latest
82
- To stay on this line, pin an OpenClaw version at or below ${support.max}.`
81
+ Pin an OpenClaw version at or below ${support.max}, or upgrade clawops.`
83
82
  });
84
83
  }
85
84
  return ok(version);