|
#!/usr/bin/env bun |
|
/** |
|
* Convert session JSONL between Claude Code and Codex CLI formats. |
|
* |
|
* Usage: |
|
* bun c2c.ts claude2codex <input.jsonl> [output.jsonl] |
|
* bun c2c.ts codex2claude <input.jsonl> [output.jsonl] |
|
* bun c2c.ts claude2codex # interactive session picker |
|
* bun c2c.ts codex2claude # interactive session picker |
|
*/ |
|
|
|
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from "fs"; |
|
import { join, basename } from "path"; |
|
import { homedir } from "os"; |
|
|
|
// ── Helpers ────────────────────────────────────────────────────────── |
|
|
|
function parseJsonl(text: string): any[] { |
|
return text.split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l)); |
|
} |
|
|
|
function toJsonl(records: any[]): string { |
|
return records.map((r) => JSON.stringify(r)).join("\n") + "\n"; |
|
} |
|
|
|
function asContentArray(content: any): any[] { |
|
if (Array.isArray(content)) return content; |
|
if (content == null) return []; |
|
return [content]; |
|
} |
|
|
|
let callIdCounter = 0; |
|
const nextCallId = () => `call_conv_${(++callIdCounter).toString(36).padStart(6, "0")}`; |
|
|
|
let toolUseCounter = 0; |
|
const nextToolUseId = () => `toolu_conv_${(++toolUseCounter).toString(36).padStart(6, "0")}`; |
|
|
|
// ── Session discovery ─────────────────────────────────────────────── |
|
|
|
interface SessionInfo { |
|
path: string; |
|
id: string; |
|
timestamp: Date; |
|
cwd: string; |
|
model: string; |
|
firstMessage: string; |
|
size: number; |
|
} |
|
|
|
function listCodexSessionPaths(): string[] { |
|
const base = join(homedir(), ".codex", "sessions"); |
|
if (!existsSync(base)) return []; |
|
const files: string[] = []; |
|
|
|
function walk(dir: string) { |
|
for (const entry of readdirSync(dir)) { |
|
const full = join(dir, entry); |
|
try { |
|
const stat = statSync(full); |
|
if (stat.isDirectory()) { |
|
walk(full); |
|
} else if (entry.endsWith(".jsonl")) { |
|
files.push(full); |
|
} |
|
} catch {} |
|
} |
|
} |
|
|
|
walk(base); |
|
return files.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs); |
|
} |
|
|
|
function findClaudeSessions(): SessionInfo[] { |
|
const base = join(homedir(), ".claude", "projects"); |
|
if (!existsSync(base)) return []; |
|
const sessions: SessionInfo[] = []; |
|
|
|
for (const project of readdirSync(base)) { |
|
const projectDir = join(base, project); |
|
try { |
|
if (!statSync(projectDir).isDirectory()) continue; |
|
} catch { continue; } |
|
|
|
for (const file of readdirSync(projectDir)) { |
|
if (!file.endsWith(".jsonl")) continue; |
|
const fullPath = join(projectDir, file); |
|
const stat = statSync(fullPath); |
|
try { |
|
const firstLines = readFileSync(fullPath, "utf-8").split("\n").slice(0, 20); |
|
let id = file.replace(".jsonl", ""); |
|
let cwd = project.replace(/-/g, "/").replace(/^\//, ""); |
|
let model = ""; |
|
let firstMessage = ""; |
|
let timestamp = stat.mtime; |
|
|
|
for (const line of firstLines) { |
|
if (!line.trim()) continue; |
|
const obj = JSON.parse(line); |
|
if (obj.type === "user" && obj.message?.content) { |
|
if (!firstMessage) { |
|
for (const b of obj.message.content) { |
|
const text = typeof b === "string" ? b : b?.text; |
|
if (text && text.length > 5 && !text.startsWith("[Time:") && !text.startsWith("[Request") && !text.startsWith("<") && !text.startsWith("#")) { |
|
firstMessage = text.slice(0, 80).replace(/\n/g, " "); |
|
break; |
|
} |
|
} |
|
} |
|
if (obj.cwd) cwd = obj.cwd; |
|
if (obj.sessionId) id = obj.sessionId; |
|
if (obj.timestamp) timestamp = new Date(obj.timestamp); |
|
} |
|
if (obj.type === "assistant" && obj.message?.model) { |
|
model = obj.message.model; |
|
} |
|
} |
|
sessions.push({ path: fullPath, id, timestamp, cwd, model, firstMessage, size: stat.size }); |
|
} catch { continue; } |
|
} |
|
} |
|
return sessions.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); |
|
} |
|
|
|
function findCodexSessions(): SessionInfo[] { |
|
const sessions: SessionInfo[] = []; |
|
|
|
for (const full of listCodexSessionPaths()) { |
|
try { |
|
const stat = statSync(full); |
|
const firstLines = readFileSync(full, "utf-8").split("\n").slice(0, 20); |
|
let id = ""; |
|
let cwd = ""; |
|
let model = ""; |
|
let firstMessage = ""; |
|
let timestamp = stat.mtime; |
|
|
|
for (const line of firstLines) { |
|
if (!line.trim()) continue; |
|
const obj = JSON.parse(line); |
|
if (obj.type === "session_meta") { |
|
id = obj.payload?.id ?? basename(full, ".jsonl"); |
|
cwd = obj.payload?.cwd ?? ""; |
|
model = obj.payload?.model ?? obj.payload?.model_provider ?? ""; |
|
timestamp = new Date(obj.payload?.timestamp ?? obj.timestamp); |
|
} |
|
if (obj.type === "response_item" && obj.payload?.role === "user" && obj.payload?.type === "message") { |
|
for (const c of obj.payload?.content ?? []) { |
|
if ((c.type === "input_text") && c.text?.length > 5 && !c.text.startsWith("#") && !c.text.startsWith("<")) { |
|
if (!firstMessage) firstMessage = c.text.slice(0, 80).replace(/\n/g, " "); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
if (!id) id = basename(full, ".jsonl"); |
|
sessions.push({ path: full, id, timestamp, cwd, model, firstMessage, size: stat.size }); |
|
} catch { continue; } |
|
} |
|
return sessions.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); |
|
} |
|
|
|
function buildCodexScaffold(sessionId: string, ts: string, cwd: string) { |
|
const templatePath = listCodexSessionPaths().find((p) => { |
|
try { |
|
const records = parseJsonl(readFileSync(p, "utf-8")).slice(0, 8); |
|
return records.some((r) => r.type === "response_item" && r.payload?.type === "message" && r.payload?.role === "developer") |
|
&& records.some((r) => r.type === "turn_context"); |
|
} catch { |
|
return false; |
|
} |
|
}); |
|
const turnId = crypto.randomUUID(); |
|
const out: any[] = []; |
|
|
|
if (!templatePath) { |
|
out.push({ |
|
timestamp: ts, |
|
type: "session_meta", |
|
payload: { |
|
id: sessionId, |
|
timestamp: ts, |
|
cwd, |
|
originator: "codex_cli_rs", |
|
cli_version: "0.114.0", |
|
source: "cli", |
|
model_provider: "openai", |
|
}, |
|
}); |
|
out.push({ |
|
timestamp: ts, |
|
type: "turn_context", |
|
payload: { |
|
turn_id: turnId, |
|
cwd, |
|
current_date: ts.slice(0, 10), |
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, |
|
approval_policy: "on-request", |
|
sandbox_policy: { |
|
type: "workspace-write", |
|
writable_roots: [join(homedir(), ".codex", "memories")], |
|
network_access: false, |
|
exclude_tmpdir_env_var: false, |
|
exclude_slash_tmp: false, |
|
}, |
|
model: "gpt-5.4", |
|
personality: "pragmatic", |
|
collaboration_mode: { mode: "default", settings: { model: "gpt-5.4", reasoning_effort: "high", developer_instructions: null } }, |
|
realtime_active: false, |
|
effort: "high", |
|
summary: "none", |
|
}, |
|
}); |
|
return out; |
|
} |
|
|
|
const template = parseJsonl(readFileSync(templatePath, "utf-8")).slice(0, 8); |
|
for (const rec of template) { |
|
const cloned = JSON.parse(JSON.stringify(rec)); |
|
cloned.timestamp = ts; |
|
|
|
if (cloned.type === "session_meta") { |
|
cloned.payload.id = sessionId; |
|
cloned.payload.timestamp = ts; |
|
cloned.payload.cwd = cwd; |
|
} |
|
|
|
if (cloned.type === "event_msg" && cloned.payload?.turn_id) { |
|
cloned.payload.turn_id = turnId; |
|
} |
|
|
|
if (cloned.type === "turn_context") { |
|
cloned.payload.turn_id = turnId; |
|
cloned.payload.cwd = cwd; |
|
cloned.payload.current_date = ts.slice(0, 10); |
|
cloned.payload.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; |
|
if (cloned.payload.collaboration_mode?.settings) { |
|
cloned.payload.collaboration_mode.settings.developer_instructions = |
|
cloned.payload.collaboration_mode.settings.developer_instructions ?? null; |
|
} |
|
} |
|
|
|
if (cloned.type === "response_item" && cloned.payload?.type === "message" && cloned.payload?.role === "user") { |
|
continue; |
|
} |
|
if (cloned.type === "event_msg" && cloned.payload?.type === "user_message") { |
|
continue; |
|
} |
|
|
|
out.push(cloned); |
|
} |
|
|
|
return out; |
|
} |
|
|
|
function formatSize(bytes: number): string { |
|
if (bytes < 1024) return `${bytes}B`; |
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}K`; |
|
return `${(bytes / (1024 * 1024)).toFixed(1)}M`; |
|
} |
|
|
|
function formatDate(d: Date): string { |
|
return d.toISOString().slice(0, 16).replace("T", " "); |
|
} |
|
|
|
async function pickSession(sessions: SessionInfo[]): Promise<SessionInfo | null> { |
|
if (sessions.length === 0) { |
|
console.error("No sessions found."); |
|
return null; |
|
} |
|
|
|
const show = sessions.slice(0, 20); |
|
console.error("\nAvailable sessions:\n"); |
|
for (let i = 0; i < show.length; i++) { |
|
const s = show[i]; |
|
const date = formatDate(s.timestamp); |
|
const size = formatSize(s.size).padStart(6); |
|
const msg = s.firstMessage || "(no message)"; |
|
const cwdShort = s.cwd.replace(homedir(), "~").split("/").slice(-2).join("/"); |
|
console.error(` ${String(i + 1).padStart(3)}) ${date} ${size} ${cwdShort.padEnd(30).slice(0, 30)} ${msg.slice(0, 50)}`); |
|
} |
|
if (sessions.length > 20) console.error(` ... and ${sessions.length - 20} more`); |
|
|
|
process.stderr.write("\nSelect session number: "); |
|
const input = await new Promise<string>((resolve) => { |
|
let buf = ""; |
|
process.stdin.setEncoding("utf-8"); |
|
process.stdin.once("data", (data) => { buf += data; resolve(buf.trim()); }); |
|
process.stdin.resume(); |
|
}); |
|
|
|
const idx = parseInt(input, 10) - 1; |
|
if (isNaN(idx) || idx < 0 || idx >= show.length) { |
|
console.error("Invalid selection."); |
|
return null; |
|
} |
|
return show[idx]; |
|
} |
|
|
|
// ── Claude → Codex ────────────────────────────────────────────────── |
|
|
|
function claude2codex(records: any[]): any[] { |
|
const out: any[] = []; |
|
const callIdMap = new Map<string, string>(); |
|
|
|
const firstMsg = records.find((r) => r.type === "user" || r.type === "assistant"); |
|
const sessionId = firstMsg?.sessionId ?? crypto.randomUUID(); |
|
const ts0 = firstMsg?.timestamp ?? new Date().toISOString(); |
|
out.push(...buildCodexScaffold(sessionId, ts0, firstMsg?.cwd ?? process.cwd())); |
|
|
|
for (const rec of records) { |
|
if (rec.type === "queue-operation" || !rec.message) continue; |
|
const ts = rec.timestamp ?? new Date().toISOString(); |
|
const content = asContentArray(rec.message.content); |
|
|
|
if (rec.type === "user") { |
|
const role = rec.__originalRole === "developer" ? "developer" : "user"; |
|
const customToolResultIds = new Set(rec.__customToolResultIds ?? []); |
|
const textParts: any[] = []; |
|
for (const block of content) { |
|
if (typeof block === "string") { |
|
textParts.push({ type: "input_text", text: block }); |
|
} else if (block.type === "text") { |
|
textParts.push({ type: "input_text", text: block.text }); |
|
} else if (block.type === "tool_result") { |
|
if (textParts.length > 0) { |
|
out.push({ timestamp: ts, type: "response_item", payload: { type: "message", role, content: [...textParts] } }); |
|
textParts.length = 0; |
|
} |
|
const callId = callIdMap.get(block.tool_use_id) ?? block.tool_use_id; |
|
const isCustom = customToolResultIds.has(block.tool_use_id); |
|
out.push({ |
|
timestamp: ts, |
|
type: "response_item", |
|
payload: { |
|
type: isCustom ? "custom_tool_call_output" : "function_call_output", |
|
call_id: callId, |
|
output: serializeToolResultContent(block.content), |
|
}, |
|
}); |
|
} |
|
} |
|
if (textParts.length > 0) { |
|
out.push({ timestamp: ts, type: "response_item", payload: { type: "message", role, content: textParts } }); |
|
} |
|
} else if (rec.type === "assistant") { |
|
const customToolUseIds = new Set(rec.__customToolUseIds ?? []); |
|
const textBuf: any[] = []; |
|
const flushText = () => { |
|
if (textBuf.length > 0) { |
|
out.push({ timestamp: ts, type: "response_item", payload: { type: "message", role: "assistant", content: [...textBuf] } }); |
|
textBuf.length = 0; |
|
} |
|
}; |
|
|
|
for (const block of content) { |
|
if (typeof block === "string") { |
|
textBuf.push({ type: "output_text", text: block }); |
|
continue; |
|
} |
|
if (block.type === "text") { |
|
textBuf.push({ type: "output_text", text: block.text }); |
|
} else if (block.type === "thinking") { |
|
flushText(); |
|
out.push({ timestamp: ts, type: "response_item", payload: { type: "reasoning", content: block.thinking, summary: [] } }); |
|
} else if (block.type === "tool_use") { |
|
flushText(); |
|
const callId = nextCallId(); |
|
callIdMap.set(block.id, callId); |
|
const isCustom = customToolUseIds.has(block.id); |
|
const mappedInput = isCustom ? unwrapCustomToolInput(block.input) : JSON.stringify(block.input); |
|
out.push({ |
|
timestamp: ts, |
|
type: "response_item", |
|
payload: { |
|
type: isCustom ? "custom_tool_call" : "function_call", |
|
name: block.name, |
|
[isCustom ? "input" : "arguments"]: mappedInput, |
|
call_id: callId, |
|
}, |
|
}); |
|
} |
|
} |
|
flushText(); |
|
} |
|
} |
|
return out; |
|
} |
|
|
|
function serializeToolResultContent(content: any): string { |
|
if (typeof content === "string") return content; |
|
if (Array.isArray(content)) return JSON.stringify({ __structured: true, blocks: content }); |
|
return JSON.stringify(content); |
|
} |
|
|
|
function deserializeToolResultContent(output: string): any { |
|
try { |
|
const parsed = JSON.parse(output); |
|
if (parsed?.__structured && Array.isArray(parsed.blocks)) return parsed.blocks; |
|
return output; |
|
} catch { |
|
return output; |
|
} |
|
} |
|
|
|
function wrapCustomToolInput(input: any): Record<string, any> { |
|
if (input && typeof input === "object" && !Array.isArray(input)) { |
|
return { ...input, __codex_custom_input_wrapped: false }; |
|
} |
|
return { |
|
__codex_custom_input_wrapped: true, |
|
raw_input: typeof input === "string" ? input : JSON.stringify(input), |
|
}; |
|
} |
|
|
|
function unwrapCustomToolInput(input: any): any { |
|
if (!input || typeof input !== "object" || Array.isArray(input)) return input; |
|
if (input.__codex_custom_input_wrapped) { |
|
return input.raw_input ?? ""; |
|
} |
|
const { __codex_custom_input_wrapped, ...rest } = input; |
|
return rest; |
|
} |
|
|
|
// ── Codex → Claude ────────────────────────────────────────────────── |
|
|
|
function codex2claude(records: any[]): any[] { |
|
const out: any[] = []; |
|
const toolIdMap = new Map<string, string>(); |
|
|
|
const meta = records.find((r) => r.type === "session_meta"); |
|
const sessionId = meta?.payload?.id ?? crypto.randomUUID(); |
|
const cwd = meta?.payload?.cwd ?? process.cwd(); |
|
const model = meta?.payload?.model ?? "unknown"; |
|
|
|
let prevUuid: string | null = null; |
|
|
|
function pushClaude(type: "user" | "assistant", content: any[], ts: string, extra: Record<string, any> = {}) { |
|
const uuid = crypto.randomUUID(); |
|
out.push({ |
|
type, |
|
message: { role: type, content, ...(type === "assistant" ? { model } : {}) }, |
|
uuid, |
|
parentUuid: prevUuid, |
|
sessionId, |
|
cwd, |
|
timestamp: ts, |
|
...extra, |
|
}); |
|
prevUuid = uuid; |
|
} |
|
|
|
let assistantContent: any[] = []; |
|
let assistantCustomToolUseIds: string[] = []; |
|
let pendingToolResults: any[] = []; |
|
let pendingCustomToolResultIds: string[] = []; |
|
let lastTs = new Date().toISOString(); |
|
|
|
function flushAssistant() { |
|
if (assistantContent.length > 0) { |
|
pushClaude( |
|
"assistant", |
|
assistantContent, |
|
lastTs, |
|
assistantCustomToolUseIds.length > 0 ? { __customToolUseIds: assistantCustomToolUseIds } : {}, |
|
); |
|
assistantContent = []; |
|
assistantCustomToolUseIds = []; |
|
} |
|
} |
|
|
|
function flushToolResults() { |
|
if (pendingToolResults.length > 0) { |
|
for (let i = 0; i < pendingToolResults.length; i++) { |
|
const block = pendingToolResults[i]; |
|
const toolUseId = block?.tool_use_id; |
|
const extra = |
|
pendingCustomToolResultIds.includes(toolUseId) |
|
? { __customToolResultIds: [toolUseId] } |
|
: {}; |
|
pushClaude("user", [block], lastTs, extra); |
|
} |
|
pendingToolResults = []; |
|
pendingCustomToolResultIds = []; |
|
} |
|
} |
|
|
|
for (const rec of records) { |
|
const ts = rec.timestamp ?? lastTs; |
|
lastTs = ts; |
|
const p = rec.payload; |
|
if (!p) continue; |
|
|
|
if (rec.type === "session_meta" || rec.type === "turn_context" || rec.type === "event_msg" || rec.type === "compacted") continue; |
|
if (rec.type !== "response_item") continue; |
|
|
|
if (p.type === "message") { |
|
if (p.role === "user" || p.role === "developer") { |
|
flushAssistant(); |
|
flushToolResults(); |
|
const content = (p.content ?? []).map((c: any) => |
|
c.type === "input_text" ? { type: "text", text: c.text } : c |
|
); |
|
pushClaude("user", content, ts, p.role === "developer" ? { __originalRole: "developer" } : {}); |
|
} else if (p.role === "assistant") { |
|
flushToolResults(); |
|
for (const c of p.content ?? []) { |
|
if (c.type === "output_text") assistantContent.push({ type: "text", text: c.text }); |
|
} |
|
} |
|
} else if (p.type === "reasoning") { |
|
const summaryText = (p.summary ?? []).map((s: any) => s.text).filter(Boolean).join("\n"); |
|
const reasoningText = p.content ?? summaryText; |
|
if (reasoningText) { |
|
assistantContent.push({ type: "text", text: `[Codex reasoning]\n${reasoningText}` }); |
|
} else if (p.encrypted_content) { |
|
assistantContent.push({ type: "text", text: "[Codex reasoning omitted: encrypted]" }); |
|
} |
|
} else if (p.type === "function_call" || p.type === "custom_tool_call") { |
|
flushToolResults(); |
|
const toolId = nextToolUseId(); |
|
toolIdMap.set(p.call_id, toolId); |
|
const isCustom = p.type === "custom_tool_call"; |
|
let input: any; |
|
if (isCustom) { input = wrapCustomToolInput(p.input); } |
|
else { try { input = JSON.parse(p.arguments); } catch { input = { raw: p.arguments }; } } |
|
assistantContent.push({ type: "tool_use", id: toolId, name: p.name, input }); |
|
if (isCustom) assistantCustomToolUseIds.push(toolId); |
|
} else if (p.type === "function_call_output" || p.type === "custom_tool_call_output") { |
|
flushAssistant(); |
|
const toolId = toolIdMap.get(p.call_id) ?? p.call_id; |
|
const isCustom = p.type === "custom_tool_call_output"; |
|
pendingToolResults.push({ |
|
type: "tool_result", tool_use_id: toolId, |
|
content: deserializeToolResultContent(p.output), |
|
}); |
|
if (isCustom) pendingCustomToolResultIds.push(toolId); |
|
} |
|
} |
|
|
|
flushAssistant(); |
|
flushToolResults(); |
|
return out; |
|
} |
|
|
|
// ── CLI ────────────────────────────────────────────────────────────── |
|
|
|
async function main() { |
|
const [direction, inputFile, outputFile] = process.argv.slice(2); |
|
|
|
if (!direction) { |
|
console.error(`Usage: |
|
bun c2c.ts claude2codex [input.jsonl] [output.jsonl] |
|
bun c2c.ts codex2claude [input.jsonl] [output.jsonl] |
|
|
|
Without input file — interactive session picker.`); |
|
process.exit(1); |
|
} |
|
|
|
if (direction !== "claude2codex" && direction !== "codex2claude") { |
|
console.error(`Unknown direction: ${direction}. Use "claude2codex" or "codex2claude".`); |
|
process.exit(1); |
|
} |
|
|
|
let srcPath: string; |
|
let dstPath: string | undefined = outputFile; |
|
|
|
if (inputFile) { |
|
srcPath = inputFile; |
|
} else { |
|
// Interactive picker |
|
const sessions = direction === "claude2codex" ? findClaudeSessions() : findCodexSessions(); |
|
const picked = await pickSession(sessions); |
|
if (!picked) process.exit(1); |
|
srcPath = picked.path; |
|
console.error(`\nSelected: ${picked.path}`); |
|
|
|
// Generate output path |
|
if (direction === "claude2codex") { |
|
const now = picked.timestamp; |
|
const y = now.getFullYear(); |
|
const m = String(now.getMonth() + 1).padStart(2, "0"); |
|
const d = String(now.getDate()).padStart(2, "0"); |
|
const ts = now.toISOString().replace(/[:.]/g, "-").slice(0, 19); |
|
const outDir = join(homedir(), ".codex", "sessions", String(y), m, d); |
|
const { mkdirSync } = await import("fs"); |
|
mkdirSync(outDir, { recursive: true }); |
|
const uuid = picked.id.length === 36 ? picked.id : crypto.randomUUID(); |
|
dstPath = join(outDir, `rollout-${ts}-${uuid}.jsonl`); |
|
} else { |
|
// Find appropriate Claude project dir from session cwd |
|
const cwdEncoded = "-" + picked.cwd.replace(/\//g, "-").replace(/^-/, ""); |
|
const outDir = join(homedir(), ".claude", "projects", cwdEncoded); |
|
const { mkdirSync } = await import("fs"); |
|
mkdirSync(outDir, { recursive: true }); |
|
// File name must match sessionId for claude -r to find it |
|
const sid = picked.id.length === 36 ? picked.id : crypto.randomUUID(); |
|
dstPath = join(outDir, `${sid}.jsonl`); |
|
} |
|
} |
|
|
|
const text = readFileSync(srcPath, "utf-8"); |
|
const records = parseJsonl(text); |
|
const result = direction === "claude2codex" ? claude2codex(records) : codex2claude(records); |
|
const output = toJsonl(result); |
|
|
|
if (dstPath) { |
|
writeFileSync(dstPath, output); |
|
console.error(`\nWrote ${result.length} records to ${dstPath}`); |
|
|
|
// Print resume command |
|
if (direction === "claude2codex") { |
|
const meta = result.find((r: any) => r.type === "session_meta"); |
|
const id = meta?.payload?.id ?? basename(dstPath, ".jsonl"); |
|
console.error(`\nResume with:\n codex resume ${id}`); |
|
} else { |
|
const sessionId = result.find((r: any) => r.sessionId)?.sessionId; |
|
if (sessionId) { |
|
console.error(`\nResume with:\n claude -r ${sessionId}`); |
|
} |
|
} |
|
} else { |
|
process.stdout.write(output); |
|
} |
|
} |
|
|
|
main(); |