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,235 @@
1
+ import os from 'node:os';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { p as piSessionNameFromTitle } from './terminalTitleSuggest-CeKsX_V6.mjs';
4
+ import { s as shellescape, t as tmuxArgs, a as scrubTmuxClientEnv } from './tmuxSocket-B8Yx20sT.mjs';
5
+
6
+ const USER_SESSION_FIELD_SEP = "<~|~>";
7
+ const USER_SESSIONS_FORMAT = [
8
+ "#{session_id}",
9
+ "#{session_name}",
10
+ "#{session_windows}",
11
+ "#{session_attached}",
12
+ "#{session_activity}",
13
+ "#{session_created}",
14
+ "#{pane_current_command}",
15
+ "#{pane_current_path}"
16
+ ].join(USER_SESSION_FIELD_SEP);
17
+ const TMUX_SESSION_ID_RE = /^\$\d{1,9}$/;
18
+ const TMUX_SESSION_NAME_MAX = 128;
19
+ function isSafeTmuxSessionName(name) {
20
+ if (typeof name !== "string") return false;
21
+ if (name.length === 0 || name.length > TMUX_SESSION_NAME_MAX) return false;
22
+ return !/[\x00-\x1f\x7f]/.test(name);
23
+ }
24
+ function isVhSessionName(name) {
25
+ return name.startsWith("vh-");
26
+ }
27
+ function parseUserSessionLine(line) {
28
+ if (!line) return void 0;
29
+ const parts = line.split(USER_SESSION_FIELD_SEP);
30
+ if (parts.length < 8) return void 0;
31
+ const [id, name, windows, attached, activity, created, command] = parts;
32
+ if (!TMUX_SESSION_ID_RE.test(id) || !isSafeTmuxSessionName(name)) return void 0;
33
+ const cwd = parts.slice(7).join(USER_SESSION_FIELD_SEP);
34
+ const num = (v) => {
35
+ const n = Number(v);
36
+ return Number.isFinite(n) ? n : void 0;
37
+ };
38
+ return {
39
+ id,
40
+ name,
41
+ windows: Math.max(0, Math.floor(num(windows) ?? 0)),
42
+ attached: (num(attached) ?? 0) > 0,
43
+ activityAt: activity ? (num(activity) ?? 0) * 1e3 || void 0 : void 0,
44
+ createdAt: created ? (num(created) ?? 0) * 1e3 || void 0 : void 0,
45
+ command: command.trim() || void 0,
46
+ cwd: cwd || void 0
47
+ };
48
+ }
49
+ const USER_SESSIONS_MAX = 50;
50
+ function parseUserSessions(stdout, max = USER_SESSIONS_MAX) {
51
+ const out = [];
52
+ for (const line of stdout.split("\n")) {
53
+ const s = parseUserSessionLine(line);
54
+ if (!s || isVhSessionName(s.name)) continue;
55
+ out.push(s);
56
+ }
57
+ out.sort((a, b) => (b.activityAt ?? 0) - (a.activityAt ?? 0));
58
+ return out.slice(0, max);
59
+ }
60
+ function attachStartupCommand(sessionId, socket) {
61
+ if (!TMUX_SESSION_ID_RE.test(sessionId)) throw new Error(`not a tmux session id: ${sessionId}`);
62
+ const sock = socket ? ` -S ${shellescape(socket)}` : "";
63
+ return ` TMUX= tmux${sock} attach-session -t ${shellescape(sessionId)}`;
64
+ }
65
+
66
+ const SHELL_COMMANDS = /* @__PURE__ */ new Set(["zsh", "bash", "fish", "sh", "dash", "ksh", "tcsh", "csh"]);
67
+ const TITLE_MAX_CHARS = 60;
68
+ const JUNK_TITLES = /* @__PURE__ */ new Set(["tmux", "claude", "node", ...SHELL_COMMANDS]);
69
+ function deriveAutoTitle(paneTitle, hostname, cwd) {
70
+ if (typeof paneTitle !== "string") return void 0;
71
+ const t = paneTitle.replace(/^[^\p{L}\p{N}]+/u, "").replace(/\s+/g, " ").trim();
72
+ if (!t) return void 0;
73
+ const piName = piSessionNameFromTitle(t, cwd);
74
+ if (piName) {
75
+ const chars2 = Array.from(piName);
76
+ return chars2.length > TITLE_MAX_CHARS ? chars2.slice(0, TITLE_MAX_CHARS).join("") : piName;
77
+ }
78
+ const lower = t.toLowerCase();
79
+ const host = (hostname || "").toLowerCase();
80
+ const shortHost = host.split(".")[0];
81
+ if (host && (lower === host || lower === shortHost)) return void 0;
82
+ if (JUNK_TITLES.has(lower)) return void 0;
83
+ const chars = Array.from(t);
84
+ return chars.length > TITLE_MAX_CHARS ? chars.slice(0, TITLE_MAX_CHARS).join("") : t;
85
+ }
86
+ const LIST_FIELD_SEP = "<~|~>";
87
+ const LIST_SESSIONS_FORMAT = [
88
+ "#{session_name}",
89
+ "#{session_created}",
90
+ "#{session_activity}",
91
+ "#{pane_current_path}",
92
+ "#{@vh_title}",
93
+ "#{@vh_title_manual}",
94
+ "#{@vh_tags}",
95
+ // B-273: the user tmux session this terminal was opened to attach (name),
96
+ // carried into close records so a restore can re-attach.
97
+ "#{@vh_attach}",
98
+ // B-287: the pane's REAL geometry, persisted with the live snapshot so a
99
+ // cold restore (daemon boot auto-restore, archive ↻) recreates the session
100
+ // at the size it last had — claude's one-shot welcome banner then never
101
+ // gets reflowed by the first web open (the "half-drawn logo").
102
+ "#{pane_width}",
103
+ "#{pane_height}",
104
+ // B-121: the control-mode client has no `pty.process`, so the agent-state
105
+ // fast path lost its live `#{pane_current_command}` equivalent. Carry it in
106
+ // the ONE list-sessions call the tracker already makes — the value goes
107
+ // from "live" to "≤ LIST_TRACK_INTERVAL_MS old", which classifyPane
108
+ // tolerates (its dialog/working judgments come from the pane TEXT; the
109
+ // command only separates shell/idle). MUST stay before pane_title: that
110
+ // field is deliberately last so a pathological 0x1f inside a title can only
111
+ // garble the title, never shift the fields.
112
+ "#{pane_current_command} #{pane_pid}",
113
+ "#{pane_title}"
114
+ ].join(LIST_FIELD_SEP);
115
+ function parseSessionListLine(line) {
116
+ if (!line) return void 0;
117
+ const parts = line.split(LIST_FIELD_SEP);
118
+ if (parts.length < 12) return void 0;
119
+ const [name, created, activity, cwd, vhTitle, manual, vhTags, vhAttach, paneW, paneH, paneCommand] = parts;
120
+ if (!name) return void 0;
121
+ const geom = parsePositiveInt(paneW) !== void 0 && parsePositiveInt(paneH) !== void 0 ? { paneCols: parsePositiveInt(paneW), paneRows: parsePositiveInt(paneH) } : {};
122
+ return {
123
+ name,
124
+ created: created ? Number(created) * 1e3 : void 0,
125
+ activity: activity ? Number(activity) * 1e3 : void 0,
126
+ cwd: cwd || void 0,
127
+ vhTitle: vhTitle.trim() || void 0,
128
+ manual: manual.trim().length > 0,
129
+ tags: parseTerminalTags(vhTags),
130
+ paneCurrentCommand: paneCommand.trim().replace(/ \d+$/, "") || void 0,
131
+ .../ (\d+)$/.test(paneCommand) ? { panePid: Number(paneCommand.match(/ (\d+)$/)[1]) } : {},
132
+ // Verbatim (no trim): tmux allows edge spaces in a session name and the
133
+ // restore lookup / attach echo compare it exactly.
134
+ attachTmux: isSafeTmuxSessionName(vhAttach) ? vhAttach : void 0,
135
+ ...geom,
136
+ // pane_title is last, so anything after field 11 is title content that
137
+ // contained the separator — rejoin it rather than dropping it.
138
+ paneTitle: parts.slice(11).join(LIST_FIELD_SEP) || void 0
139
+ };
140
+ }
141
+ function parsePositiveInt(raw) {
142
+ if (raw === void 0 || !/^[0-9]{1,5}$/.test(raw.trim())) return void 0;
143
+ const n = Number(raw.trim());
144
+ return n > 0 ? n : void 0;
145
+ }
146
+ const TERMINAL_TAG_MAX_COUNT = 64;
147
+ const TERMINAL_TAG_MAX_LENGTH = 24;
148
+ const TERMINAL_TAGS_MAX_BYTES = 4096;
149
+ function validateTerminalTags(value) {
150
+ if (!Array.isArray(value) || value.length > TERMINAL_TAG_MAX_COUNT) return void 0;
151
+ const seen = /* @__PURE__ */ new Set();
152
+ const out = [];
153
+ for (const valueTag of value) {
154
+ if (typeof valueTag !== "string") return void 0;
155
+ const tag = valueTag.trim().replace(/^#+/, "").trim().replace(/\s+/g, "-").slice(0, TERMINAL_TAG_MAX_LENGTH);
156
+ if (!tag || tag !== valueTag) return void 0;
157
+ const key = tag.toLowerCase();
158
+ if (seen.has(key)) return void 0;
159
+ seen.add(key);
160
+ out.push(tag);
161
+ }
162
+ if (Buffer.byteLength(JSON.stringify(out), "utf8") > TERMINAL_TAGS_MAX_BYTES) return void 0;
163
+ return out;
164
+ }
165
+ function parseTerminalTags(raw) {
166
+ if (typeof raw !== "string" || !raw.trim()) return [];
167
+ try {
168
+ return validateTerminalTags(JSON.parse(raw)) ?? [];
169
+ } catch {
170
+ return [];
171
+ }
172
+ }
173
+
174
+ const TMUX_TIMEOUT_MS = 3e3;
175
+ const VH_LIST_SESSIONS_FORMAT = LIST_SESSIONS_FORMAT;
176
+ function parseVhTerminals(stdout, hostname = os.hostname()) {
177
+ const out = [];
178
+ for (const line of stdout.split("\n")) {
179
+ const s = parseSessionListLine(line);
180
+ if (!s || !s.name.startsWith("vh-")) continue;
181
+ out.push({
182
+ id: s.name.slice(3),
183
+ title: s.vhTitle ?? deriveAutoTitle(s.paneTitle, hostname, s.cwd),
184
+ cwd: s.cwd,
185
+ createdAt: s.created,
186
+ activityAt: s.activity
187
+ });
188
+ }
189
+ return out;
190
+ }
191
+ function runTmux(args, input) {
192
+ try {
193
+ const r = spawnSync("tmux", tmuxArgs(args), {
194
+ encoding: "utf8",
195
+ timeout: TMUX_TIMEOUT_MS,
196
+ input,
197
+ env: scrubTmuxClientEnv({ ...process.env })
198
+ });
199
+ if (r.error) return { ok: false, stdout: "", error: r.error.message };
200
+ if (r.status !== 0) return { ok: false, stdout: r.stdout ?? "", error: (r.stderr || `tmux exited with ${r.status}`).trim() };
201
+ return { ok: true, stdout: r.stdout ?? "" };
202
+ } catch (error) {
203
+ return { ok: false, stdout: "", error: error instanceof Error ? error.message : String(error) };
204
+ }
205
+ }
206
+ function listVhTerminals() {
207
+ const r = runTmux(["list-sessions", "-F", VH_LIST_SESSIONS_FORMAT]);
208
+ if (!r.ok) {
209
+ if ((r.error ?? "").includes("no server running")) return { ok: true, terminals: [] };
210
+ return { ok: false, terminals: [], error: r.error };
211
+ }
212
+ return { ok: true, terminals: parseVhTerminals(r.stdout) };
213
+ }
214
+ function target(terminalId) {
215
+ return `=vh-${terminalId}:`;
216
+ }
217
+ function readVhTerminal(terminalId, lines) {
218
+ const n = Math.max(1, Math.min(2e3, Math.floor(lines)));
219
+ const r = runTmux(["capture-pane", "-p", "-t", target(terminalId), "-S", `-${n}`]);
220
+ return { ok: r.ok, text: r.stdout, error: r.error };
221
+ }
222
+ const PASTE_BUFFER_NAME = "vh-assistant-paste";
223
+ function sendToVhTerminal(terminalId, text, submit) {
224
+ const load = runTmux(["load-buffer", "-b", PASTE_BUFFER_NAME, "-"], text);
225
+ if (!load.ok) return { ok: false, error: `load-buffer failed: ${load.error}` };
226
+ const paste = runTmux(["paste-buffer", "-p", "-d", "-b", PASTE_BUFFER_NAME, "-t", target(terminalId)]);
227
+ if (!paste.ok) return { ok: false, error: `paste-buffer failed: ${paste.error}` };
228
+ if (submit) {
229
+ const enter = runTmux(["send-keys", "-t", target(terminalId), "Enter"]);
230
+ if (!enter.ok) return { ok: false, error: `send-keys Enter failed: ${enter.error}` };
231
+ }
232
+ return { ok: true };
233
+ }
234
+
235
+ export { LIST_SESSIONS_FORMAT as L, TMUX_SESSION_ID_RE as T, USER_SESSIONS_FORMAT as U, attachStartupCommand as a, isVhSessionName as b, parseSessionListLine as c, deriveAutoTitle as d, LIST_FIELD_SEP as e, isSafeTmuxSessionName as i, listVhTerminals as l, parseUserSessions as p, readVhTerminal as r, sendToVhTerminal as s, validateTerminalTags as v };
@@ -0,0 +1,38 @@
1
+ import { l as logger } from './logger-fXoNzNuB.mjs';
2
+
3
+ async function delay(ms) {
4
+ return new Promise((resolve) => setTimeout(resolve, ms));
5
+ }
6
+ function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
7
+ let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
8
+ return Math.round(Math.random() * maxDelayRet);
9
+ }
10
+ function createBackoff(opts) {
11
+ return async (callback) => {
12
+ let currentFailureCount = 0;
13
+ const minDelay = opts && opts.minDelay !== void 0 ? opts.minDelay : 250;
14
+ const maxDelay = opts && opts.maxDelay !== void 0 ? opts.maxDelay : 1e3;
15
+ const maxFailureCount = opts && opts.maxFailureCount !== void 0 ? opts.maxFailureCount : 50;
16
+ while (true) {
17
+ try {
18
+ return await callback();
19
+ } catch (e) {
20
+ if (currentFailureCount < maxFailureCount) {
21
+ currentFailureCount++;
22
+ }
23
+ if (opts && opts.onError) {
24
+ opts.onError(e, currentFailureCount);
25
+ }
26
+ let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
27
+ await delay(waitForRequest);
28
+ }
29
+ }
30
+ };
31
+ }
32
+ let backoff = createBackoff({
33
+ onError: (e, failuresCount) => {
34
+ logger.debug(`[BACKOFF] retry ${failuresCount}:`, e?.message || e);
35
+ }
36
+ });
37
+
38
+ export { backoff as b, delay as d, exponentialBackoffDelay as e };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ var logger = require('./logger-Bc8-u1w0.cjs');
4
+
5
+ async function delay(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
9
+ let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
10
+ return Math.round(Math.random() * maxDelayRet);
11
+ }
12
+ function createBackoff(opts) {
13
+ return async (callback) => {
14
+ let currentFailureCount = 0;
15
+ const minDelay = opts && opts.minDelay !== void 0 ? opts.minDelay : 250;
16
+ const maxDelay = opts && opts.maxDelay !== void 0 ? opts.maxDelay : 1e3;
17
+ const maxFailureCount = opts && opts.maxFailureCount !== void 0 ? opts.maxFailureCount : 50;
18
+ while (true) {
19
+ try {
20
+ return await callback();
21
+ } catch (e) {
22
+ if (currentFailureCount < maxFailureCount) {
23
+ currentFailureCount++;
24
+ }
25
+ if (opts && opts.onError) {
26
+ opts.onError(e, currentFailureCount);
27
+ }
28
+ let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
29
+ await delay(waitForRequest);
30
+ }
31
+ }
32
+ };
33
+ }
34
+ let backoff = createBackoff({
35
+ onError: (e, failuresCount) => {
36
+ logger.logger.debug(`[BACKOFF] retry ${failuresCount}:`, e?.message || e);
37
+ }
38
+ });
39
+
40
+ exports.backoff = backoff;
41
+ exports.delay = delay;
42
+ exports.exponentialBackoffDelay = exponentialBackoffDelay;
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ var node_child_process = require('node:child_process');
4
+ var module$1 = require('node:module');
5
+ var oneShotPrompts = require('./oneShotPrompts-CL0dip9C.cjs');
6
+ var node_crypto = require('node:crypto');
7
+ var path = require('node:path');
8
+ var logger = require('./logger-Bc8-u1w0.cjs');
9
+ var projectPath = require('./projectPath-ChLhdOyt.cjs');
10
+
11
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
12
+ const MAX_PROMPT_SAMPLE_CHARS = 500;
13
+ const MAX_TITLE_CHARS = 60;
14
+ const GENERATION_TIMEOUT_MS = 3e4;
15
+ function resolveClaudeBinary() {
16
+ try {
17
+ const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('titleGenerator-CFRhU-xq.cjs', document.baseURI).href)));
18
+ const utilsPath = path.resolve(path.join(projectPath.projectPath(), "scripts", "claude_version_utils.cjs"));
19
+ const { getClaudeCliPath } = require$1(utilsPath);
20
+ const path$1 = getClaudeCliPath();
21
+ if (typeof path$1 === "string" && path$1.length > 0) return path$1;
22
+ } catch (error) {
23
+ logger.logger.debug("[titleGenerator] getClaudeCliPath failed; falling back to PATH", { error: String(error) });
24
+ }
25
+ return process.env.HAPPY_CLAUDE_PATH || "claude";
26
+ }
27
+ function buildPrompt(firstUserMessage) {
28
+ const sample = firstUserMessage.slice(0, MAX_PROMPT_SAMPLE_CHARS);
29
+ return `${oneShotPrompts.TITLE_PROMPT_PREFIX} (match the message's language). Be concrete \u2014 name the actual task/subject. Do NOT output generic placeholders like "\u65B0\u5BF9\u8BDD"/"New chat"/"Conversation"/"Untitled". No quotes, no trailing punctuation. Reply with ONLY the title.
30
+
31
+ Message: "${sample}"`;
32
+ }
33
+ const GENERIC_TITLES = /* @__PURE__ */ new Set([
34
+ "new chat",
35
+ "new conversation",
36
+ "conversation",
37
+ "chat",
38
+ "untitled",
39
+ "new session",
40
+ "\u65B0\u5BF9\u8BDD",
41
+ "\u65B0\u4F1A\u8BDD",
42
+ "\u5BF9\u8BDD",
43
+ "\u4F1A\u8BDD",
44
+ "\u804A\u5929",
45
+ "\u672A\u547D\u540D"
46
+ ]);
47
+ function sanitizeTitle(raw) {
48
+ let title = raw.trim();
49
+ if (!title) return null;
50
+ const firstLine = title.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
51
+ if (!firstLine) return null;
52
+ title = firstLine;
53
+ title = title.replace(/^["'`]+/, "").replace(/["'`]+$/, "").trim();
54
+ title = title.replace(/[.。!?!?,,::;;]+$/, "").trim();
55
+ if (!title) return null;
56
+ if (title.length > MAX_TITLE_CHARS) {
57
+ title = title.slice(0, MAX_TITLE_CHARS).trim();
58
+ }
59
+ if (!title) return null;
60
+ if (GENERIC_TITLES.has(title.toLowerCase().replace(/\s+/g, " ").trim())) return null;
61
+ return title;
62
+ }
63
+ function runClaudeOneShot(binary, prompt) {
64
+ return new Promise((resolve2) => {
65
+ let settled = false;
66
+ const done = (value) => {
67
+ if (settled) return;
68
+ settled = true;
69
+ resolve2(value);
70
+ };
71
+ let child;
72
+ try {
73
+ child = node_child_process.spawn(binary, ["-p", "--model", "haiku", prompt], {
74
+ stdio: ["ignore", "pipe", "ignore"],
75
+ // HAPPY_MANAGED=1: this claude is run BY happy. The user-wide
76
+ // terminal-mirror SessionStart hook exits on it, so a one-shot
77
+ // spawned from inside a vh web terminal (VH_TERMINAL_ID
78
+ // inherited — B-500's pi terminal bridge, or `very-happy` typed
79
+ // in a terminal) never binds a mirror session to itself.
80
+ env: { ...process.env, HAPPY_MANAGED: "1" }
81
+ });
82
+ } catch (error) {
83
+ logger.logger.debug("[titleGenerator] spawn failed", { error: String(error) });
84
+ done(null);
85
+ return;
86
+ }
87
+ const timer = setTimeout(() => {
88
+ logger.logger.debug("[titleGenerator] generation timed out, killing claude one-shot");
89
+ try {
90
+ child.kill("SIGKILL");
91
+ } catch {
92
+ }
93
+ done(null);
94
+ }, GENERATION_TIMEOUT_MS);
95
+ let stdout = "";
96
+ child.stdout?.on("data", (chunk) => {
97
+ stdout += chunk.toString("utf8");
98
+ });
99
+ child.on("error", (error) => {
100
+ clearTimeout(timer);
101
+ logger.logger.debug("[titleGenerator] claude one-shot error", { error: String(error) });
102
+ done(null);
103
+ });
104
+ child.on("close", (code) => {
105
+ clearTimeout(timer);
106
+ if (code !== 0) {
107
+ logger.logger.debug(`[titleGenerator] claude one-shot exited with code ${code}`);
108
+ done(null);
109
+ return;
110
+ }
111
+ done(stdout);
112
+ });
113
+ });
114
+ }
115
+ async function suggestTitleForPrompt(prompt) {
116
+ const binary = resolveClaudeBinary();
117
+ if (!binary) {
118
+ logger.logger.debug("[titleGenerator] no claude binary; skipping auto-title");
119
+ return null;
120
+ }
121
+ const stdout = await runClaudeOneShot(binary, buildPrompt(prompt));
122
+ if (stdout == null) return null;
123
+ const title = sanitizeTitle(stdout);
124
+ if (!title) logger.logger.debug("[titleGenerator] empty/unusable title output; skipping");
125
+ return title;
126
+ }
127
+ class TitleGenerator {
128
+ constructor(session) {
129
+ this.session = session;
130
+ }
131
+ triggered = false;
132
+ /**
133
+ * Fire-and-forget. Returns immediately. Safe to call on every user
134
+ * message; only the first call on a session without an existing summary
135
+ * does anything.
136
+ */
137
+ maybeGenerate(firstUserMessage) {
138
+ if (this.triggered) return;
139
+ if (!firstUserMessage || firstUserMessage.trim().length === 0) return;
140
+ if (this.session.getMetadata()?.summary?.text) {
141
+ this.triggered = true;
142
+ return;
143
+ }
144
+ this.triggered = true;
145
+ void this.generate(firstUserMessage).catch((error) => {
146
+ logger.logger.debug("[titleGenerator] generation rejected", { error: String(error) });
147
+ });
148
+ }
149
+ async generate(firstUserMessage) {
150
+ if (this.session.getMetadata()?.summary?.text) {
151
+ logger.logger.debug("[titleGenerator] summary appeared before generation; skipping");
152
+ return;
153
+ }
154
+ const title = await suggestTitleForPrompt(firstUserMessage);
155
+ if (!title) return;
156
+ if (this.session.getMetadata()?.summary?.text) {
157
+ logger.logger.debug("[titleGenerator] summary set during generation; not overwriting");
158
+ return;
159
+ }
160
+ logger.logger.debug(`[titleGenerator] setting auto-title: ${title}`);
161
+ this.session.sendClaudeSessionMessage({
162
+ type: "summary",
163
+ summary: title,
164
+ leafUuid: node_crypto.randomUUID()
165
+ });
166
+ }
167
+ }
168
+
169
+ exports.TitleGenerator = TitleGenerator;
170
+ exports.resolveClaudeBinary = resolveClaudeBinary;
171
+ exports.runClaudeOneShot = runClaudeOneShot;
172
+ exports.suggestTitleForPrompt = suggestTitleForPrompt;
@@ -0,0 +1,166 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import { T as TITLE_PROMPT_PREFIX } from './oneShotPrompts-DT4p6RNd.mjs';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { resolve, join } from 'node:path';
6
+ import { l as logger } from './logger-fXoNzNuB.mjs';
7
+ import { p as projectPath } from './projectPath-BTBIMB57.mjs';
8
+
9
+ const MAX_PROMPT_SAMPLE_CHARS = 500;
10
+ const MAX_TITLE_CHARS = 60;
11
+ const GENERATION_TIMEOUT_MS = 3e4;
12
+ function resolveClaudeBinary() {
13
+ try {
14
+ const require = createRequire(import.meta.url);
15
+ const utilsPath = resolve(join(projectPath(), "scripts", "claude_version_utils.cjs"));
16
+ const { getClaudeCliPath } = require(utilsPath);
17
+ const path = getClaudeCliPath();
18
+ if (typeof path === "string" && path.length > 0) return path;
19
+ } catch (error) {
20
+ logger.debug("[titleGenerator] getClaudeCliPath failed; falling back to PATH", { error: String(error) });
21
+ }
22
+ return process.env.HAPPY_CLAUDE_PATH || "claude";
23
+ }
24
+ function buildPrompt(firstUserMessage) {
25
+ const sample = firstUserMessage.slice(0, MAX_PROMPT_SAMPLE_CHARS);
26
+ return `${TITLE_PROMPT_PREFIX} (match the message's language). Be concrete \u2014 name the actual task/subject. Do NOT output generic placeholders like "\u65B0\u5BF9\u8BDD"/"New chat"/"Conversation"/"Untitled". No quotes, no trailing punctuation. Reply with ONLY the title.
27
+
28
+ Message: "${sample}"`;
29
+ }
30
+ const GENERIC_TITLES = /* @__PURE__ */ new Set([
31
+ "new chat",
32
+ "new conversation",
33
+ "conversation",
34
+ "chat",
35
+ "untitled",
36
+ "new session",
37
+ "\u65B0\u5BF9\u8BDD",
38
+ "\u65B0\u4F1A\u8BDD",
39
+ "\u5BF9\u8BDD",
40
+ "\u4F1A\u8BDD",
41
+ "\u804A\u5929",
42
+ "\u672A\u547D\u540D"
43
+ ]);
44
+ function sanitizeTitle(raw) {
45
+ let title = raw.trim();
46
+ if (!title) return null;
47
+ const firstLine = title.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
48
+ if (!firstLine) return null;
49
+ title = firstLine;
50
+ title = title.replace(/^["'`]+/, "").replace(/["'`]+$/, "").trim();
51
+ title = title.replace(/[.。!?!?,,::;;]+$/, "").trim();
52
+ if (!title) return null;
53
+ if (title.length > MAX_TITLE_CHARS) {
54
+ title = title.slice(0, MAX_TITLE_CHARS).trim();
55
+ }
56
+ if (!title) return null;
57
+ if (GENERIC_TITLES.has(title.toLowerCase().replace(/\s+/g, " ").trim())) return null;
58
+ return title;
59
+ }
60
+ function runClaudeOneShot(binary, prompt) {
61
+ return new Promise((resolve2) => {
62
+ let settled = false;
63
+ const done = (value) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ resolve2(value);
67
+ };
68
+ let child;
69
+ try {
70
+ child = spawn(binary, ["-p", "--model", "haiku", prompt], {
71
+ stdio: ["ignore", "pipe", "ignore"],
72
+ // HAPPY_MANAGED=1: this claude is run BY happy. The user-wide
73
+ // terminal-mirror SessionStart hook exits on it, so a one-shot
74
+ // spawned from inside a vh web terminal (VH_TERMINAL_ID
75
+ // inherited — B-500's pi terminal bridge, or `very-happy` typed
76
+ // in a terminal) never binds a mirror session to itself.
77
+ env: { ...process.env, HAPPY_MANAGED: "1" }
78
+ });
79
+ } catch (error) {
80
+ logger.debug("[titleGenerator] spawn failed", { error: String(error) });
81
+ done(null);
82
+ return;
83
+ }
84
+ const timer = setTimeout(() => {
85
+ logger.debug("[titleGenerator] generation timed out, killing claude one-shot");
86
+ try {
87
+ child.kill("SIGKILL");
88
+ } catch {
89
+ }
90
+ done(null);
91
+ }, GENERATION_TIMEOUT_MS);
92
+ let stdout = "";
93
+ child.stdout?.on("data", (chunk) => {
94
+ stdout += chunk.toString("utf8");
95
+ });
96
+ child.on("error", (error) => {
97
+ clearTimeout(timer);
98
+ logger.debug("[titleGenerator] claude one-shot error", { error: String(error) });
99
+ done(null);
100
+ });
101
+ child.on("close", (code) => {
102
+ clearTimeout(timer);
103
+ if (code !== 0) {
104
+ logger.debug(`[titleGenerator] claude one-shot exited with code ${code}`);
105
+ done(null);
106
+ return;
107
+ }
108
+ done(stdout);
109
+ });
110
+ });
111
+ }
112
+ async function suggestTitleForPrompt(prompt) {
113
+ const binary = resolveClaudeBinary();
114
+ if (!binary) {
115
+ logger.debug("[titleGenerator] no claude binary; skipping auto-title");
116
+ return null;
117
+ }
118
+ const stdout = await runClaudeOneShot(binary, buildPrompt(prompt));
119
+ if (stdout == null) return null;
120
+ const title = sanitizeTitle(stdout);
121
+ if (!title) logger.debug("[titleGenerator] empty/unusable title output; skipping");
122
+ return title;
123
+ }
124
+ class TitleGenerator {
125
+ constructor(session) {
126
+ this.session = session;
127
+ }
128
+ triggered = false;
129
+ /**
130
+ * Fire-and-forget. Returns immediately. Safe to call on every user
131
+ * message; only the first call on a session without an existing summary
132
+ * does anything.
133
+ */
134
+ maybeGenerate(firstUserMessage) {
135
+ if (this.triggered) return;
136
+ if (!firstUserMessage || firstUserMessage.trim().length === 0) return;
137
+ if (this.session.getMetadata()?.summary?.text) {
138
+ this.triggered = true;
139
+ return;
140
+ }
141
+ this.triggered = true;
142
+ void this.generate(firstUserMessage).catch((error) => {
143
+ logger.debug("[titleGenerator] generation rejected", { error: String(error) });
144
+ });
145
+ }
146
+ async generate(firstUserMessage) {
147
+ if (this.session.getMetadata()?.summary?.text) {
148
+ logger.debug("[titleGenerator] summary appeared before generation; skipping");
149
+ return;
150
+ }
151
+ const title = await suggestTitleForPrompt(firstUserMessage);
152
+ if (!title) return;
153
+ if (this.session.getMetadata()?.summary?.text) {
154
+ logger.debug("[titleGenerator] summary set during generation; not overwriting");
155
+ return;
156
+ }
157
+ logger.debug(`[titleGenerator] setting auto-title: ${title}`);
158
+ this.session.sendClaudeSessionMessage({
159
+ type: "summary",
160
+ summary: title,
161
+ leafUuid: randomUUID()
162
+ });
163
+ }
164
+ }
165
+
166
+ export { TitleGenerator as T, runClaudeOneShot as a, resolveClaudeBinary as r, suggestTitleForPrompt as s };
@@ -0,0 +1,19 @@
1
+ function shellescape(s) {
2
+ return "'" + s.replace(/'/g, "'\\''") + "'";
3
+ }
4
+
5
+ const VH_TMUX_SOCKET_ENV = "VH_TMUX_SOCKET";
6
+ function tmuxSocketFlags(env = process.env) {
7
+ const socket = env[VH_TMUX_SOCKET_ENV];
8
+ return socket ? ["-S", socket] : [];
9
+ }
10
+ function tmuxArgs(args, env = process.env) {
11
+ return [...tmuxSocketFlags(env), ...args];
12
+ }
13
+ function scrubTmuxClientEnv(env) {
14
+ delete env.TMUX;
15
+ delete env.TMUX_PANE;
16
+ return env;
17
+ }
18
+
19
+ export { VH_TMUX_SOCKET_ENV as V, scrubTmuxClientEnv as a, shellescape as s, tmuxArgs as t };