@denial-web/clawguard 0.1.9 → 0.1.11

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
@@ -99,6 +99,29 @@ npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <ap
99
99
  npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <approval-id> --decision deny --reason "Unexpected shell access" --out ./.clawguard/decisions.jsonl
100
100
  ```
101
101
 
102
+ To turn Telegram replies into decision records, ask the owner to reply with one of:
103
+
104
+ ```text
105
+ approve <approval-id> optional reason
106
+ deny <approval-id> optional reason
107
+ ```
108
+
109
+ Then poll Telegram updates:
110
+
111
+ ```bash
112
+ TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
113
+ ```
114
+
115
+ 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
+
117
+ Apply the recorded decision to continue or block the pending install:
118
+
119
+ ```bash
120
+ npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <approval-id> --decisions ./.clawguard/decisions.jsonl
121
+ ```
122
+
123
+ 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.
124
+
102
125
  When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
103
126
 
104
127
  ```bash
@@ -93,6 +93,42 @@ clawguard approvals decide ./.clawguard/approvals.jsonl \
93
93
 
94
94
  The decision log uses `schemaVersion: "clawguard.decision.v1"` and stores the approval id, normalized decision, actor, reason, target, destination, risk summary, policy summary, and source approval path. Later Telegram, WhatsApp, OpenClaw, or Hermes bridges should write this same decision format after parsing owner replies.
95
95
 
96
+ ### Telegram Reply Polling
97
+
98
+ The first reply bridge is Telegram polling. Owners can respond to the approval message with:
99
+
100
+ ```text
101
+ approve <approval-id> optional reason
102
+ deny <approval-id> optional reason
103
+ ```
104
+
105
+ Command:
106
+
107
+ ```bash
108
+ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals poll-telegram ./.clawguard/approvals.jsonl \
109
+ --decisions ./.clawguard/decisions.jsonl
110
+ ```
111
+
112
+ 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
+
114
+ ### Approval Apply
115
+
116
+ After a decision is recorded, ClawGuard can apply that decision to the original pending install:
117
+
118
+ ```bash
119
+ clawguard approvals apply ./.clawguard/approvals.jsonl \
120
+ --id <approval-id> \
121
+ --decisions ./.clawguard/decisions.jsonl
122
+ ```
123
+
124
+ Apply behavior:
125
+
126
+ - If the latest matching decision is `approve`, copy the original scanned `target` to the original approval `destination`.
127
+ - If the latest matching decision is `deny`, exit blocked and copy nothing.
128
+ - If no matching decision exists, exit paused and copy nothing.
129
+ - Never execute the skill or install dependencies during apply.
130
+ - Refuse symlinked install sources and existing destinations, matching normal install safety.
131
+
96
132
  ### Skill Folder Scan
97
133
 
98
134
  Command:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denial-web/clawguard",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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
