|
1 | 1 | import { execSync } from "node:child_process"; |
2 | | -import { readFileSync } from "node:fs"; |
| 2 | +import { readdirSync, readFileSync } from "node:fs"; |
3 | 3 | import { homedir } from "node:os"; |
4 | 4 | import { join } from "node:path"; |
5 | 5 | import { Codex, type ThreadEvent } from "@openai/codex-sdk"; |
6 | | -import type { AgentEvent, AgentHandle, AgentProvider, ContentBlock, ExecuteOpts, UsageInfo, UsageWindow } from "./types.js"; |
| 6 | +import type { AgentEvent, AgentHandle, AgentProvider, ContentBlock, ExecuteOpts, HistoryEvent, UsageInfo, UsageWindow } from "./types.js"; |
7 | 7 | import { parseRetryAfterMs, UsageFetchError } from "./types.js"; |
8 | 8 |
|
9 | 9 | const AUTH_PATH = join(homedir(), ".codex", "auth.json"); |
| 10 | +const CODEX_SESSIONS_DIR = join(homedir(), ".codex", "sessions"); |
10 | 11 | const USAGE_API = "https://chatgpt.com/backend-api/wham/usage"; |
11 | 12 |
|
12 | 13 | function readAccessToken(): string | null { |
@@ -281,3 +282,98 @@ export const codexProvider: AgentProvider = { |
281 | 282 | return { windows, updated_at: new Date().toISOString() }; |
282 | 283 | }, |
283 | 284 | }; |
| 285 | + |
| 286 | +// ── History from local JSONL ── |
| 287 | + |
| 288 | +function findSessionFile(threadId: string): string | null { |
| 289 | + const suffix = `${threadId}.jsonl`; |
| 290 | + try { |
| 291 | + for (const year of readdirSync(CODEX_SESSIONS_DIR)) { |
| 292 | + const yearDir = join(CODEX_SESSIONS_DIR, year); |
| 293 | + for (const month of readdirSync(yearDir)) { |
| 294 | + const monthDir = join(yearDir, month); |
| 295 | + for (const day of readdirSync(monthDir)) { |
| 296 | + const dayDir = join(monthDir, day); |
| 297 | + for (const file of readdirSync(dayDir)) { |
| 298 | + if (file.endsWith(suffix)) return join(dayDir, file); |
| 299 | + } |
| 300 | + } |
| 301 | + } |
| 302 | + } |
| 303 | + } catch { |
| 304 | + /* dir missing */ |
| 305 | + } |
| 306 | + return null; |
| 307 | +} |
| 308 | + |
| 309 | +function mapResponseItem(payload: Record<string, any>): AgentEvent | null { |
| 310 | + switch (payload.type) { |
| 311 | + case "message": { |
| 312 | + if (payload.role === "assistant") { |
| 313 | + const texts = (payload.content ?? []).filter((c: any) => c.type === "output_text" && c.text).map((c: any) => c.text); |
| 314 | + if (texts.length > 0) { |
| 315 | + return { type: "message", blocks: [{ type: "text", text: texts.join("\n") }] }; |
| 316 | + } |
| 317 | + } |
| 318 | + if (payload.role === "user") { |
| 319 | + const texts = (payload.content ?? []).filter((c: any) => c.type === "input_text" && c.text).map((c: any) => c.text); |
| 320 | + if (texts.length > 0) return { type: "message.user", text: texts.join("\n") }; |
| 321 | + } |
| 322 | + return null; |
| 323 | + } |
| 324 | + case "function_call": { |
| 325 | + let input: Record<string, unknown> = {}; |
| 326 | + if (payload.arguments) { |
| 327 | + try { |
| 328 | + input = JSON.parse(payload.arguments); |
| 329 | + } catch { |
| 330 | + input = { raw: payload.arguments }; |
| 331 | + } |
| 332 | + } |
| 333 | + return { |
| 334 | + type: "message", |
| 335 | + blocks: [{ type: "tool_use", id: payload.call_id ?? `codex-hist-${Date.now()}`, name: payload.name ?? "tool", input }], |
| 336 | + }; |
| 337 | + } |
| 338 | + case "function_call_output": |
| 339 | + return { |
| 340 | + type: "message", |
| 341 | + blocks: [{ type: "tool_result", tool_use_id: payload.call_id ?? "", output: payload.output }], |
| 342 | + }; |
| 343 | + default: |
| 344 | + return null; |
| 345 | + } |
| 346 | +} |
| 347 | + |
| 348 | +/** Read Codex session history from local JSONL files. */ |
| 349 | +export function getCodexHistory(threadId: string): HistoryEvent[] { |
| 350 | + const file = findSessionFile(threadId); |
| 351 | + if (!file) return []; |
| 352 | + |
| 353 | + const lines = readFileSync(file, "utf-8").split("\n"); |
| 354 | + const events: HistoryEvent[] = []; |
| 355 | + let counter = 0; |
| 356 | + |
| 357 | + for (const line of lines) { |
| 358 | + if (!line.trim()) continue; |
| 359 | + let row: any; |
| 360 | + try { |
| 361 | + row = JSON.parse(line); |
| 362 | + } catch { |
| 363 | + continue; |
| 364 | + } |
| 365 | + if (row.type !== "response_item") continue; |
| 366 | + // Skip developer/system messages |
| 367 | + if (row.payload?.role === "developer") continue; |
| 368 | + |
| 369 | + const event = mapResponseItem(row.payload); |
| 370 | + if (event) { |
| 371 | + events.push({ |
| 372 | + id: `codex-hist-${++counter}`, |
| 373 | + event, |
| 374 | + timestamp: row.timestamp ?? new Date().toISOString(), |
| 375 | + }); |
| 376 | + } |
| 377 | + } |
| 378 | + return events; |
| 379 | +} |
0 commit comments