@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/process-profile.js
CHANGED
|
@@ -1,6 +1,147 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
3
|
|
|
4
|
+
const defaultTimeoutMs = 30_000;
|
|
5
|
+
const defaultKillGraceMs = 1_000;
|
|
6
|
+
const maxTimerMs = 2 ** 31 - 1;
|
|
7
|
+
const killWaitMs = 1_000;
|
|
8
|
+
|
|
9
|
+
// Shared by capture and profiling, not a package entrypoint. Each spawn owns
|
|
10
|
+
// its POSIX process group; never signal the inspector's inherited group.
|
|
11
|
+
export function startOwnedProcess(options, kind = "PROFILE") {
|
|
12
|
+
const env = options.env ?? process.env;
|
|
13
|
+
const { timeoutMs, killGraceMs, maxOutputBytes } = resolveProcessLimits(options, kind);
|
|
14
|
+
const stdout = createCappedCollector(maxOutputBytes);
|
|
15
|
+
const stderr = createCappedCollector(maxOutputBytes);
|
|
16
|
+
let timedOut = false;
|
|
17
|
+
let cancelled = options.signal?.aborted === true;
|
|
18
|
+
let error;
|
|
19
|
+
let closed = false;
|
|
20
|
+
let stopping = false;
|
|
21
|
+
let escalated = false;
|
|
22
|
+
let settled = false;
|
|
23
|
+
let code;
|
|
24
|
+
let exitSignal;
|
|
25
|
+
let timeoutId;
|
|
26
|
+
let forceKillId;
|
|
27
|
+
let closeDeadlineId;
|
|
28
|
+
let child;
|
|
29
|
+
let resolveResult;
|
|
30
|
+
const result = new Promise((resolve) => { resolveResult = resolve; });
|
|
31
|
+
|
|
32
|
+
const finish = () => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timeoutId);
|
|
36
|
+
clearTimeout(forceKillId);
|
|
37
|
+
clearTimeout(closeDeadlineId);
|
|
38
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
39
|
+
resolveResult({
|
|
40
|
+
exitCode: timedOut || cancelled || error ? 1 : (code ?? 1),
|
|
41
|
+
timedOut,
|
|
42
|
+
cancelled,
|
|
43
|
+
timeoutMs,
|
|
44
|
+
signal: exitSignal,
|
|
45
|
+
pid: child?.pid,
|
|
46
|
+
error,
|
|
47
|
+
stdout: stdout.text(),
|
|
48
|
+
stderr: stderr.text(),
|
|
49
|
+
outputTruncated: stdout.truncated || stderr.truncated,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const groupExists = () => {
|
|
53
|
+
if (!child?.pid) return false;
|
|
54
|
+
if (process.platform === "win32") return child.exitCode === null && child.signalCode === null;
|
|
55
|
+
try {
|
|
56
|
+
process.kill(-child.pid, 0);
|
|
57
|
+
return true;
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
if (cause.code === "ESRCH") return false;
|
|
60
|
+
error ??= cause;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const signalGroup = (signal) => {
|
|
65
|
+
if (!child?.pid) return;
|
|
66
|
+
try {
|
|
67
|
+
if (process.platform === "win32") child.kill(signal);
|
|
68
|
+
else process.kill(-child.pid, signal);
|
|
69
|
+
} catch (cause) {
|
|
70
|
+
if (cause.code !== "ESRCH") error ??= cause;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const stop = () => {
|
|
74
|
+
if (stopping || settled) return;
|
|
75
|
+
stopping = true;
|
|
76
|
+
signalGroup("SIGTERM");
|
|
77
|
+
forceKillId = setTimeout(() => {
|
|
78
|
+
// The leader may already be reaped while its descendants hold the pipes.
|
|
79
|
+
signalGroup("SIGKILL");
|
|
80
|
+
escalated = true;
|
|
81
|
+
if (closed) {
|
|
82
|
+
finish();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
closeDeadlineId = setTimeout(() => {
|
|
86
|
+
error ??= new Error("Owned child stdio did not close after SIGKILL");
|
|
87
|
+
child?.stdout?.destroy();
|
|
88
|
+
child?.stderr?.destroy();
|
|
89
|
+
child?.stdin?.destroy();
|
|
90
|
+
child?.unref();
|
|
91
|
+
finish();
|
|
92
|
+
}, killWaitMs);
|
|
93
|
+
}, killGraceMs);
|
|
94
|
+
};
|
|
95
|
+
const cancel = () => {
|
|
96
|
+
cancelled = true;
|
|
97
|
+
stop();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (cancelled) {
|
|
101
|
+
finish();
|
|
102
|
+
return { child, result };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
child = spawn(options.command, options.args ?? [], {
|
|
106
|
+
cwd: options.cwd,
|
|
107
|
+
env,
|
|
108
|
+
detached: process.platform !== "win32",
|
|
109
|
+
stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
|
|
110
|
+
});
|
|
111
|
+
} catch (cause) {
|
|
112
|
+
error = cause;
|
|
113
|
+
finish();
|
|
114
|
+
return { child, result };
|
|
115
|
+
}
|
|
116
|
+
child.stdout?.on("data", (chunk) => stdout.push(chunk));
|
|
117
|
+
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
118
|
+
const fail = (cause) => {
|
|
119
|
+
error ??= cause;
|
|
120
|
+
stop();
|
|
121
|
+
};
|
|
122
|
+
child.stdout?.on("error", fail);
|
|
123
|
+
child.stderr?.on("error", fail);
|
|
124
|
+
child.once("error", fail);
|
|
125
|
+
child.once("exit", () => {
|
|
126
|
+
// Clean descendants even after a successful leader exit or closed pipes.
|
|
127
|
+
if (groupExists()) stop();
|
|
128
|
+
});
|
|
129
|
+
child.once("close", (exitCode, signal) => {
|
|
130
|
+
closed = true;
|
|
131
|
+
code = exitCode;
|
|
132
|
+
exitSignal = signal;
|
|
133
|
+
if (!escalated && groupExists()) stop();
|
|
134
|
+
else finish();
|
|
135
|
+
});
|
|
136
|
+
timeoutId = setTimeout(() => {
|
|
137
|
+
timedOut = true;
|
|
138
|
+
stop();
|
|
139
|
+
}, timeoutMs);
|
|
140
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
141
|
+
if (options.signal?.aborted) cancel();
|
|
142
|
+
return { child, result };
|
|
143
|
+
}
|
|
144
|
+
|
|
4
145
|
export async function runProfiledProcess(options) {
|
|
5
146
|
const start = performance.now();
|
|
6
147
|
const heapStartMb = heapUsedMb();
|
|
@@ -10,129 +151,136 @@ export async function runProfiledProcess(options) {
|
|
|
10
151
|
let statSampleCount = 0;
|
|
11
152
|
let rssSampleCount = 0;
|
|
12
153
|
let cpuSampleCount = 0;
|
|
13
|
-
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
cwd: options.cwd,
|
|
19
|
-
env: options.env,
|
|
20
|
-
stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
|
|
21
|
-
});
|
|
22
|
-
const stdout = [];
|
|
23
|
-
const stderr = [];
|
|
24
|
-
child.stdout?.on("data", (chunk) => stdout.push(chunk));
|
|
25
|
-
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
26
|
-
|
|
27
|
-
const recordStats = (stats) => {
|
|
28
|
-
if (stats.rssAvailable || stats.cpuAvailable) {
|
|
29
|
-
statSampleCount += 1;
|
|
30
|
-
}
|
|
31
|
-
if (stats.rssAvailable) {
|
|
32
|
-
rssSampleCount += 1;
|
|
33
|
-
}
|
|
34
|
-
if (stats.cpuAvailable) {
|
|
35
|
-
cpuSampleCount += 1;
|
|
36
|
-
}
|
|
37
|
-
if (stats.rssAvailable && stats.rssKb > 0 && firstRssKb === 0) {
|
|
38
|
-
firstRssKb = stats.rssKb;
|
|
39
|
-
}
|
|
40
|
-
if (stats.rssAvailable) {
|
|
41
|
-
peakRssKb = Math.max(peakRssKb, stats.rssKb);
|
|
42
|
-
}
|
|
43
|
-
if (stats.cpuAvailable) {
|
|
44
|
-
peakCpuPercent = Math.max(peakCpuPercent, stats.cpuPercent);
|
|
45
|
-
cpuSamples.push(stats.cpuPercent);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
154
|
+
let cpuTotal = 0;
|
|
155
|
+
let pendingStats;
|
|
156
|
+
let stopped = false;
|
|
157
|
+
const statsController = new AbortController();
|
|
158
|
+
const running = startOwnedProcess(options);
|
|
49
159
|
const sampleStats = () => {
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
160
|
+
if (pendingStats || stopped || !running.child?.pid) return;
|
|
161
|
+
pendingStats = readProcessStats(running.child.pid, options.env, statsController.signal)
|
|
162
|
+
.then((stats) => {
|
|
163
|
+
if (stopped) return;
|
|
164
|
+
if (stats.rssAvailable || stats.cpuAvailable) statSampleCount += 1;
|
|
165
|
+
if (stats.rssAvailable) {
|
|
166
|
+
rssSampleCount += 1;
|
|
167
|
+
if (stats.rssKb > 0 && firstRssKb === 0) firstRssKb = stats.rssKb;
|
|
168
|
+
peakRssKb = Math.max(peakRssKb, stats.rssKb);
|
|
169
|
+
}
|
|
170
|
+
if (stats.cpuAvailable) {
|
|
171
|
+
cpuSampleCount += 1;
|
|
172
|
+
peakCpuPercent = Math.max(peakCpuPercent, stats.cpuPercent);
|
|
173
|
+
cpuTotal += stats.cpuPercent;
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.finally(() => { pendingStats = undefined; });
|
|
61
177
|
};
|
|
62
|
-
|
|
178
|
+
const poll = setInterval(sampleStats, positiveLimit(options.pollMs, undefined, 25));
|
|
179
|
+
const stopSampling = () => {
|
|
180
|
+
stopped = true;
|
|
181
|
+
clearInterval(poll);
|
|
182
|
+
statsController.abort();
|
|
183
|
+
};
|
|
184
|
+
running.child?.once("exit", stopSampling);
|
|
185
|
+
running.child?.once("error", stopSampling);
|
|
63
186
|
sampleStats();
|
|
64
|
-
const poll = setInterval(sampleStats, options.pollMs ?? 25);
|
|
65
|
-
|
|
66
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
67
|
-
child.on("error", (error) => {
|
|
68
|
-
clearInterval(poll);
|
|
69
|
-
reject(error);
|
|
70
|
-
});
|
|
71
|
-
child.on("exit", (code) => resolve(code ?? 1));
|
|
72
|
-
});
|
|
73
|
-
clearInterval(poll);
|
|
74
|
-
await Promise.allSettled([...pendingStats]);
|
|
75
|
-
|
|
76
|
-
const finalStats = await readProcessStats(child.pid);
|
|
77
|
-
recordStats(finalStats);
|
|
78
187
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
188
|
+
try {
|
|
189
|
+
const outcome = await running.result;
|
|
190
|
+
stopSampling();
|
|
191
|
+
await pendingStats;
|
|
192
|
+
if (outcome.error) throw outcome.error;
|
|
193
|
+
const wallMs = Math.round(performance.now() - start);
|
|
194
|
+
const averageCpuPercent = cpuSampleCount > 0 ? cpuTotal / cpuSampleCount : 0;
|
|
195
|
+
const cpuPercentForEstimate = options.roundAverageCpuPercent === true
|
|
86
196
|
? Math.round(averageCpuPercent * 10) / 10
|
|
87
197
|
: averageCpuPercent;
|
|
198
|
+
return {
|
|
199
|
+
wallMs,
|
|
200
|
+
peakRssMb: Math.round((peakRssKb / 1024) * 10) / 10,
|
|
201
|
+
rssDeltaMb: Math.round(((peakRssKb - firstRssKb) / 1024) * 10) / 10,
|
|
202
|
+
peakCpuPercent: Math.round(peakCpuPercent * 10) / 10,
|
|
203
|
+
cpuMsEstimate: Math.round((wallMs * cpuPercentForEstimate) / 100),
|
|
204
|
+
harnessHeapDeltaMb: Math.round((heapUsedMb() - heapStartMb) * 10) / 10,
|
|
205
|
+
statSampleCount,
|
|
206
|
+
rssSampleCount,
|
|
207
|
+
cpuSampleCount,
|
|
208
|
+
exitCode: outcome.exitCode,
|
|
209
|
+
timedOut: outcome.timedOut,
|
|
210
|
+
cancelled: outcome.cancelled,
|
|
211
|
+
pid: outcome.pid,
|
|
212
|
+
stdoutPreview: previewLines(outcome.stdout),
|
|
213
|
+
stderrPreview: previewLines(outcome.stderr),
|
|
214
|
+
};
|
|
215
|
+
} finally {
|
|
216
|
+
stopSampling();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
88
219
|
|
|
220
|
+
export function resolveProcessLimits(options, kind = "PROFILE") {
|
|
221
|
+
const env = options.env ?? process.env;
|
|
222
|
+
const setting = (name) => env[`PLUGIN_INSPECTOR_${kind}_${name}`] ?? process.env[`PLUGIN_INSPECTOR_${kind}_${name}`];
|
|
89
223
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
peakCpuPercent: Math.round(peakCpuPercent * 10) / 10,
|
|
94
|
-
cpuMsEstimate: Math.round((wallMs * cpuPercentForEstimate) / 100),
|
|
95
|
-
harnessHeapDeltaMb: Math.round((heapUsedMb() - heapStartMb) * 10) / 10,
|
|
96
|
-
statSampleCount,
|
|
97
|
-
rssSampleCount,
|
|
98
|
-
cpuSampleCount,
|
|
99
|
-
exitCode,
|
|
100
|
-
stdoutPreview: previewLines(stdout),
|
|
101
|
-
stderrPreview: previewLines(stderr),
|
|
224
|
+
timeoutMs: positiveLimit(options.timeoutMs, setting("TIMEOUT_MS"), defaultTimeoutMs),
|
|
225
|
+
killGraceMs: positiveLimit(options.killGraceMs, setting("KILL_GRACE_MS"), defaultKillGraceMs, 30_000),
|
|
226
|
+
maxOutputBytes: positiveLimit(options.maxOutputBytes, setting("MAX_OUTPUT_BYTES"), (kind === "CAPTURE" || kind === "PROBE" ? 10 : 1) * 1024 * 1024),
|
|
102
227
|
};
|
|
103
228
|
}
|
|
104
229
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
230
|
+
function positiveLimit(option, env, fallback, max = maxTimerMs) {
|
|
231
|
+
const valid = (value) => Number.isFinite(value) && value > 0 && value <= max;
|
|
232
|
+
if (valid(option)) return Math.ceil(option);
|
|
233
|
+
const fromEnv = typeof env === "string" ? Number(env) : NaN;
|
|
234
|
+
return valid(fromEnv) ? Math.ceil(fromEnv) : fallback;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function createCappedCollector(maxBytes) {
|
|
238
|
+
const chunks = [];
|
|
239
|
+
let size = 0;
|
|
240
|
+
let truncated = false;
|
|
241
|
+
return {
|
|
242
|
+
get truncated() { return truncated; },
|
|
243
|
+
push(chunk) {
|
|
244
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
245
|
+
const length = Math.min(buffer.length, maxBytes - size);
|
|
246
|
+
if (length < buffer.length) truncated = true;
|
|
247
|
+
if (length === 0) return;
|
|
248
|
+
chunks.push(Buffer.from(buffer.subarray(0, length)));
|
|
249
|
+
size += length;
|
|
250
|
+
},
|
|
251
|
+
text: () => Buffer.concat(chunks, size).toString("utf8"),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function readProcessStats(pid, env, signal) {
|
|
256
|
+
const unavailable = { rssAvailable: false, rssKb: 0, cpuAvailable: false, cpuPercent: 0 };
|
|
257
|
+
if (!pid || process.platform === "win32") return unavailable;
|
|
258
|
+
const { result } = startOwnedProcess({
|
|
259
|
+
command: "ps",
|
|
260
|
+
args: ["-o", "rss=", "-o", "%cpu=", "-p", String(pid)],
|
|
261
|
+
env,
|
|
262
|
+
signal,
|
|
263
|
+
timeoutMs: 250,
|
|
264
|
+
killGraceMs: 50,
|
|
265
|
+
maxOutputBytes: 4096,
|
|
129
266
|
});
|
|
267
|
+
const outcome = await result;
|
|
268
|
+
if (outcome.exitCode !== 0 || outcome.outputTruncated) return unavailable;
|
|
269
|
+
const [rssRaw, cpuRaw] = outcome.stdout.trim().split(/\s+/);
|
|
270
|
+
const rssKb = Number.parseInt(rssRaw, 10);
|
|
271
|
+
const cpuPercent = Number.parseFloat(cpuRaw);
|
|
272
|
+
return {
|
|
273
|
+
rssAvailable: Number.isFinite(rssKb),
|
|
274
|
+
rssKb: Number.isFinite(rssKb) ? rssKb : 0,
|
|
275
|
+
cpuAvailable: Number.isFinite(cpuPercent),
|
|
276
|
+
cpuPercent: Number.isFinite(cpuPercent) ? cpuPercent : 0,
|
|
277
|
+
};
|
|
130
278
|
}
|
|
131
279
|
|
|
132
280
|
function heapUsedMb() {
|
|
133
281
|
return Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 10) / 10;
|
|
134
282
|
}
|
|
135
283
|
|
|
136
|
-
function previewLines(
|
|
137
|
-
return
|
|
284
|
+
function previewLines(text) {
|
|
285
|
+
return text.trim().split("\n").slice(-2).join("\n");
|
|
138
286
|
}
|
|
@@ -12,6 +12,7 @@ export async function buildRuntimeCaptureReport(options = {}) {
|
|
|
12
12
|
const results = [];
|
|
13
13
|
for (const fixture of report.fixtures) {
|
|
14
14
|
for (const target of captureTargets(fixture, rootDir)) {
|
|
15
|
+
options.signal?.throwIfAborted();
|
|
15
16
|
results.push(await captureTarget(target, options));
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -117,6 +118,11 @@ async function captureTarget(target, options) {
|
|
|
117
118
|
mockSdk: options.mockSdk !== false,
|
|
118
119
|
apiOptions: options.apiOptions,
|
|
119
120
|
env: options.env,
|
|
121
|
+
isolateCapture: options.isolateCapture,
|
|
122
|
+
timeoutMs: options.timeoutMs,
|
|
123
|
+
killGraceMs: options.killGraceMs,
|
|
124
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
125
|
+
signal: options.signal,
|
|
120
126
|
});
|
|
121
127
|
return {
|
|
122
128
|
fixture: target.fixture,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as nodeModule from "node:module";
|
|
2
|
+
import { parse } from "acorn";
|
|
3
|
+
import { analyze } from "eslint-scope";
|
|
4
|
+
|
|
5
|
+
const literalModuleImport = /(?<![$\w.])(?:(?:const|let|var)\s+(?:\{[^{}]*\}|[$A-Z_a-z][$\w]*)\s*=\s*)?(?<kind>require|import)\s*\(\s*(?<quote>["'`])(?<specifier>[^"'`\\\r\n]+)\k<quote>/dg;
|
|
6
|
+
|
|
7
|
+
export function collectRuntimeModuleImports(text) {
|
|
8
|
+
// Mark literal occurrences without interpreting quotes or regexes; the AST owns real calls.
|
|
9
|
+
const entries = [...text.matchAll(literalModuleImport)].map(moduleImportEntry).filter(Boolean);
|
|
10
|
+
if (entries.length === 0) return entries;
|
|
11
|
+
const { runtimeText, imports } = classifyRuntimeImports(text, entries);
|
|
12
|
+
let ast;
|
|
13
|
+
let scopes;
|
|
14
|
+
try {
|
|
15
|
+
// Accept both source modules and CommonJS wrappers without changing their runtime.
|
|
16
|
+
ast = parse(runtimeText, {
|
|
17
|
+
ecmaVersion: "latest", ranges: true, sourceType: "script",
|
|
18
|
+
allowAwaitOutsideFunction: true, allowReturnOutsideFunction: true, allowImportExportEverywhere: true,
|
|
19
|
+
});
|
|
20
|
+
scopes = analyze(ast, {
|
|
21
|
+
ecmaVersion: 2026,
|
|
22
|
+
sourceType: ast.body.some((node) => /^(?:Import|Export).*Declaration$/.test(node.type)) ? "module" : "commonjs",
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
// Keep known imports if syntax is incomplete/unsupported; never guess namespace exports.
|
|
26
|
+
// Type erasure has already classified each occurrence, even when AST analysis fails.
|
|
27
|
+
const retained = new Set(imports.map(({ entry }) => entry.specifierStart));
|
|
28
|
+
return [...scanLiteralModuleImports(text)].filter((entry) => retained.has(entry.specifierStart))
|
|
29
|
+
.map((entry) => ({ ...entry, names: new Set() }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parents = new Map();
|
|
33
|
+
const nodes = [];
|
|
34
|
+
visit(ast);
|
|
35
|
+
function visit(node, parent) {
|
|
36
|
+
if (!node || typeof node.type !== "string") return;
|
|
37
|
+
parents.set(node, parent);
|
|
38
|
+
nodes.push(node);
|
|
39
|
+
for (const value of Object.values(node)) {
|
|
40
|
+
if (Array.isArray(value)) value.forEach((child) => visit(child, node));
|
|
41
|
+
else if (value && typeof value === "object") visit(value, node);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const byMarker = new Map(imports.map(({ marker, entry }) => [marker, entry]));
|
|
45
|
+
const result = [];
|
|
46
|
+
for (const node of nodes) {
|
|
47
|
+
const source = node.type === "ImportExpression" ? node.source
|
|
48
|
+
: node.type === "CallExpression" && node.callee.type === "Identifier"
|
|
49
|
+
&& node.callee.name === "require" && node.arguments.length === 1 ? node.arguments[0] : null;
|
|
50
|
+
const specifier = source?.type === "Literal" ? source.value
|
|
51
|
+
: source?.type === "TemplateLiteral" && source.expressions.length === 0 ? source.quasis[0].value.cooked : null;
|
|
52
|
+
const entry = byMarker.get(specifier);
|
|
53
|
+
if (!entry) continue;
|
|
54
|
+
const names = new Set();
|
|
55
|
+
result.push({ ...entry, names });
|
|
56
|
+
const access = parents.get(node);
|
|
57
|
+
const call = parents.get(access);
|
|
58
|
+
if (node.type === "ImportExpression" && access?.type === "MemberExpression" && access.object === node
|
|
59
|
+
&& !access.computed && access.property.name === "then" && call?.type === "CallExpression" && call.callee === access) {
|
|
60
|
+
const callback = call.arguments[0];
|
|
61
|
+
if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") {
|
|
62
|
+
collectBindingNames(callback.params[0], callback, names);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Promise methods belong to import(), not its awaited module namespace.
|
|
66
|
+
const module = entry.kind === "require" ? node
|
|
67
|
+
: parents.get(node)?.type === "AwaitExpression" ? parents.get(node) : null;
|
|
68
|
+
if (!module) continue;
|
|
69
|
+
const parent = parents.get(module);
|
|
70
|
+
if (parent?.type === "MemberExpression" && parent.object === module && !parent.computed && parent.property.type === "Identifier") {
|
|
71
|
+
names.add(parent.property.name);
|
|
72
|
+
}
|
|
73
|
+
if (parent?.type === "VariableDeclarator" && parent.init === module) collectBindingNames(parent.id, parent, names);
|
|
74
|
+
}
|
|
75
|
+
return result.sort((a, b) => a.index - b.index);
|
|
76
|
+
|
|
77
|
+
function collectBindingNames(binding, owner, names) {
|
|
78
|
+
if (binding?.type === "ObjectPattern") {
|
|
79
|
+
for (const property of binding.properties) {
|
|
80
|
+
if (property.type === "Property" && !property.computed && property.key.type === "Identifier") {
|
|
81
|
+
names.add(property.key.name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else if (binding?.type === "Identifier") {
|
|
85
|
+
// Resolve references to this declaration, including closures but excluding shadowed names.
|
|
86
|
+
for (const variable of scopes.getDeclaredVariables(owner)) {
|
|
87
|
+
if (!variable.identifiers.includes(binding)) continue;
|
|
88
|
+
for (const reference of variable.references) {
|
|
89
|
+
const access = parents.get(reference.identifier);
|
|
90
|
+
if (access?.type === "MemberExpression" && access.object === reference.identifier && !access.computed && access.property.type === "Identifier") {
|
|
91
|
+
names.add(access.property.name);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function* scanLiteralModuleImports(text) {
|
|
100
|
+
const quotedOrComment = /\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$)|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'/;
|
|
101
|
+
const code = new RegExp(quotedOrComment.source + "|" + literalModuleImport.source + "|[`{}]", "dg");
|
|
102
|
+
const template = /\\[\s\S]|`|\$\{/g;
|
|
103
|
+
const templateDepths = [];
|
|
104
|
+
let inTemplateText = false;
|
|
105
|
+
let cursor = 0;
|
|
106
|
+
// Skip quoted/comment text, but scan executable template interpolations.
|
|
107
|
+
while (cursor < text.length) {
|
|
108
|
+
const pattern = inTemplateText ? template : code;
|
|
109
|
+
pattern.lastIndex = cursor;
|
|
110
|
+
const match = pattern.exec(text);
|
|
111
|
+
if (!match) break;
|
|
112
|
+
cursor = pattern.lastIndex;
|
|
113
|
+
if (inTemplateText) {
|
|
114
|
+
if (match[0] === "`") {
|
|
115
|
+
templateDepths.pop();
|
|
116
|
+
inTemplateText = false;
|
|
117
|
+
} else if (match[0] === "${") {
|
|
118
|
+
templateDepths[templateDepths.length - 1] = 1;
|
|
119
|
+
inTemplateText = false;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (match[0] === "`") {
|
|
124
|
+
templateDepths.push(0);
|
|
125
|
+
inTemplateText = true;
|
|
126
|
+
} else if (templateDepths.length && match[0] === "{") {
|
|
127
|
+
templateDepths[templateDepths.length - 1] += 1;
|
|
128
|
+
} else if (templateDepths.length && match[0] === "}") {
|
|
129
|
+
inTemplateText = --templateDepths[templateDepths.length - 1] === 0;
|
|
130
|
+
}
|
|
131
|
+
const entry = moduleImportEntry(match);
|
|
132
|
+
if (entry) yield entry;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function moduleImportEntry(match) {
|
|
137
|
+
const groups = match.groups;
|
|
138
|
+
if (!groups?.specifier || (groups.quote === "`" && groups.specifier.includes("${"))) return null;
|
|
139
|
+
return {
|
|
140
|
+
specifier: groups.specifier,
|
|
141
|
+
specifierStart: match.indices.groups.specifier[0],
|
|
142
|
+
kind: groups.kind,
|
|
143
|
+
index: groups.kind === "import" ? match.indices.groups.kind[0] : match.index,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function classifyRuntimeImports(text, entries) {
|
|
148
|
+
const markedImports = entries.map((entry, index) => {
|
|
149
|
+
const { specifier, specifierStart } = entry;
|
|
150
|
+
return {
|
|
151
|
+
entry,
|
|
152
|
+
specifierStart,
|
|
153
|
+
specifierEnd: specifierStart + specifier.length,
|
|
154
|
+
marker: `${specifier}__plugin_inspector_runtime_import_${index}__`,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
let markedText = text;
|
|
158
|
+
for (const markedImport of markedImports.toReversed()) {
|
|
159
|
+
markedText =
|
|
160
|
+
markedText.slice(0, markedImport.specifierStart) +
|
|
161
|
+
markedImport.marker +
|
|
162
|
+
markedText.slice(markedImport.specifierEnd);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let runtimeText = null;
|
|
166
|
+
try {
|
|
167
|
+
runtimeText = eraseTypeScript(markedText);
|
|
168
|
+
} catch {
|
|
169
|
+
// Unsupported or incomplete TypeScript cannot prove an import is type-only.
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
runtimeText: runtimeText ?? markedText,
|
|
173
|
+
imports: runtimeText === null ? markedImports : markedImports.filter(({ marker }) => runtimeText.includes(marker)),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function eraseTypeScript(text) {
|
|
178
|
+
if (typeof nodeModule.stripTypeScriptTypes === "function") {
|
|
179
|
+
try {
|
|
180
|
+
return nodeModule.stripTypeScriptTypes(text, { mode: "transform" });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error?.code !== "ERR_INVALID_ARG_VALUE") throw error;
|
|
183
|
+
return nodeModule.stripTypeScriptTypes(text, { mode: "strip" });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (typeof globalThis.Bun?.Transpiler === "function") {
|
|
187
|
+
const transpiler = new globalThis.Bun.Transpiler({ loader: "ts", target: "bun" });
|
|
188
|
+
return transpiler.transformSync(text);
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
package/src/runtime-profile.js
CHANGED
|
@@ -333,6 +333,10 @@ async function profileCommand(command, options) {
|
|
|
333
333
|
env: { ...process.env, ...options.env, ...command.env },
|
|
334
334
|
stdio: ["ignore", "pipe", "pipe"],
|
|
335
335
|
roundAverageCpuPercent: true,
|
|
336
|
+
timeoutMs: command.timeoutMs ?? options.timeoutMs,
|
|
337
|
+
maxOutputBytes: command.maxOutputBytes ?? options.maxOutputBytes,
|
|
338
|
+
killGraceMs: command.killGraceMs ?? options.killGraceMs,
|
|
339
|
+
signal: options.signal,
|
|
336
340
|
});
|
|
337
341
|
}
|
|
338
342
|
|
|
@@ -81,12 +81,6 @@ function expectedRuntimeCaptureKeys(finding) {
|
|
|
81
81
|
if (finding.code === "runtime-tool-capture") {
|
|
82
82
|
return ["registration:registerTool"];
|
|
83
83
|
}
|
|
84
|
-
if (finding.code === "conversation-access-hook") {
|
|
85
|
-
return names.map((name) => `hook:${name}`);
|
|
86
|
-
}
|
|
87
|
-
if (finding.code === "before-tool-call-probe") {
|
|
88
|
-
return ["hook:before_tool_call"];
|
|
89
|
-
}
|
|
90
84
|
return [];
|
|
91
85
|
}
|
|
92
86
|
|