@@ -39,7 +39,9 @@ if (![
39
39
  "install",
40
40
  "approvals-send",
41
41
  "approvals-watch",
42
- "approvals-decide"
42
+ "approvals-decide",
43
+ "approvals-poll-telegram",
44
+ "approvals-apply"
43
45
  ].includes(command)) {
44
46
  console.error(`Unknown command: ${command}`);
45
47
  printHelp();
@@ -82,6 +84,28 @@ try {
82
84
  process.exit(0);
83
85
  }
84
86
 
87
+ if (command === "approvals-poll-telegram") {
88
+ const pollOptions = parseApprovalTelegramPollOptions(optionValues);
89
+ const result = await pollTelegramApprovals(pollOptions);
90
+ if (pollOptions.json) {
91
+ console.log(JSON.stringify(result, null, 2));
92
+ } else {
93
+ printApprovalTelegramPollResult(result);
94
+ }
95
+ process.exit(0);
96
+ }
97
+
98
+ if (command === "approvals-apply") {
99
+ const applyOptions = parseApprovalApplyOptions(optionValues);
100
+ const result = await applyApprovalDecision(applyOptions);
101
+ if (applyOptions.json) {
102
+ console.log(JSON.stringify(result, null, 2));
103
+ } else {
104
+ printApprovalApplyResult(result);
105
+ }
106
+ process.exit(approvalApplyExitCode(result));
107
+ }
108
+
85
109
  const cliOptions = parseOptions(optionValues);
86
110
  cliOptions.framework = framework;
87
111
  const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
@@ -145,6 +169,8 @@ Usage:
145
169
  clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
146
170
  clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
147
171
  clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
172
+ clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
173
+ clawguard approvals apply <approvals.jsonl> --id <id> --decisions <decisions.jsonl>
148
174
  clawguard scan-workspace <path> [--json] [--policy <preset>]
149
175
  npm run scan -- <path>
150
176
 
@@ -182,8 +208,12 @@ Options:
182
208
  --id <id> Approval id for send or decide.
183
209
  --decision <value> Approval decision: approve, deny.
184
210
  --out <path> Decision JSONL output file.
211
+ --decisions <path> Decision JSONL output file for reply polling.
185
212
  --actor <name> Decision actor. Default: local-user.
186
213
  --reason <text> Decision reason.
214
+ --offset-state <path> Telegram update offset state file.
215
+ --telegram-updates-file <path>
216
+ Read Telegram updates from a JSON file for tests or offline replay.
187
217
 
188
218
  Gate exit codes:
189
219
  0 = allow
@@ -200,6 +230,8 @@ Examples:
200
230
  npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
201
231
  npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
202
232
  npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
233
+ npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
234
+ npx @denial-web/clawguard approvals apply ./.clawguard/approvals.jsonl --id <id> --decisions ./.clawguard/decisions.jsonl
203
235
  npm run scan -- examples/risky-skill
204
236
  npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
205
237
  npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
@@ -307,6 +339,22 @@ function parseCommand(values) {
307
339
  };
308
340
  }
309
341
 
