@openclaw/plugin-inspector 0.0.0 → 0.1.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 ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-04-27
4
+
5
+ Initial public package release for `@openclaw/plugin-inspector`.
6
+
7
+ ### Added
8
+
9
+ - Plugin-root `plugin-inspector check` command with optional `plugin-inspector.config.json`.
10
+ - Static OpenClaw plugin compatibility reports, issue reports, and CI policy summaries.
11
+ - Crabpot-compatible fixture-set inspection and report assembly APIs.
12
+ - Target OpenClaw surface parsing for compat registry records, hook names, registrar names, SDK exports, and manifest type fields.
13
+ - Package metadata, manifest, SDK import, hook, registration, runtime-capture, cold-import, synthetic-probe, runtime-profile, ref-diff, and profile-diff report helpers.
14
+ - Optional `PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --capture` runtime registration capture using a temporary mocked `openclaw/plugin-sdk`.
15
+ - Copy-ready config and GitHub Actions examples under `examples/`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenClaw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,250 @@
1
- # @openclaw/plugin-inspector
1
+ # plugin-inspector
2
2
 
3
- Bootstrap placeholder. Use 0.1.0 or newer.
3
+ `plugin-inspector` is the reusable OpenClaw plugin compatibility inspector. It
4
+ wraps the static inspection, registration capture, and report model prototyped
5
+ in crabpot into an npm-publishable package.
6
+
7
+ No npm package has been published yet.
8
+
9
+ ## Install
10
+
11
+ During development, use a local checkout or packed tarball:
12
+
13
+ ```bash
14
+ npm install --save-dev ../plugin-inspector
15
+ npx plugin-inspector check --no-openclaw
16
+ ```
17
+
18
+ After the package is published, plugin repos should install it as a dev
19
+ dependency and run it from the plugin root:
20
+
21
+ ```bash
22
+ npm install --save-dev @openclaw/plugin-inspector
23
+ npx @openclaw/plugin-inspector check
24
+ ```
25
+
26
+ ## CLI
27
+
28
+ Run the default plugin-root check from a plugin package directory:
29
+
30
+ ```bash
31
+ plugin-inspector check
32
+ ```
33
+
34
+ That command reads the current directory as one plugin, inspects package
35
+ metadata, `openclaw.plugin.json`, source imports, `api.on(...)`,
36
+ `api.register*`, and writes:
37
+
38
+ - `reports/plugin-inspector-report.json`
39
+ - `reports/plugin-inspector-report.md`
40
+ - `reports/plugin-inspector-issues.md`
41
+
42
+ Use `--no-openclaw` when CI should not compare against a local OpenClaw
43
+ checkout:
44
+
45
+ ```bash
46
+ plugin-inspector check --no-openclaw
47
+ ```
48
+
49
+ Use a simple plugin-root config when you want stable fixture metadata or
50
+ expected seams:
51
+
52
+ ```json
53
+ {
54
+ "version": 1,
55
+ "plugin": {
56
+ "id": "weather",
57
+ "priority": "high",
58
+ "seams": ["dynamic-tool"],
59
+ "sourceRoot": "src",
60
+ "expect": {
61
+ "registrations": ["registerTool"]
62
+ }
63
+ },
64
+ "openclaw": {
65
+ "defaultCheckoutPath": "../openclaw"
66
+ }
67
+ }
68
+ ```
69
+
70
+ Then run:
71
+
72
+ ```bash
73
+ plugin-inspector check --config plugin-inspector.config.json
74
+ ```
75
+
76
+ Copy-ready examples live in `examples/plugin-inspector.config.json` and
77
+ `examples/github-actions-plugin-inspector.yml`.
78
+
79
+ Fixture-set configs are still supported for crabpot-style compatibility suites:
80
+
81
+ ```bash
82
+ plugin-inspector report --config crabpot.config.json --out reports
83
+ ```
84
+
85
+ Capture a plugin entrypoint in an explicitly isolated execution lane:
86
+
87
+ ```bash
88
+ PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture ./dist/index.js --mock-sdk
89
+ ```
90
+
91
+ Run the optional runtime capture smoke during `check`:
92
+
93
+ ```bash
94
+ PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --no-openclaw --capture
95
+ ```
96
+
97
+ Runtime capture creates a temporary mock `openclaw/plugin-sdk` package, imports
98
+ declared OpenClaw package entrypoints, calls their `register(api)` function with
99
+ the capture API, and writes:
100
+
101
+ - `reports/plugin-inspector-runtime-capture.json`
102
+ - `reports/plugin-inspector-runtime-capture.md`
103
+
104
+ ### CI
105
+
106
+ With a dev dependency:
107
+
108
+ ```json
109
+ {
110
+ "scripts": {
111
+ "plugin:check": "plugin-inspector check --no-openclaw",
112
+ "plugin:check:runtime": "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector check --no-openclaw --capture"
113
+ }
114
+ }
115
+ ```
116
+
117
+ GitHub Actions:
118
+
119
+ ```yaml
120
+ name: plugin-inspector
121
+
122
+ on:
123
+ pull_request:
124
+ push:
125
+ branches: [main]
126
+
127
+ jobs:
128
+ check:
129
+ runs-on: ubuntu-latest
130
+ steps:
131
+ - uses: actions/checkout@v5
132
+ - uses: actions/setup-node@v5
133
+ with:
134
+ node-version: 24
135
+ cache: npm
136
+ - run: npm ci
137
+ - run: npm run plugin:check
138
+ - run: npm run plugin:check:runtime
139
+ - uses: actions/upload-artifact@v5
140
+ if: always()
141
+ with:
142
+ name: plugin-inspector-reports
143
+ path: reports/plugin-inspector-*
144
+ ```
145
+
146
+ ## API
147
+
148
+ ```js
149
+ import {
150
+ buildCiSummary,
151
+ buildCiPolicyReport,
152
+ buildColdImportReadiness,
153
+ buildContractCapture,
154
+ buildExecutionResultsReport,
155
+ buildImportLoopProfile,
156
+ buildPlatformProbes,
157
+ buildProfileDiff,
158
+ buildRefDiff,
159
+ buildRuntimeProfile,
160
+ buildRuntimeCaptureReport,
161
+ buildWorkspacePlan,
162
+ createCaptureApi,
163
+ inspectFixtureSet,
164
+ loadInspectorConfig,
165
+ readOpenClawTargetSurface,
166
+ renderCiPolicyMarkdown,
167
+ renderColdImportReadinessMarkdown,
168
+ renderContractCaptureMarkdown,
169
+ renderExecutionResultsMarkdown,
170
+ renderImportLoopProfileMarkdown,
171
+ renderPlatformProbesMarkdown,
172
+ renderProfileDiffMarkdown,
173
+ renderRefDiffMarkdown,
174
+ renderRuntimeProfileMarkdown,
175
+ renderRuntimeCaptureMarkdown,
176
+ renderWorkspacePlanMarkdown,
177
+ renderMarkdownReport,
178
+ validateCiPolicyReport,
179
+ validateContractCoverage,
180
+ writeCiSummary,
181
+ writeCiPolicyReport,
182
+ writeColdImportReadiness,
183
+ writeContractCapture,
184
+ writeExecutionResultsReport,
185
+ writeImportLoopProfile,
186
+ writePlatformProbes,
187
+ writeProfileDiff,
188
+ writeRefDiff,
189
+ writeRuntimeProfile,
190
+ writeRuntimeCaptureReport,
191
+ writeWorkspacePlan,
192
+ writeReport,
193
+ } from "@openclaw/plugin-inspector";
194
+
195
+ const config = await loadInspectorConfig("crabpot.config.json");
196
+ const report = await inspectFixtureSet(config);
197
+ await writeReport(report, { outDir: "reports" });
198
+
199
+ const summary = await buildCiSummary({ reportsDir: "reports" });
200
+ await writeCiSummary(summary);
201
+
202
+ const policyReport = buildCiPolicyReport({ policy, compatibilityReport: report });
203
+ await writeCiPolicyReport(policyReport);
204
+
205
+ const capture = buildContractCapture({ report });
206
+ await writeContractCapture(capture);
207
+ const coverageErrors = validateContractCoverage(report);
208
+
209
+ const readiness = buildColdImportReadiness({ report });
210
+ await writeColdImportReadiness(readiness);
211
+
212
+ const target = await readOpenClawTargetSurface({ manifest: config });
213
+
214
+ const workspacePlan = await buildWorkspacePlan({ report, readiness });
215
+ await writeWorkspacePlan(workspacePlan);
216
+
217
+ const platformProbes = buildPlatformProbes({ plan: workspacePlan });
218
+ await writePlatformProbes(platformProbes);
219
+
220
+ const executionResults = await buildExecutionResultsReport({ resultsDir: ".plugin-inspector/results" });
221
+ await writeExecutionResultsReport(executionResults);
222
+
223
+ const importLoop = await buildImportLoopProfile({ entrypoint: "dist/index.js", runs: 3 });
224
+ await writeImportLoopProfile(importLoop);
225
+
226
+ const runtimeProfile = await buildRuntimeProfile({
227
+ commands: [{ id: "node-boot", label: "Node boot", category: "baseline", args: ["-e", "0"] }],
228
+ });
229
+ await writeRuntimeProfile(runtimeProfile);
230
+
231
+ const runtimeCapture = await buildRuntimeCaptureReport({ report, rootDir: process.cwd() });
232
+ await writeRuntimeCaptureReport(runtimeCapture);
233
+
234
+ const refDiff = await buildRefDiff({ baseReport, headReport });
235
+ await writeRefDiff(refDiff);
236
+
237
+ const profileDiff = await buildProfileDiff({ current, baseline, policy });
238
+ await writeProfileDiff(profileDiff);
239
+ ```
240
+
241
+ ## Scope
242
+
243
+ Default inspection is offline and credential-free. It reads manifests, package
244
+ metadata, and source files, then reports observed `api.on(...)`,
245
+ `api.register*`, `define*`, SDK imports, and manifest contracts.
246
+ OpenClaw target checkout parsing is limited to public compatibility registries,
247
+ SDK package exports, manifest types, hooks, and captured registrar metadata.
248
+
249
+ Cold import capture and synthetic contract probes are explicit opt-in modes.
250
+ Live lanes will stay credential-gated and must never run in default CI.
@@ -0,0 +1,24 @@
1
+ name: plugin-inspector
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ check:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: actions/setup-node@v5
14
+ with:
15
+ node-version: 24
16
+ cache: npm
17
+ - run: npm ci
18
+ - run: npm run plugin:check
19
+ - run: npm run plugin:check:runtime
20
+ - uses: actions/upload-artifact@v5
21
+ if: always()
22
+ with:
23
+ name: plugin-inspector-reports
24
+ path: reports/plugin-inspector-*
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 1,
3
+ "plugin": {
4
+ "id": "weather",
5
+ "priority": "high",
6
+ "seams": ["dynamic-tool"],
7
+ "sourceRoot": "src",
8
+ "expect": {
9
+ "registrations": ["registerTool"]
10
+ }
11
+ },
12
+ "openclaw": {
13
+ "defaultCheckoutPath": "../openclaw"
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.0.0",
4
- "description": "OpenClaw plugin inspector bootstrap placeholder.",
5
- "license": "MIT"
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Offline compatibility inspector for OpenClaw plugins.",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "OpenClaw",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/openclaw/plugin-inspector.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/openclaw/plugin-inspector/issues"
15
+ },
16
+ "homepage": "https://github.com/openclaw/plugin-inspector#readme",
17
+ "engines": {
18
+ "node": ">=22"
19
+ },
20
+ "bin": {
21
+ "plugin-inspector": "src/cli.js"
22
+ },
23
+ "exports": {
24
+ ".": "./src/index.js",
25
+ "./capture-api": "./src/capture-api.js",
26
+ "./ci-policy": "./src/ci-policy.js",
27
+ "./cold-import-readiness": "./src/cold-import-readiness.js",
28
+ "./contract-capture": "./src/contract-capture.js",
29
+ "./contract-coverage": "./src/contract-coverage.js",
30
+ "./execution-results": "./src/execution-results.js",
31
+ "./import-loop-profile": "./src/import-loop-profile.js",
32
+ "./openclaw-target": "./src/openclaw-target.js",
33
+ "./platform-probes": "./src/platform-probes.js",
34
+ "./profile-diff": "./src/profile-diff.js",
35
+ "./ref-diff": "./src/ref-diff.js",
36
+ "./runtime-capture-report": "./src/runtime-capture-report.js",
37
+ "./runtime-profile": "./src/runtime-profile.js",
38
+ "./workspace-plan": "./src/workspace-plan.js"
39
+ },
40
+ "files": [
41
+ "src",
42
+ "examples",
43
+ "README.md",
44
+ "CHANGELOG.md",
45
+ "LICENSE"
46
+ ],
47
+ "scripts": {
48
+ "check": "npm test && npm pack --dry-run && npm publish --dry-run --access public",
49
+ "release:local": "npm run check",
50
+ "test": "node --test test/*.test.js"
51
+ },
52
+ "keywords": [
53
+ "openclaw",
54
+ "plugin",
55
+ "compatibility",
56
+ "ci"
57
+ ]
6
58
  }
