@openclaw/plugin-inspector 0.3.24 → 0.3.26

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.
@@ -30,17 +30,36 @@ export async function readOpenClawTargetSurface(options = {}) {
30
30
  return readPackedOpenClawTargetSurface({ rootDir, requestedPaths, ...match });
31
31
  }
32
32
 
33
- const { requestedPath, resolvedPath, registryPath } = match;
33
+ const { requestedPath, resolvedPath, registryPath: registryEntryPath } = match;
34
34
  const hookTypesPath = path.join(resolvedPath, "src/plugins/hook-types.ts");
35
35
  const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
36
36
  const capturedRegistrationPath = path.join(resolvedPath, "src/plugins/captured-registration.ts");
37
37
  const currentManifestTypesPath = path.join(resolvedPath, "src/plugins/manifest-types.ts");
38
38
  const legacyManifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
39
39
  const pluginSdkEntrypointsPath = path.join(resolvedPath, "src/plugin-sdk/entrypoints.ts");
40
+ const privateLocalSdkSubpathsPath = path.join(
41
+ resolvedPath,
42
+ "scripts/lib/plugin-sdk-private-local-only-subpaths.json",
43
+ );
40
44
  const packagePath = path.join(resolvedPath, "package.json");
41
45
 
42
- const registrySource = await readFile(registryPath, "utf8");
46
+ const registryEntrySource = await readFile(registryEntryPath, "utf8");
47
+ const registryPath = importsCompatRecords(registryEntrySource)
48
+ ? path.join(path.dirname(registryEntryPath), "registry-records.ts")
49
+ : registryEntryPath;
50
+ const registrySource = registryPath === registryEntryPath
51
+ ? registryEntrySource
52
+ : await readFile(registryPath, "utf8");
43
53
  const compatRecordEntries = parseCompatRecordEntries(registrySource);
54
+ const compatRecordTests = Object.fromEntries(
55
+ compatRecordEntries.map((record) => [record.code, record.tests]),
56
+ );
57
+ const compatRecordMissingTests = Object.fromEntries(
58
+ compatRecordEntries.map((record) => [
59
+ record.code,
60
+ record.tests.filter((testPath) => !existsSync(path.join(resolvedPath, testPath))),
61
+ ]),
62
+ );
44
63
  const hookTypesSource = existsSync(hookTypesPath) ? await readFile(hookTypesPath, "utf8") : "";
45
64
  const hookNames = hookTypesSource ? parseConstStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
46
65
  const apiBuilderSource = existsSync(apiBuilderPath) ? await readFile(apiBuilderPath, "utf8") : "";
@@ -66,12 +85,21 @@ export async function readOpenClawTargetSurface(options = {}) {
66
85
  const sdkExports = existsSync(packagePath)
67
86
  ? parsePluginSdkExports(JSON.parse(await readFile(packagePath, "utf8")))
68
87
  : [];
88
+ const privateLocalSdkExports = existsSync(privateLocalSdkSubpathsPath)
89
+ ? parsePluginSdkSubpathSpecifiers(
90
+ JSON.parse(await readFile(privateLocalSdkSubpathsPath, "utf8")),
91
+ )
92
+ : [];
69
93
  const pluginSdkEntrypointsSource = existsSync(pluginSdkEntrypointsPath)
70
94
  ? await readFile(pluginSdkEntrypointsPath, "utf8")
71
95
  : "";
72
96
  const reservedSdkExports = pluginSdkEntrypointsSource
73
97
  ? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "reservedBundledPluginSdkEntrypoints")
74
98
  : [];
99
+ const bundledPluginIds = await readBundledPluginIds(resolvedPath);
100
+ const reservedSdkExportOwners = Object.fromEntries(
101
+ reservedSdkExports.map((specifier) => [specifier, resolveBundledSdkOwner(specifier, bundledPluginIds)]),
102
+ );
75
103
  const supportedFacadeSdkExports = pluginSdkEntrypointsSource
76
104
  ? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "supportedBundledFacadeSdkEntrypoints")
77
105
  : [];
