@pwddd/skills-scanner 1.0.0-beta.1

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/index.ts ADDED
@@ -0,0 +1,647 @@
1
+ /**
2
+ * OpenClaw Plugin: skills-scanner
3
+ *
4
+ * Security scanner for OpenClaw Skills to detect potential threats.
5
+ */
6
+
7
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
8
+ import { join } from "node:path";
9
+ import os from "node:os";
10
+ import { existsSync } from "node:fs";
11
+ import type { ScannerConfig } from "./src/types.js";
12
+ import { skillsScannerConfigSchema, generateConfigGuide } from "./src/config.js";
13
+ import {
14
+ loadState,
15
+ saveState,
16
+ expandPath,
17
+ defaultScanDirs,
18
+ isFirstRun,
19
+ markConfigReviewed,
20
+ getStateDir,
21
+ } from "./src/state.js";
22
+ import { runScan } from "./src/scanner.js";
23
+ import { buildDailyReport } from "./src/report.js";
24
+ import { ensureCronJobViaGateway, checkCronJobStatus } from "./src/cron-manager.js";
25
+ import { startWatcher } from "./src/watcher.js";
26
+ import { createCommandHandlers } from "./src/commands.js";
27
+ import { SKILLS_SECURITY_GUIDANCE } from "./src/prompt-guidance.js";
28
+ import { PROMPT_INJECTION_GUARD } from "./src/prompt-injection-guard.js";
29
+ import { HIGH_RISK_OPERATION_GUARD } from "./src/high-risk-operation-guard.js";
30
+ import { handleBeforeInstall } from "./src/before-install-hook.js";
31
+ import type { BeforeInstallEvent } from "./src/before-install-hook.js";
32
+ import { validateConfig } from "./src/config-validator.js";
33
+ import { getMetricsSummary } from "./src/metrics.js";
34
+ import { debugLog, isDebugMode } from "./src/debug.js";
35
+
36
+ // Constants
37
+ const PLUGIN_ROOT = process.env.OPENCLAW_PLUGIN_ROOT || __dirname;
38
+
39
+ export default definePluginEntry({
40
+ id: "skills-scanner",
41
+ name: "Skills Scanner",
42
+ description: "Security scanner for OpenClaw Skills to detect potential threats",
43
+ configSchema: skillsScannerConfigSchema,
44
+ register(api) {
45
+ // Get state directory using official API
46
+ const STATE_DIR = getStateDir(api.runtime);
47
+ const QUARANTINE_DIR = join(STATE_DIR, "quarantine");
48
+
49
+ const cfg: ScannerConfig =
50
+ api.config?.plugins?.entries?.["skills-scanner"]?.config ?? {};
51
+
52
+ // Validate configuration
53
+ const validation = validateConfig(cfg, api.logger);
54
+ if (!validation.valid) {
55
+ api.logger.error("[skills-scanner] ❌ Invalid configuration, plugin may not work correctly");
56
+ // Continue loading but with warnings
57
+ }
58
+
59
+ const apiUrl = cfg.apiUrl ?? "https://110.vemic.com/skills-scanner";
60
+ const scanDirs =
61
+ (cfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
62
+ ? cfg.scanDirs!.map(expandPath)
63
+ : defaultScanDirs();
64
+ const behavioral = cfg.behavioral ?? false;
65
+ const useLLM = cfg.useLLM ?? false;
66
+ const policy = cfg.policy ?? "balanced";
67
+ const preInstallScan = cfg.preInstallScan ?? "on";
68
+ const onUnsafe = cfg.onUnsafe ?? "warn";
69
+ const injectSecurityGuidance = cfg.injectSecurityGuidance ?? true;
70
+ const enablePromptInjectionGuard = cfg.enablePromptInjectionGuard ?? false;
71
+ const enableHighRiskOperationGuard = cfg.enableHighRiskOperationGuard ?? false;
72
+ const enableBeforeInstallHook = cfg.enableBeforeInstallHook ?? true;
73
+
74
+ api.logger.info("[skills-scanner] ═══════════════════════════════════════");
75
+ api.logger.info("[skills-scanner] Plugin loading...");
76
+ api.logger.info(`[skills-scanner] API URL: ${apiUrl}`);
77
+ api.logger.info(`[skills-scanner] Scan directories: ${scanDirs.join(", ")}`);
78
+ api.logger.info(`[skills-scanner] Before-install hook: ${enableBeforeInstallHook ? "✅ ENABLED" : "❌ DISABLED"}`);
79
+
80
+ if (isDebugMode()) {
81
+ api.logger.info("[skills-scanner] 🐛 DEBUG MODE ENABLED");
82
+ debugLog(api.logger, "Full configuration", cfg);
83
+ }
84
+
85
+ // Inject system prompt guidance (can be disabled via config)
86
+ if (injectSecurityGuidance) {
87
+ // Build combined guidance
88
+ const guidanceParts = [SKILLS_SECURITY_GUIDANCE];
89
+
90
+ if (enablePromptInjectionGuard) {
91
+ guidanceParts.push(PROMPT_INJECTION_GUARD);
92
+ }
93
+
94
+ if (enableHighRiskOperationGuard) {
95
+ guidanceParts.push(HIGH_RISK_OPERATION_GUARD);
96
+ }
97
+
98
+ const combinedGuidance = guidanceParts.join("\n\n");
99
+
100
+ api.on("before_prompt_build", async () => ({
101
+ prependSystemContext: combinedGuidance,
102
+ }));
103
+
104
+ api.logger.info("[skills-scanner] ✅ Security guidance injected into system prompt");
105
+ if (enablePromptInjectionGuard) {
106
+ api.logger.info("[skills-scanner] - Prompt injection guard enabled");
107
+ }
108
+ if (enableHighRiskOperationGuard) {
109
+ api.logger.info("[skills-scanner] - High-risk operation guard enabled");
110
+ }
111
+ } else {
112
+ api.logger.info("[skills-scanner] ⏭️ Security guidance injection disabled");
113
+ }
114
+
115
+ // Register before_install hook (CRITICAL SECURITY GATE)
116
+ if (enableBeforeInstallHook) {
117
+ api.on("before_install", async (event: BeforeInstallEvent) => {
118
+ try {
119
+ return await handleBeforeInstall(event, {
120
+ apiUrl,
121
+ behavioral,
122
+ useLLM,
123
+ policy,
124
+ logger: api.logger,
125
+ enabled: enableBeforeInstallHook,
126
+ timeoutMs: cfg.scanTimeoutMs || 30000, // Use configured timeout or 30s default
127
+ });
128
+ } catch (err: any) {
129
+ api.logger.error("[skills-scanner] ❌ before_install hook error", {
130
+ error: err.message,
131
+ stack: err.stack,
132
+ skillPath: event.skillPath,
133
+ });
134
+ // Return safe default on error - allow installation but log the failure
135
+ return { block: false };
136
+ }
137
+ });
138
+
139
+ api.logger.info("[skills-scanner] 🛡️ before_install hook registered (installation interception active)");
140
+ } else {
141
+ api.logger.warn("[skills-scanner] ⚠️ before_install hook DISABLED - installations will NOT be intercepted!");
142
+ }
143
+
144
+ // Register gateway_start hook for automatic cron job registration
145
+ api.on("gateway_start", async () => {
146
+ try {
147
+ api.logger.info("[skills-scanner] 🚀 Gateway started, checking cron job...");
148
+ await ensureCronJobViaGateway({
149
+ logger: api.logger,
150
+ callGateway: async (method: string, params: any) => {
151
+ return await api.callGateway(method, params);
152
+ },
153
+ });
154
+ api.logger.info("[skills-scanner] ✅ Cron job check completed");
155
+ } catch (err: any) {
156
+ api.logger.error("[skills-scanner] ❌ Cron job registration failed", {
157
+ error: err.message,
158
+ stack: err.stack,
159
+ });
160
+ // Don't throw - avoid blocking gateway startup
161
+ }
162
+ });
163
+
164
+ // Register plugin_uninstall hook for cleanup
165
+ api.on("plugin_uninstall", async () => {
166
+ api.logger.info("[skills-scanner] 🗑️ Plugin uninstalling, cleaning up...");
167
+
168
+ try {
169
+ // 1. Stop file watcher
170
+ if (stopWatcher) {
171
+ api.logger.debug("[skills-scanner] Stopping file watcher...");
172
+ stopWatcher();
173
+ stopWatcher = null;
174
+ api.logger.debug("[skills-scanner] ✅ File watcher stopped");
175
+ }
176
+
177
+ // 2. Remove cron jobs
178
+ try {
179
+ const listResult = await api.callGateway("cron.list", {});
180
+ const jobs = listResult?.jobs || [];
181
+ const ourJobs = jobs.filter((j: any) => j.name === "skills-weekly-report");
182
+
183
+ for (const job of ourJobs) {
184
+ const jobId = job.jobId || job.id;
185
+ await api.callGateway("cron.remove", { jobId });
186
+ api.logger.info("[skills-scanner] Removed cron job", { jobId });
187
+ }
188
+
189
+ if (ourJobs.length > 0) {
190
+ api.logger.info(`[skills-scanner] ✅ Removed ${ourJobs.length} cron job(s)`);
191
+ }
192
+ } catch (err: any) {
193
+ api.logger.warn("[skills-scanner] Failed to remove cron jobs", {
194
+ error: err.message,
195
+ });
196
+ }
197
+
198
+ // 3. Save final state
199
+ try {
200
+ const finalState = loadState(api.runtime);
201
+ finalState.lastUninstallAt = new Date().toISOString();
202
+ saveState(finalState, api.runtime);
203
+ api.logger.debug("[skills-scanner] ✅ Final state saved");
204
+ } catch (err: any) {
205
+ api.logger.warn("[skills-scanner] Failed to save final state", {
206
+ error: err.message,
207
+ });
208
+ }
209
+
210
+ api.logger.info("[skills-scanner] ✅ Cleanup completed successfully");
211
+ } catch (err: any) {
212
+ api.logger.error("[skills-scanner] ❌ Cleanup failed", {
213
+ error: err.message,
214
+ stack: err.stack,
215
+ });
216
+ }
217
+ });
218
+
219
+ // Register config_changed hook for hot reload
220
+ api.on("config_changed", async (newConfig: any) => {
221
+ api.logger.info("[skills-scanner] 🔄 Configuration changed, reloading...");
222
+
223
+ try {
224
+ const newCfg: ScannerConfig =
225
+ newConfig?.plugins?.entries?.["skills-scanner"]?.config ?? {};
226
+
227
+ // Validate new configuration
228
+ const validation = validateConfig(newCfg, api.logger);
229
+ if (!validation.valid) {
230
+ api.logger.error("[skills-scanner] ❌ Invalid new configuration, keeping old config");
231
+ return;
232
+ }
233
+
234
+ // Check what changed
235
+ const apiUrlChanged = newCfg.apiUrl !== cfg.apiUrl;
236
+ const scanDirsChanged = JSON.stringify(newCfg.scanDirs) !== JSON.stringify(cfg.scanDirs);
237
+ const preInstallScanChanged = newCfg.preInstallScan !== cfg.preInstallScan;
238
+
239
+ if (apiUrlChanged) {
240
+ api.logger.info("[skills-scanner] API URL updated", {
241
+ old: cfg.apiUrl,
242
+ new: newCfg.apiUrl,
243
+ });
244
+ // Update global apiUrl variable
245
+ Object.assign(cfg, { apiUrl: newCfg.apiUrl });
246
+ }
247
+
248
+ if (scanDirsChanged || preInstallScanChanged) {
249
+ api.logger.info("[skills-scanner] Scan configuration updated, restarting watcher...");
250
+
251
+ // Stop old watcher
252
+ if (stopWatcher) {
253
+ stopWatcher();
254
+ stopWatcher = null;
255
+ }
256
+
257
+ // Start new watcher with updated config
258
+ const newScanDirs =
259
+ (newCfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
260
+ ? newCfg.scanDirs!.map(expandPath)
261
+ : defaultScanDirs();
262
+
263
+ if (newCfg.preInstallScan === "on" && newScanDirs.length > 0) {
264
+ stopWatcher = startWatcher(
265
+ newScanDirs,
266
+ newCfg.onUnsafe ?? "warn",
267
+ newCfg.behavioral ?? false,
268
+ newCfg.apiUrl ?? apiUrl,
269
+ newCfg.useLLM ?? false,
270
+ newCfg.policy ?? "balanced",
271
+ persistWatcherAlert,
272
+ api.logger,
273
+ QUARANTINE_DIR
274
+ );
275
+ api.logger.info("[skills-scanner] ✅ Watcher restarted with new configuration");
276
+ } else {
277
+ api.logger.info("[skills-scanner] ⏭️ Watcher disabled by new configuration");
278
+ }
279
+
280
+ // Update global config
281
+ Object.assign(cfg, newCfg);
282
+ }
283
+
284
+ api.logger.info("[skills-scanner] ✅ Configuration reload completed");
285
+ } catch (err: any) {
286
+ api.logger.error("[skills-scanner] ❌ Configuration reload failed", {
287
+ error: err.message,
288
+ stack: err.stack,
289
+ });
290
+ }
291
+ });
292
+
293
+ // Check if first run
294
+ const firstRun = isFirstRun(cfg, api.runtime);
295
+ if (firstRun) {
296
+ api.logger.info("[skills-scanner] 🎉 First run detected");
297
+ const configGuide = generateConfigGuide(
298
+ cfg,
299
+ apiUrl,
300
+ scanDirs,
301
+ behavioral,
302
+ useLLM,
303
+ policy,
304
+ preInstallScan,
305
+ onUnsafe
306
+ );
307
+ console.log(configGuide);
308
+ markConfigReviewed(api.runtime);
309
+ }
310
+
311
+ // Helper for watcher alerts
312
+ function persistWatcherAlert(msg: string): void {
313
+ const state = loadState(api.runtime);
314
+ const alerts: string[] = (state as any).pendingAlerts ?? [];
315
+ alerts.push(`[${new Date().toLocaleString("en-US")}] ${msg}`);
316
+ saveState({ ...state, pendingAlerts: alerts } as any, api.runtime);
317
+ api.logger.warn(`[skills-scanner] ${msg}`);
318
+ }
319
+
320
+ // Service: start watcher
321
+ let stopWatcher: (() => void) | null = null;
322
+
323
+ api.registerService({
324
+ id: "skills-scanner-setup",
325
+ start: async () => {
326
+ api.logger.info("[skills-scanner] 🚀 Service starting...");
327
+
328
+ if (preInstallScan === "on" && scanDirs.length > 0) {
329
+ api.logger.info(`[skills-scanner] 📁 Starting file monitoring: ${scanDirs.length} directories`);
330
+ stopWatcher = startWatcher(
331
+ scanDirs,
332
+ onUnsafe,
333
+ behavioral,
334
+ apiUrl,
335
+ useLLM,
336
+ policy,
337
+ persistWatcherAlert,
338
+ api.logger,
339
+ QUARANTINE_DIR
340
+ );
341
+ api.logger.info("[skills-scanner] ✅ File monitoring started");
342
+ } else {
343
+ api.logger.info("[skills-scanner] ⏭️ Pre-install scan disabled");
344
+ }
345
+ },
346
+ stop: async () => {
347
+ api.logger.info("[skills-scanner] 🛑 Service stopping...");
348
+
349
+ try {
350
+ // 1. Stop file watcher
351
+ if (stopWatcher) {
352
+ api.logger.debug("[skills-scanner] Stopping file watcher...");
353
+ stopWatcher();
354
+ stopWatcher = null;
355
+ api.logger.debug("[skills-scanner] ✅ File watcher stopped");
356
+ }
357
+
358
+ // 2. Save final state
359
+ try {
360
+ const finalState = loadState(api.runtime);
361
+ finalState.lastShutdownAt = new Date().toISOString();
362
+ saveState(finalState, api.runtime);
363
+ api.logger.debug("[skills-scanner] ✅ Final state saved");
364
+ } catch (err: any) {
365
+ api.logger.warn("[skills-scanner] Failed to save final state", {
366
+ error: err.message,
367
+ });
368
+ }
369
+
370
+ // 3. Clear pending alerts (optional - keep for next run)
371
+ // This is intentionally commented out to preserve alerts
372
+ // const state = loadState(api.runtime);
373
+ // if ((state as any).pendingAlerts?.length > 0) {
374
+ // saveState({ ...state, pendingAlerts: [] } as any, api.runtime);
375
+ // }
376
+
377
+ api.logger.info("[skills-scanner] ✅ Service stopped cleanly");
378
+ } catch (err: any) {
379
+ api.logger.error("[skills-scanner] ❌ Error during shutdown", {
380
+ error: err.message,
381
+ stack: err.stack,
382
+ });
383
+ }
384
+ },
385
+ });
386
+
387
+ // Health check endpoint
388
+ api.registerHttpRoute({
389
+ method: "GET",
390
+ path: "/health/skills-scanner",
391
+ handler: async (req, res) => {
392
+ try {
393
+ const state = loadState(api.runtime);
394
+
395
+ // Check API availability
396
+ let apiStatus = "unknown";
397
+ try {
398
+ const response = await fetch(`${apiUrl}/health`, {
399
+ signal: AbortSignal.timeout(3000),
400
+ });
401
+ apiStatus = response.ok ? "available" : "unavailable";
402
+ } catch {
403
+ apiStatus = "unavailable";
404
+ }
405
+
406
+ // Get performance metrics
407
+ const metrics = getMetricsSummary(STATE_DIR);
408
+
409
+ const health = {
410
+ status: "healthy",
411
+ plugin: {
412
+ version: api.version || "1.0.0",
413
+ id: api.id,
414
+ name: api.name,
415
+ },
416
+ api: {
417
+ url: apiUrl,
418
+ status: apiStatus,
419
+ },
420
+ watcher: {
421
+ enabled: preInstallScan === "on",
422
+ running: stopWatcher !== null,
423
+ directories: scanDirs.length,
424
+ },
425
+ state: {
426
+ lastScanAt: state.lastScanAt || null,
427
+ lastShutdownAt: state.lastShutdownAt || null,
428
+ pendingAlerts: (state as any).pendingAlerts?.length || 0,
429
+ },
430
+ metrics: {
431
+ totalScans: metrics.totalScans,
432
+ successRate: metrics.successRate.toFixed(2) + "%",
433
+ averageDurationMs: metrics.averageDurationMs,
434
+ lastScanAt: metrics.lastScanAt || null,
435
+ },
436
+ config: {
437
+ policy,
438
+ behavioral,
439
+ useLLM,
440
+ beforeInstallHook: enableBeforeInstallHook,
441
+ },
442
+ timestamp: new Date().toISOString(),
443
+ };
444
+
445
+ res.status(200).json(health);
446
+ } catch (err: any) {
447
+ api.logger.error("[skills-scanner] Health check failed", {
448
+ error: err.message,
449
+ });
450
+
451
+ res.status(503).json({
452
+ status: "unhealthy",
453
+ error: err.message,
454
+ timestamp: new Date().toISOString(),
455
+ });
456
+ }
457
+ },
458
+ });
459
+
460
+ // Command handlers
461
+ const handlers = createCommandHandlers(
462
+ cfg,
463
+ apiUrl,
464
+ scanDirs,
465
+ behavioral,
466
+ useLLM,
467
+ policy,
468
+ preInstallScan,
469
+ onUnsafe,
470
+ api.logger,
471
+ async (method: string, params: any) => {
472
+ return await api.callGateway(method, params);
473
+ }
474
+ );
475
+
476
+ // Chat command: /skills-scanner
477
+ api.registerCommand({
478
+ name: "skills-scanner",
479
+ description: "Skills 安全扫描工具。用法:/skills-scanner <子命令> [参数]",
480
+ acceptsArgs: true,
481
+ requireAuth: true,
482
+ handler: async (ctx: any) => {
483
+ const args = (ctx.args ?? "").trim();
484
+
485
+ if (!args) {
486
+ return {
487
+ text: [
488
+ "🔍 *Skills Scanner - 安全扫描工具*",
489
+ "",
490
+ "可用命令:",
491
+ "• `/skills-scanner scan <路径> [选项]` - 扫描 Skill",
492
+ "• `/skills-scanner scan clawhub <URL> [选项]` - 扫描 ClawHub Skill",
493
+ "• `/skills-scanner health` - 健康检查",
494
+ "• `/skills-scanner config [操作]` - 配置管理",
495
+ "• `/skills-scanner cron [操作]` - 定时任务管理",
496
+ "",
497
+ "扫描选项:",
498
+ "• `--detailed` - 显示详细发现",
499
+ "• `--behavioral` - 启用行为分析",
500
+ "• `--recursive` - 递归扫描子目录",
501
+ "• `--report` - 生成日报格式",
502
+ "",
503
+ "示例:",
504
+ "```",
505
+ "/skills-scanner scan ~/my-skill",
506
+ "/skills-scanner scan ~/skills --recursive",
507
+ "/skills-scanner scan clawhub https://clawhub.ai/username/project",
508
+ "/skills-scanner health",
509
+ "```",
510
+ "",
511
+ "💡 使用 `/skills-scanner help` 查看详细帮助",
512
+ ].join("\n"),
513
+ };
514
+ }
515
+
516
+ const parts = args.split(/\s+/);
517
+ const subCommand = parts[0].toLowerCase();
518
+ const subArgs = parts.slice(1).join(" ");
519
+
520
+ if (subCommand === "scan") {
521
+ return await handlers.handleScanCommand(subArgs);
522
+ } else if (subCommand === "health") {
523
+ return await handlers.handleHealthCommand();
524
+ } else if (subCommand === "config") {
525
+ return await handlers.handleConfigCommand(subArgs);
526
+ } else if (subCommand === "cron") {
527
+ return await handlers.handleCronCommand(subArgs);
528
+ } else if (subCommand === "help" || subCommand === "--help" || subCommand === "-h") {
529
+ return { text: handlers.getHelpText() };
530
+ } else {
531
+ return {
532
+ text: `❌ 未知子命令:${subCommand}\n\n使用 \`/skills-scanner help\` 查看帮助`,
533
+ };
534
+ }
535
+ },
536
+ });
537
+
538
+ // Gateway RPC methods
539
+ api.registerGatewayMethod("skillsScanner.scan", async ({ respond, params }: any) => {
540
+ const { path: p, mode = "scan", recursive = false, detailed = false } = params ?? {};
541
+ if (!p) return respond(false, { error: "Missing path parameter" });
542
+
543
+ try {
544
+ const res = await runScan(mode === "batch" ? "batch" : "scan", expandPath(p), {
545
+ recursive,
546
+ detailed,
547
+ behavioral,
548
+ apiUrl,
549
+ useLLM,
550
+ policy,
551
+ });
552
+ respond(res.exitCode === 0, {
553
+ output: res.output,
554
+ exitCode: res.exitCode,
555
+ is_safe: res.exitCode === 0,
556
+ data: res.data,
557
+ });
558
+ } catch (err: any) {
559
+ respond(false, { error: err.message });
560
+ }
561
+ });
562
+
563
+ api.registerGatewayMethod("skillsScanner.report", async ({ respond }: any) => {
564
+ if (scanDirs.length === 0) return respond(false, { error: "No scan directories found" });
565
+ try {
566
+ const report = await buildDailyReport(
567
+ scanDirs,
568
+ behavioral,
569
+ apiUrl,
570
+ useLLM,
571
+ policy,
572
+ api.logger
573
+ );
574
+ respond(true, { report, state: loadState() });
575
+ } catch (err: any) {
576
+ respond(false, { error: err.message });
577
+ }
578
+ });
579
+
580
+ // CLI commands
581
+ api.registerCli(
582
+ ({ program }: any) => {
583
+ const cmd = program.command("skills-scanner").description("OpenClaw Skills 安全扫描工具");
584
+
585
+ cmd
586
+ .command("scan <path>")
587
+ .description("扫描单个 Skill")
588
+ .option("--detailed", "显示所有发现")
589
+ .option("--behavioral", "启用行为分析")
590
+ .action(async (p: string, opts: any) => {
591
+ const res = await runScan("scan", expandPath(p), {
592
+ ...opts,
593
+ apiUrl,
594
+ useLLM,
595
+ policy,
596
+ });
597
+ console.log(res.output);
598
+ process.exit(res.exitCode);
599
+ });
600
+
601
+ cmd
602
+ .command("batch <directory>")
603
+ .description("批量扫描目录")
604
+ .option("--recursive", "递归扫描子目录")
605
+ .option("--detailed", "显示所有发现")
606
+ .option("--behavioral", "启用行为分析")
607
+ .action(async (d: string, opts: any) => {
608
+ const res = await runScan("batch", expandPath(d), {
609
+ ...opts,
610
+ apiUrl,
611
+ useLLM,
612
+ policy,
613
+ });
614
+ console.log(res.output);
615
+ process.exit(res.exitCode);
616
+ });
617
+
618
+ cmd
619
+ .command("report")
620
+ .description("生成日报")
621
+ .action(async () => {
622
+ const report = await buildDailyReport(
623
+ scanDirs,
624
+ behavioral,
625
+ apiUrl,
626
+ useLLM,
627
+ policy,
628
+ console
629
+ );
630
+ console.log(report);
631
+ });
632
+
633
+ cmd
634
+ .command("health")
635
+ .description("检查 API 服务健康状态")
636
+ .action(async () => {
637
+ const res = await runScan("health", "", { apiUrl });
638
+ console.log(res.output);
639
+ process.exit(res.exitCode);
640
+ });
641
+ },
642
+ { commands: ["skills-scanner"] }
643
+ );
644
+
645
+ api.logger.info("[skills-scanner] ✅ Plugin registered");
646
+ },
647
+ });