@denial-web/clawguard 0.1.7 → 0.1.8
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 +10 -0
- package/package.json +1 -1
- package/src/cli.js +319 -5
package/README.md
CHANGED
|
@@ -80,6 +80,18 @@ If you want ClawGuard to own the approval channel separately, send directly thro
|
|
|
80
80
|
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
For an automatic bridge, keep a watcher running. It forwards each new pending approval once and stores sent approval ids in `./.clawguard/approvals.jsonl.sent.json` by default:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use `--once --dry-run` to verify the message flow without sending anything:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
TELEGRAM_BOT_TOKEN=123456:token npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789 --once --dry-run
|
|
93
|
+
```
|
|
94
|
+
|
|
83
95
|
When testing the published package, run `npx` from outside this repository. From inside the ClawGuard source checkout, use the local commands instead:
|
|
84
96
|
|
|
85
97
|
```bash
|
package/docs/INTEGRATION_SPEC.md
CHANGED
|
@@ -68,6 +68,16 @@ TELEGRAM_BOT_TOKEN=123456:token clawguard approvals send ./.clawguard/approvals.
|
|
|
68
68
|
|
|
69
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
70
|
|
|
71
|
+
For long-running installs, ClawGuard can watch the approval queue and forward each new pending request once:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
TELEGRAM_BOT_TOKEN=123456:token clawguard approvals watch ./.clawguard/approvals.jsonl \
|
|
75
|
+
--via telegram \
|
|
76
|
+
--chat-id 123456789
|
|
77
|
+
```
|
|
78
|
+
|
|
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
|
+
|
|
71
81
|
### Skill Folder Scan
|
|
72
82
|
|
|
73
83
|
Command:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ 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"].includes(command)) {
|
|
35
|
+
if (!["scan", "scan-workspace", "gate", "install", "approvals-send", "approvals-watch"].includes(command)) {
|
|
36
36
|
console.error(`Unknown command: ${command}`);
|
|
37
37
|
printHelp();
|
|
38
38
|
process.exit(1);
|
|
@@ -50,6 +50,19 @@ try {
|
|
|
50
50
|
process.exit(0);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
if (command === "approvals-watch") {
|
|
54
|
+
const watchOptions = parseApprovalWatchOptions(optionValues);
|
|
55
|
+
const result = await watchApprovals(watchOptions, {
|
|
56
|
+
onSend: watchOptions.json ? undefined : printApprovalWatchSend
|
|
57
|
+
});
|
|
58
|
+
if (watchOptions.json) {
|
|
59
|
+
console.log(JSON.stringify(result, null, 2));
|
|
60
|
+
} else {
|
|
61
|
+
printApprovalWatchResult(result);
|
|
62
|
+
}
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
const cliOptions = parseOptions(optionValues);
|
|
54
67
|
cliOptions.framework = framework;
|
|
55
68
|
const loadedConfig = await loadConfig(cliOptions.target, cliOptions.configPath);
|
|
@@ -111,6 +124,7 @@ Usage:
|
|
|
111
124
|
clawguard hermes install <path> --to <dir> [--approval-out <path>]
|
|
112
125
|
clawguard approvals send <approval.json|approvals.jsonl> --via openclaw --channel <name> --target <id>
|
|
113
126
|
clawguard approvals send <approval.json|approvals.jsonl> --via telegram --chat-id <id>
|
|
127
|
+
clawguard approvals watch <approvals.jsonl> --via telegram --chat-id <id>
|
|
114
128
|
clawguard scan-workspace <path> [--json] [--policy <preset>]
|
|
115
129
|
npm run scan -- <path>
|
|
116
130
|
|
|
@@ -142,6 +156,9 @@ Options:
|
|
|
142
156
|
--sender-arg <value> Extra argument before the generated sender command. Repeatable.
|
|
143
157
|
--bot-token <token> Telegram bot token. Default: TELEGRAM_BOT_TOKEN.
|
|
144
158
|
--chat-id <id> Telegram chat id. Alias for --target with --via telegram.
|
|
159
|
+
--interval <ms> Approval watch poll interval. Default: 2000.
|
|
160
|
+
--state <path> Approval watch sent-id state file.
|
|
161
|
+
--once Run approval watch once and exit.
|
|
145
162
|
|
|
146
163
|
Gate exit codes:
|
|
147
164
|
0 = allow
|
|
@@ -156,6 +173,7 @@ Examples:
|
|
|
156
173
|
npx @denial-web/clawguard hermes install ./skills/my-skill --to ~/.hermes/skills --approval-out ./.clawguard/approvals.jsonl
|
|
157
174
|
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via openclaw --channel telegram --target 123456789
|
|
158
175
|
npx @denial-web/clawguard approvals send ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
176
|
+
npx @denial-web/clawguard approvals watch ./.clawguard/approvals.jsonl --via telegram --chat-id 123456789
|
|
159
177
|
npm run scan -- examples/risky-skill
|
|
160
178
|
npm run scan -- examples/metadata-mismatch-skill --policy governed --fail-on-policy
|
|
161
179
|
npm run scan -- examples/metadata-mismatch-skill --html clawguard.html
|
|
@@ -247,6 +265,14 @@ function parseCommand(values) {
|
|
|
247
265
|
};
|
|
248
266
|
}
|
|
249
267
|
|
|
268
|
+
if (rawCommand === "approvals" && values[1] === "watch") {
|
|
269
|
+
return {
|
|
270
|
+
command: "approvals-watch",
|
|
271
|
+
framework: undefined,
|
|
272
|
+
optionValues: values.slice(2)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
250
276
|
if (["openclaw", "hermes"].includes(rawCommand)) {
|
|
251
277
|
const nestedCommand = values[1];
|
|
252
278
|
|
|
@@ -282,6 +308,10 @@ function parseCommand(values) {
|
|
|
282
308
|
|
|
283
309
|
async function sendApproval(options) {
|
|
284
310
|
const approval = await readApprovalRequest(options.approvalPath, options.id);
|
|
311
|
+
return sendApprovalRequest(approval, options);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function sendApprovalRequest(approval, options) {
|
|
285
315
|
const message = String(approval.message ?? "").trim();
|
|
286
316
|
|
|
287
317
|
if (!message) {
|
|
@@ -341,6 +371,67 @@ async function sendApproval(options) {
|
|
|
341
371
|
return result;
|
|
342
372
|
}
|
|
343
373
|
|
|
374
|
+
async function watchApprovals(options, hooks = {}) {
|
|
375
|
+
const statePath = path.resolve(options.statePath ?? `${options.approvalPath}.sent.json`);
|
|
376
|
+
const persistedIds = await readApprovalWatchState(statePath);
|
|
377
|
+
const sessionIds = new Set();
|
|
378
|
+
const result = {
|
|
379
|
+
approvalPath: path.resolve(options.approvalPath),
|
|
380
|
+
statePath,
|
|
381
|
+
once: options.once,
|
|
382
|
+
intervalMs: options.intervalMs,
|
|
383
|
+
dryRun: options.dryRun,
|
|
384
|
+
checked: 0,
|
|
385
|
+
matched: 0,
|
|
386
|
+
sent: 0,
|
|
387
|
+
skipped: 0,
|
|
388
|
+
deliveries: []
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
do {
|
|
392
|
+
const approvals = await readApprovalRequestsIfPresent(options.approvalPath);
|
|
393
|
+
result.checked += approvals.length;
|
|
394
|
+
|
|
395
|
+
for (const approval of approvals) {
|
|
396
|
+
if (approval.status !== "pending") {
|
|
397
|
+
result.skipped += 1;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!approval.id) {
|
|
402
|
+
result.skipped += 1;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (persistedIds.has(approval.id) || sessionIds.has(approval.id)) {
|
|
407
|
+
result.skipped += 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
result.matched += 1;
|
|
412
|
+
const delivery = await sendApprovalRequest(approval, options);
|
|
413
|
+
result.deliveries.push(delivery);
|
|
414
|
+
sessionIds.add(approval.id);
|
|
415
|
+
|
|
416
|
+
if (delivery.sent) {
|
|
417
|
+
result.sent += 1;
|
|
418
|
+
persistedIds.add(approval.id);
|
|
419
|
+
await writeApprovalWatchState(statePath, persistedIds);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (hooks.onSend) {
|
|
423
|
+
hooks.onSend(delivery);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (options.once) {
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await sleep(options.intervalMs);
|
|
432
|
+
} while (true);
|
|
433
|
+
}
|
|
434
|
+
|
|
344
435
|
async function sendTelegramApproval(approval, message, options) {
|
|
345
436
|
const botToken = options.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
346
437
|
|
|
@@ -421,12 +512,30 @@ function printApprovalSendResult(result) {
|
|
|
421
512
|
}
|
|
422
513
|
}
|
|
423
514
|
|
|
515
|
+
function printApprovalWatchSend(result) {
|
|
516
|
+
console.log(`ClawGuard approval watch sent: ${result.approval.id}`);
|
|
517
|
+
console.log(`Via: ${result.via}`);
|
|
518
|
+
console.log(`Target: ${result.target}`);
|
|
519
|
+
console.log(`Sent: ${result.sent ? "yes" : "no"}`);
|
|
520
|
+
if (result.dryRun && result.endpoint) {
|
|
521
|
+
console.log(`Endpoint: ${result.endpoint}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function printApprovalWatchResult(result) {
|
|
526
|
+
console.log(`ClawGuard approval watch: ${result.approvalPath}`);
|
|
527
|
+
console.log(`State: ${result.statePath}`);
|
|
528
|
+
console.log(`Once: ${result.once ? "yes" : "no"}`);
|
|
529
|
+
console.log(`Dry run: ${result.dryRun ? "yes" : "no"}`);
|
|
530
|
+
console.log(`Checked: ${result.checked}`);
|
|
531
|
+
console.log(`Matched pending: ${result.matched}`);
|
|
532
|
+
console.log(`Sent: ${result.sent}`);
|
|
533
|
+
console.log(`Skipped: ${result.skipped}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
424
536
|
async function readApprovalRequest(approvalPath, id) {
|
|
425
537
|
const resolvedPath = path.resolve(approvalPath);
|
|
426
|
-
const
|
|
427
|
-
const approvals = resolvedPath.endsWith(".jsonl")
|
|
428
|
-
? content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
|
|
429
|
-
: [JSON.parse(content)];
|
|
538
|
+
const approvals = await readApprovalRequests(resolvedPath);
|
|
430
539
|
|
|
431
540
|
if (approvals.length === 0) {
|
|
432
541
|
throw new Error(`No approval requests found in ${resolvedPath}`);
|
|
@@ -447,6 +556,59 @@ async function readApprovalRequest(approvalPath, id) {
|
|
|
447
556
|
return approval;
|
|
448
557
|
}
|
|
449
558
|
|
|
559
|
+
async function readApprovalRequestsIfPresent(approvalPath) {
|
|
560
|
+
const resolvedPath = path.resolve(approvalPath);
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
return await readApprovalRequests(resolvedPath);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (error.code === "ENOENT") {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function readApprovalRequests(resolvedPath) {
|
|
574
|
+
const content = await fs.readFile(resolvedPath, "utf8");
|
|
575
|
+
const approvals = resolvedPath.endsWith(".jsonl")
|
|
576
|
+
? content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
|
|
577
|
+
: [JSON.parse(content)];
|
|
578
|
+
|
|
579
|
+
for (const approval of approvals) {
|
|
580
|
+
if (approval.schemaVersion !== "clawguard.approval.v1") {
|
|
581
|
+
throw new Error("Unsupported approval request schema.");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return approvals;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function readApprovalWatchState(statePath) {
|
|
589
|
+
try {
|
|
590
|
+
const content = await fs.readFile(statePath, "utf8");
|
|
591
|
+
const state = JSON.parse(content);
|
|
592
|
+
const ids = Array.isArray(state) ? state : state.sentIds;
|
|
593
|
+
return new Set(Array.isArray(ids) ? ids : []);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (error.code === "ENOENT") {
|
|
596
|
+
return new Set();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function writeApprovalWatchState(statePath, sentIds) {
|
|
604
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
605
|
+
await fs.writeFile(statePath, `${JSON.stringify({
|
|
606
|
+
schemaVersion: "clawguard.approval-watch-state.v1",
|
|
607
|
+
updatedAt: new Date().toISOString(),
|
|
608
|
+
sentIds: [...sentIds].sort()
|
|
609
|
+
}, null, 2)}\n`);
|
|
610
|
+
}
|
|
611
|
+
|
|
450
612
|
function printGateResult(result, options) {
|
|
451
613
|
const decision = result.policy.decision;
|
|
452
614
|
console.log(`ClawGuard gate: ${result.target}`);
|
|
@@ -761,6 +923,14 @@ async function assertDestinationAvailable(destination) {
|
|
|
761
923
|
}
|
|
762
924
|
|
|
763
925
|
function commandLabel(commandName) {
|
|
926
|
+
if (commandName === "approvals-send") {
|
|
927
|
+
return "Approval send";
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (commandName === "approvals-watch") {
|
|
931
|
+
return "Approval watch";
|
|
932
|
+
}
|
|
933
|
+
|
|
764
934
|
if (commandName === "gate") {
|
|
765
935
|
return "Gate";
|
|
766
936
|
}
|
|
@@ -801,6 +971,12 @@ function redactTelegramToken(value) {
|
|
|
801
971
|
return String(value).replace(/\/bot[^/]+\/sendMessage$/, "/bot<redacted>/sendMessage");
|
|
802
972
|
}
|
|
803
973
|
|
|
974
|
+
function sleep(ms) {
|
|
975
|
+
return new Promise((resolve) => {
|
|
976
|
+
setTimeout(resolve, ms);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
804
980
|
function gateExitCode(decision) {
|
|
805
981
|
if (decision === "allow") {
|
|
806
982
|
return 0;
|
|
@@ -1079,6 +1255,144 @@ function parseApprovalSendOptions(values) {
|
|
|
1079
1255
|
return options;
|
|
1080
1256
|
}
|
|
1081
1257
|
|
|
1258
|
+
function parseApprovalWatchOptions(values) {
|
|
1259
|
+
const options = {
|
|
1260
|
+
approvalPath: undefined,
|
|
1261
|
+
via: "telegram",
|
|
1262
|
+
channel: undefined,
|
|
1263
|
+
target: undefined,
|
|
1264
|
+
chatId: undefined,
|
|
1265
|
+
botToken: undefined,
|
|
1266
|
+
telegramApiBase: undefined,
|
|
1267
|
+
senderBin: undefined,
|
|
1268
|
+
senderArgs: [],
|
|
1269
|
+
dryRun: false,
|
|
1270
|
+
json: false,
|
|
1271
|
+
once: false,
|
|
1272
|
+
intervalMs: 2000,
|
|
1273
|
+
statePath: undefined
|
|
1274
|
+
};
|
|
1275
|
+
const paths = [];
|
|
1276
|
+
|
|
1277
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1278
|
+
const value = values[index];
|
|
1279
|
+
|
|
1280
|
+
if (value === "--json") {
|
|
1281
|
+
options.json = true;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (value === "--dry-run") {
|
|
1286
|
+
options.dryRun = true;
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (value === "--once") {
|
|
1291
|
+
options.once = true;
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (value === "--interval") {
|
|
1296
|
+
const interval = Number.parseInt(requireNextValue(values, index, "--interval"), 10);
|
|
1297
|
+
if (!Number.isSafeInteger(interval) || interval < 250) {
|
|
1298
|
+
throw new Error("--interval must be an integer of at least 250 milliseconds.");
|
|
1299
|
+
}
|
|
1300
|
+
options.intervalMs = interval;
|
|
1301
|
+
index += 1;
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (value === "--state") {
|
|
1306
|
+
options.statePath = requireNextValue(values, index, "--state");
|
|
1307
|
+
index += 1;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (value === "--via") {
|
|
1312
|
+
options.via = requireNextValue(values, index, "--via");
|
|
1313
|
+
index += 1;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (value === "--channel") {
|
|
1318
|
+
options.channel = requireNextValue(values, index, "--channel");
|
|
1319
|
+
index += 1;
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (value === "--target") {
|
|
1324
|
+
options.target = requireNextValue(values, index, "--target");
|
|
1325
|
+
index += 1;
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (value === "--chat-id") {
|
|
1330
|
+
options.chatId = requireNextValue(values, index, "--chat-id");
|
|
1331
|
+
index += 1;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
if (value === "--bot-token") {
|
|
1336
|
+
options.botToken = requireNextValue(values, index, "--bot-token");
|
|
1337
|
+
index += 1;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (value === "--telegram-api-base") {
|
|
1342
|
+
options.telegramApiBase = requireNextValue(values, index, "--telegram-api-base");
|
|
1343
|
+
index += 1;
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (value === "--sender-bin") {
|
|
1348
|
+
options.senderBin = requireNextValue(values, index, "--sender-bin");
|
|
1349
|
+
index += 1;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (value === "--sender-arg") {
|
|
1354
|
+
options.senderArgs.push(requireNextValue(values, index, "--sender-arg"));
|
|
1355
|
+
index += 1;
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (value.startsWith("--")) {
|
|
1360
|
+
throw new Error(`Unknown option: ${value}`);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
paths.push(value);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
options.approvalPath = paths[0];
|
|
1367
|
+
|
|
1368
|
+
if (!options.approvalPath) {
|
|
1369
|
+
throw new Error("approvals watch requires <approval.jsonl>.");
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (!["openclaw", "telegram"].includes(options.via)) {
|
|
1373
|
+
throw new Error("Invalid --via value. Use one of: openclaw, telegram");
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (options.via === "openclaw" && !options.channel) {
|
|
1377
|
+
throw new Error("approvals watch --via openclaw requires --channel <name>.");
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (options.via === "openclaw" && !options.target) {
|
|
1381
|
+
throw new Error("approvals watch --via openclaw requires --target <id>.");
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (options.via === "telegram") {
|
|
1385
|
+
options.chatId = options.chatId ?? options.target;
|
|
1386
|
+
if (!options.chatId) {
|
|
1387
|
+
throw new Error("approvals watch --via telegram requires --chat-id <id>.");
|
|
1388
|
+
}
|
|
1389
|
+
options.channel = "telegram";
|
|
1390
|
+
options.target = options.chatId;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
return options;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1082
1396
|
async function writeReportFile(outputPath, content) {
|
|
1083
1397
|
const resolvedPath = path.resolve(outputPath);
|
|
1084
1398
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|