@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.
- package/CHANGELOG.md +31 -0
- package/README.md +141 -0
- package/package.json +4 -2
- package/src/api.js +7 -0
- package/src/artifacts.js +22 -1
- package/src/batch.js +7 -12
- package/src/capture-api.js +15 -0
- package/src/capture-cli.js +1 -1
- package/src/capture-config.js +10 -7
- package/src/cli.js +6 -2
- package/src/cold-import-readiness.js +3 -3
- package/src/fixture-summary.js +43 -6
- package/src/import-loop-profile.js +56 -11
- package/src/inspector.js +120 -99
- package/src/mock-sdk-capture-runner.js +61 -22
- package/src/openclaw-target.js +110 -5
- package/src/openclaw-version.js +121 -9
- package/src/process-profile.js +255 -107
- package/src/runtime-capture-report.js +6 -0
- package/src/runtime-imports.js +191 -0
- package/src/runtime-profile.js +4 -0
- package/src/runtime-reconciliation.js +0 -6
- package/src/sdk-mock.js +128 -13
- package/src/synthetic-entrypoint.js +19 -19
- package/src/synthetic-probes-cli.js +79 -2
- package/src/synthetic-probes.js +212 -34
package/src/openclaw-target.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/openclaw-version.js
CHANGED
|
@@ -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
|
|
29
|
-
version =
|
|
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
|
|
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 =
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
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) {
|