@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/src/lsp/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isEnoent, logger } from "@oh-my-pi/pi-utils";
1
+ import { isEnoent, logger, ptree } from "@oh-my-pi/pi-utils";
2
2
  import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
3
3
  import { applyWorkspaceEdit } from "./edits";
4
4
  import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
@@ -206,7 +206,7 @@ function concatBuffers(a: Uint8Array, b: Uint8Array): Uint8Array {
206
206
  }
207
207
 
208
208
  async function writeMessage(
209
- sink: import("bun").FileSink,
209
+ sink: Bun.FileSink,
210
210
  message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
211
211
  ): Promise<void> {
212
212
  const content = JSON.stringify(message);
@@ -230,7 +230,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
230
230
  if (client.isReading) return;
231
231
  client.isReading = true;
232
232
 
233
- const reader = (client.process.stdout as ReadableStream<Uint8Array>).getReader();
233
+ const reader = (client.proc.stdout as ReadableStream<Uint8Array>).getReader();
234
234
 
235
235
  try {
236
236
  while (true) {
@@ -364,7 +364,7 @@ async function sendResponse(
364
364
  };
365
365
 
366
366
  try {
367
- await writeMessage(client.process.stdin as import("bun").FileSink, response);
367
+ await writeMessage(client.proc.stdin, response);
368
368
  } catch (err) {
369
369
  logger.error("LSP failed to respond.", { method, error: String(err) });
370
370
  }
@@ -409,18 +409,17 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
409
409
  ? await getLspmuxCommand(baseCommand, baseArgs)
410
410
  : { command: baseCommand, args: baseArgs };
411
411
 
412
- const proc = Bun.spawn([command, ...args], {
412
+ const proc = ptree.spawn([command, ...args], {
413
413
  cwd,
414
+ detached: true,
414
415
  stdin: "pipe",
415
- stdout: "pipe",
416
- stderr: "pipe",
417
416
  env: env ? { ...process.env, ...env } : undefined,
418
417
  });
419
418
 
420
419
  const client: LspClient = {
421
420
  name: key,
422
421
  cwd,
423
- process: proc,
422
+ proc,
424
423
  config,
425
424
  requestId: 0,
426
425
  diagnostics: new Map(),
@@ -686,7 +685,7 @@ export function shutdownClient(key: string): void {
686
685
  sendRequest(client, "shutdown", null).catch(() => {});
687
686
 
688
687
  // Kill process
689
- client.process.kill();
688
+ client.proc.kill();
690
689
  clients.delete(key);
691
690
  }
692
691
 
@@ -773,7 +772,7 @@ export async function sendRequest(
773
772
  });
774
773
 
775
774
  // Write request
776
- writeMessage(client.process.stdin as import("bun").FileSink, request).catch(err => {
775
+ writeMessage(client.proc.stdin, request).catch(err => {
777
776
  if (timeout) clearTimeout(timeout);
778
777
  client.pendingRequests.delete(id);
779
778
  cleanup();
@@ -793,26 +792,33 @@ export async function sendNotification(client: LspClient, method: string, params
793
792
  };
794
793
 
795
794
  client.lastActivity = Date.now();
796
- await writeMessage(client.process.stdin as import("bun").FileSink, notification);
795
+ await writeMessage(client.proc.stdin, notification);
797
796
  }
798
797
 
799
798
  /**
800
799
  * Shutdown all LSP clients.
801
800
  */
802
801
  export function shutdownAll(): void {
803
- for (const client of Array.from(clients.values())) {
804
- // Reject all pending requests
805
- for (const pending of Array.from(client.pendingRequests.values())) {
806
- pending.reject(new Error("LSP client shutdown"));
807
- }
808
- client.pendingRequests.clear();
802
+ const clientsToShutdown = Array.from(clients.values());
803
+ clients.clear();
809
804
 
810
- // Send shutdown request (best effort, don't wait)
811
- sendRequest(client, "shutdown", null).catch(() => {});
805
+ const err = new Error("LSP client shutdown");
806
+ for (const client of clientsToShutdown) {
807
+ /// Reject all pending requests
808
+ const reqs = Array.from(client.pendingRequests.values());
809
+ client.pendingRequests.clear();
810
+ for (const pending of reqs) {
811
+ pending.reject(err);
812
+ }
812
813
 
813
- client.process.kill();
814
+ void (async () => {
815
+ // Send shutdown request (best effort, don't wait)
816
+ const timeout = Bun.sleep(5_000);
817
+ const result = sendRequest(client, "shutdown", null).catch(() => {});
818
+ await Promise.race([result, timeout]);
819
+ client.proc.kill();
820
+ })().catch(() => {});
814
821
  }
815
- clients.clear();
816
822
  }
817
823
 
818
824
  /** Status of an LSP server */
package/src/lsp/index.ts CHANGED
@@ -118,6 +118,7 @@ export async function warmupLspServers(cwd: string, options?: LspWarmupOptions):
118
118
  });
119
119
  } else {
120
120
  const errorMsg = result.reason?.message ?? String(result.reason);
121
+ logger.warn("LSP server failed to start", { server: name, error: errorMsg });
121
122
  servers.push({
122
123
  name,
123
124
  status: "error",
package/src/lsp/render.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * - Collapsible/expandable views
9
9
  */
10
10
  import type { RenderResultOptions } from "@oh-my-pi/pi-agent-core";
11
+ import { type HighlightColors, highlightCode as nativeHighlightCode, supportsLanguage } from "@oh-my-pi/pi-natives";
11
12
  import { type Component, Text } from "@oh-my-pi/pi-tui";
12
- import { highlight, supportsLanguage } from "cli-highlight";
13
13
  import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
14
14
  import {
15
15
  formatExpandHint,
@@ -291,29 +291,23 @@ function renderHover(
291
291
  }
292
292
 
293
293
  /**
294
- * Syntax highlight code using highlight.ts.
294
+ * Syntax highlight code using native WASM highlighter.
295
295
  */
296
296
  function highlightCode(codeText: string, language: string, theme: Theme): string[] {
297
297
  const validLang = language && supportsLanguage(language) ? language : undefined;
298
298
  try {
299
- const cliTheme = {
300
- keyword: (s: string) => theme.fg("syntaxKeyword", s),
301
- built_in: (s: string) => theme.fg("syntaxType", s),
302
- literal: (s: string) => theme.fg("syntaxNumber", s),
303
- number: (s: string) => theme.fg("syntaxNumber", s),
304
- string: (s: string) => theme.fg("syntaxString", s),
305
- comment: (s: string) => theme.fg("syntaxComment", s),
306
- function: (s: string) => theme.fg("syntaxFunction", s),
307
- title: (s: string) => theme.fg("syntaxFunction", s),
308
- class: (s: string) => theme.fg("syntaxType", s),
309
- type: (s: string) => theme.fg("syntaxType", s),
310
- attr: (s: string) => theme.fg("syntaxVariable", s),
311
- variable: (s: string) => theme.fg("syntaxVariable", s),
312
- params: (s: string) => theme.fg("syntaxVariable", s),
313
- operator: (s: string) => theme.fg("syntaxOperator", s),
314
- punctuation: (s: string) => theme.fg("syntaxPunctuation", s),
299
+ const colors: HighlightColors = {
300
+ comment: theme.getFgAnsi("syntaxComment"),
301
+ keyword: theme.getFgAnsi("syntaxKeyword"),
302
+ function: theme.getFgAnsi("syntaxFunction"),
303
+ variable: theme.getFgAnsi("syntaxVariable"),
304
+ string: theme.getFgAnsi("syntaxString"),
305
+ number: theme.getFgAnsi("syntaxNumber"),
306
+ type: theme.getFgAnsi("syntaxType"),
307
+ operator: theme.getFgAnsi("syntaxOperator"),
308
+ punctuation: theme.getFgAnsi("syntaxPunctuation"),
315
309
  };
316
- return highlight(codeText, { language: validLang, ignoreIllegals: true, theme: cliTheme }).split("\n");
310
+ return nativeHighlightCode(codeText, validLang, colors).split("\n");
317
311
  } catch {
318
312
  return codeText.split("\n");
319
313
  }
package/src/lsp/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { StringEnum } from "@oh-my-pi/pi-ai";
2
+ import type { ptree } from "@oh-my-pi/pi-utils";
2
3
  import { type Static, Type } from "@sinclair/typebox";
3
- import type { Subprocess } from "bun";
4
4
 
5
5
  // =============================================================================
6
6
  // Tool Schema
@@ -400,7 +400,7 @@ export interface LspClient {
400
400
  name: string;
401
401
  cwd: string;
402
402
  config: ServerConfig;
403
- process: Subprocess;
403
+ proc: ptree.ChildProcess<"pipe">;
404
404
  requestId: number;
405
405
  diagnostics: Map<string, Diagnostic[]>;
406
406
  diagnosticsVersion: number;
package/src/main.ts CHANGED
@@ -87,7 +87,7 @@ async function runInteractiveMode(
87
87
  versionCheckPromise: Promise<string | undefined>,
88
88
  initialMessages: string[],
89
89
  setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
90
- lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined,
90
+ lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }> | undefined,
91
91
  mcpManager: import("./mcp").MCPManager | undefined,
92
92
  initialMessage?: string,
93
93
  initialImages?: ImageContent[],
@@ -5,7 +5,6 @@ import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
5
5
  import { Loader, Markdown, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
6
6
  import { $ } from "bun";
7
7
  import { nanoid } from "nanoid";
8
- import { getDebugLogPath } from "../../config";
9
8
  import { loadCustomShare } from "../../export/custom-share";
10
9
  import type { CompactOptions } from "../../extensibility/extensions/types";
11
10
  import { getGatewayStatus } from "../../ipy/gateway-coordinator";
@@ -266,7 +265,9 @@ export class CommandController {
266
265
  info += `\n${theme.bold("LSP Servers")}\n`;
267
266
  for (const server of this.ctx.lspServers) {
268
267
  const statusColor = server.status === "ready" ? "success" : "error";
269
- info += `${theme.fg("dim", `${server.name}:`)} ${theme.fg(statusColor, server.status)} ${theme.fg("dim", `(${server.fileTypes.join(", ")})`)}\n`;
268
+ const statusText =
269
+ server.status === "error" && server.error ? `${server.status}: ${server.error}` : server.status;
270
+ info += `${theme.fg("dim", `${server.name}:`)} ${theme.fg(statusColor, statusText)} ${theme.fg("dim", `(${server.fileTypes.join(", ")})`)}\n`;
270
271
  }
271
272
  }
272
273
 
@@ -447,46 +448,6 @@ export class CommandController {
447
448
  this.ctx.ui.requestRender();
448
449
  }
449
450
 
450
- async handleDebugCommand(): Promise<void> {
451
- const width = this.ctx.ui.terminal.columns;
452
- const allLines = this.ctx.ui.render(width);
453
-
454
- const debugLogPath = getDebugLogPath();
455
- const debugData = [
456
- `Debug output at ${new Date().toISOString()}`,
457
- `Terminal width: ${width}`,
458
- `Total lines: ${allLines.length}`,
459
- "",
460
- "=== All rendered lines with visible widths ===",
461
- ...allLines.map((line, idx) => {
462
- const vw = visibleWidth(line);
463
- const escaped = JSON.stringify(line);
464
- return `[${idx}] (w=${vw}) ${escaped}`;
465
- }),
466
- "",
467
- "=== Agent messages (JSONL) ===",
468
- ...this.ctx.session.messages.map(msg => JSON.stringify(msg)),
469
- "",
470
- ].join("\n");
471
-
472
- try {
473
- await Bun.write(debugLogPath, debugData);
474
- } catch (error) {
475
- this.ctx.showError(`Failed to write debug log: ${error instanceof Error ? error.message : String(error)}`);
476
- return;
477
- }
478
-
479
- this.ctx.chatContainer.addChild(new Spacer(1));
480
- this.ctx.chatContainer.addChild(
481
- new Text(
482
- `${theme.fg("accent", `${theme.status.success} Debug log written`)}\n${theme.fg("muted", debugLogPath)}`,
483
- 1,
484
- 1,
485
- ),
486
- );
487
- this.ctx.ui.requestRender();
488
- }
489
-
490
451
  handleArminSaysHi(): void {
491
452
  this.ctx.chatContainer.addChild(new Spacer(1));
492
453
  this.ctx.chatContainer.addChild(new ArminComponent(this.ctx.ui));
@@ -64,7 +64,7 @@ export class InputController {
64
64
  this.ctx.editor.onAltP = () => this.ctx.showModelSelector({ temporaryOnly: true });
65
65
 
66
66
  // Global debug handler on TUI (works regardless of focus)
67
- this.ctx.ui.onDebug = () => void this.ctx.handleDebugCommand();
67
+ this.ctx.ui.onDebug = () => this.ctx.showDebugSelector();
68
68
  this.ctx.editor.onCtrlL = () => this.ctx.showModelSelector();
69
69
  this.ctx.editor.onCtrlR = () => this.ctx.showHistorySearch();
70
70
  this.ctx.editor.onCtrlT = () => this.ctx.toggleTodoExpansion();
@@ -293,7 +293,7 @@ export class InputController {
293
293
  return;
294
294
  }
295
295
  if (text === "/debug") {
296
- void this.ctx.handleDebugCommand();
296
+ this.ctx.showDebugSelector();
297
297
  this.ctx.editor.setText("");
298
298
  return;
299
299
  }
@@ -3,6 +3,7 @@ import type { OAuthProvider } from "@oh-my-pi/pi-ai";
3
3
  import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
5
5
  import { getAgentDbPath } from "../../config";
6
+ import { DebugSelectorComponent } from "../../debug";
6
7
  import { disableProvider, enableProvider } from "../../discovery";
7
8
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
8
9
  import { ExtensionDashboard } from "../../modes/components/extensions";
@@ -625,4 +626,11 @@ export class SelectorController {
625
626
  return { component: selector, focus: selector };
626
627
  });
627
628
  }
629
+
630
+ showDebugSelector(): void {
631
+ this.showSelector(done => {
632
+ const selector = new DebugSelectorComponent(this.ctx, done);
633
+ return { component: selector, focus: selector };
634
+ });
635
+ }
628
636
  }
@@ -134,8 +134,9 @@ export class InteractiveMode implements InteractiveModeContext {
134
134
  private planModePreviousTools: string[] | undefined;
135
135
  private planModePreviousModel: Model<any> | undefined;
136
136
  private planModeHasEntered = false;
137
- public readonly lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined =
138
- undefined;
137
+ public readonly lspServers:
138
+ | Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>
139
+ | undefined = undefined;
139
140
  public mcpManager?: import("../mcp").MCPManager;
140
141
  private readonly toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
141
142
 
@@ -151,7 +152,9 @@ export class InteractiveMode implements InteractiveModeContext {
151
152
  version: string,
152
153
  changelogMarkdown: string | undefined = undefined,
153
154
  setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
154
- lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined = undefined,
155
+ lspServers:
156
+ | Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>
157
+ | undefined = undefined,
155
158
  mcpManager?: import("../mcp").MCPManager,
156
159
  ) {
157
160
  this.session = session;
@@ -861,8 +864,8 @@ export class InteractiveMode implements InteractiveModeContext {
861
864
  return this.commandController.handleForkCommand();
862
865
  }
863
866
 
864
- handleDebugCommand(): Promise<void> {
865
- return this.commandController.handleDebugCommand();
867
+ showDebugSelector(): void {
868
+ this.selectorController.showDebugSelector();
866
869
  }
867
870
 
868
871
  handleArminSaysHi(): void {
@@ -524,7 +524,7 @@ export class RpcClient {
524
524
 
525
525
  // Write to stdin after registering the handler
526
526
  const stdin = this.process!.stdin as import("bun").FileSink;
527
- stdin.write(new TextEncoder().encode(`${JSON.stringify(fullCommand)}\n`));
527
+ stdin.write(`${JSON.stringify(fullCommand)}\n`);
528
528
  // flush() returns number | Promise<number> - handle both cases
529
529
  const flushResult = stdin.flush();
530
530
  if (flushResult instanceof Promise) {
@@ -1,11 +1,15 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import {
4
+ type HighlightColors as NativeHighlightColors,
5
+ highlightCode as nativeHighlightCode,
6
+ supportsLanguage as nativeSupportsLanguage,
7
+ } from "@oh-my-pi/pi-natives";
3
8
  import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
4
9
  import { adjustHsv, isEnoent, logger } from "@oh-my-pi/pi-utils";
5
10
  import { type Static, Type } from "@sinclair/typebox";
6
11
  import { TypeCompiler } from "@sinclair/typebox/compiler";
7
12
  import chalk from "chalk";
8
- import { highlight, supportsLanguage } from "cli-highlight";
9
13
  import { getCustomThemesDir } from "../../config";
10
14
  // Embed theme JSON files at build time
11
15
  import darkThemeJson from "./dark.json" with { type: "json" };
@@ -2029,37 +2033,25 @@ export async function getThemeExportColors(themeName?: string): Promise<{
2029
2033
  // TUI Helpers
2030
2034
  // ============================================================================
2031
2035
 
2032
- type CliHighlightTheme = Record<string, (s: string) => string>;
2033
-
2034
- let cachedHighlightThemeFor: Theme | undefined;
2035
- let cachedCliHighlightTheme: CliHighlightTheme | undefined;
2036
-
2037
- function buildCliHighlightTheme(t: Theme): CliHighlightTheme {
2038
- return {
2039
- keyword: (s: string) => t.fg("syntaxKeyword", s),
2040
- built_in: (s: string) => t.fg("syntaxType", s),
2041
- literal: (s: string) => t.fg("syntaxNumber", s),
2042
- number: (s: string) => t.fg("syntaxNumber", s),
2043
- string: (s: string) => t.fg("syntaxString", s),
2044
- comment: (s: string) => t.fg("syntaxComment", s),
2045
- function: (s: string) => t.fg("syntaxFunction", s),
2046
- title: (s: string) => t.fg("syntaxFunction", s),
2047
- class: (s: string) => t.fg("syntaxType", s),
2048
- type: (s: string) => t.fg("syntaxType", s),
2049
- attr: (s: string) => t.fg("syntaxVariable", s),
2050
- variable: (s: string) => t.fg("syntaxVariable", s),
2051
- params: (s: string) => t.fg("syntaxVariable", s),
2052
- operator: (s: string) => t.fg("syntaxOperator", s),
2053
- punctuation: (s: string) => t.fg("syntaxPunctuation", s),
2054
- };
2055
- }
2056
-
2057
- function getCliHighlightTheme(t: Theme): CliHighlightTheme {
2058
- if (cachedHighlightThemeFor !== t || !cachedCliHighlightTheme) {
2059
- cachedHighlightThemeFor = t;
2060
- cachedCliHighlightTheme = buildCliHighlightTheme(t);
2036
+ let cachedHighlightColorsFor: Theme | undefined;
2037
+ let cachedHighlightColors: NativeHighlightColors | undefined;
2038
+
2039
+ function getHighlightColors(t: Theme): NativeHighlightColors {
2040
+ if (cachedHighlightColorsFor !== t || !cachedHighlightColors) {
2041
+ cachedHighlightColorsFor = t;
2042
+ cachedHighlightColors = {
2043
+ comment: t.getFgAnsi("syntaxComment"),
2044
+ keyword: t.getFgAnsi("syntaxKeyword"),
2045
+ function: t.getFgAnsi("syntaxFunction"),
2046
+ variable: t.getFgAnsi("syntaxVariable"),
2047
+ string: t.getFgAnsi("syntaxString"),
2048
+ number: t.getFgAnsi("syntaxNumber"),
2049
+ type: t.getFgAnsi("syntaxType"),
2050
+ operator: t.getFgAnsi("syntaxOperator"),
2051
+ punctuation: t.getFgAnsi("syntaxPunctuation"),
2052
+ };
2061
2053
  }
2062
- return cachedCliHighlightTheme;
2054
+ return cachedHighlightColors;
2063
2055
  }
2064
2056
 
2065
2057
  /**
@@ -2067,15 +2059,9 @@ function getCliHighlightTheme(t: Theme): CliHighlightTheme {
2067
2059
  * Returns array of highlighted lines.
2068
2060
  */
2069
2061
  export function highlightCode(code: string, lang?: string): string[] {
2070
- // Validate language before highlighting to avoid stderr spam from cli-highlight
2071
- const validLang = lang && supportsLanguage(lang) ? lang : undefined;
2072
- const opts = {
2073
- language: validLang,
2074
- ignoreIllegals: true,
2075
- theme: getCliHighlightTheme(theme),
2076
- };
2062
+ const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
2077
2063
  try {
2078
- return highlight(code, opts).split("\n");
2064
+ return nativeHighlightCode(code, validLang, getHighlightColors(theme)).split("\n");
2079
2065
  } catch {
2080
2066
  return code.split("\n");
2081
2067
  }
@@ -2212,15 +2198,9 @@ export function getMarkdownTheme(): MarkdownTheme {
2212
2198
  symbols: getSymbolTheme(),
2213
2199
  getMermaidImage,
2214
2200
  highlightCode: (code: string, lang?: string): string[] => {
2215
- // Validate language before highlighting to avoid stderr spam from cli-highlight
2216
- const validLang = lang && supportsLanguage(lang) ? lang : undefined;
2217
- const opts = {
2218
- language: validLang,
2219
- ignoreIllegals: true,
2220
- theme: getCliHighlightTheme(theme),
2221
- };
2201
+ const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
2222
2202
  try {
2223
- return highlight(code, opts).split("\n");
2203
+ return nativeHighlightCode(code, validLang, getHighlightColors(theme)).split("\n");
2224
2204
  } catch {
2225
2205
  return code.split("\n").map(line => theme.fg("mdCodeBlock", line));
2226
2206
  }
@@ -51,7 +51,7 @@ export interface InteractiveModeContext {
51
51
  agent: AgentSession["agent"];
52
52
  historyStorage?: HistoryStorage;
53
53
  mcpManager?: MCPManager;
54
- lspServers?: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>;
54
+ lspServers?: Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>;
55
55
 
56
56
  // State
57
57
  isInitialized: boolean;
@@ -144,7 +144,6 @@ export interface InteractiveModeContext {
144
144
  handleDumpCommand(): Promise<void>;
145
145
  handleClearCommand(): Promise<void>;
146
146
  handleForkCommand(): Promise<void>;
147
- handleDebugCommand(): Promise<void>;
148
147
  handleArminSaysHi(): void;
149
148
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
150
149
  handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
@@ -164,6 +163,7 @@ export interface InteractiveModeContext {
164
163
  handleResumeSession(sessionPath: string): Promise<void>;
165
164
  showOAuthSelector(mode: "login" | "logout"): Promise<void>;
166
165
  showHookConfirm(title: string, message: string): Promise<boolean>;
166
+ showDebugSelector(): void;
167
167
 
168
168
  // Input handling
169
169
  handleCtrlC(): void;
package/src/sdk.ts CHANGED
@@ -204,7 +204,7 @@ export interface CreateAgentSessionResult {
204
204
  /** Warning if session was restored with a different model than saved */
205
205
  modelFallbackMessage?: string;
206
206
  /** LSP servers that were warmed up at startup */
207
- lspServers?: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>;
207
+ lspServers?: Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>;
208
208
  }
209
209
 
210
210
  // Re-exports
@@ -138,7 +138,18 @@ export class AgentStorage {
138
138
 
139
139
  private constructor(dbPath: string) {
140
140
  this.ensureDir(dbPath);
141
- this.db = new Database(dbPath);
141
+ try {
142
+ this.db = new Database(dbPath);
143
+ } catch (err) {
144
+ const dir = path.dirname(dbPath);
145
+ const dirExists = fs.existsSync(dir);
146
+ const errMsg = err instanceof Error ? err.message : String(err);
147
+ throw new Error(
148
+ `Failed to open agent database at '${dbPath}': ${errMsg}\n` +
149
+ `Directory '${dir}' exists: ${dirExists}\n` +
150
+ `Ensure the directory is writable and not corrupted.`,
151
+ );
152
+ }
142
153
 
143
154
  this.initializeSchema();
144
155
  this.hardenPermissions(dbPath);
@@ -537,7 +548,20 @@ CREATE TABLE settings (
537
548
  * @param dbPath - Path to the database file
538
549
  */
539
550
  private ensureDir(dbPath: string): void {
540
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
551
+ const dir = path.dirname(dbPath);
552
+ try {
553
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
554
+ } catch (err) {
555
+ const code = (err as NodeJS.ErrnoException).code;
556
+ // EEXIST is fine - directory already exists
557
+ if (code !== "EEXIST") {
558
+ throw new Error(`Failed to create agent storage directory '${dir}': ${code || err}`);
559
+ }
560
+ }
561
+ // Verify directory was created
562
+ if (!fs.existsSync(dir)) {
563
+ throw new Error(`Agent storage directory '${dir}' does not exist after creation attempt`);
564
+ }
541
565
  }
542
566
 
543
567
  private hardenPermissions(dbPath: string): void {