@pwddd/skills-scanner 1.0.0-beta.4 → 1.0.0-beta.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.
@@ -2,7 +2,7 @@
2
2
  "id": "skills-scanner",
3
3
  "name": "Skills Scanner",
4
4
  "description": "Security scanner for OpenClaw Skills to detect potential threats",
5
- "version": "1.0.0-beta.4",
5
+ "version": "1.0.0-beta.6",
6
6
  "author": "pwddd",
7
7
  "skills": ["./skills"],
8
8
  "uiHints": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pwddd/skills-scanner",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "OpenClaw Skills security scanner plugin - detect malicious code, data exfiltration, and prompt injection",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -1,237 +1,262 @@
1
- /**
2
- * Cron job management for skills-scanner
3
- *
4
- * 使用 Plugin SDK 提供的 cron store API 来管理定时任务
5
- */
1
+ import { execSync } from "node:child_process";
2
+ import type { Logger } from "@openclaw/plugin-sdk";
6
3
 
7
- import {
8
- loadCronStore,
9
- resolveCronStorePath,
10
- saveCronStore,
11
- } from "openclaw/plugin-sdk/config-runtime";
12
- import type { PluginLogger } from "./types.js";
13
-
14
- // Cron job configuration
15
4
  const CRON_JOB_NAME = "skills-weekly-report";
16
- const CRON_SCHEDULE = "5 12 * * 1"; // 每周一 12:05
5
+ const CRON_SCHEDULE = "5 12 * * 1"; // Every Monday at 12:05
17
6
  const CRON_TIMEZONE = "Asia/Shanghai";
18
7
 
19
8
  export interface CronManagerOptions {
20
- logger: PluginLogger;
21
- config?: any;
9
+ logger: Logger;
10
+ config: any;
22
11
  }
23
12
 
24
13
  /**
25
- * 确保 cron 任务已注册(启动时调用)
14
+ * Detect the correct OpenClaw command (openclaw vs npx openclaw)
26
15
  */
