@denial-web/clawguard 0.1.10 → 0.1.12

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,14 @@ 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
+ Check the approval setup and print the exact command flow:
72
+
73
+ ```bash
74
+ npx @denial-web/clawguard approvals doctor --chat-id 123456789
75
+ ```
76
+
77
+ Use `--framework hermes` to print Hermes install commands, or `--check-telegram` when you want ClawGuard to call Telegram `getMe` and verify the bot token.
78
+
71
79
  If OpenClaw already has messaging configured, ClawGuard can hand the approval message to OpenClaw:
72
80
 
73
81
  ```bash
@@ -114,6 +122,14 @@ TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals poll-telegra
114
122
 
115
123
  The poller records the Telegram update offset in `./.clawguard/decisions.jsonl.telegram-state.json` by default so the same reply is not processed again.
116
124
 
125
+ Apply the recorded decision to continue or block the pending install:
126
+
127
+ ```bash
128
+ npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <approval-id> --decisions ./.clawguard/decisions.jsonl
129
+ ```
130
+
131
+ If the latest decision is `approve`, ClawGuard copies the original scanned source to the original approved destination. If the latest decision is `deny`, it exits blocked without copying. If no decision exists yet, it stays paused.
132
+
117
133
  When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
118
134
 
119
135
  ```bash
@@ -78,6 +78,17 @@ 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
+ ### Approval Doctor
82
+
83
+ Before wiring a real agent into the approval loop, users can run:
84
+
85
+ ```bash
86
+ clawguard approvals doctor \
87
+ --chat-id 123456789
88
+ ```
89
+
90
+ 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.
91
+
81
92
  ### Approval Decisions
82
93
 
83
94
  Owner decisions are stored separately from approval requests. This keeps the original scan evidence immutable and gives messaging bridges a simple append-only target:
@@ -111,6 +122,24 @@ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals poll-telegram ./.clawguard/a
111
122
 
112
123
  The poller calls Telegram `getUpdates`, parses approval commands, looks up the referenced approval request, and appends a `clawguard.decision.v1` decision. It records the next Telegram update offset in `./.clawguard/decisions.jsonl.telegram-state.json` by default. For tests and offline replay, `--telegram-updates-file <path>` can read a captured Telegram-style update payload instead of calling the Telegram API.
113
124
 
125
+ ### Approval Apply
126
+
127
+ After a decision is recorded, ClawGuard can apply that decision to the original pending install:
128
+
129
+ ```bash
130
+ clawguard approvals apply ./.clawguard/approvals.jsonl \
131
+ --id <approval-id> \
132
+ --decisions ./.clawguard/decisions.jsonl
133
+ ```
134
+
135
+ Apply behavior:
136
+
137
+ - If the latest matching decision is `approve`, copy the original scanned `target` to the original approval `destination`.
138
+ - If the latest matching decision is `deny`, exit blocked and copy nothing.
139
+ - If no matching decision exists, exit paused and copy nothing.
140
+ - Never execute the skill or install dependencies during apply.
141
+ - Refuse symlinked install sources and existing destinations, matching normal install safety.
142
+
114
143
  ### Skill Folder Scan
115
144
 
116
145
  Command:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denial-web/clawguard",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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
