@oh-my-pi/pi-coding-agent 8.12.9 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +7 -9
  3. package/src/config/settings-manager.ts +61 -3
  4. package/src/debug/index.ts +362 -0
  5. package/src/debug/profiler.ts +158 -0
  6. package/src/debug/report-bundle.ts +280 -0
  7. package/src/debug/system-info.ts +91 -0
  8. package/src/exec/bash-executor.ts +2 -2
  9. package/src/extensibility/plugins/installer.ts +2 -2
  10. package/src/extensibility/plugins/manager.ts +3 -3
  11. package/src/index.ts +0 -1
  12. package/src/ipy/gateway-coordinator.ts +12 -22
  13. package/src/ipy/kernel.ts +3 -3
  14. package/src/lsp/client.ts +27 -21
  15. package/src/lsp/index.ts +1 -0
  16. package/src/lsp/render.ts +13 -19
  17. package/src/lsp/types.ts +2 -2
  18. package/src/main.ts +1 -1
  19. package/src/modes/controllers/command-controller.ts +3 -42
  20. package/src/modes/controllers/input-controller.ts +2 -2
  21. package/src/modes/controllers/selector-controller.ts +8 -0
  22. package/src/modes/interactive-mode.ts +8 -5
  23. package/src/modes/rpc/rpc-client.ts +1 -1
  24. package/src/modes/theme/theme.ts +27 -47
  25. package/src/modes/types.ts +2 -2
  26. package/src/sdk.ts +1 -1
  27. package/src/session/agent-storage.ts +26 -2
  28. package/src/tools/fetch.ts +55 -44
  29. package/src/tools/gemini-image.ts +2 -7
  30. package/src/tools/read.ts +175 -55
  31. package/src/utils/tools-manager.ts +35 -28
  32. package/src/web/scrapers/github.ts +11 -14
  33. package/src/web/scrapers/types.ts +2 -36
  34. package/src/web/scrapers/utils.ts +11 -14
  35. package/src/web/scrapers/youtube.ts +5 -15
  36. package/src/web/search/providers/codex.ts +358 -0
  37. package/src/web/search/providers/gemini.ts +426 -0
  38. package/src/web/search/types.ts +1 -1
  39. package/src/utils/shell.ts +0 -302
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.12.9",
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,22 +79,20 @@
79
79
  "test": "bun test"
80
80
  },