342
+ if (rawCommand === "approvals" && values[1] === "poll-telegram") {
343
+ return {
344
+ command: "approvals-poll-telegram",
345
+ framework: undefined,
346
+ optionValues: values.slice(2)
347
+ };
348
+ }
349
+
350
+ if (rawCommand === "approvals" && values[1] === "apply") {
351
+ return {
352
+ command: "approvals-apply",
353
+ framework: undefined,
354
+ optionValues: values.slice(2)
355
+ };
356
+ }
357
+
310
358
  if (["openclaw", "hermes"].includes(rawCommand)) {
311
359
  const nestedCommand = values[1];
312
360
 
@@ -471,8 +519,7 @@ async function decideApproval(options) {
471
519
  const outputPath = path.resolve(options.outPath ?? `${options.approvalPath}.decisions.jsonl`);
472
520
  const decision = createApprovalDecision(approval, options);
473
521
 
474
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
475
- await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
522
+ await appendApprovalDecision(outputPath, decision);
476
523
 
477
524
  return {
478
525
  approval: {
@@ -487,6 +534,246 @@ async function decideApproval(options) {
487
534
  };
488
535
  }
489
536
 
537
+ async function appendApprovalDecision(outputPath, decision) {
538
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
539
+ await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
540
+ }
541
+
542
+ async function pollTelegramApprovals(options) {
543
+ const offsetStatePath = path.resolve(options.offsetStatePath ?? `${options.decisionsPath}.telegram-state.json`);
544
+ const offset = await readTelegramOffsetState(offsetStatePath);
545
+ const updates = await fetchTelegramUpdates(options, offset);
546
+ const outputPath = path.resolve(options.decisionsPath);
547
+ const result = {
548
+ approvalPath: path.resolve(options.approvalPath),
549
+ decisionsPath: outputPath,
550
+ offsetStatePath,
551
+ dryRun: options.dryRun,
552
+ checked: updates.length,
553
+ commands: 0,
554
+ decided: 0,
555
+ skipped: 0,
556
+ errors: [],
557
+ decisions: [],
558
+ nextOffset: offset
559
+ };
560
+
561
+ for (const update of updates) {
562
+ const parsed = parseTelegramApprovalUpdate(update);
563
+
564
+ if (!parsed) {
565
+ result.skipped += 1;
566
+ result.nextOffset = nextTelegramOffset(result.nextOffset, update.update_id);
567
+ continue;
568
+ }
569
+
570
+ result.commands += 1;
571
+ result.nextOffset = nextTelegramOffset(result.nextOffset, update.update_id);
572
+
573
+ try {
574
+ const approval = await readApprovalRequest(options.approvalPath, parsed.approvalId);
575
+ const decision = createApprovalDecision(approval, {
576
+ approvalPath: options.approvalPath,
577
+ decision: parsed.decision,
578
+ actor: parsed.actor,
579
+ reason: parsed.reason
580
+ });
581
+ result.decisions.push(decision);
582
+
583
+ if (!options.dryRun) {
584
+ await appendApprovalDecision(outputPath, decision);
585
+ result.decided += 1;
586
+ }
587
+ } catch (error) {
588
+ result.errors.push({
589
+ updateId: update.update_id,
590
+ approvalId: parsed.approvalId,
591
+ message: error.message
592
+ });
593
+ }
594
+ }
595
+
596
+ if (!options.dryRun && result.nextOffset !== offset) {
597
+ await writeTelegramOffsetState(offsetStatePath, result.nextOffset);
598
+ }
599
+
600
+ return result;
601
+ }
602
+
603
+ async function applyApprovalDecision(options) {
604
+ const approval = await readApprovalRequest(options.approvalPath, options.id);
605
+ const decisionsPath = path.resolve(options.decisionsPath ?? `${options.approvalPath}.decisions.jsonl`);
606
+ const decision = await readLatestApprovalDecision(decisionsPath, approval.id);
607
+ const result = {
608
+ approval: {
609
+ id: approval.id,
610
+ status: approval.status,
611
+ decision: approval.decision,
612
+ risk: approval.risk,
613
+ framework: approval.framework
614
+ },
615
+ decision,
616
+ decisionsPath,
617
+ source: approval.target ? path.resolve(approval.target) : undefined,
618
+ destination: approval.destination ? path.resolve(approval.destination) : undefined,
619
+ dryRun: options.dryRun,
620
+ installed: false,
621
+ skipped: true,
622
+ reason: undefined
623
+ };
624
+
625
+ if (!decision) {
626
+ result.reason = "No decision has been recorded for this approval.";
627
+ return result;
628
+ }
629
+
630
+ if (decision.decision !== "approve") {
631
+ result.reason = decision.reason ?? "Approval was denied.";
632
+ return result;
633
+ }
634
+
635
+ if (!result.source) {
636
+ throw new Error("Approval request has no target path to install.");
637
+ }
638
+
639
+ if (!result.destination) {
640
+ throw new Error("Approval request has no destination path to install.");
641
+ }
642
+
643
+ if (options.dryRun) {
644
+ result.skipped = false;
645
+ result.reason = "Dry run passed; no files were copied.";
646
+ return result;
647
+ }
648
+
649
+ await assertInstallableSource(result.source);
650
+ await assertDestinationAvailable(result.destination);
651
+ await fs.mkdir(path.dirname(result.destination), { recursive: true });
652
+ await fs.cp(result.source, result.destination, {
653
+ recursive: true,
654
+ errorOnExist: true,
655
+ force: false,
656
+ verbatimSymlinks: true
657
+ });
658
+
659
+ result.installed = true;
660
+ result.skipped = false;
661
+ result.reason = "Copied after recorded approval.";
662
+ return result;
663
+ }
664
+
665
+ async function readLatestApprovalDecision(decisionsPath, approvalId) {
666
+ let decisions;
667
+
668
+ try {
669
+ decisions = await readApprovalDecisions(decisionsPath);
670
+ } catch (error) {
671
+ if (error.code === "ENOENT") {
672
+ return undefined;
673
+ }
674
+
675
+ throw error;
676
+ }
677
+
678
+ return decisions.filter((decision) => decision.approvalId === approvalId).at(-1);
679
+ }
680
+
681
+ async function readApprovalDecisions(decisionsPath) {
682
+ const content = await fs.readFile(decisionsPath, "utf8");
683
+ const decisions = content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
684
+
685
+ for (const decision of decisions) {
686
+ if (decision.schemaVersion !== "clawguard.decision.v1") {
687
+ throw new Error("Unsupported approval decision schema.");
688
+ }
689
+ }
690
+
691
+ return decisions;
692
+ }
693
+
694
+ async function fetchTelegramUpdates(options, offset) {
695
+ if (options.telegramUpdatesPath) {
696
+ const content = await fs.readFile(path.resolve(options.telegramUpdatesPath), "utf8");
697
+ const payload = JSON.parse(content);
698
+ const updates = Array.isArray(payload) ? payload : payload.result;
699
+
700
+ if (!Array.isArray(updates)) {
701
+ throw new Error("Telegram updates file must be an array or an object with a result array.");
702
+ }
703
+
704
+ return updates.filter((update) => !offset || !Number.isSafeInteger(update.update_id) || update.update_id >= offset);
705
+ }
706
+
707
+ const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
708
+
709
+ if (!botToken) {
710
+ throw new Error("Telegram poll requires --bot-token or TELEGRAM_BOT_TOKEN.");
711
+ }
712
+
713
+ const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
714
+ const endpoint = new URL(`${apiBase.replace(/\/$/, "")}/bot${botToken}/getUpdates`);
715
+ endpoint.searchParams.set("timeout", String(options.timeoutSeconds));
716
+
717
+ if (offset) {
718
+ endpoint.searchParams.set("offset", String(offset));
719
+ }
720
+
721
+ const response = await fetch(endpoint);
722
+ const text = await response.text();
723
+ let payload;
724
+
725
+ try {
726
+ payload = text ? JSON.parse(text) : null;
727
+ } catch {
728
+ throw new Error(`Telegram poll returned invalid JSON: ${text}`);
729
+ }
730
+
731
+ if (!response.ok || payload?.ok === false) {
732
+ throw new Error(`Telegram poll failed with HTTP ${response.status}: ${text}`);
733
+ }
734
+
735
+ if (!Array.isArray(payload?.result)) {
736
+ throw new Error("Telegram poll response did not include a result array.");
737
+ }
738
+
739
+ return payload.result;
740
+ }
741
+
742
+ function parseTelegramApprovalUpdate(update) {
743
+ const message = update.message ?? update.edited_message ?? update.channel_post;
744
+ const text = message?.text;
745
+
746
+ if (!text) {
747
+ return undefined;
748
+ }
749
+
750
+ const match = text.trim().match(/^\/?(approve|approved|deny|denied)\s+([A-Za-z0-9_.:@-]+)(?:\s+(.+))?$/i);
751
+
752
+ if (!match) {
753
+ return undefined;
754
+ }
755
+
756
+ const decision = normalizeApprovalDecision(match[1].toLowerCase());
757
+ const actorName = message.from?.username ?? message.from?.id ?? message.chat?.username ?? message.chat?.id ?? "telegram-user";
758
+ const reason = match[3]?.trim();
759
+
760
+ return {
761
+ updateId: update.update_id,
762
+ approvalId: match[2],
763
+ decision,
764
+ actor: `telegram:${actorName}`,
765
+ reason
766
+ };
767
+ }
768
+
769
+ function nextTelegramOffset(currentOffset, updateId) {
770
+ if (!Number.isSafeInteger(updateId)) {
771
+ return currentOffset;
772
+ }
773
+
774
+ return Math.max(currentOffset ?? 0, updateId + 1);
775
+ }
776
+
490
777
  function createApprovalDecision(approval, options) {
491
778
  const decision = normalizeApprovalDecision(options.decision);
492
779
  const status = decision === "approve" ? "approved" : "denied";
@@ -624,6 +911,36 @@ function printApprovalDecisionResult(result) {
624
911
  console.log(`Output: ${result.outputPath}`);
625
912
  }
626
913
 
914
+ function printApprovalTelegramPollResult(result) {
915
+ console.log(`ClawGuard Telegram approval poll: ${result.approvalPath}`);
916
+ console.log(`Decisions: ${result.decisionsPath}`);
917
+ console.log(`Offset state: ${result.offsetStatePath}`);
918
+ console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
919
+ console.log(`Updates checked: ${result.checked}`);
920
+ console.log(`Commands found: ${result.commands}`);
921
+ console.log(`Decisions written: ${result.decided}`);
922
+ console.log(`Skipped: ${result.skipped}`);
923
+ console.log(`Next offset: ${result.nextOffset ?? "none"}`);
924
+
925
+ if (result.errors.length > 0) {
926
+ console.log("Errors:");
927
+ for (const error of result.errors) {
928
+ console.log(`- update ${error.updateId}: ${error.message}`);
929
+ }
930
+ }
931
+ }
932
+
933
+ function printApprovalApplyResult(result) {
934
+ console.log(`ClawGuard approval apply: ${result.approval.id}`);
935
+ console.log(`Decision: ${result.decision ? formatDecision(result.decision.decision) : "PENDING"}`);
936
+ console.log(`Source: ${result.source ?? "not recorded"}`);
937
+ console.log(`Destination: ${result.destination ?? "not recorded"}`);
938
+ console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
939
+ console.log(`Installed: ${result.installed ? "yes" : "no"}`);
940
+ console.log(`Exit code: ${approvalApplyExitCode(result)}`);
941
+ console.log(`Reason: ${result.reason}`);
942
+ }
943
+
627
944
  async function readApprovalRequest(approvalPath, id) {
628
945
  const resolvedPath = path.resolve(approvalPath);
629
946
  const approvals = await readApprovalRequests(resolvedPath);
@@ -700,6 +1017,29 @@ async function writeApprovalWatchState(statePath, sentIds) {
700
1017
  }, null, 2)}\n`);
701
1018
  }
702
1019
 
1020
+ async function readTelegramOffsetState(statePath) {
1021
+ try {
1022
+ const content = await fs.readFile(statePath, "utf8");
1023
+ const state = JSON.parse(content);
1024
+ return Number.isSafeInteger(state.nextOffset) ? state.nextOffset : undefined;
1025
+ } catch (error) {
1026
+ if (error.code === "ENOENT") {
1027
+ return undefined;
1028
+ }
1029
+
1030
+ throw error;
1031
+ }
1032
+ }
1033
+
1034
+ async function writeTelegramOffsetState(statePath, nextOffset) {
1035
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
1036
+ await fs.writeFile(statePath, `${JSON.stringify({
1037
+ schemaVersion: "clawguard.telegram-offset.v1",
1038
+ updatedAt: new Date().toISOString(),
1039
+ nextOffset
1040
+ }, null, 2)}\n`);
1041
+ }
1042
+
703
1043
  function printGateResult(result, options) {
704
1044
  const decision = result.policy.decision;
705
1045
  console.log(`ClawGuard gate: ${result.target}`);
@@ -1026,6 +1366,14 @@ function commandLabel(commandName) {
1026
1366
  return "Approval decision";
1027
1367
  }
1028
1368
 
1369
+ if (commandName === "approvals-poll-telegram") {
1370
+ return "Telegram approval poll";
1371
+ }
1372
+
1373
+ if (commandName === "approvals-apply") {
1374
+ return "Approval apply";
1375
+ }
1376
+
1029
1377
  if (commandName === "gate") {
1030
1378
  return "Gate";
1031
1379
  }
@@ -1104,6 +1452,18 @@ function installExitCode(decision, install) {
1104
1452
  return gateExitCode(decision);
1105
1453
  }
1106
1454
 
1455
+ function approvalApplyExitCode(result) {
1456
+ if (!result.decision) {
1457
+ return 1;
1458
+ }
1459
+
1460
+ if (result.decision.decision !== "approve") {
1461
+ return 2;
1462
+ }
1463
+
1464
+ return 0;
1465
+ }
1466
+
1107
1467
  function parseOptions(values) {
1108
1468
  const options = {
1109
1469
  json: false,
@@ -1580,6 +1940,160 @@ function parseApprovalDecisionOptions(values) {
1580
1940
  return options;
1581
1941
  }
1582
1942
 
1943
+ function parseApprovalTelegramPollOptions(values) {
1944
+ const options = {
1945
+ approvalPath: undefined,
1946
+ decisionsPath: undefined,
1947
+ offsetStatePath: undefined,
1948
+ botToken: undefined,
1949
+ telegramApiBase: undefined,
1950
+ telegramUpdatesPath: undefined,
1951
+ timeoutSeconds: 0,
1952
+ dryRun: false,
1953
+ json: false
1954
+ };
1955
+ const paths = [];
1956
+
1957
+ for (let index = 0; index < values.length; index += 1) {
1958
+ const value = values[index];
1959
+
1960
+ if (value === "--json") {
1961
+ options.json = true;
1962
+ continue;
1963
+ }
1964
+
1965
+ if (value === "--dry-run") {
1966
+ options.dryRun = true;
1967
+ continue;
1968
+ }
1969
+
1970
+ if (value === "--decisions") {
1971
+ options.decisionsPath = requireNextValue(values, index, "--decisions");
1972
+ index += 1;
1973
+ continue;
1974
+ }
1975
+
1976
+ if (value === "--out") {
1977
+ options.decisionsPath = requireNextValue(values, index, "--out");
1978
+ index += 1;
1979
+ continue;
1980
+ }
1981
+
1982
+ if (value === "--offset-state") {
1983
+ options.offsetStatePath = requireNextValue(values, index, "--offset-state");
1984
+ index += 1;
1985
+ continue;
1986
+ }
1987
+
1988
+ if (value === "--bot-token") {
1989
+ options.botToken = requireNextValue(values, index, "--bot-token");
1990
+ index += 1;
1991
+ continue;
1992
+ }
1993
+
1994
+ if (value === "--telegram-api-base") {
1995
+ options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
1996
+ index += 1;
1997
+ continue;
1998
+ }
1999
+
2000
+ if (value === "--telegram-updates-file") {
2001
+ options.telegramUpdatesPath = requireNextValue(values, index, "--telegram-updates-file");
2002
+ index += 1;
2003
+ continue;
2004
+ }
2005
+
2006
+ if (value === "--timeout") {
2007
+ const timeout = Number.parseInt(requireNextValue(values, index, "--timeout"), 10);
2008
+ if (!Number.isSafeInteger(timeout) || timeout < 0) {
2009
+ throw new Error("--timeout must be a non-negative integer.");
2010
+ }
2011
+ options.timeoutSeconds = timeout;
2012
+ index += 1;
2013
+ continue;
2014
+ }
2015
+
2016
+ if (value.startsWith("--")) {
2017
+ throw new Error(`Unknown option: ${value}`);
2018
+ }
2019
+
2020
+ paths.push(value);
2021
+ }
2022
+
2023
+ options.approvalPath = paths[0];
2024
+
2025
+ if (!options.approvalPath) {
2026
+ throw new Error("approvals poll-telegram requires <approval.json|approvals.jsonl>.");
2027
+ }
2028
+
2029
+ if (!options.decisionsPath) {
2030
+ throw new Error("approvals poll-telegram requires --decisions <path>.");
2031
+ }
2032
+
2033
+ return options;
2034
+ }
2035
+
2036
+ function parseApprovalApplyOptions(values) {
2037
+ const options = {
2038
+ approvalPath: undefined,
2039
+ id: undefined,
2040
+ decisionsPath: undefined,
2041
+ dryRun: false,
2042
+ json: false
2043
+ };
2044
+ const paths = [];
2045
+
2046
+ for (let index = 0; index < values.length; index += 1) {
2047
+ const value = values[index];
2048
+
2049
+ if (value === "--json") {
2050
+ options.json = true;
2051
+ continue;
2052
+ }
2053
+
2054
+ if (value === "--dry-run") {
2055
+ options.dryRun = true;
2056
+ continue;
2057
+ }
2058
+
2059
+ if (value === "--id") {
2060
+ options.id = requireNextValue(values, index, "--id");
2061
+ index += 1;
2062
+ continue;
2063
+ }
2064
+
2065
+ if (value === "--decisions") {
2066
+ options.decisionsPath = requireNextValue(values, index, "--decisions");
2067
+ index += 1;
2068
+ continue;
2069
+ }
2070
+
2071
+ if (value === "--out") {
2072
+ options.decisionsPath = requireNextValue(values, index, "--out");
2073
+ index += 1;
2074
+ continue;
2075
+ }
2076
+
2077
+ if (value.startsWith("--")) {
2078
+ throw new Error(`Unknown option: ${value}`);
2079
+ }
2080
+
2081
+ paths.push(value);
2082
+ }
2083
+
2084
+ options.approvalPath = paths[0];
2085
+
2086
+ if (!options.approvalPath) {
2087
+ throw new Error("approvals apply requires <approval.json|approvals.jsonl>.");
2088
+ }
2089
+
2090
+ if (!options.id) {
2091
+ throw new Error("approvals apply requires --id <id>.");
2092
+ }
2093
+
2094
+ return options;
2095
+ }
2096
+
1583
2097
  async function writeReportFile(outputPath, content) {
1584
2098
  const resolvedPath = path.resolve(outputPath);
1585
2099
  await fs.mkdir(path.dirname(resolvedPath), { recursive: true });