27
- export async function ensureCronJob(
28
- options: CronManagerOptions
29
- ): Promise<void> {
30
- const { logger, config } = options;
16
+ function getOpenClawCommand(): string {
17
+ // 1. Check environment variable
18
+ if (process.env.OPENCLAW_CLI_PATH) {
19
+ return process.env.OPENCLAW_CLI_PATH;
20
+ }
31
21
 
32
- try {
33
- const storePath = resolveCronStorePath(config?.cron?.store);
34
- const store = await loadCronStore(storePath);
22
+ // 2. Check if running via npx
23
+ const argv1 = process.argv[1];
24
+ if (argv1?.includes("npx") || argv1?.includes("_npx")) {
25
+ return "npx openclaw";
26
+ }
35
27
 
36
- // 只通过名称检查是否存在任务(ID 是系统自动生成的 UUID)
37
- const existingJob = store.jobs.find((j) => j.name === CRON_JOB_NAME);
28
+ if (process.env.npm_execpath?.includes("npx")) {
29
+ return "npx openclaw";
30
+ }
38
31
 
39
- if (existingJob) {
40
- // 任务已存在,检查是否需要更新配置
41
- const needsUpdate =
42
- existingJob.schedule.kind !== "cron" ||
43
- (existingJob.schedule as any).expr !== CRON_SCHEDULE ||
44
- (existingJob.schedule as any).tz !== CRON_TIMEZONE;
45
-
46
- if (needsUpdate) {
47
- logger.info("[Cron] 检测到任务配置变更,正在更新...");
48
- existingJob.schedule = {
49
- kind: "cron",
50
- expr: CRON_SCHEDULE,
51
- tz: CRON_TIMEZONE,
52
- };
53
- existingJob.updatedAtMs = Date.now();
54
- await saveCronStore(storePath, store);
55
- logger.info("[Cron] ✅ 任务配置已更新");
56
- } else {
57
- logger.info(`[Cron] ✅ 任务已存在: ${CRON_JOB_NAME}`);
58
- }
59
- return;
60
- }
32
+ // 3. Try global openclaw command
33
+ try {
34
+ execSync("openclaw --version", {
35
+ encoding: "utf-8",
36
+ timeout: 3000,
37
+ stdio: "pipe"
38
+ });
39
+ return "openclaw";
40
+ } catch {
41
+ // openclaw command not available
42
+ }
61
43
 
62
- // 创建新任务(不设置 id,让系统自动生成)
63
- const newJob = {
64
- name: CRON_JOB_NAME,
65
- enabled: true,
66
- schedule: {
67
- kind: "cron" as const,
68
- expr: CRON_SCHEDULE,
69
- tz: CRON_TIMEZONE,
70
- },
71
- sessionTarget: "isolated" as const,
72
- payload: {
73
- kind: "agentTurn" as const,
74
- message: "生成 Skills 安全周报。请扫描所有已配置的 Skills 目录,汇总本周的安全发现,并生成详细报告。",
75
- timeoutSeconds: 600, // 10 分钟超时
76
- },
77
- delivery: {
78
- mode: "announce" as const,
79
- channel: "last" as const,
80
- },
81
- failureAlert: {
82
- after: 2,
83
- channel: "last" as const,
84
- mode: "announce" as const,
85
- },
86
- createdAtMs: Date.now(),
87
- state: {},
88
- };
89
-
90
- store.jobs.push(newJob as any);
91
- await saveCronStore(storePath, store);
92
-
93
- logger.info(`[Cron] ✅ 定时任务已注册: ${CRON_JOB_NAME}`);
94
- logger.info(`[Cron] 调度: ${CRON_SCHEDULE} (${CRON_TIMEZONE})`);
95
- } catch (err: any) {
96
- logger.error(`[Cron] ❌ 注册定时任务失败: ${err.message}`);
97
- throw err;
44
+ // 4. Try npx as fallback
45
+ try {
46
+ execSync("npx openclaw --version", {
47
+ encoding: "utf-8",
48
+ timeout: 5000,
49
+ stdio: "pipe"
50
+ });
51
+ return "npx openclaw";
52
+ } catch {
53
+ // npx also not available
98
54
  }
55
+
56
+ // 5. Default to openclaw (will fail with clear error)
57
+ return "openclaw";
99
58
  }
100
59
 
101
60
  /**
102
- * 清理 cron 任务(卸载时调用)
61
+ * Ensure cron job exists (create if not exists, update if config changed)
103
62
  */
104
- export async function cleanupCronJob(
105
- options: CronManagerOptions
106
- ): Promise<void> {
107
- const { logger, config } = options;
63
+ export async function ensureCronJob(options: CronManagerOptions): Promise<void> {
64
+ const { logger } = options;
65
+ const openclawCmd = getOpenClawCommand();
108
66
 
67
+ logger.info(`[skills-scanner] Using CLI command: ${openclawCmd}`);
68
+
69
+ // Test if command is available
109
70
  try {
110
- const storePath = resolveCronStorePath(config?.cron?.store);
111
- const store = await loadCronStore(storePath);
71
+ const testResult = execSync(`${openclawCmd} --version`, {
72
+ encoding: "utf-8",
73
+ timeout: 5000,
74
+ stdio: "pipe"
75
+ });
76
+ logger.info(`[skills-scanner] Command test successful: ${testResult.trim()}`);
77
+ } catch (testErr: any) {
78
+ logger.error(`[skills-scanner] ❌ Command not available: ${testErr.message}`);
79
+ logger.info(`[skills-scanner] 💡 Please ensure OpenClaw is installed and accessible`);
80
+ return;
81
+ }
82
+
83
+ try {
84
+ // Get cron list output (text format)
85
+ const listResult = execSync(`${openclawCmd} cron list`, {
86
+ encoding: "utf-8",
87
+ timeout: 5000,
88
+ });
89
+
90
+ // Parse text output to find existing jobs
91
+ const lines = listResult.split("\n");
92
+ const existingJobs: Array<{ id: string; name: string }> = [];
112
93
 
113
- const before = store.jobs.length;
94
+ for (const line of lines) {
95
+ if (line.includes(CRON_JOB_NAME)) {
96
+ const uuidMatch = line.match(/^([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/);
97
+ if (uuidMatch) {
98
+ existingJobs.push({ id: uuidMatch[1], name: CRON_JOB_NAME });
99
+ }
100
+ }
101
+ }
114
102
 
115
- // 只删除名称为 skills-weekly-report 的任务
116
- store.jobs = store.jobs.filter((j) => j.name !== CRON_JOB_NAME);
103
+ // If multiple jobs exist with the same name, remove duplicates
104
+ if (existingJobs.length > 1) {
105
+ logger.warn(`[skills-scanner] ⚠️ Found ${existingJobs.length} duplicate jobs, cleaning up...`);
117
106
 
118
- const removed = before - store.jobs.length;
107
+ // Keep the first one, remove the rest
108
+ for (let i = 1; i < existingJobs.length; i++) {
109
+ const jobId = existingJobs[i].id;
110
+ try {
111
+ execSync(`${openclawCmd} cron remove ${jobId}`, {
112
+ encoding: "utf-8",
113
+ timeout: 5000,
114
+ });
115
+ logger.info(`[skills-scanner] ✅ Removed duplicate job: ${jobId}`);
116
+ } catch (removeErr: any) {
117
+ logger.warn(`[skills-scanner] ⚠️ Failed to remove duplicate job ${jobId}: ${removeErr.message}`);
118
+ }
119
+ }
120
+ }
119
121
 
120
- if (removed > 0) {
121
- await saveCronStore(storePath, store);
122
- logger.info(`[Cron] ✅ 已清理 ${removed} 个定时任务`);
122
+ // Check if we have an existing job (after cleanup)
123
+ const existingJob = existingJobs.length > 0 ? existingJobs[0] : null;
124
+
125
+ if (existingJob) {
126
+ const jobId = existingJob.id;
127
+ logger.info(`[skills-scanner] ✅ Job already exists: ${jobId}`);
128
+ return;
129
+ }
130
+
131
+ logger.info("[skills-scanner] 📝 Creating cron job via CLI...");
132
+
133
+ // Create cron job with --announce and --channel last
134
+ const cronCmd = [
135
+ `${openclawCmd} cron add`,
136
+ `--name "${CRON_JOB_NAME}"`,
137
+ `--cron "${CRON_SCHEDULE}"`,
138
+ `--tz "${CRON_TIMEZONE}"`,
139
+ "--session isolated",
140
+ '--message "请执行 /skills-scanner scan --report 并将结果发送到本频道"',
141
+ "--announce",
142
+ "--channel last",
143
+ ].join(" ");
144
+
145
+ logger.info(`[skills-scanner] Executing: ${cronCmd}`);
146
+
147
+ const result = execSync(cronCmd, { encoding: "utf-8", timeout: 10000 });
148
+
149
+ const jobIdMatch =
150
+ result.match(/Job ID[:\s]+([a-zA-Z0-9-]+)/i) ||
151
+ result.match(/jobId[:\s]+([a-zA-Z0-9-]+)/i) ||
152
+ result.match(/id[:\s]+([a-zA-Z0-9-]+)/i);
153
+
154
+ if (jobIdMatch) {
155
+ const cronJobId = jobIdMatch[1];
156
+ logger.info(`[skills-scanner] ✅ Job created successfully: ${cronJobId}`);
157
+ logger.info(`[skills-scanner] 📅 Schedule: Every Monday at 12:05 (${CRON_TIMEZONE})`);
158
+ logger.info("[skills-scanner] 📬 Reports will be delivered to the last active channel");
123
159
  } else {
124
- logger.info("[Cron] 没有需要清理的定时任务");
160
+ logger.info("[skills-scanner] ✅ Job creation command executed");
161
+ logger.debug(`[skills-scanner] Output: ${result.trim()}`);
125
162
  }
126
163
  } catch (err: any) {
127
- logger.error(`[Cron] 清理定时任务失败: ${err.message}`);
128
- throw err;
164
+ logger.warn("[skills-scanner] ⚠️ Auto-registration failed");
165
+ logger.warn(`[skills-scanner] Error: ${err.message || err}`);
166
+
167
+ if (err.stderr) {
168
+ logger.warn(`[skills-scanner] stderr: ${err.stderr}`);
169
+ }
170
+ if (err.stdout) {
171
+ logger.warn(`[skills-scanner] stdout: ${err.stdout}`);
172
+ }
173
+
174
+ if (err.message.includes("permission") || err.message.includes("EACCES")) {
175
+ logger.error("[skills-scanner] ❌ Permission denied, please run with admin privileges");
176
+ } else if (
177
+ err.message.includes("command not found") ||
178
+ err.message.includes("ENOENT")
179
+ ) {
180
+ logger.error(`[skills-scanner] ❌ ${openclawCmd} command not found, please check installation`);
181
+ } else {
182
+ logger.info("[skills-scanner] 💡 Please manually register cron job:");
183
+ logger.info("[skills-scanner]");
184
+ logger.info(`[skills-scanner] ${openclawCmd} cron add \\`);
185
+ logger.info(`[skills-scanner] --name "${CRON_JOB_NAME}" \\`);
186
+ logger.info(`[skills-scanner] --cron "${CRON_SCHEDULE}" \\`);
187
+ logger.info(`[skills-scanner] --tz "${CRON_TIMEZONE}" \\`);
188
+ logger.info("[skills-scanner] --session isolated \\");
189
+ logger.info('[skills-scanner] --message "请执行 /skills-scanner scan --report 并将结果发送到本频道" \\');
190
+ logger.info("[skills-scanner] --announce \\");
191
+ logger.info("[skills-scanner] --channel last");
192
+ logger.info("[skills-scanner]");
193
+ }
129
194
  }
130
195
  }
131
196
 
132
197
  /**
133
- * 获取 cron 任务状态
198
+ * Cleanup cron job (called on plugin uninstall)
134
199
  */
135
- export async function getCronJobStatus(
136
- options: CronManagerOptions
137
- ): Promise<string> {
138
- const { logger, config } = options;
200
+ export async function cleanupCronJob(options: CronManagerOptions): Promise<void> {
201
+ const { logger } = options;
202
+ const openclawCmd = getOpenClawCommand();
203
+
204
+ logger.info(`[skills-scanner] 🗑️ Cleaning up cron job: ${CRON_JOB_NAME}`);
139
205
 
140
206
  try {
141
- const storePath = resolveCronStorePath(config?.cron?.store);
142
- const store = await loadCronStore(storePath);
143
-
144
- const job = store.jobs.find((j) => j.name === CRON_JOB_NAME);
145
-
146
- if (!job) {
147
- return [
148
- "❌ *定时任务状态*",
149
- "",
150
- "任务未注册。",
151
- "",
152
- "请重启插件或手动注册:",
153
- "```bash",
154
- "/skills-scanner cron setup",
155
- "```",
156
- ].join("\n");
207
+ // Get cron list output (text format)
208
+ const listResult = execSync(`${openclawCmd} cron list`, {
209
+ encoding: "utf-8",
210
+ timeout: 5000,
211
+ });
212
+
213
+ if (!listResult.includes(CRON_JOB_NAME)) {
214
+ logger.info(`[skills-scanner] No cron job to remove: ${CRON_JOB_NAME}`);
215
+ return;
157
216
  }
158
217
 
159
- const nextRun = job.state?.nextRunAtMs
160
- ? new Date(job.state.nextRunAtMs).toLocaleString("zh-CN", {
161
- timeZone: CRON_TIMEZONE,
162
- })
163
- : "未知";
164
-
165
- const lastRun = job.state?.lastRunAtMs
166
- ? new Date(job.state.lastRunAtMs).toLocaleString("zh-CN", {
167
- timeZone: CRON_TIMEZONE,
168
- })
169
- : "从未运行";
170
-
171
- const lastStatus = job.state?.lastRunStatus || "未知";
172
-
173
- return [
174
- "✅ *定时任务状态*",
175
- "",
176
- `• **任务 ID**: \`${(job as any).id}\``,
177
- `• **任务名称**: ${job.name}`,
178
- `• **状态**: ${job.enabled ? "✅ 已启用" : "❌ 已禁用"}`,
179
- `• **调度**: \`${CRON_SCHEDULE}\` (${CRON_TIMEZONE})`,
180
- `• **下次运行**: ${nextRun}`,
181
- `• **上次运行**: ${lastRun}`,
182
- `• **上次状态**: ${lastStatus}`,
183
- "",
184
- "管理命令:",
185
- "```bash",
186
- `openclaw cron list | grep ${CRON_JOB_NAME}`,
187
- `openclaw cron run ${(job as any).id}`,
188
- `openclaw cron update ${(job as any).id} --enabled false`,
189
- "```",
190
- ].join("\n");
191
- } catch (err: any) {
192
- logger.error(`[Cron] ❌ 获取任务状态失败: ${err.message}`);
193
- return `❌ 获取任务状态失败: ${err.message}`;
194
- }
195
- }
218
+ // Parse text output to extract job IDs
219
+ // Format: ID (UUID) followed by Name
220
+ const lines = listResult.split("\n");
221
+ const jobIdsToRemove: string[] = [];
196
222
 
197
- /**
198
- * 手动触发任务(用于测试)
199
- */
200
- export async function triggerCronJob(
201
- options: CronManagerOptions
202
- ): Promise<string> {
203
- const { logger, config } = options;
223
+ for (const line of lines) {
224
+ // Check if line contains the job name
225
+ if (line.includes(CRON_JOB_NAME)) {
226
+ // Extract UUID from the beginning of the line
227
+ // UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
228
+ const uuidMatch = line.match(/^([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/);
229
+ if (uuidMatch) {
230
+ jobIdsToRemove.push(uuidMatch[1]);
231
+ }
232
+ }
233
+ }
204
234
 
205
- try {
206
- const storePath = resolveCronStorePath(config?.cron?.store);
207
- const store = await loadCronStore(storePath);
235
+ if (jobIdsToRemove.length === 0) {
236
+ logger.info(`[skills-scanner] No cron job to remove: ${CRON_JOB_NAME}`);
237
+ return;
238
+ }
208
239
 
209
- const job = store.jobs.find((j) => j.name === CRON_JOB_NAME);
240
+ logger.info(`[skills-scanner] Found ${jobIdsToRemove.length} job(s) to remove`);
210
241
 
211
- if (!job) {
212
- return [
213
- "❌ *手动触发任务*",
214
- "",
215
- "任务未找到,请先注册任务。",
216
- ].join("\n");
242
+ // Remove all matching jobs
243
+ let successCount = 0;
244
+ for (const jobId of jobIdsToRemove) {
245
+ try {
246
+ execSync(`${openclawCmd} cron remove ${jobId}`, {
247
+ encoding: "utf-8",
248
+ timeout: 5000,
249
+ });
250
+ logger.info(`[skills-scanner] ✅ Removed cron job: ${jobId}`);
251
+ successCount++;
252
+ } catch (removeErr: any) {
253
+ logger.warn(`[skills-scanner] ⚠️ Failed to remove job ${jobId}: ${removeErr.message}`);
254
+ }
217
255
  }
218
256
 
219
- return [
220
- "💡 *手动触发任务*",
221
- "",
222
- "请使用 CLI 命令手动触发:",
223
- "```bash",
224
- `openclaw cron run ${(job as any).id} --force`,
225
- "```",
226
- "",
227
- "或者直接运行报告生成:",
228
- "```bash",
229
- "/skills-scanner scan --report",
230
- "```",
231
- ].join("\n");
257
+ logger.info(`[skills-scanner] ✅ Cleanup complete, removed ${successCount}/${jobIdsToRemove.length} job(s)`);
232
258
  } catch (err: any) {
233
- logger.error(`[Cron] ❌ 获取任务信息失败: ${err.message}`);
234
- return `❌ 获取任务信息失败: ${err.message}`;
259
+ logger.error(`[skills-scanner] ❌ Failed to cleanup cron job: ${err.message}`);
235
260
  }
236
261
  }
237
262