@pwddd/skills-scanner 1.0.3

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.
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Command handlers module
3
+ */
4
+
5
+ import { existsSync, execSync } from "node:fs";
6
+ import { join, basename } from "node:path";
7
+ import { promisify } from "node:util";
8
+ import { exec } from "node:child_process";
9
+ import { runScan } from "./scanner.js";
10
+ import { buildDailyReport } from "./report.js";
11
+ import { loadState, saveState, expandPath } from "./state.js";
12
+ import { isVenvReady } from "./deps.js";
13
+ import { generateConfigGuide } from "./config.js";
14
+ import { ensureCronJob } from "./cron.js";
15
+ import type { ScannerConfig } from "./types.js";
16
+
17
+ const execAsync = promisify(exec);
18
+
19
+ export function createCommandHandlers(
20
+ cfg: ScannerConfig,
21
+ apiUrl: string,
22
+ scanDirs: string[],
23
+ behavioral: boolean,
24
+ useLLM: boolean,
25
+ policy: string,
26
+ preInstallScan: string,
27
+ onUnsafe: string,
28
+ venvPython: string,
29
+ scanScript: string,
30
+ logger: any
31
+ ) {
32
+ async function handleScanCommand(args: string): Promise<any> {
33
+ if (!args) {
34
+ return {
35
+ text: "用法:`/skills-scanner scan <路径> [--detailed] [--behavioral] [--recursive] [--report]`\n或:`/skills-scanner scan clawhub <URL> [--detailed] [--behavioral]`",
36
+ };
37
+ }
38
+
39
+ if (!isVenvReady(venvPython)) {
40
+ return { text: "⏳ Python 依赖尚未就绪,请稍后重试或查看日志" };
41
+ }
42
+
43
+ const parts = args.split(/\s+/);
44
+
45
+ // Check if this is a ClawHub scan
46
+ if (parts[0] === "clawhub") {
47
+ const clawhubUrl = parts.find((p) => p.startsWith("https://clawhub.ai/"));
48
+ if (!clawhubUrl) {
49
+ return { text: "❌ 请提供有效的 ClawHub URL (例如: https://clawhub.ai/username/project)" };
50
+ }
51
+
52
+ const detailed = parts.includes("--detailed");
53
+ const useBehav = parts.includes("--behavioral") || behavioral;
54
+
55
+ const res = await runScan(venvPython, scanScript, "clawhub", clawhubUrl, {
56
+ detailed,
57
+ behavioral: useBehav,
58
+ apiUrl,
59
+ useLLM,
60
+ policy,
61
+ });
62
+ const icon = res.exitCode === 0 ? "✅" : "❌";
63
+ return { text: `${icon} ClawHub 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
64
+ }
65
+
66
+ const targetPath = expandPath(parts.find((p) => !p.startsWith("--")) ?? "");
67
+ const detailed = parts.includes("--detailed");
68
+ const useBehav = parts.includes("--behavioral") || behavioral;
69
+ const recursive = parts.includes("--recursive");
70
+ const isReport = parts.includes("--report");
71
+
72
+ if (!targetPath) {
73
+ return { text: "❌ 请指定扫描路径" };
74
+ }
75
+
76
+ if (!existsSync(targetPath)) {
77
+ return { text: `❌ 路径不存在: ${targetPath}` };
78
+ }
79
+
80
+ const isSingleSkill = existsSync(join(targetPath, "SKILL.md"));
81
+
82
+ if (isReport) {
83
+ if (scanDirs.length === 0) {
84
+ return { text: "⚠️ 未找到可扫描目录,请检查配置" };
85
+ }
86
+ const report = await buildDailyReport(
87
+ scanDirs,
88
+ useBehav,
89
+ apiUrl,
90
+ useLLM,
91
+ policy,
92
+ logger,
93
+ venvPython,
94
+ scanScript
95
+ );
96
+ return { text: report };
97
+ } else if (isSingleSkill) {
98
+ const res = await runScan(venvPython, scanScript, "scan", targetPath, {
99
+ detailed,
100
+ behavioral: useBehav,
101
+ apiUrl,
102
+ useLLM,
103
+ policy,
104
+ });
105
+ const icon = res.exitCode === 0 ? "✅" : "❌";
106
+ return { text: `${icon} 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
107
+ } else {
108
+ const res = await runScan(venvPython, scanScript, "batch", targetPath, {
109
+ recursive,
110
+ detailed,
111
+ behavioral: useBehav,
112
+ apiUrl,
113
+ useLLM,
114
+ policy,
115
+ });
116
+ const icon = res.exitCode === 0 ? "✅" : "❌";
117
+ return { text: `${icon} 批量扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
118
+ }
119
+ }
120
+
121
+ async function handleStatusCommand(): Promise<any> {
122
+ const state = loadState() as any;
123
+ const alerts: string[] = state.pendingAlerts ?? [];
124
+
125
+ const lines = [
126
+ "📋 *Skills Scanner 状态*",
127
+ `API 地址: ${apiUrl}`,
128
+ `Python 依赖: ${isVenvReady(venvPython) ? "✅ 就绪" : "❌ 未就绪"}`,
129
+ `安装前扫描: ${preInstallScan === "on" ? `✅ 监听中 (${onUnsafe})` : "❌ 已禁用"}`,
130
+ `扫描策略: ${policy}`,
131
+ `LLM 分析: ${useLLM ? "✅ 启用" : "❌ 禁用"}`,
132
+ `行为分析: ${behavioral ? "✅ 启用" : "❌ 禁用"}`,
133
+ `上次扫描: ${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
134
+ `扫描目录:\n${scanDirs.map((d) => ` • ${d}`).join("\n")}`,
135
+ ];
136
+
137
+ if (isVenvReady(venvPython)) {
138
+ lines.push("", "🔍 *API 服务检查*");
139
+ try {
140
+ const cmd = `"${venvPython}" "${scanScript}" --api-url "${apiUrl}" health`;
141
+ const env = { ...process.env };
142
+ delete env.http_proxy;
143
+ delete env.https_proxy;
144
+ delete env.HTTP_PROXY;
145
+ delete env.HTTPS_PROXY;
146
+ delete env.all_proxy;
147
+ delete env.ALL_PROXY;
148
+
149
+ const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
150
+ const output = (stdout + stderr).trim();
151
+
152
+ if (output.includes("✓") || output.includes("OK")) {
153
+ lines.push(`API 服务: ✅ 正常`);
154
+ const jsonMatch = output.match(/\{[\s\S]*\}/);
155
+ if (jsonMatch) {
156
+ try {
157
+ const healthData = JSON.parse(jsonMatch[0]);
158
+ if (healthData.analyzers_available) {
159
+ lines.push(`可用分析器: ${healthData.analyzers_available.join(", ")}`);
160
+ }
161
+ } catch {}
162
+ }
163
+ } else {
164
+ lines.push(`API 服务: ❌ 不可用`);
165
+ }
166
+ } catch (err: any) {
167
+ lines.push(`API 服务: ❌ 连接失败`);
168
+ lines.push(`错误: ${err.message}`);
169
+ }
170
+ }
171
+
172
+ if (alerts.length > 0) {
173
+ lines.push("", `🔔 *待查告警 (${alerts.length} 条):*`);
174
+ alerts.slice(-5).forEach((a) => lines.push(` ${a}`));
175
+ saveState({ ...state, pendingAlerts: [] });
176
+ }
177
+
178
+ lines.push("", "🕐 *定时任务*");
179
+ if (state.cronJobId && state.cronJobId !== "manual-created") {
180
+ lines.push(`状态: ✅ 已注册 (${state.cronJobId})`);
181
+ } else {
182
+ lines.push("状态: ❌ 未注册");
183
+ lines.push("💡 使用 `/skills-scanner cron register` 注册");
184
+ }
185
+
186
+ return { text: lines.join("\n") };
187
+ }
188
+
189
+ async function handleConfigCommand(args: string): Promise<any> {
190
+ const action = args.trim().toLowerCase() || "show";
191
+
192
+ if (action === "show" || action === "") {
193
+ const configGuide = generateConfigGuide(
194
+ cfg,
195
+ apiUrl,
196
+ scanDirs,
197
+ behavioral,
198
+ useLLM,
199
+ policy,
200
+ preInstallScan,
201
+ onUnsafe
202
+ );
203
+ return { text: "```\n" + configGuide + "\n```" };
204
+ } else if (action === "reset") {
205
+ const state = loadState() as any;
206
+ saveState({ ...state, configReviewed: false });
207
+ return {
208
+ text: "✅ 配置审查标记已重置\n下次重启 Gateway 时将再次显示配置向导",
209
+ };
210
+ } else {
211
+ return { text: "用法: `/skills-scanner config [show|reset]`" };
212
+ }
213
+ }
214
+
215
+ async function handleCronCommand(args: string): Promise<any> {
216
+ const action = args.trim().toLowerCase() || "status";
217
+ const state = loadState() as any;
218
+
219
+ if (action === "register") {
220
+ const oldJobId = state.cronJobId;
221
+ if (oldJobId && oldJobId !== "manual-created") {
222
+ try {
223
+ execSync(`openclaw cron remove ${oldJobId}`, { encoding: "utf-8", timeout: 5000 });
224
+ } catch {}
225
+ }
226
+
227
+ saveState({ ...state, cronJobId: undefined });
228
+ await ensureCronJob(logger);
229
+
230
+ const newState = loadState() as any;
231
+ if (newState.cronJobId) {
232
+ return { text: `✅ 定时任务注册成功\n任务 ID: ${newState.cronJobId}` };
233
+ } else {
234
+ return { text: "❌ 定时任务注册失败,请查看日志" };
235
+ }
236
+ } else if (action === "unregister") {
237
+ if (!state.cronJobId) {
238
+ return { text: "⚠️ 未找到已注册的定时任务" };
239
+ }
240
+
241
+ try {
242
+ execSync(`openclaw cron remove ${state.cronJobId}`, {
243
+ encoding: "utf-8",
244
+ timeout: 5000,
245
+ });
246
+ saveState({ ...state, cronJobId: undefined });
247
+ return { text: `✅ 定时任务已删除: ${state.cronJobId}` };
248
+ } catch (err: any) {
249
+ return { text: `❌ 删除失败: ${err.message}` };
250
+ }
251
+ } else {
252
+ const lines = ["🕐 *定时任务状态*"];
253
+ if (state.cronJobId && state.cronJobId !== "manual-created") {
254
+ lines.push(`任务 ID: ${state.cronJobId}`);
255
+ lines.push(`执行时间: 每天 08:00 (Asia/Shanghai)`);
256
+ lines.push("状态: ✅ 已注册");
257
+ } else {
258
+ lines.push("状态: ❌ 未注册");
259
+ lines.push("", "💡 使用 `/skills-scanner cron register` 注册");
260
+ }
261
+ return { text: lines.join("\n") };
262
+ }
263
+ }
264
+
265
+ function getHelpText(): string {
266
+ return [
267
+ "🔍 *Skills Scanner - 帮助*",
268
+ "",
269
+ "═══ 扫描命令 ═══",
270
+ "`/skills-scanner scan <路径> [选项]`",
271
+ "`/skills-scanner scan clawhub <URL> [选项]`",
272
+ "",
273
+ "选项:",
274
+ "• `--detailed` - 显示详细发现",
275
+ "• `--behavioral` - 启用行为分析",
276
+ "• `--recursive` - 递归扫描子目录",
277
+ "• `--report` - 生成日报格式",
278
+ "",
279
+ "示例:",
280
+ "```",
281
+ "/skills-scanner scan ~/.openclaw/skills/my-skill",
282
+ "/skills-scanner scan ~/.openclaw/skills --recursive",
283
+ "/skills-scanner scan ~/.openclaw/skills --report",
284
+ "/skills-scanner scan clawhub https://clawhub.ai/username/project",
285
+ "/skills-scanner scan clawhub https://clawhub.ai/username/project --detailed",
286
+ "```",
287
+ "",
288
+ "═══ 其他命令 ═══",
289
+ "• `/skills-scanner status` - 查看状态",
290
+ "• `/skills-scanner config [show|reset]` - 配置管理",
291
+ "• `/skills-scanner cron [register|unregister|status]` - 定时任务管理",
292
+ ].join("\n");
293
+ }
294
+
295
+ return {
296
+ handleScanCommand,
297
+ handleStatusCommand,
298
+ handleConfigCommand,
299
+ handleCronCommand,
300
+ getHelpText,
301
+ };
302
+ }
package/src/config.ts ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * 配置管理模块
3
+ */
4
+
5
+ import { Type } from "@sinclair/typebox";
6
+ import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
7
+ import type { ScannerConfig } from "./types.js";
8
+
9
+ export const skillsScannerConfigSchema: OpenClawPluginConfigSchema = {
10
+ safeParse: (value: unknown) => {
11
+ try {
12
+ const config = value as ScannerConfig;
13
+
14
+ // 验证 policy
15
+ if (config.policy && !["strict", "balanced", "permissive"].includes(config.policy)) {
16
+ return {
17
+ success: false,
18
+ error: {
19
+ issues: [{
20
+ path: ["policy"],
21
+ message: "policy 必须是 strict、balanced 或 permissive"
22
+ }]
23
+ }
24
+ };
25
+ }
26
+
27
+ // 验证 preInstallScan
28
+ if (config.preInstallScan && !["on", "off"].includes(config.preInstallScan)) {
29
+ return {
30
+ success: false,
31
+ error: {
32
+ issues: [{
33
+ path: ["preInstallScan"],
34
+ message: "preInstallScan 必须是 on 或 off"
35
+ }]
36
+ }
37
+ };
38
+ }
39
+
40
+ // 验证 onUnsafe
41
+ if (config.onUnsafe && !["quarantine", "delete", "warn"].includes(config.onUnsafe)) {
42
+ return {
43
+ success: false,
44
+ error: {
45
+ issues: [{
46
+ path: ["onUnsafe"],
47
+ message: "onUnsafe 必须是 quarantine、delete 或 warn"
48
+ }]
49
+ }
50
+ };
51
+ }
52
+
53
+ return { success: true, data: config };
54
+ } catch (err) {
55
+ return {
56
+ success: false,
57
+ error: {
58
+ issues: [{
59
+ path: [],
60
+ message: String(err)
61
+ }]
62
+ }
63
+ };
64
+ }
65
+ },
66
+
67
+ uiHints: {
68
+ apiUrl: {
69
+ label: "API 服务地址",
70
+ help: "扫描 API 服务的 URL 地址",
71
+ placeholder: "http://localhost:8000"
72
+ },
73
+ scanDirs: {
74
+ label: "扫描目录",
75
+ help: "要监控的 Skills 目录列表,支持 ~ 路径"
76
+ },
77
+ behavioral: {
78
+ label: "行为分析",
79
+ help: "启用深度行为分析(较慢但更准确)"
80
+ },
81
+ useLLM: {
82
+ label: "LLM 分析",
83
+ help: "使用 LLM 进行语义分析"
84
+ },
85
+ policy: {
86
+ label: "扫描策略",
87
+ help: "strict=严格 / balanced=平衡(推荐)/ permissive=宽松"
88
+ },
89
+ preInstallScan: {
90
+ label: "安装前扫描",
91
+ help: "监听新 Skill 并自动扫描"
92
+ },
93
+ onUnsafe: {
94
+ label: "不安全处理",
95
+ help: "quarantine=隔离(推荐)/ delete=删除 / warn=仅警告"
96
+ }
97
+ }
98
+ };
99
+
100
+ export function generateConfigGuide(
101
+ cfg: ScannerConfig,
102
+ apiUrl: string,
103
+ scanDirs: string[],
104
+ behavioral: boolean,
105
+ useLLM: boolean,
106
+ policy: string,
107
+ preInstallScan: string,
108
+ onUnsafe: string
109
+ ): string {
110
+ return [
111
+ "",
112
+ "╔════════════════════════════════════════════════════════════════╗",
113
+ "║ 🎉 Skills Scanner 首次运行 - 配置向导 ║",
114
+ "╚════════════════════════════════════════════════════════════════╝",
115
+ "",
116
+ "当前使用默认配置。建议根据您的需求自定义配置:",
117
+ "",
118
+ "📋 当前配置:",
119
+ ` • API 服务地址: ${apiUrl}`,
120
+ ` • 扫描目录: ${scanDirs.length} 个(自动检测)`,
121
+ ` • 行为分析: ${behavioral ? "✅ 启用" : "❌ 禁用"}`,
122
+ ` • LLM 分析: ${useLLM ? "✅ 启用" : "❌ 禁用"}`,
123
+ ` • 扫描策略: ${policy}`,
124
+ ` • 安装前扫描: ${preInstallScan === "on" ? "✅ 启用" : "❌ 禁用"}`,
125
+ ` • 不安全处理: ${onUnsafe}`,
126
+ "",
127
+ "🔧 配置文件位置:",
128
+ " ~/.openclaw/config.json",
129
+ "",
130
+ "📝 推荐配置示例:",
131
+ "",
132
+ "```json",
133
+ "{",
134
+ ' "plugins": {',
135
+ ' "entries": {',
136
+ ' "skills-scanner": {',
137
+ ' "enabled": true,',
138
+ ' "config": {',
139
+ ' "apiUrl": "http://localhost:8000",',
140
+ ' "scanDirs": ["~/.openclaw/skills"],',
141
+ ' "behavioral": false,',
142
+ ' "useLLM": false,',
143
+ ' "policy": "balanced",',
144
+ ' "preInstallScan": "on",',
145
+ ' "onUnsafe": "quarantine"',
146
+ ' }',
147
+ ' }',
148
+ ' }',
149
+ ' }',
150
+ "}",
151
+ "```",
152
+ "",
153
+ "💡 配置说明:",
154
+ "",
155
+ "1. apiUrl 默认 http://localhost:8000,需先启动 skill-scanner-api 服务",
156
+ "2. scanDirs 可添加多个目录(默认自动检测 ~/.openclaw/skills)",
157
+ "3. behavioral false=快速扫描(推荐),true=深度分析",
158
+ "4. useLLM false=不使用 LLM(推荐),true=语义分析",
159
+ "5. policy strict / balanced(推荐)/ permissive",
160
+ "6. preInstallScan on=监听新 Skill 并自动扫描(推荐),off=禁用",
161
+ "7. onUnsafe quarantine=隔离(推荐),delete=删除,warn=仅警告",
162
+ "",
163
+ "🚀 快速开始:",
164
+ " 编辑配置文件后重启 Gateway",
165
+ " /skills-scanner status",
166
+ "",
167
+ "提示:此消息只在首次运行时显示。",
168
+ "════════════════════════════════════════════════════════════════",
169
+ ].join("\n");
170
+ }
package/src/cron.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Cron job management module
3
+ */
4
+
5
+ import { execSync } from "node:child_process";
6
+ import { loadState, saveState } from "./state.js";
7
+
8
+ const CRON_JOB_NAME = "skills-daily-report";
9
+ const CRON_SCHEDULE = "0 8 * * *";
10
+ const CRON_TIMEZONE = "Asia/Shanghai";
11
+
12
+ export async function ensureCronJob(logger: any): Promise<void> {
13
+ const state = loadState() as any;
14
+
15
+ logger.info("[skills-scanner] ─────────────────────────────────────");
16
+ logger.info("[skills-scanner] 🕐 Checking cron job...");
17
+
18
+ try {
19
+ let jobs: any[] = [];
20
+ try {
21
+ const listResult = execSync("openclaw cron list --format json", {
22
+ encoding: "utf-8",
23
+ timeout: 5000,
24
+ });
25
+ jobs = JSON.parse(listResult.trim());
26
+ } catch (listErr: any) {
27
+ logger.debug("[skills-scanner] JSON format not supported, trying text parsing");
28
+ try {
29
+ const listResult = execSync("openclaw cron list", {
30
+ encoding: "utf-8",
31
+ timeout: 5000,
32
+ });
33
+ if (listResult.includes(CRON_JOB_NAME)) {
34
+ logger.info(`[skills-scanner] ✅ Found existing job: ${CRON_JOB_NAME}`);
35
+ if (!state.cronJobId) {
36
+ saveState({ ...state, cronJobId: "manual-created" });
37
+ }
38
+ return;
39
+ }
40
+ } catch {
41
+ logger.debug("[skills-scanner] Cannot list cron jobs, may be permission issue");
42
+ }
43
+ }
44
+
45
+ // Find all jobs with the same name (to detect duplicates)
46
+ const existingJobs = jobs.filter(
47
+ (j: any) =>
48
+ j.name === CRON_JOB_NAME ||
49
+ j.jobName === CRON_JOB_NAME
50
+ );
51
+
52
+ // If multiple jobs exist with the same name, remove duplicates
53
+ if (existingJobs.length > 1) {
54
+ logger.warn(`[skills-scanner] ⚠️ Found ${existingJobs.length} duplicate jobs, cleaning up...`);
55
+
56
+ // Keep the first one, remove the rest
57
+ for (let i = 1; i < existingJobs.length; i++) {
58
+ const jobId = existingJobs[i].id || existingJobs[i].jobId;
59
+ try {
60
+ execSync(`openclaw cron remove ${jobId}`, {
61
+ encoding: "utf-8",
62
+ timeout: 5000,
63
+ });
64
+ logger.info(`[skills-scanner] ✅ Removed duplicate job: ${jobId}`);
65
+ } catch (removeErr: any) {
66
+ logger.warn(`[skills-scanner] ⚠️ Failed to remove duplicate job ${jobId}: ${removeErr.message}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ // Check if we have an existing job (after cleanup)
72
+ const existingJob = existingJobs.length > 0 ? existingJobs[0] :
73
+ jobs.find((j: any) => j.id === state.cronJobId);
74
+
75
+ if (existingJob) {
76
+ const jobId = existingJob.id || existingJob.jobId || state.cronJobId;
77
+
78
+ const needsUpdate =
79
+ existingJob.schedule !== CRON_SCHEDULE ||
80
+ existingJob.timezone !== CRON_TIMEZONE;
81
+
82
+ if (needsUpdate) {
83
+ logger.info(`[skills-scanner] 🔄 Job config changed, updating...`);
84
+ try {
85
+ execSync(`openclaw cron remove ${jobId}`, {
86
+ encoding: "utf-8",
87
+ timeout: 5000,
88
+ });
89
+ logger.info(`[skills-scanner] ✅ Removed old job: ${jobId}`);
90
+ } catch (removeErr: any) {
91
+ logger.warn(`[skills-scanner] ⚠️ Failed to remove old job: ${removeErr.message}`);
92
+ if (state.cronJobId !== jobId) {
93
+ saveState({ ...state, cronJobId: jobId });
94
+ }
95
+ logger.info(`[skills-scanner] ✅ Keeping existing job: ${jobId}`);
96
+ return;
97
+ }
98
+ } else {
99
+ if (state.cronJobId !== jobId) {
100
+ saveState({ ...state, cronJobId: jobId });
101
+ logger.info(`[skills-scanner] ✅ Found existing job: ${jobId}`);
102
+ } else {
103
+ logger.info(`[skills-scanner] ✅ Job already exists: ${jobId}`);
104
+ }
105
+ return;
106
+ }
107
+ }
108
+
109
+ logger.info("[skills-scanner] 📝 Creating cron job...");
110
+
111
+ // Create cron job with --announce and --channel last
112
+ // This will deliver to the last place the agent replied
113
+ const cronCmd = [
114
+ "openclaw cron add",
115
+ `--name "${CRON_JOB_NAME}"`,
116
+ `--cron "${CRON_SCHEDULE}"`,
117
+ `--tz "${CRON_TIMEZONE}"`,
118
+ "--session isolated",
119
+ '--message "Please run /skills-scanner scan --report and send results to this channel"',
120
+ "--announce",
121
+ "--channel last",
122
+ ].join(" ");
123
+
124
+ const result = execSync(cronCmd, { encoding: "utf-8", timeout: 10000 });
125
+
126
+ const jobIdMatch =
127
+ result.match(/Job ID[:\s]+([a-zA-Z0-9-]+)/i) ||
128
+ result.match(/jobId[:\s]+([a-zA-Z0-9-]+)/i) ||
129
+ result.match(/id[:\s]+([a-zA-Z0-9-]+)/i);
130
+
131
+ if (jobIdMatch) {
132
+ const cronJobId = jobIdMatch[1];
133
+ saveState({ ...state, cronJobId });
134
+ logger.info(`[skills-scanner] ✅ Job created successfully: ${cronJobId}`);
135
+ logger.info(
136
+ `[skills-scanner] 📅 Schedule: Daily at ${CRON_SCHEDULE.split(" ")[1]}:${CRON_SCHEDULE.split(" ")[0]} (${CRON_TIMEZONE})`
137
+ );
138
+ logger.info("[skills-scanner] 📬 Reports will be delivered to the last active channel");
139
+ } else {
140
+ logger.info("[skills-scanner] ✅ Job creation command executed");
141
+ logger.debug(`[skills-scanner] Output: ${result.trim()}`);
142
+ saveState({ ...state, cronJobId: "created-unknown-id" });
143
+ }
144
+ } catch (err: any) {
145
+ logger.warn("[skills-scanner] ⚠️ Auto-registration failed");
146
+ logger.debug(`[skills-scanner] Error details: ${err.message}`);
147
+
148
+ if (err.message.includes("permission") || err.message.includes("EACCES")) {
149
+ logger.error("[skills-scanner] ❌ Permission denied, please run with admin privileges");
150
+ } else if (
151
+ err.message.includes("command not found") ||
152
+ err.message.includes("ENOENT")
153
+ ) {
154
+ logger.error("[skills-scanner] ❌ openclaw command not found, please check installation");
155
+ } else {
156
+ logger.info("[skills-scanner] 💡 Please manually register cron job:");
157
+ logger.info("[skills-scanner]");
158
+ logger.info("[skills-scanner] openclaw cron add \\");
159
+ logger.info(`[skills-scanner] --name "${CRON_JOB_NAME}" \\`);
160
+ logger.info(`[skills-scanner] --cron "${CRON_SCHEDULE}" \\`);
161
+ logger.info(`[skills-scanner] --tz "${CRON_TIMEZONE}" \\`);
162
+ logger.info("[skills-scanner] --session isolated \\");
163
+ logger.info(
164
+ '[skills-scanner] --message "Please run /skills-scanner scan --report and send results to this channel" \\'
165
+ );
166
+ logger.info("[skills-scanner] --announce \\");
167
+ logger.info("[skills-scanner] --channel last");
168
+ logger.info("[skills-scanner]");
169
+ logger.info("[skills-scanner] 💡 Or specify a target channel:");
170
+ logger.info("[skills-scanner] --channel feishu --target chat:<chatId>");
171
+ logger.info("[skills-scanner]");
172
+ }
173
+ }
174
+ }