@openclaw/plugin-inspector 0.3.7 → 0.3.9

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,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.9 - 2026-05-03
6
+
7
+ ### Fixed
8
+
9
+ - Follow bundled channel `loadBundledEntryExportSync` registration exports during mocked runtime capture.
10
+
11
+ ## 0.3.8 - 2026-05-03
12
+
13
+ ### Fixed
14
+
15
+ - Synthesize manifest config for isolated runtime capture so configured hooks can be observed without credentials.
16
+
5
17
  ## 0.3.7 - 2026-05-03
6
18
 
7
19
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
@@ -148,6 +148,7 @@ export function createCaptureContext(options = {}) {
148
148
  config: options.config ?? {},
149
149
  logger: options.logger ?? console,
150
150
  pluginConfig: options.pluginConfig ?? {},
151
+ resolvePath: options.resolvePath ?? ((value) => value),
151
152
  runtime: options.runtime ?? createRuntimeContext(options),
152
153
  secrets: options.secrets ?? createSecretContext(options),
153
154
  store: options.store ?? createStoreContext(options),
@@ -0,0 +1,126 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function captureApiOptionsForPlugin(apiOptions = {}, options = {}) {
5
+ if (apiOptions.pluginConfig !== undefined || !options.pluginRoot) {
6
+ return apiOptions;
7
+ }
8
+
9
+ const pluginConfig = await readSamplePluginConfig(options.pluginRoot);
10
+ if (pluginConfig === undefined) {
11
+ return apiOptions;
12
+ }
13
+ return {
14
+ ...apiOptions,
15
+ pluginConfig,
16
+ };
17
+ }
18
+
19
+ async function readSamplePluginConfig(pluginRoot) {
20
+ const manifestPath = path.join(pluginRoot, "openclaw.plugin.json");
21
+ let manifest;
22
+ try {
23
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
24
+ } catch {
25
+ return undefined;
26
+ }
27
+
28
+ const sample = sampleJsonSchema(manifest.configSchema, { key: "config" });
29
+ return isPlainObject(sample) && Object.keys(sample).length > 0 ? sample : undefined;
30
+ }
31
+
32
+ function sampleJsonSchema(schema, context = {}) {
33
+ if (!isPlainObject(schema)) {
34
+ return undefined;
35
+ }
36
+
37
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
38
+ return schema.enum[0];
39
+ }
40
+ if (Object.prototype.hasOwnProperty.call(schema, "const")) {
41
+ return schema.const;
42
+ }
43
+ if (Object.prototype.hasOwnProperty.call(schema, "default")) {
44
+ return schema.default;
45
+ }
46
+
47
+ const type = Array.isArray(schema.type) ? schema.type.find((item) => item !== "null") : schema.type;
48
+ if (type === "object" || schema.properties) {
49
+ return sampleObjectSchema(schema);
50
+ }
51
+ if (type === "array") {
52
+ return [];
53
+ }
54
+ if (type === "boolean") {
55
+ return false;
56
+ }
57
+ if (type === "number" || type === "integer") {
58
+ return typeof schema.minimum === "number" ? schema.minimum : 1;
59
+ }
60
+ if (type === "string" || !type) {
61
+ return sampleString(context.key);
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ function sampleObjectSchema(schema) {
67
+ const properties = isPlainObject(schema.properties) ? schema.properties : {};
68
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
69
+ const output = {};
70
+
71
+ for (const key of Object.keys(properties)) {
72
+ if (required.has(key)) {
73
+ const value = sampleJsonSchema(properties[key], { key });
74
+ if (value !== undefined) {
75
+ output[key] = value;
76
+ }
77
+ }
78
+ }
79
+
80
+ for (const key of Object.keys(properties)) {
81
+ if (properties[key]?.type === "boolean") {
82
+ output[key] = false;
83
+ }
84
+ }
85
+
86
+ if (Object.keys(output).length === 0 && Number(schema.minProperties ?? 0) > 0) {
87
+ const key = preferredSamplePropertyKey(properties);
88
+ if (key) {
89
+ const value = sampleJsonSchema(properties[key], { key });
90
+ if (value !== undefined) {
91
+ output[key] = value;
92
+ }
93
+ }
94
+ }
95
+
96
+ return output;
97
+ }
98
+
99
+ function preferredSamplePropertyKey(properties) {
100
+ for (const key of ["provider", "model", "apiKey", "id", "name", ...Object.keys(properties)]) {
101
+ if (Object.prototype.hasOwnProperty.call(properties, key)) {
102
+ return key;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ function sampleString(key = "") {
109
+ if (key === "provider") {
110
+ return "openai";
111
+ }
112
+ if (key === "model") {
113
+ return "text-embedding-3-small";
114
+ }
115
+ if (key === "apiKey") {
116
+ return "fixture-api-key";
117
+ }
118
+ if (key === "dbPath") {
119
+ return ".plugin-inspector/state/lancedb";
120
+ }
121
+ return "fixture";
122
+ }
123
+
124
+ function isPlainObject(value) {
125
+ return value !== null && typeof value === "object" && !Array.isArray(value);
126
+ }
package/src/inspector.js CHANGED
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { promisify } from "node:util";
7
7
  import { createCaptureApi } from "./capture-api.js";
8
+ import { captureApiOptionsForPlugin } from "./capture-config.js";
8
9
  import { fixtureCheckoutPath, fixtureSourceRoot } from "./config.js";
9
10
  import { buildCompatibilityFixtureReport } from "./fixture-summary.js";
10
11
  import { readOpenClawTargetSurface } from "./openclaw-target.js";
@@ -188,7 +189,12 @@ export async function captureEntrypoint(entrypoint, options = {}) {
188
189
  };
189
190
  }
190
191
 
191
- const api = createCaptureApi(options.apiOptions);
192
+ const apiOptions = await captureApiOptionsForPlugin(options.apiOptions, {
193
+ pluginRoot: options.pluginRoot
194
+ ? path.resolve(options.cwd ?? process.cwd(), options.pluginRoot)
195
+ : path.dirname(resolvedEntrypoint),
196
+ });
197
+ const api = createCaptureApi(apiOptions);
192
198
  try {
193
199
  await register(api);
194
200
  } catch (error) {
@@ -199,7 +205,7 @@ export async function captureEntrypoint(entrypoint, options = {}) {
199
205
  entrypoint: resolvedEntrypoint,
200
206
  captured: api.getCapturedContracts(),
201
207
  };
202
- if (options.apiOptions?.retainHandlers === true) {
208
+ if (apiOptions?.retainHandlers === true) {
203
209
  result.retained = api.getRetainedContracts();
204
210
  }
205
211
  return result;
@@ -6,6 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { createCaptureApi } from "./capture-api.js";
9
+ import { captureApiOptionsForPlugin } from "./capture-config.js";
9
10
  import { createMockSdkPackage } from "./sdk-mock.js";
10
11
 
11
12
  const options = JSON.parse(process.argv[2] ?? "{}");
@@ -30,7 +31,7 @@ async function run(options) {
30
31
  cleanupTempDirOnExit(workspace);
31
32
  const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
32
33
  register(pathToFileURL(loaderPath));
33
- return await captureLinkedEntrypoint(entrypoint, options);
34
+ return await captureLinkedEntrypoint(entrypoint, { ...options, pluginRoot });
34
35
  }
35
36
 
36
37
  function cleanupTempDirOnExit(dir) {
@@ -65,7 +66,10 @@ async function captureLinkedEntrypoint(entrypoint, options) {
65
66
  );
66
67
  }
67
68
 
68
- const api = createCaptureApi(options.apiOptions);
69
+ const apiOptions = await captureApiOptionsForPlugin(options.apiOptions, {
70
+ pluginRoot: options.pluginRoot,
71
+ });
72
+ const api = createCaptureApi(apiOptions);
69
73
  try {
70
74
  await register(api);
71
75
  } catch (error) {
@@ -80,7 +84,7 @@ async function captureLinkedEntrypoint(entrypoint, options) {
80
84
  mockSdk: true,
81
85
  captured: api.getCapturedContracts(),
82
86
  };
83
- if (options.apiOptions?.retainHandlers === true) {
87
+ if (apiOptions?.retainHandlers === true) {
84
88
  result.retained = api.getRetainedContracts();
85
89
  }
86
90
  return withProcessOutput(result, outputCapture);
package/src/sdk-mock.js CHANGED
@@ -551,6 +551,9 @@ function genericExportStatement(name) {
551
551
  if (name === "defineBundledChannelSetupEntry") {
552
552
  return "export { defineBundledChannelSetupEntry };";
553
553
  }
554
+ if (name === "loadBundledEntryExportSync") {
555
+ return "export { loadBundledEntryExportSync };";
556
+ }
554
557
  if (/^[A-Z].*Schema$/u.test(name)) {
555
558
  return `export const ${name} = createSchema();`;
556
559
  }
@@ -558,7 +561,13 @@ function genericExportStatement(name) {
558
561
  }
559
562
 
560
563
  function genericMockRuntimeSource(options = {}) {
561
- return `${options.includeSdkRuntime ? `function definePluginEntry(entry) {
564
+ return `${options.includeSdkRuntime ? `import { existsSync } from "node:fs";
565
+ import path from "node:path";
566
+ import { fileURLToPath, pathToFileURL } from "node:url";
567
+
568
+ const pendingBundledEntryLoads = new Set();
569
+
570
+ function definePluginEntry(entry) {
562
571
  if (entry && typeof entry === "object" && typeof entry.register === "function") {
563
572
  return entry;
564
573
  }
@@ -572,7 +581,7 @@ function defineBundledChannelEntry(entry = {}) {
572
581
  return {
573
582
  ...entry,
574
583
  kind: "bundled-channel-entry",
575
- register(api) {
584
+ async register(api) {
576
585
  if (api?.registrationMode === "cli-metadata") {
577
586
  return entry.registerCliMetadata?.(api);
578
587
  }
@@ -585,7 +594,12 @@ function defineBundledChannelEntry(entry = {}) {
585
594
  });
586
595
  }
587
596
  entry.registerCliMetadata?.(api);
588
- return entry.registerFull?.(api);
597
+ const result = entry.registerFull?.(api);
598
+ if (result && typeof result.then === "function") {
599
+ await result;
600
+ }
601
+ await drainBundledEntryLoads();
602
+ return result;
589
603
  },
590
604
  };
591
605
  }
@@ -596,6 +610,46 @@ function defineBundledChannelSetupEntry(entry = {}) {
596
610
  kind: "bundled-channel-setup-entry",
597
611
  };
598
612
  }
613
+
614
+ function loadBundledEntryExportSync(importMetaUrl, options = {}) {
615
+ return (...args) => {
616
+ const promise = import(resolveBundledEntryUrl(importMetaUrl, options.specifier)).then((module) => {
617
+ const loaded = module[options.exportName] ?? module.default;
618
+ return typeof loaded === "function" ? loaded(...args) : loaded;
619
+ });
620
+ pendingBundledEntryLoads.add(promise);
621
+ promise.finally(() => pendingBundledEntryLoads.delete(promise));
622
+ return promise;
623
+ };
624
+ }
625
+
626
+ async function drainBundledEntryLoads() {
627
+ while (pendingBundledEntryLoads.size > 0) {
628
+ await Promise.all([...pendingBundledEntryLoads]);
629
+ }
630
+ }
631
+
632
+ function resolveBundledEntryUrl(importMetaUrl, specifier) {
633
+ const basePath = fileURLToPath(importMetaUrl);
634
+ const target = specifier ? path.resolve(path.dirname(basePath), specifier) : basePath;
635
+ const resolved = resolveExistingSourcePath(target);
636
+ return pathToFileURL(resolved).href;
637
+ }
638
+
639
+ function resolveExistingSourcePath(target) {
640
+ if (existsSync(target)) {
641
+ return target;
642
+ }
643
+ const parsed = path.parse(target);
644
+ const withoutJsExtension = [".js", ".mjs", ".cjs"].includes(parsed.ext) ? path.join(parsed.dir, parsed.name) : null;
645
+ const candidates = [
646
+ ...(withoutJsExtension ? [\`\${withoutJsExtension}.ts\`, \`\${withoutJsExtension}.mts\`, \`\${withoutJsExtension}.cts\`] : []),
647
+ \`\${target}.js\`,
648
+ \`\${target}.mjs\`,
649
+ \`\${target}.ts\`,
650
+ ];
651
+ return candidates.find((candidate) => existsSync(candidate)) ?? target;
652
+ }
599
653
  ` : ""}
600
654
  function createMockValue(name) {
601
655
  function fn(...args) {
@@ -1122,16 +1176,36 @@ export function normalizeSecretInputString(value) {
1122
1176
  return String(value ?? "").trim();
1123
1177
  }
1124
1178
 
1179
+ function createSimpleSchema(defaultValue) {
1180
+ return {
1181
+ parse(value) {
1182
+ return value === undefined ? defaultValue : value;
1183
+ },
1184
+ default(value) {
1185
+ return createSimpleSchema(value);
1186
+ },
1187
+ optional() {
1188
+ return this;
1189
+ },
1190
+ nullable() {
1191
+ return this;
1192
+ },
1193
+ nullish() {
1194
+ return this;
1195
+ },
1196
+ };
1197
+ }
1198
+
1125
1199
  export function buildSecretInputSchema() {
1126
- return { type: "string" };
1200
+ return createSimpleSchema();
1127
1201
  }
1128
1202
 
1129
1203
  export function buildOptionalSecretInputSchema() {
1130
- return { anyOf: [buildSecretInputSchema(), { type: "undefined" }] };
1204
+ return createSimpleSchema();
1131
1205
  }
1132
1206
 
1133
1207
  export function buildSecretInputArraySchema() {
1134
- return { type: "array", items: buildSecretInputSchema() };
1208
+ return createSimpleSchema([]);
1135
1209
  }
1136
1210
 
1137
1211
  export function registerPluginHttpRoute(options = {}) {