@openclaw/plugin-inspector 0.3.18 → 0.3.20
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 +23 -0
- package/README.md +14 -6
- package/examples/github-actions-code-scanning.yml +3 -3
- package/examples/github-actions-plugin-inspector.yml +3 -3
- package/package.json +7 -2
- package/src/advanced.js +7 -0
- package/src/api.js +4 -0
- package/src/batch.js +8 -0
- package/src/cli.js +33 -10
- package/src/compatibility-report.js +6 -0
- package/src/config.js +2 -1
- package/src/fixture-summary.js +162 -20
- package/src/index.js +12 -3
- package/src/init.js +3 -3
- package/src/inspector.js +170 -11
- package/src/issues.js +4 -5
- package/src/openclaw-target.js +202 -6
- package/src/openclaw-version.js +246 -0
- package/src/report.js +1 -0
package/src/fixture-summary.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readdir } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { compatRecordForIssueCode } from "./contract-probes.js";
|
|
5
5
|
import { readJsonFile } from "./json-file.js";
|
|
6
|
+
import { satisfiesOpenClawCompatibilityRange } from "./openclaw-version.js";
|
|
6
7
|
|
|
7
8
|
const conversationAccessHooks = new Set(["agent_end", "llm_input", "llm_output"]);
|
|
8
9
|
const captureGapRegistrations = new Set([
|
|
@@ -143,6 +144,8 @@ export function summarizePackage(packagePath, packageJson, options = {}) {
|
|
|
143
144
|
extensions: arrayValues(packageJson.openclaw.extensions),
|
|
144
145
|
runtimeExtensions: arrayValues(packageJson.openclaw.runtimeExtensions),
|
|
145
146
|
setupEntry: typeof packageJson.openclaw.setupEntry === "string" ? packageJson.openclaw.setupEntry : null,
|
|
147
|
+
runtimeSetupEntry:
|
|
148
|
+
typeof packageJson.openclaw.runtimeSetupEntry === "string" ? packageJson.openclaw.runtimeSetupEntry : null,
|
|
146
149
|
compatPluginApi:
|
|
147
150
|
typeof packageJson.openclaw.compat?.pluginApi === "string" ? packageJson.openclaw.compat.pluginApi : null,
|
|
148
151
|
buildOpenClawVersion:
|
|
@@ -464,30 +467,36 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
|
|
|
464
467
|
}
|
|
465
468
|
|
|
466
469
|
export function classifyTargetOpenClawCoverage({ fixture, inspection, fixtureReport, targetOpenClaw }) {
|
|
470
|
+
const breakages = [];
|
|
467
471
|
const warnings = [];
|
|
472
|
+
const suggestions = [];
|
|
468
473
|
const logs = [];
|
|
469
474
|
const decisions = [];
|
|
475
|
+
const findingContext = { breakages, warnings, suggestions, fixtureReport, targetOpenClaw };
|
|
470
476
|
|
|
471
|
-
classifyHookNameCoverage({ fixture, inspection, targetOpenClaw,
|
|
472
|
-
classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw,
|
|
473
|
-
classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions });
|
|
477
|
+
classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, logs, findingContext });
|
|
478
|
+
classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, logs, findingContext });
|
|
479
|
+
classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions, findingContext });
|
|
474
480
|
classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions });
|
|
475
481
|
|
|
476
|
-
return { warnings, logs, decisions };
|
|
482
|
+
return { breakages, warnings, suggestions, logs, decisions };
|
|
477
483
|
}
|
|
478
484
|
|
|
479
485
|
export function classifyCompatibilityFixture({ fixture, inspection, fixtureReport, targetOpenClaw }) {
|
|
486
|
+
const breakages = [];
|
|
480
487
|
const warnings = [];
|
|
481
488
|
const suggestions = [];
|
|
482
489
|
const logs = [];
|
|
483
490
|
const decisions = [];
|
|
484
491
|
|
|
485
492
|
if (inspection.status !== "ok") {
|
|
486
|
-
return { warnings, suggestions, logs, decisions };
|
|
493
|
+
return { breakages, warnings, suggestions, logs, decisions };
|
|
487
494
|
}
|
|
488
495
|
|
|
489
496
|
const targetCoverage = classifyTargetOpenClawCoverage({ fixture, inspection, fixtureReport, targetOpenClaw });
|
|
497
|
+
breakages.push(...targetCoverage.breakages);
|
|
490
498
|
warnings.push(...targetCoverage.warnings);
|
|
499
|
+
suggestions.push(...targetCoverage.suggestions);
|
|
491
500
|
logs.push(...targetCoverage.logs);
|
|
492
501
|
decisions.push(...targetCoverage.decisions);
|
|
493
502
|
|
|
@@ -701,7 +710,7 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
701
710
|
});
|
|
702
711
|
}
|
|
703
712
|
|
|
704
|
-
return { warnings, suggestions, logs, decisions };
|
|
713
|
+
return { breakages, warnings, suggestions, logs, decisions };
|
|
705
714
|
}
|
|
706
715
|
|
|
707
716
|
function classifySdkDeprecations({ fixture, inspection, fixtureReport, warnings, decisions }) {
|
|
@@ -807,7 +816,7 @@ function registrationCaptureGapDetails(inspection, targetOpenClaw) {
|
|
|
807
816
|
return apiRegistrationDetails.filter((registration) => captureGapRegistrations.has(registration.name));
|
|
808
817
|
}
|
|
809
818
|
|
|
810
|
-
function classifyHookNameCoverage({ fixture, inspection, targetOpenClaw,
|
|
819
|
+
function classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, logs, findingContext }) {
|
|
811
820
|
if (targetOpenClaw.status !== "ok" || targetOpenClaw.hookNames.length === 0) {
|
|
812
821
|
return;
|
|
813
822
|
}
|
|
@@ -825,16 +834,18 @@ function classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, warning
|
|
|
825
834
|
return;
|
|
826
835
|
}
|
|
827
836
|
|
|
828
|
-
|
|
837
|
+
addVersionDerivedFinding({
|
|
838
|
+
...findingContext,
|
|
839
|
+
finding: {
|
|
829
840
|
fixture: fixture.id,
|
|
830
841
|
code: "unknown-hook-name",
|
|
831
|
-
level: "warning",
|
|
832
842
|
message: "fixture registers hooks that are not present in the target OpenClaw hook registry",
|
|
833
843
|
evidence: detailEvidence(unknownHooks),
|
|
844
|
+
},
|
|
834
845
|
});
|
|
835
846
|
}
|
|
836
847
|
|
|
837
|
-
function classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw,
|
|
848
|
+
function classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, logs, findingContext }) {
|
|
838
849
|
if (targetOpenClaw.status !== "ok" || targetOpenClaw.apiRegistrars.length === 0) {
|
|
839
850
|
return;
|
|
840
851
|
}
|
|
@@ -855,16 +866,18 @@ function classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw,
|
|
|
855
866
|
return;
|
|
856
867
|
}
|
|
857
868
|
|
|
858
|
-
|
|
869
|
+
addVersionDerivedFinding({
|
|
870
|
+
...findingContext,
|
|
871
|
+
finding: {
|
|
859
872
|
fixture: fixture.id,
|
|
860
873
|
code: "unknown-registration-name",
|
|
861
|
-
level: "warning",
|
|
862
874
|
message: "fixture calls api.register* methods that are not present in the target OpenClaw plugin API builder",
|
|
863
875
|
evidence: detailEvidence(unknownRegistrations),
|
|
876
|
+
},
|
|
864
877
|
});
|
|
865
878
|
}
|
|
866
879
|
|
|
867
|
-
function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
|
|
880
|
+
function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions, findingContext }) {
|
|
868
881
|
if (targetOpenClaw.status !== "ok" || targetOpenClaw.sdkExports.length === 0 || fixtureReport.sdkImports.length === 0) {
|
|
869
882
|
return;
|
|
870
883
|
}
|
|
@@ -888,13 +901,15 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
888
901
|
}
|
|
889
902
|
|
|
890
903
|
if (unknownImports.length > 0) {
|
|
891
|
-
|
|
904
|
+
addVersionDerivedFinding({
|
|
905
|
+
...findingContext,
|
|
906
|
+
finding: {
|
|
892
907
|
fixture: fixture.id,
|
|
893
908
|
code: "sdk-export-missing",
|
|
894
|
-
level: "warning",
|
|
895
909
|
message: "fixture imports plugin SDK aliases that are not exported by the target OpenClaw package",
|
|
896
910
|
evidence: detailEvidence(unknownImports, "specifier"),
|
|
897
911
|
compatRecord: "plugin-sdk-export-aliases",
|
|
912
|
+
},
|
|
898
913
|
});
|
|
899
914
|
decisions.push({
|
|
900
915
|
fixture: fixture.id,
|
|
@@ -923,6 +938,52 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
923
938
|
}
|
|
924
939
|
}
|
|
925
940
|
|
|
941
|
+
function addVersionDerivedFinding({ finding, fixtureReport, targetOpenClaw, breakages, warnings, suggestions }) {
|
|
942
|
+
const compatibility = targetCompatibility(fixtureReport, targetOpenClaw);
|
|
943
|
+
if (!compatibility) {
|
|
944
|
+
warnings.push({ ...finding, level: "warning" });
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (compatibility.inDeclaredRange) {
|
|
949
|
+
breakages.push({
|
|
950
|
+
...finding,
|
|
951
|
+
level: "breakage",
|
|
952
|
+
compatibility,
|
|
953
|
+
authorRemediation: {
|
|
954
|
+
summary:
|
|
955
|
+
"Update the plugin to use APIs available in the target OpenClaw version, or narrow its declared compatibility range.",
|
|
956
|
+
},
|
|
957
|
+
authorRemediationDocs: false,
|
|
958
|
+
});
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
suggestions.push({
|
|
963
|
+
...finding,
|
|
964
|
+
level: "information",
|
|
965
|
+
message: `${finding.message}; the resolved target is outside the plugin's declared compatibility range`,
|
|
966
|
+
compatibility,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function targetCompatibility(fixtureReport, targetOpenClaw) {
|
|
971
|
+
const declaredRange = fixtureReport.package?.openclaw?.compatPluginApi;
|
|
972
|
+
const targetVersion = targetOpenClaw.version;
|
|
973
|
+
const evaluatedVersion = targetOpenClaw.eligibilityVersion ?? targetOpenClaw.version;
|
|
974
|
+
if (!declaredRange || !targetVersion || !evaluatedVersion || !targetOpenClaw.requestedVersion) return null;
|
|
975
|
+
return {
|
|
976
|
+
declaredRange,
|
|
977
|
+
targetVersion,
|
|
978
|
+
evaluatedVersion,
|
|
979
|
+
inDeclaredRange: satisfiesOpenClawCompatibilityRange({
|
|
980
|
+
targetVersion,
|
|
981
|
+
eligibilityVersion: evaluatedVersion,
|
|
982
|
+
range: declaredRange,
|
|
983
|
+
}),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
926
987
|
function classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
|
|
927
988
|
if (targetOpenClaw.status !== "ok" || targetOpenClaw.manifestFields.length === 0) {
|
|
928
989
|
return;
|
|
@@ -984,6 +1045,9 @@ function collectOpenClawEntrypoints(packageDir, openclaw, options) {
|
|
|
984
1045
|
...openclaw.extensions.map((specifier) => ({ kind: "extension", specifier })),
|
|
985
1046
|
...openclaw.runtimeExtensions.map((specifier) => ({ kind: "runtimeExtension", specifier })),
|
|
986
1047
|
...(openclaw.setupEntry ? [{ kind: "setupEntry", specifier: openclaw.setupEntry }] : []),
|
|
1048
|
+
...(openclaw.runtimeSetupEntry
|
|
1049
|
+
? [{ kind: "runtimeSetupEntry", specifier: openclaw.runtimeSetupEntry }]
|
|
1050
|
+
: []),
|
|
987
1051
|
];
|
|
988
1052
|
|
|
989
1053
|
return entrypoints.map((entrypoint) => {
|
|
@@ -1015,7 +1079,7 @@ function hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoin
|
|
|
1015
1079
|
return true;
|
|
1016
1080
|
}
|
|
1017
1081
|
|
|
1018
|
-
if (
|
|
1082
|
+
if (entrypoints.some((candidate) => candidate.exists && isPairedRuntimeEntrypoint(entrypoint, candidate))) {
|
|
1019
1083
|
return true;
|
|
1020
1084
|
}
|
|
1021
1085
|
|
|
@@ -1038,6 +1102,13 @@ function normalizeEntrypointSpecifier(specifier) {
|
|
|
1038
1102
|
return normalized.startsWith("./") ? normalized : `./${normalized}`;
|
|
1039
1103
|
}
|
|
1040
1104
|
|
|
1105
|
+
function isPairedRuntimeEntrypoint(entrypoint, candidate) {
|
|
1106
|
+
return (
|
|
1107
|
+
(entrypoint.kind === "extension" && candidate.kind === "runtimeExtension") ||
|
|
1108
|
+
(entrypoint.kind === "setupEntry" && candidate.kind === "runtimeSetupEntry")
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1041
1112
|
async function findPackageFiles(root, options, depth = 0) {
|
|
1042
1113
|
if (!existsSync(root) || depth > options.maxDepth) {
|
|
1043
1114
|
return [];
|
|
@@ -1227,9 +1298,10 @@ function hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
|
|
|
1227
1298
|
}
|
|
1228
1299
|
|
|
1229
1300
|
if (
|
|
1230
|
-
entrypoint.kind === "extension" &&
|
|
1231
1301
|
entrypoints.some(
|
|
1232
|
-
(candidate) =>
|
|
1302
|
+
(candidate) =>
|
|
1303
|
+
isPairedRuntimeEntrypoint(entrypoint, candidate) &&
|
|
1304
|
+
repoPathIncludedInNpmPack(packageSummary, candidate.relativePath),
|
|
1233
1305
|
)
|
|
1234
1306
|
) {
|
|
1235
1307
|
return true;
|
|
@@ -1245,12 +1317,82 @@ function packageMinHostVersionDrift(packageSummary) {
|
|
|
1245
1317
|
if (!nonEmptyString(openclaw?.install?.minHostVersion) || !nonEmptyString(openclaw?.buildOpenClawVersion)) {
|
|
1246
1318
|
return false;
|
|
1247
1319
|
}
|
|
1248
|
-
|
|
1320
|
+
const minimumHostVersion = parseMinHostVersionFloor(openclaw.install.minHostVersion);
|
|
1321
|
+
const buildOpenClawVersion = parseSemver(openclaw.buildOpenClawVersion);
|
|
1322
|
+
if (!minimumHostVersion || !buildOpenClawVersion) {
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
return compareSemver(minimumHostVersion, buildOpenClawVersion) > 0;
|
|
1249
1326
|
}
|
|
1250
1327
|
|
|
1251
1328
|
function parseMinHostVersionFloor(value) {
|
|
1252
1329
|
const match = /^>=([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/.exec(value);
|
|
1253
|
-
return match
|
|
1330
|
+
return match ? parseSemver(match[1]) : null;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function parseSemver(value) {
|
|
1334
|
+
const match =
|
|
1335
|
+
/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(
|
|
1336
|
+
value,
|
|
1337
|
+
);
|
|
1338
|
+
if (!match) {
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
const coreParts = match.slice(1, 4);
|
|
1342
|
+
if (coreParts.some((part) => part.length > 1 && part.startsWith("0"))) {
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
const prerelease = match[4]?.split(".") ?? [];
|
|
1346
|
+
if (prerelease.some((part) => /^[0-9]+$/.test(part) && part.length > 1 && part.startsWith("0"))) {
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
return { core: coreParts, prerelease };
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function compareNumericIdentifier(left, right) {
|
|
1353
|
+
if (left.length !== right.length) {
|
|
1354
|
+
return left.length < right.length ? -1 : 1;
|
|
1355
|
+
}
|
|
1356
|
+
if (left === right) {
|
|
1357
|
+
return 0;
|
|
1358
|
+
}
|
|
1359
|
+
return left < right ? -1 : 1;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function compareSemver(left, right) {
|
|
1363
|
+
for (let index = 0; index < left.core.length; index += 1) {
|
|
1364
|
+
const comparison = compareNumericIdentifier(left.core[index], right.core[index]);
|
|
1365
|
+
if (comparison !== 0) {
|
|
1366
|
+
return comparison;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (left.prerelease.length === 0 || right.prerelease.length === 0) {
|
|
1370
|
+
if (left.prerelease.length === right.prerelease.length) {
|
|
1371
|
+
return 0;
|
|
1372
|
+
}
|
|
1373
|
+
return left.prerelease.length === 0 ? 1 : -1;
|
|
1374
|
+
}
|
|
1375
|
+
const count = Math.max(left.prerelease.length, right.prerelease.length);
|
|
1376
|
+
for (let index = 0; index < count; index += 1) {
|
|
1377
|
+
const leftPart = left.prerelease[index];
|
|
1378
|
+
const rightPart = right.prerelease[index];
|
|
1379
|
+
if (leftPart === undefined || rightPart === undefined) {
|
|
1380
|
+
return leftPart === undefined ? -1 : 1;
|
|
1381
|
+
}
|
|
1382
|
+
if (leftPart === rightPart) {
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
const leftNumeric = /^[0-9]+$/.test(leftPart);
|
|
1386
|
+
const rightNumeric = /^[0-9]+$/.test(rightPart);
|
|
1387
|
+
if (leftNumeric && rightNumeric) {
|
|
1388
|
+
return compareNumericIdentifier(leftPart, rightPart);
|
|
1389
|
+
}
|
|
1390
|
+
if (leftNumeric !== rightNumeric) {
|
|
1391
|
+
return leftNumeric ? -1 : 1;
|
|
1392
|
+
}
|
|
1393
|
+
return leftPart < rightPart ? -1 : 1;
|
|
1394
|
+
}
|
|
1395
|
+
return 0;
|
|
1254
1396
|
}
|
|
1255
1397
|
|
|
1256
1398
|
function repoPathIncludedInNpmPack(packageSummary, repoPath) {
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,8 @@ import * as executionResultsApi from "./execution-results.js";
|
|
|
8
8
|
import * as importLoopProfileApi from "./import-loop-profile.js";
|
|
9
9
|
import * as inspectorApi from "./inspector.js";
|
|
10
10
|
import * as issuesApi from "./issues.js";
|
|
11
|
-
import * as openClawTargetApi from "./openclaw-
|
|
11
|
+
import * as openClawTargetApi from "./openclaw-version.js";
|
|
12
|
+
import * as openClawTargetSurfaceApi from "./openclaw-target.js";
|
|
12
13
|
import * as profileDiffApi from "./profile-diff.js";
|
|
13
14
|
import * as refDiffApi from "./ref-diff.js";
|
|
14
15
|
import * as reportApi from "./report.js";
|
|
@@ -67,8 +68,16 @@ export const reports = Object.freeze({
|
|
|
67
68
|
issueId: issuesApi.issueId,
|
|
68
69
|
classifyIssueFinding: issuesApi.classifyIssueFinding,
|
|
69
70
|
knownIssueCodes: issuesApi.knownIssueCodes,
|
|
70
|
-
openClawTargetPathCandidates:
|
|
71
|
-
readOpenClawTargetSurface:
|
|
71
|
+
openClawTargetPathCandidates: openClawTargetSurfaceApi.openClawTargetPathCandidates,
|
|
72
|
+
readOpenClawTargetSurface: openClawTargetSurfaceApi.readOpenClawTargetSurface,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const openClawTargets = Object.freeze({
|
|
76
|
+
resolveVersion: openClawTargetApi.resolveOpenClawTargetVersion,
|
|
77
|
+
prepare: openClawTargetApi.prepareOpenClawTarget,
|
|
78
|
+
eligibilityVersion: openClawTargetApi.openClawEligibilityVersion,
|
|
79
|
+
satisfiesCompatibilityRange: openClawTargetApi.satisfiesOpenClawCompatibilityRange,
|
|
80
|
+
satisfiesRange: openClawTargetApi.satisfiesOpenClawVersionRange,
|
|
72
81
|
});
|
|
73
82
|
|
|
74
83
|
export const contracts = Object.freeze({
|
package/src/init.js
CHANGED
|
@@ -108,14 +108,14 @@ jobs:
|
|
|
108
108
|
check:
|
|
109
109
|
runs-on: ubuntu-latest
|
|
110
110
|
steps:
|
|
111
|
-
- uses: actions/checkout@
|
|
112
|
-
- uses: actions/setup-node@
|
|
111
|
+
- uses: actions/checkout@v7
|
|
112
|
+
- uses: actions/setup-node@v7
|
|
113
113
|
with:
|
|
114
114
|
node-version: 24
|
|
115
115
|
cache: ${setup.cache}
|
|
116
116
|
${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.install}
|
|
117
117
|
- run: ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
|
|
118
|
-
- uses: actions/upload-artifact@
|
|
118
|
+
- uses: actions/upload-artifact@v7
|
|
119
119
|
if: always()
|
|
120
120
|
with:
|
|
121
121
|
name: plugin-inspector-reports
|
package/src/inspector.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import * as nodeModule from "node:module";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
7
|
import { promisify } from "node:util";
|
|
@@ -9,6 +10,7 @@ import { captureApiOptionsForPlugin } from "./capture-config.js";
|
|
|
9
10
|
import { fixtureCheckoutPath, fixtureSourceRoot } from "./config.js";
|
|
10
11
|
import { buildCompatibilityFixtureReport } from "./fixture-summary.js";
|
|
11
12
|
import { readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
13
|
+
import { prepareOpenClawTarget, resolveOpenClawTargetVersion } from "./openclaw-version.js";
|
|
12
14
|
import { buildCompatibilityReport, buildReport } from "./report.js";
|
|
13
15
|
import { inspectSdkDeprecations } from "./sdk-deprecation-rules.js";
|
|
14
16
|
|
|
@@ -26,11 +28,13 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
|
26
28
|
const { inspections, failures } = await inspectConfiguredFixtures(config, options);
|
|
27
29
|
const targetOpenClaw =
|
|
28
30
|
options.targetOpenClaw ??
|
|
29
|
-
(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
(options.openclawVersion
|
|
32
|
+
? await prepareOpenClawTarget(await resolveOpenClawTargetVersion(options.openclawVersion, options), options)
|
|
33
|
+
: await readOpenClawTargetSurface({
|
|
34
|
+
configuredPath: options.openclawPath,
|
|
35
|
+
manifest: config,
|
|
36
|
+
rootDir: config.rootDir,
|
|
37
|
+
}));
|
|
34
38
|
|
|
35
39
|
return buildCompatibilityReport({
|
|
36
40
|
config,
|
|
@@ -165,12 +169,7 @@ export function inspectSourceText(text, filePath = "source.js") {
|
|
|
165
169
|
...collectDetailedMatches(searchableText, /\b(createChatChannelPlugin)\s*\(/g, filePath, "name"),
|
|
166
170
|
...collectDetailedMatches(searchableText, /\b(definePluginEntry)\s*\(/g, filePath, "name"),
|
|
167
171
|
];
|
|
168
|
-
const sdkImports =
|
|
169
|
-
searchableText,
|
|
170
|
-
/(?:from\s*["'`]|import\(\s*["'`])([^"'`]*openclaw\/plugin-sdk[^"'`]*)/g,
|
|
171
|
-
filePath,
|
|
172
|
-
"specifier",
|
|
173
|
-
);
|
|
172
|
+
const sdkImports = collectSdkImports(searchableText, filePath);
|
|
174
173
|
const sdkDeprecations = inspectSdkDeprecations(searchableText, filePath);
|
|
175
174
|
|
|
176
175
|
return {
|
|
@@ -385,6 +384,165 @@ function collectDetailedMatches(text, regex, filePath, key) {
|
|
|
385
384
|
return details;
|
|
386
385
|
}
|
|
387
386
|
|
|
387
|
+
function collectSdkImports(text, filePath) {
|
|
388
|
+
const details = [];
|
|
389
|
+
for (const candidate of text.matchAll(/(?:^|;)[\t ]*(import|export)\b/gm)) {
|
|
390
|
+
const keyword = candidate[1];
|
|
391
|
+
const keywordIndex = (candidate.index ?? 0) + candidate[0].lastIndexOf(keyword);
|
|
392
|
+
const declaration = parseStaticModuleDeclaration(text, keywordIndex, keyword);
|
|
393
|
+
if (!declaration?.specifier.includes("openclaw/plugin-sdk")) continue;
|
|
394
|
+
if (isTypeOnlyStaticImportClause(declaration.clause)) continue;
|
|
395
|
+
const line = lineForOffset(text, keywordIndex);
|
|
396
|
+
details.push({
|
|
397
|
+
specifier: declaration.specifier,
|
|
398
|
+
file: filePath,
|
|
399
|
+
line,
|
|
400
|
+
ref: `${filePath}:${line}`,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const dynamicMatches = [...text.matchAll(/\bimport\(\s*["'`]([^"'`]*openclaw\/plugin-sdk[^"'`]*)/g)];
|
|
405
|
+
const runtimeDynamicImports = runtimeDynamicImportIndexes(text, dynamicMatches);
|
|
406
|
+
for (const [index, match] of dynamicMatches.entries()) {
|
|
407
|
+
if (!runtimeDynamicImports.has(index)) continue;
|
|
408
|
+
const line = lineForOffset(text, match.index ?? 0);
|
|
409
|
+
details.push({
|
|
410
|
+
specifier: match[1],
|
|
411
|
+
file: filePath,
|
|
412
|
+
line,
|
|
413
|
+
ref: `${filePath}:${line}`,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return details.sort((left, right) => left.line - right.line || left.specifier.localeCompare(right.specifier));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function parseStaticModuleDeclaration(text, keywordIndex, keyword) {
|
|
420
|
+
const clauseStart = keywordIndex + keyword.length;
|
|
421
|
+
let cursor = skipWhitespace(text, clauseStart);
|
|
422
|
+
if (text[cursor] === "(") return null;
|
|
423
|
+
if (keyword === "export" && text[cursor] !== "{" && text[cursor] !== "*" && !hasWordAt(text, cursor, "type")) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let braceDepth = 0;
|
|
428
|
+
while (cursor < text.length) {
|
|
429
|
+
const char = text[cursor];
|
|
430
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
431
|
+
cursor = skipQuotedText(text, cursor, char);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (char === "{") braceDepth += 1;
|
|
435
|
+
if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
436
|
+
if (char === ";" && braceDepth === 0) return null;
|
|
437
|
+
if (braceDepth === 0 && hasWordAt(text, cursor, "from")) {
|
|
438
|
+
const clause = text.slice(clauseStart, cursor).trim();
|
|
439
|
+
cursor = skipWhitespace(text, cursor + "from".length);
|
|
440
|
+
const quote = text[cursor];
|
|
441
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") return null;
|
|
442
|
+
const specifierStart = cursor + 1;
|
|
443
|
+
const specifierEnd = skipQuotedText(text, cursor, quote) - 1;
|
|
444
|
+
return { clause, specifier: text.slice(specifierStart, specifierEnd) };
|
|
445
|
+
}
|
|
446
|
+
cursor += 1;
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function skipWhitespace(text, index) {
|
|
452
|
+
while (index < text.length && /\s/.test(text[index])) index += 1;
|
|
453
|
+
return index;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function hasWordAt(text, index, word) {
|
|
457
|
+
return (
|
|
458
|
+
text.startsWith(word, index) &&
|
|
459
|
+
!/[A-Za-z0-9_$]/.test(text[index - 1] ?? "") &&
|
|
460
|
+
!/[A-Za-z0-9_$]/.test(text[index + word.length] ?? "")
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function skipQuotedText(text, quoteIndex, quote) {
|
|
465
|
+
let cursor = quoteIndex + 1;
|
|
466
|
+
while (cursor < text.length) {
|
|
467
|
+
if (text[cursor] === "\\") {
|
|
468
|
+
cursor += 2;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
cursor += 1;
|
|
472
|
+
if (text[cursor - 1] === quote) break;
|
|
473
|
+
}
|
|
474
|
+
return cursor;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function runtimeDynamicImportIndexes(text, matches) {
|
|
478
|
+
if (matches.length === 0) return new Set();
|
|
479
|
+
const markedImports = matches.map((match, index) => {
|
|
480
|
+
const specifier = match[1];
|
|
481
|
+
const specifierStart = (match.index ?? 0) + match[0].indexOf(specifier);
|
|
482
|
+
return {
|
|
483
|
+
index,
|
|
484
|
+
specifierStart,
|
|
485
|
+
specifierEnd: specifierStart + specifier.length,
|
|
486
|
+
marker: `${specifier}/__plugin_inspector_runtime_import_${index}__`,
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
let markedText = text;
|
|
490
|
+
for (const markedImport of markedImports.toReversed()) {
|
|
491
|
+
markedText =
|
|
492
|
+
markedText.slice(0, markedImport.specifierStart) +
|
|
493
|
+
markedImport.marker +
|
|
494
|
+
markedText.slice(markedImport.specifierEnd);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const runtimeText = eraseTypeScript(markedText);
|
|
499
|
+
if (runtimeText === null) {
|
|
500
|
+
return new Set(markedImports.map((markedImport) => markedImport.index));
|
|
501
|
+
}
|
|
502
|
+
return new Set(
|
|
503
|
+
markedImports.filter((markedImport) => runtimeText.includes(markedImport.marker)).map((markedImport) => markedImport.index),
|
|
504
|
+
);
|
|
505
|
+
} catch {
|
|
506
|
+
return new Set(markedImports.map((markedImport) => markedImport.index));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function eraseTypeScript(text) {
|
|
511
|
+
if (typeof nodeModule.stripTypeScriptTypes === "function") {
|
|
512
|
+
return nodeModule.stripTypeScriptTypes(text, { mode: "transform" });
|
|
513
|
+
}
|
|
514
|
+
if (typeof globalThis.Bun?.Transpiler === "function") {
|
|
515
|
+
const transpiler = new globalThis.Bun.Transpiler({ loader: "ts", target: "bun" });
|
|
516
|
+
return transpiler.transformSync(text);
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function isTypeOnlyStaticImportClause(clause) {
|
|
522
|
+
clause = clause.trim();
|
|
523
|
+
if (/^type\b/.test(clause)) {
|
|
524
|
+
const typeOnlyClause = clause.slice("type".length).trimStart();
|
|
525
|
+
return typeOnlyClause.length > 0 && !typeOnlyClause.startsWith(",");
|
|
526
|
+
}
|
|
527
|
+
if (!clause.startsWith("{") || !clause.endsWith("}")) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const namedImports = clause
|
|
532
|
+
.slice(1, -1)
|
|
533
|
+
.split(",")
|
|
534
|
+
.map((specifier) => specifier.trim())
|
|
535
|
+
.filter(Boolean);
|
|
536
|
+
return namedImports.length > 0 && namedImports.every(isTypeOnlyNamedImport);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isTypeOnlyNamedImport(specifier) {
|
|
540
|
+
const typeModifier = /^type\b/.exec(specifier);
|
|
541
|
+
if (!typeModifier) return false;
|
|
542
|
+
const importedName = specifier.slice(typeModifier[0].length).trimStart();
|
|
543
|
+
return importedName.length > 0 && !/^as\b/.test(importedName);
|
|
544
|
+
}
|
|
545
|
+
|
|
388
546
|
async function readManifestContracts(config, checkoutPath, sourceRoot) {
|
|
389
547
|
const manifests = new Set(
|
|
390
548
|
[path.join(sourceRoot, "openclaw.plugin.json"), path.join(checkoutPath, "openclaw.plugin.json")].filter(
|
|
@@ -436,6 +594,7 @@ async function readPackageMetadata(config, checkoutPath, sourceRoot) {
|
|
|
436
594
|
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.entry);
|
|
437
595
|
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.entrypoint);
|
|
438
596
|
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.setupEntry);
|
|
597
|
+
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.runtimeSetupEntry);
|
|
439
598
|
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.exports?.["."]?.import);
|
|
440
599
|
collectEntrypoint(entrypoints, entrypointFiles, packageDir, packageJson.exports?.["."]?.default);
|
|
441
600
|
collectEntrypoints(entrypoints, entrypointFiles, packageDir, packageJson.openclaw?.extensions);
|
package/src/issues.js
CHANGED
|
@@ -518,7 +518,10 @@ export function buildIssues({ breakages = [], warnings = [], suggestions = [], t
|
|
|
518
518
|
runtimeCoverage: finding.runtimeCoverage ?? null,
|
|
519
519
|
...(finding.authorRemediation
|
|
520
520
|
? {
|
|
521
|
-
authorRemediation:
|
|
521
|
+
authorRemediation:
|
|
522
|
+
finding.authorRemediationDocs === false
|
|
523
|
+
? finding.authorRemediation
|
|
524
|
+
: withAuthorRemediationDocs(finding.code, finding.authorRemediation),
|
|
522
525
|
}
|
|
523
526
|
: {}),
|
|
524
527
|
}));
|
|
@@ -583,10 +586,6 @@ export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
|
|
|
583
586
|
};
|
|
584
587
|
}
|
|
585
588
|
|
|
586
|
-
export function isInspectorGapFinding(finding, targetOpenClaw) {
|
|
587
|
-
return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
|
|
588
|
-
}
|
|
589
|
-
|
|
590
589
|
export function isAuthorFacingFinding(finding, targetOpenClaw) {
|
|
591
590
|
return Boolean(issueMetadata(finding, targetOpenClaw).authorRemediation);
|
|
592
591
|
}
|