@openclaw/plugin-inspector 0.1.3 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - 2026-04-27
4
+
5
+ ### Added
6
+
7
+ - Add package.json `pluginInspector` config discovery for plugin-root checks.
8
+ - Add `plugin-inspector config` for resolved plugin-root config summaries.
9
+ - Add author-facing `plugin-inspector inspect` plugin-root flow.
10
+ - Add CI-native SARIF and JUnit outputs; `plugin-inspector ci` writes them by default.
11
+
12
+ ### Changed
13
+
14
+ - Make generated CI workflows use one `plugin-inspector ci --no-openclaw --runtime --mock-sdk` command.
15
+ - Harden runtime capture for string handler registrations, parse-capable config schema helpers, and provider auth/catalog SDK mocks.
16
+
3
17
  ## 0.1.3 - 2026-04-27
4
18
 
5
19
  ### Added
package/README.md CHANGED
@@ -35,17 +35,39 @@ npx plugin-inspector check
35
35
 
36
36
  ```bash
37
37
  npx @openclaw/plugin-inspector check
38
+ npx @openclaw/plugin-inspector inspect
39
+ npx @openclaw/plugin-inspector ci --no-openclaw
40
+ npx @openclaw/plugin-inspector config
38
41
  npx @openclaw/plugin-inspector check --plugin-root ./plugins/weather
39
42
  npx @openclaw/plugin-inspector init --ci --package-manager pnpm
40
43
  ```
41
44
 
42
- `check` reads the current directory as one plugin unless `--plugin-root` is set.
43
- It writes:
45
+ `check` and `inspect` read the current directory as one plugin unless
46
+ `--plugin-root` is set. `inspect` is the friendly author-facing alias; `check`
47
+ is kept for scripts. Both write:
44
48
 
45
49
  - `reports/plugin-inspector-report.json`
46
50
  - `reports/plugin-inspector-report.md`
47
51
  - `reports/plugin-inspector-issues.md`
48
52
 
53
+ Use CI-native outputs when you want annotations or test-summary ingestion:
54
+
55
+ ```bash
56
+ plugin-inspector inspect --no-openclaw --sarif --junit
57
+ ```
58
+
59
+ That also writes:
60
+
61
+ - `reports/plugin-inspector.sarif`
62
+ - `reports/plugin-inspector.junit.xml`
63
+
64
+ `config` prints the resolved plugin root, fixture id, seams, and capture
65
+ settings before CI runs:
66
+
67
+ ```bash
68
+ plugin-inspector config --json
69
+ ```
70
+
49
71
  Use `--no-openclaw` when CI should not compare against a local OpenClaw
50
72
  checkout:
51
73
 
@@ -53,8 +75,9 @@ checkout:
53
75
  plugin-inspector check --no-openclaw
54
76
  ```
55
77
 
56
- Use `plugin-inspector.config.json` when CI needs stable fixture metadata,
57
- expected seams, or runtime capture defaults:
78
+ Use either `plugin-inspector.config.json` or a `package.json`
79
+ `pluginInspector` block when CI needs stable fixture metadata, expected seams,
80
+ or runtime capture defaults:
58
81
 
59
82
  ```json
