@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 +13 -1
- package/package.json +1 -1
- package/src/capture-api.js +140 -0
- package/src/link-openclaw-workspace-cli.js +32 -0
- package/src/openclaw-target.js +10 -1
- package/src/sdk-mock.js +2 -2
- package/src/synthetic-probes.js +39 -7
- package/src/workspace-plan.js +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
package/src/capture-api.js
CHANGED
|
@@ -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
|
+
}
|
package/src/openclaw-target.js
CHANGED
|
@@ -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 ?
|
|
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 () =>
|
|
975
|
+
return () => receiver;
|
|
976
976
|
},
|
|
977
977
|
});
|
|
978
978
|
}
|
package/src/synthetic-probes.js
CHANGED
|
@@ -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
|
|
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 (
|
|
597
|
-
const
|
|
598
|
-
const retainedEntry = retained.get(index);
|
|
608
|
+
for (const { entry, captureIndex } of executionEntries) {
|
|
609
|
+
const retainedEntry = retained.get(captureIndex);
|
|
599
610
|
if (!retainedEntry) {
|
|
600
|
-
|
|
611
|
+
resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, "handler retention was not enabled")]);
|
|
601
612
|
continue;
|
|
602
613
|
}
|
|
603
614
|
if (entry.kind === "hook") {
|
|
604
|
-
|
|
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
|
-
|
|
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"}`,
|
package/src/workspace-plan.js
CHANGED
|
@@ -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:
|
|
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()),
|