@openclaw/plugin-inspector 0.3.19 → 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/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/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
  }));
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
2
+ import { readFile, readdir } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
5
  export const defaultOpenClawCheckoutPaths = ["./openclaw", "../openclaw"];
@@ -26,6 +26,10 @@ export async function readOpenClawTargetSurface(options = {}) {
26
26
  });
27
27
  }
28
28
 
29
+ if (match.kind === "package") {
30
+ return readPackedOpenClawTargetSurface({ rootDir, requestedPaths, ...match });
31
+ }
32
+
29
33
  const { requestedPath, resolvedPath, registryPath } = match;
30
34
  const hookTypesPath = path.join(resolvedPath, "src/plugins/hook-types.ts");
31
35
  const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
@@ -251,12 +255,191 @@ function findTargetCheckout(rootDir, requestedPaths) {
251
255
  const resolvedPath = path.resolve(rootDir, requestedPath);
252
256
  const registryPath = path.join(resolvedPath, "src/plugins/compat/registry.ts");
253
257
  if (existsSync(registryPath)) {
254
- return { requestedPath, resolvedPath, registryPath };
258
+ return { kind: "checkout", requestedPath, resolvedPath, registryPath };
259
+ }
260
+ if (existsSync(path.join(resolvedPath, "package.json")) && existsSync(path.join(resolvedPath, "dist"))) {
261
+ return { kind: "package", requestedPath, resolvedPath };
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+
267
+ async function readPackedOpenClawTargetSurface({ rootDir, requestedPaths, requestedPath, resolvedPath }) {
268
+ const packagePath = path.join(resolvedPath, "package.json");
269
+ const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
270
+ if (packageJson.name !== "openclaw") {
271
+ return emptyTargetSurface({ configuredPath: requestedPath, searchedPaths: requestedPaths, status: "missing" });
272
+ }
273
+
274
+ const distPath = path.join(resolvedPath, "dist");
275
+ const declarationFiles = await listDeclarationFiles(distPath);
276
+ const declarations = [];
277
+ for (const filePath of declarationFiles) {
278
+ declarations.push({ filePath, source: await readFile(filePath, "utf8") });
279
+ }
280
+ const apiDeclaration = declarations
281
+ .map((declaration) => ({
282
+ ...declaration,
283
+ values: parseObjectTypeFields(declaration.source, "OpenClawPluginApi", (value) => value.startsWith("register")),
284
+ }))
285
+ .sort((left, right) => right.values.length - left.values.length)[0];
286
+ const hookDeclaration = declarations.find((declaration) => declaration.source.includes("type PluginHookName ="));
287
+ const manifestDeclaration = declarations.find((declaration) => declaration.source.includes("type PluginManifestRecord ="));
288
+ const manifestContractDeclaration = declarations.find((declaration) => declaration.source.includes("type PluginManifestContracts ="));
289
+ const apiRegistrars = apiDeclaration?.values ?? [];
290
+ const hookNames = hookDeclaration ? parseStringUnion(hookDeclaration.source, "PluginHookName") : [];
291
+ const manifestFields = manifestDeclaration
292
+ ? parseObjectTypeFields(manifestDeclaration.source, "PluginManifestRecord")
293
+ : [];
294
+ const manifestContractFields = manifestContractDeclaration
295
+ ? parseObjectTypeFields(manifestContractDeclaration.source, "PluginManifestContracts")
296
+ : [];
297
+ const sdkExports = parsePluginSdkExports(packageJson);
298
+
299
+ return {
300
+ configuredPath: requestedPath,
301
+ searchedPaths: requestedPaths,
302
+ status: "ok",
303
+ version: packageJson.version ?? null,
304
+ compatRegistryPath: null,
305
+ compatRecordCount: 0,
306
+ compatRecords: [],
307
+ compatRecordStatuses: {},
308
+ hookTypesPath: hookDeclaration ? relativePath(rootDir, hookDeclaration.filePath) : null,
309
+ hookNameCount: hookNames.length,
310
+ hookNames,
311
+ apiBuilderPath: apiDeclaration ? relativePath(rootDir, apiDeclaration.filePath) : null,
312
+ apiRegistrarCount: apiRegistrars.length,
313
+ apiRegistrars,
314
+ capturedRegistrationPath: apiDeclaration ? relativePath(rootDir, apiDeclaration.filePath) : null,
315
+ capturedRegistrarCount: apiRegistrars.length,
316
+ capturedRegistrars: apiRegistrars,
317
+ packagePath: relativePath(rootDir, packagePath),
318
+ sdkExportCount: sdkExports.length,
319
+ sdkExports,
320
+ pluginSdkEntrypointsPath: null,
321
+ reservedSdkExportCount: 0,
322
+ reservedSdkExports: [],
323
+ supportedFacadeSdkExports: [],
324
+ publicPluginOwnedSdkExports: [],
325
+ manifestTypesPath: manifestDeclaration ? relativePath(rootDir, manifestDeclaration.filePath) : null,
326
+ manifestFieldCount: manifestFields.length,
327
+ manifestFields,
328
+ manifestContractFieldCount: manifestContractFields.length,
329
+ manifestContractFields,
330
+ };
331
+ }
332
+
333
+ async function listDeclarationFiles(rootDir) {
334
+ const files = [];
335
+ const entries = await readdir(rootDir, { withFileTypes: true });
336
+ for (const entry of entries) {
337
+ const entryPath = path.join(rootDir, entry.name);
338
+ if (entry.isDirectory()) {
339
+ files.push(...(await listDeclarationFiles(entryPath)));
340
+ } else if (entry.isFile() && entry.name.endsWith(".d.ts")) {
341
+ files.push(entryPath);
255
342
  }
256
343
  }
344
+ return files.sort();
345
+ }
346
+
347
+ function parseObjectTypeFields(source, typeName, filter = () => true) {
348
+ const body = readObjectTypeBody(source, typeName);
349
+ if (!body) return [];
350
+ return unique(parseTopLevelTypeProperties(body).filter(filter)).sort();
351
+ }
352
+
353
+ function parseTopLevelTypeProperties(body) {
354
+ const properties = [];
355
+ let braceDepth = 0;
356
+ let bracketDepth = 0;
357
+ let parenDepth = 0;
358
+ let propertyStart = true;
359
+
360
+ for (let index = 0; index < body.length; index += 1) {
361
+ const char = body[index];
362
+ const next = body[index + 1];
363
+ if (char === "/" && next === "*") {
364
+ index = body.indexOf("*/", index + 2);
365
+ if (index === -1) break;
366
+ index += 1;
367
+ continue;
368
+ }
369
+ if (char === "/" && next === "/") {
370
+ const newline = body.indexOf("\n", index + 2);
371
+ if (newline === -1) break;
372
+ index = newline;
373
+ continue;
374
+ }
375
+ if (char === '"' || char === "'" || char === "`") {
376
+ index = skipQuotedTypeText(body, index);
377
+ continue;
378
+ }
379
+ if (char === "{") braceDepth += 1;
380
+ else if (char === "}") braceDepth -= 1;
381
+ else if (char === "[") bracketDepth += 1;
382
+ else if (char === "]") bracketDepth -= 1;
383
+ else if (char === "(") parenDepth += 1;
384
+ else if (char === ")") parenDepth -= 1;
385
+
386
+ if (braceDepth !== 0 || bracketDepth !== 0 || parenDepth !== 0) continue;
387
+ if (char === ";" || char === ",") {
388
+ propertyStart = true;
389
+ continue;
390
+ }
391
+ if (/\s/.test(char)) continue;
392
+ if (!propertyStart || !/[A-Za-z_$]/.test(char)) {
393
+ propertyStart = false;
394
+ continue;
395
+ }
396
+
397
+ const match = body.slice(index).match(/^([A-Za-z_$][A-Za-z0-9_$]*)/);
398
+ if (!match) {
399
+ propertyStart = false;
400
+ continue;
401
+ }
402
+ const name = match[1];
403
+ index += name.length - 1;
404
+ if (name === "readonly") continue;
405
+ let cursor = index + 1;
406
+ while (/\s/.test(body[cursor] ?? "")) cursor += 1;
407
+ if (body[cursor] === "?") cursor += 1;
408
+ while (/\s/.test(body[cursor] ?? "")) cursor += 1;
409
+ if (body[cursor] === ":") properties.push(name);
410
+ propertyStart = false;
411
+ }
412
+ return properties;
413
+ }
414
+
415
+ function skipQuotedTypeText(source, quoteIndex) {
416
+ const quote = source[quoteIndex];
417
+ for (let index = quoteIndex + 1; index < source.length; index += 1) {
418
+ if (source[index] === "\\") index += 1;
419
+ else if (source[index] === quote) return index;
420
+ }
421
+ return source.length - 1;
422
+ }
423
+
424
+ function readObjectTypeBody(source, typeName) {
425
+ const marker = new RegExp(`(?:export\\s+)?type\\s+${typeName}(?:\\$\\d+)?\\s*=\\s*\\{`, "g");
426
+ const match = marker.exec(source);
427
+ if (!match) return null;
428
+ const start = match.index + match[0].length;
429
+ let depth = 1;
430
+ for (let index = start; index < source.length; index += 1) {
431
+ if (source[index] === "{") depth += 1;
432
+ if (source[index] === "}") depth -= 1;
433
+ if (depth === 0) return source.slice(start, index);
434
+ }
257
435
  return null;
258
436
  }
259
437
 
438
+ function parseStringUnion(source, typeName) {
439
+ const match = source.match(new RegExp(`type\\s+${typeName}\\s*=\\s*([^;]+);`));
440
+ return match ? unique([...match[1].matchAll(/["']([^"']+)["']/g)].map((item) => item[1])).sort() : [];
441
+ }
442
+
260
443
  function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status }) {
261
444
  return {
262
445
  configuredPath,