60
83
  {
@@ -83,9 +106,32 @@ Then run:
83
106
  plugin-inspector check --config plugin-inspector.config.json
84
107
  ```
85
108
 
109
+ For a single plugin package, the same config can live in `package.json`:
110
+
111
+ ```json
112
+ {
113
+ "scripts": {
114
+ "plugin:check": "plugin-inspector inspect --no-openclaw"
115
+ },
116
+ "pluginInspector": {
117
+ "version": 1,
118
+ "plugin": {
119
+ "id": "weather",
120
+ "priority": "high",
121
+ "seams": ["dynamic-tool"],
122
+ "sourceRoot": "src"
123
+ },
124
+ "capture": {
125
+ "mockSdk": true
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
86
131
  `init --ci` writes this shape for you, plus
87
132
  `.github/workflows/plugin-inspector.yml`. Copy-ready examples also live in
88
133
  `examples/plugin-inspector.config.json` and
134
+ `examples/package-json-plugin-inspector.json` and
89
135
  `examples/github-actions-plugin-inspector.yml`.
90
136
 
91
137
  ## Runtime Capture
@@ -121,8 +167,8 @@ Minimal package scripts:
121
167
  ```json
122
168
  {
123
169
  "scripts": {
124
- "plugin:check": "plugin-inspector check --no-openclaw",
125
- "plugin:check:runtime": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --no-openclaw --runtime --mock-sdk"
170
+ "plugin:check": "plugin-inspector inspect --no-openclaw",
171
+ "plugin:ci": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector ci --no-openclaw --runtime --mock-sdk"
126
172
  }
127
173
  }
128
174
  ```
@@ -147,8 +193,7 @@ jobs:
147
193
  node-version: 24
148
194
  cache: npm
149
195
  - run: npm ci
150
- - run: npx @openclaw/plugin-inspector check --no-openclaw
151
- - run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector check --no-openclaw --runtime --mock-sdk
196
+ - run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
152
197
  - uses: actions/upload-artifact@v5
153
198
  if: always()
154
199
  with:
@@ -156,6 +201,9 @@ jobs:
156
201
  path: reports/plugin-inspector-*
157
202
  ```
158
203
 
204
+ `ci` writes the normal report, CI summary, SARIF, and JUnit files by default.
205
+ Pass `--no-sarif` or `--no-junit` only if your CI surface cannot consume them.
206
+
159
207
  ## Fixture Suites
160
208
 
161
209
  Fixture-set configs are still supported for crabpot-style compatibility suites:
@@ -15,8 +15,7 @@ jobs:
15
15
  node-version: 24
16
16
  cache: npm
17
17
  - run: npm ci
18
- - run: npx @openclaw/plugin-inspector check --no-openclaw
19
- - run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector check --no-openclaw --runtime --mock-sdk
18
+ - run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
20
19
  - uses: actions/upload-artifact@v5
21
20
  if: always()
22
21
  with:
@@ -0,0 +1,21 @@
1
+ {
2
+ "scripts": {
3
+ "plugin:check": "plugin-inspector inspect --no-openclaw",
4
+ "plugin:ci": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector ci --no-openclaw --runtime --mock-sdk"
5
+ },
6
+ "pluginInspector": {
7
+ "version": 1,
8
+ "plugin": {
9
+ "id": "weather",
10
+ "priority": "high",
11
+ "seams": ["dynamic-tool"],
12
+ "sourceRoot": "src",
13
+ "expect": {
14
+ "registrations": ["registerTool"]
15
+ }
16
+ },
17
+ "capture": {
18
+ "mockSdk": true
19
+ }
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
package/src/advanced.js CHANGED
@@ -33,6 +33,14 @@ export {
33
33
  renderCiSummaryMarkdown,
34
34
  writeCiSummary,
35
35
  } from "./ci-summary.js";
36
+ export {
37
+ buildSarifReport,
38
+ defaultJunitPath,
39
+ defaultSarifPath,
40
+ renderJunitXml,
41
+ reportFindings,
42
+ writeCiOutputArtifacts,
43
+ } from "./ci-outputs.js";
36
44
  export {
37
45
  buildContractProbes,
38
46
  contractProbeRules,
@@ -122,6 +130,7 @@ export {
122
130
  loadPluginRootConfig,
123
131
  normalizeInspectorConfig,
124
132
  normalizePluginRootConfig,
133
+ packageJsonConfigKeys,
125
134
  packageId,
126
135
  validateInspectorConfig,
127
136
  } from "./config.js";
package/src/api.js CHANGED
@@ -4,6 +4,7 @@ import { loadInspectorConfig, loadPluginRootConfig } from "./config.js";
4
4
  import { writePluginInspectorInit } from "./init.js";
5
5
  import { captureEntrypoint } from "./inspector.js";
6
6
  import { renderTextSummary, writeCompatibilityReport } from "./report.js";
7
+ import { writeCiOutputArtifacts } from "./ci-outputs.js";
7
8
  import { buildRuntimeCaptureReport, writeRuntimeCaptureReport } from "./runtime-capture-report.js";
8
9
  import { inspectCompatibilityFixtureSet, inspectFixtureSet } from "./inspector.js";
9
10
 
@@ -88,4 +89,4 @@ export async function setupPluginInspector(options = {}) {
88
89
  return writePluginInspectorInit(options);
89
90
  }
90
91
 
91
- export { createCaptureApi, renderTextSummary };
92
+ export { createCaptureApi, renderTextSummary, writeCiOutputArtifacts };
@@ -223,6 +223,7 @@ function createStoreContext() {
223
223
 
224
224
  function registrationObject(args, defaults) {
225
225
  const first = args[0];
226
+ const callable = firstCallable(args);
226
227
  if (first && typeof first === "object") {
227
228
  return {
228
229
  ...defaults,
@@ -232,13 +233,29 @@ function registrationObject(args, defaults) {
232
233
  };
233
234
  }
234
235
  if (typeof first === "string") {
235
- return {
236
+ return withCallableDefaults({
236
237
  ...defaults,
237
238
  name: first,
238
239
  id: defaults.id,
239
- };
240
+ }, callable);
240
241
  }
241
- return { ...defaults };
242
+ return withCallableDefaults({ ...defaults }, callable);
243
+ }
244
+
245
+ function firstCallable(args) {
246
+ return args.find((arg) => typeof arg === "function");
247
+ }
248
+
249
+ function withCallableDefaults(value, callable) {
250
+ if (!callable) {
251
+ return value;
252
+ }
253
+ return {
254
+ ...value,
255
+ handler: typeof value.handler === "function" ? value.handler : callable,
256
+ run: typeof value.run === "function" ? value.run : callable,
257
+ execute: typeof value.execute === "function" ? value.execute : callable,
258
+ };
242
259
  }
243
260
 
244
261
  function summarizeArguments(args) {
@@ -0,0 +1,235 @@
1
+ import path from "node:path";
2
+ import { writeArtifacts } from "./artifacts.js";
3
+
4
+ export const defaultSarifPath = "plugin-inspector.sarif";
5
+ export const defaultJunitPath = "plugin-inspector.junit.xml";
6
+
7
+ export async function writeCiOutputArtifacts(report, options = {}) {
8
+ const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir ?? "reports");
9
+ const artifacts = [];
10
+
11
+ if (options.sarifPath) {
12
+ artifacts.push({
13
+ name: "sarifPath",
14
+ path: path.resolve(outDir, options.sarifPath),
15
+ json: buildSarifReport(report),
16
+ });
17
+ }
18
+
19
+ if (options.junitPath) {
20
+ artifacts.push({
21
+ name: "junitPath",
22
+ path: path.resolve(outDir, options.junitPath),
23
+ content: renderJunitXml(report),
24
+ });
25
+ }
26
+
27
+ if (artifacts.length === 0) {
28
+ return {};
29
+ }
30
+
31
+ return writeArtifacts(artifacts, { check: options.check });
32
+ }
33
+
34
+ export function buildSarifReport(report) {
35
+ const findings = reportFindings(report);
36
+ const rules = [...new Map(findings.map((finding) => [finding.code, sarifRule(finding)])).values()];
37
+ const fixtureById = new Map((report.fixtures ?? []).map((fixture) => [fixture.id, fixture]));
38
+
39
+ return {
40
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
41
+ version: "2.1.0",
42
+ runs: [
43
+ {
44
+ tool: {
45
+ driver: {
46
+ name: "plugin-inspector",
47
+ informationUri: "https://github.com/openclaw/plugin-inspector",
48
+ rules,
49
+ },
50
+ },
51
+ results: findings.map((finding) => sarifResult(finding, fixtureById)),
52
+ },
53
+ ],
54
+ };
55
+ }
56
+
57
+ export function renderJunitXml(report) {
58
+ const findings = reportFindings(report);
59
+ const testcases = findings.length > 0 ? findings.map(junitFindingTestcase) : [junitPassingTestcase(report)];
60
+ const failures = findings.filter(isBlockingFinding).length;
61
+ const tests = testcases.length;
62
+
63
+ return [
64
+ '<?xml version="1.0" encoding="UTF-8"?>',
65
+ `<testsuite name="plugin-inspector" tests="${tests}" failures="${failures}" errors="0" skipped="0">`,
66
+ ...testcases,
67
+ "</testsuite>",
68
+ "",
69
+ ].join("\n");
70
+ }
71
+
72
+ export function reportFindings(report) {
73
+ const findings = new Map();
74
+ for (const finding of [...(report.breakages ?? []), ...(report.warnings ?? []), ...(report.suggestions ?? [])]) {
75
+ findings.set(findingKey(finding), finding);
76
+ }
77
+ for (const issue of report.issues ?? []) {
78
+ const finding = issueToFinding(issue);
79
+ findings.set(findingKey(finding), {
80
+ ...findings.get(findingKey(finding)),
81
+ ...finding,
82
+ });
83
+ }
84
+ return [...findings.values()];
85
+ }
86
+
87
+ function issueToFinding(issue) {
88
+ return {
89
+ fixture: issue.fixture,
90
+ code: issue.code,
91
+ level: issue.status === "blocking" ? "breakage" : "warning",
92
+ message: issue.title,
93
+ evidence: issue.evidence ?? [],
94
+ severity: issue.severity,
95
+ issueClass: issue.issueClass,
96
+ };
97
+ }
98
+
99
+ function findingKey(finding) {
100
+ return [
101
+ finding.fixture ?? "",
102
+ finding.code ?? "",
103
+ ...normalizeEvidence(finding.evidence),
104
+ ].join("\n");
105
+ }
106
+
107
+ function sarifRule(finding) {
108
+ return {
109
+ id: finding.code,
110
+ shortDescription: {
111
+ text: finding.code,
112
+ },
113
+ fullDescription: {
114
+ text: finding.message ?? finding.code,
115
+ },
116
+ defaultConfiguration: {
117
+ level: sarifLevel(finding),
118
+ },
119
+ };
120
+ }
121
+
122
+ function sarifResult(finding, fixtureById) {
123
+ return {
124
+ ruleId: finding.code,
125
+ level: sarifLevel(finding),
126
+ message: {
127
+ text: finding.message ?? finding.code,
128
+ },
129
+ locations: [sarifLocation(finding, fixtureById)],
130
+ properties: {
131
+ fixture: finding.fixture,
132
+ severity: finding.severity ?? finding.level,
133
+ issueClass: finding.issueClass,
134
+ evidence: normalizeEvidence(finding.evidence),
135
+ },
136
+ };
137
+ }
138
+
139
+ function sarifLocation(finding, fixtureById) {
140
+ const parsed = parseEvidenceLocation(normalizeEvidence(finding.evidence)[0]);
141
+ const fixture = fixtureById.get(finding.fixture);
142
+ const uri = parsed?.uri ?? fixture?.path ?? ".";
143
+ return {
144
+ physicalLocation: {
145
+ artifactLocation: {
146
+ uri: normalizeUri(uri),
147
+ },
148
+ region: {
149
+ startLine: parsed?.line ?? 1,
150
+ },
151
+ },
152
+ };
153
+ }
154
+
155
+ function parseEvidenceLocation(evidence) {
156
+ if (!evidence) {
157
+ return null;
158
+ }
159
+
160
+ const ref = evidence.includes(" @ ") ? evidence.split(" @ ").pop() : evidence;
161
+ const match = /^(?<uri>.+?):(?<line>\d+)(?::\d+)?$/.exec(ref);
162
+ if (!match?.groups?.uri) {
163
+ return null;
164
+ }
165
+ return {
166
+ uri: match.groups.uri,
167
+ line: Number(match.groups.line),
168
+ };
169
+ }
170
+
171
+ function junitFindingTestcase(finding) {
172
+ const classname = `plugin-inspector.${xmlName(finding.fixture ?? "unknown")}`;
173
+ const name = `${finding.level ?? "finding"}:${finding.code}`;
174
+ const output = normalizeEvidence(finding.evidence).join("\n");
175
+ if (!isBlockingFinding(finding)) {
176
+ return [
177
+ ` <testcase classname="${escapeXml(classname)}" name="${escapeXml(name)}">`,
178
+ output ? ` <system-out>${escapeXml(output)}</system-out>` : "",
179
+ " </testcase>",
180
+ ]
181
+ .filter(Boolean)
182
+ .join("\n");
183
+ }
184
+
185
+ return [
186
+ ` <testcase classname="${escapeXml(classname)}" name="${escapeXml(name)}">`,
187
+ ` <failure message="${escapeXml(finding.message ?? finding.code)}">${escapeXml(output || finding.message || finding.code)}</failure>`,
188
+ " </testcase>",
189
+ ].join("\n");
190
+ }
191
+
192
+ function junitPassingTestcase(report) {
193
+ return ` <testcase classname="plugin-inspector" name="status:${escapeXml(report.status ?? "pass")}"/>`;
194
+ }
195
+
196
+ function isBlockingFinding(finding) {
197
+ return finding.level === "breakage" || finding.status === "blocking" || finding.severity === "P0";
198
+ }
199
+
200
+ function sarifLevel(finding) {
201
+ if (isBlockingFinding(finding) || finding.severity === "P1") {
202
+ return "error";
203
+ }
204
+ if (finding.level === "warning" || finding.severity === "P2") {
205
+ return "warning";
206
+ }
207
+ return "note";
208
+ }
209
+
210
+ function normalizeEvidence(evidence) {
211
+ if (Array.isArray(evidence)) {
212
+ return evidence.map(String);
213
+ }
214
+ if (evidence == null) {
215
+ return [];
216
+ }
217
+ return [String(evidence)];
218
+ }
219
+
220
+ function normalizeUri(uri) {
221
+ return String(uri).replaceAll(path.sep, "/");
222
+ }
223
+
224
+ function xmlName(value) {
225
+ return String(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
226
+ }
227
+
228
+ function escapeXml(value) {
229
+ return String(value)
230
+ .replace(/&/g, "&amp;")
231
+ .replace(/</g, "&lt;")
232
+ .replace(/>/g, "&gt;")
233
+ .replace(/"/g, "&quot;")
234
+ .replace(/'/g, "&apos;");
235
+ }
package/src/cli.js CHANGED
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
3
  import {
4
+ loadPluginConfig,
4
5
  renderTextSummary,
5
6
  runPluginCheck,
6
7
  } from "./index.js";
7
8
  import {
8
9
  buildCiSummary,
9
10
  captureEntrypoint,
11
+ defaultJunitPath,
12
+ defaultSarifPath,
10
13
  inspectCompatibilityFixtureSet,
11
14
  inspectFixtureSet,
12
15
  loadInspectorConfig,
16
+ writeCiOutputArtifacts,
13
17
  writeCiSummary,
14
18
  writeCompatibilityReport,
15
19
  writePluginInspectorInit,
@@ -28,8 +32,14 @@ try {
28
32
  await runCheck(commandArgs);
29
33
  } else if (command === "init") {
30
34
  await runInit(commandArgs);
35
+ } else if (command === "config") {
36
+ await runConfig(commandArgs);
31
37
  } else if (command === "inspect" || command === "report") {
32
- await runReport(command, commandArgs);
38
+ if (command === "inspect" && !commandArgs.includes("--config")) {
39
+ await runCheck(commandArgs);
40
+ } else {
41
+ await runReport(command, commandArgs);
42
+ }
33
43
  } else if (command === "ci") {
34
44
  await runCi(commandArgs);
35
45
  } else if (command === "capture") {
@@ -42,6 +52,18 @@ try {
42
52
  process.exitCode = 1;
43
53
  }
44
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
+
45
67
  async function runCheck(commandArgs) {
46
68
  const configPath = readFlag(commandArgs, "--config");
47
69
  const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
@@ -50,7 +72,13 @@ async function runCheck(commandArgs) {
50
72
  const json = commandArgs.includes("--json");
51
73
  const capture = readRuntimeFlag(commandArgs);
52
74
  const mockSdk = readMockSdkFlag(commandArgs);
53
- 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
+ });
54
82
 
55
83
  if (json) {
56
84
  console.log(JSON.stringify(report, null, 2));
@@ -87,9 +115,15 @@ async function runReport(command, commandArgs) {
87
115
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
88
116
  const check = commandArgs.includes("--check") || command === "ci";
89
117
  const json = commandArgs.includes("--json");
118
+ const ciOutputs = readCiOutputFlags(commandArgs);
90
119
  const config = await loadInspectorConfig(configPath);
91
120
  const report = await inspectFixtureSet(config);
92
- 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
+ });
93
127
 
94
128
  if (json) {
95
129
  console.log(JSON.stringify(report, null, 2));
@@ -108,8 +142,13 @@ async function runCi(commandArgs) {
108
142
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
109
143
  const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
110
144
  const json = commandArgs.includes("--json");
145
+ const capture = readRuntimeFlag(commandArgs);
146
+ const mockSdk = readMockSdkFlag(commandArgs);
147
+ const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
111
148
  const { report, reportDir } = await runCiCompatibilityReport({
149
+ capture,
112
150
  configPath,
151
+ mockSdk,
113
152
  openclawPath,
114
153
  outDir,
115
154
  pluginRoot,
@@ -128,6 +167,11 @@ async function runCi(commandArgs) {
128
167
  jsonPath: path.join(reportDir, "plugin-inspector-ci-summary.json"),
129
168
  markdownPath: path.join(reportDir, "plugin-inspector-ci-summary.md"),
130
169
  });
170
+ await writeCiOutputArtifacts(report, {
171
+ ...ciOutputs,
172
+ cwd: reportDir,
173
+ outDir: ".",
174
+ });
131
175
 
132
176
  if (json) {
133
177
  console.log(JSON.stringify(summary, null, 2));
@@ -140,7 +184,7 @@ async function runCi(commandArgs) {
140
184
  }
141
185
  }
142
186
 
143
- async function runCiCompatibilityReport({ configPath, openclawPath, outDir, pluginRoot }) {
187
+ async function runCiCompatibilityReport({ capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
144
188
  if (configPath) {
145
189
  const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
146
190
  const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
@@ -151,7 +195,7 @@ async function runCiCompatibilityReport({ configPath, openclawPath, outDir, plug
151
195
  };
152
196
  }
153
197
 
154
- const { report } = await runPluginCheck({ pluginRoot, outDir, openclawPath });
198
+ const { report } = await runPluginCheck({ pluginRoot, outDir, openclawPath, capture, mockSdk });
155
199
  return {
156
200
  report,
157
201
  reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
@@ -187,6 +231,26 @@ function readFlag(commandArgs, name) {
187
231
  return commandArgs[index + 1] ?? null;
188
232
  }
189
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
+
190
254
  function readRuntimeFlag(commandArgs) {
191
255
  if (commandArgs.includes("--runtime") || commandArgs.includes("--capture")) {
192
256
  return true;
@@ -226,19 +290,34 @@ function renderCiTextSummary(summary) {
226
290
  ].join("\n");
227
291
  }
228
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
+
229
306
  function printHelp() {
230
307
  console.log(`plugin-inspector
231
308
 
232
309
  Usage:
233
310
  plugin-inspector
234
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]
235
313
  plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--package-manager npm|pnpm|yarn|bun] [--force]
236
314
  plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
237
- plugin-inspector inspect --config <path> [--out <dir>] [--check] [--json]
238
- plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--json]
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]
239
317
  PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--plugin-root <path>] [--output <path>]
