@openclaw/plugin-inspector 0.3.10 → 0.3.12

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
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.12 - 2026-06-09
6
+
7
+ ### Changed
8
+
9
+ - Hide maintainer-facing `inspector-gap` findings from author-facing `check`, `ci`, and `batch` output by default, with `--include-inspector-gaps` for internal coverage reports.
10
+
11
+ ## 0.3.11 - 2026-05-26
12
+
13
+ ### Fixed
14
+
15
+ - Classify the latest generated kitchen-sink registrars for meeting notes, node CLI features, hosted media, model catalogs, embedding providers, and session actions.
16
+ - Stop classifying package source entrypoints as missing when the published package provides built runtime entrypoints, and collapse SDK alias findings into a single compat-gap row.
17
+ - Treat compat-gap issues as reconciled contract coverage for their own compatibility record.
18
+ - Count passed synthetic hook probes as runtime coverage so conversation-access and `before_tool_call` inspector gaps close when probe artifacts prove them.
19
+ - Keep mock-SDK synthetic probes in-process so retained hook and registration handlers remain callable, and harden dynamic root SDK mock exports.
20
+ - Synthesize nested manifest config samples for optional object settings with required inner shape.
21
+ - Populate message and agent lifecycle synthetic hook payloads with the fields telemetry plugins commonly read.
22
+ - Resolve plugin manifests from parent directories when runtime capture starts from built `dist` entrypoints.
23
+ - Capture CLIs no longer treat `--output`, `--plugin-root`, or `--sdk` flag values as the positional entrypoint. Thanks @KrasimirKralev.
24
+ - Mock-SDK TypeScript capture now falls back to Node's strip-only parser on Node 26.
25
+
5
26
  ## 0.3.10 - 2026-05-03
6
27
 
7
28
  ### Fixed
package/README.md CHANGED
@@ -170,6 +170,7 @@ Copy-ready examples live in:
170
170
  | `plugin-inspector config` | Print resolved plugin-root config as text or JSON. |
171
171
  | `plugin-inspector init` | Write starter config, scripts, and optional GitHub Actions workflow. |
172
172
  | `plugin-inspector report` | Run a fixture-suite config with many plugins. |
173
+ | `plugin-inspector batch` | Discover plugin roots under a folder and write one aggregate impact report. |
173
174
  | `plugin-inspector capture` | Runtime-capture one entrypoint directly. |
174
175
 
175
176
  Common options:
@@ -186,6 +187,7 @@ Common options:
186
187
  | `--mock-sdk` / `--sdk mock` | Use generated SDK and external-package mocks for runtime capture. |
187
188
  | `--real-sdk` / `--sdk real` | Use installed real SDK dependencies instead of mocks. |
188
189
  | `--allow-execute` | Permit commands that import plugin code. |
190
+ | `--include-inspector-gaps` | Include maintainer-facing scanner coverage gaps in `check`, `ci`, and `batch` output. |
189
191
  | `--json` | Print machine-readable JSON to stdout. |
190
192
  | `--sarif [path]` | Write SARIF from `check` or `inspect`; `ci` enables this by default. |