@@ -0,0 +1,113 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function writeArtifacts(artifacts, options = {}) {
5
+ if (!Array.isArray(artifacts) || artifacts.length === 0) {
6
+ throw new TypeError("writeArtifacts requires at least one artifact");
7
+ }
8
+
9
+ const written = {};
10
+ for (const artifact of artifacts) {
11
+ const artifactPath = artifact.path;
12
+ if (!artifactPath) {
13
+ throw new TypeError("artifact.path is required");
14
+ }
15
+
16
+ const content = renderArtifactContent(artifact);
17
+ await mkdir(path.dirname(artifactPath), { recursive: true });
18
+ await writeFile(artifactPath, content, "utf8");
19
+
20
+ if (options.check || artifact.check) {
21
+ await assertFileMatches(artifactPath, content);
22
+ }
23
+
24
+ if (artifact.name) {
25
+ written[artifact.name] = artifactPath;
26
+ }
27
+ }
28
+
29
+ return written;
30
+ }
31
+
32
+ export async function writeJsonMarkdownArtifacts({ jsonPath, markdownPath, json, markdown, check = false }) {
33
+ await writeArtifacts(
34
+ [
35
+ { name: "jsonPath", path: jsonPath, json },
36
+ { name: "markdownPath", path: markdownPath, markdown },
37
+ ],
38
+ { check },
39
+ );
40
+ return { jsonPath, markdownPath };
41
+ }
42
+
43
+ export function renderArtifactContent(artifact) {
44
+ if ("content" in artifact) {
45
+ return String(artifact.content);
46
+ }
47
+ if ("json" in artifact) {
48
+ return `${JSON.stringify(artifact.json, null, 2)}\n`;
49
+ }
50
+ if ("markdown" in artifact) {
51
+ return `${artifact.markdown}\n`;
52
+ }
53
+ throw new TypeError("artifact must provide content, json, or markdown");
54
+ }
55
+
56
+ export function renderMarkdownTable(rows, headers, options = {}) {
57
+ if (rows.length === 0 && options.empty != null) {
58
+ return options.empty;
59
+ }
60
+
61
+ const nullValue = options.nullValue ?? "";
62
+ const escape = options.escape !== false;
63
+ const normalizedRows = [headers, ...rows].map((row) =>
64
+ row.map((cell) => {
65
+ const value = String(cell ?? nullValue);
66
+ return escape ? escapeMarkdownTableCell(value) : value;
67
+ }),
68
+ );
69
+
70
+ if (options.padding) {
71
+ const widths = headers.map((_, columnIndex) =>
72
+ Math.max(...normalizedRows.map((row) => row[columnIndex].length)),
73
+ );
74
+ const renderRow = (row) => `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
75
+ return [
76
+ renderRow(normalizedRows[0]),
77
+ renderRow(widths.map((width) => "-".repeat(width))),
78
+ ...normalizedRows.slice(1).map(renderRow),
79
+ ].join("\n");
80
+ }
81
+
82
+ const separator = headers.map(() => options.separator ?? "---");
83
+ return [normalizedRows[0], separator, ...normalizedRows.slice(1)]
84
+ .map((row) => `| ${row.join(" | ")} |`)
85
+ .join("\n");
86
+ }
87
+
88
+ export function renderPaddedMarkdownTable(rows, headers, options = {}) {
89
+ return renderMarkdownTable(rows, headers, {
90
+ empty: "_none_",
91
+ escape: false,
92
+ padding: true,
93
+ ...options,
94
+ });
95
+ }
96
+
97
+ export function escapeMarkdownTableCell(value) {
98
+ return value.replace(/\|/g, "\\|").replace(/\n/g, "<br>");
99
+ }
100
+
101
+ async function assertFileMatches(filePath, expected) {
102
+ try {
103
+ const actual = await readFile(filePath, "utf8");
104
+ if (actual !== expected) {
105
+ throw new Error(`${filePath} is not up to date`);
106
+ }
107
+ } catch (error) {
108
+ if (error?.code === "ENOENT") {
109
+ throw new Error(`${filePath} is missing`);
110
+ }
111
+ throw error;
112
+ }
113
+ }
@@ -0,0 +1,115 @@
1
+ export function createCaptureApi(options = {}) {
2
+ const captured = [];
3
+ const retained = [];
4
+ const knownRegistrars = new Set(options.knownRegistrars ?? []);
5
+ const retainHandlers = options.retainHandlers === true;
6
+
7
+ const api = new Proxy(
8
+ {
9
+ config: options.config ?? {},
10
+ logger: options.logger ?? console,
11
+ pluginConfig: options.pluginConfig ?? {},
12
+ runtime: options.runtime ?? {},
13
+ on(name, handler) {
14
+ const captureIndex =
15
+ captured.push({
16
+ kind: "hook",
17
+ name,
18
+ handlerType: typeof handler,
19
+ arguments: summarizeArguments([name, handler]),
20
+ }) - 1;
21
+ if (retainHandlers) {
22
+ retained.push({
23
+ kind: "hook",
24
+ name,
25
+ handler,
26
+ captureIndex,
27
+ });
28
+ }
29
+ return api;
30
+ },
31
+ },
32
+ {
33
+ get(target, property) {
34
+ if (property === "getCapturedContracts") {
35
+ return () => captured.map((entry) => ({ ...entry }));
36
+ }
37
+ if (property === "getRetainedContracts") {
38
+ return () => retained.map((entry) => ({ ...entry }));
39
+ }
40
+ if (property in target) {
41
+ return target[property];
42
+ }
43
+ if (typeof property === "string" && isRegistrarProperty(property)) {
44
+ return (...args) => {
45
+ const captureIndex =
46
+ captured.push({
47
+ kind: "registration",
48
+ name: property,
49
+ known: knownRegistrars.size === 0 ? null : knownRegistrars.has(property),
50
+ arguments: summarizeArguments(args),
51
+ }) - 1;
52
+ if (retainHandlers) {
53
+ retained.push({
54
+ kind: "registration",
55
+ name: property,
56
+ arguments: args,
57
+ captureIndex,
58
+ });
59
+ }
60
+ return registrationReturnValue(property, args);
61
+ };
62
+ }
63
+ return undefined;
64
+ },
65
+ },
66
+ );
67
+
68
+ return api;
69
+ }
70
+
71
+ function isRegistrarProperty(property) {
72
+ return property.startsWith("register") || property.startsWith("define");
73
+ }
74
+
75
+ function registrationReturnValue(name, args) {
76
+ if (name === "registerService") {
77
+ return {
78
+ name: objectName(args[0]),
79
+ start: async () => undefined,
80
+ stop: async () => undefined,
81
+ };
82
+ }
83
+ return objectName(args[0]) ?? undefined;
84
+ }
85
+
86
+ function summarizeArguments(args) {
87
+ return args.map((arg) => summarizeValue(arg));
88
+ }
89
+
90
+ function summarizeValue(value) {
91
+ if (typeof value === "function") {
92
+ return { type: "function" };
93
+ }
94
+ if (Array.isArray(value)) {
95
+ return { type: "array", length: value.length };
96
+ }
97
+ if (value && typeof value === "object") {
98
+ return {
99
+ type: "object",
100
+ keys: Object.keys(value).sort(),
101
+ name: objectName(value),
102
+ };
103
+ }
104
+ return { type: typeof value, value };
105
+ }
106
+
107
+ function objectName(value) {
108
+ if (!value || typeof value !== "object") {
109
+ return null;
110
+ }
111
+ if (typeof value.name === "string") {
112
+ return value.name;
113
+ }
114
+ return typeof value.id === "string" ? value.id : null;
115
+ }