@@ -40,7 +40,9 @@ if (![
40
40
  "approvals-send",
41
41
  "approvals-watch",
42
42
  "approvals-decide",
43
- "approvals-poll-telegram"
43
+ "approvals-poll-telegram",
44
+ "approvals-apply",
45
+ "approvals-doctor"
44
46
  ].includes(command)) {
45
47
  console.error(`Unknown command: ${command}`);
46
48
  printHelp();
@@ -94,6 +96,28 @@ try {
94
96
  process.exit(0);
95
97
  }
96
98
 
99
+ if (command === "approvals-apply") {
100
+ const applyOptions = parseApprovalApplyOptions(optionValues);
101
+ const result = await applyApprovalDecision(applyOptions);
102
+ if (applyOptions.json) {
103
+ console.log(JSON.stringify(result, null, 2));
104
+ } else {
105
+ printApprovalApplyResult(result);
106
+ }
107
+ process.exit(approvalApplyExitCode(result));
108
+ }
109
+
110
+ if (command === "approvals-doctor") {
111
+ const doctorOptions = parseApprovalDoctorOptions(optionValues);
112
+ const result = await runApprovalDoctor(doctorOptions);
113
+ if (doctorOptions.json) {
114
+ console.log(JSON.stringify(result, null, 2));
115
+ } else {
116
+ printApprovalDoctorResult(result);
117
+ }
118
+ process.exit(result.ok ? 0 : 1);
119
+ }
120
+
97
121
  const cliOptions = parseOptions(optionValues);
98
122
  cliOptions.framework = framework;
99
123
  const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
@@ -158,6 +182,8 @@ Usage:
158
182
  clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
159
183
  clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
160
184
  clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
185
+ clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
186
+ clawguard approvals doctor [--chat-id <id>]
161
187
  clawguard scan-workspace <path> [--json] [--policy <preset>]
162
188
  npm run scan -- <path>
163
189
 
@@ -201,6 +227,8 @@ Options:
201
227
  --offset-state <path> Telegram update offset state file.
202
228
  --telegram-updates-file <path>
203
229
  Read Telegram updates from a JSON file for tests or offline replay.
230
+ --check-telegram In approvals doctor, call Telegram getMe to verify the bot token.
231
+ --framework <name> In approvals doctor, show openclaw or hermes commands. Default: openclaw.
204
232
 
205
233
  Gate exit codes:
206
234
  0 = allow
@@ -218,6 +246,8 @@ Examples:
218
246
  npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
219
247
  npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
220
248
  npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
249
+ npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <id> --decisions ./.clawguard/decisions.jsonl
250
+ npx @denial-web/clawguard approvals doctor --chat-id 123456789
221
251
  npm run scan -- examples/risky-skill
222
252
  npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
223
253
  npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
@@ -333,6 +363,22 @@ function parseCommand(values) {
333
363
  };
334
364
  }
335
365
 