@@ -81,12 +109,15 @@ export async function readOpenClawTargetSurface(options = {}) {
81
109
 
82
110
  return {
83
111
  configuredPath: requestedPath,
112
+ checkoutPath: relativePath(rootDir, resolvedPath) || ".",
84
113
  searchedPaths: requestedPaths,
85
114
  status: "ok",
86
115
  compatRegistryPath: relativePath(rootDir, registryPath),
87
116
  compatRecordCount: compatRecordEntries.length,
88
117
  compatRecords: compatRecordEntries.map((record) => record.code).sort(),
89
118
  compatRecordStatuses: Object.fromEntries(compatRecordEntries.map((record) => [record.code, record.status])),
119
+ compatRecordTests,
120
+ compatRecordMissingTests,
90
121
  hookTypesPath: existsSync(hookTypesPath) ? relativePath(rootDir, hookTypesPath) : null,
91
122
  hookNameCount: hookNames.length,
92
123
  hookNames,
@@ -99,11 +130,14 @@ export async function readOpenClawTargetSurface(options = {}) {
99
130
  packagePath: existsSync(packagePath) ? relativePath(rootDir, packagePath) : null,
100
131
  sdkExportCount: sdkExports.length,
101
132
  sdkExports,
133
+ privateLocalSdkExportCount: privateLocalSdkExports.length,
134
+ privateLocalSdkExports,
102
135
  pluginSdkEntrypointsPath: existsSync(pluginSdkEntrypointsPath)
103
136
  ? relativePath(rootDir, pluginSdkEntrypointsPath)
104
137
  : null,
105
138
  reservedSdkExportCount: reservedSdkExports.length,
106
139
  reservedSdkExports,
140
+ reservedSdkExportOwners,
107
141
  supportedFacadeSdkExports,
108
142
  publicPluginOwnedSdkExports,
109
143
  manifestTypesPath: existsSync(manifestTypesPath) ? relativePath(rootDir, manifestTypesPath) : null,
@@ -121,6 +155,23 @@ export function openClawTargetPathCandidates(manifest, configuredPath) {
121
155
  return unique([manifest?.openclaw?.defaultCheckoutPath, ...defaultOpenClawCheckoutPaths].filter(Boolean));
122
156
  }
123
157
 
158
+ function importsCompatRecords(source) {
159
+ // Keep quoted text atomic and discard comments before recognizing the fixed delegation.
160
+ const tokens = (source.match(/\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$)|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`|[$A-Z_a-z][$\w]*|[^\s]/g) ?? [])
161
+ .filter((token) => !token.startsWith("//") && !token.startsWith("/*"));
162
+ for (let index = 0; index < tokens.length; index += 1) {
163
+ if (tokens[index] !== "import" || tokens[index + 1] !== "{") continue;
164
+ const end = tokens.indexOf("}", index + 2);
165
+ if (end === -1 || tokens[end + 1] !== "from") continue;
166
+ if (!['"./registry-records.js"', "'./registry-records.js'"].includes(tokens[end + 2])) continue;
167
+ const bindings = tokens.slice(index + 2, end).join(" ").split(",");
168
+ if (bindings.some((binding) => /^PLUGIN_COMPAT_RECORDS(?:\s+as\s+[$A-Z_a-z][$\w]*)?$/.test(binding.trim()))) {
169
+ return true;
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+
124
175
  export function parseCompatRecordEntries(source) {
125
176
  const entries = [];
126
177
  let cursor = 0;
@@ -132,8 +183,14 @@ export function parseCompatRecordEntries(source) {
132
183
 
133
184
  const statusProperty = readStringProperty(source, "status", codeProperty.end);
134
185
  if (statusProperty) {
135
- entries.push({ code: codeProperty.value, status: statusProperty.value });
136
- cursor = statusProperty.end;
186
+ const nextCodeProperty = readStringProperty(source, "code", statusProperty.end);
187
+ const recordEnd = nextCodeProperty?.propertyIndex ?? source.length;
188
+ entries.push({
189
+ code: codeProperty.value,
190
+ status: statusProperty.value,
191
+ tests: readStringArrayProperty(source, "tests", statusProperty.end, recordEnd),
192
+ });
193
+ cursor = recordEnd;
137
194
  } else {
138
195
  cursor = codeProperty.end;
139
196
  }
@@ -157,7 +214,21 @@ function readStringProperty(source, property, fromIndex) {
157
214
  if (!isQuote(source[quoteIndex])) {
158
215
  return null;
159
216
  }
160
- return readQuotedValue(source, quoteIndex);
217
+ const value = readQuotedValue(source, quoteIndex);
218
+ return value ? { ...value, propertyIndex } : null;
219
+ }
220
+
221
+ function readStringArrayProperty(source, property, fromIndex, toIndex) {
222
+ const propertyIndex = findProperty(source, property, fromIndex);
223
+ if (propertyIndex === -1 || propertyIndex >= toIndex) return [];
224
+ const colonIndex = source.indexOf(":", propertyIndex + property.length);
225
+ const openIndex = source.indexOf("[", colonIndex + 1);
226
+ if (colonIndex === -1 || openIndex === -1 || openIndex >= toIndex) return [];
227
+ const closeIndex = source.indexOf("]", openIndex + 1);
228
+ if (closeIndex === -1 || closeIndex >= toIndex) return [];
229
+ return unique(
230
+ [...source.slice(openIndex + 1, closeIndex).matchAll(/["']([^"']+)["']/g)].map((match) => match[1]),
231
+ ).sort();
161
232
  }
162
233
 
163
234
  function findProperty(source, property, fromIndex) {
@@ -298,6 +369,7 @@ async function readPackedOpenClawTargetSurface({ rootDir, requestedPaths, reques
298
369
 
299
370
  return {
300
371
  configuredPath: requestedPath,
372
+ checkoutPath: relativePath(rootDir, resolvedPath) || ".",
301
373
  searchedPaths: requestedPaths,
302
374
  status: "ok",
303
375
  version: packageJson.version ?? null,
@@ -305,6 +377,8 @@ async function readPackedOpenClawTargetSurface({ rootDir, requestedPaths, reques
305
377
  compatRecordCount: 0,
306
378
  compatRecords: [],
307
379
  compatRecordStatuses: {},
380
+ compatRecordTests: {},
381
+ compatRecordMissingTests: {},
308
382
  hookTypesPath: hookDeclaration ? relativePath(rootDir, hookDeclaration.filePath) : null,
309
383
  hookNameCount: hookNames.length,
310
384
  hookNames,
@@ -317,9 +391,12 @@ async function readPackedOpenClawTargetSurface({ rootDir, requestedPaths, reques
317
391
  packagePath: relativePath(rootDir, packagePath),
318
392
  sdkExportCount: sdkExports.length,
319
393
  sdkExports,
394
+ privateLocalSdkExportCount: 0,
395
+ privateLocalSdkExports: [],
320
396
  pluginSdkEntrypointsPath: null,
321
397
  reservedSdkExportCount: 0,
322
398
  reservedSdkExports: [],
399
+ reservedSdkExportOwners: {},
323
400
  supportedFacadeSdkExports: [],
324
401
  publicPluginOwnedSdkExports: [],
325
402
  manifestTypesPath: manifestDeclaration ? relativePath(rootDir, manifestDeclaration.filePath) : null,
@@ -443,15 +520,20 @@ function parseStringUnion(source, typeName) {
443
520
  function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status }) {
444
521
  return {
445
522
  configuredPath,
523
+ checkoutPath: null,
446
524
  searchedPaths,
447
525
  status,
448
526
  compatRecords: [],
449
527
  compatRecordStatuses: {},
528
+ compatRecordTests: {},
529
+ compatRecordMissingTests: {},
450
530
  hookNames: [],
451
531
  apiRegistrars: [],
452
532
  capturedRegistrars: [],
453
533
  sdkExports: [],
534
+ privateLocalSdkExports: [],
454
535
  reservedSdkExports: [],
536
+ reservedSdkExportOwners: {},
455
537
  supportedFacadeSdkExports: [],
456
538
  publicPluginOwnedSdkExports: [],
457
539
  manifestFields: [],
@@ -459,10 +541,33 @@ function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status
459
541
  };
460
542
  }
461
543
 
544
+ async function readBundledPluginIds(openClawRoot) {
545
+ const extensionsRoot = path.join(openClawRoot, "extensions");
546
+ if (!existsSync(extensionsRoot)) return [];
547
+ return (await readdir(extensionsRoot, { withFileTypes: true }))
548
+ .filter((entry) => entry.isDirectory())
549
+ .map((entry) => entry.name)
550
+ .sort((left, right) => right.length - left.length || left.localeCompare(right));
551
+ }
552
+
553
+ function resolveBundledSdkOwner(specifier, pluginIds) {
554
+ const entrypoint = specifier.slice("openclaw/plugin-sdk/".length);
555
+ return pluginIds.find((pluginId) => entrypoint === pluginId || entrypoint.startsWith(`${pluginId}-`)) ?? null;
556
+ }
557
+
462
558
  export function parsePluginSdkEntrypointSpecifiers(source, exportName) {
463
559
  return parseExportedStringArray(source, exportName).map((entrypoint) => `openclaw/plugin-sdk/${entrypoint}`).sort();
464
560
  }
465
561
 
562
+ function parsePluginSdkSubpathSpecifiers(value) {
563
+ if (!Array.isArray(value)) return [];
564
+ return unique(
565
+ value
566
+ .filter((entrypoint) => typeof entrypoint === "string" && !entrypoint.includes("/"))
567
+ .map((entrypoint) => `openclaw/plugin-sdk/${entrypoint}`),
568
+ ).sort();
569
+ }
570
+
466
571
  function parseCapturedRegistrars(source) {
467
572
  return unique([...source.matchAll(/^\s*(register[A-Za-z0-9]+)\s*\(/gm)].map((match) => match[1])).sort();
468
573
  }
@@ -8,6 +8,9 @@ import { x as extractTar } from "tar";
8
8
  import { readOpenClawTargetSurface } from "./openclaw-target.js";
9
9
 
10
10
  const defaultRegistryUrl = "https://registry.npmjs.org";
11
+ const defaultFetchTimeoutMs = 30_000;
12
+ const defaultMaxArchiveBytes = 256 * 1024 * 1024;
13
+ const defaultMaxMetadataBytes = 16 * 1024 * 1024;
11
14
  const supportedTags = new Set(["latest", "beta"]);
12
15
  const downloadUrls = new WeakMap();
13
16
 
@@ -25,8 +28,8 @@ export async function resolveOpenClawTargetVersion(requestedVersion, options = {
25
28
  let distTag = null;
26
29
 
27
30
  if (supportedTags.has(requested)) {
28
- const metadata = await fetchJson(`${registryUrl}/openclaw`, fetchImpl);
29
- version = metadata["dist-tags"]?.[requested];
31
+ const distTags = await fetchJson(`${registryUrl}/-/package/openclaw/dist-tags`, fetchImpl, options);
32
+ version = distTags?.[requested];
30
33
  if (typeof version !== "string" || version.length === 0) {
31
34
  throw new Error(`OpenClaw npm dist-tag ${requested} did not resolve to an exact version`);
32
35
  }
@@ -38,7 +41,7 @@ export async function resolveOpenClawTargetVersion(requestedVersion, options = {
38
41
  throw new Error("--openclaw-version must be latest, beta, or an exact OpenClaw version");
39
42
  }
40
43
 
41
- const versionMetadata = await fetchJson(`${registryUrl}/openclaw/${encodeURIComponent(version)}`, fetchImpl);
44
+ const versionMetadata = await fetchJson(`${registryUrl}/openclaw/${encodeURIComponent(version)}`, fetchImpl, options);
42
45
  if (versionMetadata.version !== version || typeof versionMetadata.dist?.tarball !== "string") {
43
46
  throw new Error(`OpenClaw npm metadata for ${version} is incomplete`);
44
47
  }
@@ -137,11 +140,12 @@ export function satisfiesOpenClawCompatibilityRange({ targetVersion, eligibility
137
140
 
138
141
  async function preparePackageArchive(resolvedTarget, options) {
139
142
  const fetchImpl = options.fetch ?? globalThis.fetch;
140
- const response = await fetchImpl(downloadUrlFor(resolvedTarget));
143
+ const response = await fetchWithTimeout(fetchImpl, downloadUrlFor(resolvedTarget), {}, options, "npm archive");
141
144
  if (!response.ok) {
145
+ await cancelBody(response.body);
142
146
  throw new Error(`failed to download OpenClaw ${resolvedTarget.version}: HTTP ${response.status}`);
143
147
  }
144
- const archive = Buffer.from(await response.arrayBuffer());
148
+ const archive = await readLimitedBody(response, maxArchiveBytes(options), "npm archive");
145
149
  verifyArchive(archive, resolvedTarget.source);
146
150
 
147
151
  await mkdir(path.dirname(options.targetDir), { recursive: true });
@@ -191,10 +195,118 @@ function verifyArchive(archive, source) {
191
195
  throw new Error("OpenClaw npm archive has no supported integrity metadata");
192
196
  }
193
197
 
194
- async function fetchJson(url, fetchImpl) {
195
- const response = await fetchImpl(url, { headers: { accept: "application/json" } });
196
- if (!response.ok) throw new Error(`failed to resolve OpenClaw npm metadata: HTTP ${response.status}`);
197
- return response.json();
198
+ async function fetchJson(url, fetchImpl, options = {}) {
199
+ const response = await fetchWithTimeout(
200
+ fetchImpl,
201
+ url,
202
+ { headers: { accept: "application/json" } },
203
+ options,
204
+ "npm metadata",
205
+ );
206
+ if (!response.ok) {
207
+ await cancelBody(response.body);
208
+ throw new Error(`failed to resolve OpenClaw npm metadata: HTTP ${response.status}`);
209
+ }
210
+ const body = await readLimitedBody(response, maxMetadataBytes(options), "npm metadata");
211
+ return JSON.parse(body.toString("utf8"));
212
+ }
213
+
214
+ async function fetchWithTimeout(fetchImpl, url, init, options, what) {
215
+ try {
216
+ return await fetchImpl(url, { ...init, signal: AbortSignal.timeout(fetchTimeoutMs(options)) });
217
+ } catch (error) {
218
+ throw mapTargetFetchError(error, what);
219
+ }
220
+ }
221
+
222
+ async function readLimitedBody(response, maxBytes, what) {
223
+ const declared = Number(response.headers.get("content-length"));
224
+ if (Number.isFinite(declared) && declared > maxBytes) {
225
+ await cancelBody(response.body);
226
+ throw targetDownloadLimitError(what, maxBytes);
227
+ }
228
+
229
+ let reader;
230
+ try {
231
+ if (!response.body || typeof response.body.getReader !== "function") {
232
+ const buffer = Buffer.from(await response.arrayBuffer());
233
+ if (buffer.length > maxBytes) throw targetDownloadLimitError(what, maxBytes);
234
+ return buffer;
235
+ }
236
+
237
+ reader = response.body.getReader();
238
+ const chunks = [];
239
+ let received = 0;
240
+ while (true) {
241
+ const { done, value } = await reader.read();
242
+ if (done) break;
243
+ received += value.byteLength;
244
+ if (received > maxBytes) {
245
+ try {
246
+ await reader.cancel();
247
+ } catch {}
248
+ throw targetDownloadLimitError(what, maxBytes);
249
+ }
250
+ chunks.push(value);
251
+ }
252
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
253
+ } catch (error) {
254
+ throw mapTargetFetchError(error, what);
255
+ } finally {
256
+ reader?.releaseLock();
257
+ }
258
+ }
259
+
260
+ async function cancelBody(body) {
261
+ try {
262
+ await body?.cancel?.();
263
+ } catch {}
264
+ }
265
+
266
+ function fetchTimeoutMs(options) {
267
+ const timeout = positiveInteger(
268
+ options.fetchTimeoutMs ?? process.env.PLUGIN_INSPECTOR_TARGET_FETCH_TIMEOUT_MS,
269
+ defaultFetchTimeoutMs,
270
+ );
271
+ // Node clamps overflowing timer delays to 1ms instead of honoring the budget.
272
+ return timeout <= 2_147_483_647 ? timeout : defaultFetchTimeoutMs;
273
+ }
274
+
275
+ function maxArchiveBytes(options) {
276
+ return positiveInteger(options.maxArchiveBytes ?? process.env.PLUGIN_INSPECTOR_TARGET_ARCHIVE_MAX_BYTES, defaultMaxArchiveBytes);
277
+ }
278
+
279
+ function maxMetadataBytes(options) {
280
+ return positiveInteger(options.maxMetadataBytes ?? process.env.PLUGIN_INSPECTOR_TARGET_METADATA_MAX_BYTES, defaultMaxMetadataBytes);
281
+ }
282
+
283
+ function positiveInteger(value, fallback) {
284
+ const parsed = Number(value);
285
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
286
+ }
287
+
288
+ function mapTargetFetchError(error, what) {
289
+ if (error?.failureClass) return error;
290
+ if (isTimeoutError(error)) {
291
+ const wrapped = new Error(`OpenClaw ${what} download timed out`);
292
+ wrapped.failureClass = "target-download-timeout";
293
+ wrapped.cause = error;
294
+ return wrapped;
295
+ }
296
+ return error;
297
+ }
298
+
299
+ function isTimeoutError(error) {
300
+ for (let current = error; current; current = current.cause) {
301
+ if (current.name === "TimeoutError" || current.name === "AbortError") return true;
302
+ }
303
+ return false;
304
+ }
305
+
306
+ function targetDownloadLimitError(what, maxBytes) {
307
+ const error = new Error(`OpenClaw ${what} exceeds the ${maxBytes} byte download limit`);
308
+ error.failureClass = "target-download-too-large";
309
+ return error;
198
310
  }
199
311
 
200
312
  function cacheKeyFor(target) {