@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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
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
+
17
+ ## 0.1.3 - 2026-04-27
18
+
19
+ ### Added
20
+
21
+ - Add reserved bundled-plugin SDK import detection so external plugins get explicit compatibility findings for private OpenClaw SDK shims.
22
+ - Add packaged workspace capture and synthetic-probe helper CLIs for generated isolated workspace plans.
23
+
24
+ ### Changed
25
+
26
+ - Make `plugin-inspector ci` write compatibility-backed CI summary artifacts instead of the legacy inventory report.
27
+ - Default packaged helper captures to the mocked OpenClaw SDK while preserving `--real-sdk` opt-in behavior.
28
+ - Detect the default runtime capture artifact at `reports/plugin-inspector-runtime-capture.json`.
29
+ - Report the actual log count in CLI text summaries.
30
+
3
31
  ## 0.1.2 - 2026-04-27
4
32
 
5
33
  ### 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.2",
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,
@@ -100,6 +108,7 @@ export {
100
108
  openClawTargetPathCandidates,
101
109
  parseCompatRecordEntries,
102
110
  parseExportedStringArray,
111
+ parsePluginSdkEntrypointSpecifiers,
103
112
  parsePluginSdkExports,
104
113
  parseTypeFields,
105
114
  readOpenClawTargetSurface,
@@ -121,6 +130,7 @@ export {
121
130
  loadPluginRootConfig,
122
131
  normalizeInspectorConfig,
123
132
  normalizePluginRootConfig,
133
+ packageJsonConfigKeys,
124
134
  packageId,
125
135
  validateInspectorConfig,
126
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,61 @@
1
+ #!/usr/bin/env node
2
+ import { captureEntrypoint, writeArtifacts } from "./advanced.js";
3
+
4
+ const args = process.argv.slice(2);
5
+
6
+ try {
7
+ await run(args);
8
+ } catch (error) {
9
+ console.error(error.message);
10
+ process.exitCode = 1;
11
+ }
12
+
13
+ async function run(commandArgs) {
14
+ const entrypoint = commandArgs.find((arg) => !arg.startsWith("-"));
15
+ const outputPath = readFlag(commandArgs, "--output");
16
+ const pluginRoot = readFlag(commandArgs, "--plugin-root");
17
+ const mockSdk = readMockSdkFlag(commandArgs) ?? true;
18
+
19
+ if (!entrypoint) {
20
+ throw new Error("capture requires an entrypoint path");
21
+ }
22
+ if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
23
+ throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
24
+ }
25
+
26
+ const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot });
27
+ const json = `${JSON.stringify(result, null, 2)}\n`;
28
+ if (outputPath) {
29
+ await writeArtifacts([{ path: outputPath, content: json }]);
30
+ } else {
31
+ process.stdout.write(json);
32
+ }
33
+ }
34
+
35
+ function readFlag(commandArgs, name) {
36
+ const index = commandArgs.indexOf(name);
37
+ if (index === -1) {
38
+ return null;
39
+ }
40
+ return commandArgs[index + 1] ?? null;
41
+ }
42
+
43
+ function readMockSdkFlag(commandArgs) {
44
+ const sdk = readFlag(commandArgs, "--sdk");
45
+ if (sdk === "mock") {
46
+ return true;
47
+ }
48
+ if (sdk === "real") {
49
+ return false;
50
+ }
51
+ if (sdk && !["mock", "real"].includes(sdk)) {
52
+ throw new Error("--sdk must be mock or real");
53
+ }
54
+ if (commandArgs.includes("--mock-sdk")) {
55
+ return true;
56
+ }
57
+ if (commandArgs.includes("--real-sdk")) {
58
+ return false;
59
+ }
60
+ return undefined;
61
+ }
@@ -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/ci-summary.js CHANGED
@@ -5,7 +5,7 @@ import { readOptionalJsonFile } from "./json-file.js";
5
5
 
6
6
  export const defaultCiReportPaths = {
7
7
  compatibility: "reports/plugin-inspector-report.json",
8
- capture: "reports/plugin-inspector-capture.json",
8
+ capture: "reports/plugin-inspector-runtime-capture.json",
9
9
  synthetic: "reports/plugin-inspector-synthetic-probes.json",
10
10
  coldImport: "reports/plugin-inspector-cold-import.json",
11
11
  workspace: "reports/plugin-inspector-workspace-plan.json",