@openclaw/plugin-inspector 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import path from "node:path";
2
3
  import {
3
4
  renderTextSummary,
4
5
  runPluginCheck,
5
6
  } from "./index.js";
6
7
  import {
8
+ buildCiSummary,
7
9
  captureEntrypoint,
10
+ inspectCompatibilityFixtureSet,
8
11
  inspectFixtureSet,
9
12
  loadInspectorConfig,
13
+ writeCiSummary,
14
+ writeCompatibilityReport,
15
+ writePluginInspectorInit,
10
16
  writeArtifacts,
11
17
  writeReport,
12
18
  } from "./advanced.js";
@@ -20,8 +26,12 @@ try {
20
26
  printHelp();
21
27
  } else if (command === "check") {
22
28
  await runCheck(commandArgs);
23
- } else if (command === "inspect" || command === "report" || command === "ci") {
29
+ } else if (command === "init") {
30
+ await runInit(commandArgs);
31
+ } else if (command === "inspect" || command === "report") {
24
32
  await runReport(command, commandArgs);
33
+ } else if (command === "ci") {
34
+ await runCi(commandArgs);
25
35
  } else if (command === "capture") {
26
36
  await runCapture(commandArgs);
27
37
  } else {
@@ -34,11 +44,13 @@ try {
34
44
 
35
45
  async function runCheck(commandArgs) {
36
46
  const configPath = readFlag(commandArgs, "--config");
47
+ const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
37
48
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
38
49
  const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
39
50
  const json = commandArgs.includes("--json");
40
- const capture = commandArgs.includes("--capture");
41
- const { report } = await runPluginCheck({ configPath, outDir, openclawPath, capture });
51
+ const capture = readRuntimeFlag(commandArgs);
52
+ const mockSdk = readMockSdkFlag(commandArgs);
53
+ const { report } = await runPluginCheck({ configPath, pluginRoot, outDir, openclawPath, capture, mockSdk });
42
54
 
43
55
  if (json) {
44
56
  console.log(JSON.stringify(report, null, 2));
@@ -51,6 +63,25 @@ async function runCheck(commandArgs) {
51
63
  }
52
64
  }
53
65
 
66
+ async function runInit(commandArgs) {
67
+ const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
68
+ const configPath = readFlag(commandArgs, "--config") ?? undefined;
69
+ const workflowPath = readFlag(commandArgs, "--workflow") ?? undefined;
70
+ const packageManager = readFlag(commandArgs, "--package-manager") ?? "npm";
71
+ const result = await writePluginInspectorInit({
72
+ pluginRoot,
73
+ configPath,
74
+ workflowPath,
75
+ packageManager,
76
+ ci: commandArgs.includes("--ci"),
77
+ force: commandArgs.includes("--force"),
78
+ });
79
+
80
+ for (const filePath of result.written) {
81
+ console.log(`wrote ${filePath}`);
82
+ }
83
+ }
84
+
54
85
  async function runReport(command, commandArgs) {
55
86
  const configPath = readFlag(commandArgs, "--config");
56
87
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
@@ -71,11 +102,67 @@ async function runReport(command, commandArgs) {
71
102
  }
72
103
  }
73
104
 
105
+ async function runCi(commandArgs) {
106
+ const configPath = readFlag(commandArgs, "--config");
107
+ const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
108
+ const outDir = readFlag(commandArgs, "--out") ?? "reports";
109
+ const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
110
+ const json = commandArgs.includes("--json");
111
+ const { report, reportDir } = await runCiCompatibilityReport({
112
+ configPath,
113
+ openclawPath,
114
+ outDir,
115
+ pluginRoot,
116
+ });
117
+
118
+ const summary = await buildCiSummary({
119
+ artifactBaseDir: reportDir,
120
+ reportPaths: {
121
+ compatibility: "plugin-inspector-report.json",
122
+ },
123
+ reports: {
124
+ compatibility: report,
125
+ },
126
+ });
127
+ await writeCiSummary(summary, {
128
+ jsonPath: path.join(reportDir, "plugin-inspector-ci-summary.json"),
129
+ markdownPath: path.join(reportDir, "plugin-inspector-ci-summary.md"),
130
+ });
131
+
132
+ if (json) {
133
+ console.log(JSON.stringify(summary, null, 2));
134
+ } else {
135
+ console.log(renderCiTextSummary(summary));
136
+ }
137
+
138
+ if (summary.status !== "pass") {
139
+ throw new Error("plugin-inspector ci summary failed");
140
+ }
141
+ }
142
+
143
+ async function runCiCompatibilityReport({ configPath, openclawPath, outDir, pluginRoot }) {
144
+ if (configPath) {
145
+ const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
146
+ const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
147
+ await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
148
+ return {
149
+ report,
150
+ reportDir: path.resolve(config.rootDir, outDir),
151
+ };
152
+ }
153
+
154
+ const { report } = await runPluginCheck({ pluginRoot, outDir, openclawPath });
155
+ return {
156
+ report,
157
+ reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
158
+ };
159
+ }
160
+
74
161
  async function runCapture(commandArgs) {
75
162
  const entrypoint = commandArgs.find((arg) => !arg.startsWith("-"));
76
163
  const outputPath = readFlag(commandArgs, "--output");
77
164
  const pluginRoot = readFlag(commandArgs, "--plugin-root");
78
- const mockSdk = commandArgs.includes("--mock-sdk");
165
+ const mockSdk = readMockSdkFlag(commandArgs) ?? commandArgs.includes("--mock-sdk");
79
166
  if (!entrypoint) {
80
167
  throw new Error("capture requires an entrypoint path");
81
168
  }
@@ -100,14 +187,58 @@ function readFlag(commandArgs, name) {
100
187
  return commandArgs[index + 1] ?? null;
101
188
  }
102
189
 
190
+ function readRuntimeFlag(commandArgs) {
191
+ if (commandArgs.includes("--runtime") || commandArgs.includes("--capture")) {
192
+ return true;
193
+ }
194
+ if (commandArgs.includes("--no-runtime") || commandArgs.includes("--no-capture")) {
195
+ return false;
196
+ }
197
+ return undefined;
198
+ }
199
+
200
+ function readMockSdkFlag(commandArgs) {
201
+ const sdk = readFlag(commandArgs, "--sdk");
202
+ if (sdk === "mock") {
203
+ return true;
204
+ }
205
+ if (sdk === "real") {
206
+ return false;
207
+ }
208
+ if (sdk && !["mock", "real"].includes(sdk)) {
209
+ throw new Error("--sdk must be mock or real");
210
+ }
211
+ if (commandArgs.includes("--mock-sdk")) {
212
+ return true;
213
+ }
214
+ if (commandArgs.includes("--real-sdk")) {
215
+ return false;
216
+ }
217
+ return undefined;
218
+ }
219
+
220
+ function renderCiTextSummary(summary) {
221
+ return [
222
+ `Status: ${summary.status.toUpperCase()}`,
223
+ `Breakages: ${summary.summary.breakages}`,
224
+ `Issues: ${summary.summary.issues}`,
225
+ `Artifacts: ${Object.values(summary.artifacts).filter(Boolean).length}`,
226
+ ].join("\n");
227
+ }
228
+
103
229
  function printHelp() {
104
230
  console.log(`plugin-inspector
105
231
 
106
232
  Usage:
107
- plugin-inspector check [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--capture] [--json]
233
+ plugin-inspector
234
+ plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json]
235
+ plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--package-manager npm|pnpm|yarn|bun] [--force]
108
236
  plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
109
237
  plugin-inspector inspect --config <path> [--out <dir>] [--check] [--json]
110
- plugin-inspector ci --config <path> [--out <dir>]
111
- PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture <entrypoint> [--mock-sdk] [--plugin-root <path>] [--output <path>]
238
+ plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--json]
239
+ PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--plugin-root <path>] [--output <path>]
240
+
241
+ Default check runs from the current plugin root and writes reports/ unless --out is set.
242
+ Runtime capture is opt-in because it imports plugin code; use --runtime with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
112
243
  `);
113
244
  }
package/src/config.js CHANGED
@@ -52,6 +52,19 @@ export function validateInspectorConfig(config) {
52
52
  errors.push("config.fixtures must be a non-empty array");
53
53
  }
54
54
 
55
+ if (config.capture !== undefined) {
56
+ if (!config.capture || typeof config.capture !== "object" || Array.isArray(config.capture)) {
57
+ errors.push("config.capture must be an object when present");
58
+ } else {
59
+ if (config.capture.runtime !== undefined && typeof config.capture.runtime !== "boolean") {
60
+ errors.push("config.capture.runtime must be a boolean when present");
61
+ }
62
+ if (config.capture.mockSdk !== undefined && typeof config.capture.mockSdk !== "boolean") {
63
+ errors.push("config.capture.mockSdk must be a boolean when present");
64
+ }
65
+ }
66
+ }
67
+
55
68
  const ids = new Set();
56
69
  const paths = new Set();
57
70
  for (const fixture of config.fixtures ?? []) {
@@ -134,6 +147,7 @@ export async function normalizePluginRootConfig(config, options = {}) {
134
147
  return {
135
148
  version: 1,
136
149
  submoduleRoot: ".",
150
+ capture: config.capture,
137
151
  openclaw: config.openclaw,
138
152
  fixtures: [fixture],
139
153
  };
@@ -157,7 +171,7 @@ async function readJsonIfExists(filePath) {
157
171
  return JSON.parse(await readFile(filePath, "utf8"));
158
172
  }
159
173
 
160
- function packageId(packageName) {
174
+ export function packageId(packageName) {
161
175
  if (!packageName) {
162
176
  return null;
163
177
  }
@@ -170,7 +184,7 @@ function packageId(packageName) {
170
184
  .toLowerCase();
171
185
  }
172
186
 
173
- function inferPluginSeams(pluginManifest, packageJson) {
187
+ export function inferPluginSeams(pluginManifest, packageJson) {
174
188
  const contracts = Object.keys(pluginManifest?.contracts ?? {});
175
189
  if (contracts.includes("tools")) {
176
190
  return ["dynamic-tool"];
@@ -29,6 +29,11 @@ export const contractProbeRules = {
29
29
  contract: "Every observed OpenClaw plugin SDK import remains exported by the target OpenClaw package.",
30
30
  target: "sdk-alias",
31
31
  },
32
+ "reserved-sdk-import": {
33
+ id: "sdk.import.reserved-bundled-plugin-boundary",
34
+ contract: "External plugins use documented public SDK subpaths instead of reserved bundled-plugin compatibility shims.",
35
+ target: "sdk-import",
36
+ },
32
37
  "provider-auth-env-vars": {
33
38
  id: "manifest.compat.provider-auth-env-vars",
34
39
  contract: "Legacy provider auth env metadata continues to map into config/help surfaces.",
@@ -626,7 +626,12 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
626
626
 
627
627
  const sdkExports = new Set(targetOpenClaw.sdkExports);
628
628
  const unknownImports = fixtureReport.sdkImportDetails.filter((sdkImport) => !sdkExports.has(sdkImport.specifier));
629
- if (unknownImports.length === 0) {
629
+ const reservedSdkExports = new Set(targetOpenClaw.reservedSdkExports ?? []);
630
+ const reservedImports = fixtureReport.sdkImportDetails.filter((sdkImport) =>
631
+ reservedSdkExports.has(sdkImport.specifier),
632
+ );
633
+
634
+ if (reservedImports.length === 0 && unknownImports.length === 0) {
630
635
  logs.push({
631
636
  fixture: fixture.id,
632
637
  code: "sdk-exports-present",
@@ -637,21 +642,40 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
637
642
  return;
638
643
  }
639
644
 
640
- warnings.push({
641
- fixture: fixture.id,
642
- code: "sdk-export-missing",
643
- level: "warning",
644
- message: "fixture imports plugin SDK aliases that are not exported by the target OpenClaw package",
645
- evidence: detailEvidence(unknownImports, "specifier"),
646
- compatRecord: "plugin-sdk-export-aliases",
647
- });
648
- decisions.push({
649
- fixture: fixture.id,
650
- decision: "core-compat-adapter",
651
- seam: "sdk-alias",
652
- action: "Restore the package export alias or publish a versioned migration map before cold-importing old plugins.",
653
- evidence: unique(unknownImports.map((sdkImport) => sdkImport.specifier)).join(", "),
654
- });
645
+ if (unknownImports.length > 0) {
646
+ warnings.push({
647
+ fixture: fixture.id,
648
+ code: "sdk-export-missing",
649
+ level: "warning",
650
+ message: "fixture imports plugin SDK aliases that are not exported by the target OpenClaw package",
651
+ evidence: detailEvidence(unknownImports, "specifier"),
652
+ compatRecord: "plugin-sdk-export-aliases",
653
+ });
654
+ decisions.push({
655
+ fixture: fixture.id,
656
+ decision: "core-compat-adapter",
657
+ seam: "sdk-alias",
658
+ action: "Restore the package export alias or publish a versioned migration map before cold-importing old plugins.",
659
+ evidence: unique(unknownImports.map((sdkImport) => sdkImport.specifier)).join(", "),
660
+ });
661
+ }
662
+
663
+ if (reservedImports.length > 0) {
664
+ warnings.push({
665
+ fixture: fixture.id,
666
+ code: "reserved-sdk-import",
667
+ level: "warning",
668
+ message: "fixture imports reserved bundled-plugin SDK compatibility subpaths",
669
+ evidence: detailEvidence(reservedImports, "specifier"),
670
+ });
671
+ decisions.push({
672
+ fixture: fixture.id,
673
+ decision: "plugin-upstream-fix",
674
+ seam: "sdk-import",
675
+ action: "Move the plugin to documented public SDK subpaths or plugin-local helpers before relying on this compatibility shim.",
676
+ evidence: unique(reservedImports.map((sdkImport) => sdkImport.specifier)).join(", "),
677
+ });
678
+ }
655
679
  }
656
680
 
657
681
  function classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
package/src/index.js CHANGED
@@ -6,5 +6,6 @@ export {
6
6
  loadPluginConfig,
7
7
  renderTextSummary,
8
8
  runPluginCheck,
9
+ setupPluginInspector,
9
10
  writePluginReports,
10
11
  } from "./api.js";
package/src/init.js ADDED
@@ -0,0 +1,150 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { inferPluginSeams, packageId } from "./config.js";
5
+
6
+ export const defaultInitConfigPath = "plugin-inspector.config.json";
7
+ export const defaultInitWorkflowPath = ".github/workflows/plugin-inspector.yml";
8
+
9
+ export async function writePluginInspectorInit(options = {}) {
10
+ const pluginRoot = path.resolve(options.pluginRoot ?? options.cwd ?? process.cwd());
11
+ const configPath = path.resolve(pluginRoot, options.configPath ?? defaultInitConfigPath);
12
+ const written = [];
13
+
14
+ if (existsSync(configPath) && options.force !== true) {
15
+ throw new Error(`${path.relative(pluginRoot, configPath)} already exists; pass --force to overwrite it`);
16
+ }
17
+
18
+ const config = await buildPluginInspectorConfig({ pluginRoot });
19
+ await mkdir(path.dirname(configPath), { recursive: true });
20
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
21
+ written.push(configPath);
22
+
23
+ if (options.ci === true) {
24
+ const workflowPath = path.resolve(pluginRoot, options.workflowPath ?? defaultInitWorkflowPath);
25
+ if (existsSync(workflowPath) && options.force !== true) {
26
+ throw new Error(`${path.relative(pluginRoot, workflowPath)} already exists; pass --force to overwrite it`);
27
+ }
28
+ await mkdir(path.dirname(workflowPath), { recursive: true });
29
+ await writeFile(workflowPath, renderGithubActionsWorkflow({ packageManager: options.packageManager }), "utf8");
30
+ written.push(workflowPath);
31
+ }
32
+
33
+ return { pluginRoot, configPath, written };
34
+ }
35
+
36
+ export async function buildPluginInspectorConfig(options = {}) {
37
+ const pluginRoot = path.resolve(options.pluginRoot ?? options.cwd ?? process.cwd());
38
+ const packageJson = await readJsonIfExists(path.join(pluginRoot, "package.json"));
39
+ const pluginManifest = await readJsonIfExists(path.join(pluginRoot, "openclaw.plugin.json"));
40
+ const sourceRoot = inferSourceRoot(packageJson);
41
+
42
+ const plugin = {
43
+ id: pluginManifest?.id ?? packageId(packageJson?.name) ?? "plugin",
44
+ priority: "high",
45
+ seams: inferPluginSeams(pluginManifest, packageJson),
46
+ };
47
+
48
+ if (sourceRoot !== ".") {
49
+ plugin.sourceRoot = sourceRoot;
50
+ }
51
+
52
+ return {
53
+ version: 1,
54
+ plugin,
55
+ capture: {
56
+ mockSdk: true,
57
+ },
58
+ };
59
+ }
60
+
61
+ export function renderGithubActionsWorkflow(options = {}) {
62
+ const packageManager = normalizePackageManager(options.packageManager);
63
+ const setup = packageManagerSetup(packageManager);
64
+
65
+ return `name: plugin-inspector
66
+
67
+ on:
68
+ pull_request:
69
+ push:
70
+ branches: [main]
71
+
72
+ jobs:
73
+ check:
74
+ runs-on: ubuntu-latest
75
+ steps:
76
+ - uses: actions/checkout@v5
77
+ - uses: actions/setup-node@v5
78
+ with:
79
+ node-version: 24
80
+ cache: ${setup.cache}
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
84
+ - uses: actions/upload-artifact@v5
85
+ if: always()
86
+ with:
87
+ name: plugin-inspector-reports
88
+ path: reports/plugin-inspector-*
89
+ `;
90
+ }
91
+
92
+ function inferSourceRoot(packageJson) {
93
+ const entrypoints = [
94
+ packageJson?.openclaw?.entrypoint,
95
+ ...(packageJson?.openclaw?.extensions ?? []),
96
+ ...(packageJson?.openclaw?.runtimeExtensions ?? []),
97
+ ].filter((value) => typeof value === "string");
98
+ const entrypoint = entrypoints[0] ?? packageJson?.exports?.["."] ?? packageJson?.main ?? "src/index.js";
99
+ if (typeof entrypoint === "string" && entrypoint.startsWith("src/")) {
100
+ return "src";
101
+ }
102
+ return ".";
103
+ }
104
+
105
+ async function readJsonIfExists(filePath) {
106
+ if (!existsSync(filePath)) {
107
+ return null;
108
+ }
109
+ return JSON.parse(await readFile(filePath, "utf8"));
110
+ }
111
+
112
+ function normalizePackageManager(packageManager = "npm") {
113
+ if (["npm", "pnpm", "yarn", "bun"].includes(packageManager)) {
114
+ return packageManager;
115
+ }
116
+ throw new Error("--package-manager must be npm, pnpm, yarn, or bun");
117
+ }
118
+
119
+ function packageManagerSetup(packageManager) {
120
+ if (packageManager === "pnpm") {
121
+ return {
122
+ cache: "pnpm",
123
+ corepack: true,
124
+ install: "pnpm install --frozen-lockfile",
125
+ exec: "pnpm dlx",
126
+ };
127
+ }
128
+ if (packageManager === "yarn") {
129
+ return {
130
+ cache: "yarn",
131
+ corepack: true,
132
+ install: "yarn install --immutable",
133
+ exec: "yarn dlx",
134
+ };
135
+ }
136
+ if (packageManager === "bun") {
137
+ return {
138
+ cache: "npm",
139
+ corepack: false,
140
+ install: "bun install --frozen-lockfile",
141
+ exec: "bunx",
142
+ };
143
+ }
144
+ return {
145
+ cache: "npm",
146
+ corepack: false,
147
+ install: "npm ci",
148
+ exec: "npx",
149
+ };
150
+ }
package/src/inspector.js CHANGED
@@ -156,7 +156,12 @@ export async function captureEntrypoint(entrypoint, options = {}) {
156
156
  }
157
157
 
158
158
  const resolvedEntrypoint = path.resolve(options.cwd ?? process.cwd(), entrypoint);
159
- const module = await import(pathToFileURL(resolvedEntrypoint).href);
159
+ let module;
160
+ try {
161
+ module = await import(pathToFileURL(resolvedEntrypoint).href);
162
+ } catch (error) {
163
+ throw classifyCapturePhaseError(error, "entrypoint-import-error");
164
+ }
160
165
  const register = findRegisterExport(module);
161
166
 
162
167
  if (!register) {
@@ -168,7 +173,11 @@ export async function captureEntrypoint(entrypoint, options = {}) {
168
173
  }
169
174
 
170
175
  const api = createCaptureApi(options.apiOptions);
171
- await register(api);
176
+ try {
177
+ await register(api);
178
+ } catch (error) {
179
+ throw classifyCapturePhaseError(error, "registration-execution-error");
180
+ }
172
181
  const result = {
173
182
  status: "captured",
174
183
  entrypoint: resolvedEntrypoint,
@@ -188,19 +197,85 @@ export async function captureEntrypointWithMockSdk(entrypoint, options = {}) {
188
197
  pluginRoot: options.pluginRoot,
189
198
  apiOptions: options.apiOptions,
190
199
  };
191
- const { stdout } = await execFileAsync(
192
- process.execPath,
193
- ["--preserve-symlinks", runnerPath, JSON.stringify(payload)],
194
- {
195
- cwd: options.cwd ?? process.cwd(),
196
- env: {
197
- ...process.env,
198
- ...(options.env ?? {}),
200
+ try {
201
+ const { stdout } = await execFileAsync(
202
+ process.execPath,
203
+ ["--no-warnings", "--preserve-symlinks", runnerPath, JSON.stringify(payload)],
204
+ {
205
+ cwd: options.cwd ?? process.cwd(),
206
+ env: {
207
+ ...process.env,
208
+ ...(options.env ?? {}),
209
+ },
210
+ maxBuffer: 1024 * 1024 * 10,
199
211
  },
200
- maxBuffer: 1024 * 1024 * 10,
201
- },
202
- );
203
- return JSON.parse(stdout);
212
+ );
213
+ return JSON.parse(stdout);
214
+ } catch (error) {
215
+ throw classifyMockSdkCaptureError(error);
216
+ }
217
+ }
218
+
219
+ export function classifyMockSdkCaptureError(error) {
220
+ const rawMessage = [error?.stderr, error?.stdout, error?.message].filter(Boolean).join("\n");
221
+ const missingExport = rawMessage.match(/does not provide an export named ['"]([^'"]+)['"]/)?.[1];
222
+ if (missingExport) {
223
+ return enrichCaptureError(error, {
224
+ message: `Mock SDK import failed: openclaw/plugin-sdk is missing export ${missingExport}`,
225
+ failureClass: "missing-sdk-export",
226
+ missingExport,
227
+ });
228
+ }
229
+
230
+ const missingModule =
231
+ rawMessage.match(/Cannot find (?:package|module) ['"]([^'"]*openclaw\/plugin-sdk[^'"]*)['"]/)?.[1] ??
232
+ rawMessage.match(/Package subpath ['"](\.\/plugin-sdk\/[^'"]+)['"]/)?.[1];
233
+ if (missingModule || rawMessage.includes("openclaw/plugin-sdk")) {
234
+ return enrichCaptureError(error, {
235
+ message: `Mock SDK import failed: ${missingModule ?? "openclaw/plugin-sdk module could not be resolved"}`,
236
+ failureClass: "missing-sdk-module",
237
+ missingModule,
238
+ });
239
+ }
240
+
241
+ const failureClass = rawMessage.match(/\[plugin-inspector:([^\]]+)\]/)?.[1];
242
+ if (failureClass) {
243
+ return enrichCaptureError(error, {
244
+ message: firstMeaningfulErrorLine(rawMessage.replace(/\[plugin-inspector:[^\]]+\]/, "")) ?? "Mock SDK capture failed",
245
+ failureClass,
246
+ });
247
+ }
248
+
249
+ return enrichCaptureError(error, {
250
+ message: firstMeaningfulErrorLine(rawMessage) ?? "Mock SDK capture failed",
251
+ failureClass: "mock-sdk-capture-error",
252
+ });
253
+ }
254
+
255
+ export function classifyCapturePhaseError(error, failureClass) {
256
+ return enrichCaptureError(error, {
257
+ message: error instanceof Error ? error.message : String(error),
258
+ failureClass,
259
+ });
260
+ }
261
+
262
+ function enrichCaptureError(error, details) {
263
+ const wrapped = new Error(details.message, { cause: error });
264
+ wrapped.failureClass = details.failureClass;
265
+ if (details.missingExport) {
266
+ wrapped.missingExport = details.missingExport;
267
+ }
268
+ if (details.missingModule) {
269
+ wrapped.missingModule = details.missingModule;
270
+ }
271
+ return wrapped;
272
+ }
273
+
274
+ function firstMeaningfulErrorLine(message) {
275
+ return String(message)
276
+ .split("\n")
277
+ .map((line) => line.trim())
278
+ .find((line) => line && !line.startsWith("Command failed:"));
204
279
  }
205
280
 
206
281
  function findRegisterExport(module) {
package/src/issues.js CHANGED
@@ -32,6 +32,7 @@ export const knownIssueCodes = new Set([
32
32
  "provider-auth-env-vars",
33
33
  "registration-capture-gap",
34
34
  "runtime-tool-capture",
35
+ "reserved-sdk-import",
35
36
  "sdk-export-missing",
36
37
  ]);
37
38
 
@@ -78,6 +79,12 @@ export const issueMetadataByCode = {
78
79
  decision: "core-compat-adapter",
79
80
  title: "plugin SDK import aliases are missing from target package exports",
80
81
  },
82
+ "reserved-sdk-import": {
83
+ severity: "P1",
84
+ owner: "plugin",
85
+ decision: "plugin-upstream-fix",
86
+ title: "plugin imports reserved bundled-plugin SDK compatibility subpaths",
87
+ },
81
88
  "missing-compat-record": {
82
89
  severity: "P1",
83
90
  owner: "core",
@@ -310,6 +317,7 @@ function issueClassFor(code, options) {
310
317
  "package-openclaw-entry-missing",
311
318
  "package-openclaw-metadata-missing",
312
319
  "package-plugin-api-compat-missing",
320
+ "reserved-sdk-import",
313
321
  ].includes(code)
314
322
  ) {
315
323
  return "upstream-metadata";