@denial-web/clawguard 0.1.8 → 0.1.10

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
@@ -92,6 +92,28 @@ Use `--once --dry-run` to verify the message flow without sending anything:
92
92
  TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789 --once --dry-run
93
93
  ```
94
94
 
95
+ Record the owner's decision in a durable decision log:
96
+
97
+ ```bash
98
+ npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <approval-id> --decision approve --out ./.clawguard/decisions.jsonl
99
+ npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <approval-id> --decision deny --reason "Unexpected shell access" --out ./.clawguard/decisions.jsonl
100
+ ```
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
+
95
117
  When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
96
118
 
97
119
  ```bash
@@ -78,6 +78,39 @@ 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 Decisions
82
+
83
+ Owner decisions are stored separately from approval requests. This keeps the original scan evidence immutable and gives messaging bridges a simple append-only target:
84
+
85
+ ```bash
86
+ clawguard approvals decide ./.clawguard/approvals.jsonl \
87
+ --id <approval-id> \
88
+ --decision approve \
89
+ --actor owner \
90
+ --reason "Reviewed and acceptable" \
91
+ --out ./.clawguard/decisions.jsonl
92
+ ```
93
+
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
+
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
+
81
114
  ### Skill Folder Scan
82
115
 
83
116
  Command:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denial-web/clawguard",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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
@@ -32,7 +32,16 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
32
32
  const commandContext = parseCommand(args);
33
33
  const { command, framework, optionValues } = commandContext;
34
34
 