81
81
  "dependencies": {
82
- "@oh-my-pi/omp-stats": "8.12.9",
83
- "@oh-my-pi/pi-agent-core": "8.12.9",
84
- "@oh-my-pi/pi-ai": "8.12.9",
85
- "@oh-my-pi/pi-natives": "8.12.9",
86
- "@oh-my-pi/pi-tui": "8.12.9",
87
- "@oh-my-pi/pi-utils": "8.12.9",
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",
91
91
  "chalk": "^5.6.2",
92
- "cli-highlight": "^2.1.11",
93
92
  "diff": "^8.0.3",
94
93
  "file-type": "^21.3.0",
95
94
  "glob": "^13.0.0",
96
95
  "handlebars": "^4.7.8",
97
- "highlight.js": "^11.11.1",
98
96
  "marked": "^17.0.1",
99
97
  "nanoid": "^5.1.6",
100
98
  "node-html-parser": "^7.0.2",
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { isEnoent, logger } from "@oh-my-pi/pi-utils";
3
+ import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
4
4
  import { YAML } from "bun";
5
5
  import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
6
6
  import { getAgentDbPath, getAgentDir } from "../config";
@@ -461,6 +461,8 @@ export class SettingsManager {
461
461
  private settings!: Settings;
462
462
  private persist: boolean;
463
463
 
464
+ static #lastInstance: SettingsManager | null = null;
465
+
464
466
  /**
465
467
  * Private constructor - use static factory methods instead.
466
468
  * @param storage - SQLite storage instance for auth/cache, or null for in-memory mode
@@ -477,6 +479,7 @@ export class SettingsManager {
477
479
  initialSettings: Settings,
478
480
  persist: boolean,
479
481
  projectSettings: Settings,
482
+ private agentDir: string | null,
480
483
  ) {
481
484
  this.storage = storage;
482
485
  this.configPath = configPath;
@@ -517,6 +520,9 @@ export class SettingsManager {
517
520
  * @returns Configured SettingsManager with merged global and user settings
518
521
  */
519
522
  static async create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): Promise<SettingsManager> {
523
+ cwd = path.normalize(cwd);
524
+ agentDir = path.normalize(agentDir);
525
+
520
526
  const configPath = path.join(agentDir, "config.yml");
521
527
  const storage = await AgentStorage.open(getAgentDbPath(agentDir));
522
528
 
@@ -541,7 +547,9 @@ export class SettingsManager {
541
547
  // Load project settings before construction (constructor is sync)
542
548
  const projectSettings = await SettingsManager.loadProjectSettingsStatic(cwd);
543
549
 
544
- return new SettingsManager(storage, configPath, cwd, globalSettings, true, projectSettings);
550
+ const instance = new SettingsManager(storage, configPath, cwd, globalSettings, true, projectSettings, agentDir);
551
+ SettingsManager.#lastInstance = instance;
552
+ return instance;
545
553
  }
546
554
 
547
555
  /**
@@ -550,7 +558,7 @@ export class SettingsManager {
550
558
  * @returns SettingsManager that won't persist changes to disk
551
559
  */
552
560
  static inMemory(settings: Partial<Settings> = {}): SettingsManager {
553
- return new SettingsManager(null, null, null, settings, false, {});
561
+ return new SettingsManager(null, null, null, settings, false, {}, null);
554
562
  }
555
563
 
556
564
  /**
@@ -1784,4 +1792,54 @@ export class SettingsManager {
1784
1792
  await this.save();
1785
1793
  }
1786
1794
  }
1795
+
1796
+ _compareUniqueCtorKeys(cwd: string, agentDir: string): boolean {
1797
+ if (this.cwd !== cwd) {
1798
+ cwd = path.normalize(cwd);
1799
+ if (this.cwd !== cwd) {
1800
+ return false;
1801
+ }
1802
+ }
1803
+ if (this.agentDir !== agentDir) {
1804
+ agentDir = path.normalize(agentDir);
1805
+ if (this.agentDir !== agentDir) {
1806
+ return false;
1807
+ }
1808
+ }
1809
+ return true;
1810
+ }
1811
+
1812
+ /**
1813
+ * Acquire the last created SettingsManager instance.
1814
+ * If no instance exists, create a new one.
1815
+ * @returns The SettingsManager instance
1816
+ */
1817
+ static acquire(
1818
+ cwd: string = process.cwd(),
1819
+ agentDir: string = getAgentDir(),
1820
+ ): SettingsManager | Promise<SettingsManager> {
1821
+ const prev = SettingsManager.#lastInstance;
1822
+ if (prev?._compareUniqueCtorKeys(cwd, agentDir)) {
1823
+ return prev;
1824
+ }
1825
+ return SettingsManager.create(cwd, agentDir);
1826
+ }
1827
+
1828
+ /**
1829
+ * Gets the shell configuration
1830
+ * @returns The shell configuration
1831
+ */
1832
+ async getShellConfig() {
1833
+ const shell = this.getShellPath();
1834
+ return procmgr.getShellConfig(shell);
1835
+ }
1836
+
1837
+ /**
1838
+ * Gets the shell configuration from the last created SettingsManager instance.
1839
+ * @returns The shell configuration
1840
+ */
1841
+ static async getGlobalShellConfig() {
1842
+ const settings = await SettingsManager.acquire();
1843
+ return settings.getShellConfig();
1844
+ }
1787
1845
  }
@@ -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
+ }