@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.
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Debug report bundle creation.
3
+ *
4
+ * Creates a .tar.gz archive with session data, logs, system info, and optional profiling data.
5
+ */
6
+ import * as fs from "node:fs/promises";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+ import { isEnoent } from "@oh-my-pi/pi-utils";
10
+ import type { CpuProfile, HeapSnapshot } from "./profiler";
11
+ import { collectSystemInfo, sanitizeEnv } from "./system-info";
12
+
13
+ /** Reports directory path */
14
+ export function getReportsDir(): string {
15
+ return path.join(os.homedir(), ".omp", "reports");
16
+ }
17
+
18
+ /** Get today's log file path */
19
+ function getLogPath(): string {
20
+ const today = new Date().toISOString().slice(0, 10);
21
+ return path.join(os.homedir(), ".omp", "logs", `omp.${today}.log`);
22
+ }
23
+
24
+ /** Read last N lines from a file */
25
+ async function readLastLines(filePath: string, n: number): Promise<string> {
26
+ try {
27
+ const content = await Bun.file(filePath).text();
28
+ const lines = content.split("\n");
29
+ return lines.slice(-n).join("\n");
30
+ } catch (err) {
31
+ if (isEnoent(err)) return "";
32
+ throw err;
33
+ }
34
+ }
35
+
36
+ export interface ReportBundleOptions {
37
+ /** Session file path */
38
+ sessionFile: string | undefined;
39
+ /** Settings to include */
40
+ settings?: Record<string, unknown>;
41
+ /** CPU profile (for performance reports) */
42
+ cpuProfile?: CpuProfile;
43
+ /** Heap snapshot (for memory reports) */
44
+ heapSnapshot?: HeapSnapshot;
45
+ }
46
+
47
+ export interface ReportBundleResult {
48
+ path: string;
49
+ files: string[];
50
+ }
51
+
52
+ /**
53
+ * Create a debug report bundle.
54
+ *
55
+ * Bundle contents:
56
+ * - session.jsonl: Current session transcript
57
+ * - artifacts/: Session artifacts directory
58
+ * - subagents/: Subagent sessions + artifacts
59
+ * - logs.txt: Recent log entries
60
+ * - system.json: OS, arch, CPU, memory, versions
61
+ * - env.json: Sanitized environment variables
62
+ * - config.json: Resolved settings
63
+ * - profile.cpuprofile: CPU profile (performance report only)
64
+ * - profile.md: Markdown CPU profile (performance report only)
65
+ * - heap.heapsnapshot: Heap snapshot (memory report only)
66
+ */
67
+ export async function createReportBundle(options: ReportBundleOptions): Promise<ReportBundleResult> {
68
+ const reportsDir = getReportsDir();
69
+ await fs.mkdir(reportsDir, { recursive: true });
70
+
71
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
72
+ const outputPath = path.join(reportsDir, `omp-report-${timestamp}.tar.gz`);
73
+
74
+ const data: Record<string, string> = {};
75
+ const files: string[] = [];
76
+
77
+ // Collect system info
78
+ const systemInfo = await collectSystemInfo();
79
+ data["system.json"] = JSON.stringify(systemInfo, null, 2);
80
+ files.push("system.json");
81
+
82
+ // Sanitized environment
83
+ data["env.json"] = JSON.stringify(sanitizeEnv(process.env as Record<string, string>), null, 2);
84
+ files.push("env.json");
85
+
86
+ // Settings/config
87
+ if (options.settings) {
88
+ data["config.json"] = JSON.stringify(options.settings, null, 2);
89
+ files.push("config.json");
90
+ }
91
+
92
+ // Recent logs (last 1000 lines)
93
+ const logPath = getLogPath();
94
+ const logs = await readLastLines(logPath, 1000);
95
+ if (logs) {
96
+ data["logs.txt"] = logs;
97
+ files.push("logs.txt");
98
+ }
99
+
100
+ // Session file
101
+ if (options.sessionFile) {
102
+ try {
103
+ const sessionContent = await Bun.file(options.sessionFile).text();
104
+ data["session.jsonl"] = sessionContent;
105
+ files.push("session.jsonl");
106
+ } catch {
107
+ // Session file might not exist yet
108
+ }
109
+
110
+ // Artifacts directory (same path without .jsonl)
111
+ const artifactsDir = options.sessionFile.slice(0, -6);
112
+ await addDirectoryToArchive(data, files, artifactsDir, "artifacts");
113
+
114
+ // Look for subagent sessions in the same directory
115
+ const sessionDir = path.dirname(options.sessionFile);
116
+ const sessionBasename = path.basename(options.sessionFile, ".jsonl");
117
+ await addSubagentSessions(data, files, sessionDir, sessionBasename);
118
+ }
119
+
120
+ // CPU profile
121
+ if (options.cpuProfile) {
122
+ data["profile.cpuprofile"] = options.cpuProfile.data;
123
+ files.push("profile.cpuprofile");
124
+ data["profile.md"] = options.cpuProfile.markdown;
125
+ files.push("profile.md");
126
+ }
127
+
128
+ // Heap snapshot
129
+ if (options.heapSnapshot) {
130
+ data["heap.heapsnapshot"] = options.heapSnapshot.data;
131
+ files.push("heap.heapsnapshot");
132
+ }
133
+
134
+ // Write archive
135
+ await Bun.Archive.write(outputPath, data, { compress: "gzip" });
136
+
137
+ return { path: outputPath, files };
138
+ }
139
+
140
+ /** Add all files from a directory to the archive */
141
+ async function addDirectoryToArchive(
142
+ data: Record<string, string>,
143
+ files: string[],
144
+ dirPath: string,
145
+ archivePrefix: string,
146
+ ): Promise<void> {
147
+ try {
148
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
149
+ for (const entry of entries) {
150
+ if (!entry.isFile()) continue;
151
+ const filePath = path.join(dirPath, entry.name);
152
+ const archivePath = `${archivePrefix}/${entry.name}`;
153
+ try {
154
+ const content = await Bun.file(filePath).text();
155
+ data[archivePath] = content;
156
+ files.push(archivePath);
157
+ } catch {
158
+ // Skip files we can't read
159
+ }
160
+ }
161
+ } catch {
162
+ // Directory doesn't exist
163
+ }
164
+ }
165
+
166
+ /** Find and add subagent session files */
167
+ async function addSubagentSessions(
168
+ data: Record<string, string>,
169
+ files: string[],
170
+ sessionDir: string,
171
+ parentBasename: string,
172
+ ): Promise<void> {
173
+ // Subagent sessions are named with task IDs in the same directory
174
+ // They follow the pattern: {timestamp}_{sessionId}.jsonl
175
+ // We look for any sessions created after the parent session
176
+ try {
177
+ const entries = await fs.readdir(sessionDir, { withFileTypes: true });
178
+ const sessionFiles = entries
179
+ .filter(e => e.isFile() && e.name.endsWith(".jsonl") && e.name !== `${parentBasename}.jsonl`)
180
+ .map(e => e.name);
181
+
182
+ // Limit to most recent 10 subagent sessions
183
+ const sortedFiles = sessionFiles.sort().slice(-10);
184
+
185
+ for (const filename of sortedFiles) {
186
+ const filePath = path.join(sessionDir, filename);
187
+ const archivePath = `subagents/${filename}`;
188
+ try {
189
+ const content = await Bun.file(filePath).text();
190
+ data[archivePath] = content;
191
+ files.push(archivePath);
192
+
193
+ // Also add artifacts for this subagent session
194
+ const artifactsDir = filePath.slice(0, -6);
195
+ await addDirectoryToArchive(data, files, artifactsDir, `subagents/${filename.slice(0, -6)}`);
196
+ } catch {
197
+ // Skip files we can't read
198
+ }
199
+ }
200
+ } catch {
201
+ // Directory doesn't exist
202
+ }
203
+ }
204
+
205
+ /** Get recent log entries for display */
206
+ export async function getRecentLogs(lines: number): Promise<string> {
207
+ const logPath = getLogPath();
208
+ return readLastLines(logPath, lines);
209
+ }
210
+
211
+ /** Calculate total size of artifact cache */
212
+ export async function getArtifactCacheStats(
213
+ sessionsDir: string,
214
+ ): Promise<{ count: number; totalSize: number; oldestDate: Date | null }> {
215
+ let count = 0;
216
+ let totalSize = 0;
217
+ let oldestDate: Date | null = null;
218
+
219
+ try {
220
+ const sessions = await fs.readdir(sessionsDir, { withFileTypes: true });
221
+
222
+ for (const session of sessions) {
223
+ // Artifact directories don't have .jsonl extension
224
+ if (session.isDirectory()) {
225
+ const dirPath = path.join(sessionsDir, session.name);
226
+ try {
227
+ const stat = await fs.stat(dirPath);
228
+ const files = await fs.readdir(dirPath);
229
+ for (const file of files) {
230
+ const filePath = path.join(dirPath, file);
231
+ const fileStat = await fs.stat(filePath);
232
+ if (fileStat.isFile()) {
233
+ count++;
234
+ totalSize += fileStat.size;
235
+ }
236
+ }
237
+ if (!oldestDate || stat.mtime < oldestDate) {
238
+ oldestDate = stat.mtime;
239
+ }
240
+ } catch {
241
+ // Skip inaccessible directories
242
+ }
243
+ }
244
+ }
245
+ } catch {
246
+ // Directory doesn't exist
247
+ }
248
+
249
+ return { count, totalSize, oldestDate };
250
+ }
251
+
252
+ /** Clear artifact cache older than N days */
253
+ export async function clearArtifactCache(sessionsDir: string, daysOld: number = 30): Promise<{ removed: number }> {
254
+ const cutoff = new Date();
255
+ cutoff.setDate(cutoff.getDate() - daysOld);
256
+ let removed = 0;
257
+
258
+ try {
259
+ const sessions = await fs.readdir(sessionsDir, { withFileTypes: true });
260
+
261
+ for (const session of sessions) {
262
+ if (session.isDirectory()) {
263
+ const dirPath = path.join(sessionsDir, session.name);
264
+ try {
265
+ const stat = await fs.stat(dirPath);
266
+ if (stat.mtime < cutoff) {
267
+ await fs.rm(dirPath, { recursive: true, force: true });
268
+ removed++;
269
+ }
270
+ } catch {
271
+ // Skip inaccessible directories
272
+ }
273
+ }
274
+ }
275
+ } catch {
276
+ // Directory doesn't exist
277
+ }
278
+
279
+ return { removed };
280
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * System information collection for debug reports.
3
+ */
4
+ import * as os from "node:os";
5
+ import { VERSION } from "../config";
6
+
7
+ export interface SystemInfo {
8
+ os: string;
9
+ arch: string;
10
+ cpu: string;
11
+ memory: {
12
+ total: number;
13
+ free: number;
14
+ };
15
+ versions: {
16
+ app: string;
17
+ bun: string;
18
+ node: string;
19
+ };
20
+ cwd: string;
21
+ shell: string;
22
+ terminal: string | undefined;
23
+ }
24
+
25
+ /** Collect system information */
26
+ export async function collectSystemInfo(): Promise<SystemInfo> {
27
+ const cpus = os.cpus();
28
+ const cpuModel = cpus[0]?.model ?? "Unknown CPU";
29
+
30
+ // Try to get shell from environment
31
+ const shell = process.env.SHELL ?? process.env.ComSpec ?? "unknown";
32
+ const terminal = process.env.TERM_PROGRAM ?? process.env.TERM ?? undefined;
33
+
34
+ return {
35
+ os: `${os.type()} ${os.release()} (${os.platform()})`,
36
+ arch: os.arch(),
37
+ cpu: cpuModel,
38
+ memory: {
39
+ total: os.totalmem(),
40
+ free: os.freemem(),
41
+ },
42
+ versions: {
43
+ app: VERSION,
44
+ bun: Bun.version,
45
+ node: process.version,
46
+ },
47
+ cwd: process.cwd(),
48
+ shell,
49
+ terminal,
50
+ };
51
+ }
52
+
53
+ /** Format bytes to human-readable string */
54
+ function formatBytes(bytes: number): string {
55
+ const gb = bytes / (1024 * 1024 * 1024);
56
+ return `${gb.toFixed(1)} GB`;
57
+ }
58
+
59
+ /** Format system info for display */
60
+ export function formatSystemInfo(info: SystemInfo): string {
61
+ const lines = [
62
+ "System Information",
63
+ "━━━━━━━━━━━━━━━━━━",
64
+ `OS: ${info.os}`,
65
+ `Arch: ${info.arch}`,
66
+ `CPU: ${info.cpu}`,
67
+ `Memory: ${formatBytes(info.memory.total)} (${formatBytes(info.memory.free)} free)`,
68
+ `Bun: ${info.versions.bun}`,
69
+ `App: omp ${info.versions.app}`,
70
+ `Node: ${info.versions.node} (compat)`,
71
+ `CWD: ${info.cwd}`,
72
+ `Shell: ${info.shell}`,
73
+ ];
74
+ if (info.terminal) {
75
+ lines.push(`Terminal: ${info.terminal}`);
76
+ }
77
+ return lines.join("\n");
78
+ }
79
+
80
+ /** Sanitize environment variables by redacting sensitive values */
81
+ export function sanitizeEnv(env: Record<string, string | undefined>): Record<string, string> {
82
+ const SENSITIVE_PATTERNS = [/key/i, /secret/i, /token/i, /pass/i, /auth/i, /credential/i, /api/i, /private/i];
83
+
84
+ const result: Record<string, string> = {};
85
+ for (const [k, v] of Object.entries(env)) {
86
+ if (v === undefined) continue;
87
+ const isSensitive = SENSITIVE_PATTERNS.some(p => p.test(k));
88
+ result[k] = isSensitive ? "[REDACTED]" : v;
89
+ }
90
+ return result;
91
+ }
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/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 {
@@ -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 {