366
+ if (rawCommand === "approvals" && values[1] === "apply") {
367
+ return {
368
+ command: "approvals-apply",
369
+ framework: undefined,
370
+ optionValues: values.slice(2)
371
+ };
372
+ }
373
+
374
+ if (rawCommand === "approvals" && values[1] === "doctor") {
375
+ return {
376
+ command: "approvals-doctor",
377
+ framework: undefined,
378
+ optionValues: values.slice(2)
379
+ };
380
+ }
381
+
336
382
  if (["openclaw", "hermes"].includes(rawCommand)) {
337
383
  const nestedCommand = values[1];
338
384
 
@@ -578,6 +624,300 @@ async function pollTelegramApprovals(options) {
578
624
  return result;
579
625
  }
580
626
 
627
+ async function applyApprovalDecision(options) {
628
+ const approval = await readApprovalRequest(options.approvalPath, options.id);
629
+ const decisionsPath = path.resolve(options.decisionsPath ?? `${options.approvalPath}.decisions.jsonl`);
630
+ const decision = await readLatestApprovalDecision(decisionsPath, approval.id);
631
+ const result = {
632
+ approval: {
633
+ id: approval.id,
634
+ status: approval.status,
635
+ decision: approval.decision,
636
+ risk: approval.risk,
637
+ framework: approval.framework
638
+ },
639
+ decision,
640
+ decisionsPath,
641
+ source: approval.target ? path.resolve(approval.target) : undefined,
642
+ destination: approval.destination ? path.resolve(approval.destination) : undefined,
643
+ dryRun: options.dryRun,
644
+ installed: false,
645
+ skipped: true,
646
+ reason: undefined
647
+ };
648
+
649
+ if (!decision) {
650
+ result.reason = "No decision has been recorded for this approval.";
651
+ return result;
652
+ }
653
+
654
+ if (decision.decision !== "approve") {
655
+ result.reason = decision.reason ?? "Approval was denied.";
656
+ return result;
657
+ }
658
+
659
+ if (!result.source) {
660
+ throw new Error("Approval request has no target path to install.");
661
+ }
662
+
663
+ if (!result.destination) {
664
+ throw new Error("Approval request has no destination path to install.");
665
+ }
666
+
667
+ if (options.dryRun) {
668
+ result.skipped = false;
669
+ result.reason = "Dry run passed; no files were copied.";
670
+ return result;
671
+ }
672
+
673
+ await assertInstallableSource(result.source);
674
+ await assertDestinationAvailable(result.destination);
675
+ await fs.mkdir(path.dirname(result.destination), { recursive: true });
676
+ await fs.cp(result.source, result.destination, {
677
+ recursive: true,
678
+ errorOnExist: true,
679
+ force: false,
680
+ verbatimSymlinks: true
681
+ });
682
+
683
+ result.installed = true;
684
+ result.skipped = false;
685
+ result.reason = "Copied after recorded approval.";
686
+ return result;
687
+ }
688
+
689
+ async function runApprovalDoctor(options) {
690
+ const approvalPath = path.resolve(options.approvalPath);
691
+ const decisionsPath = path.resolve(options.decisionsPath);
692
+ const installDir = path.resolve(options.installDir);
693
+ const target = options.target;
694
+ const token = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
695
+ const checks = [
696
+ checkNodeVersion(),
697
+ {
698
+ id: "approval-path-format",
699
+ status: approvalPath.endsWith(".jsonl") ? "pass" : "warn",
700
+ message: approvalPath.endsWith(".jsonl")
701
+ ? "Approval queue uses JSONL."
702
+ : "Approval queue is not .jsonl; JSONL is recommended for watcher integrations.",
703
+ detail: approvalPath
704
+ },
705
+ {
706
+ id: "telegram-token",
707
+ status: token ? "pass" : "warn",
708
+ message: token
709
+ ? "Telegram bot token is configured."
710
+ : "Telegram bot token is not configured. Set TELEGRAM_BOT_TOKEN or pass --bot-token.",
711
+ detail: token ? "present" : "missing"
712
+ },
713
+ {
714
+ id: "telegram-chat",
715
+ status: options.chatId ? "pass" : "warn",
716
+ message: options.chatId
717
+ ? "Telegram chat id is configured."
718
+ : "Telegram chat id is missing. Pass --chat-id before running the watcher.",
719
+ detail: options.chatId ?? "missing"
720
+ }
721
+ ];
722
+
723
+ checks.push(await checkWritablePath("approval-directory-writable", path.dirname(approvalPath)));
724
+ checks.push(await checkWritablePath("decision-directory-writable", path.dirname(decisionsPath)));
725
+ checks.push(await checkWritablePath("install-directory-writable", installDir));
726
+
727
+ if (options.checkTelegram) {
728
+ checks.push(await checkTelegramBot(token, options));
729
+ }
730
+
731
+ const commands = createApprovalDoctorCommands({
732
+ framework: options.framework,
733
+ target,
734
+ installDir,
735
+ approvalPath,
736
+ decisionsPath,
737
+ chatId: options.chatId ?? "<telegram-chat-id>"
738
+ });
739
+ const ok = checks.every((check) => check.status !== "fail");
740
+
741
+ return {
742
+ ok,
743
+ framework: options.framework,
744
+ paths: {
745
+ target,
746
+ installDir,
747
+ approvalPath,
748
+ decisionsPath
749
+ },
750
+ checks,
751
+ commands
752
+ };
753
+ }
754
+
755
+ function checkNodeVersion() {
756
+ const major = Number.parseInt(process.versions.node.split(".")[0], 10);
757
+ return {
758
+ id: "node-version",
759
+ status: major >= 20 ? "pass" : "fail",
760
+ message: major >= 20
761
+ ? `Node.js ${process.versions.node} satisfies ClawGuard's runtime requirement.`
762
+ : `Node.js ${process.versions.node} is too old. ClawGuard requires Node.js 20 or newer.`,
763
+ detail: process.versions.node
764
+ };
765
+ }
766
+
767
+ async function checkWritablePath(id, directory) {
768
+ const resolved = path.resolve(directory);
769
+ const probePath = path.join(resolved, `.clawguard-doctor-${process.pid}.tmp`);
770
+
771
+ try {
772
+ await fs.mkdir(resolved, { recursive: true });
773
+ await fs.writeFile(probePath, "ok\n", { flag: "wx" });
774
+ await fs.unlink(probePath);
775
+ return {
776
+ id,
777
+ status: "pass",
778
+ message: "Directory is writable.",
779
+ detail: resolved
780
+ };
781
+ } catch (error) {
782
+ return {
783
+ id,
784
+ status: "fail",
785
+ message: `Directory is not writable: ${error.message}`,
786
+ detail: resolved
787
+ };
788
+ }
789
+ }
790
+
791
+ async function checkTelegramBot(botToken, options) {
792
+ if (!botToken) {
793
+ return {
794
+ id: "telegram-api",
795
+ status: "warn",
796
+ message: "Skipped Telegram API check because no bot token is configured.",
797
+ detail: "missing token"
798
+ };
799
+ }
800
+
801
+ const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
802
+ const endpoint = `${apiBase.replace(/\/$/, "")}/bot${botToken}/getMe`;
803
+
804
+ try {
805
+ const response = await fetch(endpoint);
806
+ const text = await response.text();
807
+ let payload;
808
+
809
+ try {
810
+ payload = text ? JSON.parse(text) : null;
811
+ } catch {
812
+ payload = undefined;
813
+ }
814
+
815
+ if (!response.ok || payload?.ok === false) {
816
+ return {
817
+ id: "telegram-api",
818
+ status: "fail",
819
+ message: `Telegram getMe failed with HTTP ${response.status}.`,
820
+ detail: redactTelegramToken(endpoint)
821
+ };
822
+ }
823
+
824
+ return {
825
+ id: "telegram-api",
826
+ status: "pass",
827
+ message: "Telegram bot API responded successfully.",
828
+ detail: payload?.result?.username ? `@${payload.result.username}` : redactTelegramToken(endpoint)
829
+ };
830
+ } catch (error) {
831
+ return {
832
+ id: "telegram-api",
833
+ status: "fail",
834
+ message: `Telegram getMe failed: ${error.message}`,
835
+ detail: redactTelegramToken(endpoint)
836
+ };
837
+ }
838
+ }
839
+
840
+ function createApprovalDoctorCommands(details) {
841
+ const installArgs = [
842
+ "npx",
843
+ "@denial-web/clawguard",
844
+ details.framework,
845
+ "install",
846
+ details.target,
847
+ "--to",
848
+ details.installDir,
849
+ "--approval-out",
850
+ details.approvalPath
851
+ ];
852
+ const watchArgs = [
853
+ "npx",
854
+ "@denial-web/clawguard",
855
+ "approvals",
856
+ "watch",
857
+ details.approvalPath,
858
+ "--via",
859
+ "telegram",
860
+ "--chat-id",
861
+ details.chatId
862
+ ];
863
+ const pollArgs = [
864
+ "npx",
865
+ "@denial-web/clawguard",
866
+ "approvals",
867
+ "poll-telegram",
868
+ details.approvalPath,
869
+ "--decisions",
870
+ details.decisionsPath
871
+ ];
872
+ const applyArgs = [
873
+ "npx",
874
+ "@denial-web/clawguard",
875
+ "approvals",
876
+ "apply",
877
+ details.approvalPath,
878
+ "--id",
879
+ "<approval-id>",
880
+ "--decisions",
881
+ details.decisionsPath
882
+ ];
883
+
884
+ return {
885
+ guardedInstall: installArgs.map(shellQuote).join(" "),
886
+ watchTelegram: `TELEGRAM_BOT_TOKEN=<token> ${watchArgs.map(shellQuote).join(" ")}`,
887
+ pollTelegram: `TELEGRAM_BOT_TOKEN=<token> ${pollArgs.map(shellQuote).join(" ")}`,
888
+ applyDecision: applyArgs.map(shellQuote).join(" ")
889
+ };
890
+ }
891
+
892
+ async function readLatestApprovalDecision(decisionsPath, approvalId) {
893
+ let decisions;
894
+
895
+ try {
896
+ decisions = await readApprovalDecisions(decisionsPath);
897
+ } catch (error) {
898
+ if (error.code === "ENOENT") {
899
+ return undefined;
900
+ }
901
+
902
+ throw error;
903
+ }
904
+
905
+ return decisions.filter((decision) => decision.approvalId === approvalId).at(-1);
906
+ }
907
+
908
+ async function readApprovalDecisions(decisionsPath) {
909
+ const content = await fs.readFile(decisionsPath, "utf8");
910
+ const decisions = content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
911
+
912
+ for (const decision of decisions) {
913
+ if (decision.schemaVersion !== "clawguard.decision.v1") {
914
+ throw new Error("Unsupported approval decision schema.");
915
+ }
916
+ }
917
+
918
+ return decisions;
919
+ }
920
+
581
921
  async function fetchTelegramUpdates(options, offset) {
582
922
  if (options.telegramUpdatesPath) {
583
923
  const content = await fs.readFile(path.resolve(options.telegramUpdatesPath), "utf8");
@@ -817,6 +1157,35 @@ function printApprovalTelegramPollResult(result) {
817
1157
  }
818
1158
  }
819
1159
 
1160
+ function printApprovalApplyResult(result) {
1161
+ console.log(`ClawGuard approval apply: ${result.approval.id}`);
1162
+ console.log(`Decision: ${result.decision ? formatDecision(result.decision.decision) : "PENDING"}`);
1163
+ console.log(`Source: ${result.source ?? "not recorded"}`);
1164
+ console.log(`Destination: ${result.destination ?? "not recorded"}`);
1165
+ console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
1166
+ console.log(`Installed: ${result.installed ? "yes" : "no"}`);
1167
+ console.log(`Exit code: ${approvalApplyExitCode(result)}`);
1168
+ console.log(`Reason: ${result.reason}`);
1169
+ }
1170
+
1171
+ function printApprovalDoctorResult(result) {
1172
+ console.log("ClawGuard approvals doctor");
1173
+ console.log(`Framework: ${displayFramework(result.framework)}`);
1174
+ console.log(`Ready: ${result.ok ? "yes" : "no"}`);
1175
+ console.log("\nChecks:");
1176
+ for (const check of result.checks) {
1177
+ console.log(`- [${check.status.toUpperCase()}] ${check.message}`);
1178
+ if (check.detail) {
1179
+ console.log(` ${check.detail}`);
1180
+ }
1181
+ }
1182
+ console.log("\nSuggested commands:");
1183
+ console.log(`1. ${result.commands.guardedInstall}`);
1184
+ console.log(`2. ${result.commands.watchTelegram}`);
1185
+ console.log(`3. ${result.commands.pollTelegram}`);
1186
+ console.log(`4. ${result.commands.applyDecision}`);
1187
+ }
1188
+
820
1189
  async function readApprovalRequest(approvalPath, id) {
821
1190
  const resolvedPath = path.resolve(approvalPath);
822
1191
  const approvals = await readApprovalRequests(resolvedPath);
@@ -1246,6 +1615,14 @@ function commandLabel(commandName) {
1246
1615
  return "Telegram approval poll";
1247
1616
  }
1248
1617
 
1618
+ if (commandName === "approvals-apply") {
1619
+ return "Approval apply";
1620
+ }
1621
+
1622
+ if (commandName === "approvals-doctor") {
1623
+ return "Approvals doctor";
1624
+ }
1625
+
1249
1626
  if (commandName === "gate") {
1250
1627
  return "Gate";
1251
1628
  }
@@ -1324,6 +1701,18 @@ function installExitCode(decision, install) {
1324
1701
  return gateExitCode(decision);
1325
1702
  }
1326
1703
 
1704
+ function approvalApplyExitCode(result) {
1705
+ if (!result.decision) {
1706
+ return 1;
1707
+ }
1708
+
1709
+ if (result.decision.decision !== "approve") {
1710
+ return 2;
1711
+ }
1712
+
1713
+ return 0;
1714
+ }
1715
+
1327
1716
  function parseOptions(values) {
1328
1717
  const options = {
1329
1718
  json: false,
@@ -1893,6 +2282,156 @@ function parseApprovalTelegramPollOptions(values) {
1893
2282
  return options;
1894
2283
  }
1895
2284
 
2285
+ function parseApprovalApplyOptions(values) {
2286
+ const options = {
2287
+ approvalPath: undefined,
2288
+ id: undefined,
2289
+ decisionsPath: undefined,
2290
+ dryRun: false,
2291
+ json: false
2292
+ };
2293
+ const paths = [];
2294
+
2295
+ for (let index = 0; index < values.length; index += 1) {
2296
+ const value = values[index];
2297
+
2298
+ if (value === "--json") {
2299
+ options.json = true;
2300
+ continue;
2301
+ }
2302
+
2303
+ if (value === "--dry-run") {
2304
+ options.dryRun = true;
2305
+ continue;
2306
+ }
2307
+
2308
+ if (value === "--id") {
2309
+ options.id = requireNextValue(values, index, "--id");
2310
+ index += 1;
2311
+ continue;
2312
+ }
2313
+
2314
+ if (value === "--decisions") {
2315
+ options.decisionsPath = requireNextValue(values, index, "--decisions");
2316
+ index += 1;
2317
+ continue;
2318
+ }
2319
+
2320
+ if (value === "--out") {
2321
+ options.decisionsPath = requireNextValue(values, index, "--out");
2322
+ index += 1;
2323
+ continue;
2324
+ }
2325
+
2326
+ if (value.startsWith("--")) {
2327
+ throw new Error(`Unknown option: ${value}`);
2328
+ }
2329
+
2330
+ paths.push(value);
2331
+ }
2332
+
2333
+ options.approvalPath = paths[0];
2334
+
2335
+ if (!options.approvalPath) {
2336
+ throw new Error("approvals apply requires <approval.json|approvals.jsonl>.");
2337
+ }
2338
+
2339
+ if (!options.id) {
2340
+ throw new Error("approvals apply requires --id <id>.");
2341
+ }
2342
+
2343
+ return options;
2344
+ }
2345
+
2346
+ function parseApprovalDoctorOptions(values) {
2347
+ const options = {
2348
+ approvalPath: ".clawguard/approvals.jsonl",
2349
+ decisionsPath: ".clawguard/decisions.jsonl",
2350
+ installDir: ".agents/skills",
2351
+ target: "./candidate-skill",
2352
+ framework: "openclaw",
2353
+ chatId: undefined,
2354
+ botToken: undefined,
2355
+ telegramApiBase: undefined,
2356
+ checkTelegram: false,
2357
+ json: false
2358
+ };
2359
+
2360
+ for (let index = 0; index < values.length; index += 1) {
2361
+ const value = values[index];
2362
+
2363
+ if (value === "--json") {
2364
+ options.json = true;
2365
+ continue;
2366
+ }
2367
+
2368
+ if (value === "--approval-out") {
2369
+ options.approvalPath = requireNextValue(values, index, "--approval-out");
2370
+ index += 1;
2371
+ continue;
2372
+ }
2373
+
2374
+ if (value === "--decisions") {
2375
+ options.decisionsPath = requireNextValue(values, index, "--decisions");
2376
+ index += 1;
2377
+ continue;
2378
+ }
2379
+
2380
+ if (value === "--to") {
2381
+ options.installDir = requireNextValue(values, index, "--to");
2382
+ index += 1;
2383
+ continue;
2384
+ }
2385
+
2386
+ if (value === "--target") {
2387
+ options.target = requireNextValue(values, index, "--target");
2388
+ index += 1;
2389
+ continue;
2390
+ }
2391
+
2392
+ if (value === "--framework") {
2393
+ options.framework = requireNextValue(values, index, "--framework");
2394
+ index += 1;
2395
+ continue;
2396
+ }
2397
+
2398
+ if (value === "--chat-id") {
2399
+ options.chatId = requireNextValue(values, index, "--chat-id");
2400
+ index += 1;
2401
+ continue;
2402
+ }
2403
+
2404
+ if (value === "--bot-token") {
2405
+ options.botToken = requireNextValue(values, index, "--bot-token");
2406
+ index += 1;
2407
+ continue;
2408
+ }
2409
+
2410
+ if (value === "--telegram-api-base") {
2411
+ options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
2412
+ index += 1;
2413
+ continue;
2414
+ }
2415
+
2416
+ if (value === "--check-telegram") {
2417
+ options.checkTelegram = true;
2418
+ continue;
2419
+ }
2420
+
2421
+ if (value.startsWith("--")) {
2422
+ throw new Error(`Unknown option: ${value}`);
2423
+ }
2424
+
2425
+ throw new Error(`Unexpected argument for approvals doctor: ${value}`);
2426
+ }
2427
+
2428
+ if (!["openclaw", "hermes"].includes(options.framework)) {
2429
+ throw new Error("Invalid --framework value. Use one of: openclaw, hermes");
2430
+ }
2431
+
2432
+ return options;
2433
+ }
2434
+
1896
2435
  async function writeReportFile(outputPath, content) {
1897
2436
  const resolvedPath = path.resolve(outputPath);
1898
2437
  await fs.mkdir(path.dirname(resolvedPath), { recursive: true });