@denial-web/clawguard 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,22 @@ npx @denial-web/clawguard hermes install ./candidate-skill --to ~/.hermes/skills
68
68
 
69
69
  The approval JSONL payload is designed for a bot or daemon to forward to WhatsApp, Telegram, Slack, Discord, or another owner channel before any files are copied into a trusted skill folder.
70
70
 
71
+ To prove the full approval loop locally without Telegram, WhatsApp, OpenClaw, or Hermes credentials, run:
72
+
73
+ ```bash
74
+ npx @denial-web/clawguard approvals demo-flow --keep
75
+ ```
76
+
77
+ The demo creates a harmless temporary skill, writes a pending approval, records a local owner approval, applies the decision, and installs the skill into a temporary trusted folder. Remove `--keep` when you want ClawGuard to clean up the temporary workspace automatically.
78
+
79
+ Check the approval setup and print the exact command flow:
80
+
81
+ ```bash
82
+ npx @denial-web/clawguard approvals doctor --chat-id 123456789
83
+ ```
84
+
85
+ Use `--framework hermes` to print Hermes install commands, or `--check-telegram` when you want ClawGuard to call Telegram `getMe` and verify the bot token.
86
+
71
87
  If OpenClaw already has messaging configured, ClawGuard can hand the approval message to OpenClaw:
72
88
 
