@openclaw/plugin-inspector 0.3.17 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.3.18 - 2026-07-21
4
+
5
+ ### Highlights
6
+
7
+ - Keep compatibility capture current with OpenClaw's evolving hook, gateway, workspace-linking, blob-store, and Zod runtime surfaces.
8
+
9
+ ### Fixed
10
+
11
+ - Mock the current plugin blob-store runtime so Diffs and other trusted plugins can be captured without persistent state.
12
+ - Parse OpenClaw hook names after their source declaration became private and added a TypeScript `satisfies` constraint.
13
+ - Run synthetic gateway lifecycle hooks around ordinary probes while preserving capture indexes and report order, so `gateway_stop` teardown cannot invalidate later compatibility checks.
14
+ - Link isolated plugin workspaces to the OpenClaw checkout without npm normalizing duplicated dependency and peer declarations back to registry ranges.
15
+ - Keep mocked Zod schemas chainable through unsupported methods such as `pipe()` and `catch()`.
4
16
 
5
17
  ## 0.3.17 - 2026-06-29
6
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
@@ -194,6 +194,8 @@ function registrationReturnValue(name, args, context) {
194
194
 
195
195
  function createRuntimeContext(options) {
196
196
  const runtime = options.runtime ?? {};
197
+ const blobStores = new Map();
198
+ const syncKeyedStores = new Map();
197
199
  return {
198
200
  ...runtime,
199
201
  agent: runtime.agent ?? {},
@@ -203,11 +205,149 @@ function createRuntimeContext(options) {
203
205
  tts: runtime.tts ?? {},
204
206
  state: {
205
207
  resolveStateDir: () => options.stateDir ?? process.cwd(),
208
+ openBlobStore(storeOptions) {
209
+ const existing = blobStores.get(storeOptions.namespace);
210
+ if (existing) {
211
+ return existing;
212
+ }
213
+ const store = createBlobStoreContext(storeOptions);
214
+ blobStores.set(storeOptions.namespace, store);
215
+ return store;
216
+ },
217
+ openSyncKeyedStore({ namespace }) {
218
+ const existing = syncKeyedStores.get(namespace);
219
+ if (existing) {
220
+ return existing;
221
+ }
222
+ const store = createSyncKeyedStoreContext();
223
+ syncKeyedStores.set(namespace, store);
224
+ return store;
225
+ },
206
226
  ...(runtime.state ?? {}),
207
227
  },
208
228
  };
209
229
  }
210
230
 
231
+ function createBlobStoreContext(options) {
232
+ const values = new Map();
233
+ const entryInfo = (key, entry) => ({
234
+ key,
235
+ metadata: entry.metadata,
236
+ sizeBytes: entry.bytes.byteLength,
237
+ createdAt: entry.createdAt,
238
+ ...(entry.expiresAt === undefined ? {} : { expiresAt: entry.expiresAt }),
239
+ });
240
+ const read = (key) => {
241
+ const entry = values.get(key);
242
+ if (!entry) {
243
+ return undefined;
244
+ }
245
+ if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {
246
+ return undefined;
247
+ }
248
+ return { ...entryInfo(key, entry), bytes: Uint8Array.from(entry.bytes) };
249
+ };
250
+ const register = async (key, bytes, metadata, registerOptions) => {
251
+ const createdAt = Date.now();
252
+ const ttlMs = registerOptions?.ttlMs ?? options.defaultTtlMs;
253
+ values.set(key, {
254
+ bytes: Uint8Array.from(bytes),
255
+ metadata,
256
+ createdAt,
257
+ ...(ttlMs === undefined ? {} : { expiresAt: createdAt + ttlMs }),
258
+ });
259
+ };
260
+ return {
261
+ register,
262
+ async registerIfAbsent(key, bytes, metadata, registerOptions) {
263
+ if (values.has(key)) {
264
+ return false;
265
+ }
266
+ await register(key, bytes, metadata, registerOptions);
267
+ return true;
268
+ },
269
+ async lookup(key) {
270
+ return read(key);
271
+ },
272
+ async entries() {
273
+ return [...values.keys()].flatMap((key) => {
274
+ const entry = read(key);
275
+ if (!entry) {
276
+ return [];
277
+ }
278
+ const { bytes: _bytes, ...info } = entry;
279
+ return [info];
280
+ });
281
+ },
282
+ async delete(key) {
283
+ return values.delete(key);
284
+ },
285
+ async deleteExpiredKey(key) {
286
+ const entry = values.get(key);
287
+ if (!entry || entry.expiresAt === undefined || entry.expiresAt > Date.now()) {
288
+ return undefined;
289
+ }
290
+ values.delete(key);
291
+ return entryInfo(key, entry);
292
+ },
293
+ async deleteExpired() {
294
+ const expired = [];
295
+ for (const [key, entry] of values) {
296
+ if (entry.expiresAt === undefined || entry.expiresAt > Date.now()) {
297
+ continue;
298
+ }
299
+ values.delete(key);
300
+ expired.push(entryInfo(key, entry));
301
+ }
302
+ return expired;
303
+ },
304
+ async clear() {
305
+ values.clear();
306
+ },
307
+ };
308
+ }
309
+
310
+ function createSyncKeyedStoreContext() {
311
+ const values = new Map();
312
+ return {
313
+ register(key, value) {
314
+ values.set(key, value);
315
+ },
316
+ registerIfAbsent(key, value) {
317
+ if (values.has(key)) {
318
+ return false;
319
+ }
320
+ values.set(key, value);
321
+ return true;
322
+ },
323
+ update(key, updateValue) {
324
+ const next = updateValue(values.get(key));
325
+ if (next === undefined) {
326
+ return false;
327
+ }
328
+ values.set(key, next);
329
+ return true;
330
+ },
331
+ lookup(key) {
332
+ return values.get(key);
333
+ },
334
+ consume(key) {
335
+ const value = values.get(key);
336
+ values.delete(key);
337
+ return value;
338
+ },
339
+ delete(key) {
340
+ return values.delete(key);
341
+ },
342
+ entries() {
343
+ return [...values].map(([key, value]) => ({ key, value, createdAt: 0 }));
344
+ },
345
+ clear() {
346
+ values.clear();
347
+ },
348
+ };
349
+ }
350
+
211
351
  function createSecretContext(options) {
212
352
  const secrets = new Map(Object.entries(options.secretValues ?? {}));
213
353
  return {
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { writeJsonFileAtomic } from "./json-file.js";
6
+
7
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
8
+ const specifier = process.argv[2];
9
+ await linkOpenClawWorkspace(path.resolve(process.cwd(), "package.json"), specifier);
10
+ }
11
+
12
+ export async function linkOpenClawWorkspace(packageJsonPath, specifier) {
13
+ if (typeof specifier !== "string" || !specifier.startsWith("file:")) {
14
+ throw new TypeError("link-openclaw-workspace requires a file: dependency specifier");
15
+ }
16
+
17
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
18
+ let linked = false;
19
+ for (const section of ["dependencies", "optionalDependencies"]) {
20
+ if (Object.hasOwn(packageJson[section] ?? {}, "openclaw")) {
21
+ packageJson[section].openclaw = specifier;
22
+ linked = true;
23
+ }
24
+ }
25
+
26
+ if (!linked) {
27
+ packageJson.dependencies ??= {};
28
+ packageJson.dependencies.openclaw = specifier;
29
+ }
30
+
31
+ await writeJsonFileAtomic(packageJsonPath, packageJson);
32
+ }
@@ -37,7 +37,7 @@ export async function readOpenClawTargetSurface(options = {}) {
37
37
  const registrySource = await readFile(registryPath, "utf8");
38
38
  const compatRecordEntries = parseCompatRecordEntries(registrySource);
39
39
  const hookTypesSource = existsSync(hookTypesPath) ? await readFile(hookTypesPath, "utf8") : "";
40
- const hookNames = hookTypesSource ? parseExportedStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
40
+ const hookNames = hookTypesSource ? parseConstStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
41
41
  const apiBuilderSource = existsSync(apiBuilderPath) ? await readFile(apiBuilderPath, "utf8") : "";
42
42
  const apiRegistrars = apiBuilderSource ? parseApiRegistrars(apiBuilderSource) : [];
43
43
  const manifestTypesSource = existsSync(manifestTypesPath) ? await readFile(manifestTypesPath, "utf8") : "";
@@ -198,6 +198,15 @@ export function parsePluginSdkExports(packageJson) {
198
198
 
199
199
  export function parseExportedStringArray(source, exportName) {
200
200
  const match = source.match(new RegExp(`export\\s+const\\s+${exportName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s+as\\s+const`));
201
+ return parseStringArrayMatch(match);
202
+ }
203
+
204
+ function parseConstStringArray(source, constName) {
205
+ const match = source.match(new RegExp(`(?:export\\s+)?const\\s+${constName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s+as\\s+const`));
206
+ return parseStringArrayMatch(match);
207
+ }
208
+
209
+ function parseStringArrayMatch(match) {
201
210
  if (!match) {
202
211
  return [];
203
212
  }
package/src/sdk-mock.js CHANGED
@@ -968,11 +968,11 @@ function createSchema(defaultValue, shape) {
968
968
  },
969
969
  };
970
970
  return new Proxy(schema, {
971
- get(target, property) {
971
+ get(target, property, receiver) {
972
972
  if (property in target) {
973
973
  return target[property];
974
974
  }
975
- return () => target;
975
+ return () => receiver;
976
976
  },
977
977
  });
978
978
  }
@@ -236,6 +236,11 @@ export const syntheticRegistrationExecutionProfiles = {
236
236
  callableProperties: [],
237
237
  reason: "session actions are captured as registration metadata before session runtime execution",
238
238
  },
239
+ registerSessionCatalog: {
240
+ mode: "metadata-only",
241
+ callableProperties: [],
242
+ reason: "session catalogs are captured as registration metadata before catalog runtime execution",
243
+ },
239
244
  registerService: {
240
245
  mode: "lifecycle-opt-in",
241
246
  callableProperties: ["start", "stop", "dispose"],
@@ -591,23 +596,37 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
591
596
  const hookContexts = options.hookContexts ?? defaultSyntheticHookContexts;
592
597
  const captured = capture.captured ?? [];
593
598
  const retained = new Map((capture.retained ?? []).map((item) => [item.captureIndex, item]));
594
- const results = [];
599
+ const resultsByCaptureIndex = new Map();
600
+ const executionEntries = captured
601
+ .map((entry, captureIndex) => ({ entry, captureIndex }))
602
+ .sort(
603
+ (left, right) =>
604
+ syntheticLifecyclePhase(left.entry) - syntheticLifecyclePhase(right.entry) ||
605
+ left.captureIndex - right.captureIndex,
606
+ );
595
607
 
596
- for (let index = 0; index < captured.length; index += 1) {
597
- const entry = captured[index];
598
- const retainedEntry = retained.get(index);
608
+ for (const { entry, captureIndex } of executionEntries) {
609
+ const retainedEntry = retained.get(captureIndex);
599
610
  if (!retainedEntry) {
600
- results.push(blockedResult(entry, index, "handler retention was not enabled"));
611
+ resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, "handler retention was not enabled")]);
601
612
  continue;
602
613
  }
603
614
  if (entry.kind === "hook") {
604
- results.push(await runHookProbe(entry, retainedEntry, index, { hookEvents, hookContexts }));
615
+ resultsByCaptureIndex.set(captureIndex, [
616
+ await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }),
617
+ ]);
605
618
  continue;
606
619
  }
607
620
  if (entry.kind === "registration") {
608
- results.push(...(await runRegistrationProbes(entry, retainedEntry, index, options)));
621
+ resultsByCaptureIndex.set(
622
+ captureIndex,
623
+ await runRegistrationProbes(entry, retainedEntry, captureIndex, options),
624
+ );
609
625
  }
610
626
  }
627
+ // Execute in lifecycle order, but keep report rows and captureIndex values
628
+ // stable for downstream consumers that compare artifacts across runs.
629
+ const results = captured.flatMap((_, captureIndex) => resultsByCaptureIndex.get(captureIndex) ?? []);
611
630
 
612
631
  return {
613
632
  entrypoint: capture.entrypoint,
@@ -622,6 +641,19 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
622
641
  };
623
642
  }
624
643
 
644
+ function syntheticLifecyclePhase(entry) {
645
+ if (entry.kind !== "hook") {
646
+ return 1;
647
+ }
648
+ if (entry.name === "gateway_start") {
649
+ return 0;
650
+ }
651
+ if (entry.name === "gateway_stop") {
652
+ return 2;
653
+ }
654
+ return 1;
655
+ }
656
+
625
657
  export function renderSyntheticProbeMarkdown(plan, options = {}) {
626
658
  return [
627
659
  `# ${options.title ?? "Plugin Inspector Synthetic Probes"}`,
@@ -241,9 +241,10 @@ async function buildEntrypointPlan({ fixtureId, entrypoint, packageSummary, pack
241
241
  });
242
242
 
243
243
  if (requiredCapabilities.includes("target-openclaw-link")) {
244
+ const targetSpecifier = `file:${targetOpenClawWorkspacePath(settings, fixtureId, targetOpenClawPath)}`;
244
245
  steps.push({
245
246
  kind: "link-openclaw",
246
- command: `${shellQuote(packageManager)} pkg set ${shellQuote(`dependencies.openclaw=file:${targetOpenClawWorkspacePath(settings, fixtureId, targetOpenClawPath)}`)}`,
247
+ command: `node ${shellQuote(helperScript(settings, workspacePath, settings.linkOpenClawWorkspaceScript, "link-openclaw-workspace-cli.js"))} ${shellQuote(targetSpecifier)}`,
247
248
  cwd: workspacePath,
248
249
  reason: "link the plugin's openclaw peer dependency to the target checkout under test",
249
250
  });
@@ -325,6 +326,7 @@ function workspaceSettings(options) {
325
326
  return {
326
327
  captureScript: options.captureScript ?? defaultWorkspacePlanOptions.captureScript,
327
328
  defaultTargetOpenClawWorkspacePath: options.defaultTargetOpenClawWorkspacePath ?? "../../../openclaw",
329
+ linkOpenClawWorkspaceScript: options.linkOpenClawWorkspaceScript,
328
330
  optInEnv: options.optInEnv ?? defaultWorkspacePlanOptions.optInEnv,
329
331
  resultsRoot: repoRelative(options.resultsRoot ?? defaultWorkspacePlanOptions.resultsRoot),
330
332
  rootDir: path.resolve(options.rootDir ?? process.cwd()),