@openclaw/plugin-inspector 0.0.0 → 0.1.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +151 -2
- package/examples/github-actions-plugin-inspector.yml +24 -0
- package/examples/plugin-inspector.config.json +15 -0
- package/package.json +56 -3
- package/src/advanced.js +186 -0
- package/src/api.js +85 -0
- package/src/artifacts.js +113 -0
- package/src/capture-api.js +115 -0
- package/src/ci-policy.js +259 -0
- package/src/ci-summary.js +223 -0
- package/src/cli.js +113 -0
- package/src/cold-import-readiness.js +271 -0
- package/src/compatibility-report.js +356 -0
- package/src/config.js +182 -0
- package/src/contract-capture.js +288 -0
- package/src/contract-coverage.js +167 -0
- package/src/contract-probes.js +156 -0
- package/src/execution-results.js +297 -0
- package/src/fixture-summary.js +780 -0
- package/src/import-loop-profile.js +169 -0
- package/src/index.js +10 -0
- package/src/inspector.js +405 -0
- package/src/issues.js +366 -0
- package/src/json-file.js +10 -0
- package/src/mock-sdk-capture-runner.js +69 -0
- package/src/openclaw-target.js +179 -0
- package/src/path-utils.js +28 -0
- package/src/platform-probes.js +238 -0
- package/src/process-profile.js +117 -0
- package/src/profile-diff.js +222 -0
- package/src/ref-diff.js +335 -0
- package/src/report.js +435 -0
- package/src/runtime-capture-report.js +134 -0
- package/src/runtime-profile.js +289 -0
- package/src/sdk-mock.js +61 -0
- package/src/stats.js +13 -0
- package/src/synthetic-probes.js +544 -0
- package/src/workspace-plan.js +496 -0
package/src/issues.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const deprecatedCompatRecords = new Set([
|
|
4
|
+
"channel-env-vars",
|
|
5
|
+
"legacy-before-agent-start",
|
|
6
|
+
"legacy-root-sdk-import",
|
|
7
|
+
"provider-auth-env-vars",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export const knownIssueCodes = new Set([
|
|
11
|
+
"before-tool-call-probe",
|
|
12
|
+
"channel-contract-probe",
|
|
13
|
+
"channel-env-vars",
|
|
14
|
+
"conversation-access-hook",
|
|
15
|
+
"legacy-before-agent-start",
|
|
16
|
+
"legacy-root-sdk-import",
|
|
17
|
+
"manifest-unknown-contracts",
|
|
18
|
+
"manifest-unknown-fields",
|
|
19
|
+
"missing-expected-seam",
|
|
20
|
+
"missing-compat-record",
|
|
21
|
+
"unknown-hook-name",
|
|
22
|
+
"unknown-registration-name",
|
|
23
|
+
"package-build-artifact-entrypoint",
|
|
24
|
+
"package-dependency-install-required",
|
|
25
|
+
"package-entrypoint-missing",
|
|
26
|
+
"package-json-missing",
|
|
27
|
+
"package-manifest-version-drift",
|
|
28
|
+
"package-openclaw-entry-missing",
|
|
29
|
+
"package-openclaw-metadata-missing",
|
|
30
|
+
"package-plugin-api-compat-missing",
|
|
31
|
+
"package-typescript-source-entrypoint",
|
|
32
|
+
"provider-auth-env-vars",
|
|
33
|
+
"registration-capture-gap",
|
|
34
|
+
"runtime-tool-capture",
|
|
35
|
+
"sdk-export-missing",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export const issueMetadataByCode = {
|
|
39
|
+
"before-tool-call-probe": {
|
|
40
|
+
severity: "P1",
|
|
41
|
+
owner: "inspector",
|
|
42
|
+
decision: "inspector-follow-up",
|
|
43
|
+
title: "before_tool_call needs terminal/block/approval probes",
|
|
44
|
+
},
|
|
45
|
+
"channel-contract-probe": {
|
|
46
|
+
severity: "P2",
|
|
47
|
+
owner: "inspector",
|
|
48
|
+
decision: "inspector-follow-up",
|
|
49
|
+
title: "channel runtime needs envelope/config probes",
|
|
50
|
+
},
|
|
51
|
+
"channel-env-vars": {
|
|
52
|
+
severity: "P2",
|
|
53
|
+
owner: "core",
|
|
54
|
+
decision: "core-compat-adapter",
|
|
55
|
+
title: "channelEnvVars legacy manifest metadata must stay covered",
|
|
56
|
+
},
|
|
57
|
+
"conversation-access-hook": {
|
|
58
|
+
severity: "P1",
|
|
59
|
+
owner: "core",
|
|
60
|
+
decision: "inspector-follow-up",
|
|
61
|
+
title: "conversation-access hooks need privacy-boundary probes",
|
|
62
|
+
},
|
|
63
|
+
"legacy-before-agent-start": {
|
|
64
|
+
severity: "P2",
|
|
65
|
+
owner: "core",
|
|
66
|
+
decision: "core-compat-adapter",
|
|
67
|
+
title: "legacy before_agent_start hook compatibility is still used",
|
|
68
|
+
},
|
|
69
|
+
"legacy-root-sdk-import": {
|
|
70
|
+
severity: "P2",
|
|
71
|
+
owner: "core",
|
|
72
|
+
decision: "core-compat-adapter",
|
|
73
|
+
title: "root plugin SDK barrel is still used by fixtures",
|
|
74
|
+
},
|
|
75
|
+
"sdk-export-missing": {
|
|
76
|
+
severity: "P1",
|
|
77
|
+
owner: "core",
|
|
78
|
+
decision: "core-compat-adapter",
|
|
79
|
+
title: "plugin SDK import aliases are missing from target package exports",
|
|
80
|
+
},
|
|
81
|
+
"missing-compat-record": {
|
|
82
|
+
severity: "P1",
|
|
83
|
+
owner: "core",
|
|
84
|
+
decision: "core-compat-adapter",
|
|
85
|
+
title: "compat-dependent behavior lacks registry coverage",
|
|
86
|
+
},
|
|
87
|
+
"missing-expected-seam": {
|
|
88
|
+
severity: "P0",
|
|
89
|
+
owner: "inspector",
|
|
90
|
+
decision: "inspector-follow-up",
|
|
91
|
+
title: "fixture no longer exposes an expected seam",
|
|
92
|
+
},
|
|
93
|
+
"manifest-unknown-contracts": {
|
|
94
|
+
severity: "P1",
|
|
95
|
+
owner: "plugin",
|
|
96
|
+
decision: "plugin-upstream-fix",
|
|
97
|
+
title: "manifest declares unsupported contract keys",
|
|
98
|
+
},
|
|
99
|
+
"manifest-unknown-fields": {
|
|
100
|
+
severity: "P2",
|
|
101
|
+
owner: "plugin",
|
|
102
|
+
decision: "plugin-upstream-fix",
|
|
103
|
+
title: "manifest uses unsupported top-level fields",
|
|
104
|
+
},
|
|
105
|
+
"package-build-artifact-entrypoint": {
|
|
106
|
+
severity: "P2",
|
|
107
|
+
owner: "inspector",
|
|
108
|
+
decision: "inspector-follow-up",
|
|
109
|
+
title: "cold import requires package build output",
|
|
110
|
+
},
|
|
111
|
+
"package-dependency-install-required": {
|
|
112
|
+
severity: "P2",
|
|
113
|
+
owner: "inspector",
|
|
114
|
+
decision: "inspector-follow-up",
|
|
115
|
+
title: "cold import requires isolated dependency installation",
|
|
116
|
+
},
|
|
117
|
+
"package-entrypoint-missing": {
|
|
118
|
+
severity: "P1",
|
|
119
|
+
owner: "plugin",
|
|
120
|
+
decision: "plugin-upstream-fix",
|
|
121
|
+
title: "OpenClaw package entrypoint is missing",
|
|
122
|
+
},
|
|
123
|
+
"package-json-missing": {
|
|
124
|
+
severity: "P2",
|
|
125
|
+
owner: "plugin",
|
|
126
|
+
decision: "plugin-upstream-fix",
|
|
127
|
+
title: "package metadata is missing",
|
|
128
|
+
},
|
|
129
|
+
"package-manifest-version-drift": {
|
|
130
|
+
severity: "P2",
|
|
131
|
+
owner: "plugin",
|
|
132
|
+
decision: "plugin-upstream-fix",
|
|
133
|
+
title: "package and manifest versions drift",
|
|
134
|
+
},
|
|
135
|
+
"package-openclaw-entry-missing": {
|
|
136
|
+
severity: "P2",
|
|
137
|
+
owner: "plugin",
|
|
138
|
+
decision: "plugin-upstream-fix",
|
|
139
|
+
title: "OpenClaw package entrypoint metadata is missing",
|
|
140
|
+
},
|
|
141
|
+
"package-openclaw-metadata-missing": {
|
|
142
|
+
severity: "P2",
|
|
143
|
+
owner: "plugin",
|
|
144
|
+
decision: "plugin-upstream-fix",
|
|
145
|
+
title: "OpenClaw package metadata is missing",
|
|
146
|
+
},
|
|
147
|
+
"package-plugin-api-compat-missing": {
|
|
148
|
+
severity: "P2",
|
|
149
|
+
owner: "plugin",
|
|
150
|
+
decision: "plugin-upstream-fix",
|
|
151
|
+
title: "plugin API compatibility range is missing",
|
|
152
|
+
},
|
|
153
|
+
"package-typescript-source-entrypoint": {
|
|
154
|
+
severity: "P2",
|
|
155
|
+
owner: "inspector",
|
|
156
|
+
decision: "inspector-follow-up",
|
|
157
|
+
title: "cold import needs TypeScript source entrypoint support",
|
|
158
|
+
},
|
|
159
|
+
"provider-auth-env-vars": {
|
|
160
|
+
severity: "P2",
|
|
161
|
+
owner: "core",
|
|
162
|
+
decision: "core-compat-adapter",
|
|
163
|
+
title: "providerAuthEnvVars legacy manifest metadata must stay covered",
|
|
164
|
+
},
|
|
165
|
+
"registration-capture-gap": {
|
|
166
|
+
severity: "P1",
|
|
167
|
+
owner: "inspector",
|
|
168
|
+
decision: "inspector-follow-up",
|
|
169
|
+
title: "runtime registrations need capture before contract judgment",
|
|
170
|
+
},
|
|
171
|
+
"runtime-tool-capture": {
|
|
172
|
+
severity: "P2",
|
|
173
|
+
owner: "inspector",
|
|
174
|
+
decision: "inspector-follow-up",
|
|
175
|
+
title: "runtime tool schema needs registration capture",
|
|
176
|
+
},
|
|
177
|
+
"unknown-hook-name": {
|
|
178
|
+
severity: "P0",
|
|
179
|
+
owner: "core",
|
|
180
|
+
decision: "core-compat-adapter",
|
|
181
|
+
title: "fixture uses a hook missing from target OpenClaw",
|
|
182
|
+
},
|
|
183
|
+
"unknown-registration-name": {
|
|
184
|
+
severity: "P0",
|
|
185
|
+
owner: "core",
|
|
186
|
+
decision: "core-compat-adapter",
|
|
187
|
+
title: "fixture calls a registrar missing from target OpenClaw",
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export function buildIssues({ breakages = [], warnings = [], suggestions = [], targetOpenClaw, idPrefix = "CRABPOT" }) {
|
|
192
|
+
const findings = [
|
|
193
|
+
...breakages.map((finding) => issueMetadata({ ...finding, severity: "P0" }, targetOpenClaw)),
|
|
194
|
+
...warnings.map((finding) => issueMetadata(finding, targetOpenClaw)),
|
|
195
|
+
...suggestions.map((finding) => issueMetadata(finding, targetOpenClaw)),
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
return findings
|
|
199
|
+
.filter((finding) => finding.severity)
|
|
200
|
+
.sort(issueSort)
|
|
201
|
+
.map((finding) => ({
|
|
202
|
+
id: issueId(finding, { prefix: idPrefix }),
|
|
203
|
+
fixture: finding.fixture,
|
|
204
|
+
severity: finding.severity,
|
|
205
|
+
owner: finding.owner,
|
|
206
|
+
code: finding.code,
|
|
207
|
+
decision: finding.decision,
|
|
208
|
+
status: finding.severity === "P0" || finding.level === "breakage" ? "blocking" : "open",
|
|
209
|
+
issueClass: finding.issueClass,
|
|
210
|
+
live: finding.live,
|
|
211
|
+
deprecated: finding.deprecated,
|
|
212
|
+
compatStatus: finding.compatStatus,
|
|
213
|
+
title: issueTitle(finding),
|
|
214
|
+
evidence: finding.evidence ?? [],
|
|
215
|
+
compatRecord: finding.compatRecord ?? null,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function issueId(finding, options = {}) {
|
|
220
|
+
const stableKey = [
|
|
221
|
+
finding.fixture,
|
|
222
|
+
finding.code,
|
|
223
|
+
finding.severity,
|
|
224
|
+
finding.compatRecord ?? "",
|
|
225
|
+
...(finding.evidence ?? []),
|
|
226
|
+
].join("\n");
|
|
227
|
+
return `${options.prefix ?? "CRABPOT"}-${createHash("sha256").update(stableKey).digest("hex").slice(0, 8).toUpperCase()}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function issueMetadata(finding, targetOpenClaw) {
|
|
231
|
+
const metadata = issueMetadataByCode[finding.code] ?? {
|
|
232
|
+
severity: "P3",
|
|
233
|
+
owner: "inspector",
|
|
234
|
+
decision: "inspector-follow-up",
|
|
235
|
+
title: finding.message,
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
...finding,
|
|
239
|
+
...metadata,
|
|
240
|
+
...classifyIssueFinding(finding, targetOpenClaw, metadata),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
|
|
245
|
+
const compatStatus = compatStatusFor(finding, targetOpenClaw);
|
|
246
|
+
const deprecated = compatStatus === "deprecated";
|
|
247
|
+
const code = finding.code;
|
|
248
|
+
const issueClass = issueClassFor(code, { deprecated, compatRecord: finding.compatRecord });
|
|
249
|
+
const live = issueClass === "live-issue" || finding.level === "breakage";
|
|
250
|
+
const severity = severityForClass(code, metadata?.severity, {
|
|
251
|
+
issueClass,
|
|
252
|
+
compatRecord: finding.compatRecord,
|
|
253
|
+
compatStatus,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
compatStatus,
|
|
258
|
+
deprecated,
|
|
259
|
+
issueClass,
|
|
260
|
+
live,
|
|
261
|
+
severity,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function summarizeIssueClasses(issues) {
|
|
266
|
+
const summary = {
|
|
267
|
+
"compat-gap": 0,
|
|
268
|
+
"deprecation-warning": 0,
|
|
269
|
+
"fixture-regression": 0,
|
|
270
|
+
"inspector-gap": 0,
|
|
271
|
+
"live-issue": 0,
|
|
272
|
+
"upstream-metadata": 0,
|
|
273
|
+
};
|
|
274
|
+
for (const issue of issues) {
|
|
275
|
+
summary[issue.issueClass] = (summary[issue.issueClass] ?? 0) + 1;
|
|
276
|
+
}
|
|
277
|
+
return summary;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function issueClassFor(code, options) {
|
|
281
|
+
if (["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing", "sdk-export-missing"].includes(code)) {
|
|
282
|
+
return "live-issue";
|
|
283
|
+
}
|
|
284
|
+
if (code === "missing-compat-record") {
|
|
285
|
+
return "compat-gap";
|
|
286
|
+
}
|
|
287
|
+
if (options.deprecated || ["channel-env-vars", "legacy-before-agent-start", "legacy-root-sdk-import", "provider-auth-env-vars"].includes(code)) {
|
|
288
|
+
return "deprecation-warning";
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
[
|
|
292
|
+
"before-tool-call-probe",
|
|
293
|
+
"channel-contract-probe",
|
|
294
|
+
"conversation-access-hook",
|
|
295
|
+
"package-build-artifact-entrypoint",
|
|
296
|
+
"package-dependency-install-required",
|
|
297
|
+
"package-typescript-source-entrypoint",
|
|
298
|
+
"registration-capture-gap",
|
|
299
|
+
"runtime-tool-capture",
|
|
300
|
+
].includes(code)
|
|
301
|
+
) {
|
|
302
|
+
return "inspector-gap";
|
|
303
|
+
}
|
|
304
|
+
if (
|
|
305
|
+
[
|
|
306
|
+
"manifest-unknown-contracts",
|
|
307
|
+
"manifest-unknown-fields",
|
|
308
|
+
"package-json-missing",
|
|
309
|
+
"package-manifest-version-drift",
|
|
310
|
+
"package-openclaw-entry-missing",
|
|
311
|
+
"package-openclaw-metadata-missing",
|
|
312
|
+
"package-plugin-api-compat-missing",
|
|
313
|
+
].includes(code)
|
|
314
|
+
) {
|
|
315
|
+
return "upstream-metadata";
|
|
316
|
+
}
|
|
317
|
+
if (code === "missing-expected-seam") {
|
|
318
|
+
return "fixture-regression";
|
|
319
|
+
}
|
|
320
|
+
return options.compatRecord ? "compat-gap" : "inspector-gap";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function severityForClass(code, defaultSeverity, options) {
|
|
324
|
+
if (
|
|
325
|
+
options.issueClass === "live-issue" &&
|
|
326
|
+
["none", "untracked"].includes(options.compatStatus) &&
|
|
327
|
+
["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing", "sdk-export-missing"].includes(code)
|
|
328
|
+
) {
|
|
329
|
+
return "P0";
|
|
330
|
+
}
|
|
331
|
+
return defaultSeverity ?? "P3";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function compatStatusFor(finding, targetOpenClaw) {
|
|
335
|
+
if (finding.code === "missing-compat-record") {
|
|
336
|
+
return "missing";
|
|
337
|
+
}
|
|
338
|
+
if (!finding.compatRecord) {
|
|
339
|
+
return "none";
|
|
340
|
+
}
|
|
341
|
+
const targetStatus = targetOpenClaw?.compatRecordStatuses?.[finding.compatRecord];
|
|
342
|
+
if (targetStatus) {
|
|
343
|
+
return targetStatus;
|
|
344
|
+
}
|
|
345
|
+
if (deprecatedCompatRecords.has(finding.compatRecord)) {
|
|
346
|
+
return "deprecated";
|
|
347
|
+
}
|
|
348
|
+
return "untracked";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function issueSort(left, right) {
|
|
352
|
+
return (
|
|
353
|
+
priorityRank(left.severity) - priorityRank(right.severity) ||
|
|
354
|
+
left.fixture.localeCompare(right.fixture) ||
|
|
355
|
+
left.code.localeCompare(right.code) ||
|
|
356
|
+
(left.evidence ?? []).join(",").localeCompare((right.evidence ?? []).join(","))
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function issueTitle(finding) {
|
|
361
|
+
return `${finding.fixture}: ${finding.title ?? finding.message}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function priorityRank(priority) {
|
|
365
|
+
return { P0: 0, P1: 1, P2: 2, P3: 3 }[priority] ?? 4;
|
|
366
|
+
}
|
package/src/json-file.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export async function readJsonFile(jsonPath) {
|
|
5
|
+
return JSON.parse(await readFile(jsonPath, "utf8"));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function readOptionalJsonFile(jsonPath) {
|
|
9
|
+
return existsSync(jsonPath) ? readJsonFile(jsonPath) : null;
|
|
10
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdtemp, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { createCaptureApi } from "./capture-api.js";
|
|
7
|
+
import { createMockSdkPackage } from "./sdk-mock.js";
|
|
8
|
+
|
|
9
|
+
const options = JSON.parse(process.argv[2] ?? "{}");
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const result = await run(options);
|
|
13
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
process.stderr.write(`${error.stack ?? error.message}\n`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function run(options) {
|
|
20
|
+
const entrypoint = path.resolve(options.cwd ?? process.cwd(), options.entrypoint);
|
|
21
|
+
const pluginRoot = path.resolve(options.cwd ?? process.cwd(), options.pluginRoot ?? path.dirname(entrypoint));
|
|
22
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-sdk-"));
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await createMockSdkPackage(workspace);
|
|
26
|
+
const linkedPluginRoot = path.join(workspace, "plugin");
|
|
27
|
+
await symlink(pluginRoot, linkedPluginRoot, "junction");
|
|
28
|
+
const linkedEntrypoint = path.join(linkedPluginRoot, path.relative(pluginRoot, entrypoint));
|
|
29
|
+
return await captureLinkedEntrypoint(linkedEntrypoint, options);
|
|
30
|
+
} finally {
|
|
31
|
+
await rm(workspace, { force: true, recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function captureLinkedEntrypoint(entrypoint, options) {
|
|
36
|
+
const module = await import(pathToFileURL(entrypoint).href);
|
|
37
|
+
const register = findRegisterExport(module);
|
|
38
|
+
|
|
39
|
+
if (!register) {
|
|
40
|
+
return {
|
|
41
|
+
status: "no-register-export",
|
|
42
|
+
entrypoint: options.entrypoint,
|
|
43
|
+
mockSdk: true,
|
|
44
|
+
captured: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const api = createCaptureApi(options.apiOptions);
|
|
49
|
+
await register(api);
|
|
50
|
+
return {
|
|
51
|
+
status: "captured",
|
|
52
|
+
entrypoint: options.entrypoint,
|
|
53
|
+
mockSdk: true,
|
|
54
|
+
captured: api.getCapturedContracts(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findRegisterExport(module) {
|
|
59
|
+
if (typeof module.register === "function") {
|
|
60
|
+
return module.register;
|
|
61
|
+
}
|
|
62
|
+
if (typeof module.default === "function") {
|
|
63
|
+
return module.default;
|
|
64
|
+
}
|
|
65
|
+
if (typeof module.default?.register === "function") {
|
|
66
|
+
return module.default.register;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const defaultOpenClawCheckoutPaths = ["./openclaw", "../openclaw"];
|
|
6
|
+
|
|
7
|
+
export async function readOpenClawTargetSurface(options = {}) {
|
|
8
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
9
|
+
const configuredPath = options.configuredPath;
|
|
10
|
+
|
|
11
|
+
if (configuredPath === false) {
|
|
12
|
+
return emptyTargetSurface({ configuredPath: null, status: "disabled" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const requestedPaths = openClawTargetPathCandidates(options.manifest, configuredPath);
|
|
16
|
+
if (requestedPaths.length === 0) {
|
|
17
|
+
return emptyTargetSurface({ configuredPath: null, status: "not-configured" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const match = findTargetCheckout(rootDir, requestedPaths);
|
|
21
|
+
if (!match) {
|
|
22
|
+
return emptyTargetSurface({
|
|
23
|
+
configuredPath: requestedPaths[0],
|
|
24
|
+
searchedPaths: requestedPaths,
|
|
25
|
+
status: "missing",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { requestedPath, resolvedPath, registryPath } = match;
|
|
30
|
+
const hookTypesPath = path.join(resolvedPath, "src/plugins/hook-types.ts");
|
|
31
|
+
const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
|
|
32
|
+
const capturedRegistrationPath = path.join(resolvedPath, "src/plugins/captured-registration.ts");
|
|
33
|
+
const manifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
|
|
34
|
+
const packagePath = path.join(resolvedPath, "package.json");
|
|
35
|
+
|
|
36
|
+
const registrySource = await readFile(registryPath, "utf8");
|
|
37
|
+
const compatRecordEntries = parseCompatRecordEntries(registrySource);
|
|
38
|
+
const hookTypesSource = existsSync(hookTypesPath) ? await readFile(hookTypesPath, "utf8") : "";
|
|
39
|
+
const hookNames = hookTypesSource ? parseExportedStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
|
|
40
|
+
const apiBuilderSource = existsSync(apiBuilderPath) ? await readFile(apiBuilderPath, "utf8") : "";
|
|
41
|
+
const apiRegistrars = apiBuilderSource ? parseApiRegistrars(apiBuilderSource) : [];
|
|
42
|
+
const manifestTypesSource = existsSync(manifestTypesPath) ? await readFile(manifestTypesPath, "utf8") : "";
|
|
43
|
+
const manifestFields = manifestTypesSource ? parseTypeFields(manifestTypesSource, "PluginManifest") : [];
|
|
44
|
+
const manifestContractFields = manifestTypesSource ? parseTypeFields(manifestTypesSource, "PluginManifestContracts") : [];
|
|
45
|
+
const capturedRegistrars = existsSync(capturedRegistrationPath)
|
|
46
|
+
? parseCapturedRegistrars(await readFile(capturedRegistrationPath, "utf8"))
|
|
47
|
+
: [];
|
|
48
|
+
const sdkExports = existsSync(packagePath)
|
|
49
|
+
? parsePluginSdkExports(JSON.parse(await readFile(packagePath, "utf8")))
|
|
50
|
+
: [];
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
configuredPath: requestedPath,
|
|
54
|
+
searchedPaths: requestedPaths,
|
|
55
|
+
status: "ok",
|
|
56
|
+
compatRegistryPath: relativePath(rootDir, registryPath),
|
|
57
|
+
compatRecordCount: compatRecordEntries.length,
|
|
58
|
+
compatRecords: compatRecordEntries.map((record) => record.code).sort(),
|
|
59
|
+
compatRecordStatuses: Object.fromEntries(compatRecordEntries.map((record) => [record.code, record.status])),
|
|
60
|
+
hookTypesPath: existsSync(hookTypesPath) ? relativePath(rootDir, hookTypesPath) : null,
|
|
61
|
+
hookNameCount: hookNames.length,
|
|
62
|
+
hookNames,
|
|
63
|
+
apiBuilderPath: existsSync(apiBuilderPath) ? relativePath(rootDir, apiBuilderPath) : null,
|
|
64
|
+
apiRegistrarCount: apiRegistrars.length,
|
|
65
|
+
apiRegistrars,
|
|
66
|
+
capturedRegistrationPath: existsSync(capturedRegistrationPath) ? relativePath(rootDir, capturedRegistrationPath) : null,
|
|
67
|
+
capturedRegistrarCount: capturedRegistrars.length,
|
|
68
|
+
capturedRegistrars,
|
|
69
|
+
packagePath: existsSync(packagePath) ? relativePath(rootDir, packagePath) : null,
|
|
70
|
+
sdkExportCount: sdkExports.length,
|
|
71
|
+
sdkExports,
|
|
72
|
+
manifestTypesPath: existsSync(manifestTypesPath) ? relativePath(rootDir, manifestTypesPath) : null,
|
|
73
|
+
manifestFieldCount: manifestFields.length,
|
|
74
|
+
manifestFields,
|
|
75
|
+
manifestContractFieldCount: manifestContractFields.length,
|
|
76
|
+
manifestContractFields,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function openClawTargetPathCandidates(manifest, configuredPath) {
|
|
81
|
+
if (typeof configuredPath === "string") {
|
|
82
|
+
return [configuredPath];
|
|
83
|
+
}
|
|
84
|
+
return unique([manifest?.openclaw?.defaultCheckoutPath, ...defaultOpenClawCheckoutPaths].filter(Boolean));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseCompatRecordEntries(source) {
|
|
88
|
+
const entries = [];
|
|
89
|
+
for (const match of source.matchAll(/\{[\s\S]*?\bcode:\s*["'`]([^"'`]+)["'`][\s\S]*?\bstatus:\s*["'`]([^"'`]+)["'`][\s\S]*?\}/g)) {
|
|
90
|
+
entries.push({ code: match[1], status: match[2] });
|
|
91
|
+
}
|
|
92
|
+
return dedupeBy(entries, (entry) => entry.code).sort((left, right) => left.code.localeCompare(right.code));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function parsePluginSdkExports(packageJson) {
|
|
96
|
+
return Object.keys(packageJson.exports ?? {})
|
|
97
|
+
.filter((specifier) => specifier === "./plugin-sdk" || specifier.startsWith("./plugin-sdk/"))
|
|
98
|
+
.map((specifier) => `openclaw/${specifier.slice(2)}`)
|
|
99
|
+
.sort();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function parseExportedStringArray(source, exportName) {
|
|
103
|
+
const match = source.match(new RegExp(`export\\s+const\\s+${exportName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s+as\\s+const`));
|
|
104
|
+
if (!match) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return unique([...match[1].matchAll(/["'`]([^"'`]+)["'`]/g)].map((item) => item[1])).sort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function parseTypeFields(source, typeName) {
|
|
112
|
+
const marker = `export type ${typeName} = {`;
|
|
113
|
+
const start = source.indexOf(marker);
|
|
114
|
+
if (start === -1) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const bodyStart = start + marker.length;
|
|
118
|
+
const end = source.indexOf("\n};", bodyStart);
|
|
119
|
+
if (end === -1) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const body = source.slice(bodyStart, end);
|
|
123
|
+
return unique(
|
|
124
|
+
[...body.matchAll(/^\s*([A-Za-z][A-Za-z0-9]*)\??:/gm)]
|
|
125
|
+
.map((match) => match[1])
|
|
126
|
+
.filter((field) => !field.startsWith("PluginManifest")),
|
|
127
|
+
).sort();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findTargetCheckout(rootDir, requestedPaths) {
|
|
131
|
+
for (const requestedPath of requestedPaths) {
|
|
132
|
+
const resolvedPath = path.resolve(rootDir, requestedPath);
|
|
133
|
+
const registryPath = path.join(resolvedPath, "src/plugins/compat/registry.ts");
|
|
134
|
+
if (existsSync(registryPath)) {
|
|
135
|
+
return { requestedPath, resolvedPath, registryPath };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status }) {
|
|
142
|
+
return {
|
|
143
|
+
configuredPath,
|
|
144
|
+
searchedPaths,
|
|
145
|
+
status,
|
|
146
|
+
compatRecords: [],
|
|
147
|
+
compatRecordStatuses: {},
|
|
148
|
+
hookNames: [],
|
|
149
|
+
apiRegistrars: [],
|
|
150
|
+
capturedRegistrars: [],
|
|
151
|
+
sdkExports: [],
|
|
152
|
+
manifestFields: [],
|
|
153
|
+
manifestContractFields: [],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseCapturedRegistrars(source) {
|
|
158
|
+
return unique([...source.matchAll(/^\s*(register[A-Za-z0-9]+)\s*\(/gm)].map((match) => match[1])).sort();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseApiRegistrars(source) {
|
|
162
|
+
return unique([...source.matchAll(/\b(register[A-Za-z0-9]+)\b/g)].map((match) => match[1])).sort();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function relativePath(rootDir, filePath) {
|
|
166
|
+
return path.relative(rootDir, filePath).replaceAll("\\", "/");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function dedupeBy(values, keyForValue) {
|
|
170
|
+
const byKey = new Map();
|
|
171
|
+
for (const value of values) {
|
|
172
|
+
byKey.set(keyForValue(value), value);
|
|
173
|
+
}
|
|
174
|
+
return [...byKey.values()];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function unique(values) {
|
|
178
|
+
return [...new Set(values)];
|
|
179
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function resolveFromRoot(rootDir, value) {
|
|
4
|
+
return path.isAbsolute(value) ? value : path.join(rootDir, value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveRequiredFromRoot(rootDir, value, label) {
|
|
8
|
+
if (!value) {
|
|
9
|
+
throw new Error(`${label} path is required`);
|
|
10
|
+
}
|
|
11
|
+
return resolveFromRoot(rootDir, value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function toRepoPath(value) {
|
|
15
|
+
return normalizeRepoPath(value).replaceAll(path.sep, "/");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeRepoPath(value) {
|
|
19
|
+
return String(value).replaceAll("\\", "/");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function posixJoin(...parts) {
|
|
23
|
+
return parts.filter(Boolean).join("/").replace(/\/+/g, "/");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function slugForArtifact(value) {
|
|
27
|
+
return String(value).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
28
|
+
}
|