@pwddd/skills-scanner 3.0.22 → 4.0.0
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 +3 -509
- package/index.ts +210 -212
- package/openclaw.plugin.json +112 -70
- package/package.json +64 -49
- package/skills/skills-scanner/SKILL.md +245 -1062
- package/src/api-client.ts +275 -0
- package/src/before-install-hook.ts +274 -0
- package/src/cache.ts +138 -0
- package/src/commands.ts +56 -152
- package/src/config-validator.ts +94 -0
- package/src/config.ts +187 -170
- package/src/cron-manager.ts +158 -0
- package/src/debug.ts +40 -0
- package/src/error-handler.ts +103 -0
- package/src/metrics.ts +140 -0
- package/src/prompt-guidance.ts +42 -250
- package/src/rate-limiter.ts +102 -0
- package/src/scanner.ts +230 -54
- package/src/state.ts +119 -71
- package/src/structured-logger.ts +97 -0
- package/src/types.ts +72 -50
- package/skills/skills-scanner/__pycache__/scan.cpython-314.pyc +0 -0
- package/skills/skills-scanner/scan.py +0 -446
- package/src/cron.ts +0 -292
- package/src/deps.ts +0 -77
- package/src/high-risk-operation-guard.ts +0 -62
- package/src/prompt-injection-guard.ts +0 -56
- package/src/report.ts +0 -100
- package/src/watcher.ts +0 -125
package/src/scanner.ts
CHANGED
|
@@ -1,54 +1,230 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scanner module -
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Scanner module - calls skill-scanner-api via HTTP
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SkillScannerApiClient, type ScanResult as ApiScanResult } from "./api-client.js";
|
|
6
|
+
import { recordScan } from "./metrics.js";
|
|
7
|
+
import { createActionableError } from "./error-handler.js";
|
|
8
|
+
|
|
9
|
+
export interface ScanOptions {
|
|
10
|
+
detailed?: boolean;
|
|
11
|
+
behavioral?: boolean;
|
|
12
|
+
recursive?: boolean;
|
|
13
|
+
jsonOut?: string;
|
|
14
|
+
apiUrl?: string;
|
|
15
|
+
useLLM?: boolean;
|
|
16
|
+
policy?: string;
|
|
17
|
+
timeoutMs?: number; // Scan timeout in milliseconds
|
|
18
|
+
stateDir?: string; // State directory for metrics
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScanResult {
|
|
22
|
+
exitCode: number;
|
|
23
|
+
output: string;
|
|
24
|
+
data?: ApiScanResult;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runScan(
|
|
28
|
+
mode: "scan" | "batch" | "clawhub" | "health",
|
|
29
|
+
target: string,
|
|
30
|
+
opts: ScanOptions = {}
|
|
31
|
+
): Promise<ScanResult> {
|
|
32
|
+
const apiUrl = opts.apiUrl || "https://110.vemic.com/skills-scanner";
|
|
33
|
+
const timeoutMs = opts.timeoutMs || 180000; // Default 3 minutes
|
|
34
|
+
const policy = (opts.policy || "balanced") as "strict" | "balanced" | "permissive";
|
|
35
|
+
const client = new SkillScannerApiClient(apiUrl, timeoutMs);
|
|
36
|
+
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
let success = false;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (mode === "health") {
|
|
42
|
+
const health = await client.health();
|
|
43
|
+
const lines = [];
|
|
44
|
+
if (health.status === "healthy") {
|
|
45
|
+
lines.push("✅ API 服务正常");
|
|
46
|
+
if (health.version) lines.push(` 版本:${health.version}`);
|
|
47
|
+
if (health.analyzers_available) {
|
|
48
|
+
lines.push(` 可用分析器:${health.analyzers_available.join(", ")}`);
|
|
49
|
+
}
|
|
50
|
+
return { exitCode: 0, output: lines.join("\n"), data: health as any };
|
|
51
|
+
} else {
|
|
52
|
+
lines.push(`❌ API 服务不可用:${apiUrl}`);
|
|
53
|
+
lines.push(` 错误:${health.error || "未知错误"}`);
|
|
54
|
+
return { exitCode: 1, output: lines.join("\n"), data: health as any };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (mode === "clawhub") {
|
|
59
|
+
const result = await client.scanClawHub(target, {
|
|
60
|
+
policy: opts.policy as any,
|
|
61
|
+
useLLM: opts.useLLM,
|
|
62
|
+
useBehavioral: opts.behavioral,
|
|
63
|
+
});
|
|
64
|
+
const output = formatScanResult(result, opts.detailed);
|
|
65
|
+
return {
|
|
66
|
+
exitCode: result.is_safe ? 0 : 1,
|
|
67
|
+
output,
|
|
68
|
+
data: result,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (mode === "scan") {
|
|
73
|
+
const result = await client.scanUpload(target, {
|
|
74
|
+
policy: policy as any,
|
|
75
|
+
useLLM: opts.useLLM,
|
|
76
|
+
useBehavioral: opts.behavioral,
|
|
77
|
+
});
|
|
78
|
+
const output = formatScanResult(result, opts.detailed);
|
|
79
|
+
success = true;
|
|
80
|
+
|
|
81
|
+
// Record metrics
|
|
82
|
+
if (opts.stateDir) {
|
|
83
|
+
recordScan(opts.stateDir, {
|
|
84
|
+
success: true,
|
|
85
|
+
durationMs: Date.now() - startTime,
|
|
86
|
+
policy,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
exitCode: result.is_safe ? 0 : 1,
|
|
92
|
+
output,
|
|
93
|
+
data: result,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (mode === "batch") {
|
|
98
|
+
const { readdirSync, statSync } = await import("node:fs");
|
|
99
|
+
const { join } = await import("node:path");
|
|
100
|
+
const { existsSync } = await import("node:fs");
|
|
101
|
+
|
|
102
|
+
if (!existsSync(target)) {
|
|
103
|
+
return { exitCode: 1, output: `路径不存在:${target}` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const skills: string[] = [];
|
|
107
|
+
const entries = readdirSync(target);
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
const skillPath = join(target, entry);
|
|
110
|
+
if (statSync(skillPath).isDirectory() && existsSync(join(skillPath, "SKILL.md"))) {
|
|
111
|
+
skills.push(skillPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (skills.length === 0) {
|
|
116
|
+
return { exitCode: 1, output: `未找到任何 Skill:${target}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const results: ApiScanResult[] = [];
|
|
120
|
+
const lines: string[] = [];
|
|
121
|
+
let safe = 0;
|
|
122
|
+
let unsafe = 0;
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < skills.length; i++) {
|
|
125
|
+
const skillPath = skills[i];
|
|
126
|
+
lines.push(`[${i + 1}/${skills.length}] 正在扫描:${skillPath}`);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = await client.scanUpload(skillPath, {
|
|
130
|
+
policy: opts.policy as any,
|
|
131
|
+
useLLM: opts.useLLM,
|
|
132
|
+
useBehavioral: opts.behavioral,
|
|
133
|
+
});
|
|
134
|
+
results.push(result);
|
|
135
|
+
if (result.is_safe) {
|
|
136
|
+
safe++;
|
|
137
|
+
lines.push(` ✅ ${result.skill_name}: 安全`);
|
|
138
|
+
} else {
|
|
139
|
+
unsafe++;
|
|
140
|
+
lines.push(` ❌ ${result.skill_name}: ${result.max_severity} (${result.findings_count} 个发现)`);
|
|
141
|
+
}
|
|
142
|
+
} catch (err: any) {
|
|
143
|
+
results.push({
|
|
144
|
+
skill_name: entry,
|
|
145
|
+
is_safe: false,
|
|
146
|
+
max_severity: "ERROR",
|
|
147
|
+
findings_count: 0,
|
|
148
|
+
error: err.message,
|
|
149
|
+
});
|
|
150
|
+
lines.push(` ❌ ${entry}: 扫描失败 - ${err.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push(`批量扫描完成:${safe} 安全,${unsafe} 问题`);
|
|
156
|
+
|
|
157
|
+
if (opts.detailed && unsafe > 0) {
|
|
158
|
+
lines.push("");
|
|
159
|
+
lines.push("问题 Skills:");
|
|
160
|
+
for (const r of results.filter((r) => !r.is_safe)) {
|
|
161
|
+
lines.push(` • ${r.skill_name} [${r.max_severity}] - ${r.findings_count} 条发现`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
exitCode: unsafe === 0 ? 0 : 1,
|
|
167
|
+
output: lines.join("\n"),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { exitCode: 1, output: `未知模式:${mode}` };
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
// Record failed scan
|
|
174
|
+
if (opts.stateDir && mode !== "health") {
|
|
175
|
+
recordScan(opts.stateDir, {
|
|
176
|
+
success: false,
|
|
177
|
+
durationMs: Date.now() - startTime,
|
|
178
|
+
policy,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Create actionable error message
|
|
183
|
+
const errorMessage = createActionableError(
|
|
184
|
+
mode === "health" ? "健康检查" : "扫描操作",
|
|
185
|
+
err,
|
|
186
|
+
{
|
|
187
|
+
apiUrl,
|
|
188
|
+
path: target,
|
|
189
|
+
mode,
|
|
190
|
+
policy,
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
exitCode: 1,
|
|
196
|
+
output: errorMessage,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Format scan result for display
|
|
203
|
+
*/
|
|
204
|
+
function formatScanResult(result: ApiScanResult, detailed = false): string {
|
|
205
|
+
const lines: string[] = [];
|
|
206
|
+
const statusIcon = result.is_safe ? "✅" : "❌";
|
|
207
|
+
|
|
208
|
+
lines.push(`${statusIcon} ${result.skill_name}`);
|
|
209
|
+
lines.push(` 严重性:${result.max_severity}`);
|
|
210
|
+
lines.push(` 发现数:${result.findings_count}`);
|
|
211
|
+
|
|
212
|
+
if (detailed && result.findings && result.findings.length > 0) {
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("发现详情:");
|
|
215
|
+
for (let i = 0; i < Math.min(result.findings.length, 10); i++) {
|
|
216
|
+
const finding = result.findings[i];
|
|
217
|
+
lines.push(` ${i + 1}. [${finding.severity}] ${finding.category}`);
|
|
218
|
+
lines.push(` ${finding.description}`);
|
|
219
|
+
if (finding.file) {
|
|
220
|
+
lines.push(` 文件:${finding.file}${finding.line ? `:${finding.line}` : ""}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (result.findings.length > 10) {
|
|
225
|
+
lines.push(` ... 还有 ${result.findings.length - 10} 条发现`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -1,71 +1,119 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State management module
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import os from "node:os";
|
|
8
|
-
import type { ScanState, ScannerConfig } from "./types.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
/**
|
|
2
|
+
* State management module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import type { ScanState, ScannerConfig } from "./types.js";
|
|
9
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
|
|
10
|
+
|
|
11
|
+
// Current state schema version
|
|
12
|
+
const CURRENT_STATE_VERSION = 2;
|
|
13
|
+
|
|
14
|
+
// Legacy fallback path (used when runtime is not available)
|
|
15
|
+
const LEGACY_STATE_DIR = join(os.homedir(), ".openclaw", "skills-scanner");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get state directory path using official OpenClaw API
|
|
19
|
+
*/
|
|
20
|
+
export function getStateDir(runtime?: PluginRuntime): string {
|
|
21
|
+
if (runtime?.state?.resolveStateDir) {
|
|
22
|
+
const stateDir = runtime.state.resolveStateDir();
|
|
23
|
+
return join(stateDir, "skills-scanner");
|
|
24
|
+
}
|
|
25
|
+
// Fallback to legacy path
|
|
26
|
+
return LEGACY_STATE_DIR;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get state file path
|
|
31
|
+
*/
|
|
32
|
+
export function getStateFile(runtime?: PluginRuntime): string {
|
|
33
|
+
return join(getStateDir(runtime), "state.json");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Migrate state from old versions to current version
|
|
38
|
+
*/
|
|
39
|
+
function migrateState(state: any): ScanState {
|
|
40
|
+
const version = state.version || 1;
|
|
41
|
+
|
|
42
|
+
if (version === CURRENT_STATE_VERSION) {
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Migration from v1 to v2
|
|
47
|
+
if (version === 1) {
|
|
48
|
+
// v1 didn't have version field or lastShutdownAt
|
|
49
|
+
return {
|
|
50
|
+
...state,
|
|
51
|
+
version: 2,
|
|
52
|
+
lastShutdownAt: undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Unknown version - return as-is with current version
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
version: CURRENT_STATE_VERSION,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function loadState(runtime?: PluginRuntime): ScanState {
|
|
64
|
+
try {
|
|
65
|
+
const stateFile = getStateFile(runtime);
|
|
66
|
+
const rawState = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
67
|
+
return migrateState(rawState);
|
|
68
|
+
} catch {
|
|
69
|
+
return { version: CURRENT_STATE_VERSION };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function saveState(s: ScanState, runtime?: PluginRuntime): void {
|
|
74
|
+
const stateDir = getStateDir(runtime);
|
|
75
|
+
const stateFile = getStateFile(runtime);
|
|
76
|
+
|
|
77
|
+
// Ensure version is set
|
|
78
|
+
const stateWithVersion = {
|
|
79
|
+
...s,
|
|
80
|
+
version: CURRENT_STATE_VERSION,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
mkdirSync(stateDir, { recursive: true });
|
|
84
|
+
writeFileSync(stateFile, JSON.stringify(stateWithVersion, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isFirstRun(cfg: ScannerConfig, runtime?: PluginRuntime): boolean {
|
|
88
|
+
const state = loadState(runtime) as any;
|
|
89
|
+
|
|
90
|
+
if (state.configReviewed) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isDefaultConfig =
|
|
95
|
+
!cfg.apiUrl &&
|
|
96
|
+
cfg.behavioral !== true &&
|
|
97
|
+
cfg.useLLM !== true &&
|
|
98
|
+
cfg.policy !== "strict" &&
|
|
99
|
+
cfg.policy !== "permissive" &&
|
|
100
|
+
cfg.onUnsafe !== "delete" &&
|
|
101
|
+
cfg.onUnsafe !== "warn";
|
|
102
|
+
|
|
103
|
+
return isDefaultConfig;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function markConfigReviewed(runtime?: PluginRuntime): void {
|
|
107
|
+
const state = loadState(runtime) as any;
|
|
108
|
+
saveState({ ...state, configReviewed: true }, runtime);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function expandPath(p: string): string {
|
|
112
|
+
return p.replace(/^~/, os.homedir());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Export for backward compatibility
|
|
116
|
+
export { LEGACY_STATE_DIR as STATE_DIR };
|
|
117
|
+
export function STATE_FILE(runtime?: PluginRuntime): string {
|
|
118
|
+
return getStateFile(runtime);
|
|
119
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PluginLogger } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface LogContext {
|
|
8
|
+
module?: string;
|
|
9
|
+
action?: string;
|
|
10
|
+
duration?: number;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Log with structured context
|
|
16
|
+
*/
|
|
17
|
+
export function logInfo(
|
|
18
|
+
logger: PluginLogger,
|
|
19
|
+
message: string,
|
|
20
|
+
context?: LogContext
|
|
21
|
+
): void {
|
|
22
|
+
if (context) {
|
|
23
|
+
logger.info(message, context);
|
|
24
|
+
} else {
|
|
25
|
+
logger.info(message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function logDebug(
|
|
30
|
+
logger: PluginLogger,
|
|
31
|
+
message: string,
|
|
32
|
+
context?: LogContext
|
|
33
|
+
): void {
|
|
34
|
+
if (context) {
|
|
35
|
+
logger.debug(message, context);
|
|
36
|
+
} else {
|
|
37
|
+
logger.debug(message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function logWarn(
|
|
42
|
+
logger: PluginLogger,
|
|
43
|
+
message: string,
|
|
44
|
+
context?: LogContext
|
|
45
|
+
): void {
|
|
46
|
+
if (context) {
|
|
47
|
+
logger.warn(message, context);
|
|
48
|
+
} else {
|
|
49
|
+
logger.warn(message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function logError(
|
|
54
|
+
logger: PluginLogger,
|
|
55
|
+
message: string,
|
|
56
|
+
error?: Error,
|
|
57
|
+
context?: LogContext
|
|
58
|
+
): void {
|
|
59
|
+
const errorContext = {
|
|
60
|
+
...context,
|
|
61
|
+
error: error?.message,
|
|
62
|
+
stack: error?.stack,
|
|
63
|
+
};
|
|
64
|
+
logger.error(message, errorContext);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Log operation with timing
|
|
69
|
+
*/
|
|
70
|
+
export async function logOperation<T>(
|
|
71
|
+
logger: PluginLogger,
|
|
72
|
+
operation: string,
|
|
73
|
+
fn: () => Promise<T>,
|
|
74
|
+
context?: LogContext
|
|
75
|
+
): Promise<T> {
|
|
76
|
+
const startTime = Date.now();
|
|
77
|
+
logDebug(logger, `${operation} started`, context);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await fn();
|
|
81
|
+
const duration = Date.now() - startTime;
|
|
82
|
+
logInfo(logger, `${operation} completed`, {
|
|
83
|
+
...context,
|
|
84
|
+
duration,
|
|
85
|
+
success: true,
|
|
86
|
+
});
|
|
87
|
+
return result;
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
const duration = Date.now() - startTime;
|
|
90
|
+
logError(logger, `${operation} failed`, error, {
|
|
91
|
+
...context,
|
|
92
|
+
duration,
|
|
93
|
+
success: false,
|
|
94
|
+
});
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|