@openclaw/plugin-inspector 0.3.10 → 0.3.11

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,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.11 - 2026-05-26
6
+
7
+ ### Fixed
8
+
9
+ - Classify the latest generated kitchen-sink registrars for meeting notes, node CLI features, hosted media, model catalogs, embedding providers, and session actions.
10
+ - 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.
11
+ - Treat compat-gap issues as reconciled contract coverage for their own compatibility record.
12
+ - Count passed synthetic hook probes as runtime coverage so conversation-access and `before_tool_call` inspector gaps close when probe artifacts prove them.
13
+ - Keep mock-SDK synthetic probes in-process so retained hook and registration handlers remain callable, and harden dynamic root SDK mock exports.
14
+ - Synthesize nested manifest config samples for optional object settings with required inner shape.
15
+ - Populate message and agent lifecycle synthetic hook payloads with the fields telemetry plugins commonly read.
16
+ - Resolve plugin manifests from parent directories when runtime capture starts from built `dist` entrypoints.
17
+ - Capture CLIs no longer treat `--output`, `--plugin-root`, or `--sdk` flag values as the positional entrypoint. Thanks @KrasimirKralev.
18
+ - Mock-SDK TypeScript capture now falls back to Node's strip-only parser on Node 26.
19
+
5
20
  ## 0.3.10 - 2026-05-03
6
21
 