35
- if (!["scan", "scan-workspace", "gate", "install", "approvals-send", "approvals-watch"].includes(command)) {
35
+ if (![
36
+ "scan",
37
+ "scan-workspace",
38
+ "gate",
39
+ "install",
40
+ "approvals-send",
41
+ "approvals-watch",
42
+ "approvals-decide",
43
+ "approvals-poll-telegram"
44
+ ].includes(command)) {
36
45
  console.error(`Unknown command: ${command}`);
37
46
  printHelp();
38
47
  process.exit(1);
@@ -63,6 +72,28 @@ try {
63
72
  process.exit(0);
64
73
  }
65
74
 
75
+ if (command === "approvals-decide") {
76
+ const decisionOptions = parseApprovalDecisionOptions(optionValues);
77
+ const result = await decideApproval(decisionOptions);
78
+ if (decisionOptions.json) {
79
+ console.log(JSON.stringify(result, null, 2));
80
+ } else {
81
+ printApprovalDecisionResult(result);
82
+ }
83
+ process.exit(0);
84
+ }
85
+
86
+ if (command === "approvals-poll-telegram") {
87
+ const pollOptions = parseApprovalTelegramPollOptions(optionValues);
88
+ const result = await pollTelegramApprovals(pollOptions);
89
+ if (pollOptions.json) {
90
+ console.log(JSON.stringify(result, null, 2));
91
+ } else {
92
+ printApprovalTelegramPollResult(result);
93
+ }
94
+ process.exit(0);
95
+ }
96
+
66
97
  const cliOptions = parseOptions(optionValues);
67
98
  cliOptions.framework = framework;
68
99
  const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
@@ -125,6 +156,8 @@ Usage:
125
156
  clawguard approvals send <approval.json|approvals.jsonl> --via openclaw --channel <name> --target <id>
126
157
  clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
127
158
  clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
159
+ clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
160
+ clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
128
161
  clawguard scan-workspace <path> [--json] [--policy <preset>]
129
162
  npm run scan -- <path>
130
163
 
@@ -159,6 +192,15 @@ Options:
159
192
  --interval <ms> Approval watch poll interval. Default: 2000.
160
193
  --state <path> Approval watch sent-id state file.
161
194
  --once Run approval watch once and exit.
195
+ --id <id> Approval id for send or decide.
196
+ --decision <value> Approval decision: approve, deny.
197
+ --out <path> Decision JSONL output file.
198
+ --decisions <path> Decision JSONL output file for reply polling.
199
+ --actor <name> Decision actor. Default: local-user.
200
+ --reason <text> Decision reason.
201
+ --offset-state <path> Telegram update offset state file.
202
+ --telegram-updates-file <path>
203
+ Read Telegram updates from a JSON file for tests or offline replay.
162
204
 
163
205
  Gate exit codes:
164
206
  0 = allow
@@ -174,6 +216,8 @@ Examples:
174
216
  npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
175
217
  npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
176
218
  npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
219
+ npx @denial-web/clawguard approvals decide ./.clawguard/approvals.jsonl --id <id> --decision approve
220
+ npx @denial-web/clawguard approvals poll-telegram ./.clawguard/approvals.jsonl --decisions ./.clawguard/decisions.jsonl
177
221
  npm run scan -- examples/risky-skill
178
222
  npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
179
223
  npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
@@ -273,6 +317,22 @@ function parseCommand(values) {
273
317
  };
274
318
  }
275
319
 
320
+ if (rawCommand === "approvals" && values[1] === "decide") {
321
+ return {
322
+ command: "approvals-decide",
323
+ framework: undefined,
324
+ optionValues: values.slice(2)
325
+ };
326
+ }
327
+
328
+ if (rawCommand === "approvals" && values[1] === "poll-telegram") {
329
+ return {
330
+ command: "approvals-poll-telegram",
331
+ framework: undefined,
332
+ optionValues: values.slice(2)
333
+ };
334
+ }
335
+
276
336
  if (["openclaw", "hermes"].includes(rawCommand)) {
277
337
  const nestedCommand = values[1];
278
338
 
@@ -432,6 +492,200 @@ async function watchApprovals(options, hooks = {}) {
432
492
  } while (true);
433
493
  }
434
494
 
495
+ async function decideApproval(options) {
496
+ const approval = await readApprovalRequest(options.approvalPath, options.id);
497
+ const outputPath = path.resolve(options.outPath ?? `${options.approvalPath}.decisions.jsonl`);
498
+ const decision = createApprovalDecision(approval, options);
499
+
500
+ await appendApprovalDecision(outputPath, decision);
501
+
502
+ return {
503
+ approval: {
504
+ id: approval.id,
505
+ status: approval.status,
506
+ decision: approval.decision,
507
+ risk: approval.risk,
508
+ framework: approval.framework
509
+ },
510
+ outputPath,
511
+ decision
512
+ };
513
+ }
514
+
515
+ async function appendApprovalDecision(outputPath, decision) {
516
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
517
+ await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
518
+ }
519
+
520
+ async function pollTelegramApprovals(options) {
521
+ const offsetStatePath = path.resolve(options.offsetStatePath ?? `${options.decisionsPath}.telegram-state.json`);
522
+ const offset = await readTelegramOffsetState(offsetStatePath);
523
+ const updates = await fetchTelegramUpdates(options, offset);
524
+ const outputPath = path.resolve(options.decisionsPath);
525
+ const result = {
526
+ approvalPath: path.resolve(options.approvalPath),
527
+ decisionsPath: outputPath,
528
+ offsetStatePath,
529
+ dryRun: options.dryRun,
530
+ checked: updates.length,
531
+ commands: 0,
532
+ decided: 0,
533
+ skipped: 0,
534
+ errors: [],
535
+ decisions: [],
536
+ nextOffset: offset
537
+ };
538
+
539
+ for (const update of updates) {
540
+ const parsed = parseTelegramApprovalUpdate(update);
541
+
542
+ if (!parsed) {
543
+ result.skipped += 1;
544
+ result.nextOffset = nextTelegramOffset(result.nextOffset, update.update_id);
545
+ continue;
546
+ }
547
+
548
+ result.commands += 1;
549
+ result.nextOffset = nextTelegramOffset(result.nextOffset, update.update_id);
550
+
551
+ try {
552
+ const approval = await readApprovalRequest(options.approvalPath, parsed.approvalId);
553
+ const decision = createApprovalDecision(approval, {
554
+ approvalPath: options.approvalPath,
555
+ decision: parsed.decision,
556
+ actor: parsed.actor,
557
+ reason: parsed.reason
558
+ });
559
+ result.decisions.push(decision);
560
+
561
+ if (!options.dryRun) {
562
+ await appendApprovalDecision(outputPath, decision);
563
+ result.decided += 1;
564
+ }
565
+ } catch (error) {
566
+ result.errors.push({
567
+ updateId: update.update_id,
568
+ approvalId: parsed.approvalId,
569
+ message: error.message
570
+ });
571
+ }
572
+ }
573
+
574
+ if (!options.dryRun && result.nextOffset !== offset) {
575
+ await writeTelegramOffsetState(offsetStatePath, result.nextOffset);
576
+ }
577
+
578
+ return result;
579
+ }
580
+
581
+ async function fetchTelegramUpdates(options, offset) {
582
+ if (options.telegramUpdatesPath) {
583
+ const content = await fs.readFile(path.resolve(options.telegramUpdatesPath), "utf8");
584
+ const payload = JSON.parse(content);
585
+ const updates = Array.isArray(payload) ? payload : payload.result;
586
+
587
+ if (!Array.isArray(updates)) {
588
+ throw new Error("Telegram updates file must be an array or an object with a result array.");
589
+ }
590
+
591
+ return updates.filter((update) => !offset || !Number.isSafeInteger(update.update_id) || update.update_id >= offset);
592
+ }
593
+
594
+ const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
595
+
596
+ if (!botToken) {
597
+ throw new Error("Telegram poll requires --bot-token or TELEGRAM_BOT_TOKEN.");
598
+ }
599
+
600
+ const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
601
+ const endpoint = new URL(`${apiBase.replace(/\/$/, "")}/bot${botToken}/getUpdates`);
602
+ endpoint.searchParams.set("timeout", String(options.timeoutSeconds));
603
+
604
+ if (offset) {
605
+ endpoint.searchParams.set("offset", String(offset));
606
+ }
607
+
608
+ const response = await fetch(endpoint);
609
+ const text = await response.text();
610
+ let payload;
611
+
612
+ try {
613
+ payload = text ? JSON.parse(text) : null;
614
+ } catch {
615
+ throw new Error(`Telegram poll returned invalid JSON: ${text}`);
616
+ }
617
+
618
+ if (!response.ok || payload?.ok === false) {
619
+ throw new Error(`Telegram poll failed with HTTP ${response.status}: ${text}`);
620
+ }
621
+
622
+ if (!Array.isArray(payload?.result)) {
623
+ throw new Error("Telegram poll response did not include a result array.");
624
+ }
625
+
626
+ return payload.result;
627
+ }
628
+
629
+ function parseTelegramApprovalUpdate(update) {
630
+ const message = update.message ?? update.edited_message ?? update.channel_post;
631
+ const text = message?.text;
632
+
633
+ if (!text) {
634
+ return undefined;
635
+ }
636
+
637
+ const match = text.trim().match(/^\/?(approve|approved|deny|denied)\s+([A-Za-z0-9_.:@-]+)(?:\s+(.+))?$/i);
638
+
639
+ if (!match) {
640
+ return undefined;
641
+ }
642
+
643
+ const decision = normalizeApprovalDecision(match[1].toLowerCase());
644
+ const actorName = message.from?.username ?? message.from?.id ?? message.chat?.username ?? message.chat?.id ?? "telegram-user";
645
+ const reason = match[3]?.trim();
646
+
647
+ return {
648
+ updateId: update.update_id,
649
+ approvalId: match[2],
650
+ decision,
651
+ actor: `telegram:${actorName}`,
652
+ reason
653
+ };
654
+ }
655
+
656
+ function nextTelegramOffset(currentOffset, updateId) {
657
+ if (!Number.isSafeInteger(updateId)) {
658
+ return currentOffset;
659
+ }
660
+
661
+ return Math.max(currentOffset ?? 0, updateId + 1);
662
+ }
663
+
664
+ function createApprovalDecision(approval, options) {
665
+ const decision = normalizeApprovalDecision(options.decision);
666
+ const status = decision === "approve" ? "approved" : "denied";
667
+
668
+ return {
669
+ schemaVersion: "clawguard.decision.v1",
670
+ id: randomUUID(),
671
+ approvalId: approval.id,
672
+ status,
673
+ decision,
674
+ decidedAt: new Date().toISOString(),
675
+ actor: options.actor,
676
+ reason: options.reason,
677
+ framework: approval.framework,
678
+ target: approval.target,
679
+ destination: approval.destination,
680
+ risk: approval.risk,
681
+ policy: approval.policy,
682
+ source: {
683
+ path: path.resolve(options.approvalPath),
684
+ approvalCreatedAt: approval.createdAt
685
+ }
686
+ };
687
+ }
688
+
435
689
  async function sendTelegramApproval(approval, message, options) {
436
690
  const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
437
691
 
@@ -533,6 +787,36 @@ function printApprovalWatchResult(result) {
533
787
  console.log(`Skipped: ${result.skipped}`);
534
788
  }
535
789
 
790
+ function printApprovalDecisionResult(result) {
791
+ console.log(`ClawGuard approval decision: ${result.approval.id}`);
792
+ console.log(`Decision: ${formatDecision(result.decision.decision)}`);
793
+ console.log(`Status: ${result.decision.status}`);
794
+ console.log(`Actor: ${result.decision.actor}`);
795
+ if (result.decision.reason) {
796
+ console.log(`Reason: ${result.decision.reason}`);
797
+ }
798
+ console.log(`Output: ${result.outputPath}`);
799
+ }
800
+
801
+ function printApprovalTelegramPollResult(result) {
802
+ console.log(`ClawGuard Telegram approval poll: ${result.approvalPath}`);
803
+ console.log(`Decisions: ${result.decisionsPath}`);
804
+ console.log(`Offset state: ${result.offsetStatePath}`);
805
+ console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
806
+ console.log(`Updates checked: ${result.checked}`);
807
+ console.log(`Commands found: ${result.commands}`);
808
+ console.log(`Decisions written: ${result.decided}`);
809
+ console.log(`Skipped: ${result.skipped}`);
810
+ console.log(`Next offset: ${result.nextOffset ?? "none"}`);
811
+
812
+ if (result.errors.length > 0) {
813
+ console.log("Errors:");
814
+ for (const error of result.errors) {
815
+ console.log(`- update ${error.updateId}: ${error.message}`);
816
+ }
817
+ }
818
+ }
819
+
536
820
  async function readApprovalRequest(approvalPath, id) {
537
821
  const resolvedPath = path.resolve(approvalPath);
538
822
  const approvals = await readApprovalRequests(resolvedPath);
@@ -609,6 +893,29 @@ async function writeApprovalWatchState(statePath, sentIds) {
609
893
  }, null, 2)}\n`);
610
894
  }
611
895
 
896
+ async function readTelegramOffsetState(statePath) {
897
+ try {
898
+ const content = await fs.readFile(statePath, "utf8");
899
+ const state = JSON.parse(content);
900
+ return Number.isSafeInteger(state.nextOffset) ? state.nextOffset : undefined;
901
+ } catch (error) {
902
+ if (error.code === "ENOENT") {
903
+ return undefined;
904
+ }
905
+
906
+ throw error;
907
+ }
908
+ }
909
+
910
+ async function writeTelegramOffsetState(statePath, nextOffset) {
911
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
912
+ await fs.writeFile(statePath, `${JSON.stringify({
913
+ schemaVersion: "clawguard.telegram-offset.v1",
914
+ updatedAt: new Date().toISOString(),
915
+ nextOffset
916
+ }, null, 2)}\n`);
917
+ }
918
+
612
919
  function printGateResult(result, options) {
613
920
  const decision = result.policy.decision;
614
921
  console.log(`ClawGuard gate: ${result.target}`);
@@ -931,6 +1238,14 @@ function commandLabel(commandName) {
931
1238
  return "Approval watch";
932
1239
  }
933
1240
 
1241
+ if (commandName === "approvals-decide") {
1242
+ return "Approval decision";
1243
+ }
1244
+
1245
+ if (commandName === "approvals-poll-telegram") {
1246
+ return "Telegram approval poll";
1247
+ }
1248
+
934
1249
  if (commandName === "gate") {
935
1250
  return "Gate";
936
1251
  }
@@ -977,6 +1292,18 @@ function sleep(ms) {
977
1292
  });
978
1293
  }
979
1294
 
1295
+ function normalizeApprovalDecision(value) {
1296
+ if (value === "approved") {
1297
+ return "approve";
1298
+ }
1299
+
1300
+ if (value === "denied") {
1301
+ return "deny";
1302
+ }
1303
+
1304
+ return value;
1305
+ }
1306
+
980
1307
  function gateExitCode(decision) {
981
1308
  if (decision === "allow") {
982
1309
  return 0;
@@ -1393,6 +1720,179 @@ function parseApprovalWatchOptions(values) {
1393
1720
  return options;
1394
1721
  }
1395
1722
 
1723
+ function parseApprovalDecisionOptions(values) {
1724
+ const options = {
1725
+ approvalPath: undefined,
1726
+ id: undefined,
1727
+ decision: undefined,
1728
+ outPath: undefined,
1729
+ actor: "local-user",
1730
+ reason: undefined,
1731
+ json: false
1732
+ };
1733
+ const paths = [];
1734
+
1735
+ for (let index = 0; index < values.length; index += 1) {
1736
+ const value = values[index];
1737
+
1738
+ if (value === "--json") {
1739
+ options.json = true;
1740
+ continue;
1741
+ }
1742
+
1743
+ if (value === "--id") {
1744
+ options.id = requireNextValue(values, index, "--id");
1745
+ index += 1;
1746
+ continue;
1747
+ }
1748
+
1749
+ if (value === "--decision") {
1750
+ options.decision = requireNextValue(values, index, "--decision");
1751
+ index += 1;
1752
+ continue;
1753
+ }
1754
+
1755
+ if (value === "--out") {
1756
+ options.outPath = requireNextValue(values, index, "--out");
1757
+ index += 1;
1758
+ continue;
1759
+ }
1760
+
1761
+ if (value === "--actor") {
1762
+ options.actor = requireNextValue(values, index, "--actor");
1763
+ index += 1;
1764
+ continue;
1765
+ }
1766
+
1767
+ if (value === "--reason") {
1768
+ options.reason = requireNextValue(values, index, "--reason");
1769
+ index += 1;
1770
+ continue;
1771
+ }
1772
+
1773
+ if (value.startsWith("--")) {
1774
+ throw new Error(`Unknown option: ${value}`);
1775
+ }
1776
+
1777
+ paths.push(value);
1778
+ }
1779
+
1780
+ options.approvalPath = paths[0];
1781
+
1782
+ if (!options.approvalPath) {
1783
+ throw new Error("approvals decide requires <approval.json|approvals.jsonl>.");
1784
+ }
1785
+
1786
+ if (!options.id) {
1787
+ throw new Error("approvals decide requires --id <id>.");
1788
+ }
1789
+
1790
+ if (!options.decision) {
1791
+ throw new Error("approvals decide requires --decision approve|deny.");
1792
+ }
1793
+
1794
+ options.decision = normalizeApprovalDecision(options.decision);
1795
+
1796
+ if (!["approve", "deny"].includes(options.decision)) {
1797
+ throw new Error("Invalid --decision value. Use one of: approve, deny");
1798
+ }
1799
+
1800
+ return options;
1801
+ }
1802
+
1803
+ function parseApprovalTelegramPollOptions(values) {
1804
+ const options = {
1805
+ approvalPath: undefined,
1806
+ decisionsPath: undefined,
1807
+ offsetStatePath: undefined,
1808
+ botToken: undefined,
1809
+ telegramApiBase: undefined,
1810
+ telegramUpdatesPath: undefined,
1811
+ timeoutSeconds: 0,
1812
+ dryRun: false,
1813
+ json: false
1814
+ };
1815
+ const paths = [];
1816
+
1817
+ for (let index = 0; index < values.length; index += 1) {
1818
+ const value = values[index];
1819
+
1820
+ if (value === "--json") {
1821
+ options.json = true;
1822
+ continue;
1823
+ }
1824
+
1825
+ if (value === "--dry-run") {
1826
+ options.dryRun = true;
1827
+ continue;
1828
+ }
1829
+
1830
+ if (value === "--decisions") {
1831
+ options.decisionsPath = requireNextValue(values, index, "--decisions");
1832
+ index += 1;
1833
+ continue;
1834
+ }
1835
+
1836
+ if (value === "--out") {
1837
+ options.decisionsPath = requireNextValue(values, index, "--out");
1838
+ index += 1;
1839
+ continue;
1840
+ }
1841
+
1842
+ if (value === "--offset-state") {
1843
+ options.offsetStatePath = requireNextValue(values, index, "--offset-state");
1844
+ index += 1;
1845
+ continue;
1846
+ }
1847
+
1848
+ if (value === "--bot-token") {
1849
+ options.botToken = requireNextValue(values, index, "--bot-token");
1850
+ index += 1;
1851
+ continue;
1852
+ }
1853
+
1854
+ if (value === "--telegram-api-base") {
1855
+ options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
1856
+ index += 1;
1857
+ continue;
1858
+ }
1859
+
1860
+ if (value === "--telegram-updates-file") {
1861
+ options.telegramUpdatesPath = requireNextValue(values, index, "--telegram-updates-file");
1862
+ index += 1;
1863
+ continue;
1864
+ }
1865
+
1866
+ if (value === "--timeout") {
1867
+ const timeout = Number.parseInt(requireNextValue(values, index, "--timeout"), 10);
1868
+ if (!Number.isSafeInteger(timeout) || timeout < 0) {
1869
+ throw new Error("--timeout must be a non-negative integer.");
1870
+ }
1871
+ options.timeoutSeconds = timeout;
1872
+ index += 1;
1873
+ continue;
1874
+ }
1875
+
1876
+ if (value.startsWith("--")) {
1877
+ throw new Error(`Unknown option: ${value}`);
1878
+ }
1879
+
1880
+ paths.push(value);
1881
+ }
1882
+
1883
+ options.approvalPath = paths[0];
1884
+
1885
+ if (!options.approvalPath) {
1886
+ throw new Error("approvals poll-telegram requires <approval.json|approvals.jsonl>.");
1887
+ }
1888
+
1889
+ if (!options.decisionsPath) {
1890
+ throw new Error("approvals poll-telegram requires --decisions <path>.");
1891
+ }
1892
+
1893
+ return options;
1894
+ }
1895
+
1396
1896
  async function writeReportFile(outputPath, content) {
1397
1897
  const resolvedPath = path.resolve(outputPath);
1398
1898
  await fs.mkdir(path.dirname(resolvedPath), { recursive: true });