@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.
@@ -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
- const cpuSamples = [];
14
- let pollInFlight = false;
15
- const pendingStats = new Set();
16
-
17
- const child = spawn(options.command, options.args ?? [], {
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 (pollInFlight) {
51
- return;
52
- }
53
- pollInFlight = true;
54
- const pending = readProcessStats(child.pid)
55
- .then(recordStats)
56
- .finally(() => {
57
- pollInFlight = false;
58
- pendingStats.delete(pending);
59
- });
60
- pendingStats.add(pending);
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
- const wallMs = Math.round(performance.now() - start);
80
- const averageCpuPercent =
81
- cpuSamples.length > 0
82
- ? cpuSamples.reduce((sum, value) => sum + value, 0) / cpuSamples.length
83
- : 0;
84
- const cpuPercentForEstimate =
85
- options.roundAverageCpuPercent === true
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
- wallMs,
91
- peakRssMb: Math.round((peakRssKb / 1024) * 10) / 10,
92
- rssDeltaMb: Math.round(((peakRssKb - firstRssKb) / 1024) * 10) / 10,
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
- async function readProcessStats(pid) {
106
- if (!pid || process.platform === "win32") {
107
- return { rssAvailable: false, rssKb: 0, cpuAvailable: false, cpuPercent: 0 };
108
- }
109
- return new Promise((resolve) => {
110
- const ps = spawn("ps", ["-o", "rss=", "-o", "%cpu=", "-p", String(pid)], {
111
- stdio: ["ignore", "pipe", "ignore"],
112
- });
113
- const chunks = [];
114
- ps.stdout.on("data", (chunk) => chunks.push(chunk));
115
- ps.on("error", () => resolve({ rssAvailable: false, rssKb: 0, cpuAvailable: false, cpuPercent: 0 }));
116
- ps.on("exit", () => {
117
- const [rssRaw, cpuRaw] = Buffer.concat(chunks).toString("utf8").trim().split(/\s+/);
118
- const rssKb = Number.parseInt(rssRaw, 10);
119
- const cpuPercent = Number.parseFloat(cpuRaw);
120
- const rssAvailable = Number.isFinite(rssKb);
121
- const cpuAvailable = Number.isFinite(cpuPercent);
122
- resolve({
123
- rssAvailable,
124
- rssKb: rssAvailable ? rssKb : 0,
125
- cpuAvailable,
126
- cpuPercent: cpuAvailable ? cpuPercent : 0,
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(chunks) {
137
- return Buffer.concat(chunks).toString("utf8").trim().split("\n").slice(-2).join("\n");
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
+ }
@@ -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