very-happy-cli 0.2.159 → 0.2.161
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/dist/{AcpBackend-VsfkZiVF.mjs → AcpBackend-6U6pQgx1.mjs} +4 -2
- package/dist/{AcpBackend-DUYFlyfN.cjs → AcpBackend-Ba98QDif.cjs} +78 -76
- package/dist/{AcpSessionManager-Bdv8LWXh.cjs → AcpSessionManager-BAY1Pggx.cjs} +9 -9
- package/dist/{AcpSessionManager-rYtVb0kN.mjs → AcpSessionManager-CoMWFVw3.mjs} +1 -1
- package/dist/BasePermissionHandler-2R0uH2CI.cjs +157 -0
- package/dist/BasePermissionHandler-BvvhBTja.mjs +155 -0
- package/dist/BaseReasoningProcessor-DyhO1uoU.mjs +198 -0
- package/dist/BaseReasoningProcessor-fsVNmCEF.cjs +200 -0
- package/dist/RemoteModeDisplay-D-HewyXg.mjs +150 -0
- package/dist/RemoteModeDisplay-NC1gPHtj.cjs +153 -0
- package/dist/agentAttachments-Dbw8iMCZ.cjs +20 -0
- package/dist/agentAttachments-M_B5qEF2.mjs +18 -0
- package/dist/agentGuidance-B5NATuFa.cjs +28 -0
- package/dist/agentGuidance-CQuNlNx8.mjs +20 -0
- package/dist/{agentHome-CvsJyeQB.mjs → agentHome-Ck1i1TQB.mjs} +8 -22
- package/dist/{agentHome-CHh4lmjM.cjs → agentHome-DvNnGphb.cjs} +14 -28
- package/dist/api-B2vLMS-z.cjs +3346 -0
- package/dist/api-SffjRQ1b.mjs +3339 -0
- package/dist/{types-Dh-s4Lqs.mjs → apiMachine-BpW9t8MO.mjs} +9116 -16205
- package/dist/{types-BbvK1Tr_.cjs → apiMachine-gFA54X7y.cjs} +9104 -16247
- package/dist/assistantTools-BfA0R3_m.cjs +380 -0
- package/dist/assistantTools-CgiTbnWb.mjs +376 -0
- package/dist/attachmentContent-CSGxr71v.mjs +82 -0
- package/dist/attachmentContent-DMMZzevR.cjs +88 -0
- package/dist/attachmentLimits-CzVrqgLR.cjs +354 -0
- package/dist/attachmentLimits-yba1uHFO.mjs +348 -0
- package/dist/auth-Bt4ceBBy.cjs +227 -0
- package/dist/auth-CYHjQ8oQ.mjs +225 -0
- package/dist/auth-DdyVZSo6.mjs +218 -0
- package/dist/auth-x4CEoysH.cjs +220 -0
- package/dist/authRelay-BdYkC1Po.cjs +156 -0
- package/dist/authRelay-DSAvg1uv.mjs +143 -0
- package/dist/{auto-DKmEls8Z.cjs → auto-BDXDUnjB.cjs} +70 -79
- package/dist/{auto-nb8GhRRy.mjs → auto-CXLlRi3a.mjs} +27 -36
- package/dist/backgroundTaskActivity-8PdPxrvy.mjs +149 -0
- package/dist/backgroundTaskActivity-CBvUdj39.cjs +157 -0
- package/dist/bootstrap-BNQq05jR.mjs +112 -0
- package/dist/bootstrap-CkgIq8N3.cjs +116 -0
- package/dist/browser-9pyhQr1R.mjs +20 -0
- package/dist/browser-BDOlKWYe.cjs +22 -0
- package/dist/claudeCliPath-BqkaH2oR.mjs +8 -0
- package/dist/claudeCliPath-U4HHmV4r.cjs +10 -0
- package/dist/claudeLocal-BDuGFnjJ.cjs +343 -0
- package/dist/claudeLocal-CQa8qnwq.mjs +339 -0
- package/dist/claudeLocalLauncher-Bd5I2-j6.cjs +154 -0
- package/dist/claudeLocalLauncher-CDqQI9ak.mjs +152 -0
- package/dist/cliUpdate-DsKXPFYp.cjs +120 -0
- package/dist/cliUpdate-Y34VGe9Y.mjs +112 -0
- package/dist/client-Brn-UwVk.mjs +100 -0
- package/dist/client-CXb1VJui.mjs +112 -0
- package/dist/client-C_axwRry.cjs +104 -0
- package/dist/client-DOBWJL3u.cjs +119 -0
- package/dist/codex/happyMcpStdioBridge.cjs +33 -14
- package/dist/codex/happyMcpStdioBridge.mjs +26 -7
- package/dist/codexAppServerClient-ZzhUTWPo.cjs +1090 -0
- package/dist/codexAppServerClient-oeQKD6Qf.mjs +1088 -0
- package/dist/codexCommand-C7H0NpBe.cjs +2088 -0
- package/dist/codexCommand-CJ82bl3l.mjs +2086 -0
- package/dist/codexThreadFork-BCaEMBiM.mjs +338 -0
- package/dist/codexThreadFork-BZqF-9bX.cjs +350 -0
- package/dist/{config-ClLtM6F3.mjs → config-BglnktIo.mjs} +7 -40
- package/dist/{config-DLx-vW3I.cjs → config-CYpL1Ahd.cjs} +26 -59
- package/dist/configuration-DoWFJUPm.mjs +170 -0
- package/dist/configuration-v9f8LM9T.cjs +181 -0
- package/dist/connect-hgcNjyAB.mjs +682 -0
- package/dist/connect-yEvjdRvx.cjs +684 -0
- package/dist/constants-BC4St0xd.mjs +11 -0
- package/dist/constants-VJ0N6WuG.cjs +17 -0
- package/dist/contentLogMetadata-BlWI0egu.mjs +51 -0
- package/dist/contentLogMetadata-ZSFk_NjQ.cjs +54 -0
- package/dist/context-B45vMtSZ.cjs +69 -0
- package/dist/context-C9GHPLEm.mjs +65 -0
- package/dist/controlClient-DjoKTtgK.mjs +260 -0
- package/dist/controlClient-fEHE85bH.cjs +279 -0
- package/dist/createSessionMetadata-BRLB9CNO.cjs +58 -0
- package/dist/createSessionMetadata-mIeehclp.mjs +54 -0
- package/dist/defaultPermissionMode-BnxJPVHT.mjs +4 -0
- package/dist/defaultPermissionMode-CxmTXUQR.cjs +7 -0
- package/dist/detectCLI-CSGMlV51.mjs +58 -0
- package/dist/detectCLI-DR9ZaN6B.cjs +61 -0
- package/dist/deterministicJson-DKDP_mM-.cjs +507 -0
- package/dist/deterministicJson-p835AdAU.mjs +502 -0
- package/dist/doctor-BPkYWSuq.mjs +75 -0
- package/dist/doctor-BeUuQXFs.mjs +391 -0
- package/dist/doctor-hVNnSZJn.cjs +395 -0
- package/dist/doctor-u46qYYKX.cjs +79 -0
- package/dist/doctorReadiness-BCZGcN0a.cjs +151 -0
- package/dist/doctorReadiness-C208SMN9.mjs +142 -0
- package/dist/editPaths-BVfS_FFL.mjs +68 -0
- package/dist/editPaths-Bqp4OFPw.cjs +73 -0
- package/dist/ensureDaemonRunning-CO7d1gOC.mjs +61 -0
- package/dist/ensureDaemonRunning-Dg9aZfjs.cjs +64 -0
- package/dist/handleResumeCommand-BiTPK0qd.mjs +240 -0
- package/dist/handleResumeCommand-DnVmy7a3.cjs +245 -0
- package/dist/ids-B6ZC0HGt.cjs +13 -0
- package/dist/ids-DU6sLx4C.mjs +10 -0
- package/dist/index-B6UGRQ7h.mjs +102 -0
- package/dist/{index-2mCaeKyf.mjs → index-CAFwetLk.mjs} +68 -38
- package/dist/index-Cqx-Raq6.mjs +68 -0
- package/dist/index-D3kI0hDR.cjs +107 -0
- package/dist/index-DA7yINuk.cjs +994 -0
- package/dist/{index-BEOMECtE.cjs → index-LIn6nCcq.cjs} +121 -91
- package/dist/index-Pf2cV9cn.cjs +182 -0
- package/dist/index-SPGPenK5.mjs +158 -0
- package/dist/index-gNTWtSKN.cjs +71 -0
- package/dist/index-qdS1friN.mjs +949 -0
- package/dist/index.cjs +5 -46
- package/dist/index.mjs +5 -46
- package/dist/{install-BPhJppvd.mjs → install-BO_W7VhL.mjs} +37 -37
- package/dist/install-Bu85vlHD.cjs +88 -0
- package/dist/install-ClUUl3yp.mjs +86 -0
- package/dist/{install-BOHrUaFM.cjs → install-Cs5qAPrB.cjs} +38 -38
- package/dist/{installPiTools-DuZ6jZvX.cjs → installPiTools-BKnXjrVu.cjs} +2 -3
- package/dist/{installPiTools-Cj7yU9Fc.mjs → installPiTools-B_CdM5_2.mjs} +1 -2
- package/dist/{installTerminalHooks-Cou3WUf3.cjs → installTerminalHooks-BQZbvxsZ.cjs} +5 -40
- package/dist/{installTerminalHooks-WeznjkuC.mjs → installTerminalHooks-By7gBMEF.mjs} +3 -38
- package/dist/lib.cjs +28 -19
- package/dist/lib.d.cts +12 -2
- package/dist/lib.d.mts +12 -2
- package/dist/lib.mjs +23 -14
- package/dist/lidState-C2dI0FAN.mjs +297 -0
- package/dist/lidState-KHHu1015.cjs +305 -0
- package/dist/limits-C-a7gr04.cjs +43 -0
- package/dist/limits-DIo7rEBR.mjs +36 -0
- package/dist/localHappyAgentAuth-BXiHPXdC.cjs +86 -0
- package/dist/localHappyAgentAuth-DCsetiyG.mjs +82 -0
- package/dist/logger-Bc8-u1w0.cjs +275 -0
- package/dist/logger-fXoNzNuB.mjs +271 -0
- package/dist/loginShellEnv-BkuYtExZ.cjs +182 -0
- package/dist/loginShellEnv-Dw8M7tkI.mjs +167 -0
- package/dist/machineMetadata-BHjehxJY.mjs +38 -0
- package/dist/machineMetadata-BibDMFVx.cjs +41 -0
- package/dist/main-BktM4A1K.mjs +939 -0
- package/dist/main-CLaX-t-b.cjs +941 -0
- package/dist/manager-BQ4flvQZ.mjs +101 -0
- package/dist/manager-Cly8xVIR.cjs +105 -0
- package/dist/{mcp-Dy3f_K0h.mjs → mcp-CeUd9KDr.mjs} +27 -33
- package/dist/{mcp-CPYbtsYG.cjs → mcp-GBfE70nk.cjs} +42 -48
- package/dist/messageBuffer-BWlGgR3A.mjs +71 -0
- package/dist/messageBuffer-fJMH6uY8.cjs +73 -0
- package/dist/mirrorProtocol-BNLtoWjO.mjs +170 -0
- package/dist/mirrorProtocol-BUtmg1ws.cjs +186 -0
- package/dist/oneShotPrompts-CL0dip9C.cjs +16 -0
- package/dist/oneShotPrompts-DT4p6RNd.mjs +12 -0
- package/dist/package-Cl5BEP5x.cjs +173 -0
- package/dist/package-uOg9SCKi.mjs +171 -0
- package/dist/path-B7zW-WwE.cjs +18 -0
- package/dist/path-D2wdaNSk.mjs +15 -0
- package/dist/peerTools-CIeiy9wn.cjs +283 -0
- package/dist/peerTools-Dz3_7Bkk.mjs +272 -0
- package/dist/permissionMode-CH4FoZfU.mjs +80 -0
- package/dist/permissionMode-CrV-XuMs.cjs +87 -0
- package/dist/persistence-CBNMzSa8.cjs +414 -0
- package/dist/persistence-oET5n61f.mjs +372 -0
- package/dist/piExtension.mjs +2 -1
- package/dist/piRunnerArgs-Bf7RUF__.cjs +66 -0
- package/dist/piRunnerArgs-Dk1ncwHF.mjs +59 -0
- package/dist/{piRuntime-NtF0YqjH.cjs → piRuntime-B_6TE9Eb.cjs} +4 -4
- package/dist/{piRuntime-kLZNpAAl.mjs → piRuntime-BwRdhK_N.mjs} +2 -2
- package/dist/{piTerminal-B0fEOV_2.mjs → piTerminal-Bap3R8Re.mjs} +32 -35
- package/dist/{piTerminal-9J0GAKzb.cjs → piTerminal-CagmEoez.cjs} +32 -35
- package/dist/previewPath-B0rBdhy1.cjs +83 -0
- package/dist/previewPath-CXN4Vag0.mjs +81 -0
- package/dist/projectPath-BTBIMB57.mjs +10 -0
- package/dist/projectPath-ChLhdOyt.cjs +13 -0
- package/dist/registerCommonHandlers-BjSV2Skz.cjs +538 -0
- package/dist/registerCommonHandlers-CrgvJsOy.mjs +536 -0
- package/dist/registerKillSessionHandler-6TC2S3lf.cjs +366 -0
- package/dist/registerKillSessionHandler-Cbc8Rgtd.mjs +363 -0
- package/dist/remoteSessionClient-BNx78NLb.cjs +634 -0
- package/dist/remoteSessionClient-CO7qOjR0.mjs +612 -0
- package/dist/resources-BdFBQi8r.mjs +117 -0
- package/dist/resources-JDPugcDq.cjs +120 -0
- package/dist/run-2l5vSt2h.mjs +5743 -0
- package/dist/run-D5xAEWSB.cjs +5766 -0
- package/dist/runClaude-BX6ixjtW.cjs +894 -0
- package/dist/runClaude-BgN6xo05.mjs +892 -0
- package/dist/runClaudeDeps-Bp20MScE.cjs +4168 -0
- package/dist/runClaudeDeps-COBBhD9o.mjs +4129 -0
- package/dist/{runGemini-F-mqx3_K.cjs → runGemini-CSMPkt0p.cjs} +202 -175
- package/dist/{runGemini-DV6Ouadt.mjs → runGemini-Cc2jq-uh.mjs} +58 -31
- package/dist/{runOpenClaw-D7MlPS5n.mjs → runOpenClaw-CIlCwdW2.mjs} +31 -32
- package/dist/{runOpenClaw-Qqo9j63M.cjs → runOpenClaw-YBwua4Rf.cjs} +64 -65
- package/dist/sandbox-C4gXLP1p.cjs +187 -0
- package/dist/sandbox-CNkwOtHI.mjs +180 -0
- package/dist/selfCheck-CU8xSDsl.cjs +87 -0
- package/dist/selfCheck-hJ6PCh7v.mjs +84 -0
- package/dist/{send-CLhYXNgy.mjs → send-CGHcKZ-w.mjs} +12 -5
- package/dist/{send-4qJTCAYW.cjs → send-DY9Nr-x4.cjs} +16 -9
- package/dist/server-II51Mimf.cjs +407 -0
- package/dist/server-m2b5pamZ.mjs +404 -0
- package/dist/sessionLock-7ePAYDOg.cjs +205 -0
- package/dist/sessionLock-Cw6FmbMv.mjs +197 -0
- package/dist/sessionModeFile-BVCV5f7E.cjs +36 -0
- package/dist/sessionModeFile-CDmDyRkD.mjs +32 -0
- package/dist/sessionOps-C_f1wKOr.mjs +760 -0
- package/dist/sessionOps-DJdExmBy.cjs +783 -0
- package/dist/sessionScanner-CaAHLyNV.mjs +206 -0
- package/dist/sessionScanner-D1goKdd3.cjs +209 -0
- package/dist/{sessions-Bh1pyddi.cjs → sessions-DGs6ALNo.cjs} +37 -28
- package/dist/{sessions-CzIH2rGp.mjs → sessions-DnvmWUel.mjs} +17 -8
- package/dist/setupOfflineReconnection-CVViqZft.mjs +72 -0
- package/dist/setupOfflineReconnection-R7XuR_sj.cjs +74 -0
- package/dist/smallFastModel-BLNgMrwR.mjs +18 -0
- package/dist/smallFastModel-D_uxGMo3.cjs +21 -0
- package/dist/{spawn-CmFOSVdW.cjs → spawn-CXTH7qg1.cjs} +61 -65
- package/dist/{spawn-CPIWlPOM.mjs → spawn-DNYSvFtC.mjs} +35 -40
- package/dist/spawnAgents-BGQiEF-w.cjs +52 -0
- package/dist/spawnAgents-DZzZ9jMI.mjs +45 -0
- package/dist/spawnHappyCLI-BhYj84rv.cjs +66 -0
- package/dist/spawnHappyCLI-cqWQbwsb.mjs +64 -0
- package/dist/specialCommands-BSqhYg5X.mjs +52 -0
- package/dist/specialCommands-DSsTeRej.cjs +54 -0
- package/dist/startFileWatcher-CKKx-8ZO.cjs +71 -0
- package/dist/startFileWatcher-CdegMbJj.mjs +69 -0
- package/dist/startupPlan-BLMbfQIH.cjs +12 -0
- package/dist/startupPlan-DFMyNgAs.mjs +10 -0
- package/dist/streamRelay-BiKw59Cc.cjs +233 -0
- package/dist/streamRelay-FPVbHpbM.mjs +231 -0
- package/dist/sync-CVk4QERO.cjs +71 -0
- package/dist/sync-CxD-w_40.mjs +69 -0
- package/dist/systemPrompt-BygJcmbi.cjs +97 -0
- package/dist/systemPrompt-DmtypUc7.mjs +94 -0
- package/dist/{teams-CEahj4cm.mjs → teams-BOC6nIPL.mjs} +9 -40
- package/dist/{teams-CB1R9-Z9.cjs → teams-BsvpOBfn.cjs} +14 -45
- package/dist/template-BaHAmH2K.cjs +129 -0
- package/dist/template-CiOPa2rd.mjs +121 -0
- package/dist/{terminalTitleSuggest-DKgX3osW.mjs → terminalTitleSuggest-CeKsX_V6.mjs} +1 -9
- package/dist/{terminalTitleSuggest-Dji8I0Jy.cjs → terminalTitleSuggest-DhvRFX3H.cjs} +0 -10
- package/dist/terminals-B_bfC9c2.cjs +250 -0
- package/dist/terminals-Dlg0x3nY.mjs +235 -0
- package/dist/time-BZNdNiIV.mjs +38 -0
- package/dist/time-CNRE4ChD.cjs +42 -0
- package/dist/titleGenerator-CFRhU-xq.cjs +172 -0
- package/dist/titleGenerator-ox6ptCsp.mjs +166 -0
- package/dist/tmuxSocket-B8Yx20sT.mjs +19 -0
- package/dist/tmuxSocket-D-Xt6Vn-.cjs +24 -0
- package/dist/{todo-DqyRI4wP.cjs → todo-B-KB7oAB.cjs} +15 -12
- package/dist/{todo-Cknp5D4U.mjs → todo-C5KBVbUw.mjs} +11 -8
- package/dist/tools-BZabNIid.mjs +177 -0
- package/dist/tools-BmQQUATY.mjs +60 -0
- package/dist/tools-CsSovyNp.cjs +180 -0
- package/dist/tools-lMDra4Pv.cjs +63 -0
- package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
- package/dist/trimIdent-BAjmv_TG.mjs +20 -0
- package/dist/types-CRS-srZK.cjs +66 -0
- package/dist/types-CkqAACXx.mjs +64 -0
- package/dist/uninstall-B1WLtC1d.mjs +47 -0
- package/dist/uninstall-fvA5aWpU.cjs +49 -0
- package/package.json +7 -7
- package/scripts/session_hook_forwarder.cjs +9 -2
- package/dist/agentGuidance-BRDN1NoZ.cjs +0 -2217
- package/dist/agentGuidance-KgvYuF3R.mjs +0 -2145
- package/dist/index-BF6WfgV5.mjs +0 -19624
- package/dist/index-C-JazrMB.mjs +0 -52
- package/dist/index-blDSadUR.cjs +0 -58
- package/dist/index-rH3DrQ5B.cjs +0 -19688
- package/dist/limits-BH5CV5vR.cjs +0 -2683
- package/dist/limits-CjRj8IX5.mjs +0 -2558
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { copyFile, unlink, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { l as logger } from './logger-fXoNzNuB.mjs';
|
|
7
|
+
|
|
8
|
+
class ForkTruncateUuidNotFoundError extends Error {
|
|
9
|
+
constructor(cutAfterUuid, sourcePath) {
|
|
10
|
+
super(`Truncation UUID ${cutAfterUuid} not found in ${sourcePath}`);
|
|
11
|
+
this.cutAfterUuid = cutAfterUuid;
|
|
12
|
+
this.sourcePath = sourcePath;
|
|
13
|
+
this.name = "ForkTruncateUuidNotFoundError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
class ForkSourceMissingError extends Error {
|
|
17
|
+
constructor(sourcePath) {
|
|
18
|
+
super(`Source Claude session JSONL not found: ${sourcePath}`);
|
|
19
|
+
this.sourcePath = sourcePath;
|
|
20
|
+
this.name = "ForkSourceMissingError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function jsonlPath(projectDir, sessionId) {
|
|
24
|
+
return join(projectDir, `${sessionId}.jsonl`);
|
|
25
|
+
}
|
|
26
|
+
function isUserPrompt(parsed) {
|
|
27
|
+
if (!parsed || parsed.type !== "user") return false;
|
|
28
|
+
if (parsed.isSidechain) return false;
|
|
29
|
+
const content = parsed.message?.content;
|
|
30
|
+
return typeof content === "string";
|
|
31
|
+
}
|
|
32
|
+
function hasPromptAttachments(parsed) {
|
|
33
|
+
const content = parsed.message?.content;
|
|
34
|
+
return typeof content === "string" && content.includes("<attached_files>") || Array.isArray(content) && content.some((block) => block?.type !== "text" || typeof block.text === "string" && block.text.includes("<attached_files>"));
|
|
35
|
+
}
|
|
36
|
+
function userPromptText(parsed) {
|
|
37
|
+
if (parsed?.type !== "user" || parsed.isSidechain || parsed.isMeta || parsed.isSynthetic) return null;
|
|
38
|
+
const content = parsed.message?.content;
|
|
39
|
+
if (typeof content === "string") return content.trim() ? content : null;
|
|
40
|
+
if (!Array.isArray(content) || content.some((block) => block?.type === "tool_result")) return null;
|
|
41
|
+
const text = content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
42
|
+
return text.trim() ? text : null;
|
|
43
|
+
}
|
|
44
|
+
async function discardForkedSession(projectDir, claudeSessionId) {
|
|
45
|
+
try {
|
|
46
|
+
await unlink(jsonlPath(projectDir, claudeSessionId));
|
|
47
|
+
logger.debug(`[CLAUDE FORK] Discarded unused fork ${claudeSessionId}`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code !== "ENOENT") {
|
|
50
|
+
logger.debug(`[CLAUDE FORK] Failed to discard fork ${claudeSessionId}:`, error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function forkSession(projectDir, sourceClaudeSessionId) {
|
|
55
|
+
const newId = randomUUID();
|
|
56
|
+
const src = jsonlPath(projectDir, sourceClaudeSessionId);
|
|
57
|
+
const dst = jsonlPath(projectDir, newId);
|
|
58
|
+
try {
|
|
59
|
+
await copyFile(src, dst);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code === "ENOENT") {
|
|
62
|
+
throw new ForkSourceMissingError(src);
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
logger.debug(`[CLAUDE FORK] Forked ${sourceClaudeSessionId} -> ${newId}`);
|
|
67
|
+
return newId;
|
|
68
|
+
}
|
|
69
|
+
async function forkAndTruncateSession(projectDir, sourceClaudeSessionId, cutAfterUuid) {
|
|
70
|
+
const newId = randomUUID();
|
|
71
|
+
const src = jsonlPath(projectDir, sourceClaudeSessionId);
|
|
72
|
+
const finalDst = jsonlPath(projectDir, newId);
|
|
73
|
+
const tempDst = `${finalDst}.tmp-${process.pid}-${Date.now()}`;
|
|
74
|
+
const readStream = createReadStream(src, { encoding: "utf-8" });
|
|
75
|
+
const writeStream = createWriteStream(tempDst, { encoding: "utf-8" });
|
|
76
|
+
const rl = createInterface({ input: readStream, crlfDelay: Infinity });
|
|
77
|
+
let foundMarker = false;
|
|
78
|
+
let iterationError = null;
|
|
79
|
+
try {
|
|
80
|
+
for await (const line of rl) {
|
|
81
|
+
if (line.length === 0) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
let parsed = null;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(line);
|
|
87
|
+
} catch {
|
|
88
|
+
logger.debug(`[CLAUDE FORK] Skipped malformed JSON line during truncation copy`);
|
|
89
|
+
}
|
|
90
|
+
if (foundMarker && parsed && isUserPrompt(parsed)) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
writeStream.write(`${line}
|
|
95
|
+
`, (err) => err ? reject(err) : resolve());
|
|
96
|
+
});
|
|
97
|
+
if (!foundMarker && parsed && typeof parsed.uuid === "string" && parsed.uuid === cutAfterUuid) {
|
|
98
|
+
foundMarker = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
iterationError = e;
|
|
103
|
+
} finally {
|
|
104
|
+
rl.close();
|
|
105
|
+
readStream.destroy();
|
|
106
|
+
await new Promise((resolve, reject) => writeStream.end((err) => err ? reject(err) : resolve()));
|
|
107
|
+
}
|
|
108
|
+
if (iterationError) {
|
|
109
|
+
await unlink(tempDst).catch(() => void 0);
|
|
110
|
+
if (iterationError.code === "ENOENT") {
|
|
111
|
+
throw new ForkSourceMissingError(src);
|
|
112
|
+
}
|
|
113
|
+
throw iterationError;
|
|
114
|
+
}
|
|
115
|
+
if (!foundMarker) {
|
|
116
|
+
await unlink(tempDst).catch(() => void 0);
|
|
117
|
+
throw new ForkTruncateUuidNotFoundError(cutAfterUuid, src);
|
|
118
|
+
}
|
|
119
|
+
await rename(tempDst, finalDst);
|
|
120
|
+
logger.debug(`[CLAUDE FORK] Forked ${sourceClaudeSessionId} -> ${newId}, cut after ${cutAfterUuid}`);
|
|
121
|
+
return newId;
|
|
122
|
+
}
|
|
123
|
+
async function listClaudeRewindPoints(projectDir, claudeSessionId) {
|
|
124
|
+
const path = jsonlPath(projectDir, claudeSessionId);
|
|
125
|
+
let raw;
|
|
126
|
+
try {
|
|
127
|
+
raw = await readFile(path, "utf-8");
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error.code === "ENOENT") {
|
|
130
|
+
throw new ForkSourceMissingError(path);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
const points = [];
|
|
135
|
+
for (const line of raw.split("\n")) {
|
|
136
|
+
if (line.length === 0) continue;
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(line);
|
|
140
|
+
} catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const content = userPromptText(parsed);
|
|
144
|
+
if (content === null) continue;
|
|
145
|
+
if (typeof parsed.uuid !== "string" || parsed.uuid.length === 0) continue;
|
|
146
|
+
const timestampRaw = parsed.timestamp;
|
|
147
|
+
const timestamp = typeof timestampRaw === "string" ? Date.parse(timestampRaw) : typeof timestampRaw === "number" ? timestampRaw : Date.now();
|
|
148
|
+
points.push({
|
|
149
|
+
uuid: parsed.uuid,
|
|
150
|
+
text: content,
|
|
151
|
+
...hasPromptAttachments(parsed) ? { hasAttachments: true } : {},
|
|
152
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return points;
|
|
156
|
+
}
|
|
157
|
+
async function forkBeforeUserMessage(projectDir, sourceClaudeSessionId, cutBeforeUuid) {
|
|
158
|
+
const src = jsonlPath(projectDir, sourceClaudeSessionId);
|
|
159
|
+
let raw;
|
|
160
|
+
try {
|
|
161
|
+
raw = await readFile(src, "utf-8");
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error.code === "ENOENT") throw new ForkSourceMissingError(src);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
const lines = raw.split("\n");
|
|
167
|
+
let hasPreviousConversation = false;
|
|
168
|
+
let hasPreviousPrompt = false;
|
|
169
|
+
let cut = -1;
|
|
170
|
+
for (let i = 0; i < lines.length; i++) {
|
|
171
|
+
let entry;
|
|
172
|
+
try {
|
|
173
|
+
entry = JSON.parse(lines[i]);
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (userPromptText(entry) !== null && entry.uuid === cutBeforeUuid) {
|
|
178
|
+
if (hasPromptAttachments(entry)) throw new Error("Cannot edit and rerun a message with attachments");
|
|
179
|
+
cut = i;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
if (!entry?.isSidechain && !entry?.isMeta && !entry?.isSynthetic && (entry?.type === "user" || entry?.type === "assistant")) {
|
|
183
|
+
hasPreviousConversation = true;
|
|
184
|
+
const content = entry.message?.content;
|
|
185
|
+
if (entry.type === "user" && (typeof content === "string" || Array.isArray(content) && content.length > 0 && !content.some((block) => block?.type === "tool_result"))) {
|
|
186
|
+
hasPreviousPrompt = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (cut < 0) throw new ForkTruncateUuidNotFoundError(cutBeforeUuid, src);
|
|
191
|
+
if (!hasPreviousConversation) return null;
|
|
192
|
+
if (!hasPreviousPrompt) throw new Error("Cannot safely rewind: earlier history has no user prompt");
|
|
193
|
+
const newId = randomUUID();
|
|
194
|
+
const destination = jsonlPath(projectDir, newId);
|
|
195
|
+
const temporary = `${destination}.tmp-${process.pid}`;
|
|
196
|
+
try {
|
|
197
|
+
await writeFile(temporary, lines.slice(0, cut).join("\n") + "\n", { encoding: "utf-8", flag: "wx" });
|
|
198
|
+
await rename(temporary, destination);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
await unlink(temporary).catch(() => void 0);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
return newId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class CodexForkRewindPointNotFoundError extends Error {
|
|
207
|
+
constructor(itemId, threadId) {
|
|
208
|
+
super(`Codex rewind point ${itemId} not found in thread ${threadId}`);
|
|
209
|
+
this.itemId = itemId;
|
|
210
|
+
this.threadId = threadId;
|
|
211
|
+
this.name = "CodexForkRewindPointNotFoundError";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function hasUserAttachments(item) {
|
|
215
|
+
const content = item.content;
|
|
216
|
+
return Array.isArray(content) && content.some((part) => !part || typeof part !== "object" || part.type !== "text" || typeof part.text === "string" && part.text.includes("<attached_files>"));
|
|
217
|
+
}
|
|
218
|
+
function rejectAttachedTarget(thread, itemId) {
|
|
219
|
+
const item = thread.turns?.flatMap((turn) => turn.items ?? []).find((item2) => item2.id === itemId);
|
|
220
|
+
if (item && hasUserAttachments(item)) throw new Error("Cannot edit and rerun a message with attachments");
|
|
221
|
+
}
|
|
222
|
+
function textFromUserItem(item) {
|
|
223
|
+
if (item.type !== "userMessage") {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const content = item.content;
|
|
227
|
+
if (!Array.isArray(content)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const textParts = content.filter((part) => Boolean(part) && typeof part === "object" && part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim();
|
|
231
|
+
return textParts.length > 0 ? textParts : null;
|
|
232
|
+
}
|
|
233
|
+
function timestampFromTurn(turn) {
|
|
234
|
+
const seconds = turn.startedAt ?? turn.completedAt;
|
|
235
|
+
return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
|
|
236
|
+
}
|
|
237
|
+
function listCodexRewindPoints(thread) {
|
|
238
|
+
const points = [];
|
|
239
|
+
for (const turn of thread.turns ?? []) {
|
|
240
|
+
for (const item of turn.items ?? []) {
|
|
241
|
+
const text = textFromUserItem(item);
|
|
242
|
+
if (!text) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
points.push({
|
|
246
|
+
itemId: item.id,
|
|
247
|
+
text,
|
|
248
|
+
...hasUserAttachments(item) ? { hasAttachments: true } : {},
|
|
249
|
+
timestamp: timestampFromTurn(turn)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return points;
|
|
254
|
+
}
|
|
255
|
+
function findCutTurn(thread, itemId) {
|
|
256
|
+
const turns = thread.turns ?? [];
|
|
257
|
+
for (let index = 0; index < turns.length; index++) {
|
|
258
|
+
const turn = turns[index];
|
|
259
|
+
const item = (turn.items ?? []).find((candidate) => candidate.id === itemId);
|
|
260
|
+
if (!item) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const text = textFromUserItem(item);
|
|
264
|
+
if (text) {
|
|
265
|
+
return { index, text };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
async function forkCodexThread(client, opts) {
|
|
271
|
+
const forked = await client.forkThread({
|
|
272
|
+
threadId: opts.threadId,
|
|
273
|
+
...opts.cwd ? { cwd: opts.cwd } : {},
|
|
274
|
+
...opts.model ? { model: opts.model } : {},
|
|
275
|
+
...opts.approvalPolicy ? { approvalPolicy: opts.approvalPolicy } : {},
|
|
276
|
+
...opts.sandbox ? { sandbox: opts.sandbox } : {},
|
|
277
|
+
...opts.mcpServers ? { mcpServers: opts.mcpServers } : {}
|
|
278
|
+
});
|
|
279
|
+
if (opts.cutAfterItemId) {
|
|
280
|
+
const cutTurn = findCutTurn(forked.thread, opts.cutAfterItemId);
|
|
281
|
+
if (!cutTurn) {
|
|
282
|
+
throw new CodexForkRewindPointNotFoundError(opts.cutAfterItemId, opts.threadId);
|
|
283
|
+
}
|
|
284
|
+
const turns = forked.thread.turns ?? [];
|
|
285
|
+
const turnsToDrop = turns.length - cutTurn.index;
|
|
286
|
+
if (turnsToDrop > 0) {
|
|
287
|
+
await client.rollbackThread({
|
|
288
|
+
threadId: forked.threadId,
|
|
289
|
+
numTurns: turnsToDrop
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
await client.injectItems({
|
|
293
|
+
threadId: forked.threadId,
|
|
294
|
+
items: [{
|
|
295
|
+
type: "message",
|
|
296
|
+
role: "user",
|
|
297
|
+
content: [{ type: "input_text", text: cutTurn.text }]
|
|
298
|
+
}]
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
type: "success",
|
|
303
|
+
newCodexThreadId: forked.threadId
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function forkCodexBeforeUserMessage(client, opts) {
|
|
307
|
+
const { thread: source } = await client.readThread({ threadId: opts.threadId, includeTurns: true });
|
|
308
|
+
rejectAttachedTarget(source, opts.cutBeforeItemId);
|
|
309
|
+
const sourceCut = findCutTurn(source, opts.cutBeforeItemId);
|
|
310
|
+
if (!sourceCut) throw new CodexForkRewindPointNotFoundError(opts.cutBeforeItemId, opts.threadId);
|
|
311
|
+
if ((source.turns?.[sourceCut.index].items ?? []).findIndex((item) => item.id === opts.cutBeforeItemId) !== 0) {
|
|
312
|
+
throw new Error("Cannot safely rewind a user message inside an existing turn");
|
|
313
|
+
}
|
|
314
|
+
if (sourceCut.index === 0) return { type: "success", startFresh: true };
|
|
315
|
+
const forked = await client.forkThread({ threadId: opts.threadId, cwd: opts.cwd });
|
|
316
|
+
if (forked.threadId === opts.threadId) throw new Error("Codex did not create an independent fork");
|
|
317
|
+
rejectAttachedTarget(forked.thread, opts.cutBeforeItemId);
|
|
318
|
+
const cut = findCutTurn(forked.thread, opts.cutBeforeItemId);
|
|
319
|
+
if (!cut) throw new CodexForkRewindPointNotFoundError(opts.cutBeforeItemId, opts.threadId);
|
|
320
|
+
const turns = forked.thread.turns ?? [];
|
|
321
|
+
const items = turns[cut.index].items ?? [];
|
|
322
|
+
if (items.findIndex((item) => item.id === opts.cutBeforeItemId) !== 0) {
|
|
323
|
+
throw new Error("Cannot safely rewind a user message inside an existing turn");
|
|
324
|
+
}
|
|
325
|
+
if (cut.index === 0) return { type: "success", startFresh: true };
|
|
326
|
+
const expectedTurnIds = turns.slice(0, cut.index).map((turn) => turn.id);
|
|
327
|
+
const { thread } = await client.rollbackThread({
|
|
328
|
+
threadId: forked.threadId,
|
|
329
|
+
numTurns: turns.length - cut.index
|
|
330
|
+
});
|
|
331
|
+
const remaining = thread.turns ?? [];
|
|
332
|
+
if (remaining.length !== cut.index || remaining.some((turn, index) => turn.id !== expectedTurnIds[index])) {
|
|
333
|
+
throw new Error("Codex rollback did not preserve the expected history");
|
|
334
|
+
}
|
|
335
|
+
return { type: "success", newCodexThreadId: forked.threadId };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export { CodexForkRewindPointNotFoundError as C, ForkSourceMissingError as F, forkCodexThread as a, forkAndTruncateSession as b, ForkTruncateUuidNotFoundError as c, discardForkedSession as d, forkBeforeUserMessage as e, forkSession as f, listCodexRewindPoints as g, forkCodexBeforeUserMessage as h, listClaudeRewindPoints as l };
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
|
+
var promises = require('node:fs/promises');
|
|
5
|
+
var node_fs = require('node:fs');
|
|
6
|
+
var path = require('node:path');
|
|
7
|
+
var node_readline = require('node:readline');
|
|
8
|
+
var logger = require('./logger-Bc8-u1w0.cjs');
|
|
9
|
+
|
|
10
|
+
class ForkTruncateUuidNotFoundError extends Error {
|
|
11
|
+
constructor(cutAfterUuid, sourcePath) {
|
|
12
|
+
super(`Truncation UUID ${cutAfterUuid} not found in ${sourcePath}`);
|
|
13
|
+
this.cutAfterUuid = cutAfterUuid;
|
|
14
|
+
this.sourcePath = sourcePath;
|
|
15
|
+
this.name = "ForkTruncateUuidNotFoundError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
class ForkSourceMissingError extends Error {
|
|
19
|
+
constructor(sourcePath) {
|
|
20
|
+
super(`Source Claude session JSONL not found: ${sourcePath}`);
|
|
21
|
+
this.sourcePath = sourcePath;
|
|
22
|
+
this.name = "ForkSourceMissingError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function jsonlPath(projectDir, sessionId) {
|
|
26
|
+
return path.join(projectDir, `${sessionId}.jsonl`);
|
|
27
|
+
}
|
|
28
|
+
function isUserPrompt(parsed) {
|
|
29
|
+
if (!parsed || parsed.type !== "user") return false;
|
|
30
|
+
if (parsed.isSidechain) return false;
|
|
31
|
+
const content = parsed.message?.content;
|
|
32
|
+
return typeof content === "string";
|
|
33
|
+
}
|
|
34
|
+
function hasPromptAttachments(parsed) {
|
|
35
|
+
const content = parsed.message?.content;
|
|
36
|
+
return typeof content === "string" && content.includes("<attached_files>") || Array.isArray(content) && content.some((block) => block?.type !== "text" || typeof block.text === "string" && block.text.includes("<attached_files>"));
|
|
37
|
+
}
|
|
38
|
+
function userPromptText(parsed) {
|
|
39
|
+
if (parsed?.type !== "user" || parsed.isSidechain || parsed.isMeta || parsed.isSynthetic) return null;
|
|
40
|
+
const content = parsed.message?.content;
|
|
41
|
+
if (typeof content === "string") return content.trim() ? content : null;
|
|
42
|
+
if (!Array.isArray(content) || content.some((block) => block?.type === "tool_result")) return null;
|
|
43
|
+
const text = content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
44
|
+
return text.trim() ? text : null;
|
|
45
|
+
}
|
|
46
|
+
async function discardForkedSession(projectDir, claudeSessionId) {
|
|
47
|
+
try {
|
|
48
|
+
await promises.unlink(jsonlPath(projectDir, claudeSessionId));
|
|
49
|
+
logger.logger.debug(`[CLAUDE FORK] Discarded unused fork ${claudeSessionId}`);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code !== "ENOENT") {
|
|
52
|
+
logger.logger.debug(`[CLAUDE FORK] Failed to discard fork ${claudeSessionId}:`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function forkSession(projectDir, sourceClaudeSessionId) {
|
|
57
|
+
const newId = node_crypto.randomUUID();
|
|
58
|
+
const src = jsonlPath(projectDir, sourceClaudeSessionId);
|
|
59
|
+
const dst = jsonlPath(projectDir, newId);
|
|
60
|
+
try {
|
|
61
|
+
await promises.copyFile(src, dst);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code === "ENOENT") {
|
|
64
|
+
throw new ForkSourceMissingError(src);
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
logger.logger.debug(`[CLAUDE FORK] Forked ${sourceClaudeSessionId} -> ${newId}`);
|
|
69
|
+
return newId;
|
|
70
|
+
}
|
|
71
|
+
async function forkAndTruncateSession(projectDir, sourceClaudeSessionId, cutAfterUuid) {
|
|
72
|
+
const newId = node_crypto.randomUUID();
|
|
73
|
+
const src = jsonlPath(projectDir, sourceClaudeSessionId);
|
|
74
|
+
const finalDst = jsonlPath(projectDir, newId);
|
|
75
|
+
const tempDst = `${finalDst}.tmp-${process.pid}-${Date.now()}`;
|
|
76
|
+
const readStream = node_fs.createReadStream(src, { encoding: "utf-8" });
|
|
77
|
+
const writeStream = node_fs.createWriteStream(tempDst, { encoding: "utf-8" });
|
|
78
|
+
const rl = node_readline.createInterface({ input: readStream, crlfDelay: Infinity });
|
|
79
|
+
let foundMarker = false;
|
|
80
|
+
let iterationError = null;
|
|
81
|
+
try {
|
|
82
|
+
for await (const line of rl) {
|
|
83
|
+
if (line.length === 0) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
let parsed = null;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(line);
|
|
89
|
+
} catch {
|
|
90
|
+
logger.logger.debug(`[CLAUDE FORK] Skipped malformed JSON line during truncation copy`);
|
|
91
|
+
}
|
|
92
|
+
if (foundMarker && parsed && isUserPrompt(parsed)) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
writeStream.write(`${line}
|
|
97
|
+
`, (err) => err ? reject(err) : resolve());
|
|
98
|
+
});
|
|
99
|
+
if (!foundMarker && parsed && typeof parsed.uuid === "string" && parsed.uuid === cutAfterUuid) {
|
|
100
|
+
foundMarker = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {
|
|
104
|
+
iterationError = e;
|
|
105
|
+
} finally {
|
|
106
|
+
rl.close();
|
|
107
|
+
readStream.destroy();
|
|
108
|
+
await new Promise((resolve, reject) => writeStream.end((err) => err ? reject(err) : resolve()));
|
|
109
|
+
}
|
|
110
|
+
if (iterationError) {
|
|
111
|
+
await promises.unlink(tempDst).catch(() => void 0);
|
|
112
|
+
if (iterationError.code === "ENOENT") {
|
|
113
|
+
throw new ForkSourceMissingError(src);
|
|
114
|
+
}
|
|
115
|
+
throw iterationError;
|
|
116
|
+
}
|
|
117
|
+
if (!foundMarker) {
|
|
118
|
+
await promises.unlink(tempDst).catch(() => void 0);
|
|
119
|
+
throw new ForkTruncateUuidNotFoundError(cutAfterUuid, src);
|
|
120
|
+
}
|
|
121
|
+
await promises.rename(tempDst, finalDst);
|
|
122
|
+
logger.logger.debug(`[CLAUDE FORK] Forked ${sourceClaudeSessionId} -> ${newId}, cut after ${cutAfterUuid}`);
|
|
123
|
+
return newId;
|
|
124
|
+
}
|
|
125
|
+
async function listClaudeRewindPoints(projectDir, claudeSessionId) {
|
|
126
|
+
const path = jsonlPath(projectDir, claudeSessionId);
|
|
127
|
+
let raw;
|
|
128
|
+
try {
|
|
129
|
+
raw = await promises.readFile(path, "utf-8");
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error.code === "ENOENT") {
|
|
132
|
+
throw new ForkSourceMissingError(path);
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const points = [];
|
|
137
|
+
for (const line of raw.split("\n")) {
|
|
138
|
+
if (line.length === 0) continue;
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(line);
|
|
142
|
+
} catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const content = userPromptText(parsed);
|
|
146
|
+
if (content === null) continue;
|
|
147
|
+
if (typeof parsed.uuid !== "string" || parsed.uuid.length === 0) continue;
|
|
148
|
+
const timestampRaw = parsed.timestamp;
|
|
149
|
+
const timestamp = typeof timestampRaw === "string" ? Date.parse(timestampRaw) : typeof timestampRaw === "number" ? timestampRaw : Date.now();
|
|
150
|
+
points.push({
|
|
151
|
+
uuid: parsed.uuid,
|
|
152
|
+
text: content,
|
|
153
|
+
...hasPromptAttachments(parsed) ? { hasAttachments: true } : {},
|
|
154
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return points;
|
|
158
|
+
}
|
|
159
|
+
async function forkBeforeUserMessage(projectDir, sourceClaudeSessionId, cutBeforeUuid) {
|
|
160
|
+
const src = jsonlPath(projectDir, sourceClaudeSessionId);
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = await promises.readFile(src, "utf-8");
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error.code === "ENOENT") throw new ForkSourceMissingError(src);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const lines = raw.split("\n");
|
|
169
|
+
let hasPreviousConversation = false;
|
|
170
|
+
let hasPreviousPrompt = false;
|
|
171
|
+
let cut = -1;
|
|
172
|
+
for (let i = 0; i < lines.length; i++) {
|
|
173
|
+
let entry;
|
|
174
|
+
try {
|
|
175
|
+
entry = JSON.parse(lines[i]);
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (userPromptText(entry) !== null && entry.uuid === cutBeforeUuid) {
|
|
180
|
+
if (hasPromptAttachments(entry)) throw new Error("Cannot edit and rerun a message with attachments");
|
|
181
|
+
cut = i;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (!entry?.isSidechain && !entry?.isMeta && !entry?.isSynthetic && (entry?.type === "user" || entry?.type === "assistant")) {
|
|
185
|
+
hasPreviousConversation = true;
|
|
186
|
+
const content = entry.message?.content;
|
|
187
|
+
if (entry.type === "user" && (typeof content === "string" || Array.isArray(content) && content.length > 0 && !content.some((block) => block?.type === "tool_result"))) {
|
|
188
|
+
hasPreviousPrompt = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (cut < 0) throw new ForkTruncateUuidNotFoundError(cutBeforeUuid, src);
|
|
193
|
+
if (!hasPreviousConversation) return null;
|
|
194
|
+
if (!hasPreviousPrompt) throw new Error("Cannot safely rewind: earlier history has no user prompt");
|
|
195
|
+
const newId = node_crypto.randomUUID();
|
|
196
|
+
const destination = jsonlPath(projectDir, newId);
|
|
197
|
+
const temporary = `${destination}.tmp-${process.pid}`;
|
|
198
|
+
try {
|
|
199
|
+
await promises.writeFile(temporary, lines.slice(0, cut).join("\n") + "\n", { encoding: "utf-8", flag: "wx" });
|
|
200
|
+
await promises.rename(temporary, destination);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
await promises.unlink(temporary).catch(() => void 0);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
return newId;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
class CodexForkRewindPointNotFoundError extends Error {
|
|
209
|
+
constructor(itemId, threadId) {
|
|
210
|
+
super(`Codex rewind point ${itemId} not found in thread ${threadId}`);
|
|
211
|
+
this.itemId = itemId;
|
|
212
|
+
this.threadId = threadId;
|
|
213
|
+
this.name = "CodexForkRewindPointNotFoundError";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function hasUserAttachments(item) {
|
|
217
|
+
const content = item.content;
|
|
218
|
+
return Array.isArray(content) && content.some((part) => !part || typeof part !== "object" || part.type !== "text" || typeof part.text === "string" && part.text.includes("<attached_files>"));
|
|
219
|
+
}
|
|
220
|
+
function rejectAttachedTarget(thread, itemId) {
|
|
221
|
+
const item = thread.turns?.flatMap((turn) => turn.items ?? []).find((item2) => item2.id === itemId);
|
|
222
|
+
if (item && hasUserAttachments(item)) throw new Error("Cannot edit and rerun a message with attachments");
|
|
223
|
+
}
|
|
224
|
+
function textFromUserItem(item) {
|
|
225
|
+
if (item.type !== "userMessage") {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const content = item.content;
|
|
229
|
+
if (!Array.isArray(content)) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const textParts = content.filter((part) => Boolean(part) && typeof part === "object" && part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim();
|
|
233
|
+
return textParts.length > 0 ? textParts : null;
|
|
234
|
+
}
|
|
235
|
+
function timestampFromTurn(turn) {
|
|
236
|
+
const seconds = turn.startedAt ?? turn.completedAt;
|
|
237
|
+
return typeof seconds === "number" && Number.isFinite(seconds) ? seconds * 1e3 : Date.now();
|
|
238
|
+
}
|
|
239
|
+
function listCodexRewindPoints(thread) {
|
|
240
|
+
const points = [];
|
|
241
|
+
for (const turn of thread.turns ?? []) {
|
|
242
|
+
for (const item of turn.items ?? []) {
|
|
243
|
+
const text = textFromUserItem(item);
|
|
244
|
+
if (!text) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
points.push({
|
|
248
|
+
itemId: item.id,
|
|
249
|
+
text,
|
|
250
|
+
...hasUserAttachments(item) ? { hasAttachments: true } : {},
|
|
251
|
+
timestamp: timestampFromTurn(turn)
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return points;
|
|
256
|
+
}
|
|
257
|
+
function findCutTurn(thread, itemId) {
|
|
258
|
+
const turns = thread.turns ?? [];
|
|
259
|
+
for (let index = 0; index < turns.length; index++) {
|
|
260
|
+
const turn = turns[index];
|
|
261
|
+
const item = (turn.items ?? []).find((candidate) => candidate.id === itemId);
|
|
262
|
+
if (!item) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const text = textFromUserItem(item);
|
|
266
|
+
if (text) {
|
|
267
|
+
return { index, text };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
async function forkCodexThread(client, opts) {
|
|
273
|
+
const forked = await client.forkThread({
|
|
274
|
+
threadId: opts.threadId,
|
|
275
|
+
...opts.cwd ? { cwd: opts.cwd } : {},
|
|
276
|
+
...opts.model ? { model: opts.model } : {},
|
|
277
|
+
...opts.approvalPolicy ? { approvalPolicy: opts.approvalPolicy } : {},
|
|
278
|
+
...opts.sandbox ? { sandbox: opts.sandbox } : {},
|
|
279
|
+
...opts.mcpServers ? { mcpServers: opts.mcpServers } : {}
|
|
280
|
+
});
|
|
281
|
+
if (opts.cutAfterItemId) {
|
|
282
|
+
const cutTurn = findCutTurn(forked.thread, opts.cutAfterItemId);
|
|
283
|
+
if (!cutTurn) {
|
|
284
|
+
throw new CodexForkRewindPointNotFoundError(opts.cutAfterItemId, opts.threadId);
|
|
285
|
+
}
|
|
286
|
+
const turns = forked.thread.turns ?? [];
|
|
287
|
+
const turnsToDrop = turns.length - cutTurn.index;
|
|
288
|
+
if (turnsToDrop > 0) {
|
|
289
|
+
await client.rollbackThread({
|
|
290
|
+
threadId: forked.threadId,
|
|
291
|
+
numTurns: turnsToDrop
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
await client.injectItems({
|
|
295
|
+
threadId: forked.threadId,
|
|
296
|
+
items: [{
|
|
297
|
+
type: "message",
|
|
298
|
+
role: "user",
|
|
299
|
+
content: [{ type: "input_text", text: cutTurn.text }]
|
|
300
|
+
}]
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
type: "success",
|
|
305
|
+
newCodexThreadId: forked.threadId
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async function forkCodexBeforeUserMessage(client, opts) {
|
|
309
|
+
const { thread: source } = await client.readThread({ threadId: opts.threadId, includeTurns: true });
|
|
310
|
+
rejectAttachedTarget(source, opts.cutBeforeItemId);
|
|
311
|
+
const sourceCut = findCutTurn(source, opts.cutBeforeItemId);
|
|
312
|
+
if (!sourceCut) throw new CodexForkRewindPointNotFoundError(opts.cutBeforeItemId, opts.threadId);
|
|
313
|
+
if ((source.turns?.[sourceCut.index].items ?? []).findIndex((item) => item.id === opts.cutBeforeItemId) !== 0) {
|
|
314
|
+
throw new Error("Cannot safely rewind a user message inside an existing turn");
|
|
315
|
+
}
|
|
316
|
+
if (sourceCut.index === 0) return { type: "success", startFresh: true };
|
|
317
|
+
const forked = await client.forkThread({ threadId: opts.threadId, cwd: opts.cwd });
|
|
318
|
+
if (forked.threadId === opts.threadId) throw new Error("Codex did not create an independent fork");
|
|
319
|
+
rejectAttachedTarget(forked.thread, opts.cutBeforeItemId);
|
|
320
|
+
const cut = findCutTurn(forked.thread, opts.cutBeforeItemId);
|
|
321
|
+
if (!cut) throw new CodexForkRewindPointNotFoundError(opts.cutBeforeItemId, opts.threadId);
|
|
322
|
+
const turns = forked.thread.turns ?? [];
|
|
323
|
+
const items = turns[cut.index].items ?? [];
|
|
324
|
+
if (items.findIndex((item) => item.id === opts.cutBeforeItemId) !== 0) {
|
|
325
|
+
throw new Error("Cannot safely rewind a user message inside an existing turn");
|
|
326
|
+
}
|
|
327
|
+
if (cut.index === 0) return { type: "success", startFresh: true };
|
|
328
|
+
const expectedTurnIds = turns.slice(0, cut.index).map((turn) => turn.id);
|
|
329
|
+
const { thread } = await client.rollbackThread({
|
|
330
|
+
threadId: forked.threadId,
|
|
331
|
+
numTurns: turns.length - cut.index
|
|
332
|
+
});
|
|
333
|
+
const remaining = thread.turns ?? [];
|
|
334
|
+
if (remaining.length !== cut.index || remaining.some((turn, index) => turn.id !== expectedTurnIds[index])) {
|
|
335
|
+
throw new Error("Codex rollback did not preserve the expected history");
|
|
336
|
+
}
|
|
337
|
+
return { type: "success", newCodexThreadId: forked.threadId };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
exports.CodexForkRewindPointNotFoundError = CodexForkRewindPointNotFoundError;
|
|
341
|
+
exports.ForkSourceMissingError = ForkSourceMissingError;
|
|
342
|
+
exports.ForkTruncateUuidNotFoundError = ForkTruncateUuidNotFoundError;
|
|
343
|
+
exports.discardForkedSession = discardForkedSession;
|
|
344
|
+
exports.forkAndTruncateSession = forkAndTruncateSession;
|
|
345
|
+
exports.forkBeforeUserMessage = forkBeforeUserMessage;
|
|
346
|
+
exports.forkCodexBeforeUserMessage = forkCodexBeforeUserMessage;
|
|
347
|
+
exports.forkCodexThread = forkCodexThread;
|
|
348
|
+
exports.forkSession = forkSession;
|
|
349
|
+
exports.listClaudeRewindPoints = listClaudeRewindPoints;
|
|
350
|
+
exports.listCodexRewindPoints = listCodexRewindPoints;
|