73
89
  ```bash
@@ -78,6 +78,27 @@ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals watch ./.clawguard/approvals
78
78
 
79
79
  The watcher keeps search and discovery unrestricted. It only reacts after a guarded install writes a pending approval request. By default it records sent ids in `./.clawguard/approvals.jsonl.sent.json`, so restarting the bridge does not resend the same request. Use `--once --dry-run` for setup checks and CI smoke tests.
80
80
 
81
+ ### Local Approval Demo
82
+
83
+ For demos, onboarding, and smoke tests, users can prove the full approval loop without OpenClaw, Hermes, Telegram, WhatsApp, or Slack credentials:
84
+
85
+ ```bash
86
+ clawguard approvals demo-flow --keep
87
+ ```
88
+
89
+ The demo creates a harmless temporary skill, scans it with the governed policy, forces an approval request with `--approval-mode always`, writes a local `approve` decision, applies that decision, and copies the skill into a temporary trusted folder. By default it removes the temporary workspace after the run. Use `--keep` when recording a demo or inspecting the generated approval and decision logs.
90
+
91
+ ### Approval Doctor
92
+
93
+ Before wiring a real agent into the approval loop, users can run:
94
+
95
+ ```bash
96
+ clawguard approvals doctor \
97
+ --chat-id 123456789
98
+ ```
99
+
100
+ The doctor checks local runtime readiness, writable approval and decision paths, install destination writability, Telegram token presence, and Telegram chat id presence. It does not call Telegram by default. With `--check-telegram`, it calls Telegram `getMe` to verify the configured bot token. The command prints suggested guarded install, watcher, poller, and apply commands for OpenClaw by default; `--framework hermes` switches the guarded install example to Hermes.
101
+
81
102
  ### Approval Decisions
82
103
 
83
104
  Owner decisions are stored separately from approval requests. This keeps the original scan evidence immutable and gives messaging bridges a simple append-only target:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denial-web/clawguard",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Explainable security scanner for OpenClaw-style skills and MCP tool configs.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { promises as fs } from "node:fs";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { execFile } from "node:child_process";
6
+ import os from "node:os";
6
7
  import path from "node:path";
7
8
  import { promisify } from "node:util";
8
9
  import { loadConfig, mergeConfig, parseSize } from "./config.js";
@@ -41,7 +42,9 @@ if (![
41
42
  "approvals-watch",
42
43
  "approvals-decide",
43
44
  "approvals-poll-telegram",
44
- "approvals-apply"
45
+ "approvals-apply",
46
+ "approvals-doctor",
47
+ "approvals-demo-flow"
45
48
  ].includes(command)) {
46
49
  console.error(`Unknown command: ${command}`);
47
50
  printHelp();
@@ -106,6 +109,28 @@ try {
106
109
  process.exit(approvalApplyExitCode(result));
107
110
  }
108
111
 
112
+ if (command === "approvals-doctor") {
113
+ const doctorOptions = parseApprovalDoctorOptions(optionValues);
114
+ const result = await runApprovalDoctor(doctorOptions);
115
+ if (doctorOptions.json) {
116
+ console.log(JSON.stringify(result, null, 2));
117
+ } else {
118
+ printApprovalDoctorResult(result);
119
+ }
120
+ process.exit(result.ok ? 0 : 1);
121
+ }
122
+
123
+ if (command === "approvals-demo-flow") {
124
+ const demoOptions = parseApprovalDemoFlowOptions(optionValues);
125
+ const result = await runApprovalDemoFlow(demoOptions);
126
+ if (demoOptions.json) {
127
+ console.log(JSON.stringify(result, null, 2));
128
+ } else {
129
+ printApprovalDemoFlowResult(result);
130
+ }
131
+ process.exit(result.ok ? 0 : 1);
132
+ }
133
+
109
134
  const cliOptions = parseOptions(optionValues);
110
135
  cliOptions.framework = framework;
111
136
  const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
@@ -171,6 +196,8 @@ Usage:
171
196
  clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
172
197
  clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
173
198
  clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
199
+ clawguard approvals doctor [--chat-id <id>]
200
+ clawguard approvals demo-flow [--keep]
174
201
  clawguard scan-workspace <path> [--json] [--policy <preset>]
175
202
  npm run scan -- <path>
176
203
 
@@ -214,6 +241,10 @@ Options:
214
241
  --offset-state <path> Telegram update offset state file.
215
242
  --telegram-updates-file <path>
216
243
  Read Telegram updates from a JSON file for tests or offline replay.
244
+ --check-telegram In approvals doctor, call Telegram getMe to verify the bot token.
245
+ --framework <name> In approvals doctor, show openclaw or hermes commands. Default: openclaw.
246
+ In approvals demo-flow, label the demo as openclaw or hermes.
247
+ --keep In approvals demo-flow, keep the temporary demo workspace.
217
248
 
218
249
  Gate exit codes:
219
250
  0 = allow
@@ -232,6 +263,8 @@ Examples:
232
263
  npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
233
264
  npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
234
265
  npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <id> --decisions ./.clawguard/decisions.jsonl
266
+ npx @denial-web/clawguard approvals doctor --chat-id 123456789
267
+ npx @denial-web/clawguard approvals demo-flow --keep
235
268
  npm run scan -- examples/risky-skill
236
269
  npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
237
270
  npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
@@ -355,6 +388,22 @@ function parseCommand(values) {
355
388
  };
356
389
  }
357
390
 
391
+ if (rawCommand === "approvals" && values[1] === "doctor") {
392
+ return {
393
+ command: "approvals-doctor",
394
+ framework: undefined,
395
+ optionValues: values.slice(2)
396
+ };
397
+ }
398
+
399
+ if (rawCommand === "approvals" && values[1] === "demo-flow") {
400
+ return {
401
+ command: "approvals-demo-flow",
402
+ framework: undefined,
403
+ optionValues: values.slice(2)
404
+ };
405
+ }
406
+
358
407
  if (["openclaw", "hermes"].includes(rawCommand)) {
359
408
  const nestedCommand = values[1];
360
409
 
@@ -662,6 +711,357 @@ async function applyApprovalDecision(options) {
662
711
  return result;
663
712
  }
664
713
 
714
+ async function runApprovalDoctor(options) {
715
+ const approvalPath = path.resolve(options.approvalPath);
716
+ const decisionsPath = path.resolve(options.decisionsPath);
717
+ const installDir = path.resolve(options.installDir);
718
+ const target = options.target;
719
+ const token = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
720
+ const checks = [
721
+ checkNodeVersion(),
722
+ {
723
+ id: "approval-path-format",
724
+ status: approvalPath.endsWith(".jsonl") ? "pass" : "warn",
725
+ message: approvalPath.endsWith(".jsonl")
726
+ ? "Approval queue uses JSONL."
727
+ : "Approval queue is not .jsonl; JSONL is recommended for watcher integrations.",
728
+ detail: approvalPath
729
+ },
730
+ {
731
+ id: "telegram-token",
732
+ status: token ? "pass" : "warn",
733
+ message: token
734
+ ? "Telegram bot token is configured."
735
+ : "Telegram bot token is not configured. Set TELEGRAM_BOT_TOKEN or pass --bot-token.",
736
+ detail: token ? "present" : "missing"
737
+ },
738
+ {
739
+ id: "telegram-chat",
740
+ status: options.chatId ? "pass" : "warn",
741
+ message: options.chatId
742
+ ? "Telegram chat id is configured."
743
+ : "Telegram chat id is missing. Pass --chat-id before running the watcher.",
744
+ detail: options.chatId ?? "missing"
745
+ }
746
+ ];
747
+
748
+ checks.push(await checkWritablePath("approval-directory-writable", path.dirname(approvalPath)));
749
+ checks.push(await checkWritablePath("decision-directory-writable", path.dirname(decisionsPath)));
750
+ checks.push(await checkWritablePath("install-directory-writable", installDir));
751
+
752
+ if (options.checkTelegram) {
753
+ checks.push(await checkTelegramBot(token, options));
754
+ }
755
+
756
+ const commands = createApprovalDoctorCommands({
757
+ framework: options.framework,
758
+ target,
759
+ installDir,
760
+ approvalPath,
761
+ decisionsPath,
762
+ chatId: options.chatId ?? "<telegram-chat-id>"
763
+ });
764
+ const ok = checks.every((check) => check.status !== "fail");
765
+
766
+ return {
767
+ ok,
768
+ framework: options.framework,
769
+ paths: {
770
+ target,
771
+ installDir,
772
+ approvalPath,
773
+ decisionsPath
774
+ },
775
+ checks,
776
+ commands
777
+ };
778
+ }
779
+
780
+ async function runApprovalDemoFlow(options) {
781
+ const workspace = await fs.mkdtemp(path.join(os.tmpdir(), "clawguard-demo-flow-"));
782
+ const candidatePath = path.join(workspace, "candidate-skill");
783
+ const installDir = path.join(workspace, "trusted-skills");
784
+ const approvalPath = path.join(workspace, ".clawguard", "approvals.jsonl");
785
+ const decisionsPath = path.join(workspace, ".clawguard", "decisions.jsonl");
786
+ const steps = [];
787
+
788
+ await fs.mkdir(candidatePath, { recursive: true });
789
+ await fs.writeFile(path.join(candidatePath, "SKILL.md"), [
790
+ "# ClawGuard Demo Skill",
791
+ "",
792
+ "A harmless local skill used to prove the approval gate flow.",
793
+ "",
794
+ "It does not execute code, fetch network resources, or install dependencies.",
795
+ ""
796
+ ].join("\n"));
797
+ steps.push({
798
+ name: "create-demo-skill",
799
+ status: "pass",
800
+ detail: candidatePath
801
+ });
802
+
803
+ const scan = await scanTarget(candidatePath, {
804
+ policy: options.policy
805
+ });
806
+ steps.push({
807
+ name: "scan",
808
+ status: "pass",
809
+ detail: `${formatDecision(scan.policy.decision)} / ${scan.level.toUpperCase()} (${scan.score}/100)`
810
+ });
811
+
812
+ const install = await handleInstall(scan, {
813
+ target: candidatePath,
814
+ installDir,
815
+ installName: "demo-skill",
816
+ dryRun: false,
817
+ approvalOut: approvalPath,
818
+ approvalMode: "always",
819
+ framework: options.framework
820
+ });
821
+
822
+ if (!install.approvalRequest) {
823
+ throw new Error("Demo flow expected an approval request but none was created.");
824
+ }
825
+
826
+ steps.push({
827
+ name: "write-approval",
828
+ status: "pass",
829
+ detail: install.approvalRequest.id
830
+ });
831
+
832
+ const approval = await readApprovalRequest(approvalPath, install.approvalRequest.id);
833
+ const decisionResult = await decideApproval({
834
+ approvalPath,
835
+ id: approval.id,
836
+ decision: "approve",
837
+ outPath: decisionsPath,
838
+ actor: "clawguard-demo-flow",
839
+ reason: "Local demo approval.",
840
+ json: false
841
+ });
842
+ steps.push({
843
+ name: "record-owner-decision",
844
+ status: "pass",
845
+ detail: formatDecision(decisionResult.decision.decision)
846
+ });
847
+
848
+ const apply = await applyApprovalDecision({
849
+ approvalPath,
850
+ id: approval.id,
851
+ decisionsPath,
852
+ dryRun: false,
853
+ json: false
854
+ });
855
+ steps.push({
856
+ name: "apply-decision",
857
+ status: apply.installed ? "pass" : "fail",
858
+ detail: apply.reason
859
+ });
860
+
861
+ const installedSkillPath = path.join(install.destination, "SKILL.md");
862
+ const installedSkill = await fs.readFile(installedSkillPath, "utf8");
863
+ const result = {
864
+ ok: apply.installed && installedSkill.includes("ClawGuard Demo Skill"),
865
+ cleanedUp: false,
866
+ kept: options.keep,
867
+ framework: options.framework,
868
+ policy: options.policy,
869
+ workspace,
870
+ paths: {
871
+ candidate: candidatePath,
872
+ installDir,
873
+ destination: install.destination,
874
+ installedSkill: installedSkillPath,
875
+ approvalPath,
876
+ decisionsPath
877
+ },
878
+ scan: {
879
+ decision: scan.policy.decision,
880
+ risk: {
881
+ level: scan.level,
882
+ score: scan.score
883
+ },
884
+ findings: scan.findings.length
885
+ },
886
+ approval: {
887
+ id: approval.id,
888
+ status: approval.status,
889
+ decision: approval.decision
890
+ },
891
+ decision: {
892
+ id: decisionResult.decision.id,
893
+ decision: decisionResult.decision.decision,
894
+ status: decisionResult.decision.status,
895
+ actor: decisionResult.decision.actor
896
+ },
897
+ apply: {
898
+ installed: apply.installed,
899
+ skipped: apply.skipped,
900
+ reason: apply.reason
901
+ },
902
+ steps
903
+ };
904
+
905
+ if (!options.keep) {
906
+ try {
907
+ await fs.rm(workspace, { recursive: true, force: true });
908
+ result.cleanedUp = true;
909
+ steps.push({
910
+ name: "cleanup",
911
+ status: "pass",
912
+ detail: "Temporary workspace removed."
913
+ });
914
+ } catch (error) {
915
+ result.ok = false;
916
+ result.cleanupError = error.message;
917
+ steps.push({
918
+ name: "cleanup",
919
+ status: "fail",
920
+ detail: error.message
921
+ });
922
+ }
923
+ }
924
+
925
+ return result;
926
+ }
927
+
928
+ function checkNodeVersion() {
929
+ const major = Number.parseInt(process.versions.node.split(".")[0], 10);
930
+ return {
931
+ id: "node-version",
932
+ status: major >= 20 ? "pass" : "fail",
933
+ message: major >= 20
934
+ ? `Node.js ${process.versions.node} satisfies ClawGuard's runtime requirement.`
935
+ : `Node.js ${process.versions.node} is too old. ClawGuard requires Node.js 20 or newer.`,
936
+ detail: process.versions.node
937
+ };
938
+ }
939
+
940
+ async function checkWritablePath(id, directory) {
941
+ const resolved = path.resolve(directory);
942
+ const probePath = path.join(resolved, `.clawguard-doctor-${process.pid}.tmp`);
943
+
944
+ try {
945
+ await fs.mkdir(resolved, { recursive: true });
946
+ await fs.writeFile(probePath, "ok\n", { flag: "wx" });
947
+ await fs.unlink(probePath);
948
+ return {
949
+ id,
950
+ status: "pass",
951
+ message: "Directory is writable.",
952
+ detail: resolved
953
+ };
954
+ } catch (error) {
955
+ return {
956
+ id,
957
+ status: "fail",
958
+ message: `Directory is not writable: ${error.message}`,
959
+ detail: resolved
960
+ };
961
+ }
962
+ }
963
+
964
+ async function checkTelegramBot(botToken, options) {
965
+ if (!botToken) {
966
+ return {
967
+ id: "telegram-api",
968
+ status: "warn",
969
+ message: "Skipped Telegram API check because no bot token is configured.",
970
+ detail: "missing token"
971
+ };
972
+ }
973
+
974
+ const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
975
+ const endpoint = `${apiBase.replace(/\/$/, "")}/bot${botToken}/getMe`;
976
+
977
+ try {
978
+ const response = await fetch(endpoint);
979
+ const text = await response.text();
980
+ let payload;
981
+
982
+ try {
983
+ payload = text ? JSON.parse(text) : null;
984
+ } catch {
985
+ payload = undefined;
986
+ }
987
+
988
+ if (!response.ok || payload?.ok === false) {
989
+ return {
990
+ id: "telegram-api",
991
+ status: "fail",
992
+ message: `Telegram getMe failed with HTTP ${response.status}.`,
993
+ detail: redactTelegramToken(endpoint)
994
+ };
995
+ }
996
+
997
+ return {
998
+ id: "telegram-api",
999
+ status: "pass",
1000
+ message: "Telegram bot API responded successfully.",
1001
+ detail: payload?.result?.username ? `@${payload.result.username}` : redactTelegramToken(endpoint)
1002
+ };
1003
+ } catch (error) {
1004
+ return {
1005
+ id: "telegram-api",
1006
+ status: "fail",
1007
+ message: `Telegram getMe failed: ${error.message}`,
1008
+ detail: redactTelegramToken(endpoint)
1009
+ };
1010
+ }
1011
+ }
1012
+
1013
+ function createApprovalDoctorCommands(details) {
1014
+ const installArgs = [
1015
+ "npx",
1016
+ "@denial-web/clawguard",
1017
+ details.framework,
1018
+ "install",
1019
+ details.target,
1020
+ "--to",
1021
+ details.installDir,
1022
+ "--approval-out",
1023
+ details.approvalPath
1024
+ ];
1025
+ const watchArgs = [
1026
+ "npx",
1027
+ "@denial-web/clawguard",
1028
+ "approvals",
1029
+ "watch",
1030
+ details.approvalPath,
1031
+ "--via",
1032
+ "telegram",
1033
+ "--chat-id",
1034
+ details.chatId
1035
+ ];
1036
+ const pollArgs = [
1037
+ "npx",
1038
+ "@denial-web/clawguard",
1039
+ "approvals",
1040
+ "poll-telegram",
1041
+ details.approvalPath,
1042
+ "--decisions",
1043
+ details.decisionsPath
1044
+ ];
1045
+ const applyArgs = [
1046
+ "npx",
1047
+ "@denial-web/clawguard",
1048
+ "approvals",
1049
+ "apply",
1050
+ details.approvalPath,
1051
+ "--id",
1052
+ "<approval-id>",
1053
+ "--decisions",
1054
+ details.decisionsPath
1055
+ ];
1056
+
1057
+ return {
1058
+ guardedInstall: installArgs.map(shellQuote).join(" "),
1059
+ watchTelegram: `TELEGRAM_BOT_TOKEN=<token> ${watchArgs.map(shellQuote).join(" ")}`,
1060
+ pollTelegram: `TELEGRAM_BOT_TOKEN=<token> ${pollArgs.map(shellQuote).join(" ")}`,
1061
+ applyDecision: applyArgs.map(shellQuote).join(" ")
1062
+ };
1063
+ }
1064
+
665
1065
  async function readLatestApprovalDecision(decisionsPath, approvalId) {
666
1066
  let decisions;
667
1067
 
@@ -941,6 +1341,48 @@ function printApprovalApplyResult(result) {
941
1341
  console.log(`Reason: ${result.reason}`);
942
1342
  }
943
1343
 
1344
+ function printApprovalDoctorResult(result) {
1345
+ console.log("ClawGuard approvals doctor");
1346
+ console.log(`Framework: ${displayFramework(result.framework)}`);
1347
+ console.log(`Ready: ${result.ok ? "yes" : "no"}`);
1348
+ console.log("\nChecks:");
1349
+ for (const check of result.checks) {
1350
+ console.log(`- [${check.status.toUpperCase()}] ${check.message}`);
1351
+ if (check.detail) {
1352
+ console.log(` ${check.detail}`);
1353
+ }
1354
+ }
1355
+ console.log("\nSuggested commands:");
1356
+ console.log(`1. ${result.commands.guardedInstall}`);
1357
+ console.log(`2. ${result.commands.watchTelegram}`);
1358
+ console.log(`3. ${result.commands.pollTelegram}`);
1359
+ console.log(`4. ${result.commands.applyDecision}`);
1360
+ }
1361
+
1362
+ function printApprovalDemoFlowResult(result) {
1363
+ console.log("ClawGuard approvals demo-flow");
1364
+ console.log(`Framework: ${displayFramework(result.framework)}`);
1365
+ console.log(`Policy: ${result.policy}`);
1366
+ console.log(`Ready: ${result.ok ? "yes" : "no"}`);
1367
+ console.log(`Workspace: ${result.workspace}${result.cleanedUp ? " (cleaned up)" : ""}`);
1368
+ console.log(`Approval id: ${result.approval.id}`);
1369
+ console.log(`Scan: ${formatDecision(result.scan.decision)} / ${result.scan.risk.level.toUpperCase()} (${result.scan.risk.score}/100)`);
1370
+ console.log(`Decision: ${formatDecision(result.decision.decision)}`);
1371
+ console.log(`Installed: ${result.apply.installed ? "yes" : "no"}`);
1372
+
1373
+ console.log("\nSteps:");
1374
+ for (const step of result.steps) {
1375
+ console.log(`- [${step.status.toUpperCase()}] ${step.name}: ${step.detail}`);
1376
+ }
1377
+
1378
+ if (!result.cleanedUp) {
1379
+ console.log("\nArtifacts:");
1380
+ console.log(`Approval queue: ${result.paths.approvalPath}`);
1381
+ console.log(`Decision log: ${result.paths.decisionsPath}`);
1382
+ console.log(`Installed skill: ${result.paths.installedSkill}`);
1383
+ }
1384
+ }
1385
+
944
1386
  async function readApprovalRequest(approvalPath, id) {
945
1387
  const resolvedPath = path.resolve(approvalPath);
946
1388
  const approvals = await readApprovalRequests(resolvedPath);
@@ -1374,6 +1816,14 @@ function commandLabel(commandName) {
1374
1816
  return "Approval apply";
1375
1817
  }
1376
1818
 
1819
+ if (commandName === "approvals-doctor") {
1820
+ return "Approvals doctor";
1821
+ }
1822
+
1823
+ if (commandName === "approvals-demo-flow") {
1824
+ return "Approvals demo flow";
1825
+ }
1826
+
1377
1827
  if (commandName === "gate") {
1378
1828
  return "Gate";
1379
1829
  }
@@ -2094,6 +2544,146 @@ function parseApprovalApplyOptions(values) {
2094
2544
  return options;
2095
2545
  }
2096
2546
 
2547
+ function parseApprovalDoctorOptions(values) {
2548
+ const options = {
2549
+ approvalPath: ".clawguard/approvals.jsonl",
2550
+ decisionsPath: ".clawguard/decisions.jsonl",
2551
+ installDir: ".agents/skills",
2552
+ target: "./candidate-skill",
2553
+ framework: "openclaw",
2554
+ chatId: undefined,
2555
+ botToken: undefined,
2556
+ telegramApiBase: undefined,
2557
+ checkTelegram: false,
2558
+ json: false
2559
+ };
2560
+
2561
+ for (let index = 0; index < values.length; index += 1) {
2562
+ const value = values[index];
2563
+
2564
+ if (value === "--json") {
2565
+ options.json = true;
2566
+ continue;
2567
+ }
2568
+
2569
+ if (value === "--approval-out") {
2570
+ options.approvalPath = requireNextValue(values, index, "--approval-out");
2571
+ index += 1;
2572
+ continue;
2573
+ }
2574
+
2575
+ if (value === "--decisions") {
2576
+ options.decisionsPath = requireNextValue(values, index, "--decisions");
2577
+ index += 1;
2578
+ continue;
2579
+ }
2580
+
2581
+ if (value === "--to") {
2582
+ options.installDir = requireNextValue(values, index, "--to");
2583
+ index += 1;
2584
+ continue;
2585
+ }
2586
+
2587
+ if (value === "--target") {
2588
+ options.target = requireNextValue(values, index, "--target");
2589
+ index += 1;
2590
+ continue;
2591
+ }
2592
+
2593
+ if (value === "--framework") {
2594
+ options.framework = requireNextValue(values, index, "--framework");
2595
+ index += 1;
2596
+ continue;
2597
+ }
2598
+
2599
+ if (value === "--chat-id") {
2600
+ options.chatId = requireNextValue(values, index, "--chat-id");
2601
+ index += 1;
2602
+ continue;
2603
+ }
2604
+
2605
+ if (value === "--bot-token") {
2606
+ options.botToken = requireNextValue(values, index, "--bot-token");
2607
+ index += 1;
2608
+ continue;
2609
+ }
2610
+
2611
+ if (value === "--telegram-api-base") {
2612
+ options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
2613
+ index += 1;
2614
+ continue;
2615
+ }
2616
+
2617
+ if (value === "--check-telegram") {
2618
+ options.checkTelegram = true;
2619
+ continue;
2620
+ }
2621
+
2622
+ if (value.startsWith("--")) {
2623
+ throw new Error(`Unknown option: ${value}`);
2624
+ }
2625
+
2626
+ throw new Error(`Unexpected argument for approvals doctor: ${value}`);
2627
+ }
2628
+
2629
+ if (!["openclaw", "hermes"].includes(options.framework)) {
2630
+ throw new Error("Invalid --framework value. Use one of: openclaw, hermes");
2631
+ }
2632
+
2633
+ return options;
2634
+ }
2635
+
2636
+ function parseApprovalDemoFlowOptions(values) {
2637
+ const options = {
2638
+ framework: "openclaw",
2639
+ policy: "governed",
2640
+ keep: false,
2641
+ json: false
2642
+ };
2643
+
2644
+ for (let index = 0; index < values.length; index += 1) {
2645
+ const value = values[index];
2646
+
2647
+ if (value === "--json") {
2648
+ options.json = true;
2649
+ continue;
2650
+ }
2651
+
2652
+ if (value === "--keep") {
2653
+ options.keep = true;
2654
+ continue;
2655
+ }
2656
+
2657
+ if (value === "--framework") {
2658
+ options.framework = requireNextValue(values, index, "--framework");
2659
+ index += 1;
2660
+ continue;
2661
+ }
2662
+
2663
+ if (value === "--policy") {
2664
+ options.policy = requireNextValue(values, index, "--policy");
2665
+ index += 1;
2666
+ continue;
2667
+ }
2668
+
2669
+ if (value.startsWith("--")) {
2670
+ throw new Error(`Unknown option: ${value}`);
2671
+ }
2672
+
2673
+ throw new Error(`Unexpected argument for approvals demo-flow: ${value}`);
2674
+ }
2675
+
2676
+ if (!["openclaw", "hermes"].includes(options.framework)) {
2677
+ throw new Error("Invalid --framework value. Use one of: openclaw, hermes");
2678
+ }
2679
+
2680
+ if (!policyPresets.includes(options.policy)) {
2681
+ throw new Error(`Invalid --policy value. Use one of: ${policyPresets.join(", ")}`);
2682
+ }
2683
+
2684
+ return options;
2685
+ }
2686
+
2097
2687
  async function writeReportFile(outputPath, content) {
2098
2688
  const resolvedPath = path.resolve(outputPath);
2099
2689
  await fs.mkdir(path.dirname(resolvedPath), { recursive: true });