very-happy-cli 0.2.158 → 0.2.160

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 (258) hide show
  1. package/dist/{AcpBackend-DIDRig5G.cjs → AcpBackend-Bm2rdke8.cjs} +78 -76
  2. package/dist/{AcpBackend-vDVLHuYJ.mjs → AcpBackend-mzv8Dhba.mjs} +4 -2
  3. package/dist/{AcpSessionManager-C44U99CC.cjs → AcpSessionManager-BAY1Pggx.cjs} +9 -9
  4. package/dist/{AcpSessionManager-CmRjfhn3.mjs → AcpSessionManager-CoMWFVw3.mjs} +1 -1
  5. package/dist/BasePermissionHandler-CFHAxumV.cjs +157 -0
  6. package/dist/BasePermissionHandler-m3WVholL.mjs +155 -0
  7. package/dist/BaseReasoningProcessor-BFa9_rZA.cjs +200 -0
  8. package/dist/BaseReasoningProcessor-CYrKBXGm.mjs +198 -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-Dw8Jx_CJ.cjs → agentHome-CboCeKWq.cjs} +14 -28
  16. package/dist/{agentHome-CyLmGH5L.mjs → agentHome-Dj1C43aJ.mjs} +8 -22
  17. package/dist/api-CLMUr-SX.mjs +3339 -0
  18. package/dist/api-CgundNNX.cjs +3346 -0
  19. package/dist/{types-DIBF4U0Y.mjs → apiMachine-B9dLW_p5.mjs} +9116 -16015
  20. package/dist/{types-DnrPeN80.cjs → apiMachine-Bn8482VM.cjs} +9104 -16057
  21. package/dist/assistantTools-BTqPnlFl.cjs +380 -0
  22. package/dist/assistantTools-D6tuc6o4.mjs +376 -0
  23. package/dist/attachmentContent-DpiTJZb4.cjs +88 -0
  24. package/dist/attachmentContent-_kxRznNf.mjs +82 -0
  25. package/dist/attachmentLimits-CzVrqgLR.cjs +354 -0
  26. package/dist/attachmentLimits-yba1uHFO.mjs +348 -0
  27. package/dist/auth-BDOVh5h5.cjs +220 -0
  28. package/dist/auth-Cwutmdsa.mjs +225 -0
  29. package/dist/auth-PYHZQNPp.mjs +218 -0
  30. package/dist/auth-ba4WMYrN.cjs +227 -0
  31. package/dist/authRelay-BdYkC1Po.cjs +156 -0
  32. package/dist/authRelay-DSAvg1uv.mjs +143 -0
  33. package/dist/{auto-DZbNcTXx.cjs → auto-BOX_fgmR.cjs} +70 -79
  34. package/dist/{auto-BlMZPwOk.mjs → auto-BXu14yP_.mjs} +27 -36
  35. package/dist/backgroundTaskActivity-8PdPxrvy.mjs +149 -0
  36. package/dist/backgroundTaskActivity-CBvUdj39.cjs +157 -0
  37. package/dist/bootstrap-B5G1TijC.cjs +116 -0
  38. package/dist/bootstrap-hxXPCFMx.mjs +112 -0
  39. package/dist/browser-Y6Bl8L_O.cjs +22 -0
  40. package/dist/browser-jh2P0UVZ.mjs +20 -0
  41. package/dist/claudeCliPath-BqkaH2oR.mjs +8 -0
  42. package/dist/claudeCliPath-U4HHmV4r.cjs +10 -0
  43. package/dist/claudeLocal-BaY4fZKi.mjs +339 -0
  44. package/dist/claudeLocal-CRGG4Pea.cjs +343 -0
  45. package/dist/claudeLocalLauncher-D4T7MTqd.mjs +152 -0
  46. package/dist/claudeLocalLauncher-DyEbVzUN.cjs +154 -0
  47. package/dist/cliUpdate-DsKXPFYp.cjs +120 -0
  48. package/dist/cliUpdate-Y34VGe9Y.mjs +112 -0
  49. package/dist/client-3Ac-O4pw.cjs +104 -0
  50. package/dist/client-BPNbmtvO.cjs +119 -0
  51. package/dist/client-CeGKqqcJ.mjs +100 -0
  52. package/dist/client-ClR0K8zX.mjs +112 -0
  53. package/dist/codex/happyMcpStdioBridge.cjs +33 -14
  54. package/dist/codex/happyMcpStdioBridge.mjs +26 -7
  55. package/dist/codexAppServerClient-I3teiV-N.mjs +1088 -0
  56. package/dist/codexAppServerClient-q2FjnRcK.cjs +1090 -0
  57. package/dist/codexCommand-4zcccIoq.cjs +2088 -0
  58. package/dist/codexCommand-Cc9gcbdX.mjs +2086 -0
  59. package/dist/codexThreadFork-BH9IZPqD.cjs +350 -0
  60. package/dist/codexThreadFork-slzAseLx.mjs +338 -0
  61. package/dist/{config-BtPArqRH.mjs → config-CTx_jb-C.mjs} +7 -40
  62. package/dist/{config-DG84bjwF.cjs → config-Ci9Kpgwa.cjs} +26 -59
  63. package/dist/configuration-BSYpcPwE.cjs +181 -0
  64. package/dist/configuration-RfeNjFmd.mjs +170 -0
  65. package/dist/connect-BpOdzNbE.mjs +682 -0
  66. package/dist/connect-C2CiOUFs.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-DsDr6E2t.cjs +69 -0
  72. package/dist/context-H0Bcb9S_.mjs +65 -0
  73. package/dist/controlClient-BGbDcgWS.mjs +260 -0
  74. package/dist/controlClient-Bcwv3D21.cjs +279 -0
  75. package/dist/createSessionMetadata-BJxEnM-o.cjs +58 -0
  76. package/dist/createSessionMetadata-CVVkGjxV.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-C5CYTVpm.cjs +507 -0
  82. package/dist/deterministicJson-CzkFJWw1.mjs +502 -0
  83. package/dist/doctor-BPkYWSuq.mjs +75 -0
  84. package/dist/doctor-D2YJ-Yn2.cjs +395 -0
  85. package/dist/doctor-DOvByjbk.mjs +391 -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-BCwNrMyv.cjs +64 -0
  92. package/dist/ensureDaemonRunning-Dqa2pOjY.mjs +61 -0
  93. package/dist/handleResumeCommand-BvXYf5El.mjs +240 -0
  94. package/dist/handleResumeCommand-C2cKchpX.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-DPCsWlWK.cjs → index-B6pAdvJx.cjs} +131 -91
  99. package/dist/index-Bu5Zi_a9.cjs +182 -0
  100. package/dist/index-D3kI0hDR.cjs +107 -0
  101. package/dist/index-DA7yINuk.cjs +994 -0
  102. package/dist/index-DDeHXrIX.cjs +71 -0
  103. package/dist/index-DKLU_fD0.mjs +158 -0
  104. package/dist/{index-ZChGsuB7.mjs → index-DMZqWwsP.mjs} +79 -39
  105. package/dist/index-DVkMGPyS.mjs +68 -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-DarS_Wco.mjs → install-BO_W7VhL.mjs} +37 -37
  110. package/dist/install-BgoC2e_2.cjs +88 -0
  111. package/dist/install-CFY3tYtJ.mjs +86 -0
  112. package/dist/{install-wKQGgXvN.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-DcQfee_O.cjs → installTerminalHooks-BQZbvxsZ.cjs} +5 -40
  116. package/dist/{installTerminalHooks-CpNbRFPz.mjs → installTerminalHooks-By7gBMEF.mjs} +3 -38
  117. package/dist/lib.cjs +28 -19
  118. package/dist/lib.d.cts +126 -2
  119. package/dist/lib.d.mts +126 -2
  120. package/dist/lib.mjs +23 -14
  121. package/dist/lidState-BeDfxxEX.cjs +305 -0
  122. package/dist/lidState-CeEt442o.mjs +297 -0
  123. package/dist/limits-C-a7gr04.cjs +43 -0
  124. package/dist/limits-DIo7rEBR.mjs +36 -0
  125. package/dist/localHappyAgentAuth-C-HQyiv6.mjs +82 -0
  126. package/dist/localHappyAgentAuth-D6rFhaoc.cjs +86 -0
  127. package/dist/logger-6HYUEXNk.mjs +271 -0
  128. package/dist/logger-CS0mw3fa.cjs +275 -0
  129. package/dist/loginShellEnv-BkuYtExZ.cjs +182 -0
  130. package/dist/loginShellEnv-Dw8M7tkI.mjs +167 -0
  131. package/dist/machineMetadata-DyDbODYd.cjs +41 -0
  132. package/dist/machineMetadata-oOzBfDfs.mjs +38 -0
  133. package/dist/main-BkxJP6JQ.cjs +941 -0
  134. package/dist/main-BwNIAI1A.mjs +939 -0
  135. package/dist/manager-BQ4flvQZ.mjs +101 -0
  136. package/dist/manager-Cly8xVIR.cjs +105 -0
  137. package/dist/{mcp-BN6bKIGb.mjs → mcp-B1A4I3TK.mjs} +27 -33
  138. package/dist/{mcp-DGdeUUDJ.cjs → mcp-Hszd7qpS.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-BhaKlnxz.mjs +171 -0
  146. package/dist/package-BwMrPwqH.cjs +173 -0
  147. package/dist/path-B7zW-WwE.cjs +18 -0
  148. package/dist/path-D2wdaNSk.mjs +15 -0
  149. package/dist/peerTools-BTqw97Gb.mjs +272 -0
  150. package/dist/peerTools-BvKtHXj8.cjs +283 -0
  151. package/dist/permissionMode-Cr5JCbB7.mjs +80 -0
  152. package/dist/permissionMode-CrV-XuMs.cjs +87 -0
  153. package/dist/persistence-HGW_ZVs9.mjs +372 -0
  154. package/dist/persistence-yKTH3-5X.cjs +414 -0
  155. package/dist/piExtension.mjs +2 -1
  156. package/dist/piRunnerArgs-BNloJCCg.cjs +66 -0
  157. package/dist/piRunnerArgs-BuX1UFWy.mjs +59 -0
  158. package/dist/{piRuntime-bkw2NAZU.mjs → piRuntime-CSGkwCKQ.mjs} +2 -2
  159. package/dist/{piRuntime-3qtbKujX.cjs → piRuntime-C_H7MJCM.cjs} +4 -4
  160. package/dist/{piTerminal-DilKwM7B.mjs → piTerminal-C8a1OrNQ.mjs} +32 -35
  161. package/dist/{piTerminal-BYxp3LMr.cjs → piTerminal-DNCZ6yTM.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-Ccvsz7Ou.mjs +536 -0
  167. package/dist/registerCommonHandlers-DskirN-_.cjs +538 -0
  168. package/dist/registerKillSessionHandler-DZeLXWcB.cjs +361 -0
  169. package/dist/registerKillSessionHandler-GYvHdipX.mjs +358 -0
  170. package/dist/remoteSessionClient-B0LraSB9.cjs +634 -0
  171. package/dist/remoteSessionClient-CYnXRynE.mjs +612 -0
  172. package/dist/resources-BdFBQi8r.mjs +117 -0
  173. package/dist/resources-JDPugcDq.cjs +120 -0
  174. package/dist/run-CJzxoHVh.cjs +5762 -0
  175. package/dist/run-D3ifupU3.mjs +5739 -0
  176. package/dist/runClaude-Ba01qT-u.mjs +848 -0
  177. package/dist/runClaude-Da3RirzE.cjs +850 -0
  178. package/dist/runClaudeDeps-DOt79DjU.mjs +3624 -0
  179. package/dist/runClaudeDeps-DOzEBMls.cjs +3649 -0
  180. package/dist/{runGemini-Cjii069p.mjs → runGemini-BGeTz57m.mjs} +58 -31
  181. package/dist/{runGemini-28O2sqyI.cjs → runGemini-DSr9LIxL.cjs} +202 -175
  182. package/dist/{runOpenClaw-CiMZad-H.cjs → runOpenClaw-B1mTxN35.cjs} +64 -65
  183. package/dist/{runOpenClaw-jaLr7KF1.mjs → runOpenClaw-CogSedIc.mjs} +31 -32
  184. package/dist/sandbox-CG7IyDTU.cjs +187 -0
  185. package/dist/sandbox-DTe6d54R.mjs +180 -0
  186. package/dist/selfCheck-C9mVrQwf.mjs +84 -0
  187. package/dist/selfCheck-Dc-mNSLb.cjs +87 -0
  188. package/dist/{send-D5ejHPXG.cjs → send-B7J_eaG3.cjs} +16 -9
  189. package/dist/{send-C9mOtChi.mjs → send-CBXJKc6O.mjs} +12 -5
  190. package/dist/server-CZTP_iOn.cjs +407 -0
  191. package/dist/server-DcVBv04l.mjs +404 -0
  192. package/dist/sessionLock-Bys1PNSo.cjs +205 -0
  193. package/dist/sessionLock-D-EG2CfG.mjs +197 -0
  194. package/dist/sessionModeFile-DMl3dw3r.mjs +32 -0
  195. package/dist/sessionModeFile-RSm0NQkH.cjs +36 -0
  196. package/dist/sessionOps-Dn8z85h_.cjs +783 -0
  197. package/dist/sessionOps-SuHW8Dfe.mjs +760 -0
  198. package/dist/sessionScanner-BOrGjMXL.cjs +209 -0
  199. package/dist/sessionScanner-CX3onqXh.mjs +206 -0
  200. package/dist/{sessions-W6771g3X.mjs → sessions-BEG_HTSi.mjs} +17 -8
  201. package/dist/{sessions-BNMPQoLE.cjs → sessions-D74aJbhL.cjs} +37 -28
  202. package/dist/setupOfflineReconnection-ByElY60m.cjs +74 -0
  203. package/dist/setupOfflineReconnection-DpvWTt69.mjs +72 -0
  204. package/dist/smallFastModel-BLNgMrwR.mjs +18 -0
  205. package/dist/smallFastModel-D_uxGMo3.cjs +21 -0
  206. package/dist/{spawn-D0QVhzRb.mjs → spawn-B7yro0b2.mjs} +26 -38
  207. package/dist/{spawn-Ddtom1cb.cjs → spawn-DgR0RKRs.cjs} +52 -64
  208. package/dist/spawnAgents-BGQiEF-w.cjs +52 -0
  209. package/dist/spawnAgents-DZzZ9jMI.mjs +45 -0
  210. package/dist/spawnHappyCLI-B2H5s4B_.mjs +64 -0
  211. package/dist/spawnHappyCLI-DBhwZT7O.cjs +66 -0
  212. package/dist/specialCommands-BSqhYg5X.mjs +52 -0
  213. package/dist/specialCommands-DSsTeRej.cjs +54 -0
  214. package/dist/startFileWatcher-B4VsLgjD.cjs +71 -0
  215. package/dist/startFileWatcher-DErVWZE4.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-C8gyQFMc.mjs +69 -0
  221. package/dist/sync-DccIKZ5_.cjs +71 -0
  222. package/dist/systemPrompt-BwTPWfMu.cjs +97 -0
  223. package/dist/systemPrompt-DX521JJz.mjs +94 -0
  224. package/dist/{teams-CqSwtw2O.mjs → teams-C32F1hqd.mjs} +9 -40
  225. package/dist/{teams-BZ1tayg_.cjs → teams-Dl8g8kTI.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-D0XyHOid.mjs +38 -0
  233. package/dist/time-DCuTyook.cjs +42 -0
  234. package/dist/titleGenerator-CQ8CoGhg.mjs +166 -0
  235. package/dist/titleGenerator-ChzMJsyg.cjs +172 -0
  236. package/dist/tmuxSocket-B8Yx20sT.mjs +19 -0
  237. package/dist/tmuxSocket-D-Xt6Vn-.cjs +24 -0
  238. package/dist/{todo-BYMg0fR0.cjs → todo-BvUOTa8G.cjs} +15 -12
  239. package/dist/{todo-BbusB1zM.mjs → todo-CZ6cFrrn.mjs} +11 -8
  240. package/dist/tools-BZtHF_qQ.mjs +177 -0
  241. package/dist/tools-Be3AoAwu.cjs +63 -0
  242. package/dist/tools-DQj0AeLk.cjs +180 -0
  243. package/dist/tools-DkluDcG4.mjs +60 -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-BkLzaJ6I.mjs +47 -0
  249. package/dist/uninstall-x-TUtf5H.cjs +49 -0
  250. package/package.json +7 -7
  251. package/dist/agentGuidance-CrR9l62U.mjs +0 -2145
  252. package/dist/agentGuidance-DwOWbX55.cjs +0 -2217
  253. package/dist/index-BG932eCh.cjs +0 -19655
  254. package/dist/index-CVCt3kSt.mjs +0 -19591
  255. package/dist/index-D5ScEErM.cjs +0 -58
  256. package/dist/index-DgO0FHOx.mjs +0 -52
  257. package/dist/limits-D2uN090G.mjs +0 -2528
  258. package/dist/limits-kqFqhfsh.cjs +0 -2652
@@ -1,2217 +0,0 @@
1
- 'use strict';
2
-
3
- var z = require('zod');
4
- var node_crypto = require('node:crypto');
5
- var persistence = require('./limits-kqFqhfsh.cjs');
6
- var promises = require('node:fs/promises');
7
- var node_fs = require('node:fs');
8
- var path = require('node:path');
9
- var os = require('node:os');
10
- var axios = require('axios');
11
- var socket_ioClient = require('socket.io-client');
12
-
13
- const TeamScopeSchema = z.z.object({
14
- serverUrl: z.z.string(),
15
- scopeToken: z.z.string().min(1),
16
- teamId: z.z.string().min(1),
17
- botId: z.z.string().min(1),
18
- taskId: z.z.string().optional(),
19
- attemptId: z.z.string().optional()
20
- });
21
- function teamScopePath(sessionId) {
22
- const supplied = process.env.VH_TEAM_SCOPE_FILE;
23
- if (supplied) {
24
- if (!path.isAbsolute(supplied)) throw new Error("VH_TEAM_SCOPE_FILE must be absolute");
25
- return supplied;
26
- }
27
- if (!sessionId || !/^[a-zA-Z0-9_-]+$/.test(sessionId)) throw new Error("Teams requires a managed session or VH_TEAM_SCOPE_FILE");
28
- return path.join(persistence.configuration.happyHomeDir, "teams", "scopes", `${sessionId}.json`);
29
- }
30
- async function readTeamScope(path) {
31
- let file;
32
- try {
33
- file = await promises.open(path, node_fs.constants.O_RDONLY | (node_fs.constants.O_NOFOLLOW ?? 0));
34
- } catch (error) {
35
- if (error.code === "ENOENT") return null;
36
- throw new Error("Cannot open team scope");
37
- }
38
- try {
39
- const stat = await file.stat();
40
- if (typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("Team scope owner mismatch");
41
- if (!stat.isFile() || process.platform !== "win32" && (stat.mode & 63) !== 0) throw new Error("Team scope must be a private 0600 file");
42
- const scope = TeamScopeSchema.parse(JSON.parse(await file.readFile("utf8")));
43
- scope.serverUrl = persistence.normalizeHttpEndpoint(scope.serverUrl, "team scope");
44
- if (scope.serverUrl !== persistence.configuration.serverUrl) throw new Error("Team scope belongs to another server");
45
- return scope;
46
- } catch {
47
- throw new Error("Invalid or unsafe team scope file");
48
- } finally {
49
- await file.close();
50
- }
51
- }
52
- async function writeTeamScope(path$1, scope) {
53
- await promises.mkdir(path.dirname(path$1), { recursive: true, mode: 448 });
54
- const temporary = `${path$1}.${node_crypto.randomUUID()}.new`;
55
- const file = await promises.open(temporary, "wx", 384);
56
- try {
57
- await file.writeFile(JSON.stringify(TeamScopeSchema.parse(scope)));
58
- await file.sync();
59
- } finally {
60
- await file.close();
61
- }
62
- try {
63
- await promises.rename(temporary, path$1);
64
- } finally {
65
- await promises.unlink(temporary).catch(() => {
66
- });
67
- }
68
- }
69
-
70
- async function teamRequest(path, method, token, scoped, body) {
71
- const response = await fetch(`${persistence.configuration.serverUrl}${path}`, {
72
- method,
73
- redirect: "error",
74
- headers: { "content-type": "application/json", ...scoped ? { "x-happy-team-token": token } : { authorization: `Bearer ${token}` } },
75
- body: body === void 0 ? void 0 : JSON.stringify(body),
76
- signal: AbortSignal.timeout(25e3)
77
- }).catch(() => {
78
- throw new Error("Teams connection failed; outcome may be unknown. Reuse the request ID when retrying.");
79
- });
80
- if (!response.ok) throw new Error(`Teams request failed (HTTP ${response.status})`);
81
- try {
82
- return await response.json();
83
- } catch {
84
- throw new Error("Teams returned an invalid response");
85
- }
86
- }
87
- class TeamConnectionPreflightError extends Error {
88
- }
89
- const initializationLocks = /* @__PURE__ */ new Map();
90
- async function serializeInitialization(path, work) {
91
- const previous = initializationLocks.get(path) ?? Promise.resolve();
92
- const next = previous.catch(() => {
93
- }).then(work);
94
- initializationLocks.set(path, next);
95
- try {
96
- return await next;
97
- } finally {
98
- if (initializationLocks.get(path) === next) initializationLocks.delete(path);
99
- }
100
- }
101
- function createTeamsClient(sessionId) {
102
- const scopePath = () => teamScopePath(sessionId ?? process.env.HAPPY_SESSION_ID);
103
- return {
104
- async initialize(input, options) {
105
- const path = scopePath();
106
- return serializeInitialization(path, async () => {
107
- const priorScope = await readTeamScope(path);
108
- if (priorScope && !options?.resumeAdoption) throw new Error("This session already has a team scope; use scoped team tools");
109
- if (process.env.VH_TEAM_SCOPE_FILE) throw new Error("Assigned team scope is missing or already managed; ask the owner to restore it");
110
- const sid = sessionId ?? process.env.HAPPY_SESSION_ID;
111
- if (!sid) throw new Error("A managed session identity is required");
112
- const machineId = (await persistence.readSettings()).machineId;
113
- if (!machineId) throw new TeamConnectionPreflightError("Connect this machine to Very Happy before joining a team");
114
- if (input.machineId && input.machineId !== machineId) throw new TeamConnectionPreflightError("Teams must use this session\u2019s local machine");
115
- const persisted = persistence.readPersistedSessions()[sid];
116
- if (!persisted || persisted.metadata.machineId !== machineId) throw new TeamConnectionPreflightError("Team identity must be a persisted session on this machine");
117
- const credentials = await persistence.readCredentialsForConfiguredRelay();
118
- if (!credentials) throw new TeamConnectionPreflightError("Sign in to Very Happy before creating or joining a team");
119
- if (priorScope && !options?.expectedTeamId) throw new Error("This session already belongs to another team scope");
120
- let teamId = input.teamId ?? options?.expectedTeamId;
121
- if (!teamId) {
122
- const created = await teamRequest("/v1/teams", "POST", credentials.token, false, { name: input.name, machineId, requestId: input.requestId });
123
- teamId = created.team.id;
124
- } else {
125
- const existing = await teamRequest(`/v1/teams/${encodeURIComponent(teamId)}`, "GET", credentials.token, false);
126
- if (existing.team?.machineId !== machineId) throw new TeamConnectionPreflightError("Team belongs to another machine");
127
- }
128
- if (options?.onTeamResolved) await options.onTeamResolved(teamId);
129
- if (priorScope) {
130
- if (priorScope.teamId !== teamId || priorScope.taskId) throw new Error("This session already belongs to another team scope");
131
- const inspected = await teamRequest(`/v1/teams/${encodeURIComponent(teamId)}/agent`, "GET", priorScope.scopeToken, true);
132
- const bot = inspected.team?.bots?.find((candidate) => candidate.id === priorScope.botId);
133
- if (!bot?.root || bot.managed || bot.sessionId !== sid) throw new Error("Existing scope is not this session\u2019s root identity");
134
- return { team: inspected.team, botId: priorScope.botId };
135
- }
136
- const result = await teamRequest(
137
- `/v1/teams/${encodeURIComponent(teamId)}/actions`,
138
- "POST",
139
- credentials.token,
140
- false,
141
- { requestId: input.requestId, action: { type: "join", name: input.name, sessionId: sid, ...input.botId ? { botId: input.botId } : {} } }
142
- );
143
- if (!result.credential?.token || !result.credential?.botId) throw new Error("Team join did not issue a credential");
144
- await writeTeamScope(path, { serverUrl: persistence.configuration.serverUrl, teamId, botId: result.credential.botId, scopeToken: result.credential.token });
145
- return { team: result.team, botId: result.credential.botId };
146
- });
147
- },
148
- async inspect() {
149
- const scope = await readTeamScope(scopePath());
150
- if (!scope) throw new Error("Join or create a team first");
151
- const { credential: _credential, ...safe } = await teamRequest(`/v1/teams/${encodeURIComponent(scope.teamId)}/agent`, "GET", scope.scopeToken, true);
152
- return safe;
153
- },
154
- async action(action, requestId = node_crypto.randomUUID()) {
155
- const scope = await readTeamScope(scopePath());
156
- if (!scope) throw new Error("Join or create a team first");
157
- const result = await teamRequest(`/v1/teams/${encodeURIComponent(scope.teamId)}/agent`, "POST", scope.scopeToken, true, { requestId, action });
158
- const { credential: _credential, ...safe } = result;
159
- return safe;
160
- }
161
- };
162
- }
163
-
164
- const TEAM_TOOL_NAMES = ["team_create", "team_join", "team_inspect", "team_delegate", "team_message", "team_submit", "team_accept", "team_return", "team_cancel", "team_handoff", "team_schedule_create", "team_schedule_pause", "team_schedule_resume", "team_schedule_cancel"];
165
- const id$1 = z.z.string().min(1).max(128);
166
- const text = z.z.string().min(1).max(32e3);
167
- const requestId = id$1.describe("Unique operation ID. Reuse the same ID and arguments after an uncertain response.");
168
- function registerTeamsTools(server, sessionId, client = createTeamsClient(sessionId)) {
169
- const register = (name, description, inputSchema, execute) => {
170
- server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: name === "team_inspect", idempotentHint: true, openWorldHint: false, destructiveHint: ["team_cancel", "team_handoff", "team_return", "team_schedule_cancel"].includes(name) } }, async (args) => {
171
- try {
172
- return { content: [{ type: "text", text: JSON.stringify(await execute(args)) }], isError: false };
173
- } catch (error) {
174
- return { content: [{ type: "text", text: error instanceof Error ? error.message : "Teams operation failed" }], isError: true };
175
- }
176
- });
177
- };
178
- register(
179
- "team_create",
180
- "Create an official Very Happy team and join this session as its root bot. Does not grant access outside this account.",
181
- { name: id$1, machineId: id$1.optional(), requestId },
182
- (args) => client.initialize(args)
183
- );
184
- register(
185
- "team_join",
186
- "Join an existing team as a root bot using this session owner\u2019s account. Scoped workers cannot elevate their role.",
187
- { teamId: id$1, name: id$1, botId: id$1.optional().describe("Existing root bot to reconnect; preserves its identity"), requestId },
188
- (args) => client.initialize(args)
189
- );
190
- register("team_inspect", "Read your visible team tasks, attempts and messages. Inspect before decisions; submission is not acceptance.", {}, () => client.inspect());
191
- const action = (name, description, fields) => register(
192
- `team_${name}`,
193
- description,
194
- { requestId, ...fields },
195
- ({ requestId: key, ...args }) => client.action({ type: name, ...args, ...name === "delegate" && args.directory === void 0 ? { directory: process.cwd() } : {} }, key)
196
- );
197
- action("delegate", "Delegate a goal with acceptance criteria. Use parentTaskId for subdelegation. Existing assigneeBotId or a managed worker may execute it.", {
198
- goal: text,
199
- acceptance: z.z.array(text).min(1).max(32),
200
- parentTaskId: id$1.optional(),
201
- assigneeBotId: id$1.optional(),
202
- botName: id$1.optional(),
203
- directory: z.z.string().min(1).optional(),
204
- assistant: z.z.enum(["claude", "codex", "pi-acp"]).optional(),
205
- model: z.z.string().trim().min(1).max(256).optional()
206
- });
207
- action("message", "Send a persistent task-related message. Delivery does not mean the model has read or acted on it.", { taskId: id$1, body: text, recipientBotId: id$1.optional() });
208
- action("submit", "Submit evidence for your current attempt and goal version. The owner still has to accept it.", { taskId: id$1, attemptId: id$1, goalVersion: z.z.number().int().positive(), result: text });
209
- const current = { attemptId: id$1, goalVersion: z.z.number().int().positive() };
210
- action("accept", "Accept a task you own after checking its evidence. Resource cleanup is tracked separately.", { taskId: id$1, ...current });
211
- action("return", "Return your task for further work with a concrete reason.", { taskId: id$1, reason: text, ...current });
212
- action("cancel", "Cancel an owned task and its dependent work. Cleanup may remain pending.", { taskId: id$1, reason: text, ...current });
213
- action("handoff", "Move an owned task to another bot; prior execution authority is superseded.", { taskId: id$1, assigneeBotId: id$1, ...current });
214
- register("team_schedule_create", "Create a persistent one-time or fixed-interval reminder for your own root bot. Delivery wakes the bound session; it does not prove task completion. Use epoch milliseconds, and inspect before retrying.", { requestId, name: id$1, botId: id$1, body: text, runAt: z.z.number().int().nonnegative(), intervalMs: z.z.number().int().min(6e4).optional() }, ({ requestId: key, ...args }) => client.action({ type: "schedule-create", ...args }, key));
215
- for (const operation of ["pause", "resume", "cancel"]) {
216
- register(`team_schedule_${operation}`, `${operation} your root bot\u2019s schedule using its current version from team_inspect. An already in-flight message may still arrive.`, { requestId, scheduleId: id$1, version: z.z.number().int().positive() }, ({ requestId: key, ...args }) => client.action({ type: `schedule-${operation}`, ...args }, key));
217
- }
218
- }
219
-
220
- class AutomationsRequestError extends Error {
221
- constructor(status, code, runStatus) {
222
- super(describeAutomationError(status, code, runStatus));
223
- this.status = status;
224
- this.code = code;
225
- this.runStatus = runStatus;
226
- }
227
- }
228
- function isAutomationsUnavailable(error) {
229
- return error instanceof AutomationsRequestError && error.status === 404 && !error.code.endsWith("_not_found");
230
- }
231
- const AUTOMATIONS_UNAVAILABLE_MESSAGE = "Automations are not enabled on this server (set VH_AUTOMATIONS_ENABLED=true on the server, or upgrade it).";
232
- function describeAutomationError(status, code, runStatus) {
233
- if (status === 404 && !code.endsWith("_not_found")) return AUTOMATIONS_UNAVAILABLE_MESSAGE;
234
- const known = {
235
- automation_not_found: "No automation with that name or id.",
236
- run_not_found: "No automation run with that id.",
237
- machine_not_found: "That machine is not registered on this account.",
238
- session_not_found: "That session does not belong to this account.",
239
- automation_name_taken: "An automation with that name already exists.",
240
- automation_count_quota_exceeded: "This account already holds the maximum number of automations (active + paused). Delete one you no longer need (`very-happy auto delete <name>`) before creating another.",
241
- automation_paused: "That automation is paused; resume it before firing.",
242
- stale_automation: "The automation changed since you read it; read it again and retry with the new version.",
243
- run_finished: `The run already reached a terminal state${runStatus ? ` (${runStatus})` : ""}. In a continued (sticky) session, pass the runId named in the latest prompt header \u2014 the environment default is the run that started the session.`,
244
- claim_mismatch: "Another daemon holds the claim for that run.",
245
- run_not_claimed: "The run has not been claimed by its machine yet; cancel it instead of reporting.",
246
- status_required: "An explicit report needs status done or failed.",
247
- claim_required: "Lease, session and sticky fields are daemon-only; report just the outcome.",
248
- sticky_requires_session: "A sticky key needs a session id.",
249
- transaction_conflict: "The server was busy; retry the same request.",
250
- invalid_cron: "The cron expression is invalid.",
251
- invalid_timezone: "The time zone is not a valid IANA zone."
252
- };
253
- if (known[code]) return known[code];
254
- if (status === 400) return "The server rejected the request (HTTP 400): check the arguments, or the server may expect a newer request shape.";
255
- return `Automations request failed (HTTP ${status}${code ? `, ${code}` : ""}).`;
256
- }
257
- function createAutomationsClient(options) {
258
- const doFetch = options.fetch ?? fetch;
259
- const base = options.serverUrl.replace(/\/+$/, "");
260
- async function request(method, path, body, query) {
261
- const url = new URL(`${base}${path}`);
262
- for (const [key, value] of Object.entries(query ?? {})) if (value !== void 0) url.searchParams.set(key, value);
263
- let response;
264
- try {
265
- response = await doFetch(url.toString(), {
266
- method,
267
- redirect: "error",
268
- headers: { "content-type": "application/json", authorization: `Bearer ${options.token}`, "x-happy-client": `${options.client ?? "cli-auto"}/${persistence.configuration.currentCliVersion}` },
269
- body: body === void 0 ? void 0 : JSON.stringify(body),
270
- signal: AbortSignal.timeout(options.timeoutMs ?? 25e3)
271
- });
272
- } catch {
273
- throw new Error("Automations connection failed; the outcome may be unknown. Read the current state before retrying.");
274
- }
275
- let parsed = null;
276
- try {
277
- parsed = await response.json();
278
- } catch {
279
- parsed = null;
280
- }
281
- if (!response.ok) {
282
- const code = typeof parsed?.error === "string" ? parsed.error : "";
283
- throw new AutomationsRequestError(response.status, code, typeof parsed?.status === "string" ? parsed.status : void 0);
284
- }
285
- return parsed;
286
- }
287
- const enc = encodeURIComponent;
288
- return {
289
- list: (machineId) => request("GET", "/v1/automations", void 0, { machineId }).then((r) => r.automations),
290
- get: (id) => request("GET", `/v1/automations/${enc(id)}`).then((r) => r.automation),
291
- getByName: (name) => request("GET", `/v1/automations/by-name/${enc(name)}`).then((r) => r.automation),
292
- create: (input) => request("POST", "/v1/automations", input).then((r) => r.automation),
293
- update: (id, input) => request("PATCH", `/v1/automations/${enc(id)}`, input).then((r) => r.automation),
294
- remove: (id) => request("DELETE", `/v1/automations/${enc(id)}`),
295
- pause: (id) => request("POST", `/v1/automations/${enc(id)}/pause`).then((r) => r.automation),
296
- resume: (id) => request("POST", `/v1/automations/${enc(id)}/resume`).then((r) => r.automation),
297
- run: (id, input = {}) => request("POST", `/v1/automations/${enc(id)}/run`, input).then((r) => r.run),
298
- fire: (name, input = {}) => request("POST", `/v1/automations/by-name/${enc(name)}/fire`, input),
299
- runs: (query = {}) => request("GET", "/v1/automations/runs", void 0, {
300
- automationId: query.automationId,
301
- name: query.name,
302
- status: query.status,
303
- attention: query.attention ? "1" : void 0,
304
- limit: query.limit === void 0 ? void 0 : String(query.limit)
305
- }).then((r) => r.runs),
306
- getRun: (id) => request("GET", `/v1/automations/runs/${enc(id)}`).then((r) => r.run),
307
- cancel: (id) => request("POST", `/v1/automations/runs/${enc(id)}/cancel`).then((r) => r.run),
308
- /** B-508: `by` records what resolved the flag (server default `owner`; older servers ignore the body). */
309
- ack: (id, by) => request("POST", `/v1/automations/runs/${enc(id)}/ack`, by === void 0 ? void 0 : { by }).then((r) => r.run),
310
- report: (id, input) => request("POST", `/v1/automations/runs/${enc(id)}/report`, input).then((r) => r.run),
311
- claim: (input) => request("POST", "/v1/automations/claim", input),
312
- stickies: (id, key) => request("GET", `/v1/automations/${enc(id)}/stickies`, void 0, { key }).then((r) => r.stickies),
313
- putSticky: (id, key, sessionId) => request("PUT", `/v1/automations/${enc(id)}/stickies`, { key, sessionId }).then((r) => r.sticky),
314
- deleteSticky: (id, key) => request("DELETE", `/v1/automations/${enc(id)}/stickies`, { key }).then((r) => r.removed)
315
- };
316
- }
317
- async function authenticatedAutomationsClient(client = "cli-auto") {
318
- const credentials = await persistence.readCredentialsForConfiguredRelay();
319
- if (!credentials) throw new Error("Not authenticated. Run `very-happy auth login` first.");
320
- const settings = await persistence.readSettings();
321
- return { client: createAutomationsClient({ serverUrl: persistence.configuration.serverUrl, token: credentials.token, client }), machineId: settings.machineId };
322
- }
323
- function automationRunIdFromEnv(env = process.env) {
324
- const value = env.VH_AUTOMATION_RUN_ID;
325
- return value && /^[a-zA-Z0-9_-]{1,128}$/.test(value) ? value : void 0;
326
- }
327
-
328
- const AUTOMATION_TOOL_NAMES = [
329
- "automation_list",
330
- "automation_get",
331
- "automation_create",
332
- "automation_update",
333
- "automation_pause",
334
- "automation_resume",
335
- "automation_delete",
336
- "automation_run",
337
- "automation_fire",
338
- "automation_runs",
339
- "automation_report",
340
- "automation_ack"
341
- ];
342
- const id = z.z.string().min(1).max(128);
343
- const ref = { name: persistence.AutomationNameSchema.optional().describe("Automation name"), id: id.optional().describe("Automation id (alternative to name)") };
344
- const payload = z.z.string().max(65536).optional().describe("Payload text; JSON lets prompts use {{payload.field}}");
345
- const maxRuntimeMs = z.z.number().int().min(6e4).max(7 * 864e5).optional();
346
- const AUTOMATION_TOOL_SCHEMAS = {
347
- automation_list: { description: "List this account\u2019s automations (name, trigger, action, machine, status, next run). Defaults to every machine.", readOnly: true, inputSchema: { machineId: id.optional().describe("Only this machine id, or 'this' for the current machine") } },
348
- automation_get: { description: "Read one automation by name or id, including its version (needed for automation_update).", readOnly: true, inputSchema: ref },
349
- automation_create: {
350
- description: "Create an automation: a trigger (cron+tz / interval / once / manual) and an action (spawn a session, send into a session, or run a script argv) on one machine. Defaults to the current machine. Prompts and argv may use {{payload}}, {{payload.path}}, {{run.id}}, {{automation.name}}, {{now}}.",
351
- inputSchema: {
352
- name: persistence.AutomationNameSchema,
353
- description: z.z.string().max(4e3).optional(),
354
- machineId: id.optional().describe("Target machine id; omit or 'this' for the current machine"),
355
- trigger: persistence.AutomationTriggerSchema,
356
- action: persistence.AutomationActionSchema,
357
- concurrency: persistence.AutomationConcurrencySchema.optional().describe("skip (default): a run while another is active is recorded as skipped; queue: it waits"),
358
- maxRuntimeMs,
359
- paused: z.z.boolean().optional().describe("Create paused")
360
- }
361
- },
362
- automation_update: {
363
- description: "Change an automation (name / description / machine / trigger / action / concurrency / maxRuntimeMs). Requires the current version from automation_get; a stale version is rejected.",
364
- inputSchema: {
365
- ...ref,
366
- version: z.z.number().int().positive(),
367
- newName: persistence.AutomationNameSchema.optional(),
368
- description: z.z.string().max(4e3).nullable().optional(),
369
- machineId: id.optional(),
370
- trigger: persistence.AutomationTriggerSchema.optional(),
371
- action: persistence.AutomationActionSchema.optional(),
372
- concurrency: persistence.AutomationConcurrencySchema.optional(),
373
- maxRuntimeMs
374
- }
375
- },
376
- automation_pause: { description: "Pause an automation: no new scheduled runs and fire is refused until resumed.", inputSchema: ref },
377
- automation_resume: { description: "Resume a paused automation; the next run is computed from now.", inputSchema: ref },
378
- automation_delete: { description: "Delete an automation and its run history. Irreversible.", destructive: true, inputSchema: ref },
379
- automation_run: { description: "Queue one run of an automation now (source: manual), optionally with a payload.", inputSchema: { ...ref, payload } },
380
- automation_fire: { description: "Trigger an automation by name as an external event, with an optional payload and dedupe key (the same key within 24h returns the original run).", inputSchema: { name: persistence.AutomationNameSchema, payload, dedupeKey: z.z.string().min(1).max(256).optional() } },
381
- automation_runs: { description: "List runs, newest first: status, session, summary, error, attention. Filter by automation, status or attention.", readOnly: true, inputSchema: { ...ref, status: z.z.string().min(1).max(64).optional(), attention: z.z.boolean().optional().describe("Only runs flagged for a human"), limit: z.z.number().int().min(1).max(200).optional() } },
382
- automation_report: {
383
- description: 'Report the result of an automation run. Without runId it targets the run that STARTED this session (VH_AUTOMATION_RUN_ID); in a continued sticky session always pass the runId named in the latest "[Very Happy automation \u2026 run <id>; \u2026]" prompt header. Use status done with a short summary, or failed with the error; set needsAttention when a human must look.',
384
- inputSchema: { runId: id.optional(), status: z.z.enum(["done", "failed"]), summary: z.z.string().max(4096).optional(), error: z.z.string().max(4096).optional(), needsAttention: z.z.boolean().optional(), attentionReason: z.z.string().max(512).optional() }
385
- },
386
- automation_ack: { description: "Clear the attention flag of a run after a human handled it.", inputSchema: { runId: id } }
387
- };
388
- async function resolve(context, args) {
389
- if (args.id) return context.client.get(args.id);
390
- if (args.name) return context.client.getByName(args.name);
391
- throw new Error("Give the automation name or id");
392
- }
393
- function machineFor(context, requested) {
394
- if (requested && requested !== "this") return requested;
395
- return context.machineId;
396
- }
397
- function requireMachine(context, requested) {
398
- const machine = machineFor(context, requested);
399
- if (!machine) throw new Error("This machine is not registered with Very Happy; pass machineId explicitly");
400
- return machine;
401
- }
402
- async function executeAutomationTool(name, args, context) {
403
- const { client } = context;
404
- switch (name) {
405
- case "automation_list":
406
- return { automations: await client.list(machineFor(context, args.machineId)) };
407
- case "automation_get":
408
- return { automation: await resolve(context, args) };
409
- case "automation_create": {
410
- const input = {
411
- name: args.name,
412
- machineId: requireMachine(context, args.machineId),
413
- trigger: args.trigger,
414
- action: args.action,
415
- ...args.description !== void 0 ? { description: args.description } : {},
416
- ...args.concurrency !== void 0 ? { concurrency: args.concurrency } : {},
417
- ...args.maxRuntimeMs !== void 0 ? { maxRuntimeMs: args.maxRuntimeMs } : {},
418
- ...args.paused ? { status: "paused" } : {}
419
- };
420
- return { automation: await client.create(input) };
421
- }
422
- case "automation_update": {
423
- const current = await resolve(context, args);
424
- const input = {
425
- version: args.version,
426
- ...args.newName !== void 0 ? { name: args.newName } : {},
427
- ...args.description !== void 0 ? { description: args.description } : {},
428
- ...args.machineId !== void 0 ? { machineId: machineFor(context, args.machineId) } : {},
429
- ...args.trigger !== void 0 ? { trigger: args.trigger } : {},
430
- ...args.action !== void 0 ? { action: args.action } : {},
431
- ...args.concurrency !== void 0 ? { concurrency: args.concurrency } : {},
432
- ...args.maxRuntimeMs !== void 0 ? { maxRuntimeMs: args.maxRuntimeMs } : {}
433
- };
434
- return { automation: await client.update(current.id, input) };
435
- }
436
- case "automation_pause":
437
- return { automation: await client.pause((await resolve(context, args)).id) };
438
- case "automation_resume":
439
- return { automation: await client.resume((await resolve(context, args)).id) };
440
- case "automation_delete": {
441
- const target = await resolve(context, args);
442
- await client.remove(target.id);
443
- return { deleted: true, id: target.id, name: target.name };
444
- }
445
- case "automation_run":
446
- return { run: await client.run((await resolve(context, args)).id, args.payload !== void 0 ? { payload: args.payload } : {}) };
447
- case "automation_fire":
448
- return client.fire(args.name, { ...args.payload !== void 0 ? { payload: args.payload } : {}, ...args.dedupeKey !== void 0 ? { dedupeKey: args.dedupeKey } : {} });
449
- case "automation_runs": {
450
- const automationId = args.id ?? (args.name ? (await client.getByName(args.name)).id : void 0);
451
- return { runs: await client.runs({ automationId, status: args.status, attention: args.attention === true, limit: args.limit }) };
452
- }
453
- case "automation_report": {
454
- const runId = args.runId ?? automationRunIdFromEnv(context.env ?? process.env);
455
- if (!runId) throw new Error("No run id: pass runId (this session was not started by an automation)");
456
- const report = {
457
- status: args.status,
458
- ...args.summary !== void 0 ? { summary: args.summary } : {},
459
- ...args.error !== void 0 ? { error: args.error } : {},
460
- ...args.needsAttention !== void 0 ? { needsAttention: args.needsAttention } : {},
461
- ...args.attentionReason !== void 0 ? { attentionReason: args.attentionReason } : {}
462
- };
463
- return { run: await client.report(runId, report) };
464
- }
465
- case "automation_ack":
466
- return { run: await client.ack(args.runId) };
467
- }
468
- }
469
- function createAutomationToolExecutor() {
470
- let context = null;
471
- return async (name, args) => {
472
- context ??= authenticatedAutomationsClient("mcp-auto").then(({ client, machineId }) => ({ client, machineId }));
473
- let resolved;
474
- try {
475
- resolved = await context;
476
- } catch (error) {
477
- context = null;
478
- throw error;
479
- }
480
- return executeAutomationTool(name, args, resolved);
481
- };
482
- }
483
- function registerAutomationTools(server, execute = createAutomationToolExecutor()) {
484
- for (const name of AUTOMATION_TOOL_NAMES) {
485
- const spec = AUTOMATION_TOOL_SCHEMAS[name];
486
- server.registerTool(name, {
487
- description: spec.description,
488
- inputSchema: spec.inputSchema,
489
- annotations: { readOnlyHint: spec.readOnly === true, idempotentHint: name !== "automation_run" && name !== "automation_create", openWorldHint: false, destructiveHint: spec.destructive === true }
490
- }, async (args) => {
491
- try {
492
- return { content: [{ type: "text", text: JSON.stringify(await execute(name, args ?? {})) }], isError: false };
493
- } catch (error) {
494
- return { content: [{ type: "text", text: error instanceof Error ? error.message : "Automation operation failed" }], isError: true };
495
- }
496
- });
497
- }
498
- }
499
-
500
- function sessionWebUrl(sessionId) {
501
- return `${persistence.configuration.webappUrl.replace(/\/+$/, "")}/session/${sessionId}`;
502
- }
503
- async function waitForSessionKey(sessionId, timeoutMs) {
504
- const deadline = Date.now() + timeoutMs;
505
- while (true) {
506
- const entry = persistence.readPersistedSessions()[sessionId];
507
- if (entry) return entry;
508
- if (Date.now() >= deadline) {
509
- throw new Error(
510
- `Session ${sessionId} has no encryption key in ${persistence.configuration.sessionsFile}. Either it was not spawned by this machine's daemon, or the daemon is too old to persist session keys.`
511
- );
512
- }
513
- await persistence.delay(200);
514
- }
515
- }
516
- async function sendUserMessage(sessionId, persisted, text, client, options = {}) {
517
- const credentials = await persistence.readCredentialsForConfiguredRelay();
518
- if (!credentials) {
519
- throw new Error("Not authenticated. Run `very-happy auth login` first.");
520
- }
521
- const envelope = {
522
- role: "user",
523
- content: {
524
- type: "text",
525
- text
526
- },
527
- meta: {
528
- sentFrom: options.sentFrom ?? "cli",
529
- ...options.model !== void 0 ? { model: options.model } : {},
530
- ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {},
531
- ...options.effort !== void 0 ? { effort: options.effort } : {},
532
- ...options.delivery !== void 0 ? { delivery: options.delivery } : {}
533
- }
534
- };
535
- const encrypted = persistence.encodeBase64(persistence.encrypt(
536
- persistence.decodeBase64(persisted.encryptionKey),
537
- persisted.encryptionVariant,
538
- envelope
539
- ));
540
- await axios.post(
541
- `${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
542
- { messages: [{ content: encrypted, localId: options.localId ?? node_crypto.randomUUID() }] },
543
- {
544
- headers: {
545
- "Authorization": `Bearer ${credentials.token}`,
546
- "Content-Type": "application/json",
547
- "X-Happy-Client": `${client}/${persistence.configuration.currentCliVersion}`
548
- },
549
- timeout: 3e4
550
- }
551
- );
552
- }
553
-
554
- const MAX_LINE_CHARS = 500;
555
- const MAX_ARGS_CHARS = 160;
556
- const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
557
- const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
558
- function truncateText(text, max = MAX_LINE_CHARS) {
559
- const chars = Array.from(text);
560
- if (chars.length <= max) return text;
561
- return chars.slice(0, max).join("") + ` \u2026[+${chars.length - max} chars]`;
562
- }
563
- function squash(text) {
564
- return text.replace(/\s+/g, " ").trim();
565
- }
566
- function safeJson(value, max) {
567
- try {
568
- return truncateText(JSON.stringify(value) ?? "", max);
569
- } catch {
570
- return "[unserializable]";
571
- }
572
- }
573
- function formatMessageBody(body, limits = DEFAULT_LIMITS) {
574
- if (!body || typeof body !== "object") return null;
575
- const b = body;
576
- if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
577
- const text = squash(b.content.text);
578
- return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
579
- }
580
- if (b.role === "session" && b.content && typeof b.content === "object") {
581
- const env = b.content;
582
- const ev = env.ev;
583
- if (!ev || typeof ev.t !== "string") return null;
584
- switch (ev.t) {
585
- case "text": {
586
- if (ev.thinking) return null;
587
- const text = squash(String(ev.text ?? ""));
588
- if (text.length === 0) return null;
589
- const role = env.role === "user" ? "user" : "assistant";
590
- return `${role}: ${truncateText(text, limits.line)}`;
591
- }
592
- case "tool-call-start": {
593
- const name = String(ev.name ?? "tool");
594
- const title = squash(String(ev.title ?? ""));
595
- const args = ev.args ? safeJson(ev.args, limits.args) : "";
596
- const detail = title || args;
597
- return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
598
- }
599
- case "service": {
600
- const text = squash(String(ev.text ?? ""));
601
- return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
602
- }
603
- case "turn-end": {
604
- if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
605
- return null;
606
- }
607
- default:
608
- return null;
609
- }
610
- }
611
- if (b.role === "agent" && b.content?.type === "acp" && b.content.data && typeof b.content.data === "object") {
612
- const d = b.content.data;
613
- switch (d.type) {
614
- case "message": {
615
- const text = squash(String(d.message ?? ""));
616
- return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
617
- }
618
- case "tool-call":
619
- return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
620
- case "permission-request":
621
- return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
622
- default:
623
- return null;
624
- }
625
- }
626
- if (b.role === "agent" && b.content?.type === "output") {
627
- const msg = b.content.data?.message;
628
- if (msg && Array.isArray(msg.content)) {
629
- const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
630
- if (texts.length > 0) {
631
- const role = msg.role === "user" ? "user" : "assistant";
632
- return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
633
- }
634
- }
635
- return null;
636
- }
637
- return null;
638
- }
639
- function formatTranscript(bodies, options = {}) {
640
- const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
641
- const lines = [];
642
- for (const body of bodies) {
643
- const line = formatMessageBody(body, limits);
644
- if (line !== null) lines.push(line);
645
- }
646
- return lines.join("\n");
647
- }
648
-
649
- function asRecord(value) {
650
- return value && typeof value === "object" ? value : null;
651
- }
652
- function sessionEnvelope(body) {
653
- const b = asRecord(body);
654
- if (!b || b.role !== "session") return null;
655
- const env = asRecord(b.content);
656
- if (!env) return null;
657
- const ev = asRecord(env.ev);
658
- return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
659
- }
660
- function isUserPrompt(body) {
661
- const b = asRecord(body);
662
- return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
663
- }
664
- function agentPiece(body) {
665
- const env = sessionEnvelope(body);
666
- if (env && env.role !== "user") {
667
- if (env.ev.t === "text" && !env.ev.thinking) {
668
- const text = String(env.ev.text ?? "");
669
- return text.trim().length > 0 ? { kind: "text", text } : null;
670
- }
671
- if (env.ev.t === "tool-call-start") return { kind: "tool" };
672
- return null;
673
- }
674
- const b = asRecord(body);
675
- if (b?.role === "agent" && b.content?.type === "acp") {
676
- const d = asRecord(b.content.data);
677
- if (d?.type === "message") {
678
- const text = String(d.message ?? "");
679
- return text.trim().length > 0 ? { kind: "text", text } : null;
680
- }
681
- if (d?.type === "tool-call") return { kind: "tool" };
682
- }
683
- return null;
684
- }
685
- function userPromptText(body) {
686
- return isUserPrompt(body) ? body.content.text : null;
687
- }
688
- function analyzeLatestTurn(entries, options = {}) {
689
- const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
690
- let userIndex = -1;
691
- for (let i = entries.length - 1; i >= 0; i--) {
692
- if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
693
- userIndex = i;
694
- break;
695
- }
696
- }
697
- let ended = false;
698
- let status = null;
699
- let error = null;
700
- let texts = [];
701
- let lastMarker = null;
702
- let startedAfterPrompt = false;
703
- const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
704
- for (let i = userIndex + 1; i < entries.length; i++) {
705
- const body = entries[i].body;
706
- if (options.anchor && isUserPrompt(body)) {
707
- if (ended) break;
708
- continue;
709
- }
710
- const env = sessionEnvelope(body);
711
- if (env?.ev?.t === "turn-start") {
712
- lastMarker = "start";
713
- startedAfterPrompt = true;
714
- ended = false;
715
- status = null;
716
- error = null;
717
- texts = [];
718
- continue;
719
- }
720
- if (env?.ev?.t === "turn-end") {
721
- lastMarker = "end";
722
- if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
723
- ended = true;
724
- status = typeof env.ev.status === "string" ? env.ev.status : null;
725
- error = typeof env.ev.error === "string" ? env.ev.error : null;
726
- continue;
727
- }
728
- const piece = agentPiece(body);
729
- if (!piece) continue;
730
- if (piece.kind === "tool") texts = [];
731
- else texts.push(piece.text);
732
- }
733
- if (userIndex < 0) ended = lastMarker === "end";
734
- return {
735
- userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
736
- ended,
737
- status: ended ? status : null,
738
- error: ended ? error : null,
739
- answer: texts.join("\n\n").trim(),
740
- lastSeq
741
- };
742
- }
743
-
744
- const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
745
- async function openUserScopedSocket(token, options = {}) {
746
- const socket = socket_ioClient.io(persistence.configuration.serverUrl, {
747
- auth: {
748
- token,
749
- clientType: "user-scoped",
750
- happyClient: `cli-session-ops/${persistence.configuration.currentCliVersion}`
751
- },
752
- path: "/v1/updates",
753
- transports: ["websocket"],
754
- reconnection: false,
755
- autoConnect: false
756
- });
757
- await new Promise((resolve, reject) => {
758
- const timer = setTimeout(() => {
759
- socket.close();
760
- reject(new Error(`Timed out connecting to ${persistence.configuration.serverUrl}`));
761
- }, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
762
- socket.once("connect", () => {
763
- clearTimeout(timer);
764
- resolve();
765
- });
766
- socket.once("connect_error", (error) => {
767
- clearTimeout(timer);
768
- socket.close();
769
- reject(new Error(`Could not connect to ${persistence.configuration.serverUrl}: ${error.message}`));
770
- });
771
- socket.connect();
772
- });
773
- return {
774
- async rpcCall(payload, timeoutMs) {
775
- const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
776
- return ack;
777
- },
778
- close() {
779
- socket.close();
780
- }
781
- };
782
- }
783
-
784
- const RPC_RATE_LIMIT_ERROR = "RPC rate limit reached";
785
- const RPC_ACCOUNT_RATE_LIMIT_ERROR = "RPC account rate limit reached";
786
- const RPC_RATE_RETRY_MIN_DELAY_MS = 2e3;
787
- const RPC_RATE_RETRY_MAX_DELAY_MS = 6e4;
788
- const RPC_RATE_MAX_RETRIES = 2;
789
- function isRpcRateLimitAck(ack) {
790
- if (!ack || typeof ack !== "object") return false;
791
- const a = ack;
792
- if (a.ok !== false) return false;
793
- if (a.code === "rpc_rate_limited" || a.code === "rpc_account_rate_limited") return true;
794
- return a.error === RPC_RATE_LIMIT_ERROR || a.error === RPC_ACCOUNT_RATE_LIMIT_ERROR;
795
- }
796
- function rpcRateRetryDelayMs(attempt, retryAfterMs, random = Math.random()) {
797
- if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
798
- return Math.min(RPC_RATE_RETRY_MAX_DELAY_MS, Math.round(retryAfterMs * (1 + random * 0.5)));
799
- }
800
- const exponent = Math.max(0, Math.min(attempt, 32) - 1);
801
- const ceiling = Math.min(RPC_RATE_RETRY_MIN_DELAY_MS * 2 ** exponent, RPC_RATE_RETRY_MAX_DELAY_MS);
802
- return Math.round(RPC_RATE_RETRY_MIN_DELAY_MS + random * (ceiling - RPC_RATE_RETRY_MIN_DELAY_MS));
803
- }
804
-
805
- const PERMISSION_OPS_CLIENT = "session-ops";
806
- const PERMISSION_RPC_TIMEOUT_MS = 3e4;
807
- const SETTLE_TIMEOUT_MS = 5e3;
808
- const SETTLE_POLL_MS = 500;
809
- function buildPermissionRpcPayload(requestId, verdict) {
810
- if (verdict.kind === "approve") {
811
- return {
812
- id: requestId,
813
- approved: true,
814
- decision: verdict.forSession ? "approved_for_session" : "approved"
815
- };
816
- }
817
- return {
818
- id: requestId,
819
- approved: false,
820
- decision: "denied",
821
- ...verdict.reason ? { reason: verdict.reason } : {}
822
- };
823
- }
824
- function interpretPermissionAck(ack, decryptResult) {
825
- if (!ack.ok) {
826
- const message = ack.error ?? "RPC call failed";
827
- if (message === "RPC method not available") {
828
- return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
829
- }
830
- if (message === "RPC target disconnected") {
831
- return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
832
- }
833
- if (/timed? ?out/i.test(message)) {
834
- return { status: "timeout", message };
835
- }
836
- if (isRpcRateLimitAck(ack)) {
837
- return { status: "rate-limited", message, retryAfterMs: rpcRateRetryDelayMs(1, ack.retryAfterMs, 0) };
838
- }
839
- return { status: "rejected", message };
840
- }
841
- let body = null;
842
- if (typeof ack.result === "string" && ack.result.length > 0) {
843
- try {
844
- body = decryptResult(ack.result);
845
- } catch {
846
- body = null;
847
- }
848
- }
849
- if (body && typeof body === "object" && typeof body.error === "string") {
850
- return { status: "handler-error", message: body.error };
851
- }
852
- return { status: "acknowledged" };
853
- }
854
- function pendingRequestsOf(agentState, now) {
855
- const requests = agentState?.requests;
856
- if (!requests || typeof requests !== "object") return [];
857
- return Object.entries(requests).map(([id, request]) => ({
858
- id,
859
- tool: typeof request?.tool === "string" ? request.tool : "unknown",
860
- ...request?.kind ? { kind: request.kind } : {},
861
- ...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
862
- })).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
863
- }
864
- const RPC_DEADLINE_GRACE_MS = 2e3;
865
- function withDeadline(promise, ms, message) {
866
- return new Promise((resolve, reject) => {
867
- const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
868
- promise.then(
869
- (value) => {
870
- clearTimeout(timer);
871
- resolve(value);
872
- },
873
- (error) => {
874
- clearTimeout(timer);
875
- reject(error);
876
- }
877
- );
878
- });
879
- }
880
- async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
881
- const now = deps.now ?? Date.now;
882
- const persisted = (deps.readPersisted ?? persistence.readPersistedSessions)()[sessionId];
883
- if (!persisted) {
884
- throw new Error(
885
- `No local key for session ${sessionId} \u2014 approve/deny needs the session key from ~/.happy/sessions.json (the RPC payload is encrypted with it), so only sessions this machine's daemon spawned within 14 days can be answered from here.`
886
- );
887
- }
888
- const token = await (deps.bearerToken ?? bearerToken$2)();
889
- const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
890
- const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
891
- if (!before.some((request) => request.id === requestId)) {
892
- const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
893
- throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
894
- }
895
- const payload = buildPermissionRpcPayload(requestId, verdict);
896
- const key = persistence.decodeBase64(persisted.encryptionKey);
897
- const params = persistence.encodeBase64(persistence.encrypt(key, persisted.encryptionVariant, payload));
898
- const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
899
- const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
900
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
901
- let ack;
902
- try {
903
- for (let attempt = 1; ; attempt++) {
904
- try {
905
- ack = await withDeadline(
906
- transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
907
- rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
908
- `permission RPC did not settle within ${rpcTimeoutMs}ms`
909
- );
910
- } catch (error) {
911
- ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
912
- }
913
- if (!isRpcRateLimitAck(ack) || attempt > RPC_RATE_MAX_RETRIES) break;
914
- const waitMs = rpcRateRetryDelayMs(attempt, ack.retryAfterMs, (deps.random ?? Math.random)());
915
- deps.onRateLimited?.({ attempt, waitMs, serverError: ack.error ?? "RPC rate limit reached" });
916
- await sleep(waitMs);
917
- }
918
- } finally {
919
- transport.close();
920
- }
921
- const outcome = interpretPermissionAck(ack, (encrypted) => persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(encrypted)));
922
- if (outcome.status !== "acknowledged") {
923
- return { sessionId, requestId, payload, outcome };
924
- }
925
- const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
926
- let settled = false;
927
- for (; ; ) {
928
- const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
929
- if (!pending.some((request) => request.id === requestId)) {
930
- settled = true;
931
- break;
932
- }
933
- if (now() >= deadline) break;
934
- await sleep(SETTLE_POLL_MS);
935
- }
936
- return { sessionId, requestId, payload, outcome, settled };
937
- }
938
- async function bearerToken$2() {
939
- const credentials = await persistence.readCredentialsForConfiguredRelay();
940
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
941
- return credentials.token;
942
- }
943
- async function fetchDecryptedAgentState(sessionId, persisted, token) {
944
- const response = await axios.get(
945
- `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
946
- {
947
- headers: {
948
- "Authorization": `Bearer ${token}`,
949
- "X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
950
- },
951
- timeout: 15e3,
952
- // 404 is a real answer ("not yours / gone"), not a transport fault.
953
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
954
- }
955
- );
956
- if (response.status === 404) {
957
- throw new Error(`Session ${sessionId} was not found on this account (it may have been deleted, or the local key belongs to a session on another relay).`);
958
- }
959
- const raw = response.data?.session?.agentState;
960
- if (typeof raw !== "string" || raw.length === 0) return null;
961
- return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(raw));
962
- }
963
-
964
- const SESSION_OPS_CLIENT = "session-ops";
965
- const DEFAULT_RECENT_LIMIT = 15;
966
- const MAX_READ_LIMIT = 100;
967
- function backgroundTasksOf(child) {
968
- const raw = child.backgroundTasks;
969
- if (!raw || typeof raw !== "object") return void 0;
970
- const r = raw;
971
- if (typeof r.count !== "number" || !Array.isArray(r.tasks)) return void 0;
972
- return {
973
- count: r.count,
974
- tasks: r.tasks.filter((task) => !!task && typeof task === "object" && typeof task.id === "string"),
975
- ...typeof r.reportedAt === "number" ? { reportedAt: r.reportedAt } : {},
976
- ...r.stale === true ? { stale: true } : {}
977
- };
978
- }
979
- function mergeSessionSummaries(live, persisted, options = {}) {
980
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
981
- const seen = /* @__PURE__ */ new Set();
982
- const summaries = [];
983
- for (const child of live) {
984
- const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
985
- if (!id || seen.has(id)) continue;
986
- seen.add(id);
987
- summaries.push(toSummary(id, persisted[id], livenessOf(child)));
988
- }
989
- const rest = Object.entries(persisted).filter(([id]) => !seen.has(id)).filter(([, entry]) => entry.metadata?.flavor !== "terminal-mirror").sort((a, b) => b[1].savedAt - a[1].savedAt).slice(0, recentLimit);
990
- for (const [id, entry] of rest) {
991
- summaries.push(toSummary(id, entry, { live: false }));
992
- }
993
- if (!options.tag) return summaries;
994
- const wanted = options.tag;
995
- return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
996
- }
997
- function livenessOf(child) {
998
- const backgroundTasks = backgroundTasksOf(child);
999
- return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0, ...backgroundTasks ? { backgroundTasks } : {} };
1000
- }
1001
- function sessionLiveness(live, sessionId) {
1002
- const child = live.find((entry) => entry.happySessionId === sessionId);
1003
- return child ? livenessOf(child) : { live: false };
1004
- }
1005
- function toSummary(id, persisted, extra) {
1006
- const meta = persisted?.metadata;
1007
- return {
1008
- id,
1009
- live: extra.live,
1010
- ...extra.pid !== void 0 ? { pid: extra.pid } : {},
1011
- ...extra.backgroundTasks ? { backgroundTasks: extra.backgroundTasks } : {},
1012
- ...meta?.summary?.text ? { title: meta.summary.text } : {},
1013
- ...meta?.path ? { cwd: meta.path } : {},
1014
- ...meta?.flavor ? { flavor: meta.flavor } : {},
1015
- ...meta?.tags?.length ? { tags: [...meta.tags] } : {},
1016
- ...meta?.variant ? { variant: meta.variant } : {},
1017
- url: sessionWebUrl(id),
1018
- ...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {}
1019
- };
1020
- }
1021
- async function listSessions(options = {}) {
1022
- const live = await persistence.listDaemonSessions();
1023
- return mergeSessionSummaries(live, persistence.readPersistedSessions(), options);
1024
- }
1025
- function summarizeAccountSession(row, persisted, liveIds, now) {
1026
- const base = {
1027
- id: row.id,
1028
- live: liveIds.has(row.id),
1029
- url: sessionWebUrl(row.id),
1030
- decryptable: false,
1031
- readable: false,
1032
- active: row.active === true,
1033
- archived: typeof row.archivedAt === "number",
1034
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
1035
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
1036
- attention: false
1037
- };
1038
- if (!persisted) return base;
1039
- const key = persistence.decodeBase64(persisted.encryptionKey);
1040
- let metadata = null;
1041
- let agentState = null;
1042
- try {
1043
- metadata = row.metadata ? persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(row.metadata)) : null;
1044
- agentState = row.agentState ? persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(row.agentState)) : null;
1045
- } catch {
1046
- return base;
1047
- }
1048
- if (!metadata) return base;
1049
- const meta = metadata;
1050
- const pending = pendingRequestsOf(agentState, now);
1051
- const backgroundTasks = persistence.backgroundTasksReport(agentState?.backgroundTasks, now, { active: row.active === true });
1052
- return {
1053
- ...base,
1054
- decryptable: true,
1055
- readable: true,
1056
- backgroundTasks,
1057
- ...meta.summary?.text ? { title: meta.summary.text } : {},
1058
- ...meta.path ? { cwd: meta.path } : {},
1059
- ...meta.flavor ? { flavor: meta.flavor } : {},
1060
- ...meta.tags?.length ? { tags: [...meta.tags] } : {},
1061
- ...meta.variant ? { variant: meta.variant } : {},
1062
- ...meta.machineId ? { machineId: meta.machineId } : {},
1063
- ...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
1064
- pending,
1065
- attention: pending.length > 0
1066
- };
1067
- }
1068
- function orderAccountSessions(summaries, options = {}) {
1069
- const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
1070
- const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
1071
- let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
1072
- const byRank = rank(a) - rank(b);
1073
- if (byRank !== 0) return byRank;
1074
- if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
1075
- return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
1076
- });
1077
- if (options.tag) {
1078
- const wanted = options.tag;
1079
- rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
1080
- }
1081
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
1082
- let idleKept = 0;
1083
- return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
1084
- }
1085
- async function listAccountSessions(options = {}) {
1086
- const token = await bearerToken$1();
1087
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions`, {
1088
- headers: {
1089
- "Authorization": `Bearer ${token}`,
1090
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
1091
- },
1092
- timeout: 15e3
1093
- });
1094
- const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
1095
- const persisted = persistence.readPersistedSessions();
1096
- const live = await persistence.listDaemonSessions();
1097
- const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
1098
- const now = Date.now();
1099
- const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
1100
- return orderAccountSessions(summaries, options);
1101
- }
1102
- async function bearerToken$1() {
1103
- const credentials = await persistence.readCredentialsForConfiguredRelay();
1104
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
1105
- return credentials.token;
1106
- }
1107
- function requirePersisted(sessionId) {
1108
- const persisted = persistence.readPersistedSessions()[sessionId];
1109
- if (!persisted) {
1110
- throw new Error(
1111
- `No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
1112
- );
1113
- }
1114
- return persisted;
1115
- }
1116
- async function readSessionLog(sessionId, persisted, limit) {
1117
- const token = await bearerToken$1();
1118
- const response = await axios.get(
1119
- `${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
1120
- {
1121
- params: { before_seq: 2147483647, limit },
1122
- headers: {
1123
- "Authorization": `Bearer ${token}`,
1124
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
1125
- },
1126
- timeout: 15e3
1127
- }
1128
- );
1129
- const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
1130
- messages.reverse();
1131
- const key = persistence.decodeBase64(persisted.encryptionKey);
1132
- return messages.map((message) => {
1133
- if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
1134
- try {
1135
- return { seq: message.seq, body: persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(message.content.c)) };
1136
- } catch {
1137
- return { seq: message.seq, body: null };
1138
- }
1139
- });
1140
- }
1141
- const TURN_WINDOW = 500;
1142
- async function readSessionTranscript(sessionId, limit, options = {}) {
1143
- const persisted = requirePersisted(sessionId);
1144
- const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
1145
- const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
1146
- const shown = entries.slice(-bounded);
1147
- const live = await persistence.listDaemonSessions();
1148
- return {
1149
- summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
1150
- messageCount: shown.length,
1151
- transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
1152
- turn: analyzeLatestTurn(entries)
1153
- };
1154
- }
1155
- async function readSessionMetadata(sessionId, persisted) {
1156
- const token = await bearerToken$1();
1157
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
1158
- headers: {
1159
- "Authorization": `Bearer ${token}`,
1160
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
1161
- },
1162
- timeout: 15e3
1163
- });
1164
- const ciphertext = response.data?.session?.metadata;
1165
- if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
1166
- return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(ciphertext));
1167
- }
1168
- async function readSessionState(sessionId, persisted) {
1169
- const token = await bearerToken$1();
1170
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
1171
- headers: {
1172
- "Authorization": `Bearer ${token}`,
1173
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
1174
- },
1175
- timeout: 15e3,
1176
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
1177
- });
1178
- if (response.status === 404) return { found: false, active: false };
1179
- const row = response.data?.session;
1180
- if (!row || typeof row.id !== "string") throw new Error(`Server returned no session row for ${sessionId}`);
1181
- const state = {
1182
- found: true,
1183
- active: row.active === true,
1184
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
1185
- archivedAt: typeof row.archivedAt === "number" ? row.archivedAt : null,
1186
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {}
1187
- };
1188
- if (persisted && typeof row.metadata === "string" && row.metadata.length > 0) {
1189
- try {
1190
- const metadata = persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(row.metadata));
1191
- if (metadata?.machineId) state.machineId = metadata.machineId;
1192
- } catch {
1193
- }
1194
- }
1195
- return state;
1196
- }
1197
- class TurnWaitTimeoutError extends Error {
1198
- constructor(sessionId, timeoutMs, turn) {
1199
- super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
1200
- this.sessionId = sessionId;
1201
- this.timeoutMs = timeoutMs;
1202
- this.turn = turn;
1203
- }
1204
- }
1205
- async function waitForTurnEnd(sessionId, options) {
1206
- const persisted = requirePersisted(sessionId);
1207
- const pollMs = options.pollMs ?? 3e3;
1208
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1209
- const deadline = Date.now() + options.timeoutMs;
1210
- while (true) {
1211
- const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
1212
- if (turn.ended) return turn;
1213
- if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
1214
- await sleep(pollMs);
1215
- }
1216
- }
1217
- async function stopSession(sessionId) {
1218
- return await persistence.stopDaemonSession(sessionId);
1219
- }
1220
- async function archiveSession(sessionId) {
1221
- const token = await bearerToken$1();
1222
- await axios.post(
1223
- `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
1224
- {},
1225
- {
1226
- headers: {
1227
- "Authorization": `Bearer ${token}`,
1228
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
1229
- },
1230
- timeout: 1e4
1231
- }
1232
- );
1233
- }
1234
- async function unarchiveSession(sessionId) {
1235
- const token = await bearerToken$1();
1236
- await axios.post(
1237
- `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/unarchive`,
1238
- {},
1239
- {
1240
- headers: {
1241
- "Authorization": `Bearer ${token}`,
1242
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
1243
- },
1244
- timeout: 1e4
1245
- }
1246
- );
1247
- }
1248
-
1249
- const ACTIVE_FRESH_WINDOW_MS = 15 * 60 * 1e3;
1250
- const DEFAULT_RESUME_WAIT_MS = 3e4;
1251
- const DEFAULT_RESUME_POLL_MS = 1e3;
1252
- function classifySessionState(input) {
1253
- const { liveHere, server, now } = input;
1254
- if (!server.found) return "not_found";
1255
- if (typeof server.archivedAt === "number") return "archived";
1256
- if (liveHere) return "live";
1257
- if (server.active && typeof server.activeAt === "number" && now - server.activeAt <= ACTIVE_FRESH_WINDOW_MS) return "live";
1258
- return "offline";
1259
- }
1260
- function explainNotLive(sessionId, status, resumeHint) {
1261
- switch (status) {
1262
- case "archived":
1263
- return `Session ${sessionId} is archived \u2014 no live wrapper will receive this message. ${resumeHint}`;
1264
- case "offline":
1265
- return `Session ${sessionId} is offline (its wrapper exited or timed out) \u2014 no live wrapper will receive this message. ${resumeHint}`;
1266
- case "not_found":
1267
- return `Session ${sessionId} was not found on this account \u2014 it cannot be messaged or resumed. Spawn a new session instead.`;
1268
- }
1269
- }
1270
- const CLI_RESUME_HINT = "Use --resume to bring it back on this machine, or spawn a new session.";
1271
- const defaultDeps$1 = {
1272
- listLive: () => persistence.listDaemonSessions(),
1273
- readState: (sessionId, persisted) => readSessionState(sessionId, persisted),
1274
- send: (sessionId, persisted, text, client, options) => sendUserMessage(sessionId, persisted, text, client, options),
1275
- unarchive: unarchiveSession,
1276
- archive: archiveSession,
1277
- resume: persistence.resumeDaemonSession,
1278
- sleep: (ms) => persistence.delay(ms).then(() => void 0),
1279
- now: () => Date.now()
1280
- };
1281
- async function deliverToSession(sessionId, persisted, text, client, options = {}, deps = {}) {
1282
- const d = { ...defaultDeps$1, ...deps };
1283
- const resumeHint = options.resumeHint ?? CLI_RESUME_HINT;
1284
- const classify = async () => {
1285
- const [live, server] = await Promise.all([d.listLive(), d.readState(sessionId, persisted)]);
1286
- return classifySessionState({ liveHere: sessionLiveness(live, sessionId).live, server, now: d.now() });
1287
- };
1288
- let status = await classify();
1289
- let resumed = false;
1290
- let resumeOutcome;
1291
- if (status !== "live") {
1292
- if (!options.resume || status === "not_found") {
1293
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: explainNotLive(sessionId, status, resumeHint) };
1294
- }
1295
- const wasArchived = status === "archived";
1296
- if (wasArchived) {
1297
- try {
1298
- await d.unarchive(sessionId);
1299
- } catch (error) {
1300
- const message = `unarchive failed: ${error instanceof Error ? error.message : String(error)}`;
1301
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
1302
- }
1303
- }
1304
- const outcome = await d.resume(sessionId, {
1305
- ...typeof options.model === "string" ? { model: options.model } : {},
1306
- ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {}
1307
- });
1308
- if (!outcome.success) {
1309
- let message = outcome.error ?? "resume failed";
1310
- if (wasArchived) {
1311
- try {
1312
- await d.archive(sessionId);
1313
- } catch (error) {
1314
- message += `; failed to restore archive state: ${error instanceof Error ? error.message : String(error)}`;
1315
- }
1316
- }
1317
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
1318
- }
1319
- resumed = true;
1320
- resumeOutcome = { ok: true };
1321
- status = await waitUntilLive(classify, d, options);
1322
- if (status !== "live") {
1323
- const waited = Math.round((options.waitMs ?? DEFAULT_RESUME_WAIT_MS) / 1e3);
1324
- return {
1325
- sessionId,
1326
- status,
1327
- delivered: false,
1328
- resumed,
1329
- stored: false,
1330
- resume: resumeOutcome,
1331
- error: `Session ${sessionId} was resumed but no live wrapper showed up within ${waited}s (state: ${status}); message not sent`
1332
- };
1333
- }
1334
- }
1335
- await d.send(sessionId, persisted, text, client, {
1336
- ...options.model !== void 0 ? { model: options.model } : {},
1337
- ...options.sentFrom !== void 0 ? { sentFrom: options.sentFrom } : {}
1338
- });
1339
- let after = "live";
1340
- try {
1341
- after = await classify();
1342
- } catch {
1343
- after = "live";
1344
- }
1345
- if (after !== "live") {
1346
- return {
1347
- sessionId,
1348
- status: after,
1349
- delivered: false,
1350
- resumed,
1351
- stored: true,
1352
- ...resumeOutcome ? { resume: resumeOutcome } : {},
1353
- error: `Session ${sessionId} went ${after} while sending: the message is stored server-side but no wrapper is attached to read it. ${after === "not_found" ? "Spawn a new session." : resumeHint}`
1354
- };
1355
- }
1356
- return { sessionId, status: "live", delivered: true, resumed, stored: true, ...resumeOutcome ? { resume: resumeOutcome } : {} };
1357
- }
1358
- async function waitUntilLive(classify, d, options) {
1359
- const waitMs = options.waitMs ?? DEFAULT_RESUME_WAIT_MS;
1360
- const pollMs = options.pollMs ?? DEFAULT_RESUME_POLL_MS;
1361
- const deadline = d.now() + waitMs;
1362
- let status = await classify();
1363
- while (status !== "live" && d.now() < deadline) {
1364
- await d.sleep(pollMs);
1365
- status = await classify();
1366
- }
1367
- return status;
1368
- }
1369
-
1370
- const SESSION_PEER_SENT_FROM = "session-peer";
1371
- const CLI_PEER_SENDER_ID = "cli";
1372
- const MESSAGE_FOOTER = "This message comes from another agent session on this machine, not from the user.";
1373
- const MESSAGE_FOOTER_PREFIX = "This message comes from another agent session";
1374
- function messageFooterFor(sender) {
1375
- return sender.machine ? `${MESSAGE_FOOTER_PREFIX} on machine ${sanitizeHeaderValue(sender.machine, 128)}, not from the user.` : MESSAGE_FOOTER;
1376
- }
1377
- function sanitizeHeaderValue(value, max = 200) {
1378
- if (!value) return "";
1379
- const collapsed = value.replace(/\s+/g, " ").trim().slice(0, max);
1380
- return collapsed.replace(/[%"\];]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
1381
- }
1382
- function senderFields(sender) {
1383
- const parts = [];
1384
- if (sender.flavor) parts.push(`agent ${sanitizeHeaderValue(sender.flavor, 32)}`);
1385
- if (sender.cwd) parts.push(`cwd ${sanitizeHeaderValue(sender.cwd, 400)}`);
1386
- if (sender.machine) parts.push(`machine ${sanitizeHeaderValue(sender.machine, 128)}`);
1387
- return parts.map((part) => `; ${part}`).join("");
1388
- }
1389
- function quotedTitle(sender) {
1390
- return `"${sanitizeHeaderValue(sender.title, 120)}"`;
1391
- }
1392
- function formatSessionPeerMessage(input) {
1393
- const header = `[Very Happy session message ${input.id} from ${quotedTitle(input.from)} ${input.from.sessionId}${senderFields(input.from)}${input.replyTo ? `; re ${sanitizeHeaderValue(input.replyTo, 64)}` : ""}]`;
1394
- const base = messageFooterFor(input.from);
1395
- const routed = input.from.machine ? " (it is routed to that machine for you)" : "";
1396
- const footer = input.from.sessionId === CLI_PEER_SENDER_ID ? `${base} It was sent from the command line (\`very-happy sessions message\`), so there is no session to reply to; act on it here.` : `${base} Reply with session_message(to: "${input.from.sessionId}")${routed} \u2014 or \`very-happy sessions message ${input.from.sessionId} "<text>"\` \u2014 only when you have something to add (a question, a decision, a heads-up). Do not reply just to acknowledge or thank; the exchange ends when nothing is left to coordinate.`;
1397
- return `${header}
1398
- ${input.body.trim()}
1399
-
1400
- ${footer}`;
1401
- }
1402
- function formatDuration(ms) {
1403
- const seconds = Math.max(0, Math.round(ms / 1e3));
1404
- if (seconds < 90) return `${seconds}s`;
1405
- const minutes = Math.round(seconds / 60);
1406
- if (minutes < 90) return `${minutes}m`;
1407
- return `${Math.round(minutes / 60)}h`;
1408
- }
1409
- function formatEditConflictNotice(input) {
1410
- const paths = input.paths && input.paths.length > 0 ? input.paths : [input.path];
1411
- const more = paths.length - 1;
1412
- const header = `[Very Happy edit conflict ${input.id}; file ${sanitizeHeaderValue(paths[0], 600)}${more > 0 ? `; more ${more}` : ""}; peer ${quotedTitle(input.peer)} ${input.peer.sessionId}${senderFields(input.peer)}; peer edited ${formatDuration(input.peerEditedAgoMs)} ago]`;
1413
- const window = formatDuration(input.windowMs);
1414
- const mirror = input.peer.flavor === "terminal-mirror";
1415
- const what = more > 0 ? `the same ${paths.length} files` : "the same file";
1416
- const advice = mirror ? `The peer is a terminal session (agent terminal-mirror): it cannot be messaged, and the person at that terminal owns those edits \u2014 re-read the file before changing it further and keep your change minimal.` : `No lock is held \u2014 coordinate before continuing: session_message(to: "${input.peer.sessionId}", body: "\u2026") saying what you are changing in ${more > 0 ? "these files" : "that file"} and asking what they are changing. One message that covers everything; do not send one per file.`;
1417
- const list = more > 0 ? `
1418
- Files:
1419
- ${paths.map((p) => `- ${p}`).join("\n")}` : "";
1420
- return `${header}
1421
- Another session on this machine edited ${what} within the last ${window}. Both sessions received this notice. ${advice}${list}`;
1422
- }
1423
-
1424
- const realFs = {
1425
- existsSync: node_fs.existsSync,
1426
- isDirectory: (path) => {
1427
- try {
1428
- return node_fs.statSync(path).isDirectory();
1429
- } catch {
1430
- return false;
1431
- }
1432
- },
1433
- readFileSync: (path) => node_fs.readFileSync(path, "utf8")
1434
- };
1435
- function resolveRepoIdentity(cwd, fs = realFs) {
1436
- let dir = path.resolve(cwd);
1437
- for (let depth = 0; depth < 64; depth++) {
1438
- const dotGit = path.resolve(dir, ".git");
1439
- try {
1440
- if (fs.existsSync(dotGit)) {
1441
- if (fs.isDirectory(dotGit)) return { root: dir, common: dotGit };
1442
- const gitdir = fs.readFileSync(dotGit).match(/^gitdir:\s*(.+?)\s*$/m)?.[1];
1443
- if (gitdir) {
1444
- const absolute = path.isAbsolute(gitdir) ? gitdir : path.resolve(dir, gitdir);
1445
- const worktree = absolute.match(/^(.*[\\/]\.git)[\\/]worktrees[\\/][^\\/]+[\\/]?$/);
1446
- return { root: dir, common: worktree ? worktree[1] : absolute };
1447
- }
1448
- return { root: dir, common: dotGit };
1449
- }
1450
- } catch {
1451
- }
1452
- const parent = path.dirname(dir);
1453
- if (parent === dir) break;
1454
- dir = parent;
1455
- }
1456
- return { root: null, common: null };
1457
- }
1458
- const PEER_SCOPES$1 = ["repo", "cwd", "machine"];
1459
- function isPeerInScope(scope, self, other) {
1460
- if (scope === "machine") return true;
1461
- if (scope === "cwd") return path.resolve(self.cwd) === path.resolve(other.cwd);
1462
- if (self.repo.common && other.repo.common) return self.repo.common === other.repo.common;
1463
- return path.resolve(self.cwd) === path.resolve(other.cwd);
1464
- }
1465
-
1466
- const REMOTE_SESSION_OPS_MIN_CLI_VERSION = "0.2.157";
1467
- const REMOTE_OPS_PROTOCOL_VERSION = 1;
1468
- const REMOTE_TEXT_MAX_BYTES = 64 * 1024;
1469
- const REMOTE_TRANSCRIPT_MAX_BYTES = 200 * 1024;
1470
- const REMOTE_CALLS_PER_MINUTE = 60;
1471
- const REMOTE_WRITE_CALLS_PER_MINUTE = 30;
1472
- const REMOTE_LOCATE_MAX_IDS = 200;
1473
- const REMOTE_OP_DEADLINE_MS = 25e3;
1474
- const REMOTE_SEND_COMMIT_DEADLINE_MS = 18e3;
1475
- const REMOTE_RESUME_WAIT_MS = 1e4;
1476
- const REMOTE_WAIT_POLL_MS = 5e3;
1477
- const REMOTE_ANSWER_MAX_BYTES = 64 * 1024;
1478
- const REMOTE_TITLE_MAX_BYTES = 1024;
1479
- const REMOTE_SENT_FROM_ALLOWED = ["cli", "assistant", "automation"];
1480
- const LOCAL_ID_RE = /^[a-zA-Z0-9_.:-]{1,128}$/;
1481
- function remoteError(code, message) {
1482
- return { ok: false, error: { code, message } };
1483
- }
1484
- const SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
1485
- const PEER_SCOPES = ["repo", "cwd", "machine"];
1486
- function isSessionId(value) {
1487
- return typeof value === "string" && SESSION_ID_RE.test(value);
1488
- }
1489
- function utf8Bytes(text) {
1490
- return Buffer.byteLength(text, "utf8");
1491
- }
1492
- function sanitizeSender(raw) {
1493
- if (!raw || typeof raw !== "object") return null;
1494
- const r = raw;
1495
- if (typeof r.sessionId !== "string" || r.sessionId.length === 0 || r.sessionId.length > 128) return null;
1496
- const optional = (key, max) => typeof r[key] === "string" && r[key].length > 0 ? { [key]: r[key].slice(0, max) } : {};
1497
- return {
1498
- sessionId: r.sessionId,
1499
- ...optional("title", 200),
1500
- ...optional("flavor", 32),
1501
- ...optional("cwd", 1024),
1502
- ...optional("machine", 128)
1503
- };
1504
- }
1505
- function guardRemoteSessionOpsRequest(method, raw, options) {
1506
- if (!options.enabled) {
1507
- return remoteError("disabled", 'Remote session operations are turned off on this machine (settings.json remoteSessionOps = "off")');
1508
- }
1509
- if (!raw || typeof raw !== "object") return remoteError("bad_request", "Request must be an object");
1510
- const r = raw;
1511
- if (r.v !== REMOTE_OPS_PROTOCOL_VERSION) return remoteError("bad_request", `Unsupported protocol version ${String(r.v)} (this daemon speaks v${REMOTE_OPS_PROTOCOL_VERSION})`);
1512
- const from = r.from;
1513
- if (!from || typeof from !== "object" || typeof from.host !== "string" || from.host.length === 0 || typeof from.cli !== "string") {
1514
- return remoteError("bad_request", "from.host and from.cli are required");
1515
- }
1516
- const caller = {
1517
- host: from.host.slice(0, 128),
1518
- cli: from.cli.slice(0, 32),
1519
- ...typeof from.machineId === "string" && from.machineId.length > 0 ? { machineId: from.machineId.slice(0, 128) } : {},
1520
- ...isSessionId(from.sessionId) ? { sessionId: from.sessionId } : {}
1521
- };
1522
- const args = r.args && typeof r.args === "object" ? r.args : {};
1523
- const limit = typeof args.limit === "number" && Number.isFinite(args.limit) && args.limit >= 1 ? Math.floor(args.limit) : void 0;
1524
- let clean;
1525
- switch (method) {
1526
- case "sessions.list": {
1527
- if (args.ids !== void 0) {
1528
- if (!Array.isArray(args.ids) || !args.ids.every(isSessionId)) return remoteError("bad_request", "ids must be an array of session ids");
1529
- if (args.ids.length > REMOTE_LOCATE_MAX_IDS) return remoteError("too_large", `ids: at most ${REMOTE_LOCATE_MAX_IDS} per call`);
1530
- }
1531
- const out = {
1532
- ...args.ids !== void 0 ? { ids: args.ids } : {},
1533
- ...args.all === true ? { all: true } : {},
1534
- ...typeof args.tag === "string" && args.tag.length > 0 ? { tag: args.tag.slice(0, 128) } : {},
1535
- ...limit !== void 0 ? { limit } : {}
1536
- };
1537
- clean = out;
1538
- break;
1539
- }
1540
- case "sessions.read": {
1541
- if (!isSessionId(args.sessionId)) return remoteError("bad_request", "sessionId is required");
1542
- const out = { sessionId: args.sessionId, ...limit !== void 0 ? { limit } : {}, ...args.full === true ? { full: true } : {} };
1543
- clean = out;
1544
- break;
1545
- }
1546
- case "sessions.send": {
1547
- if (!isSessionId(args.sessionId)) return remoteError("bad_request", "sessionId is required");
1548
- if (typeof args.text !== "string" || args.text.trim().length === 0) return remoteError("bad_request", "text must be non-empty");
1549
- if (utf8Bytes(args.text) > REMOTE_TEXT_MAX_BYTES) return remoteError("too_large", `text exceeds ${REMOTE_TEXT_MAX_BYTES} bytes`);
1550
- if (args.localId !== void 0 && (typeof args.localId !== "string" || !LOCAL_ID_RE.test(args.localId))) return remoteError("bad_request", "localId must be a short id");
1551
- const out = {
1552
- sessionId: args.sessionId,
1553
- text: args.text,
1554
- ...args.resume === true ? { resume: true } : {},
1555
- ...args.model === null || typeof args.model === "string" && args.model.length <= 128 ? { model: args.model } : {},
1556
- ...REMOTE_SENT_FROM_ALLOWED.includes(args.sentFrom) ? { sentFrom: args.sentFrom } : {},
1557
- ...typeof args.localId === "string" ? { localId: args.localId } : {}
1558
- };
1559
- clean = out;
1560
- break;
1561
- }
1562
- case "sessions.peers": {
1563
- if (args.scope !== void 0 && !PEER_SCOPES.includes(args.scope)) return remoteError("bad_request", `scope must be one of ${PEER_SCOPES.join(", ")}`);
1564
- const out = {
1565
- ...args.scope !== void 0 ? { scope: args.scope } : {},
1566
- ...typeof args.cwd === "string" && args.cwd.length > 0 ? { cwd: args.cwd.slice(0, 4096) } : {}
1567
- };
1568
- clean = out;
1569
- break;
1570
- }
1571
- case "sessions.message": {
1572
- if (!isSessionId(args.to)) return remoteError("bad_request", "to is required");
1573
- if (typeof args.body !== "string" || args.body.trim().length === 0) return remoteError("bad_request", "body must be non-empty");
1574
- if (utf8Bytes(args.body) > REMOTE_TEXT_MAX_BYTES) return remoteError("too_large", `body exceeds ${REMOTE_TEXT_MAX_BYTES} bytes`);
1575
- const sender = sanitizeSender(args.from);
1576
- if (!sender) return remoteError("bad_request", "from.sessionId (the sender) is required");
1577
- const out = {
1578
- to: args.to,
1579
- body: args.body,
1580
- ...typeof args.replyTo === "string" && args.replyTo.length > 0 ? { replyTo: args.replyTo.slice(0, 64) } : {},
1581
- // The recipient's header names the machine the call came
1582
- // from, as the caller identified itself — never a value the
1583
- // sender picked for someone else.
1584
- from: { ...sender, machine: caller.host }
1585
- };
1586
- clean = out;
1587
- break;
1588
- }
1589
- }
1590
- return { ok: true, request: { v: REMOTE_OPS_PROTOCOL_VERSION, from: caller, args: clean } };
1591
- }
1592
- class RemoteCallBudget {
1593
- constructor(perMinute = REMOTE_CALLS_PER_MINUTE, now = Date.now) {
1594
- this.perMinute = perMinute;
1595
- this.now = now;
1596
- this.tokens = perMinute;
1597
- this.lastRefill = now();
1598
- }
1599
- tokens;
1600
- lastRefill;
1601
- /** Take one token; false = over budget (with ms until the next token). */
1602
- take() {
1603
- if (this.perMinute <= 0) return { ok: true };
1604
- const t = this.now();
1605
- const refill = (t - this.lastRefill) / 6e4 * this.perMinute;
1606
- if (refill > 0) {
1607
- this.tokens = Math.min(this.perMinute, this.tokens + refill);
1608
- this.lastRefill = t;
1609
- }
1610
- if (this.tokens >= 1) {
1611
- this.tokens -= 1;
1612
- return { ok: true };
1613
- }
1614
- return { ok: false, retryAfterMs: Math.ceil((1 - this.tokens) / this.perMinute * 6e4) };
1615
- }
1616
- }
1617
- function sanitizeAuditValue(value, max = 64) {
1618
- if (typeof value !== "string" || value.length === 0) return "?";
1619
- return value.slice(0, max).replace(/[^\x20-\x7e]/g, "?");
1620
- }
1621
- function formatRemoteAuditLine(input) {
1622
- const from = input.from;
1623
- const opt = (value) => value === void 0 ? "-" : sanitizeAuditValue(value);
1624
- return `[REMOTE SESSION OPS] ${sanitizeAuditValue(input.method)} from machine=${sanitizeAuditValue(from?.machineId)} host=${sanitizeAuditValue(from?.host)} cli=${sanitizeAuditValue(from?.cli, 32)} session=${opt(from?.sessionId)} target=${opt(input.sessionId)} \u2192 ${input.outcome} (${Math.max(0, Math.round(input.durationMs))}ms)`;
1625
- }
1626
- function capRemoteText(text, maxBytes, keep = "head") {
1627
- if (utf8Bytes(text) <= maxBytes) return text;
1628
- const buffer = Buffer.from(text, "utf8");
1629
- const marker = "\u2026";
1630
- const room = Math.max(0, maxBytes - Buffer.byteLength(marker));
1631
- const slice = keep === "head" ? buffer.subarray(0, room) : buffer.subarray(buffer.length - room);
1632
- const clean = slice.toString("utf8").replace(/^�+|�+$/g, "");
1633
- return keep === "head" ? clean + marker : marker + clean;
1634
- }
1635
- function capRemoteTranscript(transcript, maxBytes = REMOTE_TRANSCRIPT_MAX_BYTES) {
1636
- if (utf8Bytes(transcript) <= maxBytes) return { transcript, truncated: false };
1637
- const buffer = Buffer.from(transcript, "utf8");
1638
- const marker = "\u2026 [truncated by the remote machine: transcript exceeded the RPC budget; use --limit] \u2026\n";
1639
- const tail = buffer.subarray(buffer.length - (maxBytes - Buffer.byteLength(marker))).toString("utf8");
1640
- return { transcript: marker + tail.replace(/^�+/, ""), truncated: true };
1641
- }
1642
- function parseHappyClientVersion(value) {
1643
- if (typeof value !== "string") return null;
1644
- const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
1645
- return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
1646
- }
1647
- function compareVersions(a, b) {
1648
- const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
1649
- const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
1650
- for (let i = 0; i < 3; i++) {
1651
- const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1652
- if (d !== 0) return d;
1653
- }
1654
- return 0;
1655
- }
1656
- function supportsRemoteSessionOps(cliVersion) {
1657
- if (!cliVersion) return null;
1658
- return compareVersions(cliVersion, REMOTE_SESSION_OPS_MIN_CLI_VERSION) >= 0;
1659
- }
1660
-
1661
- const REMOTE_CLIENT_TAG = "session-ops";
1662
- const REMOTE_RPC_TIMEOUT_MS = 3e4;
1663
- const REMOTE_LOCATE_MAX_MACHINES = 8;
1664
- class RemoteSessionOpsError extends Error {
1665
- constructor(code, message, machineId, ambiguous = false) {
1666
- super(message);
1667
- this.code = code;
1668
- this.machineId = machineId;
1669
- this.ambiguous = ambiguous;
1670
- }
1671
- }
1672
- async function bearerToken() {
1673
- const credentials = await persistence.readCredentialsForConfiguredRelay();
1674
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
1675
- return credentials.token;
1676
- }
1677
- async function listAccountMachines() {
1678
- const token = await bearerToken();
1679
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/machines`, {
1680
- headers: { "Authorization": `Bearer ${token}`, "X-Happy-Client": `${REMOTE_CLIENT_TAG}/${persistence.configuration.currentCliVersion}` },
1681
- timeout: 15e3
1682
- });
1683
- const rows = Array.isArray(response.data) ? response.data : [];
1684
- return rows.filter((row) => !!row && typeof row === "object" && typeof row.id === "string").map((row) => ({
1685
- id: row.id,
1686
- active: row.active === true,
1687
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
1688
- cliVersion: parseHappyClientVersion(row.lastHappyClient)
1689
- }));
1690
- }
1691
- async function callerIdentity(env = process.env) {
1692
- const settings = await persistence.readSettings().catch(() => null);
1693
- const sessionId = env.VH_PEER_SESSION_ID;
1694
- return {
1695
- ...settings?.machineId ? { machineId: settings.machineId } : {},
1696
- host: os.hostname(),
1697
- cli: persistence.configuration.currentCliVersion,
1698
- ...sessionId && /^[a-zA-Z0-9_-]{1,128}$/.test(sessionId) ? { sessionId } : {}
1699
- };
1700
- }
1701
- const defaultDeps = {
1702
- listMachines: listAccountMachines,
1703
- selfMachineId: async () => (await persistence.readSettings().catch(() => null))?.machineId,
1704
- openTransport: async () => {
1705
- const socket = await openUserScopedSocket(await bearerToken());
1706
- return {
1707
- call: (method, paramsJson) => socket.rpcCall({ method, params: paramsJson }, REMOTE_RPC_TIMEOUT_MS),
1708
- close: () => socket.close()
1709
- };
1710
- },
1711
- caller: () => callerIdentity(),
1712
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
1713
- now: () => Date.now()
1714
- };
1715
- function withDefaults(deps) {
1716
- return { ...defaultDeps, ...deps };
1717
- }
1718
- function pickRemoteCandidates(machines, selfId) {
1719
- const candidates = [];
1720
- const skipped = [];
1721
- for (const machine of machines) {
1722
- if (selfId && machine.id === selfId) {
1723
- skipped.push({ machine, reason: "self" });
1724
- continue;
1725
- }
1726
- if (!machine.active) {
1727
- skipped.push({ machine, reason: "offline" });
1728
- continue;
1729
- }
1730
- if (supportsRemoteSessionOps(machine.cliVersion) === false) {
1731
- skipped.push({ machine, reason: "too_old" });
1732
- continue;
1733
- }
1734
- candidates.push(machine);
1735
- }
1736
- candidates.sort((a, b) => (b.activeAt ?? 0) - (a.activeAt ?? 0));
1737
- return { candidates: candidates.slice(0, REMOTE_LOCATE_MAX_MACHINES), skipped };
1738
- }
1739
- function describeAge(activeAt, now) {
1740
- if (activeAt === void 0) return "never seen";
1741
- const minutes = Math.round((now - activeAt) / 6e4);
1742
- if (minutes < 2) return "seen just now";
1743
- if (minutes < 120) return `last seen ${minutes}m ago`;
1744
- if (minutes < 48 * 60) return `last seen ${Math.round(minutes / 60)}h ago`;
1745
- return `last seen ${Math.round(minutes / 1440)}d ago`;
1746
- }
1747
- async function resolveExplicitMachine(machineId, deps) {
1748
- const machines = await deps.listMachines();
1749
- const machine = machines.find((row) => row.id === machineId);
1750
- if (!machine) throw new RemoteSessionOpsError("unknown_machine", `Machine ${machineId} is not on this account`, machineId);
1751
- if (!machine.active) throw new RemoteSessionOpsError("offline", `Machine ${machineId} is offline (${describeAge(machine.activeAt, deps.now())}); its sessions cannot be reached until its daemon reconnects`, machineId);
1752
- if (supportsRemoteSessionOps(machine.cliVersion) === false) {
1753
- throw new RemoteSessionOpsError("too_old", `Machine ${machineId} runs CLI ${machine.cliVersion}; cross-machine session operations need \u2265 ${REMOTE_SESSION_OPS_MIN_CLI_VERSION} on the target`, machineId);
1754
- }
1755
- return machine;
1756
- }
1757
- async function callRemoteSessionOp(transport, machineId, method, args, from) {
1758
- const params = JSON.stringify({ v: REMOTE_OPS_PROTOCOL_VERSION, from, args });
1759
- let ack;
1760
- try {
1761
- ack = await transport.call(`${machineId}:${method}`, params);
1762
- } catch (error) {
1763
- const reason = error instanceof Error ? error.message : String(error);
1764
- throw new RemoteSessionOpsError("unreachable", `Machine ${machineId} did not answer ${method} (${reason})`, machineId, true);
1765
- }
1766
- if (!ack?.ok) {
1767
- const reason = ack?.error ?? "no ack";
1768
- if (/not available/i.test(reason)) {
1769
- throw new RemoteSessionOpsError("unreachable", `Machine ${machineId} did not answer ${method}: its daemon is offline, restarting, or runs a CLI older than ${REMOTE_SESSION_OPS_MIN_CLI_VERSION}`, machineId);
1770
- }
1771
- throw new RemoteSessionOpsError("unreachable", `Machine ${machineId}: ${reason}`, machineId, true);
1772
- }
1773
- let envelope;
1774
- try {
1775
- envelope = typeof ack.result === "string" ? JSON.parse(ack.result) : ack.result;
1776
- } catch {
1777
- throw new RemoteSessionOpsError("unreachable", `Machine ${machineId} returned an unreadable ${method} response`, machineId);
1778
- }
1779
- if (!envelope || typeof envelope !== "object") throw new RemoteSessionOpsError("unreachable", `Machine ${machineId} returned an empty ${method} response`, machineId);
1780
- if ("ok" in envelope) {
1781
- if (envelope.ok) return envelope.result;
1782
- throw new RemoteSessionOpsError(envelope.error.code, `Machine ${machineId}: ${envelope.error.message}`, machineId);
1783
- }
1784
- throw new RemoteSessionOpsError("internal", `Machine ${machineId}: ${envelope.error ?? "remote handler failed"}`, machineId);
1785
- }
1786
- async function locateRemoteSession(transport, sessionId, options, deps) {
1787
- const from = await deps.caller();
1788
- if (options.machineId) {
1789
- const machine = await resolveExplicitMachine(options.machineId, deps);
1790
- const listing = await callRemoteSessionOp(transport, machine.id, "sessions.list", { ids: [sessionId] }, from);
1791
- if (!listing.sessions.some((row) => row.id === sessionId)) {
1792
- throw new RemoteSessionOpsError("no_local_key", `Machine ${machine.id} (${listing.host}) has no key for session ${sessionId}: it was not spawned there (or is older than 14 days)`, machine.id);
1793
- }
1794
- return { machine, host: listing.host };
1795
- }
1796
- const { candidates, skipped } = pickRemoteCandidates(await deps.listMachines(), await deps.selfMachineId());
1797
- const failures = [];
1798
- for (const machine of candidates) {
1799
- try {
1800
- const listing = await callRemoteSessionOp(transport, machine.id, "sessions.list", { ids: [sessionId] }, from);
1801
- if (listing.sessions.some((row) => row.id === sessionId)) return { machine, host: listing.host };
1802
- failures.push(`${listing.host || machine.id}: not there`);
1803
- } catch (error) {
1804
- failures.push(`${machine.id}: ${error instanceof RemoteSessionOpsError ? error.code : error instanceof Error ? error.message : String(error)}`);
1805
- }
1806
- }
1807
- const skippedText = skipped.filter((s) => s.reason !== "self").map((s) => `${s.machine.id}: ${s.reason}`);
1808
- const detail = [...failures, ...skippedText];
1809
- throw new RemoteSessionOpsError(
1810
- "not_located",
1811
- `Session ${sessionId} is not on this machine and no online machine of this account reports it${detail.length ? ` (${detail.join("; ")})` : " (no other machines are online)"}. Pass --machine <id> to name one, or bring its machine online.`
1812
- );
1813
- }
1814
- async function withRemoteTransport(deps, fn) {
1815
- const transport = await deps.openTransport();
1816
- try {
1817
- return await fn(transport);
1818
- } finally {
1819
- try {
1820
- transport.close();
1821
- } catch {
1822
- }
1823
- }
1824
- }
1825
- async function readRemoteTranscript(sessionId, options = {}, overrides = {}) {
1826
- const deps = withDefaults(overrides);
1827
- return withRemoteTransport(deps, async (transport) => {
1828
- const located = await locateRemoteSession(transport, sessionId, { machineId: options.machineId }, deps);
1829
- return callRemoteSessionOp(transport, located.machine.id, "sessions.read", {
1830
- sessionId,
1831
- ...options.limit !== void 0 ? { limit: options.limit } : {},
1832
- ...options.full ? { full: true } : {}
1833
- }, await deps.caller());
1834
- });
1835
- }
1836
- async function waitForRemoteTurnEnd(sessionId, options, overrides = {}) {
1837
- const deps = withDefaults(overrides);
1838
- const pollMs = options.pollMs ?? REMOTE_WAIT_POLL_MS;
1839
- return withRemoteTransport(deps, async (transport) => {
1840
- const located = await locateRemoteSession(transport, sessionId, { machineId: options.machineId }, deps);
1841
- const from = await deps.caller();
1842
- const deadline = deps.now() + options.timeoutMs;
1843
- const args = { sessionId, ...options.limit !== void 0 ? { limit: options.limit } : {}, ...options.full ? { full: true } : {} };
1844
- while (true) {
1845
- const read = await callRemoteSessionOp(transport, located.machine.id, "sessions.read", args, from);
1846
- if (read.turn.ended) return { read, timedOut: false };
1847
- if (deps.now() + pollMs > deadline) return { read, timedOut: true };
1848
- await deps.sleep(pollMs);
1849
- }
1850
- });
1851
- }
1852
- async function sendRemoteMessage(sessionId, text, options = {}, overrides = {}) {
1853
- const deps = withDefaults(overrides);
1854
- return withRemoteTransport(deps, async (transport) => {
1855
- const located = await locateRemoteSession(transport, sessionId, { machineId: options.machineId }, deps);
1856
- const from = await deps.caller();
1857
- const localId = options.localId ?? `remote-send-${node_crypto.randomUUID()}`;
1858
- const args = {
1859
- sessionId,
1860
- text,
1861
- localId,
1862
- ...options.resume ? { resume: true } : {},
1863
- ...options.model !== void 0 ? { model: options.model } : {},
1864
- ...options.sentFrom !== void 0 ? { sentFrom: options.sentFrom } : {}
1865
- };
1866
- const attempt = () => callRemoteSessionOp(transport, located.machine.id, "sessions.send", args, from);
1867
- let result;
1868
- try {
1869
- result = await attempt();
1870
- } catch (error) {
1871
- const retryable = error instanceof RemoteSessionOpsError && (error.ambiguous || error.code === "timeout");
1872
- if (!retryable) throw error;
1873
- result = await attempt();
1874
- }
1875
- return { ...result, machine: { id: located.machine.id, host: located.host } };
1876
- });
1877
- }
1878
- async function sendRemotePeerMessage(args, options = {}, overrides = {}) {
1879
- const deps = withDefaults(overrides);
1880
- return withRemoteTransport(deps, async (transport) => {
1881
- const located = await locateRemoteSession(transport, args.to, { machineId: options.machineId }, deps);
1882
- const caller = await deps.caller();
1883
- const result = await callRemoteSessionOp(transport, located.machine.id, "sessions.message", {
1884
- to: args.to,
1885
- body: args.body,
1886
- ...args.replyTo ? { replyTo: args.replyTo } : {},
1887
- from: { ...args.from, machine: args.from.machine ?? caller.host }
1888
- }, caller);
1889
- return { ...result, machine: { id: located.machine.id, host: located.host } };
1890
- });
1891
- }
1892
- async function listRemotePeers(machineId, options = {}, overrides = {}) {
1893
- const deps = withDefaults(overrides);
1894
- return withRemoteTransport(deps, async (transport) => {
1895
- const machine = await resolveExplicitMachine(machineId, deps);
1896
- return callRemoteSessionOp(transport, machine.id, "sessions.peers", {
1897
- ...options.scope ? { scope: options.scope } : {},
1898
- ...options.cwd ? { cwd: options.cwd } : {}
1899
- }, await deps.caller());
1900
- });
1901
- }
1902
- async function listRemoteSessions(machineId, options = {}, overrides = {}) {
1903
- const deps = withDefaults(overrides);
1904
- return withRemoteTransport(deps, async (transport) => {
1905
- const machine = await resolveExplicitMachine(machineId, deps);
1906
- return callRemoteSessionOp(transport, machine.id, "sessions.list", {
1907
- ...options.tag ? { tag: options.tag } : {},
1908
- ...options.limit !== void 0 ? { limit: options.limit } : {}
1909
- }, await deps.caller());
1910
- });
1911
- }
1912
- async function fillForeignAccountRows(rows, overrides = {}) {
1913
- const deps = withDefaults(overrides);
1914
- const pending = new Set(rows.filter((row) => !row.decryptable).map((row) => row.id));
1915
- const asked = [];
1916
- if (pending.size === 0) return { rows, asked, skipped: [] };
1917
- const { candidates, skipped } = pickRemoteCandidates(await deps.listMachines(), await deps.selfMachineId());
1918
- const filled = /* @__PURE__ */ new Map();
1919
- if (candidates.length > 0) {
1920
- await withRemoteTransport(deps, async (transport) => {
1921
- const from = await deps.caller();
1922
- for (const machine of candidates) {
1923
- if (pending.size === 0) break;
1924
- try {
1925
- const listing = await callRemoteSessionOp(transport, machine.id, "sessions.list", { ids: [...pending], all: true }, from);
1926
- let count = 0;
1927
- for (const row of listing.sessions) {
1928
- if (!pending.has(row.id)) continue;
1929
- pending.delete(row.id);
1930
- count++;
1931
- filled.set(row.id, { ...row, decryptable: false, readable: true, via: machine.id, machine: { id: machine.id, host: listing.host } });
1932
- }
1933
- asked.push({ machineId: machine.id, host: listing.host, filled: count });
1934
- } catch (error) {
1935
- asked.push({ machineId: machine.id, filled: 0, error: error instanceof Error ? error.message : String(error) });
1936
- }
1937
- }
1938
- });
1939
- }
1940
- return {
1941
- rows: rows.map((row) => filled.get(row.id) ?? row),
1942
- asked,
1943
- skipped: skipped.filter((s) => s.reason !== "self").map((s) => ({ machineId: s.machine.id, reason: s.reason }))
1944
- };
1945
- }
1946
-
1947
- const SESSION_PEER_TOOL_NAMES = ["session_message", "session_peers"];
1948
- const SESSION_PEER_TOOL_SCHEMAS = {
1949
- session_message: {
1950
- title: "Message Another Session",
1951
- readOnly: false,
1952
- description: "Send a message to another agent session (find ids with session_peers). It lands in that session's chat with your title, session id and cwd, and tells the peer how to reply to you. Use it to coordinate when you both edit the same files: say what you are changing and ask what they are changing. Returns as soon as the message is queued; the reply arrives later as a message from that session. Sessions on this machine are reached directly; a session spawned by another machine of this account is routed to that machine's daemon (it must be online). Terminal-mirror sessions cannot receive messages.",
1953
- inputSchema: {
1954
- to: z.z.string().min(1).max(128).describe("Target session id (from session_peers, or a peer's message header)"),
1955
- body: z.z.string().min(1).max(16e3).describe("The message text"),
1956
- replyTo: z.z.string().min(1).max(64).optional().describe("Id of the peer message you are answering (from its header), if any"),
1957
- machineId: z.z.string().min(1).max(128).optional().describe("Machine id the target session runs on, when known (skips the lookup across the account's machines)")
1958
- }
1959
- },
1960
- session_peers: {
1961
- title: "List Peer Sessions",
1962
- readOnly: true,
1963
- description: 'List the other agent sessions running on this machine and the files each edited in the last 30 minutes. Default scope "repo": sessions in the same git repository, including its other worktrees (sameWorktree says whether they share your checkout). "cwd": the same directory only. "machine": every live session here. With machineId, list the live sessions on that other machine of this account instead (scope "machine" unless you pass a cwd that exists there).',
1964
- inputSchema: {
1965
- scope: z.z.enum(["repo", "cwd", "machine"]).optional().describe("repo (default) | cwd | machine"),
1966
- machineId: z.z.string().min(1).max(128).optional().describe("Another machine of this account (from the web's machine list) to list instead of this one"),
1967
- cwd: z.z.string().min(1).max(4096).optional().describe("With machineId: a directory on that machine to compute repo/cwd scope from")
1968
- }
1969
- }
1970
- };
1971
- const PEER_MESSAGE_RATE_LIMIT = 8;
1972
- const PEER_MESSAGE_RATE_WINDOW_MS = 10 * 6e4;
1973
- async function listPeerSessions(context, scope = "repo") {
1974
- const self = context.self();
1975
- const repoOf = context.repoOf ?? resolveRepoIdentity;
1976
- const selfRepo = repoOf(self.cwd);
1977
- const sessions = await (context.listPeers ?? persistence.listDaemonPeers)();
1978
- const peers = sessions.filter((session) => session.sessionId !== self.sessionId).filter((session) => {
1979
- const cwd = session.cwd ?? "";
1980
- if (!cwd) return scope === "machine";
1981
- return isPeerInScope(scope, { cwd: self.cwd, repo: selfRepo }, { cwd, repo: repoOf(cwd) });
1982
- }).map((session) => ({
1983
- ...session,
1984
- url: sessionWebUrl(session.sessionId),
1985
- sameWorktree: !!session.cwd && !!selfRepo.root && repoOf(session.cwd).root === selfRepo.root
1986
- }));
1987
- return { self: { sessionId: self.sessionId, cwd: self.cwd, repoRoot: selfRepo.root, scope }, scope, peers };
1988
- }
1989
- function defaultDeliver(to, persisted, text, localId) {
1990
- return deliverToSession(to, persisted, text, "session-message", {
1991
- sentFrom: SESSION_PEER_SENT_FROM,
1992
- resumeHint: "It has to be running to read a peer message; spawn a new session or ask the person."
1993
- }, {
1994
- send: (sessionId, key, body, client, options) => sendUserMessage(sessionId, key, body, client, { ...options, localId })
1995
- });
1996
- }
1997
- function checkPeerSendBudget(sentTo, to, now, limit = PEER_MESSAGE_RATE_LIMIT, windowMs = PEER_MESSAGE_RATE_WINDOW_MS) {
1998
- const recent = (sentTo.get(to) ?? []).filter((t) => now - t <= windowMs);
1999
- sentTo.set(to, recent);
2000
- return { ok: recent.length < limit, count: recent.length };
2001
- }
2002
- async function sendPeerMessage(context, args) {
2003
- const self = context.self();
2004
- if (!persistence.isValidSessionId(args.to)) throw new Error("Invalid session id");
2005
- if (args.to === self.sessionId) throw new Error("That is this session; message a peer instead");
2006
- const body = typeof args.body === "string" ? args.body.trim() : "";
2007
- if (!body) throw new Error("body must be non-empty");
2008
- const now = (context.now ?? Date.now)();
2009
- const persisted = (context.readPersisted ?? persistence.readPersistedSessions)()[args.to];
2010
- if (!persisted || args.machineId) {
2011
- const remote = context.remoteMessage === void 0 ? sendRemotePeerMessage : context.remoteMessage;
2012
- if (remote === null) {
2013
- throw new Error(`Session ${args.to} is not on this machine (no local key)`);
2014
- }
2015
- if (context.sentTo) {
2016
- const budget = checkPeerSendBudget(context.sentTo, args.to, now);
2017
- if (!budget.ok) throw new Error(replyLoopRefusal(budget.count, args.to));
2018
- }
2019
- const result2 = await remote({ to: args.to, body, replyTo: args.replyTo, from: self, machineId: args.machineId });
2020
- context.sentTo?.get(args.to)?.push(now);
2021
- return result2;
2022
- }
2023
- const live = (await (context.listPeers ?? persistence.listDaemonPeers)()).find((session) => session.sessionId === args.to);
2024
- if (!live) throw new Error(`Session ${args.to} is not running on this machine; nothing would read the message`);
2025
- if (live.kind === "mirror") throw new Error(`Session ${args.to} is a terminal mirror (a person typing claude in a terminal); it cannot receive messages`);
2026
- if (context.sentTo) {
2027
- const budget = checkPeerSendBudget(context.sentTo, args.to, now);
2028
- if (!budget.ok) throw new Error(replyLoopRefusal(budget.count, args.to));
2029
- }
2030
- const messageId = node_crypto.randomUUID().slice(0, 8);
2031
- const text = formatSessionPeerMessage({ id: messageId, from: self, body, replyTo: args.replyTo });
2032
- const result = await (context.deliver ?? defaultDeliver)(args.to, persisted, text, `session-message-${messageId}`);
2033
- context.sentTo?.get(args.to)?.push(now);
2034
- return {
2035
- delivered: result.delivered,
2036
- stored: result.stored,
2037
- status: result.status,
2038
- messageId,
2039
- to: args.to,
2040
- url: sessionWebUrl(args.to),
2041
- ...result.error ? { error: result.error } : {}
2042
- };
2043
- }
2044
- function replyLoopRefusal(count, to) {
2045
- return `Refused: this session already sent ${count} messages to ${to} in the last ${Math.round(PEER_MESSAGE_RATE_WINDOW_MS / 6e4)} minutes. That is a reply loop, not coordination \u2014 STOP replying to that session now; do not send acknowledgements or thanks. Continue your own work.`;
2046
- }
2047
- async function executeSessionPeerTool(name, args, context) {
2048
- switch (name) {
2049
- case "session_message":
2050
- return sendPeerMessage(context, {
2051
- to: String(args?.to ?? ""),
2052
- body: String(args?.body ?? ""),
2053
- ...args?.replyTo ? { replyTo: String(args.replyTo) } : {},
2054
- ...args?.machineId ? { machineId: String(args.machineId) } : {}
2055
- });
2056
- case "session_peers": {
2057
- const scope = args?.scope;
2058
- if (scope !== void 0 && !PEER_SCOPES$1.includes(scope)) throw new Error(`scope must be one of ${PEER_SCOPES$1.join(", ")}`);
2059
- if (args?.machineId) {
2060
- return (context.remotePeers ?? listRemotePeers)(String(args.machineId), {
2061
- ...scope !== void 0 ? { scope } : {},
2062
- ...typeof args.cwd === "string" && args.cwd ? { cwd: args.cwd } : {}
2063
- });
2064
- }
2065
- return listPeerSessions(context, scope ?? "repo");
2066
- }
2067
- }
2068
- }
2069
- function cliPeerSelf(env, cwd, persisted) {
2070
- const sessionId = env.VH_PEER_SESSION_ID;
2071
- if (sessionId && persistence.isValidSessionId(sessionId)) {
2072
- const meta = persisted[sessionId]?.metadata;
2073
- return { sessionId, title: meta?.summary?.text, flavor: meta?.flavor, cwd: meta?.path ?? cwd };
2074
- }
2075
- let user = "user";
2076
- try {
2077
- user = os.userInfo().username;
2078
- } catch {
2079
- }
2080
- return { sessionId: CLI_PEER_SENDER_ID, title: `cli ${user}@${os.hostname()}`, flavor: "cli", cwd };
2081
- }
2082
- function createSessionPeerToolExecutor(client) {
2083
- const context = {
2084
- self: () => {
2085
- const meta = client.getMetadata();
2086
- return { sessionId: client.sessionId, title: meta?.summary?.text, flavor: meta?.flavor, cwd: meta?.path ?? process.cwd() };
2087
- },
2088
- // One budget per session process: the reply-loop guard.
2089
- sentTo: /* @__PURE__ */ new Map()
2090
- };
2091
- return (name, args) => executeSessionPeerTool(name, args, context);
2092
- }
2093
- function registerSessionPeerTools(server, execute) {
2094
- for (const name of SESSION_PEER_TOOL_NAMES) {
2095
- const spec = SESSION_PEER_TOOL_SCHEMAS[name];
2096
- server.registerTool(name, {
2097
- description: spec.description,
2098
- title: spec.title,
2099
- inputSchema: spec.inputSchema,
2100
- annotations: { readOnlyHint: spec.readOnly, idempotentHint: spec.readOnly, openWorldHint: false, destructiveHint: false }
2101
- }, async (args) => {
2102
- try {
2103
- return { content: [{ type: "text", text: JSON.stringify(await execute(name, args ?? {})) }], isError: false };
2104
- } catch (error) {
2105
- return { content: [{ type: "text", text: error instanceof Error ? error.message : "Session peer operation failed" }], isError: true };
2106
- }
2107
- });
2108
- }
2109
- }
2110
-
2111
- function trimIdent(text) {
2112
- const lines = text.split("\n");
2113
- while (lines.length > 0 && lines[0].trim() === "") {
2114
- lines.shift();
2115
- }
2116
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
2117
- lines.pop();
2118
- }
2119
- const minSpaces = lines.reduce((min, line) => {
2120
- if (line.trim() === "") {
2121
- return min;
2122
- }
2123
- const leadingSpaces = line.match(/^\s*/)[0].length;
2124
- return Math.min(min, leadingSpaces);
2125
- }, Infinity);
2126
- const trimmedLines = lines.map((line) => line.slice(minSpaces));
2127
- return trimmedLines.join("\n");
2128
- }
2129
-
2130
- const AGENT_GUIDANCE = (() => trimIdent(`
2131
- You are running inside Happy: the user is reading this session in a web client (often a phone), NOT in the terminal where you run. Two consequences:
2132
-
2133
- - Terminal output is not a good delivery channel. When the user asks for a piece of text ("copy that", "give me X", "\u590D\u5236\u7ED9\u6211", "\u53D1\u6211"), hand it over with mcp__happy__copy_to_clipboard instead of printing it for them to select by hand.
2134
- - When you produce or want to show a FILE (a document you wrote, a report, an image, a PDF, a diff), call mcp__happy__open_preview with its path instead of cat-ing it. Their client opens the rendered file.
2135
-
2136
- Judgement, not reflex: use these when the user's goal is to RECEIVE something. If they are asking a question you can just answer, answer it. Do not preview a file you only read for your own reasoning, and do not preview the same file repeatedly within a turn.
2137
-
2138
- If you are working on a task the user is tracking, call mcp__happy__report_progress at meaningful milestones (started / blocked / needs review / done) so their board reflects reality. Do not report every tool call.
2139
- `))();
2140
- const PREVIEW_TOOL_NAME = "open_preview";
2141
- const PREVIEW_TOOL_TITLE = "Open File Preview for the User";
2142
- const PREVIEW_TOOL_DESCRIPTION = 'Open a file in the preview panel of every web client the user currently has open. Use this whenever you produce or want to show the user a file: a document or report you just wrote, a generated image or diagram, a PDF, a data file, or a source file you want them to look at. Trigger phrases include: "write me a doc", "show me that file", "let me see it", "\u5199\u4E2A\u6587\u6863", "\u7ED9\u6211\u770B\u770B\u8FD9\u4E2A\u6587\u4EF6", "\u6253\u5F00\u770B\u4E0B". Pass an absolute path (or one starting with ~). The file is read on this machine and rendered in their browser \u2014 markdown is rendered, images and PDFs are displayed, other text is syntax-highlighted. Prefer this over printing file contents to the terminal, which the user is probably not looking at. NOTE: this only requests that their client open the file; it cannot confirm the user actually looked at it, so do not assume they have seen it.';
2143
- const REPORT_PROGRESS_TOOL_NAME = "report_progress";
2144
- const REPORT_PROGRESS_TOOL_TITLE = "Report Progress to the Task Board";
2145
- const REPORT_PROGRESS_TOOL_DESCRIPTION = `Report what you are doing to the user's task board, so they can see the state of this session at a glance without reading the transcript. Call it at meaningful milestones only: when you start substantive work, when you get blocked and need the user, when something is ready for their review, and when you are done. Do NOT call it after every tool call or every file edit \u2014 it is a status line, not a log. Set attention to "blocked" when you cannot proceed without the user, "review" when you want them to look at something, and "none" for ordinary progress. Keep progress to one short line describing the current state (the user reads it on a card).`;
2146
-
2147
- exports.AGENT_GUIDANCE = AGENT_GUIDANCE;
2148
- exports.AUTOMATIONS_UNAVAILABLE_MESSAGE = AUTOMATIONS_UNAVAILABLE_MESSAGE;
2149
- exports.AUTOMATION_TOOL_NAMES = AUTOMATION_TOOL_NAMES;
2150
- exports.AutomationsRequestError = AutomationsRequestError;
2151
- exports.CLI_PEER_SENDER_ID = CLI_PEER_SENDER_ID;
2152
- exports.DEFAULT_RECENT_LIMIT = DEFAULT_RECENT_LIMIT;
2153
- exports.MAX_READ_LIMIT = MAX_READ_LIMIT;
2154
- exports.PEER_SCOPES = PEER_SCOPES$1;
2155
- exports.PREVIEW_TOOL_DESCRIPTION = PREVIEW_TOOL_DESCRIPTION;
2156
- exports.PREVIEW_TOOL_NAME = PREVIEW_TOOL_NAME;
2157
- exports.PREVIEW_TOOL_TITLE = PREVIEW_TOOL_TITLE;
2158
- exports.REMOTE_ANSWER_MAX_BYTES = REMOTE_ANSWER_MAX_BYTES;
2159
- exports.REMOTE_OP_DEADLINE_MS = REMOTE_OP_DEADLINE_MS;
2160
- exports.REMOTE_RESUME_WAIT_MS = REMOTE_RESUME_WAIT_MS;
2161
- exports.REMOTE_SEND_COMMIT_DEADLINE_MS = REMOTE_SEND_COMMIT_DEADLINE_MS;
2162
- exports.REMOTE_TITLE_MAX_BYTES = REMOTE_TITLE_MAX_BYTES;
2163
- exports.REMOTE_WRITE_CALLS_PER_MINUTE = REMOTE_WRITE_CALLS_PER_MINUTE;
2164
- exports.REPORT_PROGRESS_TOOL_DESCRIPTION = REPORT_PROGRESS_TOOL_DESCRIPTION;
2165
- exports.REPORT_PROGRESS_TOOL_NAME = REPORT_PROGRESS_TOOL_NAME;
2166
- exports.REPORT_PROGRESS_TOOL_TITLE = REPORT_PROGRESS_TOOL_TITLE;
2167
- exports.RemoteCallBudget = RemoteCallBudget;
2168
- exports.RemoteSessionOpsError = RemoteSessionOpsError;
2169
- exports.SESSION_PEER_SENT_FROM = SESSION_PEER_SENT_FROM;
2170
- exports.SESSION_PEER_TOOL_NAMES = SESSION_PEER_TOOL_NAMES;
2171
- exports.TEAM_TOOL_NAMES = TEAM_TOOL_NAMES;
2172
- exports.TURN_WINDOW = TURN_WINDOW;
2173
- exports.TeamConnectionPreflightError = TeamConnectionPreflightError;
2174
- exports.TurnWaitTimeoutError = TurnWaitTimeoutError;
2175
- exports.analyzeLatestTurn = analyzeLatestTurn;
2176
- exports.archiveSession = archiveSession;
2177
- exports.authenticatedAutomationsClient = authenticatedAutomationsClient;
2178
- exports.automationRunIdFromEnv = automationRunIdFromEnv;
2179
- exports.capRemoteText = capRemoteText;
2180
- exports.capRemoteTranscript = capRemoteTranscript;
2181
- exports.cliPeerSelf = cliPeerSelf;
2182
- exports.createAutomationsClient = createAutomationsClient;
2183
- exports.createSessionPeerToolExecutor = createSessionPeerToolExecutor;
2184
- exports.createTeamsClient = createTeamsClient;
2185
- exports.deliverToSession = deliverToSession;
2186
- exports.fillForeignAccountRows = fillForeignAccountRows;
2187
- exports.formatEditConflictNotice = formatEditConflictNotice;
2188
- exports.formatRemoteAuditLine = formatRemoteAuditLine;
2189
- exports.guardRemoteSessionOpsRequest = guardRemoteSessionOpsRequest;
2190
- exports.isAutomationsUnavailable = isAutomationsUnavailable;
2191
- exports.listAccountSessions = listAccountSessions;
2192
- exports.listPeerSessions = listPeerSessions;
2193
- exports.listRemotePeers = listRemotePeers;
2194
- exports.listRemoteSessions = listRemoteSessions;
2195
- exports.listSessions = listSessions;
2196
- exports.readRemoteTranscript = readRemoteTranscript;
2197
- exports.readSessionLog = readSessionLog;
2198
- exports.readSessionMetadata = readSessionMetadata;
2199
- exports.readSessionTranscript = readSessionTranscript;
2200
- exports.readTeamScope = readTeamScope;
2201
- exports.registerAutomationTools = registerAutomationTools;
2202
- exports.registerSessionPeerTools = registerSessionPeerTools;
2203
- exports.registerTeamsTools = registerTeamsTools;
2204
- exports.remoteError = remoteError;
2205
- exports.resolvePermissionRequest = resolvePermissionRequest;
2206
- exports.sendPeerMessage = sendPeerMessage;
2207
- exports.sendRemoteMessage = sendRemoteMessage;
2208
- exports.sendUserMessage = sendUserMessage;
2209
- exports.sessionWebUrl = sessionWebUrl;
2210
- exports.stopSession = stopSession;
2211
- exports.teamRequest = teamRequest;
2212
- exports.teamScopePath = teamScopePath;
2213
- exports.trimIdent = trimIdent;
2214
- exports.userPromptText = userPromptText;
2215
- exports.waitForRemoteTurnEnd = waitForRemoteTurnEnd;
2216
- exports.waitForSessionKey = waitForSessionKey;
2217
- exports.waitForTurnEnd = waitForTurnEnd;