@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.
@@ -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, warnings, logs });
472
- classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs });
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, warnings, logs }) {
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
- warnings.push({
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, warnings, logs }) {
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
- warnings.push({
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
- warnings.push({
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 (entrypoint.kind === "extension" && entrypoints.some((candidate) => candidate.kind === "runtimeExtension" && candidate.exists)) {
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) => candidate.kind === "runtimeExtension" && repoPathIncludedInNpmPack(packageSummary, candidate.relativePath),
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
- return parseMinHostVersionFloor(openclaw.install.minHostVersion) !== openclaw.buildOpenClawVersion;
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?.[1] ?? null;
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-target.js";
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: openClawTargetApi.openClawTargetPathCandidates,
71
- readOpenClawTargetSurface: openClawTargetApi.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@v5
112
- - uses: actions/setup-node@v5
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@v5
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
- (await readOpenClawTargetSurface({
30
- configuredPath: options.openclawPath,
31
- manifest: config,
32
- rootDir: config.rootDir,
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 = collectDetailedMatches(
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: withAuthorRemediationDocs(finding.code, finding.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
  }