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.
Files changed (259) hide show
  1. package/dist/{AcpBackend-VsfkZiVF.mjs → AcpBackend-6U6pQgx1.mjs} +4 -2
  2. package/dist/{AcpBackend-DUYFlyfN.cjs → AcpBackend-Ba98QDif.cjs} +78 -76
  3. package/dist/{AcpSessionManager-Bdv8LWXh.cjs → AcpSessionManager-BAY1Pggx.cjs} +9 -9
  4. package/dist/{AcpSessionManager-rYtVb0kN.mjs → AcpSessionManager-CoMWFVw3.mjs} +1 -1
  5. package/dist/BasePermissionHandler-2R0uH2CI.cjs +157 -0
  6. package/dist/BasePermissionHandler-BvvhBTja.mjs +155 -0
  7. package/dist/BaseReasoningProcessor-DyhO1uoU.mjs +198 -0
  8. package/dist/BaseReasoningProcessor-fsVNmCEF.cjs +200 -0
  9. package/dist/RemoteModeDisplay-D-HewyXg.mjs +150 -0
  10. package/dist/RemoteModeDisplay-NC1gPHtj.cjs +153 -0
  11. package/dist/agentAttachments-Dbw8iMCZ.cjs +20 -0
  12. package/dist/agentAttachments-M_B5qEF2.mjs +18 -0
  13. package/dist/agentGuidance-B5NATuFa.cjs +28 -0
  14. package/dist/agentGuidance-CQuNlNx8.mjs +20 -0
  15. package/dist/{agentHome-CvsJyeQB.mjs → agentHome-Ck1i1TQB.mjs} +8 -22
  16. package/dist/{agentHome-CHh4lmjM.cjs → agentHome-DvNnGphb.cjs} +14 -28
  17. package/dist/api-B2vLMS-z.cjs +3346 -0
  18. package/dist/api-SffjRQ1b.mjs +3339 -0
  19. package/dist/{types-Dh-s4Lqs.mjs → apiMachine-BpW9t8MO.mjs} +9116 -16205
  20. package/dist/{types-BbvK1Tr_.cjs → apiMachine-gFA54X7y.cjs} +9104 -16247
  21. package/dist/assistantTools-BfA0R3_m.cjs +380 -0
  22. package/dist/assistantTools-CgiTbnWb.mjs +376 -0
  23. package/dist/attachmentContent-CSGxr71v.mjs +82 -0
  24. package/dist/attachmentContent-DMMZzevR.cjs +88 -0
  25. package/dist/attachmentLimits-CzVrqgLR.cjs +354 -0
  26. package/dist/attachmentLimits-yba1uHFO.mjs +348 -0
  27. package/dist/auth-Bt4ceBBy.cjs +227 -0
  28. package/dist/auth-CYHjQ8oQ.mjs +225 -0
  29. package/dist/auth-DdyVZSo6.mjs +218 -0
  30. package/dist/auth-x4CEoysH.cjs +220 -0
  31. package/dist/authRelay-BdYkC1Po.cjs +156 -0
  32. package/dist/authRelay-DSAvg1uv.mjs +143 -0
  33. package/dist/{auto-DKmEls8Z.cjs → auto-BDXDUnjB.cjs} +70 -79
  34. package/dist/{auto-nb8GhRRy.mjs → auto-CXLlRi3a.mjs} +27 -36
  35. package/dist/backgroundTaskActivity-8PdPxrvy.mjs +149 -0
  36. package/dist/backgroundTaskActivity-CBvUdj39.cjs +157 -0
  37. package/dist/bootstrap-BNQq05jR.mjs +112 -0
  38. package/dist/bootstrap-CkgIq8N3.cjs +116 -0
  39. package/dist/browser-9pyhQr1R.mjs +20 -0
  40. package/dist/browser-BDOlKWYe.cjs +22 -0
  41. package/dist/claudeCliPath-BqkaH2oR.mjs +8 -0
  42. package/dist/claudeCliPath-U4HHmV4r.cjs +10 -0
  43. package/dist/claudeLocal-BDuGFnjJ.cjs +343 -0
  44. package/dist/claudeLocal-CQa8qnwq.mjs +339 -0
  45. package/dist/claudeLocalLauncher-Bd5I2-j6.cjs +154 -0
  46. package/dist/claudeLocalLauncher-CDqQI9ak.mjs +152 -0
  47. package/dist/cliUpdate-DsKXPFYp.cjs +120 -0
  48. package/dist/cliUpdate-Y34VGe9Y.mjs +112 -0
  49. package/dist/client-Brn-UwVk.mjs +100 -0
  50. package/dist/client-CXb1VJui.mjs +112 -0
  51. package/dist/client-C_axwRry.cjs +104 -0
  52. package/dist/client-DOBWJL3u.cjs +119 -0
  53. package/dist/codex/happyMcpStdioBridge.cjs +33 -14
  54. package/dist/codex/happyMcpStdioBridge.mjs +26 -7
  55. package/dist/codexAppServerClient-ZzhUTWPo.cjs +1090 -0
  56. package/dist/codexAppServerClient-oeQKD6Qf.mjs +1088 -0
  57. package/dist/codexCommand-C7H0NpBe.cjs +2088 -0
  58. package/dist/codexCommand-CJ82bl3l.mjs +2086 -0
  59. package/dist/codexThreadFork-BCaEMBiM.mjs +338 -0
  60. package/dist/codexThreadFork-BZqF-9bX.cjs +350 -0
  61. package/dist/{config-ClLtM6F3.mjs → config-BglnktIo.mjs} +7 -40
  62. package/dist/{config-DLx-vW3I.cjs → config-CYpL1Ahd.cjs} +26 -59
  63. package/dist/configuration-DoWFJUPm.mjs +170 -0
  64. package/dist/configuration-v9f8LM9T.cjs +181 -0
  65. package/dist/connect-hgcNjyAB.mjs +682 -0
  66. package/dist/connect-yEvjdRvx.cjs +684 -0
  67. package/dist/constants-BC4St0xd.mjs +11 -0
  68. package/dist/constants-VJ0N6WuG.cjs +17 -0
  69. package/dist/contentLogMetadata-BlWI0egu.mjs +51 -0
  70. package/dist/contentLogMetadata-ZSFk_NjQ.cjs +54 -0
  71. package/dist/context-B45vMtSZ.cjs +69 -0
  72. package/dist/context-C9GHPLEm.mjs +65 -0
  73. package/dist/controlClient-DjoKTtgK.mjs +260 -0
  74. package/dist/controlClient-fEHE85bH.cjs +279 -0
  75. package/dist/createSessionMetadata-BRLB9CNO.cjs +58 -0
  76. package/dist/createSessionMetadata-mIeehclp.mjs +54 -0
  77. package/dist/defaultPermissionMode-BnxJPVHT.mjs +4 -0
  78. package/dist/defaultPermissionMode-CxmTXUQR.cjs +7 -0
  79. package/dist/detectCLI-CSGMlV51.mjs +58 -0
  80. package/dist/detectCLI-DR9ZaN6B.cjs +61 -0
  81. package/dist/deterministicJson-DKDP_mM-.cjs +507 -0
  82. package/dist/deterministicJson-p835AdAU.mjs +502 -0
  83. package/dist/doctor-BPkYWSuq.mjs +75 -0
  84. package/dist/doctor-BeUuQXFs.mjs +391 -0
  85. package/dist/doctor-hVNnSZJn.cjs +395 -0
  86. package/dist/doctor-u46qYYKX.cjs +79 -0
  87. package/dist/doctorReadiness-BCZGcN0a.cjs +151 -0
  88. package/dist/doctorReadiness-C208SMN9.mjs +142 -0
  89. package/dist/editPaths-BVfS_FFL.mjs +68 -0
  90. package/dist/editPaths-Bqp4OFPw.cjs +73 -0
  91. package/dist/ensureDaemonRunning-CO7d1gOC.mjs +61 -0
  92. package/dist/ensureDaemonRunning-Dg9aZfjs.cjs +64 -0
  93. package/dist/handleResumeCommand-BiTPK0qd.mjs +240 -0
  94. package/dist/handleResumeCommand-DnVmy7a3.cjs +245 -0
  95. package/dist/ids-B6ZC0HGt.cjs +13 -0
  96. package/dist/ids-DU6sLx4C.mjs +10 -0
  97. package/dist/index-B6UGRQ7h.mjs +102 -0
  98. package/dist/{index-2mCaeKyf.mjs → index-CAFwetLk.mjs} +68 -38
  99. package/dist/index-Cqx-Raq6.mjs +68 -0
  100. package/dist/index-D3kI0hDR.cjs +107 -0
  101. package/dist/index-DA7yINuk.cjs +994 -0
  102. package/dist/{index-BEOMECtE.cjs → index-LIn6nCcq.cjs} +121 -91
  103. package/dist/index-Pf2cV9cn.cjs +182 -0
  104. package/dist/index-SPGPenK5.mjs +158 -0
  105. package/dist/index-gNTWtSKN.cjs +71 -0
  106. package/dist/index-qdS1friN.mjs +949 -0
  107. package/dist/index.cjs +5 -46
  108. package/dist/index.mjs +5 -46
  109. package/dist/{install-BPhJppvd.mjs → install-BO_W7VhL.mjs} +37 -37
  110. package/dist/install-Bu85vlHD.cjs +88 -0
  111. package/dist/install-ClUUl3yp.mjs +86 -0
  112. package/dist/{install-BOHrUaFM.cjs → install-Cs5qAPrB.cjs} +38 -38
  113. package/dist/{installPiTools-DuZ6jZvX.cjs → installPiTools-BKnXjrVu.cjs} +2 -3
  114. package/dist/{installPiTools-Cj7yU9Fc.mjs → installPiTools-B_CdM5_2.mjs} +1 -2
  115. package/dist/{installTerminalHooks-Cou3WUf3.cjs → installTerminalHooks-BQZbvxsZ.cjs} +5 -40
  116. package/dist/{installTerminalHooks-WeznjkuC.mjs → installTerminalHooks-By7gBMEF.mjs} +3 -38
  117. package/dist/lib.cjs +28 -19
  118. package/dist/lib.d.cts +12 -2
  119. package/dist/lib.d.mts +12 -2
  120. package/dist/lib.mjs +23 -14
  121. package/dist/lidState-C2dI0FAN.mjs +297 -0
  122. package/dist/lidState-KHHu1015.cjs +305 -0
  123. package/dist/limits-C-a7gr04.cjs +43 -0
  124. package/dist/limits-DIo7rEBR.mjs +36 -0
  125. package/dist/localHappyAgentAuth-BXiHPXdC.cjs +86 -0
  126. package/dist/localHappyAgentAuth-DCsetiyG.mjs +82 -0
  127. package/dist/logger-Bc8-u1w0.cjs +275 -0
  128. package/dist/logger-fXoNzNuB.mjs +271 -0
  129. package/dist/loginShellEnv-BkuYtExZ.cjs +182 -0
  130. package/dist/loginShellEnv-Dw8M7tkI.mjs +167 -0
  131. package/dist/machineMetadata-BHjehxJY.mjs +38 -0
  132. package/dist/machineMetadata-BibDMFVx.cjs +41 -0
  133. package/dist/main-BktM4A1K.mjs +939 -0
  134. package/dist/main-CLaX-t-b.cjs +941 -0
  135. package/dist/manager-BQ4flvQZ.mjs +101 -0
  136. package/dist/manager-Cly8xVIR.cjs +105 -0
  137. package/dist/{mcp-Dy3f_K0h.mjs → mcp-CeUd9KDr.mjs} +27 -33
  138. package/dist/{mcp-CPYbtsYG.cjs → mcp-GBfE70nk.cjs} +42 -48
  139. package/dist/messageBuffer-BWlGgR3A.mjs +71 -0
  140. package/dist/messageBuffer-fJMH6uY8.cjs +73 -0
  141. package/dist/mirrorProtocol-BNLtoWjO.mjs +170 -0
  142. package/dist/mirrorProtocol-BUtmg1ws.cjs +186 -0
  143. package/dist/oneShotPrompts-CL0dip9C.cjs +16 -0
  144. package/dist/oneShotPrompts-DT4p6RNd.mjs +12 -0
  145. package/dist/package-Cl5BEP5x.cjs +173 -0
  146. package/dist/package-uOg9SCKi.mjs +171 -0
  147. package/dist/path-B7zW-WwE.cjs +18 -0
  148. package/dist/path-D2wdaNSk.mjs +15 -0
  149. package/dist/peerTools-CIeiy9wn.cjs +283 -0
  150. package/dist/peerTools-Dz3_7Bkk.mjs +272 -0
  151. package/dist/permissionMode-CH4FoZfU.mjs +80 -0
  152. package/dist/permissionMode-CrV-XuMs.cjs +87 -0
  153. package/dist/persistence-CBNMzSa8.cjs +414 -0
  154. package/dist/persistence-oET5n61f.mjs +372 -0
  155. package/dist/piExtension.mjs +2 -1
  156. package/dist/piRunnerArgs-Bf7RUF__.cjs +66 -0
  157. package/dist/piRunnerArgs-Dk1ncwHF.mjs +59 -0
  158. package/dist/{piRuntime-NtF0YqjH.cjs → piRuntime-B_6TE9Eb.cjs} +4 -4
  159. package/dist/{piRuntime-kLZNpAAl.mjs → piRuntime-BwRdhK_N.mjs} +2 -2
  160. package/dist/{piTerminal-B0fEOV_2.mjs → piTerminal-Bap3R8Re.mjs} +32 -35
  161. package/dist/{piTerminal-9J0GAKzb.cjs → piTerminal-CagmEoez.cjs} +32 -35
  162. package/dist/previewPath-B0rBdhy1.cjs +83 -0
  163. package/dist/previewPath-CXN4Vag0.mjs +81 -0
  164. package/dist/projectPath-BTBIMB57.mjs +10 -0
  165. package/dist/projectPath-ChLhdOyt.cjs +13 -0
  166. package/dist/registerCommonHandlers-BjSV2Skz.cjs +538 -0
  167. package/dist/registerCommonHandlers-CrgvJsOy.mjs +536 -0
  168. package/dist/registerKillSessionHandler-6TC2S3lf.cjs +366 -0
  169. package/dist/registerKillSessionHandler-Cbc8Rgtd.mjs +363 -0
  170. package/dist/remoteSessionClient-BNx78NLb.cjs +634 -0
  171. package/dist/remoteSessionClient-CO7qOjR0.mjs +612 -0
  172. package/dist/resources-BdFBQi8r.mjs +117 -0
  173. package/dist/resources-JDPugcDq.cjs +120 -0
  174. package/dist/run-2l5vSt2h.mjs +5743 -0
  175. package/dist/run-D5xAEWSB.cjs +5766 -0
  176. package/dist/runClaude-BX6ixjtW.cjs +894 -0
  177. package/dist/runClaude-BgN6xo05.mjs +892 -0
  178. package/dist/runClaudeDeps-Bp20MScE.cjs +4168 -0
  179. package/dist/runClaudeDeps-COBBhD9o.mjs +4129 -0
  180. package/dist/{runGemini-F-mqx3_K.cjs → runGemini-CSMPkt0p.cjs} +202 -175
  181. package/dist/{runGemini-DV6Ouadt.mjs → runGemini-Cc2jq-uh.mjs} +58 -31
  182. package/dist/{runOpenClaw-D7MlPS5n.mjs → runOpenClaw-CIlCwdW2.mjs} +31 -32
  183. package/dist/{runOpenClaw-Qqo9j63M.cjs → runOpenClaw-YBwua4Rf.cjs} +64 -65
  184. package/dist/sandbox-C4gXLP1p.cjs +187 -0
  185. package/dist/sandbox-CNkwOtHI.mjs +180 -0
  186. package/dist/selfCheck-CU8xSDsl.cjs +87 -0
  187. package/dist/selfCheck-hJ6PCh7v.mjs +84 -0
  188. package/dist/{send-CLhYXNgy.mjs → send-CGHcKZ-w.mjs} +12 -5
  189. package/dist/{send-4qJTCAYW.cjs → send-DY9Nr-x4.cjs} +16 -9
  190. package/dist/server-II51Mimf.cjs +407 -0
  191. package/dist/server-m2b5pamZ.mjs +404 -0
  192. package/dist/sessionLock-7ePAYDOg.cjs +205 -0
  193. package/dist/sessionLock-Cw6FmbMv.mjs +197 -0
  194. package/dist/sessionModeFile-BVCV5f7E.cjs +36 -0
  195. package/dist/sessionModeFile-CDmDyRkD.mjs +32 -0
  196. package/dist/sessionOps-C_f1wKOr.mjs +760 -0
  197. package/dist/sessionOps-DJdExmBy.cjs +783 -0
  198. package/dist/sessionScanner-CaAHLyNV.mjs +206 -0
  199. package/dist/sessionScanner-D1goKdd3.cjs +209 -0
  200. package/dist/{sessions-Bh1pyddi.cjs → sessions-DGs6ALNo.cjs} +37 -28
  201. package/dist/{sessions-CzIH2rGp.mjs → sessions-DnvmWUel.mjs} +17 -8
  202. package/dist/setupOfflineReconnection-CVViqZft.mjs +72 -0
  203. package/dist/setupOfflineReconnection-R7XuR_sj.cjs +74 -0
  204. package/dist/smallFastModel-BLNgMrwR.mjs +18 -0
  205. package/dist/smallFastModel-D_uxGMo3.cjs +21 -0
  206. package/dist/{spawn-CmFOSVdW.cjs → spawn-CXTH7qg1.cjs} +61 -65
  207. package/dist/{spawn-CPIWlPOM.mjs → spawn-DNYSvFtC.mjs} +35 -40
  208. package/dist/spawnAgents-BGQiEF-w.cjs +52 -0
  209. package/dist/spawnAgents-DZzZ9jMI.mjs +45 -0
  210. package/dist/spawnHappyCLI-BhYj84rv.cjs +66 -0
  211. package/dist/spawnHappyCLI-cqWQbwsb.mjs +64 -0
  212. package/dist/specialCommands-BSqhYg5X.mjs +52 -0
  213. package/dist/specialCommands-DSsTeRej.cjs +54 -0
  214. package/dist/startFileWatcher-CKKx-8ZO.cjs +71 -0
  215. package/dist/startFileWatcher-CdegMbJj.mjs +69 -0
  216. package/dist/startupPlan-BLMbfQIH.cjs +12 -0
  217. package/dist/startupPlan-DFMyNgAs.mjs +10 -0
  218. package/dist/streamRelay-BiKw59Cc.cjs +233 -0
  219. package/dist/streamRelay-FPVbHpbM.mjs +231 -0
  220. package/dist/sync-CVk4QERO.cjs +71 -0
  221. package/dist/sync-CxD-w_40.mjs +69 -0
  222. package/dist/systemPrompt-BygJcmbi.cjs +97 -0
  223. package/dist/systemPrompt-DmtypUc7.mjs +94 -0
  224. package/dist/{teams-CEahj4cm.mjs → teams-BOC6nIPL.mjs} +9 -40
  225. package/dist/{teams-CB1R9-Z9.cjs → teams-BsvpOBfn.cjs} +14 -45
  226. package/dist/template-BaHAmH2K.cjs +129 -0
  227. package/dist/template-CiOPa2rd.mjs +121 -0
  228. package/dist/{terminalTitleSuggest-DKgX3osW.mjs → terminalTitleSuggest-CeKsX_V6.mjs} +1 -9
  229. package/dist/{terminalTitleSuggest-Dji8I0Jy.cjs → terminalTitleSuggest-DhvRFX3H.cjs} +0 -10
  230. package/dist/terminals-B_bfC9c2.cjs +250 -0
  231. package/dist/terminals-Dlg0x3nY.mjs +235 -0
  232. package/dist/time-BZNdNiIV.mjs +38 -0
  233. package/dist/time-CNRE4ChD.cjs +42 -0
  234. package/dist/titleGenerator-CFRhU-xq.cjs +172 -0
  235. package/dist/titleGenerator-ox6ptCsp.mjs +166 -0
  236. package/dist/tmuxSocket-B8Yx20sT.mjs +19 -0
  237. package/dist/tmuxSocket-D-Xt6Vn-.cjs +24 -0
  238. package/dist/{todo-DqyRI4wP.cjs → todo-B-KB7oAB.cjs} +15 -12
  239. package/dist/{todo-Cknp5D4U.mjs → todo-C5KBVbUw.mjs} +11 -8
  240. package/dist/tools-BZabNIid.mjs +177 -0
  241. package/dist/tools-BmQQUATY.mjs +60 -0
  242. package/dist/tools-CsSovyNp.cjs +180 -0
  243. package/dist/tools-lMDra4Pv.cjs +63 -0
  244. package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
  245. package/dist/trimIdent-BAjmv_TG.mjs +20 -0
  246. package/dist/types-CRS-srZK.cjs +66 -0
  247. package/dist/types-CkqAACXx.mjs +64 -0
  248. package/dist/uninstall-B1WLtC1d.mjs +47 -0
  249. package/dist/uninstall-fvA5aWpU.cjs +49 -0
  250. package/package.json +7 -7
  251. package/scripts/session_hook_forwarder.cjs +9 -2
  252. package/dist/agentGuidance-BRDN1NoZ.cjs +0 -2217
  253. package/dist/agentGuidance-KgvYuF3R.mjs +0 -2145
  254. package/dist/index-BF6WfgV5.mjs +0 -19624
  255. package/dist/index-C-JazrMB.mjs +0 -52
  256. package/dist/index-blDSadUR.cjs +0 -58
  257. package/dist/index-rH3DrQ5B.cjs +0 -19688
  258. package/dist/limits-BH5CV5vR.cjs +0 -2683
  259. package/dist/limits-CjRj8IX5.mjs +0 -2558