191
193
  | `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
@@ -305,8 +307,9 @@ Important report sections:
305
307
  | `logs` | Informational inventory and coverage rows. |
306
308
  | `decisions` | Maintainer-facing follow-up or compatibility-policy decisions. |
307
309
 
308
- Issue classes currently flow through the reports as live issues, compat gaps,
309
- deprecation warnings, inspector gaps, upstream metadata, and fixture regressions.
310
+ Author-facing `check`, `ci`, and `batch` output hides inspector gaps by default
311
+ because they are scanner coverage backlog, not plugin-author fixes. Pass
312
+ `--include-inspector-gaps` for Crabpot-style maintainer coverage reports.
310
313
 
311
314
  ## CI Policy And Shared Reporting Primitives
312
315
 
@@ -404,6 +407,7 @@ Stable grouped facades:
404
407
  | `reports` | Render/write reports and classify issue findings. |
405
408
  | `contracts` | Build, render, validate, and write contract captures and coverage. |
406
409
  | `ci` | Build summaries, policy reports, execution results, SARIF, and JUnit outputs. |
410
+ | `batch` | Discover plugin roots and aggregate compatibility findings across a corpus. |
407
411
  | `runtime` | Build runtime profiles, profile diffs, ref diffs, and import-loop profiles. |
408
412
  | `synthetic` | Build and run synthetic probe plans. |
409
413
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
@@ -56,7 +56,13 @@
56
56
  "release:readiness": "npm run release:local && npm run release:crabpot",
57
57
  "release:local": "npm run check",
58
58
  "release:notes": "node scripts/release-notes.mjs --unreleased",
59
- "test": "node --test test/*.test.js"
59
+ "test": "node --test test/*.test.js",
60
+ "check:changed": "npm run check",
61
+ "test:changed": "npm test",
62
+ "crabbox:hydrate": "crabbox actions hydrate",
63
+ "crabbox:run": "crabbox run",
64
+ "crabbox:stop": "crabbox stop",
65
+ "crabbox:warmup": "crabbox warmup"
60
66
  },
61
67
  "keywords": [
62
68
  "openclaw",
package/src/advanced.js CHANGED
@@ -194,6 +194,7 @@ export {
194
194
  writeRuntimeCaptureReport,
195
195
  } from "./runtime-capture-report.js";
196
196
  export { createMockSdkPackage } from "./sdk-mock.js";
197
+ export { runEntrypointSyntheticProbes } from "./synthetic-entrypoint.js";
197
198
  export {
198
199
  buildSyntheticProbePlan,
199
200
  defaultSyntheticHookContexts,
package/src/api.js CHANGED
@@ -44,6 +44,7 @@ export async function inspectPluginRoot(options = {}) {
44
44
  const config = await loadPluginConfig(options);
45
45
  return inspectCompatibilityFixtureSet(config, {
46
46
  generatedAt: options.generatedAt,
47
+ includeInspectorGaps: options.includeInspectorGaps,
47
48
  openclawPath: options.openclawPath,
48
49
  executionResults: options.executionResults,
49
50
  targetOpenClaw: options.targetOpenClaw,
@@ -59,6 +60,7 @@ export async function inspectCompatibilityFixtureSetConfig(options = {}) {
59
60
  const config = await loadFixtureSetConfig(options);
60
61
  return inspectCompatibilityFixtureSet(config, {
61
62
  generatedAt: options.generatedAt,
63
+ includeInspectorGaps: options.includeInspectorGaps,
62
64
  openclawPath: options.openclawPath,
63
65
  executionResults: options.executionResults,
64
66
  targetOpenClaw: options.targetOpenClaw,
@@ -113,6 +115,7 @@ export async function buildFixtureSetColdImportReadiness(options = {}) {
113
115
  options.report ??
114
116
  (await inspectCompatibilityFixtureSet(config, {
115
117
  generatedAt: options.generatedAt,
118
+ includeInspectorGaps: options.includeInspectorGaps,
116
119
  openclawPath: options.openclawPath,
117
120
  executionResults: options.executionResults,
118
121
  targetOpenClaw: options.targetOpenClaw,
@@ -145,6 +148,7 @@ export async function buildFixtureSetWorkspacePlan(options = {}) {
145
148
  options.report ??
146
149
  (await inspectCompatibilityFixtureSet(config, {
147
150
  generatedAt: options.generatedAt,
151
+ includeInspectorGaps: options.includeInspectorGaps,
148
152
  openclawPath: options.openclawPath,
149
153
  executionResults: options.executionResults,
150
154
  targetOpenClaw: options.targetOpenClaw,
package/src/batch.js ADDED
@@ -0,0 +1,297 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { renderMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
6
+ import { runPluginCheck } from "./api.js";
7
+
8
+ const ignoredDirs = new Set([
9
+ ".git",
10
+ ".hg",
11
+ ".svn",
12
+ "node_modules",
13
+ "reports",
14
+ "dist",
15
+ "build",
16
+ ".plugin-inspector",
17
+ ]);
18
+
19
+ export async function runBatchAnalysis(options = {}) {
20
+ const rootDir = path.resolve(options.rootDir ?? options.inputDir ?? process.cwd());
21
+ const outDir = options.outDir ?? "reports";
22
+ const outRoot = path.resolve(rootDir, outDir);
23
+ const concurrency = Math.max(1, Math.min(Math.round(options.concurrency ?? 4), 32));
24
+ const keepPluginReports = options.keepPluginReports === true;
25
+ const pluginRoots = await discoverPluginRoots(rootDir);
26
+ const tempRoot = keepPluginReports ? null : await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-batch-"));
27
+ const entries = [];
28
+
29
+ try {
30
+ await runWithConcurrency(pluginRoots, concurrency, async (pluginRoot) => {
31
+ const reportsRoot = keepPluginReports
32
+ ? path.join(outRoot, "plugins", slugForPath(path.relative(rootDir, pluginRoot)))
33
+ : path.join(tempRoot, slugForPath(path.relative(rootDir, pluginRoot)));
34
+ entries.push(
35
+ await inspectBatchPlugin(pluginRoot, {
36
+ ...options,
37
+ rootDir,
38
+ outDir: reportsRoot,
39
+ openclawPath: options.openclawPath,
40
+ }),
41
+ );
42
+ });
43
+ } finally {
44
+ if (tempRoot) await rm(tempRoot, { recursive: true, force: true });
45
+ }
46
+
47
+ entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
48
+ const report = buildBatchReport({
49
+ rootDir,
50
+ entries,
51
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
52
+ });
53
+ const paths = await writeBatchReport(report, { outDir: outRoot, check: options.checkArtifacts });
54
+ return { report, paths };
55
+ }
56
+
57
+ export async function discoverPluginRoots(rootDir) {
58
+ const roots = [];
59
+ await walk(path.resolve(rootDir));
60
+ roots.sort();
61
+ return roots;
62
+
63
+ async function walk(dir) {
64
+ if (await isPluginRoot(dir)) {
65
+ roots.push(dir);
66
+ return;
67
+ }
68
+ const entries = await readdir(dir, { withFileTypes: true });
69
+ for (const entry of entries) {
70
+ if (!entry.isDirectory() || ignoredDirs.has(entry.name)) continue;
71
+ await walk(path.join(dir, entry.name));
72
+ }
73
+ }
74
+ }
75
+
76
+ export async function writeBatchReport(report, options = {}) {
77
+ return writeJsonMarkdownArtifacts({
78
+ jsonPath: path.join(options.outDir ?? "reports", "plugin-inspector-batch-report.json"),
79
+ markdownPath: path.join(options.outDir ?? "reports", "plugin-inspector-batch-report.md"),
80
+ json: report,
81
+ markdown: renderBatchMarkdown(report),
82
+ check: options.check,
83
+ });
84
+ }
85
+
86
+ function buildBatchReport({ rootDir, entries, generatedAt }) {
87
+ const findingFrequency = findingFrequencyRows(entries);
88
+ const summary = {
89
+ pluginCount: entries.length,
90
+ passed: entries.filter((entry) => entry.status === "pass").length,
91
+ failed: entries.filter((entry) => entry.status !== "pass").length,
92
+ pluginsWithErrors: entries.filter((entry) => entry.errorCount > 0).length,
93
+ pluginsWithWarnings: entries.filter((entry) => entry.warningCount > 0).length,
94
+ errorCount: entries.reduce((sum, entry) => sum + entry.errorCount, 0),
95
+ warningCount: entries.reduce((sum, entry) => sum + entry.warningCount, 0),
96
+ findingCodeCount: findingFrequency.length,
97
+ };
98
+ return {
99
+ generatedAt,
100
+ rootDir,
101
+ summary,
102
+ findingFrequency,
103
+ plugins: entries,
104
+ };
105
+ }
106
+
107
+ async function inspectBatchPlugin(pluginRoot, options) {
108
+ try {
109
+ const { report } = await runPluginCheck({
110
+ allowExecution: options.allowExecution,
111
+ capture: options.capture,
112
+ configPath: options.configPath,
113
+ includeInspectorGaps: options.includeInspectorGaps,
114
+ mockSdk: options.mockSdk,
115
+ openclawPath: options.openclawPath,
116
+ outDir: options.outDir,
117
+ pluginRoot,
118
+ });
119
+ const findings = normalizeReportFindings(report);
120
+ return {
121
+ pluginRoot,
122
+ relativePath: path.relative(options.rootDir ?? process.cwd(), pluginRoot) || ".",
123
+ status: report.status,
124
+ packageName: packageNameFromReport(report),
125
+ targetOpenClaw: report.targetOpenClaw,
126
+ errorCount: findings.filter((finding) => finding.kind === "error").length,
127
+ warningCount: findings.filter((finding) => finding.kind === "warning").length,
128
+ findings,
129
+ };
130
+ } catch (error) {
131
+ return {
132
+ pluginRoot,
133
+ relativePath: path.relative(options.rootDir ?? process.cwd(), pluginRoot) || ".",
134
+ status: "error",
135
+ packageName: path.basename(pluginRoot),
136
+ targetOpenClaw: null,
137
+ errorCount: 1,
138
+ warningCount: 0,
139
+ findings: [
140
+ {
141
+ kind: "error",
142
+ code: "plugin-inspector-batch-failure",
143
+ message: error instanceof Error ? error.message : String(error),
144
+ },
145
+ ],
146
+ };
147
+ }
148
+ }
149
+
150
+ function normalizeReportFindings(report) {
151
+ return [
152
+ ...(report.breakages ?? []).map((finding) => normalizeFinding(finding, "error")),
153
+ ...(report.issues ?? []).map((finding) =>
154
+ normalizeFinding(
155
+ finding,
156
+ finding.status === "blocking" || finding.severity === "P0" ? "error" : "warning",
157
+ ),
158
+ ),
159
+ ...(report.warnings ?? []).map((finding) => normalizeFinding(finding, "warning")),
160
+ ...(report.suggestions ?? []).map((finding) => normalizeFinding(finding, "warning")),
161
+ ];
162
+ }
163
+
164
+ function normalizeFinding(finding, kind) {
165
+ return {
166
+ kind,
167
+ code: finding.code ?? "plugin-inspector-finding",
168
+ severity: finding.severity,
169
+ issueClass: finding.issueClass,
170
+ message: finding.message ?? finding.title ?? "See plugin report.",
171
+ evidence: finding.evidence,
172
+ };
173
+ }
174
+
175
+ function findingFrequencyRows(entries) {
176
+ const byCode = new Map();
177
+ for (const entry of entries) {
178
+ const seenForPlugin = new Set();
179
+ for (const finding of entry.findings) {
180
+ const current = byCode.get(finding.code) ?? {
181
+ code: finding.code,
182
+ count: 0,
183
+ plugins: 0,
184
+ errors: 0,
185
+ warnings: 0,
186
+ };
187
+ current.count += 1;
188
+ if (!seenForPlugin.has(finding.code)) {
189
+ current.plugins += 1;
190
+ seenForPlugin.add(finding.code);
191
+ }
192
+ if (finding.kind === "error") current.errors += 1;
193
+ else current.warnings += 1;
194
+ byCode.set(finding.code, current);
195
+ }
196
+ }
197
+ return [...byCode.values()].sort((a, b) => b.plugins - a.plugins || b.count - a.count);
198
+ }
199
+
200
+ function renderBatchMarkdown(report) {
201
+ return [
202
+ "# Plugin Inspector Batch Report",
203
+ "",
204
+ `Generated: ${report.generatedAt}`,
205
+ `Root: ${report.rootDir}`,
206
+ "",
207
+ "## Summary",
208
+ "",
209
+ renderMarkdownTable(
210
+ [
211
+ ["Plugins", report.summary.pluginCount],
212
+ ["Passed", report.summary.passed],
213
+ ["Failed", report.summary.failed],
214
+ ["Plugins with errors", report.summary.pluginsWithErrors],
215
+ ["Plugins with warnings", report.summary.pluginsWithWarnings],
216
+ ["Errors", report.summary.errorCount],
217
+ ["Warnings", report.summary.warningCount],
218
+ ],
219
+ ["Metric", "Value"],
220
+ ),
221
+ "",
222
+ "## Finding Frequency",
223
+ "",
224
+ report.findingFrequency.length
225
+ ? renderMarkdownTable(
226
+ report.findingFrequency.map((row) => [
227
+ row.code,
228
+ row.plugins,
229
+ row.count,
230
+ row.errors,
231
+ row.warnings,
232
+ ]),
233
+ ["Code", "Plugins", "Findings", "Errors", "Warnings"],
234
+ )
235
+ : "_No findings._",
236
+ "",
237
+ "## Plugins",
238
+ "",
239
+ report.plugins.length
240
+ ? renderMarkdownTable(
241
+ report.plugins.map((plugin) => [
242
+ plugin.packageName,
243
+ plugin.relativePath,
244
+ plugin.status,
245
+ plugin.errorCount,
246
+ plugin.warningCount,
247
+ ]),
248
+ ["Package", "Path", "Status", "Errors", "Warnings"],
249
+ )
250
+ : "_No plugin roots discovered._",
251
+ ].join("\n");
252
+ }
253
+
254
+ async function runWithConcurrency(items, concurrency, worker) {
255
+ let index = 0;
256
+ const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
257
+ while (index < items.length) {
258
+ const item = items[index];
259
+ index += 1;
260
+ await worker(item);
261
+ }
262
+ });
263
+ await Promise.all(runners);
264
+ }
265
+
266
+ async function isPluginRoot(dir) {
267
+ if (existsSync(path.join(dir, "plugin-inspector.config.json"))) return true;
268
+ if (existsSync(path.join(dir, ".plugin-inspector.json"))) return true;
269
+ if (existsSync(path.join(dir, "openclaw.plugin.json"))) return true;
270
+ const packageJsonPath = path.join(dir, "package.json");
271
+ if (!existsSync(packageJsonPath)) return false;
272
+ try {
273
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
274
+ return Boolean(packageJson.openclaw || packageJson.pluginInspector || packageJson["plugin-inspector"]);
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ function packageNameFromReport(report) {
281
+ return (
282
+ report.fixtures?.[0]?.package?.packageJson?.name ??
283
+ report.fixtures?.[0]?.package?.name ??
284
+ report.fixtures?.[0]?.name ??
285
+ report.fixtures?.[0]?.id ??
286
+ "plugin"
287
+ );
288
+ }
289
+
290
+ function slugForPath(value) {
291
+ return (
292
+ String(value)
293
+ .replaceAll(path.sep, "-")
294
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
295
+ .replace(/^-+|-+$/g, "") || "plugin"
296
+ );
297
+ }
@@ -11,7 +11,7 @@ try {
11
11
  }
12
12
 
13
13
  async function run(commandArgs) {
14
- const entrypoint = commandArgs.find((arg) => !arg.startsWith("-"));
14
+ const entrypoint = findEntrypoint(commandArgs);
15
15
  const outputPath = readFlag(commandArgs, "--output");
16
16
  const pluginRoot = readFlag(commandArgs, "--plugin-root");
17
17
  const mockSdk = readMockSdkFlag(commandArgs) ?? true;
@@ -40,6 +40,17 @@ function readFlag(commandArgs, name) {
40
40
  return commandArgs[index + 1] ?? null;
41
41
  }
42
42
 
43
+ function findEntrypoint(commandArgs) {
44
+ const flagsWithValues = new Set(["--output", "--plugin-root", "--sdk"]);
45
+ const consumedIndexes = new Set();
46
+ for (const [index, arg] of commandArgs.entries()) {
47
+ if (flagsWithValues.has(arg)) {
48
+ consumedIndexes.add(index + 1);
49
+ }
50
+ }
51
+ return commandArgs.find((arg, index) => !arg.startsWith("-") && !consumedIndexes.has(index)) ?? null;
52
+ }
53
+
43
54
  function readMockSdkFlag(commandArgs) {
44
55
  const sdk = readFlag(commandArgs, "--sdk");
45
56
  if (sdk === "mock") {
@@ -17,7 +17,10 @@ export async function captureApiOptionsForPlugin(apiOptions = {}, options = {})
17
17
  }
18
18
 
19
19
  async function readSamplePluginConfig(pluginRoot) {
20
- const manifestPath = path.join(pluginRoot, "openclaw.plugin.json");
20
+ const manifestPath = await findNearestManifestPath(pluginRoot);
21
+ if (!manifestPath) {
22
+ return undefined;
23
+ }
21
24
  let manifest;
22
25
  try {
23
26
  manifest = JSON.parse(await readFile(manifestPath, "utf8"));
@@ -29,6 +32,23 @@ async function readSamplePluginConfig(pluginRoot) {
29
32
  return isPlainObject(sample) && Object.keys(sample).length > 0 ? sample : undefined;
30
33
  }
31
34
 
35
+ async function findNearestManifestPath(pluginRoot) {
36
+ let current = path.resolve(pluginRoot);
37
+ while (true) {
38
+ const manifestPath = path.join(current, "openclaw.plugin.json");
39
+ try {
40
+ await readFile(manifestPath, "utf8");
41
+ return manifestPath;
42
+ } catch {}
43
+
44
+ const parent = path.dirname(current);
45
+ if (parent === current) {
46
+ return null;
47
+ }
48
+ current = parent;
49
+ }
50
+ }
51
+
32
52
  function sampleJsonSchema(schema, context = {}) {
33
53
  if (!isPlainObject(schema)) {
34
54
  return undefined;
@@ -83,6 +103,16 @@ function sampleObjectSchema(schema) {
83
103
  }
84
104
  }
85
105
 
106
+ if (!hasNonBooleanSample(output, properties)) {
107
+ const key = preferredNestedConfigKey(properties);
108
+ if (key) {
109
+ const value = sampleJsonSchema(properties[key], { key });
110
+ if (value !== undefined) {
111
+ output[key] = value;
112
+ }
113
+ }
114
+ }
115
+
86
116
  if (Object.keys(output).length === 0 && Number(schema.minProperties ?? 0) > 0) {
87
117
  const key = preferredSamplePropertyKey(properties);
88
118
  if (key) {
@@ -96,6 +126,24 @@ function sampleObjectSchema(schema) {
96
126
  return output;
97
127
  }
98
128
 
129
+ function hasNonBooleanSample(output, properties) {
130
+ return Object.keys(output).some((key) => properties[key]?.type !== "boolean");
131
+ }
132
+
133
+ function preferredNestedConfigKey(properties) {
134
+ for (const key of ["embedding", "credentials", "auth", "provider", ...Object.keys(properties)]) {
135
+ const schema = properties[key];
136
+ if (!isPlainObject(schema)) {
137
+ continue;
138
+ }
139
+ const required = Array.isArray(schema.required) ? schema.required : [];
140
+ if ((schema.type === "object" || schema.properties) && (Number(schema.minProperties ?? 0) > 0 || required.length > 0)) {
141
+ return key;
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+
99
147
  function preferredSamplePropertyKey(properties) {
100
148
  for (const key of ["provider", "model", "apiKey", "id", "name", ...Object.keys(properties)]) {
101
149
  if (Object.prototype.hasOwnProperty.call(properties, key)) {