7
22
  ### Fixed
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.11",
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,
@@ -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)) {
package/src/cli.js CHANGED
@@ -223,7 +223,7 @@ async function runCiCompatibilityReport({ allowExecution, capture, configPath, m
223
223
  }
224
224
 
225
225
  async function runCapture(commandArgs) {
226
- const entrypoint = commandArgs.find((arg) => !arg.startsWith("-"));
226
+ const entrypoint = findCaptureEntrypoint(commandArgs);
227
227
  const outputPath = readFlag(commandArgs, "--output");
228
228
  const pluginRoot = readFlag(commandArgs, "--plugin-root");
229
229
  const mockSdk = readMockSdkFlag(commandArgs) ?? commandArgs.includes("--mock-sdk");
@@ -252,6 +252,17 @@ function readFlag(commandArgs, name) {
252
252
  return commandArgs[index + 1] ?? null;
253
253
  }
254
254
 
255
+ function findCaptureEntrypoint(commandArgs) {
256
+ const flagsWithValues = new Set(["--output", "--plugin-root", "--sdk"]);
257
+ const consumedIndexes = new Set();
258
+ for (const [index, arg] of commandArgs.entries()) {
259
+ if (flagsWithValues.has(arg)) {
260
+ consumedIndexes.add(index + 1);
261
+ }
262
+ }
263
+ return commandArgs.find((arg, index) => !arg.startsWith("-") && !consumedIndexes.has(index)) ?? null;
264
+ }
265
+
255
266
  function readOptionalPathFlag(commandArgs, name, defaultPath) {
256
267
  const index = commandArgs.indexOf(name);
257
268
  if (index === -1) {
@@ -58,9 +58,9 @@ export function renderCompatibilityMarkdownReport(report, options = {}) {
58
58
  options,
59
59
  ),
60
60
  "",
61
- "## Live Issues",
61
+ "## Other Live Issues",
62
62
  "",
63
- issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
63
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity !== "P0"), options),
64
64
  "",
65
65
  "## Compat Gaps",
66
66
  "",
@@ -183,9 +183,9 @@ export function renderCompatibilityIssuesReport(report, options = {}) {
183
183
  options,
184
184
  ),
185
185
  "",
186
- "## Live Issues",
186
+ "## Other Live Issues",
187
187
  "",
188
- issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
188
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity !== "P0"), options),
189
189
  "",
190
190
  "## Compat Gaps",
191
191
  "",
@@ -157,10 +157,15 @@ function requireCompatRecordReconciliation(report, errors) {
157
157
  .filter((finding) => finding.code === "missing-compat-record")
158
158
  .map((finding) => `${finding.fixture}:${finding.compatRecord}`),
159
159
  );
160
+ const compatGapRecords = new Set(
161
+ report.issues
162
+ .filter((issue) => issue.issueClass === "compat-gap" && issue.compatRecord)
163
+ .map((issue) => `${issue.fixture}:${issue.compatRecord}`),
164
+ );
160
165
 
161
166
  for (const finding of [...report.warnings, ...report.suggestions].filter((item) => item.compatRecord)) {
162
167
  const key = `${finding.fixture}:${finding.compatRecord}`;
163
- if (!presentRecords.has(key) && !missingRecords.has(key)) {
168
+ if (!presentRecords.has(key) && !missingRecords.has(key) && !compatGapRecords.has(key)) {
164
169
  errors.push(`${finding.fixture}: compat record ${finding.compatRecord} was not reconciled`);
165
170
  }
166
171
  }
@@ -114,6 +114,11 @@ export const contractProbeRules = {
114
114
  contract: "Package and OpenClaw manifest versions stay aligned for release compatibility reporting.",
115
115
  target: "package-loader",
116
116
  },
117
+ "manifest-name-missing": {
118
+ id: "manifest.metadata.name",
119
+ contract: "OpenClaw plugin manifests declare a human-readable display name for registry and tooling metadata.",
120
+ target: "manifest-loader",
121
+ },
117
122
  "package-plugin-api-compat-missing": {
118
123
  id: "package.compat.plugin-api-range",
119
124
  contract: "Package metadata declares the OpenClaw plugin API range used by the plugin.",
@@ -204,6 +204,7 @@ function summarizeArtifact({ artifactPath, parsed, rootDir }) {
204
204
  : "capture";
205
205
  const fixture = normalizedArtifactPath.split("/").at(-2) ?? "unknown";
206
206
  if (kind === "synthetic") {
207
+ const passed = (parsed.results ?? []).filter((result) => result.status === "pass");
207
208
  return {
208
209
  artifactPath: normalizedArtifactPath,
209
210
  fixture,
@@ -211,6 +212,8 @@ function summarizeArtifact({ artifactPath, parsed, rootDir }) {
211
212
  entrypoint: scrubPath(parsed.entrypoint, { rootDir }),
212
213
  status: parsed.status,
213
214
  summary: parsed.summary,
215
+ captured: passed.map(syntheticRuntimeCaptureKey).filter(Boolean),
216
+ passed,
214
217
  failures: (parsed.results ?? []).filter((result) => result.status === "fail"),
215
218
  blocked: (parsed.results ?? []).filter((result) => result.status === "blocked"),
216
219
  };
@@ -261,6 +264,13 @@ function summarizeArtifactResult(artifact) {
261
264
  return `${artifact.capturedCount} captured`;
262
265
  }
263
266
 
267
+ function syntheticRuntimeCaptureKey(result) {
268
+ if (!result.kind || !result.seam) {
269
+ return null;
270
+ }
271
+ return `${result.kind}:${result.seam}`;
272
+ }
273
+
264
274
  function auditFindingCount(parsed) {
265
275
  const vulnerabilities = parsed.metadata?.vulnerabilities;
266
276
  if (vulnerabilities && typeof vulnerabilities === "object") {
@@ -233,6 +233,26 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
233
233
  });
234
234
  }
235
235
 
236
+ const missingManifestNames = fixtureReport.pluginManifests.filter(
237
+ (manifest) => typeof manifest.name !== "string" || manifest.name.trim().length === 0,
238
+ );
239
+ if (missingManifestNames.length > 0) {
240
+ warnings.push({
241
+ fixture: fixture.id,
242
+ code: "manifest-name-missing",
243
+ level: "warning",
244
+ message: "openclaw.plugin.json does not declare a display name",
245
+ evidence: missingManifestNames.map((manifest) => manifest.path ?? "openclaw.plugin.json"),
246
+ });
247
+ decisions.push({
248
+ fixture: fixture.id,
249
+ decision: "plugin-upstream-fix",
250
+ seam: "manifest-metadata",
251
+ action: "Ask the plugin to declare openclaw.plugin.json name so registries and tools can derive a human-readable title.",
252
+ evidence: missingManifestNames.map((manifest) => manifest.path ?? "openclaw.plugin.json").join(", "),
253
+ });
254
+ }
255
+
236
256
  if (packageSummary.openclaw && !packageSummary.openclaw.compatPluginApi) {
237
257
  warnings.push({
238
258
  fixture: fixture.id,
@@ -340,9 +360,12 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
340
360
  });
341
361
  }
342
362
 
343
- const missingEntrypoints = packageSummary.openclaw?.entrypoints.filter((entrypoint) => !entrypoint.exists) ?? [];
363
+ const entrypoints = packageSummary.openclaw?.entrypoints ?? [];
364
+ const missingEntrypoints = entrypoints.filter((entrypoint) => !entrypoint.exists);
344
365
  const buildEntrypoints = missingEntrypoints.filter((entrypoint) => entrypoint.requiresBuild);
345
- const plainMissingEntrypoints = missingEntrypoints.filter((entrypoint) => !entrypoint.requiresBuild);
366
+ const plainMissingEntrypoints = missingEntrypoints.filter(
367
+ (entrypoint) => !entrypoint.requiresBuild && !hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoints),
368
+ );
346
369
 
347
370
  if (buildEntrypoints.length > 0) {
348
371
  suggestions.push({
@@ -920,6 +943,46 @@ function collectOpenClawEntrypoints(packageDir, openclaw, options) {
920
943
  });
921
944
  }
922
945
 
946
+ function hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
947
+ if (!isSourceEntrypoint(entrypoint.specifier)) {
948
+ return false;
949
+ }
950
+
951
+ const runtimeBuildSpecifier = runtimeBuildSpecifierFor(entrypoint.specifier);
952
+ if (
953
+ entrypoints.some(
954
+ (candidate) =>
955
+ candidate.exists &&
956
+ candidate.requiresBuild &&
957
+ normalizeEntrypointSpecifier(candidate.specifier) === normalizeEntrypointSpecifier(runtimeBuildSpecifier),
958
+ )
959
+ ) {
960
+ return true;
961
+ }
962
+
963
+ if (entrypoint.kind === "extension" && entrypoints.some((candidate) => candidate.kind === "runtimeExtension" && candidate.exists)) {
964
+ return true;
965
+ }
966
+
967
+ const packageDir = path.dirname(packageSummary.path);
968
+ return existsSync(path.resolve(packageDir, runtimeBuildSpecifier));
969
+ }
970
+
971
+ function isSourceEntrypoint(specifier) {
972
+ return /\.(?:ts|tsx)$/.test(specifier);
973
+ }
974
+
975
+ function runtimeBuildSpecifierFor(specifier) {
976
+ const normalized = normalizeEntrypointSpecifier(specifier);
977
+ const basename = path.posix.basename(normalized).replace(/\.(?:ts|tsx)$/, ".js");
978
+ return `./dist/${basename}`;
979
+ }
980
+
981
+ function normalizeEntrypointSpecifier(specifier) {
982
+ const normalized = specifier.replaceAll("\\", "/");
983
+ return normalized.startsWith("./") ? normalized : `./${normalized}`;
984
+ }
985
+
923
986
  async function findPackageFiles(root, options, depth = 0) {
924
987
  if (!existsSync(root) || depth > options.maxDepth) {
925
988
  return [];
@@ -1059,8 +1122,13 @@ function packageNpmPackIssues(packageSummary, fixtureReport) {
1059
1122
  });
1060
1123
  }
1061
1124
 
1062
- const missingEntrypoints = packageSummary.openclaw?.entrypoints
1063
- .filter((entrypoint) => !repoPathIncludedInNpmPack(packageSummary, entrypoint.relativePath))
1125
+ const entrypoints = packageSummary.openclaw?.entrypoints ?? [];
1126
+ const missingEntrypoints = entrypoints
1127
+ .filter(
1128
+ (entrypoint) =>
1129
+ !repoPathIncludedInNpmPack(packageSummary, entrypoint.relativePath) &&
1130
+ !hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints),
1131
+ )
1064
1132
  .map((entrypoint) => `${entrypoint.kind}:${entrypoint.specifier} -> ${entrypoint.relativePath}`) ?? [];
1065
1133
  if (missingEntrypoints.length > 0) {
1066
1134
  findings.push({
@@ -1088,6 +1156,35 @@ function packageNpmPackMissingMetadata(packageSummary, fixtureReport) {
1088
1156
  return missing;
1089
1157
  }
1090
1158
 
1159
+ function hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
1160
+ if (!isSourceEntrypoint(entrypoint.specifier)) {
1161
+ return false;
1162
+ }
1163
+
1164
+ const runtimeBuildSpecifier = runtimeBuildSpecifierFor(entrypoint.specifier);
1165
+ const matchingRuntimeEntrypoint = entrypoints.find(
1166
+ (candidate) =>
1167
+ candidate.requiresBuild &&
1168
+ normalizeEntrypointSpecifier(candidate.specifier) === normalizeEntrypointSpecifier(runtimeBuildSpecifier),
1169
+ );
1170
+ if (matchingRuntimeEntrypoint && repoPathIncludedInNpmPack(packageSummary, matchingRuntimeEntrypoint.relativePath)) {
1171
+ return true;
1172
+ }
1173
+
1174
+ if (
1175
+ entrypoint.kind === "extension" &&
1176
+ entrypoints.some(
1177
+ (candidate) => candidate.kind === "runtimeExtension" && repoPathIncludedInNpmPack(packageSummary, candidate.relativePath),
1178
+ )
1179
+ ) {
1180
+ return true;
1181
+ }
1182
+
1183
+ const packageDir = path.posix.dirname(normalizeRepoPath(packageSummary.path));
1184
+ const runtimeBuildPath = path.posix.join(packageDir === "." ? "" : packageDir, normalizeEntrypointSpecifier(runtimeBuildSpecifier));
1185
+ return repoPathIncludedInNpmPack(packageSummary, runtimeBuildPath);
1186
+ }
1187
+
1091
1188
  function packageMinHostVersionDrift(packageSummary) {
1092
1189
  const openclaw = packageSummary.openclaw;
1093
1190
  if (!nonEmptyString(openclaw?.install?.minHostVersion) || !nonEmptyString(openclaw?.buildOpenClawVersion)) {
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ import * as refDiffApi from "./ref-diff.js";
14
14
  import * as reportApi from "./report.js";
15
15
  import * as runtimeProfileApi from "./runtime-profile.js";
16
16
  import * as runtimeReconciliationApi from "./runtime-reconciliation.js";
17
+ import * as syntheticEntrypointApi from "./synthetic-entrypoint.js";
17
18
  import * as syntheticProbeSuiteApi from "./synthetic-probe-suite.js";
18
19
  import * as syntheticProbesApi from "./synthetic-probes.js";
19
20
 
@@ -123,6 +124,7 @@ export const synthetic = Object.freeze({
123
124
  renderPlan: syntheticProbesApi.renderSyntheticProbeMarkdown,
124
125
  validatePlan: syntheticProbesApi.validateSyntheticProbePlan,
125
126
  runCaptured: syntheticProbesApi.runCapturedSyntheticProbes,
127
+ runEntrypoint: syntheticEntrypointApi.runEntrypointSyntheticProbes,
126
128
  registrationExecutionProfiles: syntheticProbesApi.syntheticRegistrationExecutionProfiles,
127
129
  defaultHookEvents: syntheticProbesApi.defaultSyntheticHookEvents,
128
130
  defaultHookContexts: syntheticProbesApi.defaultSyntheticHookContexts,
@@ -234,6 +236,7 @@ export {
234
236
  applyRuntimeExecutionCoverage,
235
237
  buildRuntimeExecutionCoverage,
236
238
  } from "./runtime-reconciliation.js";
239
+ export { runEntrypointSyntheticProbes } from "./synthetic-entrypoint.js";
237
240
  export { buildSyntheticProbePlanFromReport } from "./synthetic-probe-suite.js";
238
241
  export {
239
242
  buildSyntheticProbePlan,
package/src/issues.js CHANGED
@@ -14,6 +14,7 @@ export const knownIssueCodes = new Set([
14
14
  "conversation-access-hook",
15
15
  "legacy-before-agent-start",
16
16
  "legacy-root-sdk-import",
17
+ "manifest-name-missing",
17
18
  "manifest-unknown-contracts",
18
19
  "manifest-unknown-fields",
19
20
  "missing-expected-seam",
@@ -111,6 +112,12 @@ export const issueMetadataByCode = {
111
112
  decision: "inspector-follow-up",
112
113
  title: "fixture no longer exposes an expected seam",
113
114
  },
115
+ "manifest-name-missing": {
116
+ severity: "P2",
117
+ owner: "plugin",
118
+ decision: "plugin-upstream-fix",
119
+ title: "manifest display name is missing",
120
+ },
114
121
  "manifest-unknown-contracts": {
115
122
  severity: "P1",
116
123
  owner: "plugin",
@@ -342,7 +349,10 @@ export function summarizeIssueClasses(issues) {
342
349
  }
343
350
 
344
351
  function issueClassFor(code, options) {
345
- if (["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing", "sdk-export-missing"].includes(code)) {
352
+ if (code === "sdk-export-missing" && options.compatRecord) {
353
+ return "compat-gap";
354
+ }
355
+ if (["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"].includes(code)) {
346
356
  return "live-issue";
347
357
  }
348
358
  if (code === "missing-compat-record") {
@@ -369,6 +379,7 @@ function issueClassFor(code, options) {
369
379
  [
370
380
  "manifest-unknown-contracts",
371
381
  "manifest-unknown-fields",
382
+ "manifest-name-missing",
372
383
  "package-json-missing",
373
384
  "package-manifest-version-drift",
374
385
  "package-min-host-version-drift",
@@ -397,7 +408,7 @@ function severityForClass(code, defaultSeverity, options) {
397
408
  if (
398
409
  options.issueClass === "live-issue" &&
399
410
  ["none", "untracked"].includes(options.compatStatus) &&
400
- ["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing", "sdk-export-missing"].includes(code)
411
+ ["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"].includes(code)
401
412
  ) {
402
413
  return "P0";
403
414
  }
package/src/report.js CHANGED
@@ -225,6 +225,10 @@ export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggest
225
225
  continue;
226
226
  }
227
227
 
228
+ if (finding.code === "sdk-export-missing") {
229
+ continue;
230
+ }
231
+
228
232
  suggestions.push({
229
233
  fixture: finding.fixture,
230
234
  code: "missing-compat-record",
@@ -28,7 +28,7 @@ export function applyRuntimeExecutionCoverage({ findings = [], executionResults
28
28
  export function buildRuntimeExecutionCoverage(executionResults) {
29
29
  const fixtures = new Map();
30
30
  for (const artifact of executionResults?.artifacts ?? []) {
31
- if (artifact.kind !== "capture") {
31
+ if (!["capture", "synthetic"].includes(artifact.kind)) {
32
32
  continue;
33
33
  }
34
34
 
@@ -84,6 +84,9 @@ function expectedRuntimeCaptureKeys(finding) {
84
84
  if (finding.code === "conversation-access-hook") {
85
85
  return names.map((name) => `hook:${name}`);
86
86
  }
87
+ if (finding.code === "before-tool-call-probe") {
88
+ return ["hook:before_tool_call"];
89
+ }
87
90
  return [];
88
91
  }
89
92
 
package/src/sdk-mock.js CHANGED
@@ -377,6 +377,9 @@ function parseModuleImports(text) {
377
377
  ];
378
378
  for (const pattern of patterns) {
379
379
  for (const match of text.matchAll(pattern)) {
380
+ if (isTypeOnlyImportOrExport(match[0], match[1] ?? "")) {
381
+ continue;
382
+ }
380
383
  const specifier = match[2];
381
384
  if (specifier) {
382
385
  entries.push({ specifier, names: parseNamedImports(match[1] ?? "") });
@@ -389,6 +392,10 @@ function parseModuleImports(text) {
389
392
  return entries;
390
393
  }
391
394
 
395
+ function isTypeOnlyImportOrExport(statement, clause) {
396
+ return /^\s*import\s+type\b/u.test(statement) || /^\s*export\s+type\b/u.test(statement) || /^\s*type\b/u.test(clause);
397
+ }
398
+
392
399
  function parseNamedImports(clause) {
393
400
  const names = new Set();
394
401
  const named = /\{([\s\S]*?)\}/.exec(clause)?.[1] ?? clause;
@@ -459,11 +466,22 @@ export async function resolve(specifier, context, nextResolve) {
459
466
  export async function load(url, context, nextLoad) {
460
467
  if (url.startsWith("file:") && /\\.[cm]?ts$/u.test(fileURLToPath(url))) {
461
468
  const rawSource = await readFile(fileURLToPath(url), "utf8");
462
- return { format: "module", source: stripTypeScriptTypes(rawSource, { mode: "transform" }), shortCircuit: true };
469
+ return { format: "module", source: stripPluginTypeScript(rawSource), shortCircuit: true };
463
470
  }
464
471
  return nextLoad(url, context);
465
472
  }
466
473
 
474
+ function stripPluginTypeScript(source) {
475
+ try {
476
+ return stripTypeScriptTypes(source, { mode: "transform" });
477
+ } catch (error) {
478
+ if (error?.code !== "ERR_INVALID_ARG_VALUE") {
479
+ throw error;
480
+ }
481
+ return stripTypeScriptTypes(source, { mode: "strip" });
482
+ }
483
+ }
484
+
467
485
  function moduleUrl(filePath) {
468
486
  return { url: pathToFileURL(filePath).href, shortCircuit: true };
469
487
  }
@@ -1665,11 +1683,53 @@ export const OPENAI_RESPONSES_STREAM_HOOKS = buildProviderStreamFamilyHooks("ope
1665
1683
  export const OPENROUTER_THINKING_STREAM_HOOKS = buildProviderStreamFamilyHooks("openrouter-thinking");
1666
1684
  export const TOOL_STREAM_DEFAULT_ON_HOOKS = buildProviderStreamFamilyHooks("tool-stream-default");
1667
1685
  export const pluginSdkMock = true;
1686
+ ${dynamicExportNames.length > 0 ? mockValueRuntimeSource() : ""}
1668
1687
  ${dynamicExportNames.map(genericExportStatement).join("\n")}
1669
1688
 
1670
1689
  export default {
1671
1690
  ${[...exportNames].map((name) => ` ${name},`).join("\n")}
1672
1691
  };
1692
+ `;
1693
+ }
1694
+
1695
+ function mockValueRuntimeSource() {
1696
+ return `function createMockValue(name) {
1697
+ function fn(...args) {
1698
+ if (name === "resolvePreferredOpenClawTmpDir") {
1699
+ return process.env.TMPDIR || "/tmp";
1700
+ }
1701
+ if (name.startsWith("normalize")) {
1702
+ return typeof args[0] === "string" ? args[0] : "";
1703
+ }
1704
+ if (name === "jsonResult") {
1705
+ return { type: "json", value: args[0] };
1706
+ }
1707
+ if (name === "readStringParam") {
1708
+ return typeof args[0] === "string" ? args[0] : "";
1709
+ }
1710
+ return createMockValue(name);
1711
+ }
1712
+ return new Proxy(fn, {
1713
+ get(_target, property) {
1714
+ if (property === "then") {
1715
+ return undefined;
1716
+ }
1717
+ if (property === Symbol.toPrimitive) {
1718
+ return () => name;
1719
+ }
1720
+ if (property === "toString") {
1721
+ return () => name;
1722
+ }
1723
+ if (property === "valueOf") {
1724
+ return () => name;
1725
+ }
1726
+ return createMockValue(\`\${name}.\${String(property)}\`);
1727
+ },
1728
+ construct() {
1729
+ return createMockValue(name);
1730
+ },
1731
+ });
1732
+ }
1673
1733
  `;
1674
1734
  }
1675
1735
 
@@ -0,0 +1,47 @@
1
+ import { rmSync } from "node:fs";
2
+ import { mkdtemp } from "node:fs/promises";
3
+ import { register } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { captureEntrypoint } from "./inspector.js";
8
+ import { createMockSdkPackage } from "./sdk-mock.js";
9
+ import { runCapturedSyntheticProbes } from "./synthetic-probes.js";
10
+
11
+ export async function runEntrypointSyntheticProbes(entrypoint, options = {}) {
12
+ const capture = await captureEntrypointForSyntheticProbes(entrypoint, {
13
+ ...options,
14
+ apiOptions: {
15
+ ...(options.apiOptions ?? {}),
16
+ retainHandlers: true,
17
+ },
18
+ });
19
+ return runCapturedSyntheticProbes(capture, options);
20
+ }
21
+
22
+ async function captureEntrypointForSyntheticProbes(entrypoint, options) {
23
+ if (options.mockSdk !== true) {
24
+ return captureEntrypoint(entrypoint, options);
25
+ }
26
+
27
+ const cwd = options.cwd ?? process.cwd();
28
+ const resolvedEntrypoint = path.resolve(cwd, entrypoint);
29
+ const pluginRoot = path.resolve(cwd, options.pluginRoot ?? path.dirname(resolvedEntrypoint));
30
+ const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-mock-sdk-"));
31
+ cleanupTempDirOnExit(workspace);
32
+ const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
33
+ register(pathToFileURL(loaderPath));
34
+
35
+ return captureEntrypoint(entrypoint, {
36
+ ...options,
37
+ cwd,
38
+ mockSdk: false,
39
+ pluginRoot,
40
+ });
41
+ }
42
+
43
+ function cleanupTempDirOnExit(dir) {
44
+ process.once("exit", () => {
45
+ rmSync(dir, { force: true, recursive: true });
46
+ });
47
+ }
@@ -1,12 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { rmSync } from "node:fs";
3
- import { mkdtemp } from "node:fs/promises";
4
- import { register } from "node:module";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { pathToFileURL } from "node:url";
8
- import { captureEntrypoint, runCapturedSyntheticProbes, writeArtifacts } from "./advanced.js";
9
- import { createMockSdkPackage } from "./sdk-mock.js";
2
+ import { runEntrypointSyntheticProbes, writeArtifacts } from "./advanced.js";
10
3
 
11
4
  const args = process.argv.slice(2);
12
5
 
@@ -33,12 +26,10 @@ async function run(commandArgs) {
33
26
  throw new Error("synthetic probes import plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
34
27
  }
35
28
 
36
- const capture = await captureForSyntheticProbes(entrypoint, {
29
+ const results = await runEntrypointSyntheticProbes(entrypoint, {
37
30
  mockSdk,
38
31
  pluginRoot,
39
32
  apiOptions: { retainHandlers: true },
40
- });
41
- const results = await runCapturedSyntheticProbes(capture, {
42
33
  includeLifecycle,
43
34
  includeChannelRuntime,
44
35
  includeProviderCapabilities,
@@ -52,30 +43,6 @@ async function run(commandArgs) {
52
43
  }
53
44
  }
54
45
 
55
- async function captureForSyntheticProbes(entrypoint, options) {
56
- if (options.mockSdk !== true) {
57
- return captureEntrypoint(entrypoint, options);
58
- }
59
-
60
- const resolvedEntrypoint = path.resolve(process.cwd(), entrypoint);
61
- const pluginRoot = path.resolve(process.cwd(), options.pluginRoot ?? path.dirname(resolvedEntrypoint));
62
- const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-mock-sdk-"));
63
- cleanupTempDirOnExit(workspace);
64
- const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
65
- register(pathToFileURL(loaderPath));
66
- return captureEntrypoint(entrypoint, {
67
- ...options,
68
- mockSdk: false,
69
- pluginRoot,
70
- });
71
- }
72
-
73
- function cleanupTempDirOnExit(dir) {
74
- process.once("exit", () => {
75
- rmSync(dir, { force: true, recursive: true });
76
- });
77
- }
78
-
79
46
  function readFlag(commandArgs, name) {
80
47
  const index = commandArgs.indexOf(name);
81
48
  if (index === -1) {
@@ -89,6 +89,11 @@ export const syntheticRegistrationExecutionProfiles = {
89
89
  callableProperties: [],
90
90
  reason: "detached task runtimes are captured as registration metadata before async task execution",
91
91
  },
92
+ registerEmbeddingProvider: {
93
+ mode: "metadata-only",
94
+ callableProperties: [],
95
+ reason: "embedding providers are captured as registration metadata before provider runtime execution",
96
+ },
92
97
  registerGatewayDiscoveryService: {
93
98
  mode: "metadata-only",
94
99
  callableProperties: [],
@@ -116,11 +121,21 @@ export const syntheticRegistrationExecutionProfiles = {
116
121
  callableProperties: [],
117
122
  reason: "image generation providers are captured as registration metadata before provider runtime execution",
118
123
  },
124
+ registerHostedMediaResolver: {
125
+ mode: "metadata-only",
126
+ callableProperties: [],
127
+ reason: "hosted media resolvers are captured as registration metadata before media URL resolution",
128
+ },
119
129
  registerMemoryPromptSection: {
120
130
  mode: "metadata-only",
121
131
  callableProperties: [],
122
132
  reason: "memory prompt section renderers are captured as metadata before prompt-runtime execution",
123
133
  },
134
+ registerMeetingNotesSourceProvider: {
135
+ mode: "metadata-only",
136
+ callableProperties: [],
137
+ reason: "meeting notes source providers are captured as registration metadata before source discovery",
138
+ },
124
139
  registerMediaUnderstandingProvider: {
125
140
  mode: "metadata-only",
126
141
  callableProperties: [],
@@ -161,11 +176,21 @@ export const syntheticRegistrationExecutionProfiles = {
161
176
  callableProperties: [],
162
177
  reason: "migration providers are captured as registration metadata before migration runtime execution",
163
178
  },
179
+ registerModelCatalogProvider: {
180
+ mode: "metadata-only",
181
+ callableProperties: [],
182
+ reason: "model catalog providers are captured as registration metadata before catalog runtime execution",
183
+ },
164
184
  registerMusicGenerationProvider: {
165
185
  mode: "metadata-only",
166
186
  callableProperties: [],
167
187
  reason: "music generation providers are captured as registration metadata before provider runtime execution",
168
188
  },
189
+ registerNodeCliFeature: {
190
+ mode: "metadata-only",
191
+ callableProperties: [],
192
+ reason: "node CLI features are captured as registration metadata before host CLI integration",
193
+ },
169
194
  registerNodeHostCommand: {
170
195
  mode: "metadata-only",
171
196
  callableProperties: [],
@@ -206,6 +231,11 @@ export const syntheticRegistrationExecutionProfiles = {
206
231
  callableProperties: [],
207
232
  reason: "security audit collectors are captured as registration metadata before filesystem or policy scans",
208
233
  },
234
+ registerSessionAction: {
235
+ mode: "metadata-only",
236
+ callableProperties: [],
237
+ reason: "session actions are captured as registration metadata before session runtime execution",
238
+ },
209
239
  registerService: {
210
240
  mode: "lifecycle-opt-in",
211
241
  callableProperties: ["start", "stop", "dispose"],
@@ -267,12 +297,17 @@ export const defaultSyntheticHookEvents = {
267
297
  runId: "run-fixture",
268
298
  agentId: "agent-fixture",
269
299
  conversationId: "conversation-fixture",
300
+ success: true,
301
+ durationMs: 1,
302
+ error: null,
303
+ messages: [{ role: "assistant", content: "[redacted fixture output]" }],
270
304
  status: "completed",
271
305
  transcript: [{ role: "assistant", content: "[redacted fixture output]" }],
272
306
  },
273
307
  before_agent_start: {
274
308
  agentId: "agent-fixture",
275
309
  runId: "run-fixture",
310
+ prompt: "fixture prompt",
276
311
  config: { source: "plugin-inspector" },
277
312
  },
278
313
  before_prompt_build: {
@@ -303,6 +338,18 @@ export const defaultSyntheticHookEvents = {
303
338
  model: "gpt-5.4",
304
339
  output: { role: "assistant", content: "[redacted fixture output]" },
305
340
  },
341
+ message_received: {
342
+ from: "fixture-user",
343
+ content: "fixture inbound message",
344
+ messageId: "message-fixture",
345
+ },
346
+ message_sent: {
347
+ to: "fixture-user",
348
+ content: "fixture outbound message",
349
+ success: true,
350
+ error: null,
351
+ messageId: "message-fixture-reply",
352
+ },
306
353
  subagent_delivery_target: {
307
354
  childSessionKey: "child-session",
308
355
  agentId: "agent-child",
@@ -361,6 +408,18 @@ export const defaultSyntheticHookContexts = {
361
408
  agentId: "agent-fixture",
362
409
  sessionId: "session-fixture",
363
410
  },
411
+ message_received: {
412
+ runId: "run-fixture",
413
+ agentId: "agent-fixture",
414
+ sessionId: "session-fixture",
415
+ channelId: "fixture-channel",
416
+ },
417
+ message_sent: {
418
+ runId: "run-fixture",
419
+ agentId: "agent-fixture",
420
+ sessionId: "session-fixture",
421
+ channelId: "fixture-channel",
422
+ },
364
423
  subagent_delivery_target: {
365
424
  runId: "run-fixture",
366
425
  parentAgentId: "agent-parent",