@denial-web/clawguard 0.1.9 → 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 +15 -0
- package/docs/INTEGRATION_SPEC.md +18 -0
- package/package.json +1 -1
- package/src/cli.js +316 -3
package/README.md
CHANGED
|
@@ -99,6 +99,21 @@ 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
|
+
|
|
102
117
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
103
118
|
|
|
104
119
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -93,6 +93,24 @@ 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
|
+
|
|
96
114
|
### Skill Folder Scan
|
|
97
115
|
|
|
98
116
|
Command:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -39,7 +39,8 @@ if (![
|
|
|
39
39
|
"install",
|
|
40
40
|
"approvals-send",
|
|
41
41
|
"approvals-watch",
|
|
42
|
-
"approvals-decide"
|
|
42
|
+
"approvals-decide",
|
|
43
|
+
"approvals-poll-telegram"
|
|
43
44
|
].includes(command)) {
|
|
44
45
|
console.error(`Unknown command: ${command}`);
|
|
45
46
|
printHelp();
|
|
@@ -82,6 +83,17 @@ try {
|
|
|
82
83
|
process.exit(0);
|
|
83
84
|
}
|
|
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
|
+
|
|
85
97
|
const cliOptions = parseOptions(optionValues);
|
|
86
98
|
cliOptions.framework = framework;
|
|
87
99
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -145,6 +157,7 @@ Usage:
|
|
|
145
157
|
clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
|
|
146
158
|
clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
|
|
147
159
|
clawguard approvals decide <approval.json|approvals.jsonl> --id <id> --decision approve|deny
|
|
160
|
+
clawguard approvals poll-telegram <approvals.jsonl> --decisions <decisions.jsonl>
|
|
148
161
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
149
162
|
npm run scan -- <path>
|
|
150
163
|
|
|
@@ -182,8 +195,12 @@ Options:
|
|
|
182
195
|
--id <id> Approval id for send or decide.
|
|
183
196
|
--decision <value> Approval decision: approve, deny.
|
|
184
197
|
--out <path> Decision JSONL output file.
|
|
198
|
+
--decisions <path> Decision JSONL output file for reply polling.
|
|
185
199
|
--actor <name> Decision actor. Default: local-user.
|
|
186
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.
|
|
187
204
|
|
|
188
205
|
Gate exit codes:
|
|
189
206
|
0 = allow
|
|
@@ -200,6 +217,7 @@ Examples:
|
|
|
200
217
|
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
201
218
|
npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
202
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
|
|
203
221
|
npm run scan -- examples/risky-skill
|
|
204
222
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
205
223
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -307,6 +325,14 @@ function parseCommand(values) {
|
|
|
307
325
|
};
|
|
308
326
|
}
|
|
309
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
|
+
|
|
310
336
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
311
337
|
const nestedCommand = values[1];
|
|
312
338
|
|
|
@@ -471,8 +497,7 @@ async function decideApproval(options) {
|
|
|
471
497
|
const outputPath = path.resolve(options.outPath ?? `${options.approvalPath}.decisions.jsonl`);
|
|
472
498
|
const decision = createApprovalDecision(approval, options);
|
|
473
499
|
|
|
474
|
-
await
|
|
475
|
-
await fs.appendFile(outputPath, `${JSON.stringify(decision)}\n`);
|
|
500
|
+
await appendApprovalDecision(outputPath, decision);
|
|
476
501
|
|
|
477
502
|
return {
|
|
478
503
|
approval: {
|
|
@@ -487,6 +512,155 @@ async function decideApproval(options) {
|
|
|
487
512
|
};
|
|
488
513
|
}
|
|
489
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
|
+
|
|
490
664
|
function createApprovalDecision(approval, options) {
|
|
491
665
|
const decision = normalizeApprovalDecision(options.decision);
|
|
492
666
|
const status = decision === "approve" ? "approved" : "denied";
|
|
@@ -624,6 +798,25 @@ function printApprovalDecisionResult(result) {
|
|
|
624
798
|
console.log(`Output: ${result.outputPath}`);
|
|
625
799
|
}
|
|
626
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
|
+
|
|
627
820
|
async function readApprovalRequest(approvalPath, id) {
|
|
628
821
|
const resolvedPath = path.resolve(approvalPath);
|
|
629
822
|
const approvals = await readApprovalRequests(resolvedPath);
|
|
@@ -700,6 +893,29 @@ async function writeApprovalWatchState(statePath, sentIds) {
|
|
|
700
893
|
}, null, 2)}\n`);
|
|
701
894
|
}
|
|
702
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
|
+
|
|
703
919
|
function printGateResult(result, options) {
|
|
704
920
|
const decision = result.policy.decision;
|
|
705
921
|
console.log(`ClawGuard gate: ${result.target}`);
|
|
@@ -1026,6 +1242,10 @@ function commandLabel(commandName) {
|
|
|
1026
1242
|
return "Approval decision";
|
|
1027
1243
|
}
|
|
1028
1244
|
|
|
1245
|
+
if (commandName === "approvals-poll-telegram") {
|
|
1246
|
+
return "Telegram approval poll";
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1029
1249
|
if (commandName === "gate") {
|
|
1030
1250
|
return "Gate";
|
|
1031
1251
|
}
|
|
@@ -1580,6 +1800,99 @@ function parseApprovalDecisionOptions(values) {
|
|
|
1580
1800
|
return options;
|
|
1581
1801
|
}
|
|
1582
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
|
+
|
|
1583
1896
|
async function writeReportFile(outputPath, content) {
|
|
1584
1897
|
const resolvedPath = path.resolve(outputPath);
|
|
1585
1898
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|