@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
|
@@ -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
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
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:
|
|
138
|
-
|
|
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:
|
|
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
|
-
|
|
865
|
-
|
|
867
|
+
showDebugSelector(): void {
|
|
868
|
+
this.selectorController.showDebugSelector();
|
|
866
869
|
}
|
|
867
870
|
|
|
868
871
|
handleArminSaysHi(): void {
|
package/src/modes/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|