@oh-my-pi/pi-coding-agent 8.12.10 → 8.13.0
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 +14 -0
- package/package.json +7 -7
- package/src/debug/index.ts +362 -0
- package/src/debug/profiler.ts +158 -0
- package/src/debug/report-bundle.ts +280 -0
- package/src/debug/system-info.ts +91 -0
- package/src/lsp/index.ts +1 -0
- package/src/main.ts +1 -1
- package/src/modes/controllers/command-controller.ts +3 -42
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +8 -0
- package/src/modes/interactive-mode.ts +8 -5
- package/src/modes/types.ts +2 -2
- package/src/sdk.ts +1 -1
- package/src/session/agent-storage.ts +26 -2
- package/src/tools/fetch.ts +55 -44
- package/src/tools/gemini-image.ts +2 -7
- package/src/tools/read.ts +1 -1
- package/src/utils/tools-manager.ts +35 -28
- package/src/web/scrapers/github.ts +11 -14
- package/src/web/scrapers/types.ts +2 -36
- package/src/web/scrapers/utils.ts +11 -14
- package/src/web/scrapers/youtube.ts +5 -15
- package/src/web/search/providers/codex.ts +358 -0
- package/src/web/search/providers/gemini.ts +426 -0
- package/src/web/search/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [8.13.0] - 2026-01-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `/debug` command with interactive menu for bug report generation:
|
|
9
|
+
- `Report: performance issue` - CPU profiling with reproduction flow
|
|
10
|
+
- `Report: dump session` - Immediate session bundle creation
|
|
11
|
+
- `Report: memory issue` - Heap snapshot with bundle
|
|
12
|
+
- `View: recent logs` - Display last 50 log entries
|
|
13
|
+
- `View: system info` - Show environment details
|
|
14
|
+
- `Clear: artifact cache` - Remove old session artifacts
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Fixed LSP server errors not being visible in `/session` output or logs when startup fails
|
|
18
|
+
|
|
5
19
|
## [8.12.7] - 2026-01-29
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.13.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -79,12 +79,12 @@
|
|
|
79
79
|
"test": "bun test"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@oh-my-pi/omp-stats": "8.
|
|
83
|
-
"@oh-my-pi/pi-agent-core": "8.
|
|
84
|
-
"@oh-my-pi/pi-ai": "8.
|
|
85
|
-
"@oh-my-pi/pi-natives": "8.
|
|
86
|
-
"@oh-my-pi/pi-tui": "8.
|
|
87
|
-
"@oh-my-pi/pi-utils": "8.
|
|
82
|
+
"@oh-my-pi/omp-stats": "8.13.0",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "8.13.0",
|
|
84
|
+
"@oh-my-pi/pi-ai": "8.13.0",
|
|
85
|
+
"@oh-my-pi/pi-natives": "8.13.0",
|
|
86
|
+
"@oh-my-pi/pi-tui": "8.13.0",
|
|
87
|
+
"@oh-my-pi/pi-utils": "8.13.0",
|
|
88
88
|
"@openai/agents": "^0.4.4",
|
|
89
89
|
"@sinclair/typebox": "^0.34.48",
|
|
90
90
|
"ajv": "^8.17.1",
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug command handler with interactive menu.
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for debugging, bug report generation, and system diagnostics.
|
|
5
|
+
*/
|
|
6
|
+
import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { getSessionsDir } from "../config";
|
|
8
|
+
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
9
|
+
import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
|
|
10
|
+
import type { InteractiveModeContext } from "../modes/types";
|
|
11
|
+
import { generateHeapSnapshotData, type ProfilerSession, startCpuProfile } from "./profiler";
|
|
12
|
+
import { clearArtifactCache, createReportBundle, getArtifactCacheStats, getRecentLogs } from "./report-bundle";
|
|
13
|
+
import { collectSystemInfo, formatSystemInfo } from "./system-info";
|
|
14
|
+
|
|
15
|
+
/** Debug menu options */
|
|
16
|
+
const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
17
|
+
{ value: "performance", label: "Report: performance issue", description: "Profile CPU, reproduce, then bundle" },
|
|
18
|
+
{ value: "dump", label: "Report: dump session", description: "Create report bundle immediately" },
|
|
19
|
+
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
|
20
|
+
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
|
21
|
+
{ value: "system", label: "View: system info", description: "Show environment details" },
|
|
22
|
+
{ value: "clear-cache", label: "Clear: artifact cache", description: "Remove old session artifacts" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Debug selector component.
|
|
27
|
+
*/
|
|
28
|
+
export class DebugSelectorComponent extends Container {
|
|
29
|
+
private selectList: SelectList;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private ctx: InteractiveModeContext,
|
|
33
|
+
onDone: () => void,
|
|
34
|
+
) {
|
|
35
|
+
super();
|
|
36
|
+
|
|
37
|
+
// Title
|
|
38
|
+
this.addChild(new DynamicBorder());
|
|
39
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", "Debug Tools")), 1, 0));
|
|
40
|
+
this.addChild(new Spacer(1));
|
|
41
|
+
|
|
42
|
+
// Select list
|
|
43
|
+
this.selectList = new SelectList(DEBUG_MENU_ITEMS, 6, getSelectListTheme());
|
|
44
|
+
|
|
45
|
+
this.selectList.onSelect = item => {
|
|
46
|
+
onDone();
|
|
47
|
+
void this.handleSelection(item.value);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.selectList.onCancel = () => {
|
|
51
|
+
onDone();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.addChild(this.selectList);
|
|
55
|
+
this.addChild(new DynamicBorder());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleInput(keyData: string): void {
|
|
59
|
+
this.selectList.handleInput(keyData);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async handleSelection(value: string): Promise<void> {
|
|
63
|
+
switch (value) {
|
|
64
|
+
case "performance":
|
|
65
|
+
await this.handlePerformanceReport();
|
|
66
|
+
break;
|
|
67
|
+
case "dump":
|
|
68
|
+
await this.handleDumpReport();
|
|
69
|
+
break;
|
|
70
|
+
case "memory":
|
|
71
|
+
await this.handleMemoryReport();
|
|
72
|
+
break;
|
|
73
|
+
case "logs":
|
|
74
|
+
await this.handleViewLogs();
|
|
75
|
+
break;
|
|
76
|
+
case "system":
|
|
77
|
+
await this.handleViewSystemInfo();
|
|
78
|
+
break;
|
|
79
|
+
case "clear-cache":
|
|
80
|
+
await this.handleClearCache();
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async handlePerformanceReport(): Promise<void> {
|
|
86
|
+
// Start profiling
|
|
87
|
+
let session: ProfilerSession;
|
|
88
|
+
try {
|
|
89
|
+
session = await startCpuProfile();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
this.ctx.showError(`Failed to start profiler: ${err instanceof Error ? err.message : String(err)}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Show message and wait for keypress
|
|
96
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
97
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", `${theme.status.info} CPU profiling started`), 1, 0));
|
|
98
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
99
|
+
this.ctx.chatContainer.addChild(
|
|
100
|
+
new Text(theme.fg("muted", "Reproduce the performance issue, then press Enter to stop profiling."), 1, 0),
|
|
101
|
+
);
|
|
102
|
+
this.ctx.ui.requestRender();
|
|
103
|
+
|
|
104
|
+
// Wait for Enter keypress
|
|
105
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
106
|
+
const originalOnEscape = this.ctx.editor.onEscape;
|
|
107
|
+
const originalOnSubmit = this.ctx.editor.onSubmit;
|
|
108
|
+
|
|
109
|
+
this.ctx.editor.onSubmit = () => {
|
|
110
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
111
|
+
this.ctx.editor.onSubmit = originalOnSubmit;
|
|
112
|
+
resolve();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
this.ctx.editor.onEscape = () => {
|
|
116
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
117
|
+
this.ctx.editor.onSubmit = originalOnSubmit;
|
|
118
|
+
resolve();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
await promise;
|
|
122
|
+
|
|
123
|
+
// Stop profiling and create report
|
|
124
|
+
const loader = new Loader(
|
|
125
|
+
this.ctx.ui,
|
|
126
|
+
spinner => theme.fg("accent", spinner),
|
|
127
|
+
text => theme.fg("muted", text),
|
|
128
|
+
"Generating report...",
|
|
129
|
+
getSymbolTheme().spinnerFrames,
|
|
130
|
+
);
|
|
131
|
+
this.ctx.statusContainer.addChild(loader);
|
|
132
|
+
this.ctx.ui.requestRender();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const cpuProfile = await session.stop();
|
|
136
|
+
const result = await createReportBundle({
|
|
137
|
+
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
138
|
+
settings: this.getResolvedSettings(),
|
|
139
|
+
cpuProfile,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
loader.stop();
|
|
143
|
+
this.ctx.statusContainer.clear();
|
|
144
|
+
|
|
145
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
146
|
+
this.ctx.chatContainer.addChild(
|
|
147
|
+
new Text(theme.fg("success", `${theme.status.success} Performance report saved`), 1, 0),
|
|
148
|
+
);
|
|
149
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
150
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
151
|
+
} catch (err) {
|
|
152
|
+
loader.stop();
|
|
153
|
+
this.ctx.statusContainer.clear();
|
|
154
|
+
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.ctx.ui.requestRender();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async handleDumpReport(): Promise<void> {
|
|
161
|
+
const loader = new Loader(
|
|
162
|
+
this.ctx.ui,
|
|
163
|
+
spinner => theme.fg("accent", spinner),
|
|
164
|
+
text => theme.fg("muted", text),
|
|
165
|
+
"Creating report bundle...",
|
|
166
|
+
getSymbolTheme().spinnerFrames,
|
|
167
|
+
);
|
|
168
|
+
this.ctx.statusContainer.addChild(loader);
|
|
169
|
+
this.ctx.ui.requestRender();
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = await createReportBundle({
|
|
173
|
+
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
174
|
+
settings: this.getResolvedSettings(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
loader.stop();
|
|
178
|
+
this.ctx.statusContainer.clear();
|
|
179
|
+
|
|
180
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
181
|
+
this.ctx.chatContainer.addChild(
|
|
182
|
+
new Text(theme.fg("success", `${theme.status.success} Report bundle saved`), 1, 0),
|
|
183
|
+
);
|
|
184
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
185
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
186
|
+
} catch (err) {
|
|
187
|
+
loader.stop();
|
|
188
|
+
this.ctx.statusContainer.clear();
|
|
189
|
+
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.ctx.ui.requestRender();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async handleMemoryReport(): Promise<void> {
|
|
196
|
+
const loader = new Loader(
|
|
197
|
+
this.ctx.ui,
|
|
198
|
+
spinner => theme.fg("accent", spinner),
|
|
199
|
+
text => theme.fg("muted", text),
|
|
200
|
+
"Generating heap snapshot...",
|
|
201
|
+
getSymbolTheme().spinnerFrames,
|
|
202
|
+
);
|
|
203
|
+
this.ctx.statusContainer.addChild(loader);
|
|
204
|
+
this.ctx.ui.requestRender();
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const heapSnapshot = generateHeapSnapshotData();
|
|
208
|
+
loader.setText("Creating report bundle...");
|
|
209
|
+
|
|
210
|
+
const result = await createReportBundle({
|
|
211
|
+
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
212
|
+
settings: this.getResolvedSettings(),
|
|
213
|
+
heapSnapshot,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
loader.stop();
|
|
217
|
+
this.ctx.statusContainer.clear();
|
|
218
|
+
|
|
219
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
220
|
+
this.ctx.chatContainer.addChild(
|
|
221
|
+
new Text(theme.fg("success", `${theme.status.success} Memory report saved`), 1, 0),
|
|
222
|
+
);
|
|
223
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
224
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
225
|
+
} catch (err) {
|
|
226
|
+
loader.stop();
|
|
227
|
+
this.ctx.statusContainer.clear();
|
|
228
|
+
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.ctx.ui.requestRender();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async handleViewLogs(): Promise<void> {
|
|
235
|
+
try {
|
|
236
|
+
const logs = await getRecentLogs(50);
|
|
237
|
+
if (!logs) {
|
|
238
|
+
this.ctx.showWarning("No log entries found for today.");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
243
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
244
|
+
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Recent Logs")), 1, 0));
|
|
245
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
246
|
+
|
|
247
|
+
// Display logs with dim styling
|
|
248
|
+
const lines = logs.split("\n").slice(-50);
|
|
249
|
+
for (const line of lines) {
|
|
250
|
+
if (line.trim()) {
|
|
251
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
256
|
+
} catch (err) {
|
|
257
|
+
this.ctx.showError(`Failed to read logs: ${err instanceof Error ? err.message : String(err)}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.ctx.ui.requestRender();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private async handleViewSystemInfo(): Promise<void> {
|
|
264
|
+
try {
|
|
265
|
+
const info = await collectSystemInfo();
|
|
266
|
+
const formatted = formatSystemInfo(info);
|
|
267
|
+
|
|
268
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
269
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
270
|
+
this.ctx.chatContainer.addChild(new Text(formatted, 1, 0));
|
|
271
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this.ctx.showError(`Failed to collect system info: ${err instanceof Error ? err.message : String(err)}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.ctx.ui.requestRender();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async handleClearCache(): Promise<void> {
|
|
280
|
+
const sessionsDir = getSessionsDir();
|
|
281
|
+
|
|
282
|
+
// Get stats first
|
|
283
|
+
const stats = await getArtifactCacheStats(sessionsDir);
|
|
284
|
+
|
|
285
|
+
if (stats.count === 0) {
|
|
286
|
+
this.ctx.showStatus("Artifact cache is empty.");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sizeStr = formatBytes(stats.totalSize);
|
|
291
|
+
const oldestStr = stats.oldestDate ? stats.oldestDate.toLocaleDateString() : "unknown";
|
|
292
|
+
|
|
293
|
+
// Show confirmation
|
|
294
|
+
const confirmed = await this.ctx.showHookConfirm(
|
|
295
|
+
"Clear Artifact Cache",
|
|
296
|
+
`Found ${stats.count} artifact files (${sizeStr})\nOldest: ${oldestStr}\n\nRemove artifacts older than 30 days?`,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (!confirmed) {
|
|
300
|
+
this.ctx.showStatus("Cache clear cancelled.");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Clear cache
|
|
305
|
+
const loader = new Loader(
|
|
306
|
+
this.ctx.ui,
|
|
307
|
+
spinner => theme.fg("accent", spinner),
|
|
308
|
+
text => theme.fg("muted", text),
|
|
309
|
+
"Clearing artifact cache...",
|
|
310
|
+
getSymbolTheme().spinnerFrames,
|
|
311
|
+
);
|
|
312
|
+
this.ctx.statusContainer.addChild(loader);
|
|
313
|
+
this.ctx.ui.requestRender();
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const result = await clearArtifactCache(sessionsDir, 30);
|
|
317
|
+
|
|
318
|
+
loader.stop();
|
|
319
|
+
this.ctx.statusContainer.clear();
|
|
320
|
+
|
|
321
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
322
|
+
this.ctx.chatContainer.addChild(
|
|
323
|
+
new Text(
|
|
324
|
+
theme.fg("success", `${theme.status.success} Cleared ${result.removed} artifact directories`),
|
|
325
|
+
1,
|
|
326
|
+
0,
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
loader.stop();
|
|
331
|
+
this.ctx.statusContainer.clear();
|
|
332
|
+
this.ctx.showError(`Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.ctx.ui.requestRender();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private getResolvedSettings(): Record<string, unknown> {
|
|
339
|
+
// Extract key settings for the report
|
|
340
|
+
return {
|
|
341
|
+
model: this.ctx.session.model?.id,
|
|
342
|
+
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
343
|
+
planModeEnabled: this.ctx.planModeEnabled,
|
|
344
|
+
toolOutputExpanded: this.ctx.toolOutputExpanded,
|
|
345
|
+
hideThinkingBlock: this.ctx.hideThinkingBlock,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function formatBytes(bytes: number): string {
|
|
351
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
352
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
353
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Show the debug selector.
|
|
358
|
+
*/
|
|
359
|
+
export function showDebugSelector(ctx: InteractiveModeContext, done: () => void): DebugSelectorComponent {
|
|
360
|
+
const selector = new DebugSelectorComponent(ctx, done);
|
|
361
|
+
return selector;
|
|
362
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPU and heap profiling wrappers for debug reports.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface CpuProfile {
|
|
6
|
+
data: string;
|
|
7
|
+
markdown: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProfilerSession {
|
|
11
|
+
/** Stop profiling and return the profile data */
|
|
12
|
+
stop(): Promise<CpuProfile>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** V8 CPU Profile node structure */
|
|
16
|
+
interface CpuProfileNode {
|
|
17
|
+
id: number;
|
|
18
|
+
callFrame?: {
|
|
19
|
+
functionName?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
lineNumber?: number;
|
|
22
|
+
};
|
|
23
|
+
hitCount?: number;
|
|
24
|
+
children?: number[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** V8 CPU Profile structure */
|
|
28
|
+
interface CpuProfileData {
|
|
29
|
+
nodes?: CpuProfileNode[];
|
|
30
|
+
samples?: number[];
|
|
31
|
+
timeDeltas?: number[];
|
|
32
|
+
startTime?: number;
|
|
33
|
+
endTime?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format CPU profile data as markdown for LLM analysis.
|
|
38
|
+
* Extracts top functions by self time and call counts.
|
|
39
|
+
*/
|
|
40
|
+
function formatProfileAsMarkdown(profileJson: string): string {
|
|
41
|
+
try {
|
|
42
|
+
const profile = JSON.parse(profileJson) as CpuProfileData;
|
|
43
|
+
const nodes = profile.nodes ?? [];
|
|
44
|
+
|
|
45
|
+
interface NodeInfo {
|
|
46
|
+
id: number;
|
|
47
|
+
functionName: string;
|
|
48
|
+
url: string;
|
|
49
|
+
lineNumber: number;
|
|
50
|
+
selfTime: number;
|
|
51
|
+
hitCount: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const nodeMap = new Map<number, NodeInfo>();
|
|
55
|
+
for (const node of nodes) {
|
|
56
|
+
nodeMap.set(node.id, {
|
|
57
|
+
id: node.id,
|
|
58
|
+
functionName: node.callFrame?.functionName ?? "(anonymous)",
|
|
59
|
+
url: node.callFrame?.url ?? "",
|
|
60
|
+
lineNumber: node.callFrame?.lineNumber ?? 0,
|
|
61
|
+
selfTime: 0,
|
|
62
|
+
hitCount: node.hitCount ?? 0,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Distribute sample times to nodes
|
|
67
|
+
const samples = profile.samples ?? [];
|
|
68
|
+
const timeDeltas = profile.timeDeltas ?? [];
|
|
69
|
+
for (let i = 0; i < samples.length; i++) {
|
|
70
|
+
const nodeId = samples[i];
|
|
71
|
+
const info = nodeId !== undefined ? nodeMap.get(nodeId) : undefined;
|
|
72
|
+
const delta = timeDeltas[i] ?? 0;
|
|
73
|
+
if (info) {
|
|
74
|
+
info.selfTime += delta;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Sort by self time and get top functions
|
|
79
|
+
const sorted = Array.from(nodeMap.values())
|
|
80
|
+
.filter(n => n.selfTime > 0 && n.functionName !== "(root)" && n.functionName !== "(idle)")
|
|
81
|
+
.sort((a, b) => b.selfTime - a.selfTime)
|
|
82
|
+
.slice(0, 30);
|
|
83
|
+
|
|
84
|
+
if (sorted.length === 0) {
|
|
85
|
+
return "# CPU Profile Summary\n\nNo significant CPU activity recorded.";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const totalTime = sorted.reduce((sum, n) => sum + n.selfTime, 0);
|
|
89
|
+
|
|
90
|
+
const lines = ["# CPU Profile Summary", ""];
|
|
91
|
+
lines.push(`Total profiled time: ${(totalTime / 1000).toFixed(1)}ms`);
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push("## Top Functions by Self Time");
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("| Function | Self Time (ms) | % | Location |");
|
|
96
|
+
lines.push("|----------|----------------|---|----------|");
|
|
97
|
+
|
|
98
|
+
for (const node of sorted) {
|
|
99
|
+
const selfMs = (node.selfTime / 1000).toFixed(1);
|
|
100
|
+
const pct = ((node.selfTime / totalTime) * 100).toFixed(1);
|
|
101
|
+
const location = node.url ? `${node.url}:${node.lineNumber}` : "-";
|
|
102
|
+
lines.push(`| ${node.functionName} | ${selfMs} | ${pct}% | ${location} |`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
} catch {
|
|
107
|
+
return "# CPU Profile Summary\n\nFailed to parse profile data.";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Start CPU profiling.
|
|
113
|
+
* Returns a session that can be stopped to get the profile data.
|
|
114
|
+
*/
|
|
115
|
+
export async function startCpuProfile(): Promise<ProfilerSession> {
|
|
116
|
+
const v8 = await import("node:v8");
|
|
117
|
+
v8.setFlagsFromString("--allow-natives-syntax");
|
|
118
|
+
|
|
119
|
+
const { Session } = await import("node:inspector/promises");
|
|
120
|
+
const session = new Session();
|
|
121
|
+
session.connect();
|
|
122
|
+
|
|
123
|
+
await session.post("Profiler.enable");
|
|
124
|
+
await session.post("Profiler.start");
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
async stop(): Promise<CpuProfile> {
|
|
128
|
+
const result = await session.post("Profiler.stop");
|
|
129
|
+
await session.post("Profiler.disable");
|
|
130
|
+
session.disconnect();
|
|
131
|
+
|
|
132
|
+
const data = JSON.stringify(result.profile, null, 2);
|
|
133
|
+
const markdown = formatProfileAsMarkdown(data);
|
|
134
|
+
|
|
135
|
+
return { data, markdown };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface HeapSnapshot {
|
|
141
|
+
data: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate a heap snapshot.
|
|
146
|
+
* Uses Bun's built-in generateHeapSnapshot.
|
|
147
|
+
*/
|
|
148
|
+
export function generateHeapSnapshotData(): HeapSnapshot {
|
|
149
|
+
// Force GC before snapshot
|
|
150
|
+
Bun.gc(true);
|
|
151
|
+
|
|
152
|
+
// Use V8 format for Chrome DevTools compatibility
|
|
153
|
+
const snapshot = Bun.generateHeapSnapshot("v8");
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
data: snapshot,
|
|
157
|
+
};
|
|
158
|
+
}
|