@openclaw/plugin-inspector 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -1,12 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ import path from "node:path";
2
3
  import {
4
+ loadPluginConfig,
3
5
  renderTextSummary,
4
6
  runPluginCheck,
5
7
  } from "./index.js";
6
8
  import {
9
+ buildCiSummary,
7
10
  captureEntrypoint,
11
+ defaultJunitPath,
12
+ defaultSarifPath,
13
+ inspectCompatibilityFixtureSet,
8
14
  inspectFixtureSet,
9
15
  loadInspectorConfig,
16
+ writeCiOutputArtifacts,
17
+ writeCiSummary,
18
+ writeCompatibilityReport,
10
19
  writePluginInspectorInit,
11
20
  writeArtifacts,
12
21
  writeReport,
@@ -23,8 +32,16 @@ try {
23
32
  await runCheck(commandArgs);
24
33
  } else if (command === "init") {
25
34
  await runInit(commandArgs);
26
- } else if (command === "inspect" || command === "report" || command === "ci") {
27
- await runReport(command, commandArgs);
35
+ } else if (command === "config") {
36
+ await runConfig(commandArgs);
37
+ } else if (command === "inspect" || command === "report") {
38
+ if (command === "inspect" && !commandArgs.includes("--config")) {
39
+ await runCheck(commandArgs);
40
+ } else {
41
+ await runReport(command, commandArgs);
42
+ }
43
+ } else if (command === "ci") {
44
+ await runCi(commandArgs);
28
45
  } else if (command === "capture") {
29
46
  await runCapture(commandArgs);
30
47
  } else {
@@ -35,6 +52,18 @@ try {
35
52
  process.exitCode = 1;
36
53
  }
37
54
 
55
+ async function runConfig(commandArgs) {
56
+ const configPath = readFlag(commandArgs, "--config");
57
+ const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
58
+ const config = await loadPluginConfig({ configPath, pluginRoot });
59
+
60
+ if (commandArgs.includes("--json")) {
61
+ console.log(JSON.stringify(config, null, 2));
62
+ } else {
63
+ console.log(renderConfigTextSummary(config));
64
+ }
65
+ }
66
+
38
67
  async function runCheck(commandArgs) {
39
68
  const configPath = readFlag(commandArgs, "--config");
40
69
  const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
@@ -43,7 +72,13 @@ async function runCheck(commandArgs) {
43
72
  const json = commandArgs.includes("--json");
44
73
  const capture = readRuntimeFlag(commandArgs);
45
74
  const mockSdk = readMockSdkFlag(commandArgs);
46
- const { report } = await runPluginCheck({ configPath, pluginRoot, outDir, openclawPath, capture, mockSdk });
75
+ const ciOutputs = readCiOutputFlags(commandArgs);
76
+ const { report, paths } = await runPluginCheck({ configPath, pluginRoot, outDir, openclawPath, capture, mockSdk });
77
+ await writeCiOutputArtifacts(report, {
78
+ ...ciOutputs,
79
+ cwd: path.dirname(paths.jsonPath),
80
+ outDir: ".",
81
+ });
47
82
 
48
83
  if (json) {
49
84
  console.log(JSON.stringify(report, null, 2));
@@ -80,9 +115,15 @@ async function runReport(command, commandArgs) {
80
115
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
81
116
  const check = commandArgs.includes("--check") || command === "ci";
82
117
  const json = commandArgs.includes("--json");
118
+ const ciOutputs = readCiOutputFlags(commandArgs);
83
119
  const config = await loadInspectorConfig(configPath);
84
120
  const report = await inspectFixtureSet(config);
85
- await writeReport(report, { outDir });
121
+ const paths = await writeReport(report, { outDir });
122
+ await writeCiOutputArtifacts(report, {
123
+ ...ciOutputs,
124
+ cwd: path.dirname(paths.jsonPath),
125
+ outDir: ".",
126
+ });
86
127
 
87
128
  if (json) {
88
129
  console.log(JSON.stringify(report, null, 2));
@@ -95,6 +136,72 @@ async function runReport(command, commandArgs) {
95
136
  }
96
137
  }
97
138
 
139
+ async function runCi(commandArgs) {
140
+ const configPath = readFlag(commandArgs, "--config");
141
+ const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
142
+ const outDir = readFlag(commandArgs, "--out") ?? "reports";
143
+ const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
144
+ const json = commandArgs.includes("--json");
145
+ const capture = readRuntimeFlag(commandArgs);
146
+ const mockSdk = readMockSdkFlag(commandArgs);
147
+ const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
148
+ const { report, reportDir } = await runCiCompatibilityReport({
149
+ capture,
150
+ configPath,
151
+ mockSdk,
152
+ openclawPath,
153
+ outDir,
154
+ pluginRoot,
155
+ });
156
+
157
+ const summary = await buildCiSummary({
158
+ artifactBaseDir: reportDir,
159
+ reportPaths: {
160
+ compatibility: "plugin-inspector-report.json",
161
+ },
162
+ reports: {
163
+ compatibility: report,
164
+ },
165
+ });
166
+ await writeCiSummary(summary, {
167
+ jsonPath: path.join(reportDir, "plugin-inspector-ci-summary.json"),
168
+ markdownPath: path.join(reportDir, "plugin-inspector-ci-summary.md"),
169
+ });
170
+ await writeCiOutputArtifacts(report, {
171
+ ...ciOutputs,
172
+ cwd: reportDir,
173
+ outDir: ".",
174
+ });
175
+
176
+ if (json) {
177
+ console.log(JSON.stringify(summary, null, 2));
178
+ } else {
179
+ console.log(renderCiTextSummary(summary));
180
+ }
181
+
182
+ if (summary.status !== "pass") {
183
+ throw new Error("plugin-inspector ci summary failed");
184
+ }
185
+ }
186
+
187
+ async function runCiCompatibilityReport({ capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
188
+ if (configPath) {
189
+ const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
190
+ const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
191
+ await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
192
+ return {
193
+ report,
194
+ reportDir: path.resolve(config.rootDir, outDir),
195
+ };
196
+ }
197
+
198
+ const { report } = await runPluginCheck({ pluginRoot, outDir, openclawPath, capture, mockSdk });
199
+ return {
200
+ report,
201
+ reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
202
+ };
203
+ }
204
+
98
205
  async function runCapture(commandArgs) {
99
206
  const entrypoint = commandArgs.find((arg) => !arg.startsWith("-"));
100
207
  const outputPath = readFlag(commandArgs, "--output");
@@ -124,6 +231,26 @@ function readFlag(commandArgs, name) {
124
231
  return commandArgs[index + 1] ?? null;
125
232
  }
126
233
 
234
+ function readOptionalPathFlag(commandArgs, name, defaultPath) {
235
+ const index = commandArgs.indexOf(name);
236
+ if (index === -1) {
237
+ return null;
238
+ }
239
+ const value = commandArgs[index + 1];
240
+ return value && !value.startsWith("-") ? value : defaultPath;
241
+ }
242
+
243
+ function readCiOutputFlags(commandArgs, options = {}) {
244
+ return {
245
+ sarifPath: commandArgs.includes("--no-sarif")
246
+ ? null
247
+ : (readOptionalPathFlag(commandArgs, "--sarif", defaultSarifPath) ?? (options.defaultEnabled ? defaultSarifPath : null)),
248
+ junitPath: commandArgs.includes("--no-junit")
249
+ ? null
250
+ : (readOptionalPathFlag(commandArgs, "--junit", defaultJunitPath) ?? (options.defaultEnabled ? defaultJunitPath : null)),
251
+ };
252
+ }
253
+
127
254
  function readRuntimeFlag(commandArgs) {
128
255
  if (commandArgs.includes("--runtime") || commandArgs.includes("--capture")) {
129
256
  return true;
@@ -154,19 +281,43 @@ function readMockSdkFlag(commandArgs) {
154
281
  return undefined;
155
282
  }
156
283
 
284
+ function renderCiTextSummary(summary) {
285
+ return [
286
+ `Status: ${summary.status.toUpperCase()}`,
287
+ `Breakages: ${summary.summary.breakages}`,
288
+ `Issues: ${summary.summary.issues}`,
289
+ `Artifacts: ${Object.values(summary.artifacts).filter(Boolean).length}`,
290
+ ].join("\n");
291
+ }
292
+
293
+ function renderConfigTextSummary(config) {
294
+ const fixture = config.fixtures[0];
295
+ return [
296
+ `Plugin: ${fixture.id}`,
297
+ `Root: ${config.rootDir}`,
298
+ `Config: ${config.configPath ?? "auto"}`,
299
+ `Priority: ${fixture.priority}`,
300
+ `Seams: ${fixture.seams.join(", ")}`,
301
+ `Runtime capture: ${config.capture?.runtime === true ? "on" : "off"}`,
302
+ `Mock SDK: ${config.capture?.mockSdk === false ? "off" : "on"}`,
303
+ ].join("\n");
304
+ }
305
+
157
306
  function printHelp() {
158
307
  console.log(`plugin-inspector
159
308
 
160
309
  Usage:
161
310
  plugin-inspector
162
311
  plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json]
312
+ plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
163
313
  plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--package-manager npm|pnpm|yarn|bun] [--force]
164
314
  plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
165
- plugin-inspector inspect --config <path> [--out <dir>] [--check] [--json]
166
- plugin-inspector ci --config <path> [--out <dir>]
315
+ plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]]
316
+ plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json] [--no-sarif] [--no-junit]
167
317
  PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--plugin-root <path>] [--output <path>]
168
318
 
169
319
  Default check runs from the current plugin root and writes reports/ unless --out is set.
320
+ CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
170
321
  Runtime capture is opt-in because it imports plugin code; use --runtime with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
171
322
  `);
172
323
  }
package/src/config.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
 
5
5
  export const npmPackagePayloadDir = ".crabpot-package";
6
6
  export const defaultPluginRootConfigFiles = ["plugin-inspector.config.json", ".plugin-inspector.json"];
7
+ export const packageJsonConfigKeys = ["pluginInspector", "plugin-inspector"];
7
8
 
8
9
  export async function loadInspectorConfig(configPath, options = {}) {
9
10
  if (!configPath) {
@@ -24,16 +25,23 @@ export async function loadInspectorConfig(configPath, options = {}) {
24
25
  export async function loadPluginRootConfig(configPath = null, options = {}) {
25
26
  const rootDir = path.resolve(options.cwd ?? process.cwd());
26
27
  const resolvedPath = configPath ? path.resolve(rootDir, configPath) : findPluginRootConfigPath(rootDir);
27
- if (!resolvedPath && !existsSync(path.join(rootDir, "package.json")) && !existsSync(path.join(rootDir, "openclaw.plugin.json"))) {
28
+ const packageJsonPath = path.join(rootDir, "package.json");
29
+ const packageJson = await readJsonIfExists(packageJsonPath);
30
+ const packageConfig = packageJsonConfig(packageJson);
31
+
32
+ if (!resolvedPath && !packageJson && !existsSync(path.join(rootDir, "openclaw.plugin.json"))) {
28
33
  throw new Error("run from a plugin root with package.json/openclaw.plugin.json, or pass --config");
29
34
  }
30
- const config = resolvedPath ? JSON.parse(await readFile(resolvedPath, "utf8")) : { version: 1 };
35
+
36
+ const config = resolvedPath
37
+ ? JSON.parse(await readFile(resolvedPath, "utf8"))
38
+ : (packageConfig.config ?? { version: 1 });
31
39
  const normalizedConfig = await normalizePluginRootConfig(config, { rootDir });
32
40
  validateInspectorConfig(normalizedConfig);
33
41
  return {
34
42
  ...normalizedConfig,
35
43
  rootDir,
36
- configPath: resolvedPath,
44
+ configPath: resolvedPath ?? packageConfig.configPath,
37
45
  };
38
46
  }
39
47
 
@@ -164,6 +172,25 @@ function findPluginRootConfigPath(rootDir) {
164
172
  return defaultPluginRootConfigFiles.map((file) => path.join(rootDir, file)).find(existsSync) ?? null;
165
173
  }
166
174
 
175
+ function packageJsonConfig(packageJson) {
176
+ if (!packageJson) {
177
+ return { config: null, configPath: null };
178
+ }
179
+ for (const key of packageJsonConfigKeys) {
180
+ if (packageJson[key] === undefined) {
181
+ continue;
182
+ }
183
+ if (!packageJson[key] || typeof packageJson[key] !== "object" || Array.isArray(packageJson[key])) {
184
+ throw new Error(`package.json ${key} must be an object`);
185
+ }
186
+ return {
187
+ config: packageJson[key],
188
+ configPath: `package.json#${key}`,
189
+ };
190
+ }
191
+ return { config: null, configPath: null };
192
+ }
193
+
167
194
  async function readJsonIfExists(filePath) {
168
195
  if (!existsSync(filePath)) {
169
196
  return null;
@@ -29,6 +29,11 @@ export const contractProbeRules = {
29
29
  contract: "Every observed OpenClaw plugin SDK import remains exported by the target OpenClaw package.",
30
30
  target: "sdk-alias",
31
31
  },
32
+ "reserved-sdk-import": {
33
+ id: "sdk.import.reserved-bundled-plugin-boundary",
34
+ contract: "External plugins use documented public SDK subpaths instead of reserved bundled-plugin compatibility shims.",
35
+ target: "sdk-import",
36
+ },
32
37
  "provider-auth-env-vars": {
33
38
  id: "manifest.compat.provider-auth-env-vars",
34
39
  contract: "Legacy provider auth env metadata continues to map into config/help surfaces.",
@@ -626,7 +626,12 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
626
626
 
627
627
  const sdkExports = new Set(targetOpenClaw.sdkExports);
628
628
  const unknownImports = fixtureReport.sdkImportDetails.filter((sdkImport) => !sdkExports.has(sdkImport.specifier));
629
- if (unknownImports.length === 0) {
629
+ const reservedSdkExports = new Set(targetOpenClaw.reservedSdkExports ?? []);
630
+ const reservedImports = fixtureReport.sdkImportDetails.filter((sdkImport) =>
631
+ reservedSdkExports.has(sdkImport.specifier),
632
+ );
633
+
634
+ if (reservedImports.length === 0 && unknownImports.length === 0) {
630
635
  logs.push({
631
636
  fixture: fixture.id,
632
637
  code: "sdk-exports-present",
@@ -637,21 +642,40 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
637
642
  return;
638
643
  }
639
644
 
640
- warnings.push({
641
- fixture: fixture.id,
642
- code: "sdk-export-missing",
643
- level: "warning",
644
- message: "fixture imports plugin SDK aliases that are not exported by the target OpenClaw package",
645
- evidence: detailEvidence(unknownImports, "specifier"),
646
- compatRecord: "plugin-sdk-export-aliases",
647
- });
648
- decisions.push({
649
- fixture: fixture.id,
650
- decision: "core-compat-adapter",
651
- seam: "sdk-alias",
652
- action: "Restore the package export alias or publish a versioned migration map before cold-importing old plugins.",
653
- evidence: unique(unknownImports.map((sdkImport) => sdkImport.specifier)).join(", "),
654
- });
645
+ if (unknownImports.length > 0) {
646
+ warnings.push({
647
+ fixture: fixture.id,
648
+ code: "sdk-export-missing",
649
+ level: "warning",
650
+ message: "fixture imports plugin SDK aliases that are not exported by the target OpenClaw package",
651
+ evidence: detailEvidence(unknownImports, "specifier"),
652
+ compatRecord: "plugin-sdk-export-aliases",
653
+ });
654
+ decisions.push({
655
+ fixture: fixture.id,
656
+ decision: "core-compat-adapter",
657
+ seam: "sdk-alias",
658
+ action: "Restore the package export alias or publish a versioned migration map before cold-importing old plugins.",
659
+ evidence: unique(unknownImports.map((sdkImport) => sdkImport.specifier)).join(", "),
660
+ });
661
+ }
662
+
663
+ if (reservedImports.length > 0) {
664
+ warnings.push({
665
+ fixture: fixture.id,
666
+ code: "reserved-sdk-import",
667
+ level: "warning",
668
+ message: "fixture imports reserved bundled-plugin SDK compatibility subpaths",
669
+ evidence: detailEvidence(reservedImports, "specifier"),
670
+ });
671
+ decisions.push({
672
+ fixture: fixture.id,
673
+ decision: "plugin-upstream-fix",
674
+ seam: "sdk-import",
675
+ action: "Move the plugin to documented public SDK subpaths or plugin-local helpers before relying on this compatibility shim.",
676
+ evidence: unique(reservedImports.map((sdkImport) => sdkImport.specifier)).join(", "),
677
+ });
678
+ }
655
679
  }
656
680
 
657
681
  function classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
package/src/index.js CHANGED
@@ -7,5 +7,6 @@ export {
7
7
  renderTextSummary,
8
8
  runPluginCheck,
9
9
  setupPluginInspector,
10
+ writeCiOutputArtifacts,
10
11
  writePluginReports,
11
12
  } from "./api.js";
package/src/init.js CHANGED
@@ -79,8 +79,7 @@ jobs:
79
79
  node-version: 24
80
80
  cache: ${setup.cache}
81
81
  ${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.install}
82
- - run: ${setup.exec} @openclaw/plugin-inspector check --no-openclaw
83
- - run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 ${setup.exec} @openclaw/plugin-inspector check --no-openclaw --runtime --mock-sdk
82
+ - run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
84
83
  - uses: actions/upload-artifact@v5
85
84
  if: always()
86
85
  with:
package/src/issues.js CHANGED
@@ -32,6 +32,7 @@ export const knownIssueCodes = new Set([
32
32
  "provider-auth-env-vars",
33
33
  "registration-capture-gap",
34
34
  "runtime-tool-capture",
35
+ "reserved-sdk-import",
35
36
  "sdk-export-missing",
36
37
  ]);
37
38
 
@@ -78,6 +79,12 @@ export const issueMetadataByCode = {
78
79
  decision: "core-compat-adapter",
79
80
  title: "plugin SDK import aliases are missing from target package exports",
80
81
  },
82
+ "reserved-sdk-import": {
83
+ severity: "P1",
84
+ owner: "plugin",
85
+ decision: "plugin-upstream-fix",
86
+ title: "plugin imports reserved bundled-plugin SDK compatibility subpaths",
87
+ },
81
88
  "missing-compat-record": {
82
89
  severity: "P1",
83
90
  owner: "core",
@@ -310,6 +317,7 @@ function issueClassFor(code, options) {
310
317
  "package-openclaw-entry-missing",
311
318
  "package-openclaw-metadata-missing",
312
319
  "package-plugin-api-compat-missing",
320
+ "reserved-sdk-import",
313
321
  ].includes(code)
314
322
  ) {
315
323
  return "upstream-metadata";
@@ -31,6 +31,7 @@ export async function readOpenClawTargetSurface(options = {}) {
31
31
  const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
32
32
  const capturedRegistrationPath = path.join(resolvedPath, "src/plugins/captured-registration.ts");
33
33
  const manifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
34
+ const pluginSdkEntrypointsPath = path.join(resolvedPath, "src/plugin-sdk/entrypoints.ts");
34
35
  const packagePath = path.join(resolvedPath, "package.json");
35
36
 
36
37
  const registrySource = await readFile(registryPath, "utf8");
@@ -48,6 +49,18 @@ export async function readOpenClawTargetSurface(options = {}) {
48
49
  const sdkExports = existsSync(packagePath)
49
50
  ? parsePluginSdkExports(JSON.parse(await readFile(packagePath, "utf8")))
50
51
  : [];
52
+ const pluginSdkEntrypointsSource = existsSync(pluginSdkEntrypointsPath)
53
+ ? await readFile(pluginSdkEntrypointsPath, "utf8")
54
+ : "";
55
+ const reservedSdkExports = pluginSdkEntrypointsSource
56
+ ? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "reservedBundledPluginSdkEntrypoints")
57
+ : [];
58
+ const supportedFacadeSdkExports = pluginSdkEntrypointsSource
59
+ ? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "supportedBundledFacadeSdkEntrypoints")
60
+ : [];
61
+ const publicPluginOwnedSdkExports = pluginSdkEntrypointsSource
62
+ ? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "publicPluginOwnedSdkEntrypoints")
63
+ : [];
51
64
 
52
65
  return {
53
66
  configuredPath: requestedPath,
@@ -69,6 +82,13 @@ export async function readOpenClawTargetSurface(options = {}) {
69
82
  packagePath: existsSync(packagePath) ? relativePath(rootDir, packagePath) : null,
70
83
  sdkExportCount: sdkExports.length,
71
84
  sdkExports,
85
+ pluginSdkEntrypointsPath: existsSync(pluginSdkEntrypointsPath)
86
+ ? relativePath(rootDir, pluginSdkEntrypointsPath)
87
+ : null,
88
+ reservedSdkExportCount: reservedSdkExports.length,
89
+ reservedSdkExports,
90
+ supportedFacadeSdkExports,
91
+ publicPluginOwnedSdkExports,
72
92
  manifestTypesPath: existsSync(manifestTypesPath) ? relativePath(rootDir, manifestTypesPath) : null,
73
93
  manifestFieldCount: manifestFields.length,
74
94
  manifestFields,
@@ -149,11 +169,18 @@ function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status
149
169
  apiRegistrars: [],
150
170
  capturedRegistrars: [],
151
171
  sdkExports: [],
172
+ reservedSdkExports: [],
173
+ supportedFacadeSdkExports: [],
174
+ publicPluginOwnedSdkExports: [],
152
175
  manifestFields: [],
153
176
  manifestContractFields: [],
154
177
  };
155
178
  }
156
179
 
180
+ export function parsePluginSdkEntrypointSpecifiers(source, exportName) {
181
+ return parseExportedStringArray(source, exportName).map((entrypoint) => `openclaw/plugin-sdk/${entrypoint}`).sort();
182
+ }
183
+
157
184
  function parseCapturedRegistrars(source) {
158
185
  return unique([...source.matchAll(/^\s*(register[A-Za-z0-9]+)\s*\(/gm)].map((match) => match[1])).sort();
159
186
  }
package/src/report.js CHANGED
@@ -160,6 +160,7 @@ export async function buildCompatibilityReport(options = {}) {
160
160
  warningCount: warnings.length,
161
161
  suggestionCount: suggestions.length,
162
162
  decisionCount: decisions.length,
163
+ logCount: logs.length,
163
164
  issueCount: issues.length,
164
165
  p0IssueCount: issues.filter((issue) => issue.severity === "P0").length,
165
166
  p1IssueCount: issues.filter((issue) => issue.severity === "P1").length,
package/src/sdk-mock.js CHANGED
@@ -718,6 +718,37 @@ function mockSdkSource() {
718
718
  return typeof entry === "function" ? { register: entry } : entry;
719
719
  }
720
720
 
721
+ function isPlainObject(value) {
722
+ return value !== null && typeof value === "object" && !Array.isArray(value);
723
+ }
724
+
725
+ function parseWithSchema(schema, value) {
726
+ return schema && typeof schema.parse === "function" ? schema.parse(value) : value;
727
+ }
728
+
729
+ function createConfigSchema(schema = {}) {
730
+ if (schema && typeof schema.parse === "function") {
731
+ return schema;
732
+ }
733
+ const shape = isPlainObject(schema?.shape) ? schema.shape : isPlainObject(schema?.properties) ? schema.properties : schema;
734
+ return {
735
+ ...schema,
736
+ parse(value = {}) {
737
+ if (!isPlainObject(shape)) {
738
+ return isPlainObject(value) ? value : {};
739
+ }
740
+ const source = isPlainObject(value) ? value : {};
741
+ const output = { ...source };
742
+ for (const [key, fieldSchema] of Object.entries(shape)) {
743
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
744
+ output[key] = parseWithSchema(fieldSchema, source[key]);
745
+ }
746
+ }
747
+ return output;
748
+ },
749
+ };
750
+ }
751
+
721
752
  export function definePluginEntry(entry) {
722
753
  return normalizeEntry(entry);
723
754
  }
@@ -759,13 +790,13 @@ export function defineSingleProviderPluginEntry(options) {
759
790
  }
760
791
 
761
792
  export function buildPluginConfigSchema(schema = {}) {
762
- return schema;
793
+ return createConfigSchema(schema);
763
794
  }
764
795
 
765
- export const emptyPluginConfigSchema = { type: "object", properties: {}, additionalProperties: false };
796
+ export const emptyPluginConfigSchema = createConfigSchema({ type: "object", properties: {}, additionalProperties: false });
766
797
 
767
798
  export function buildChannelConfigSchema(schema = {}) {
768
- return schema;
799
+ return createConfigSchema(schema);
769
800
  }
770
801
 
771
802
  export const emptyChannelConfigSchema = emptyPluginConfigSchema;
@@ -951,14 +982,28 @@ export function createAuthRateLimiter() {
951
982
  }
952
983
 
953
984
  export function createProviderApiKeyAuthMethod(options = {}) {
954
- return { type: "apiKey", ...options };
985
+ return {
986
+ id: options.id ?? "apiKey",
987
+ type: "apiKey",
988
+ ...options,
989
+ async resolve(ctx = {}) {
990
+ return ctx.apiKey ?? ctx.key ?? ctx.token ?? null;
991
+ },
992
+ };
955
993
  }
956
994
 
957
995
  export function buildSingleProviderApiKeyCatalog(options = {}) {
996
+ const auth = options.auth ?? createProviderApiKeyAuthMethod(options.authOptions);
958
997
  return {
998
+ auth,
959
999
  order: "simple",
960
1000
  async run(ctx) {
961
- return { provider: await options.buildProvider?.(ctx) };
1001
+ const provider = (await options.buildProvider?.(ctx)) ?? options.provider ?? { id: options.id ?? "provider", auth };
1002
+ return {
1003
+ provider,
1004
+ providers: [provider],
1005
+ models: (await options.buildModels?.(ctx)) ?? options.models ?? [],
1006
+ };
962
1007
  },
963
1008
  };
964
1009
  }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { register } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { captureEntrypoint, runCapturedSyntheticProbes, writeArtifacts } from "./advanced.js";
8
+ import { createMockSdkPackage } from "./sdk-mock.js";
9
+
10
+ const args = process.argv.slice(2);
11
+
12
+ try {
13
+ await run(args);
14
+ } catch (error) {
15
+ console.error(error.message);
16
+ process.exitCode = 1;
17
+ }
18
+
19
+ async function run(commandArgs) {
20
+ const entrypoint = readFlag(commandArgs, "--entrypoint") ?? commandArgs.find((arg) => !arg.startsWith("-"));
21
+ const outputPath = readFlag(commandArgs, "--output");
22
+ const pluginRoot = readFlag(commandArgs, "--plugin-root");
23
+ const includeLifecycle = commandArgs.includes("--include-lifecycle");
24
+ const includeChannelRuntime = commandArgs.includes("--include-channel-runtime");
25
+ const includeProviderCapabilities = commandArgs.includes("--include-provider-capabilities");
26
+ const mockSdk = readMockSdkFlag(commandArgs) ?? true;
27
+
28
+ if (!entrypoint) {
29
+ throw new Error("synthetic probes require --entrypoint <path>");
30
+ }
31
+ if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
32
+ throw new Error("synthetic probes import plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
33
+ }
34
+
35
+ const capture = await captureForSyntheticProbes(entrypoint, {
36
+ mockSdk,
37
+ pluginRoot,
38
+ apiOptions: { retainHandlers: true },
39
+ });
40
+ const results = await runCapturedSyntheticProbes(capture, {
41
+ includeLifecycle,
42
+ includeChannelRuntime,
43
+ includeProviderCapabilities,
44
+ });
45
+ const json = `${JSON.stringify(results, null, 2)}\n`;
46
+
47
+ if (outputPath) {
48
+ await writeArtifacts([{ path: outputPath, content: json }]);
49
+ } else {
50
+ process.stdout.write(json);
51
+ }
52
+ }
53
+
54
+ async function captureForSyntheticProbes(entrypoint, options) {
55
+ if (options.mockSdk !== true) {
56
+ return captureEntrypoint(entrypoint, options);
57
+ }
58
+
59
+ const resolvedEntrypoint = path.resolve(process.cwd(), entrypoint);
60
+ const pluginRoot = path.resolve(process.cwd(), options.pluginRoot ?? path.dirname(resolvedEntrypoint));
61
+ const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-mock-sdk-"));
62
+ try {
63
+ const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
64
+ register(pathToFileURL(loaderPath));
65
+ return captureEntrypoint(entrypoint, {
66
+ ...options,
67
+ mockSdk: false,
68
+ pluginRoot,
69
+ });
70
+ } finally {
71
+ await rm(workspace, { force: true, recursive: true });
72
+ }
73
+ }
74
+
75
+ function readFlag(commandArgs, name) {
76
+ const index = commandArgs.indexOf(name);
77
+ if (index === -1) {
78
+ return null;
79
+ }
80
+ return commandArgs[index + 1] ?? null;
81
+ }
82
+
83
+ function readMockSdkFlag(commandArgs) {
84
+ const sdk = readFlag(commandArgs, "--sdk");
85
+ if (sdk === "mock") {
86
+ return true;
87
+ }
88
+ if (sdk === "real") {
89
+ return false;
90
+ }
91
+ if (sdk && !["mock", "real"].includes(sdk)) {
92
+ throw new Error("--sdk must be mock or real");
93
+ }
94
+ if (commandArgs.includes("--mock-sdk")) {
95
+ return true;
96
+ }
97
+ if (commandArgs.includes("--real-sdk")) {
98
+ return false;
99
+ }
100
+ return undefined;
101
+ }