@denial-web/clawguard 0.1.4 → 0.1.6
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 +12 -0
- package/docs/INTEGRATION_SPEC.md +29 -0
- package/package.json +1 -1
- package/src/cli.js +335 -1
package/README.md
CHANGED
|
@@ -68,6 +68,18 @@ 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
|
+
If OpenClaw already has messaging configured, ClawGuard can hand the approval message to OpenClaw:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If you want ClawGuard to own the approval channel separately, send directly through Telegram:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
81
|
+
```
|
|
82
|
+
|
|
71
83
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
72
84
|
|
|
73
85
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -39,6 +39,35 @@ trusted skill folder
|
|
|
39
39
|
|
|
40
40
|
If `--approval-out` is set, non-allow decisions create a pending approval JSON payload instead of copying files. With `--approval-mode always`, even allow decisions pause for explicit owner approval. A messaging adapter can forward the `message` field to WhatsApp, Telegram, Slack, Discord, or another owner channel.
|
|
41
41
|
|
|
42
|
+
### Approval Message Delivery
|
|
43
|
+
|
|
44
|
+
Option A uses OpenClaw's native messaging command after OpenClaw is already configured by the user:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
clawguard approvals send ./.clawguard/approvals.jsonl \
|
|
48
|
+
--via openclaw \
|
|
49
|
+
--channel telegram \
|
|
50
|
+
--target 123456789
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The adapter calls:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
openclaw message send --channel telegram --target 123456789 --message "<approval message>"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is the easiest path for OpenClaw users because ClawGuard does not need to own Telegram, WhatsApp, Slack, or Discord credentials.
|
|
60
|
+
|
|
61
|
+
Option B is a ClawGuard-owned Telegram sender:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
TELEGRAM_BOT_TOKEN=123456:token clawguard approvals send ./.clawguard/approvals.jsonl \
|
|
65
|
+
--via telegram \
|
|
66
|
+
--chat-id 123456789
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That path is better when the user wants the approval channel to stay independent from the agent runtime. Use `--dry-run` first to verify the redacted endpoint and message payload before sending.
|
|
70
|
+
|
|
42
71
|
### Skill Folder Scan
|
|
43
72
|
|
|
44
73
|
Command:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
5
6
|
import path from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
6
8
|
import { loadConfig, mergeConfig, parseSize } from "./config.js";
|
|
7
9
|
import { policyShouldFail } from "./policy.js";
|
|
8
10
|
import { createHtmlReport } from "./reporters/html.js";
|
|
@@ -10,6 +12,7 @@ import { createSarifReport } from "./reporters/sarif.js";
|
|
|
10
12
|
import { scanTarget } from "./scanner.js";
|
|
11
13
|
|
|
12
14
|
const args = process.argv.slice(2);
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
13
16
|
const failLevels = ["none", "low", "medium", "high", "critical"];
|
|
14
17
|
const policyPresets = ["personal", "governed", "enterprise"];
|
|
15
18
|
const policyFailDecisions = ["warn", "manual_review", "sandbox_required", "dual_approval", "block"];
|
|
@@ -29,13 +32,24 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
|
29
32
|
const commandContext = parseCommand(args);
|
|
30
33
|
const { command, framework, optionValues } = commandContext;
|
|
31
34
|
|
|
32
|
-
if (!["scan", "scan-workspace", "gate", "install"].includes(command)) {
|
|
35
|
+
if (!["scan", "scan-workspace", "gate", "install", "approvals-send"].includes(command)) {
|
|
33
36
|
console.error(`Unknown command: ${command}`);
|
|
34
37
|
printHelp();
|
|
35
38
|
process.exit(1);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
try {
|
|
42
|
+
if (command === "approvals-send") {
|
|
43
|
+
const sendOptions = parseApprovalSendOptions(optionValues);
|
|
44
|
+
const result = await sendApproval(sendOptions);
|
|
45
|
+
if (sendOptions.json) {
|
|
46
|
+
console.log(JSON.stringify(result, null, 2));
|
|
47
|
+
} else {
|
|
48
|
+
printApprovalSendResult(result);
|
|
49
|
+
}
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
const cliOptions = parseOptions(optionValues);
|
|
40
54
|
cliOptions.framework = framework;
|
|
41
55
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -95,6 +109,8 @@ Usage:
|
|
|
95
109
|
clawguard install <path> --to <dir> [--policy <preset>] [--dry-run]
|
|
96
110
|
clawguard openclaw install <path> --to <dir> [--approval-out <path>]
|
|
97
111
|
clawguard hermes install <path> --to <dir> [--approval-out <path>]
|
|
112
|
+
clawguard approvals send <approval.json|approvals.jsonl> --via openclaw --channel <name> --target <id>
|
|
113
|
+
clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
|
|
98
114
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
99
115
|
npm run scan -- <path>
|
|
100
116
|
|
|
@@ -119,6 +135,13 @@ Options:
|
|
|
119
135
|
--approval-out <path> Write a pending approval JSON request before copying.
|
|
120
136
|
Use .jsonl to append JSON lines for bot/daemon integrations.
|
|
121
137
|
--approval-mode <mode> Approval mode: non-allow, always. Default: non-allow.
|
|
138
|
+
--via <adapter> Approval send adapter: openclaw, telegram.
|
|
139
|
+
--channel <name> Messaging channel for approval send, such as telegram.
|
|
140
|
+
--target <id> Messaging target/chat id for approval send.
|
|
141
|
+
--sender-bin <path> Sender binary. Default for --via openclaw: openclaw.
|
|
142
|
+
--sender-arg <value> Extra argument before the generated sender command. Repeatable.
|
|
143
|
+
--bot-token <token> Telegram bot token. Default: TELEGRAM_BOT_TOKEN.
|
|
144
|
+
--chat-id <id> Telegram chat id. Alias for --target with --via telegram.
|
|
122
145
|
|
|
123
146
|
Gate exit codes:
|
|
124
147
|
0 = allow
|
|
@@ -131,6 +154,8 @@ Examples:
|
|
|
131
154
|
npx @denial-web/clawguard install ./skills/my-skill --to ./.agents/skills --policy governed
|
|
132
155
|
npx @denial-web/clawguard openclaw install ./skills/my-skill --to ./.agents/skills --approval-out ./.clawguard/approvals.jsonl
|
|
133
156
|
npx @denial-web/clawguard hermes install ./skills/my-skill --to ~/.hermes/skills --approval-out ./.clawguard/approvals.jsonl
|
|
157
|
+
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
|
|
158
|
+
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
134
159
|
npm run scan -- examples/risky-skill
|
|
135
160
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
136
161
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -214,6 +239,14 @@ function printHumanResult(result, options) {
|
|
|
214
239
|
function parseCommand(values) {
|
|
215
240
|
const rawCommand = values[0];
|
|
216
241
|
|
|
242
|
+
if (rawCommand === "approvals" && values[1] === "send") {
|
|
243
|
+
return {
|
|
244
|
+
command: "approvals-send",
|
|
245
|
+
framework: undefined,
|
|
246
|
+
optionValues: values.slice(2)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
217
250
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
218
251
|
const nestedCommand = values[1];
|
|
219
252
|
|
|
@@ -247,6 +280,173 @@ function parseCommand(values) {
|
|
|
247
280
|
};
|
|
248
281
|
}
|
|
249
282
|
|
|
283
|
+
async function sendApproval(options) {
|
|
284
|
+
const approval = await readApprovalRequest(options.approvalPath, options.id);
|
|
285
|
+
const message = String(approval.message ?? "").trim();
|
|
286
|
+
|
|
287
|
+
if (!message) {
|
|
288
|
+
throw new Error("Approval request has no message field.");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (options.via === "telegram") {
|
|
292
|
+
return sendTelegramApproval(approval, message, options);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (options.via !== "openclaw") {
|
|
296
|
+
throw new Error("Only --via openclaw or --via telegram is supported right now.");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const senderBin = options.senderBin ?? "openclaw";
|
|
300
|
+
const commandArgs = [
|
|
301
|
+
...options.senderArgs,
|
|
302
|
+
"message",
|
|
303
|
+
"send",
|
|
304
|
+
"--channel",
|
|
305
|
+
options.channel,
|
|
306
|
+
"--target",
|
|
307
|
+
options.target,
|
|
308
|
+
"--message",
|
|
309
|
+
message
|
|
310
|
+
];
|
|
311
|
+
const result = {
|
|
312
|
+
approval: {
|
|
313
|
+
id: approval.id,
|
|
314
|
+
status: approval.status,
|
|
315
|
+
decision: approval.decision,
|
|
316
|
+
risk: approval.risk,
|
|
317
|
+
framework: approval.framework
|
|
318
|
+
},
|
|
319
|
+
via: options.via,
|
|
320
|
+
channel: options.channel,
|
|
321
|
+
target: options.target,
|
|
322
|
+
senderBin,
|
|
323
|
+
command: [senderBin, ...commandArgs],
|
|
324
|
+
dryRun: options.dryRun,
|
|
325
|
+
sent: false,
|
|
326
|
+
stdout: "",
|
|
327
|
+
stderr: ""
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (options.dryRun) {
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const output = await execFileAsync(senderBin, commandArgs, {
|
|
335
|
+
maxBuffer: 1024 * 1024
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
result.sent = true;
|
|
339
|
+
result.stdout = output.stdout;
|
|
340
|
+
result.stderr = output.stderr;
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function sendTelegramApproval(approval, message, options) {
|
|
345
|
+
const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
346
|
+
|
|
347
|
+
if (!botToken) {
|
|
348
|
+
throw new Error("Telegram send requires --bot-token or TELEGRAM_BOT_TOKEN.");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const apiBase = options.telegramApiBase ?? "https://api.telegram.org";
|
|
352
|
+
const endpoint = `${apiBase.replace(/\/$/, "")}/bot${botToken}/sendMessage`;
|
|
353
|
+
const body = {
|
|
354
|
+
chat_id: options.chatId,
|
|
355
|
+
text: message,
|
|
356
|
+
disable_web_page_preview: true
|
|
357
|
+
};
|
|
358
|
+
const result = {
|
|
359
|
+
approval: {
|
|
360
|
+
id: approval.id,
|
|
361
|
+
status: approval.status,
|
|
362
|
+
decision: approval.decision,
|
|
363
|
+
risk: approval.risk,
|
|
364
|
+
framework: approval.framework
|
|
365
|
+
},
|
|
366
|
+
via: "telegram",
|
|
367
|
+
channel: "telegram",
|
|
368
|
+
target: options.chatId,
|
|
369
|
+
endpoint: redactTelegramToken(endpoint),
|
|
370
|
+
request: body,
|
|
371
|
+
dryRun: options.dryRun,
|
|
372
|
+
sent: false,
|
|
373
|
+
response: null
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
if (options.dryRun) {
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const response = await fetch(endpoint, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"content-type": "application/json"
|
|
384
|
+
},
|
|
385
|
+
body: JSON.stringify(body)
|
|
386
|
+
});
|
|
387
|
+
const text = await response.text();
|
|
388
|
+
let payload;
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
payload = text ? JSON.parse(text) : null;
|
|
392
|
+
} catch {
|
|
393
|
+
payload = text;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
result.response = payload;
|
|
397
|
+
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
throw new Error(`Telegram send failed with HTTP ${response.status}: ${text}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
result.sent = true;
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function printApprovalSendResult(result) {
|
|
407
|
+
console.log(`ClawGuard approval send: ${result.approval.id}`);
|
|
408
|
+
console.log(`Via: ${result.via}`);
|
|
409
|
+
console.log(`Channel: ${result.channel}`);
|
|
410
|
+
console.log(`Target: ${result.target}`);
|
|
411
|
+
console.log(`Decision: ${formatDecision(result.approval.decision ?? "unknown")}`);
|
|
412
|
+
console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
|
|
413
|
+
console.log(`Sent: ${result.sent ? "yes" : "no"}`);
|
|
414
|
+
|
|
415
|
+
if (result.dryRun) {
|
|
416
|
+
if (result.command) {
|
|
417
|
+
console.log(`Command: ${result.command.map(shellQuote).join(" ")}`);
|
|
418
|
+
} else if (result.endpoint) {
|
|
419
|
+
console.log(`Endpoint: ${result.endpoint}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function readApprovalRequest(approvalPath, id) {
|
|
425
|
+
const resolvedPath = path.resolve(approvalPath);
|
|
426
|
+
const content = await fs.readFile(resolvedPath, "utf8");
|
|
427
|
+
const approvals = resolvedPath.endsWith(".jsonl")
|
|
428
|
+
? content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
|
|
429
|
+
: [JSON.parse(content)];
|
|
430
|
+
|
|
431
|
+
if (approvals.length === 0) {
|
|
432
|
+
throw new Error(`No approval requests found in ${resolvedPath}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const approval = id
|
|
436
|
+
? approvals.find((candidate) => candidate.id === id)
|
|
437
|
+
: approvals.at(-1);
|
|
438
|
+
|
|
439
|
+
if (!approval) {
|
|
440
|
+
throw new Error(`Approval request not found: ${id}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (approval.schemaVersion !== "clawguard.approval.v1") {
|
|
444
|
+
throw new Error("Unsupported approval request schema.");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return approval;
|
|
448
|
+
}
|
|
449
|
+
|
|
250
450
|
function printGateResult(result, options) {
|
|
251
451
|
const decision = result.policy.decision;
|
|
252
452
|
console.log(`ClawGuard gate: ${result.target}`);
|
|
@@ -588,6 +788,19 @@ function formatDecision(decision) {
|
|
|
588
788
|
return decision.replaceAll("_", " ").toUpperCase();
|
|
589
789
|
}
|
|
590
790
|
|
|
791
|
+
function shellQuote(value) {
|
|
792
|
+
const text = String(value);
|
|
793
|
+
if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) {
|
|
794
|
+
return text;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function redactTelegramToken(value) {
|
|
801
|
+
return String(value).replace(/\/bot[^/]+\/sendMessage$/, "/bot<redacted>/sendMessage");
|
|
802
|
+
}
|
|
803
|
+
|
|
591
804
|
function gateExitCode(decision) {
|
|
592
805
|
if (decision === "allow") {
|
|
593
806
|
return 0;
|
|
@@ -745,6 +958,127 @@ function parseOptions(values) {
|
|
|
745
958
|
return options;
|
|
746
959
|
}
|
|
747
960
|
|
|
961
|
+
function parseApprovalSendOptions(values) {
|
|
962
|
+
const options = {
|
|
963
|
+
approvalPath: undefined,
|
|
964
|
+
id: undefined,
|
|
965
|
+
via: "openclaw",
|
|
966
|
+
channel: undefined,
|
|
967
|
+
target: undefined,
|
|
968
|
+
chatId: undefined,
|
|
969
|
+
botToken: undefined,
|
|
970
|
+
telegramApiBase: undefined,
|
|
971
|
+
senderBin: undefined,
|
|
972
|
+
senderArgs: [],
|
|
973
|
+
dryRun: false,
|
|
974
|
+
json: false
|
|
975
|
+
};
|
|
976
|
+
const paths = [];
|
|
977
|
+
|
|
978
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
979
|
+
const value = values[index];
|
|
980
|
+
|
|
981
|
+
if (value === "--json") {
|
|
982
|
+
options.json = true;
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (value === "--dry-run") {
|
|
987
|
+
options.dryRun = true;
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (value === "--id") {
|
|
992
|
+
options.id = requireNextValue(values, index, "--id");
|
|
993
|
+
index += 1;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (value === "--via") {
|
|
998
|
+
options.via = requireNextValue(values, index, "--via");
|
|
999
|
+
index += 1;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (value === "--channel") {
|
|
1004
|
+
options.channel = requireNextValue(values, index, "--channel");
|
|
1005
|
+
index += 1;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (value === "--target") {
|
|
1010
|
+
options.target = requireNextValue(values, index, "--target");
|
|
1011
|
+
index += 1;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (value === "--chat-id") {
|
|
1016
|
+
options.chatId = requireNextValue(values, index, "--chat-id");
|
|
1017
|
+
index += 1;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (value === "--bot-token") {
|
|
1022
|
+
options.botToken = requireNextValue(values, index, "--bot-token");
|
|
1023
|
+
index += 1;
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (value === "--telegram-api-base") {
|
|
1028
|
+
options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
|
|
1029
|
+
index += 1;
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (value === "--sender-bin") {
|
|
1034
|
+
options.senderBin = requireNextValue(values, index, "--sender-bin");
|
|
1035
|
+
index += 1;
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (value === "--sender-arg") {
|
|
1040
|
+
options.senderArgs.push(requireNextValue(values, index, "--sender-arg"));
|
|
1041
|
+
index += 1;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (value.startsWith("--")) {
|
|
1046
|
+
throw new Error(`Unknown option: ${value}`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
paths.push(value);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
options.approvalPath = paths[0];
|
|
1053
|
+
|
|
1054
|
+
if (!options.approvalPath) {
|
|
1055
|
+
throw new Error("approvals send requires <approval.json|approvals.jsonl>.");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (!["openclaw", "telegram"].includes(options.via)) {
|
|
1059
|
+
throw new Error("Invalid --via value. Use one of: openclaw, telegram");
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (options.via === "openclaw" && !options.channel) {
|
|
1063
|
+
throw new Error("approvals send requires --channel <name>.");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (options.via === "openclaw" && !options.target) {
|
|
1067
|
+
throw new Error("approvals send requires --target <id>.");
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (options.via === "telegram") {
|
|
1071
|
+
options.chatId = options.chatId ?? options.target;
|
|
1072
|
+
if (!options.chatId) {
|
|
1073
|
+
throw new Error("approvals send --via telegram requires --chat-id <id>.");
|
|
1074
|
+
}
|
|
1075
|
+
options.channel = "telegram";
|
|
1076
|
+
options.target = options.chatId;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return options;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
748
1082
|
async function writeReportFile(outputPath, content) {
|
|
749
1083
|
const resolvedPath = path.resolve(outputPath);
|
|
750
1084
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|