@@ -0,0 +1,1088 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { spawn } from 'cross-spawn';
3
+ import { createInterface } from 'node:readline';
4
+ import { l as logger } from './logger-fXoNzNuB.mjs';
5
+ import { c as contentLogMetadata, e as errorLogMetadata } from './contentLogMetadata-BlWI0egu.mjs';
6
+ import { i as initializeSandbox, w as wrapForMcpTransport } from './manager-BQ4flvQZ.mjs';
7
+ import { p as packageJson } from './package-uOg9SCKi.mjs';
8
+
9
+ function isAppServerAvailable() {
10
+ try {
11
+ const version = execSync("codex --version", { encoding: "utf8", windowsHide: true }).trim();
12
+ const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+)/);
13
+ if (!match) return false;
14
+ const [, ver] = match;
15
+ const [major, minor] = ver.split(".").map(Number);
16
+ return major > 0 || minor >= 100;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ function normalizeRawFileChangeList(changes) {
22
+ if (!Array.isArray(changes)) {
23
+ return void 0;
24
+ }
25
+ const normalized = {};
26
+ for (const change of changes) {
27
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
28
+ continue;
29
+ }
30
+ const path = typeof change.path === "string" ? change.path : null;
31
+ if (!path) {
32
+ continue;
33
+ }
34
+ const entry = {};
35
+ const changeRecord = change;
36
+ const kind = changeRecord.kind && typeof changeRecord.kind === "object" && !Array.isArray(changeRecord.kind) ? changeRecord.kind : null;
37
+ const type = typeof changeRecord.type === "string" ? changeRecord.type : typeof kind?.type === "string" ? kind.type : null;
38
+ const movePath = changeRecord.move_path ?? kind?.move_path ?? null;
39
+ if (kind) {
40
+ entry.kind = kind;
41
+ } else if (type) {
42
+ entry.kind = { type, move_path: movePath };
43
+ }
44
+ const diff = typeof changeRecord.diff === "string" ? changeRecord.diff : typeof changeRecord.unified_diff === "string" ? changeRecord.unified_diff : null;
45
+ if (diff !== null) {
46
+ entry.diff = diff;
47
+ }
48
+ if (changeRecord.add && typeof changeRecord.add === "object" && !Array.isArray(changeRecord.add)) {
49
+ entry.add = changeRecord.add;
50
+ }
51
+ if (changeRecord.modify && typeof changeRecord.modify === "object" && !Array.isArray(changeRecord.modify)) {
52
+ entry.modify = changeRecord.modify;
53
+ }
54
+ if (changeRecord.delete && typeof changeRecord.delete === "object" && !Array.isArray(changeRecord.delete)) {
55
+ entry.delete = changeRecord.delete;
56
+ }
57
+ const content = typeof changeRecord.content === "string" ? changeRecord.content : null;
58
+ if (type === "add" && content !== null) {
59
+ entry.add = { content };
60
+ }
61
+ if (type === "delete" && content !== null) {
62
+ entry.delete = { content };
63
+ }
64
+ const oldContent = typeof changeRecord.oldContent === "string" ? changeRecord.oldContent : typeof changeRecord.old_content === "string" ? changeRecord.old_content : null;
65
+ const newContent = typeof changeRecord.newContent === "string" ? changeRecord.newContent : typeof changeRecord.new_content === "string" ? changeRecord.new_content : null;
66
+ if ((oldContent !== null || newContent !== null) && type !== "add" && type !== "delete") {
67
+ entry.modify = {
68
+ old_content: oldContent ?? "",
69
+ new_content: newContent ?? ""
70
+ };
71
+ }
72
+ normalized[path] = entry;
73
+ }
74
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
75
+ }
76
+ class CodexAppServerClient {
77
+ process = null;
78
+ readline = null;
79
+ nextId = 1;
80
+ pending = /* @__PURE__ */ new Map();
81
+ processEpoch = 0;
82
+ connected = false;
83
+ sandboxConfig;
84
+ sandboxCleanup = null;
85
+ sandboxEnabled = false;
86
+ // Session state
87
+ _threadId = null;
88
+ _turnId = null;
89
+ threadDefaults = null;
90
+ // Turn completion tracking for the currently active sendTurnAndWait call.
91
+ // A completion event only resolves once we have seen task_started for this turn.
92
+ pendingTurnCompletion = null;
93
+ // Tracks in-flight interruptTurn() RPCs so sendTurnAndWait can wait for them
94
+ // before starting a new turn (prevents stale turn/interrupt from aborting the next turn).
95
+ pendingInterrupt = null;
96
+ // In-flight idle release (B-461); ensureConnected() waits for it so a message
97
+ // arriving mid-release cannot spawn a second app-server against a dying one.
98
+ idleRelease = null;
99
+ notificationProtocol = "unknown";
100
+ completedTurnIds = /* @__PURE__ */ new Set();
101
+ rawFileChangesByItemId = /* @__PURE__ */ new Map();
102
+ // Handlers set by the consumer (runCodex.ts)
103
+ eventHandler = null;
104
+ approvalHandler = null;
105
+ constructor(sandboxConfig) {
106
+ this.sandboxConfig = sandboxConfig;
107
+ }
108
+ get threadId() {
109
+ return this._threadId;
110
+ }
111
+ get turnId() {
112
+ return this._turnId;
113
+ }
114
+ setEventHandler(handler) {
115
+ this.eventHandler = handler;
116
+ }
117
+ setApprovalHandler(handler) {
118
+ this.approvalHandler = handler;
119
+ }
120
+ extractTurnId(params) {
121
+ const turnId = params?.turn?.id ?? params?.turnId ?? params?.turn_id ?? null;
122
+ return typeof turnId === "string" && turnId.length > 0 ? turnId : null;
123
+ }
124
+ extractTurnStatus(params) {
125
+ const status = params?.turn?.status ?? params?.status ?? null;
126
+ return typeof status === "string" && status.length > 0 ? status : null;
127
+ }
128
+ shouldHandleRawNotification(method) {
129
+ const isRawNotification = method === "thread/started" || method === "turn/started" || method === "turn/completed" || method === "thread/status/changed" || method === "thread/tokenUsage/updated" || method.startsWith("item/");
130
+ if (!isRawNotification) {
131
+ return false;
132
+ }
133
+ if (this.notificationProtocol === "legacy") {
134
+ return false;
135
+ }
136
+ if (this.notificationProtocol === "unknown") {
137
+ this.notificationProtocol = "raw";
138
+ }
139
+ return true;
140
+ }
141
+ emitRawTurnCompletion(turnId, status, error, source) {
142
+ const aborted = status === "cancelled" || status === "canceled" || status === "aborted" || status === "interrupted";
143
+ this.tryResolvePendingTurn(aborted, turnId, source);
144
+ this._turnId = null;
145
+ if (turnId && this.completedTurnIds.has(turnId)) {
146
+ return;
147
+ }
148
+ if (turnId) {
149
+ this.completedTurnIds.add(turnId);
150
+ }
151
+ if (aborted) {
152
+ this.eventHandler?.({
153
+ type: "turn_aborted",
154
+ ...turnId ? { turn_id: turnId } : {},
155
+ ...status ? { status } : {},
156
+ ...error !== void 0 && error !== null ? { error } : {}
157
+ });
158
+ return;
159
+ }
160
+ this.eventHandler?.({
161
+ type: "task_complete",
162
+ ...turnId ? { turn_id: turnId } : {},
163
+ ...status ? { status } : {},
164
+ ...error !== void 0 && error !== null ? { error } : {}
165
+ });
166
+ }
167
+ handleRawNotification(method, params) {
168
+ if (!this.shouldHandleRawNotification(method)) {
169
+ return false;
170
+ }
171
+ if (method === "turn/started") {
172
+ const turnId = this.extractTurnId(params);
173
+ if (turnId) {
174
+ this._turnId = turnId;
175
+ }
176
+ this.markPendingTurnStarted(turnId);
177
+ this.eventHandler?.({
178
+ type: "task_started",
179
+ ...turnId ? { turn_id: turnId } : {}
180
+ });
181
+ return true;
182
+ }
183
+ if (method === "turn/completed") {
184
+ this.emitRawTurnCompletion(
185
+ this.extractTurnId(params),
186
+ this.extractTurnStatus(params),
187
+ params?.turn?.error ?? params?.error,
188
+ method
189
+ );
190
+ return true;
191
+ }
192
+ if (method === "thread/status/changed") {
193
+ const statusType = params?.status?.type;
194
+ if (statusType === "idle" && this.pendingTurnCompletion) {
195
+ this.emitRawTurnCompletion(this._turnId, "completed", null, method);
196
+ }
197
+ return true;
198
+ }
199
+ if (method === "thread/tokenUsage/updated") {
200
+ const tokenUsage = params?.tokenUsage;
201
+ if (tokenUsage && typeof tokenUsage === "object") {
202
+ this.eventHandler?.({
203
+ type: "token_count",
204
+ ...tokenUsage
205
+ });
206
+ }
207
+ return true;
208
+ }
209
+ const item = params?.item;
210
+ if (!item || typeof item !== "object") {
211
+ return method.startsWith("item/");
212
+ }
213
+ if ((method === "item/started" || method === "item/completed") && (item.type === "collabAgentToolCall" || item.type === "subAgentActivity")) {
214
+ this.eventHandler?.({ type: "collab_agent_item", item });
215
+ return true;
216
+ }
217
+ if (method === "item/started" && item.type === "commandExecution") {
218
+ const callId = typeof item.id === "string" ? item.id : "";
219
+ this.eventHandler?.({
220
+ type: "exec_command_begin",
221
+ call_id: callId,
222
+ callId,
223
+ command: item.command,
224
+ cwd: item.cwd,
225
+ description: item.command
226
+ });
227
+ return true;
228
+ }
229
+ if (method === "item/completed" && item.type === "commandExecution") {
230
+ const callId = typeof item.id === "string" ? item.id : "";
231
+ this.eventHandler?.({
232
+ type: "exec_command_end",
233
+ call_id: callId,
234
+ callId,
235
+ output: item.aggregatedOutput ?? "",
236
+ exit_code: item.exitCode ?? null,
237
+ duration_ms: item.durationMs ?? null,
238
+ status: item.status,
239
+ cwd: item.cwd,
240
+ command: item.command
241
+ });
242
+ return true;
243
+ }
244
+ if (item.type === "fileChange") {
245
+ const callId = typeof item.id === "string" ? item.id : "";
246
+ const changes = normalizeRawFileChangeList(item.changes);
247
+ if (callId && changes) {
248
+ this.rawFileChangesByItemId.set(callId, changes);
249
+ }
250
+ if (method === "item/started") {
251
+ this.eventHandler?.({
252
+ type: "patch_apply_begin",
253
+ call_id: callId,
254
+ callId,
255
+ changes: changes ?? {}
256
+ });
257
+ return true;
258
+ }
259
+ if (method === "item/completed") {
260
+ this.eventHandler?.({
261
+ type: "patch_apply_end",
262
+ call_id: callId,
263
+ callId,
264
+ status: item.status
265
+ });
266
+ if (callId && (item.status === "completed" || item.status === "failed" || item.status === "declined")) {
267
+ this.rawFileChangesByItemId.delete(callId);
268
+ }
269
+ return true;
270
+ }
271
+ }
272
+ if (method === "item/completed" && item.type === "agentMessage") {
273
+ const text = typeof item.text === "string" ? item.text : "";
274
+ if (text.length > 0) {
275
+ this.eventHandler?.({
276
+ type: "agent_message",
277
+ message: text,
278
+ item_id: item.id,
279
+ phase: item.phase
280
+ });
281
+ }
282
+ if (item.phase === "final_answer" && this.pendingTurnCompletion) {
283
+ this.emitRawTurnCompletion(
284
+ this.extractTurnId(params),
285
+ "completed",
286
+ null,
287
+ `${method}:final_answer`
288
+ );
289
+ }
290
+ return true;
291
+ }
292
+ return method.startsWith("item/");
293
+ }
294
+ // ─── Lifecycle ──────────────────────────────────────────────
295
+ async connect() {
296
+ if (this.connected) return;
297
+ if (!isAppServerAvailable()) {
298
+ throw new Error(
299
+ "Codex CLI is not installed\n\nPlease install Codex CLI using one of these methods:\n\nOption 1 - npm (recommended):\n npm install -g @openai/codex\n\nOption 2 - Homebrew (macOS):\n brew install --cask codex\n\nAlternatively, use Claude Code:\n very-happy claude"
300
+ );
301
+ }
302
+ let command = "codex";
303
+ let args = ["app-server", "--listen", "stdio://"];
304
+ this.sandboxEnabled = false;
305
+ if (this.sandboxConfig?.enabled && process.platform !== "win32") {
306
+ try {
307
+ this.sandboxCleanup = await initializeSandbox(this.sandboxConfig, process.cwd());
308
+ const wrapped = await wrapForMcpTransport("codex", ["app-server", "--listen", "stdio://"]);
309
+ command = wrapped.command;
310
+ args = wrapped.args;
311
+ this.sandboxEnabled = true;
312
+ logger.info(`[CodexAppServer] Sandbox enabled`);
313
+ } catch (error) {
314
+ logger.warn("[CodexAppServer] Failed to initialize sandbox; continuing without.", error);
315
+ this.sandboxCleanup = null;
316
+ }
317
+ }
318
+ const env = {};
319
+ for (const [key, value] of Object.entries(process.env)) {
320
+ if (typeof value === "string") env[key] = value;
321
+ }
322
+ const filter = "codex_core::rollout::list=off";
323
+ if (!env.RUST_LOG) {
324
+ env.RUST_LOG = filter;
325
+ } else if (!env.RUST_LOG.includes("codex_core::rollout::list=")) {
326
+ env.RUST_LOG += `,${filter}`;
327
+ }
328
+ if (this.sandboxEnabled) {
329
+ env.CODEX_SANDBOX = "seatbelt";
330
+ }
331
+ logger.debug(`[CodexAppServer] Spawning: ${command} ${args.join(" ")}`);
332
+ const epoch = ++this.processEpoch;
333
+ const proc = spawn(command, args, {
334
+ stdio: ["pipe", "pipe", "pipe"],
335
+ env,
336
+ windowsHide: true
337
+ });
338
+ this.process = proc;
339
+ proc.on("error", (err) => {
340
+ logger.debug("[CodexAppServer] Process error:", err);
341
+ });
342
+ proc.on("exit", (code, signal) => {
343
+ logger.debug(`[CodexAppServer] Process exited: code=${code} signal=${signal}`);
344
+ if (this.process !== proc || this.processEpoch !== epoch) {
345
+ logger.debug("[CodexAppServer] Ignoring stale process exit");
346
+ return;
347
+ }
348
+ this.connected = false;
349
+ for (const [id, req] of this.pending) {
350
+ if (req.epoch !== epoch) continue;
351
+ req.reject(new Error(`Codex process exited (code=${code}) while waiting for ${req.method}`));
352
+ this.pending.delete(id);
353
+ }
354
+ this.resolvePendingTurn(true);
355
+ });
356
+ proc.stderr?.on("data", (chunk) => {
357
+ if (this.process !== proc || this.processEpoch !== epoch) return;
358
+ const text = chunk.toString().trim();
359
+ if (text) logger.debug("[CodexAppServer:stderr]", contentLogMetadata(text));
360
+ });
361
+ this.readline = createInterface({ input: proc.stdout });
362
+ this.readline.on("line", (line) => {
363
+ if (this.process !== proc || this.processEpoch !== epoch) return;
364
+ this.handleLine(line, epoch);
365
+ });
366
+ const initParams = {
367
+ clientInfo: {
368
+ name: "happy-codex",
369
+ title: "Happy Codex Client",
370
+ version: packageJson.version
371
+ },
372
+ capabilities: {
373
+ experimentalApi: true
374
+ }
375
+ };
376
+ await this.request("initialize", initParams);
377
+ this.notify("initialized");
378
+ this.connected = true;
379
+ logger.debug("[CodexAppServer] Connected and initialized");
380
+ }
381
+ async disconnectInternal(opts) {
382
+ if (!this.connected && !this.process) return;
383
+ const proc = this.process;
384
+ const pid = proc?.pid;
385
+ const epoch = this.processEpoch;
386
+ logger.debug(`[CodexAppServer] Disconnecting; pid=${pid ?? "none"}`);
387
+ this.readline?.close();
388
+ this.readline = null;
389
+ try {
390
+ proc?.stdin?.end();
391
+ proc?.kill("SIGTERM");
392
+ } catch {
393
+ }
394
+ if (pid) {
395
+ const killTimer = setTimeout(() => {
396
+ try {
397
+ process.kill(pid, 0);
398
+ process.kill(pid, "SIGKILL");
399
+ } catch {
400
+ }
401
+ }, 2e3);
402
+ killTimer.unref();
403
+ }
404
+ this.process = null;
405
+ this.connected = false;
406
+ this._turnId = null;
407
+ this.notificationProtocol = "unknown";
408
+ this.completedTurnIds.clear();
409
+ if (!opts?.preserveThreadState) {
410
+ this._threadId = null;
411
+ this.threadDefaults = null;
412
+ }
413
+ for (const [id, req] of this.pending) {
414
+ if (req.epoch !== epoch) continue;
415
+ req.reject(new Error(`Codex process disconnected while waiting for ${req.method}`));
416
+ this.pending.delete(id);
417
+ }
418
+ this.resolvePendingTurn(true);
419
+ if (this.sandboxCleanup) {
420
+ try {
421
+ await this.sandboxCleanup();
422
+ } catch {
423
+ }
424
+ this.sandboxCleanup = null;
425
+ }
426
+ this.sandboxEnabled = false;
427
+ logger.debug("[CodexAppServer] Disconnected");
428
+ }
429
+ async disconnect() {
430
+ await this.disconnectInternal();
431
+ }
432
+ /** True while an app-server child process is owned by this client. */
433
+ get hasProcess() {
434
+ return this.process !== null;
435
+ }
436
+ /**
437
+ * Drop the app-server process while the session is idle, keeping the thread
438
+ * id and defaults so the next turn can `thread/resume` it (B-461). Refuses —
439
+ * returning false — whenever anything is still in flight: an active turn, a
440
+ * pending interrupt, or any outstanding RPC. See codexIdleRelease.ts.
441
+ */
442
+ async releaseIdleProcess() {
443
+ if (this.idleRelease) return this.idleRelease;
444
+ if (!this.connected && !this.process) return false;
445
+ if (this.pendingTurnCompletion || this.pendingInterrupt || this.pending.size > 0) {
446
+ logger.debug("[CodexAppServer] Idle release skipped: work still in flight");
447
+ return false;
448
+ }
449
+ const run = (async () => {
450
+ const threadId = this._threadId;
451
+ logger.debug(`[CodexAppServer] Releasing idle app-server; thread=${threadId ?? "none"} kept for resume`);
452
+ await this.disconnectInternal({ preserveThreadState: !!threadId });
453
+ return true;
454
+ })();
455
+ this.idleRelease = run;
456
+ try {
457
+ return await run;
458
+ } finally {
459
+ if (this.idleRelease === run) this.idleRelease = null;
460
+ }
461
+ }
462
+ /**
463
+ * Make sure an app-server is running and, if a thread is remembered, resumed
464
+ * in it. Cheap no-op while connected. Covers both the idle release above and
465
+ * an app-server that exited on its own between turns.
466
+ */
467
+ async ensureConnected() {
468
+ if (this.idleRelease) await this.idleRelease;
469
+ if (this.connected) return;
470
+ const threadId = this._threadId;
471
+ if (this.process) {
472
+ await this.disconnectInternal({ preserveThreadState: !!threadId });
473
+ }
474
+ await this.connect();
475
+ if (threadId) {
476
+ await this.resumeThread({ threadId });
477
+ }
478
+ }
479
+ buildThreadConfig(mcpServers) {
480
+ return mcpServers ? { mcp_servers: mcpServers } : null;
481
+ }
482
+ rememberThreadDefaults(opts) {
483
+ this.threadDefaults = {
484
+ model: opts.model,
485
+ cwd: opts.cwd,
486
+ approvalPolicy: opts.approvalPolicy,
487
+ sandbox: opts.sandbox,
488
+ mcpServers: opts.mcpServers
489
+ };
490
+ }
491
+ // ─── Thread management ──────────────────────────────────────
492
+ async startThread(opts) {
493
+ await this.ensureConnected();
494
+ const params = {
495
+ model: opts.model ?? null,
496
+ modelProvider: null,
497
+ profile: null,
498
+ cwd: opts.cwd ?? process.cwd(),
499
+ approvalPolicy: opts.approvalPolicy ?? null,
500
+ sandbox: opts.sandbox ?? null,
501
+ config: this.buildThreadConfig(opts.mcpServers),
502
+ baseInstructions: null,
503
+ developerInstructions: null,
504
+ compactPrompt: null,
505
+ includeApplyPatchTool: null,
506
+ experimentalRawEvents: false,
507
+ persistExtendedHistory: true
508
+ };
509
+ const result = await this.request("thread/start", params);
510
+ this._threadId = result.thread.id;
511
+ this._turnId = null;
512
+ this.rememberThreadDefaults(opts);
513
+ logger.debug("[CodexAppServer] Thread started:", this._threadId);
514
+ return { threadId: result.thread.id, model: result.model };
515
+ }
516
+ async resumeThread(opts) {
517
+ const threadId = opts?.threadId ?? this._threadId;
518
+ if (!threadId) {
519
+ throw new Error("No thread available to resume.");
520
+ }
521
+ await this.ensureConnected();
522
+ const defaults = this.threadDefaults ?? {};
523
+ const params = {
524
+ threadId,
525
+ model: opts?.model ?? defaults.model ?? null,
526
+ modelProvider: null,
527
+ cwd: opts?.cwd ?? defaults.cwd ?? process.cwd(),
528
+ approvalPolicy: opts?.approvalPolicy ?? defaults.approvalPolicy ?? null,
529
+ sandbox: opts?.sandbox ?? defaults.sandbox ?? null,
530
+ config: this.buildThreadConfig(opts?.mcpServers ?? defaults.mcpServers),
531
+ baseInstructions: null,
532
+ developerInstructions: null,
533
+ persistExtendedHistory: true
534
+ };
535
+ const result = await this.request("thread/resume", params);
536
+ this._threadId = result.thread.id;
537
+ this._turnId = null;
538
+ this.rememberThreadDefaults({
539
+ model: opts?.model ?? defaults.model,
540
+ cwd: opts?.cwd ?? defaults.cwd,
541
+ approvalPolicy: opts?.approvalPolicy ?? defaults.approvalPolicy,
542
+ sandbox: opts?.sandbox ?? defaults.sandbox,
543
+ mcpServers: opts?.mcpServers ?? defaults.mcpServers
544
+ });
545
+ logger.debug("[CodexAppServer] Thread resumed:", this._threadId);
546
+ return { threadId: result.thread.id, model: result.model };
547
+ }
548
+ async forkThread(opts) {
549
+ await this.ensureConnected();
550
+ const defaults = this.threadDefaults ?? {};
551
+ const params = {
552
+ threadId: opts.threadId,
553
+ model: opts.model ?? defaults.model ?? null,
554
+ modelProvider: null,
555
+ cwd: opts.cwd ?? defaults.cwd ?? process.cwd(),
556
+ approvalPolicy: opts.approvalPolicy ?? defaults.approvalPolicy ?? null,
557
+ sandbox: opts.sandbox ?? defaults.sandbox ?? null,
558
+ config: this.buildThreadConfig(opts.mcpServers ?? defaults.mcpServers),
559
+ baseInstructions: null,
560
+ developerInstructions: null,
561
+ ephemeral: false,
562
+ threadSource: null
563
+ };
564
+ const result = await this.request("thread/fork", params);
565
+ this._threadId = result.thread.id;
566
+ this._turnId = null;
567
+ this.rememberThreadDefaults({
568
+ model: opts.model ?? defaults.model,
569
+ cwd: opts.cwd ?? defaults.cwd,
570
+ approvalPolicy: opts.approvalPolicy ?? defaults.approvalPolicy,
571
+ sandbox: opts.sandbox ?? defaults.sandbox,
572
+ mcpServers: opts.mcpServers ?? defaults.mcpServers
573
+ });
574
+ logger.debug("[CodexAppServer] Thread forked:", opts.threadId, "->", this._threadId);
575
+ return { threadId: result.thread.id, model: result.model, thread: result.thread };
576
+ }
577
+ async readThread(opts) {
578
+ await this.ensureConnected();
579
+ const params = {
580
+ threadId: opts.threadId,
581
+ includeTurns: opts.includeTurns ?? true
582
+ };
583
+ return await this.request("thread/read", params);
584
+ }
585
+ async rollbackThread(opts) {
586
+ await this.ensureConnected();
587
+ const params = {
588
+ threadId: opts.threadId,
589
+ numTurns: opts.numTurns
590
+ };
591
+ return await this.request("thread/rollback", params);
592
+ }
593
+ async injectItems(opts) {
594
+ await this.ensureConnected();
595
+ const params = {
596
+ threadId: opts.threadId,
597
+ items: opts.items
598
+ };
599
+ return await this.request("thread/inject_items", params);
600
+ }
601
+ async reconnectAndResumeThread() {
602
+ const threadId = this._threadId;
603
+ await this.disconnectInternal({ preserveThreadState: !!threadId });
604
+ await this.connect();
605
+ if (!threadId) {
606
+ return false;
607
+ }
608
+ try {
609
+ await this.resumeThread({ threadId });
610
+ return true;
611
+ } catch (error) {
612
+ logger.warn("[CodexAppServer] Failed to resume thread after reconnect", error);
613
+ this._threadId = null;
614
+ this.threadDefaults = null;
615
+ return false;
616
+ }
617
+ }
618
+ // ─── Turn management ────────────────────────────────────────
619
+ /** Default grace period after interrupt before forcing a restart (ms). */
620
+ static ABORT_GRACE_MS = 3e3;
621
+ hasPendingTurnCompletion() {
622
+ return this.pendingTurnCompletion !== null;
623
+ }
624
+ resolvePendingTurn(aborted) {
625
+ if (!this.pendingTurnCompletion) return;
626
+ this.pendingTurnCompletion.resolve(aborted);
627
+ this.pendingTurnCompletion = null;
628
+ }
629
+ markPendingTurnStarted(turnId) {
630
+ if (!this.pendingTurnCompletion) return;
631
+ if (turnId) {
632
+ this.pendingTurnCompletion.turnId = turnId;
633
+ }
634
+ }
635
+ tryResolvePendingTurn(aborted, turnId, source) {
636
+ const pending = this.pendingTurnCompletion;
637
+ if (!pending) return;
638
+ if (pending.turnId && turnId && pending.turnId !== turnId) {
639
+ logger.debug(
640
+ `[CodexAppServer] Ignoring ${source} for turn ${turnId}; awaiting ${pending.turnId}`
641
+ );
642
+ return;
643
+ }
644
+ this.resolvePendingTurn(aborted);
645
+ }
646
+ async waitForTurnCompletion(timeoutMs) {
647
+ if (!this.hasPendingTurnCompletion()) {
648
+ return true;
649
+ }
650
+ const deadline = Date.now() + Math.max(0, timeoutMs);
651
+ while (this.hasPendingTurnCompletion()) {
652
+ if (Date.now() >= deadline) {
653
+ return false;
654
+ }
655
+ await new Promise((resolve) => setTimeout(resolve, 25));
656
+ }
657
+ return true;
658
+ }
659
+ /**
660
+ * Request turn interruption and optionally force-restart the app-server if
661
+ * the turn does not settle within a short grace period.
662
+ */
663
+ async abortTurnWithFallback(opts) {
664
+ const hadActiveTurn = this.hasPendingTurnCompletion();
665
+ if (!hadActiveTurn) {
666
+ return { hadActiveTurn: false, aborted: false, forcedRestart: false, resumedThread: false };
667
+ }
668
+ await this.interruptTurn();
669
+ const gracePeriodMs = opts?.gracePeriodMs ?? CodexAppServerClient.ABORT_GRACE_MS;
670
+ const settled = await this.waitForTurnCompletion(gracePeriodMs);
671
+ if (settled) {
672
+ return { hadActiveTurn: true, aborted: true, forcedRestart: false, resumedThread: false };
673
+ }
674
+ const shouldForceRestart = opts?.forceRestartOnTimeout ?? true;
675
+ if (!shouldForceRestart) {
676
+ return { hadActiveTurn: true, aborted: false, forcedRestart: false, resumedThread: false };
677
+ }
678
+ logger.warn(`[CodexAppServer] interrupt did not settle turn in ${gracePeriodMs}ms; force-restarting app-server`);
679
+ const pendingTurnId = this.pendingTurnCompletion?.turnId ?? this._turnId;
680
+ if (this.pendingTurnCompletion) {
681
+ this.eventHandler?.({
682
+ type: "turn_aborted",
683
+ reason: "interrupted",
684
+ ...pendingTurnId ? { turn_id: pendingTurnId } : {},
685
+ forced_restart: true
686
+ });
687
+ }
688
+ const resumedThread = await this.reconnectAndResumeThread();
689
+ return { hadActiveTurn: true, aborted: true, forcedRestart: true, resumedThread };
690
+ }
691
+ /**
692
+ * Send a user turn and wait for it to complete.
693
+ * Returns when task_complete or turn_aborted is received.
694
+ */
695
+ async sendTurn(prompt, opts) {
696
+ if (!this._threadId) {
697
+ throw new Error("No active thread. Call startThread first.");
698
+ }
699
+ await this.ensureConnected();
700
+ const input = [
701
+ { type: "text", text: prompt },
702
+ ...(opts?.images ?? []).map((path) => ({ type: "localImage", path }))
703
+ ];
704
+ const params = {
705
+ threadId: this._threadId,
706
+ input
707
+ };
708
+ if (opts?.cwd) params.cwd = opts.cwd;
709
+ if (opts?.approvalPolicy) params.approvalPolicy = opts.approvalPolicy;
710
+ if (opts?.model) params.model = opts.model;
711
+ if (opts?.effort) params.effort = opts.effort;
712
+ if (opts?.sandbox) {
713
+ switch (opts.sandbox) {
714
+ case "workspace-write":
715
+ params.sandboxPolicy = { type: "workspaceWrite" };
716
+ break;
717
+ case "danger-full-access":
718
+ params.sandboxPolicy = { type: "dangerFullAccess" };
719
+ break;
720
+ case "read-only":
721
+ params.sandboxPolicy = { type: "readOnly" };
722
+ break;
723
+ }
724
+ }
725
+ const result = await this.request("turn/start", params);
726
+ const turnId = result?.turn?.id;
727
+ if (typeof turnId === "string" && turnId.length > 0) {
728
+ this._turnId = turnId;
729
+ if (this.pendingTurnCompletion) {
730
+ this.pendingTurnCompletion.turnId = turnId;
731
+ }
732
+ }
733
+ }
734
+ /** Default timeout for waiting on turn completion (ms). 10 minutes. */
735
+ static TURN_TIMEOUT_MS = 10 * 60 * 1e3;
736
+ /**
737
+ * Send a user turn and wait for it to complete (task_complete or turn_aborted).
738
+ * Returns { aborted: true } if the turn was aborted (user cancel, permission reject, etc.).
739
+ */
740
+ async sendTurnAndWait(prompt, opts) {
741
+ if (this.pendingInterrupt) {
742
+ await this.pendingInterrupt;
743
+ await new Promise((resolve) => setTimeout(resolve, 0));
744
+ }
745
+ await this.ensureConnected();
746
+ const timeoutMs = opts?.turnTimeoutMs ?? CodexAppServerClient.TURN_TIMEOUT_MS;
747
+ let timer = null;
748
+ const completion = new Promise((resolve) => {
749
+ this.pendingTurnCompletion = {
750
+ resolve,
751
+ turnId: null
752
+ };
753
+ timer = setTimeout(() => {
754
+ if (this.pendingTurnCompletion) {
755
+ logger.warn(`[CodexAppServer] Turn timed out after ${timeoutMs}ms \u2014 treating as abort`);
756
+ this.resolvePendingTurn(true);
757
+ }
758
+ }, timeoutMs);
759
+ });
760
+ try {
761
+ await this.sendTurn(prompt, opts);
762
+ } catch (err) {
763
+ if (timer) clearTimeout(timer);
764
+ this.pendingTurnCompletion = null;
765
+ throw err;
766
+ }
767
+ const aborted = await completion;
768
+ if (timer) clearTimeout(timer);
769
+ return { aborted };
770
+ }
771
+ async interruptTurn() {
772
+ if (!this._threadId) return;
773
+ if (!this._turnId) {
774
+ logger.debug("[CodexAppServer] interruptTurn: no active turnId, skipping");
775
+ return;
776
+ }
777
+ const params = {
778
+ threadId: this._threadId,
779
+ turnId: this._turnId
780
+ };
781
+ const doInterrupt = async () => {
782
+ try {
783
+ await this.request("turn/interrupt", params);
784
+ } catch (err) {
785
+ logger.debug("[CodexAppServer] interruptTurn error (may be expected):", err);
786
+ } finally {
787
+ this.pendingInterrupt = null;
788
+ }
789
+ };
790
+ this.pendingInterrupt = doInterrupt();
791
+ return this.pendingInterrupt;
792
+ }
793
+ // ─── State queries ──────────────────────────────────────────
794
+ hasActiveThread() {
795
+ return this._threadId !== null;
796
+ }
797
+ clearThreadState() {
798
+ logger.debug(
799
+ `[CodexAppServer] Clearing thread state: thread=${this._threadId ?? "none"} turn=${this._turnId ?? "none"}`
800
+ );
801
+ this.resolvePendingTurn(true);
802
+ this._threadId = null;
803
+ this._turnId = null;
804
+ this.threadDefaults = null;
805
+ this.completedTurnIds.clear();
806
+ this.rawFileChangesByItemId.clear();
807
+ }
808
+ // ─── JSON-RPC transport ─────────────────────────────────────
809
+ /** Default timeout for RPC requests (ms). */
810
+ static REQUEST_TIMEOUT_MS = 3e4;
811
+ async listModels() {
812
+ const models = [];
813
+ let cursor = null;
814
+ const seenCursors = /* @__PURE__ */ new Set();
815
+ do {
816
+ const response = await this.request("model/list", { limit: 100, cursor, includeHidden: false });
817
+ models.push(...response.data.filter((model) => !model.hidden));
818
+ cursor = response.nextCursor ?? null;
819
+ if (cursor && seenCursors.has(cursor)) throw new Error("Repeated model/list cursor");
820
+ if (cursor) seenCursors.add(cursor);
821
+ } while (cursor);
822
+ return models;
823
+ }
824
+ request(method, params, timeoutMs) {
825
+ const timeout = timeoutMs ?? CodexAppServerClient.REQUEST_TIMEOUT_MS;
826
+ return new Promise((resolve, reject) => {
827
+ if (!this.process?.stdin?.writable) {
828
+ reject(new Error(`Cannot send ${method}: stdin not writable`));
829
+ return;
830
+ }
831
+ const id = this.nextId++;
832
+ const timer = setTimeout(() => {
833
+ this.pending.delete(id);
834
+ reject(new Error(`${method} timed out after ${timeout}ms (id=${id})`));
835
+ }, timeout);
836
+ this.pending.set(id, {
837
+ resolve: (result) => {
838
+ clearTimeout(timer);
839
+ resolve(result);
840
+ },
841
+ reject: (err) => {
842
+ clearTimeout(timer);
843
+ reject(err);
844
+ },
845
+ method,
846
+ epoch: this.processEpoch
847
+ });
848
+ const msg = { jsonrpc: "2.0", id, method, params };
849
+ const line = JSON.stringify(msg) + "\n";
850
+ logger.debug(`[CodexAppServer] \u2192 ${method} (id=${id})`);
851
+ this.process.stdin.write(line);
852
+ });
853
+ }
854
+ notify(method, params) {
855
+ if (!this.process?.stdin?.writable) return;
856
+ const msg = { jsonrpc: "2.0", method, params };
857
+ this.process.stdin.write(JSON.stringify(msg) + "\n");
858
+ logger.debug(`[CodexAppServer] \u2192 ${method} (notification)`);
859
+ }
860
+ respond(id, result) {
861
+ if (!this.process?.stdin?.writable) return;
862
+ const msg = { jsonrpc: "2.0", id, result };
863
+ this.process.stdin.write(JSON.stringify(msg) + "\n");
864
+ logger.debug(`[CodexAppServer] \u2192 response (id=${id})`);
865
+ }
866
+ handleLine(line, sourceEpoch = this.processEpoch) {
867
+ if (sourceEpoch !== this.processEpoch) {
868
+ return;
869
+ }
870
+ if (!line.trim()) return;
871
+ let msg;
872
+ try {
873
+ msg = JSON.parse(line);
874
+ } catch {
875
+ logger.debug("[CodexAppServer] Non-JSON line received:", contentLogMetadata(line));
876
+ return;
877
+ }
878
+ if (msg.id != null && (msg.result !== void 0 || msg.error !== void 0)) {
879
+ const pending = this.pending.get(msg.id);
880
+ if (pending) {
881
+ if (pending.epoch !== sourceEpoch) {
882
+ logger.debug(`[CodexAppServer] Ignoring response from stale epoch for id=${msg.id}`);
883
+ return;
884
+ }
885
+ this.pending.delete(msg.id);
886
+ if (msg.error) {
887
+ pending.reject(new Error(`${pending.method}: ${msg.error.message} (code=${msg.error.code})`));
888
+ } else {
889
+ pending.resolve(msg.result);
890
+ }
891
+ }
892
+ return;
893
+ }
894
+ if (msg.id != null && msg.method) {
895
+ this.handleServerRequest(msg.id, msg.method, msg.params).catch((err) => {
896
+ logger.debug("[CodexAppServer] Error handling server request:", errorLogMetadata(err));
897
+ });
898
+ return;
899
+ }
900
+ if (msg.method) {
901
+ this.handleNotification(msg.method, msg.params);
902
+ return;
903
+ }
904
+ logger.debug("[CodexAppServer] Unhandled message:", contentLogMetadata(msg));
905
+ }
906
+ /**
907
+ * Map our internal ReviewDecision to the wire format the server expects.
908
+ * Server uses: accept, acceptForSession, decline, cancel
909
+ * Our handler uses: approved, approved_for_session, denied, abort
910
+ */
911
+ /**
912
+ * Map our internal ReviewDecision to the wire format codex expects.
913
+ * v2 methods (item/*) use: accept/acceptForSession/decline/cancel
914
+ * Legacy methods (execCommandApproval/applyPatchApproval) use: approved/approved_for_session/denied/abort
915
+ */
916
+ mapDecisionToWire(decision, legacy) {
917
+ if (typeof decision === "string") {
918
+ if (legacy) {
919
+ return decision;
920
+ }
921
+ switch (decision) {
922
+ case "approved":
923
+ return "accept";
924
+ case "approved_for_session":
925
+ return "acceptForSession";
926
+ case "denied":
927
+ return "decline";
928
+ case "abort":
929
+ return "cancel";
930
+ default:
931
+ return "decline";
932
+ }
933
+ }
934
+ if ("approved_execpolicy_amendment" in decision) {
935
+ return decision;
936
+ }
937
+ return legacy ? "denied" : "decline";
938
+ }
939
+ parseToolNameFromElicitationMessage(message) {
940
+ if (typeof message !== "string") {
941
+ return null;
942
+ }
943
+ const match = message.match(/tool "([^"]+)"/i);
944
+ return match?.[1] ?? null;
945
+ }
946
+ mapDecisionToMcpElicitationResponse(decision, params) {
947
+ if (typeof decision === "string") {
948
+ switch (decision) {
949
+ case "approved":
950
+ case "approved_for_session":
951
+ return {
952
+ action: "accept",
953
+ content: params?.mode === "form" ? {} : null,
954
+ _meta: null
955
+ };
956
+ case "abort":
957
+ return {
958
+ action: "cancel",
959
+ content: null,
960
+ _meta: null
961
+ };
962
+ case "denied":
963
+ default:
964
+ return {
965
+ action: "decline",
966
+ content: null,
967
+ _meta: null
968
+ };
969
+ }
970
+ }
971
+ return {
972
+ action: "decline",
973
+ content: null,
974
+ _meta: null
975
+ };
976
+ }
977
+ async handleServerRequest(id, method, params) {
978
+ if (method === "mcpServer/elicitation/request") {
979
+ const toolName = this.parseToolNameFromElicitationMessage(params?.message) ?? params?.serverName ?? "McpTool";
980
+ const decision = await this.handleApproval({
981
+ type: "mcp",
982
+ callId: `${params?.serverName ?? "mcp"}:${id}`,
983
+ toolName,
984
+ input: params?._meta?.tool_params ?? {},
985
+ serverName: params?.serverName,
986
+ message: params?.message
987
+ });
988
+ this.respond(id, this.mapDecisionToMcpElicitationResponse(decision, params));
989
+ return;
990
+ }
991
+ if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
992
+ const legacy = method === "execCommandApproval";
993
+ const callId = params.itemId ?? params.callId ?? String(id);
994
+ const decision = await this.handleApproval({
995
+ type: "exec",
996
+ callId,
997
+ command: params.command != null ? [params.command] : [],
998
+ cwd: params.cwd,
999
+ reason: params.reason
1000
+ });
1001
+ this.respond(id, { decision: this.mapDecisionToWire(decision, legacy) });
1002
+ return;
1003
+ }
1004
+ if (method === "item/fileChange/requestApproval" || method === "applyPatchApproval") {
1005
+ const legacy = method === "applyPatchApproval";
1006
+ const callId = params.itemId ?? params.callId ?? String(id);
1007
+ const decision = await this.handleApproval({
1008
+ type: "patch",
1009
+ callId,
1010
+ fileChanges: params.fileChanges ?? (typeof callId === "string" ? this.rawFileChangesByItemId.get(callId) : void 0),
1011
+ reason: params.reason
1012
+ });
1013
+ this.respond(id, { decision: this.mapDecisionToWire(decision, legacy) });
1014
+ return;
1015
+ }
1016
+ logger.debug(`[CodexAppServer] Unknown server request: ${method}`);
1017
+ this.respond(id, {});
1018
+ }
1019
+ async handleApproval(params) {
1020
+ if (this.approvalHandler) {
1021
+ try {
1022
+ return await this.approvalHandler(params);
1023
+ } catch (err) {
1024
+ logger.debug("[CodexAppServer] Approval handler error:", err);
1025
+ return "denied";
1026
+ }
1027
+ }
1028
+ return "denied";
1029
+ }
1030
+ handleNotification(method, params) {
1031
+ if (method === "codex/event" || method.startsWith("codex/event/")) {
1032
+ this.notificationProtocol = "legacy";
1033
+ const msg = params?.msg;
1034
+ if (msg) {
1035
+ if (msg.type === "task_started" && msg.turn_id) {
1036
+ this._turnId = msg.turn_id;
1037
+ }
1038
+ if (msg.type === "task_started") {
1039
+ this.markPendingTurnStarted(msg.turn_id ?? msg.turnId ?? null);
1040
+ }
1041
+ this.eventHandler?.(msg);
1042
+ if (msg.type === "task_complete" || msg.type === "turn_aborted") {
1043
+ const turnId = msg.turn_id ?? msg.turnId ?? null;
1044
+ if (turnId) {
1045
+ this.completedTurnIds.add(turnId);
1046
+ }
1047
+ this.tryResolvePendingTurn(
1048
+ msg.type === "turn_aborted",
1049
+ turnId,
1050
+ `codex/event/${msg.type}`
1051
+ );
1052
+ this._turnId = null;
1053
+ }
1054
+ }
1055
+ return;
1056
+ }
1057
+ if (this.handleRawNotification(method, params)) {
1058
+ logger.debug(`[CodexAppServer] Raw notification: ${method}`);
1059
+ return;
1060
+ }
1061
+ if (method === "thread/started" || method === "turn/started" || method === "turn/completed" || method === "thread/status/changed") {
1062
+ logger.debug(`[CodexAppServer] Lifecycle notification: ${method}`);
1063
+ if (method === "turn/started") {
1064
+ const turnId = this.extractTurnId(params);
1065
+ if (turnId) {
1066
+ this._turnId = turnId;
1067
+ }
1068
+ this.markPendingTurnStarted(turnId);
1069
+ }
1070
+ if (method === "turn/completed") {
1071
+ this.emitRawTurnCompletion(
1072
+ this.extractTurnId(params),
1073
+ this.extractTurnStatus(params),
1074
+ params?.turn?.error ?? params?.error,
1075
+ method
1076
+ );
1077
+ }
1078
+ return;
1079
+ }
1080
+ if (method === "mcpServer/startupStatus/updated") {
1081
+ logger.debug(`[CodexAppServer] mcpServer startup status:`, params);
1082
+ return;
1083
+ }
1084
+ logger.debug(`[CodexAppServer] Notification: ${method}`);
1085
+ }
1086
+ }
1087
+
1088
+ export { CodexAppServerClient as C };