240
318
 
241
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.
242
321
  Runtime capture is opt-in because it imports plugin code; use --runtime with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
243
322
  `);
244
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;
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/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
  }
@@ -13,7 +13,7 @@ export const syntheticRegistrationExecutionProfiles = {
13
13
  },
14
14
  registerChannel: {
15
15
  mode: "channel-opt-in",
16
- callableProperties: ["send", "receive"],
16
+ callableProperties: ["send", "sendMessage", "receive", "handleMessage"],
17
17
  option: "includeChannelRuntime",
18
18
  },
19
19
  registerCli: {
@@ -31,7 +31,7 @@ export const syntheticRegistrationExecutionProfiles = {
31
31
  },
32
32
  registerGatewayMethod: {
33
33
  mode: "direct",
34
- callableProperties: ["handler", "run", "execute"],
34
+ callableProperties: ["handler", "run", "execute", "invoke"],
35
35
  },
36
36
  registerHttpRoute: {
37
37
  mode: "direct",
@@ -58,12 +58,12 @@ export const syntheticRegistrationExecutionProfiles = {
58
58
  },
59
59
  registerService: {
60
60
  mode: "lifecycle-opt-in",
61
- callableProperties: ["start", "stop"],
61
+ callableProperties: ["start", "stop", "dispose"],
62
62
  option: "includeLifecycle",
63
63
  },
64
64
  registerSpeechProvider: {
65
65
  mode: "provider-opt-in",
66
- callableProperties: ["speak", "synthesize"],
66
+ callableProperties: ["speak", "synthesize", "tts"],
67
67
  option: "includeProviderCapabilities",
68
68
  },
69
69
  registerTool: {
@@ -452,7 +452,8 @@ async function runRegistrationProbes(entry, retainedEntry, captureIndex, options
452
452
  return [metadataOnlyResult(entry, captureIndex, profile.reason)];
453
453
  }
454
454
 
455
- const descriptor = retainedEntry.arguments?.[0] ?? retainedEntry.returnValue;
455
+ const descriptor =
456
+ retainedEntry.arguments?.find((value) => value && typeof value === "object") ?? retainedEntry.returnValue;
456
457
  if (!descriptor || typeof descriptor !== "object") {
457
458
  return [blockedResult(entry, captureIndex, "captured registration has no object descriptor")];
458
459
  }