very-happy-cli 0.2.159 → 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.
- package/dist/{AcpBackend-DUYFlyfN.cjs → AcpBackend-Bm2rdke8.cjs} +78 -76
- package/dist/{AcpBackend-VsfkZiVF.mjs → AcpBackend-mzv8Dhba.mjs} +4 -2
- package/dist/{AcpSessionManager-Bdv8LWXh.cjs → AcpSessionManager-BAY1Pggx.cjs} +9 -9
- package/dist/{AcpSessionManager-rYtVb0kN.mjs → AcpSessionManager-CoMWFVw3.mjs} +1 -1
- package/dist/BasePermissionHandler-CFHAxumV.cjs +157 -0
- package/dist/BasePermissionHandler-m3WVholL.mjs +155 -0
- package/dist/BaseReasoningProcessor-BFa9_rZA.cjs +200 -0
- package/dist/BaseReasoningProcessor-CYrKBXGm.mjs +198 -0
- package/dist/RemoteModeDisplay-D-HewyXg.mjs +150 -0
- package/dist/RemoteModeDisplay-NC1gPHtj.cjs +153 -0
- package/dist/agentAttachments-Dbw8iMCZ.cjs +20 -0
- package/dist/agentAttachments-M_B5qEF2.mjs +18 -0
- package/dist/agentGuidance-B5NATuFa.cjs +28 -0
- package/dist/agentGuidance-CQuNlNx8.mjs +20 -0
- package/dist/{agentHome-CHh4lmjM.cjs → agentHome-CboCeKWq.cjs} +14 -28
- package/dist/{agentHome-CvsJyeQB.mjs → agentHome-Dj1C43aJ.mjs} +8 -22
- package/dist/api-CLMUr-SX.mjs +3339 -0
- package/dist/api-CgundNNX.cjs +3346 -0
- package/dist/{types-Dh-s4Lqs.mjs → apiMachine-B9dLW_p5.mjs} +9116 -16205
- package/dist/{types-BbvK1Tr_.cjs → apiMachine-Bn8482VM.cjs} +9104 -16247
- package/dist/assistantTools-BTqPnlFl.cjs +380 -0
- package/dist/assistantTools-D6tuc6o4.mjs +376 -0
- package/dist/attachmentContent-DpiTJZb4.cjs +88 -0
- package/dist/attachmentContent-_kxRznNf.mjs +82 -0
- package/dist/attachmentLimits-CzVrqgLR.cjs +354 -0
- package/dist/attachmentLimits-yba1uHFO.mjs +348 -0
- package/dist/auth-BDOVh5h5.cjs +220 -0
- package/dist/auth-Cwutmdsa.mjs +225 -0
- package/dist/auth-PYHZQNPp.mjs +218 -0
- package/dist/auth-ba4WMYrN.cjs +227 -0
- package/dist/authRelay-BdYkC1Po.cjs +156 -0
- package/dist/authRelay-DSAvg1uv.mjs +143 -0
- package/dist/{auto-DKmEls8Z.cjs → auto-BOX_fgmR.cjs} +70 -79
- package/dist/{auto-nb8GhRRy.mjs → auto-BXu14yP_.mjs} +27 -36
- package/dist/backgroundTaskActivity-8PdPxrvy.mjs +149 -0
- package/dist/backgroundTaskActivity-CBvUdj39.cjs +157 -0
- package/dist/bootstrap-B5G1TijC.cjs +116 -0
- package/dist/bootstrap-hxXPCFMx.mjs +112 -0
- package/dist/browser-Y6Bl8L_O.cjs +22 -0
- package/dist/browser-jh2P0UVZ.mjs +20 -0
- package/dist/claudeCliPath-BqkaH2oR.mjs +8 -0
- package/dist/claudeCliPath-U4HHmV4r.cjs +10 -0
- package/dist/claudeLocal-BaY4fZKi.mjs +339 -0
- package/dist/claudeLocal-CRGG4Pea.cjs +343 -0
- package/dist/claudeLocalLauncher-D4T7MTqd.mjs +152 -0
- package/dist/claudeLocalLauncher-DyEbVzUN.cjs +154 -0
- package/dist/cliUpdate-DsKXPFYp.cjs +120 -0
- package/dist/cliUpdate-Y34VGe9Y.mjs +112 -0
- package/dist/client-3Ac-O4pw.cjs +104 -0
- package/dist/client-BPNbmtvO.cjs +119 -0
- package/dist/client-CeGKqqcJ.mjs +100 -0
- package/dist/client-ClR0K8zX.mjs +112 -0
- package/dist/codex/happyMcpStdioBridge.cjs +33 -14
- package/dist/codex/happyMcpStdioBridge.mjs +26 -7
- package/dist/codexAppServerClient-I3teiV-N.mjs +1088 -0
- package/dist/codexAppServerClient-q2FjnRcK.cjs +1090 -0
- package/dist/codexCommand-4zcccIoq.cjs +2088 -0
- package/dist/codexCommand-Cc9gcbdX.mjs +2086 -0
- package/dist/codexThreadFork-BH9IZPqD.cjs +350 -0
- package/dist/codexThreadFork-slzAseLx.mjs +338 -0
- package/dist/{config-ClLtM6F3.mjs → config-CTx_jb-C.mjs} +7 -40
- package/dist/{config-DLx-vW3I.cjs → config-Ci9Kpgwa.cjs} +26 -59
- package/dist/configuration-BSYpcPwE.cjs +181 -0
- package/dist/configuration-RfeNjFmd.mjs +170 -0
- package/dist/connect-BpOdzNbE.mjs +682 -0
- package/dist/connect-C2CiOUFs.cjs +684 -0
- package/dist/constants-BC4St0xd.mjs +11 -0
- package/dist/constants-VJ0N6WuG.cjs +17 -0
- package/dist/contentLogMetadata-BlWI0egu.mjs +51 -0
- package/dist/contentLogMetadata-ZSFk_NjQ.cjs +54 -0
- package/dist/context-DsDr6E2t.cjs +69 -0
- package/dist/context-H0Bcb9S_.mjs +65 -0
- package/dist/controlClient-BGbDcgWS.mjs +260 -0
- package/dist/controlClient-Bcwv3D21.cjs +279 -0
- package/dist/createSessionMetadata-BJxEnM-o.cjs +58 -0
- package/dist/createSessionMetadata-CVVkGjxV.mjs +54 -0
- package/dist/defaultPermissionMode-BnxJPVHT.mjs +4 -0
- package/dist/defaultPermissionMode-CxmTXUQR.cjs +7 -0
- package/dist/detectCLI-CSGMlV51.mjs +58 -0
- package/dist/detectCLI-DR9ZaN6B.cjs +61 -0
- package/dist/deterministicJson-C5CYTVpm.cjs +507 -0
- package/dist/deterministicJson-CzkFJWw1.mjs +502 -0
- package/dist/doctor-BPkYWSuq.mjs +75 -0
- package/dist/doctor-D2YJ-Yn2.cjs +395 -0
- package/dist/doctor-DOvByjbk.mjs +391 -0
- package/dist/doctor-u46qYYKX.cjs +79 -0
- package/dist/doctorReadiness-BCZGcN0a.cjs +151 -0
- package/dist/doctorReadiness-C208SMN9.mjs +142 -0
- package/dist/editPaths-BVfS_FFL.mjs +68 -0
- package/dist/editPaths-Bqp4OFPw.cjs +73 -0
- package/dist/ensureDaemonRunning-BCwNrMyv.cjs +64 -0
- package/dist/ensureDaemonRunning-Dqa2pOjY.mjs +61 -0
- package/dist/handleResumeCommand-BvXYf5El.mjs +240 -0
- package/dist/handleResumeCommand-C2cKchpX.cjs +245 -0
- package/dist/ids-B6ZC0HGt.cjs +13 -0
- package/dist/ids-DU6sLx4C.mjs +10 -0
- package/dist/index-B6UGRQ7h.mjs +102 -0
- package/dist/{index-BEOMECtE.cjs → index-B6pAdvJx.cjs} +121 -91
- package/dist/index-Bu5Zi_a9.cjs +182 -0
- package/dist/index-D3kI0hDR.cjs +107 -0
- package/dist/index-DA7yINuk.cjs +994 -0
- package/dist/index-DDeHXrIX.cjs +71 -0
- package/dist/index-DKLU_fD0.mjs +158 -0
- package/dist/{index-2mCaeKyf.mjs → index-DMZqWwsP.mjs} +68 -38
- package/dist/index-DVkMGPyS.mjs +68 -0
- package/dist/index-qdS1friN.mjs +949 -0
- package/dist/index.cjs +5 -46
- package/dist/index.mjs +5 -46
- package/dist/{install-BPhJppvd.mjs → install-BO_W7VhL.mjs} +37 -37
- package/dist/install-BgoC2e_2.cjs +88 -0
- package/dist/install-CFY3tYtJ.mjs +86 -0
- package/dist/{install-BOHrUaFM.cjs → install-Cs5qAPrB.cjs} +38 -38
- package/dist/{installPiTools-DuZ6jZvX.cjs → installPiTools-BKnXjrVu.cjs} +2 -3
- package/dist/{installPiTools-Cj7yU9Fc.mjs → installPiTools-B_CdM5_2.mjs} +1 -2
- package/dist/{installTerminalHooks-Cou3WUf3.cjs → installTerminalHooks-BQZbvxsZ.cjs} +5 -40
- package/dist/{installTerminalHooks-WeznjkuC.mjs → installTerminalHooks-By7gBMEF.mjs} +3 -38
- package/dist/lib.cjs +28 -19
- package/dist/lib.d.cts +12 -2
- package/dist/lib.d.mts +12 -2
- package/dist/lib.mjs +23 -14
- package/dist/lidState-BeDfxxEX.cjs +305 -0
- package/dist/lidState-CeEt442o.mjs +297 -0
- package/dist/limits-C-a7gr04.cjs +43 -0
- package/dist/limits-DIo7rEBR.mjs +36 -0
- package/dist/localHappyAgentAuth-C-HQyiv6.mjs +82 -0
- package/dist/localHappyAgentAuth-D6rFhaoc.cjs +86 -0
- package/dist/logger-6HYUEXNk.mjs +271 -0
- package/dist/logger-CS0mw3fa.cjs +275 -0
- package/dist/loginShellEnv-BkuYtExZ.cjs +182 -0
- package/dist/loginShellEnv-Dw8M7tkI.mjs +167 -0
- package/dist/machineMetadata-DyDbODYd.cjs +41 -0
- package/dist/machineMetadata-oOzBfDfs.mjs +38 -0
- package/dist/main-BkxJP6JQ.cjs +941 -0
- package/dist/main-BwNIAI1A.mjs +939 -0
- package/dist/manager-BQ4flvQZ.mjs +101 -0
- package/dist/manager-Cly8xVIR.cjs +105 -0
- package/dist/{mcp-Dy3f_K0h.mjs → mcp-B1A4I3TK.mjs} +27 -33
- package/dist/{mcp-CPYbtsYG.cjs → mcp-Hszd7qpS.cjs} +42 -48
- package/dist/messageBuffer-BWlGgR3A.mjs +71 -0
- package/dist/messageBuffer-fJMH6uY8.cjs +73 -0
- package/dist/mirrorProtocol-BNLtoWjO.mjs +170 -0
- package/dist/mirrorProtocol-BUtmg1ws.cjs +186 -0
- package/dist/oneShotPrompts-CL0dip9C.cjs +16 -0
- package/dist/oneShotPrompts-DT4p6RNd.mjs +12 -0
- package/dist/package-BhaKlnxz.mjs +171 -0
- package/dist/package-BwMrPwqH.cjs +173 -0
- package/dist/path-B7zW-WwE.cjs +18 -0
- package/dist/path-D2wdaNSk.mjs +15 -0
- package/dist/peerTools-BTqw97Gb.mjs +272 -0
- package/dist/peerTools-BvKtHXj8.cjs +283 -0
- package/dist/permissionMode-Cr5JCbB7.mjs +80 -0
- package/dist/permissionMode-CrV-XuMs.cjs +87 -0
- package/dist/persistence-HGW_ZVs9.mjs +372 -0
- package/dist/persistence-yKTH3-5X.cjs +414 -0
- package/dist/piExtension.mjs +2 -1
- package/dist/piRunnerArgs-BNloJCCg.cjs +66 -0
- package/dist/piRunnerArgs-BuX1UFWy.mjs +59 -0
- package/dist/{piRuntime-kLZNpAAl.mjs → piRuntime-CSGkwCKQ.mjs} +2 -2
- package/dist/{piRuntime-NtF0YqjH.cjs → piRuntime-C_H7MJCM.cjs} +4 -4
- package/dist/{piTerminal-B0fEOV_2.mjs → piTerminal-C8a1OrNQ.mjs} +32 -35
- package/dist/{piTerminal-9J0GAKzb.cjs → piTerminal-DNCZ6yTM.cjs} +32 -35
- package/dist/previewPath-B0rBdhy1.cjs +83 -0
- package/dist/previewPath-CXN4Vag0.mjs +81 -0
- package/dist/projectPath-BTBIMB57.mjs +10 -0
- package/dist/projectPath-ChLhdOyt.cjs +13 -0
- package/dist/registerCommonHandlers-Ccvsz7Ou.mjs +536 -0
- package/dist/registerCommonHandlers-DskirN-_.cjs +538 -0
- package/dist/registerKillSessionHandler-DZeLXWcB.cjs +361 -0
- package/dist/registerKillSessionHandler-GYvHdipX.mjs +358 -0
- package/dist/remoteSessionClient-B0LraSB9.cjs +634 -0
- package/dist/remoteSessionClient-CYnXRynE.mjs +612 -0
- package/dist/resources-BdFBQi8r.mjs +117 -0
- package/dist/resources-JDPugcDq.cjs +120 -0
- package/dist/run-CJzxoHVh.cjs +5762 -0
- package/dist/run-D3ifupU3.mjs +5739 -0
- package/dist/runClaude-Ba01qT-u.mjs +848 -0
- package/dist/runClaude-Da3RirzE.cjs +850 -0
- package/dist/runClaudeDeps-DOt79DjU.mjs +3624 -0
- package/dist/runClaudeDeps-DOzEBMls.cjs +3649 -0
- package/dist/{runGemini-DV6Ouadt.mjs → runGemini-BGeTz57m.mjs} +58 -31
- package/dist/{runGemini-F-mqx3_K.cjs → runGemini-DSr9LIxL.cjs} +202 -175
- package/dist/{runOpenClaw-Qqo9j63M.cjs → runOpenClaw-B1mTxN35.cjs} +64 -65
- package/dist/{runOpenClaw-D7MlPS5n.mjs → runOpenClaw-CogSedIc.mjs} +31 -32
- package/dist/sandbox-CG7IyDTU.cjs +187 -0
- package/dist/sandbox-DTe6d54R.mjs +180 -0
- package/dist/selfCheck-C9mVrQwf.mjs +84 -0
- package/dist/selfCheck-Dc-mNSLb.cjs +87 -0
- package/dist/{send-4qJTCAYW.cjs → send-B7J_eaG3.cjs} +16 -9
- package/dist/{send-CLhYXNgy.mjs → send-CBXJKc6O.mjs} +12 -5
- package/dist/server-CZTP_iOn.cjs +407 -0
- package/dist/server-DcVBv04l.mjs +404 -0
- package/dist/sessionLock-Bys1PNSo.cjs +205 -0
- package/dist/sessionLock-D-EG2CfG.mjs +197 -0
- package/dist/sessionModeFile-DMl3dw3r.mjs +32 -0
- package/dist/sessionModeFile-RSm0NQkH.cjs +36 -0
- package/dist/sessionOps-Dn8z85h_.cjs +783 -0
- package/dist/sessionOps-SuHW8Dfe.mjs +760 -0
- package/dist/sessionScanner-BOrGjMXL.cjs +209 -0
- package/dist/sessionScanner-CX3onqXh.mjs +206 -0
- package/dist/{sessions-CzIH2rGp.mjs → sessions-BEG_HTSi.mjs} +17 -8
- package/dist/{sessions-Bh1pyddi.cjs → sessions-D74aJbhL.cjs} +37 -28
- package/dist/setupOfflineReconnection-ByElY60m.cjs +74 -0
- package/dist/setupOfflineReconnection-DpvWTt69.mjs +72 -0
- package/dist/smallFastModel-BLNgMrwR.mjs +18 -0
- package/dist/smallFastModel-D_uxGMo3.cjs +21 -0
- package/dist/{spawn-CPIWlPOM.mjs → spawn-B7yro0b2.mjs} +26 -38
- package/dist/{spawn-CmFOSVdW.cjs → spawn-DgR0RKRs.cjs} +52 -64
- package/dist/spawnAgents-BGQiEF-w.cjs +52 -0
- package/dist/spawnAgents-DZzZ9jMI.mjs +45 -0
- package/dist/spawnHappyCLI-B2H5s4B_.mjs +64 -0
- package/dist/spawnHappyCLI-DBhwZT7O.cjs +66 -0
- package/dist/specialCommands-BSqhYg5X.mjs +52 -0
- package/dist/specialCommands-DSsTeRej.cjs +54 -0
- package/dist/startFileWatcher-B4VsLgjD.cjs +71 -0
- package/dist/startFileWatcher-DErVWZE4.mjs +69 -0
- package/dist/startupPlan-BLMbfQIH.cjs +12 -0
- package/dist/startupPlan-DFMyNgAs.mjs +10 -0
- package/dist/streamRelay-BiKw59Cc.cjs +233 -0
- package/dist/streamRelay-FPVbHpbM.mjs +231 -0
- package/dist/sync-C8gyQFMc.mjs +69 -0
- package/dist/sync-DccIKZ5_.cjs +71 -0
- package/dist/systemPrompt-BwTPWfMu.cjs +97 -0
- package/dist/systemPrompt-DX521JJz.mjs +94 -0
- package/dist/{teams-CEahj4cm.mjs → teams-C32F1hqd.mjs} +9 -40
- package/dist/{teams-CB1R9-Z9.cjs → teams-Dl8g8kTI.cjs} +14 -45
- package/dist/template-BaHAmH2K.cjs +129 -0
- package/dist/template-CiOPa2rd.mjs +121 -0
- package/dist/{terminalTitleSuggest-DKgX3osW.mjs → terminalTitleSuggest-CeKsX_V6.mjs} +1 -9
- package/dist/{terminalTitleSuggest-Dji8I0Jy.cjs → terminalTitleSuggest-DhvRFX3H.cjs} +0 -10
- package/dist/terminals-B_bfC9c2.cjs +250 -0
- package/dist/terminals-Dlg0x3nY.mjs +235 -0
- package/dist/time-D0XyHOid.mjs +38 -0
- package/dist/time-DCuTyook.cjs +42 -0
- package/dist/titleGenerator-CQ8CoGhg.mjs +166 -0
- package/dist/titleGenerator-ChzMJsyg.cjs +172 -0
- package/dist/tmuxSocket-B8Yx20sT.mjs +19 -0
- package/dist/tmuxSocket-D-Xt6Vn-.cjs +24 -0
- package/dist/{todo-DqyRI4wP.cjs → todo-BvUOTa8G.cjs} +15 -12
- package/dist/{todo-Cknp5D4U.mjs → todo-CZ6cFrrn.mjs} +11 -8
- package/dist/tools-BZtHF_qQ.mjs +177 -0
- package/dist/tools-Be3AoAwu.cjs +63 -0
- package/dist/tools-DQj0AeLk.cjs +180 -0
- package/dist/tools-DkluDcG4.mjs +60 -0
- package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
- package/dist/trimIdent-BAjmv_TG.mjs +20 -0
- package/dist/types-CRS-srZK.cjs +66 -0
- package/dist/types-CkqAACXx.mjs +64 -0
- package/dist/uninstall-BkLzaJ6I.mjs +47 -0
- package/dist/uninstall-x-TUtf5H.cjs +49 -0
- package/package.json +7 -7
- package/dist/agentGuidance-BRDN1NoZ.cjs +0 -2217
- package/dist/agentGuidance-KgvYuF3R.mjs +0 -2145
- package/dist/index-BF6WfgV5.mjs +0 -19624
- package/dist/index-C-JazrMB.mjs +0 -52
- package/dist/index-blDSadUR.cjs +0 -58
- package/dist/index-rH3DrQ5B.cjs +0 -19688
- package/dist/limits-BH5CV5vR.cjs +0 -2683
- package/dist/limits-CjRj8IX5.mjs +0 -2558
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
var configuration = require('./configuration-BSYpcPwE.cjs');
|
|
5
|
+
var authRelay = require('./authRelay-BdYkC1Po.cjs');
|
|
6
|
+
var controlClient = require('./controlClient-Bcwv3D21.cjs');
|
|
7
|
+
var persistence = require('./persistence-yKTH3-5X.cjs');
|
|
8
|
+
var node_crypto = require('node:crypto');
|
|
9
|
+
var time = require('./time-DCuTyook.cjs');
|
|
10
|
+
var socket_ioClient = require('socket.io-client');
|
|
11
|
+
var backgroundTaskActivity = require('./backgroundTaskActivity-CBvUdj39.cjs');
|
|
12
|
+
|
|
13
|
+
function sessionWebUrl(sessionId) {
|
|
14
|
+
return `${configuration.configuration.webappUrl.replace(/\/+$/, "")}/session/${sessionId}`;
|
|
15
|
+
}
|
|
16
|
+
async function waitForSessionKey(sessionId, timeoutMs) {
|
|
17
|
+
const deadline = Date.now() + timeoutMs;
|
|
18
|
+
while (true) {
|
|
19
|
+
const entry = persistence.readPersistedSessions()[sessionId];
|
|
20
|
+
if (entry) return entry;
|
|
21
|
+
if (Date.now() >= deadline) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Session ${sessionId} has no encryption key in ${configuration.configuration.sessionsFile}. Either it was not spawned by this machine's daemon, or the daemon is too old to persist session keys.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
await time.delay(200);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function sendUserMessage(sessionId, persisted, text, client, options = {}) {
|
|
30
|
+
const credentials = await persistence.readCredentialsForConfiguredRelay();
|
|
31
|
+
if (!credentials) {
|
|
32
|
+
throw new Error("Not authenticated. Run `very-happy auth login` first.");
|
|
33
|
+
}
|
|
34
|
+
const envelope = {
|
|
35
|
+
role: "user",
|
|
36
|
+
content: {
|
|
37
|
+
type: "text",
|
|
38
|
+
text
|
|
39
|
+
},
|
|
40
|
+
meta: {
|
|
41
|
+
sentFrom: options.sentFrom ?? "cli",
|
|
42
|
+
...options.model !== void 0 ? { model: options.model } : {},
|
|
43
|
+
...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {},
|
|
44
|
+
...options.effort !== void 0 ? { effort: options.effort } : {},
|
|
45
|
+
...options.delivery !== void 0 ? { delivery: options.delivery } : {}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const encrypted = authRelay.encodeBase64(authRelay.encrypt(
|
|
49
|
+
authRelay.decodeBase64(persisted.encryptionKey),
|
|
50
|
+
persisted.encryptionVariant,
|
|
51
|
+
envelope
|
|
52
|
+
));
|
|
53
|
+
await axios.post(
|
|
54
|
+
`${configuration.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
55
|
+
{ messages: [{ content: encrypted, localId: options.localId ?? node_crypto.randomUUID() }] },
|
|
56
|
+
{
|
|
57
|
+
headers: {
|
|
58
|
+
"Authorization": `Bearer ${credentials.token}`,
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"X-Happy-Client": `${client}/${configuration.configuration.currentCliVersion}`
|
|
61
|
+
},
|
|
62
|
+
timeout: 3e4
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const MAX_LINE_CHARS = 500;
|
|
68
|
+
const MAX_ARGS_CHARS = 160;
|
|
69
|
+
const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
|
|
70
|
+
const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
|
|
71
|
+
function truncateText(text, max = MAX_LINE_CHARS) {
|
|
72
|
+
const chars = Array.from(text);
|
|
73
|
+
if (chars.length <= max) return text;
|
|
74
|
+
return chars.slice(0, max).join("") + ` \u2026[+${chars.length - max} chars]`;
|
|
75
|
+
}
|
|
76
|
+
function squash(text) {
|
|
77
|
+
return text.replace(/\s+/g, " ").trim();
|
|
78
|
+
}
|
|
79
|
+
function safeJson(value, max) {
|
|
80
|
+
try {
|
|
81
|
+
return truncateText(JSON.stringify(value) ?? "", max);
|
|
82
|
+
} catch {
|
|
83
|
+
return "[unserializable]";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function formatMessageBody(body, limits = DEFAULT_LIMITS) {
|
|
87
|
+
if (!body || typeof body !== "object") return null;
|
|
88
|
+
const b = body;
|
|
89
|
+
if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
|
|
90
|
+
const text = squash(b.content.text);
|
|
91
|
+
return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
|
|
92
|
+
}
|
|
93
|
+
if (b.role === "session" && b.content && typeof b.content === "object") {
|
|
94
|
+
const env = b.content;
|
|
95
|
+
const ev = env.ev;
|
|
96
|
+
if (!ev || typeof ev.t !== "string") return null;
|
|
97
|
+
switch (ev.t) {
|
|
98
|
+
case "text": {
|
|
99
|
+
if (ev.thinking) return null;
|
|
100
|
+
const text = squash(String(ev.text ?? ""));
|
|
101
|
+
if (text.length === 0) return null;
|
|
102
|
+
const role = env.role === "user" ? "user" : "assistant";
|
|
103
|
+
return `${role}: ${truncateText(text, limits.line)}`;
|
|
104
|
+
}
|
|
105
|
+
case "tool-call-start": {
|
|
106
|
+
const name = String(ev.name ?? "tool");
|
|
107
|
+
const title = squash(String(ev.title ?? ""));
|
|
108
|
+
const args = ev.args ? safeJson(ev.args, limits.args) : "";
|
|
109
|
+
const detail = title || args;
|
|
110
|
+
return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
|
|
111
|
+
}
|
|
112
|
+
case "service": {
|
|
113
|
+
const text = squash(String(ev.text ?? ""));
|
|
114
|
+
return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
|
|
115
|
+
}
|
|
116
|
+
case "turn-end": {
|
|
117
|
+
if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (b.role === "agent" && b.content?.type === "acp" && b.content.data && typeof b.content.data === "object") {
|
|
125
|
+
const d = b.content.data;
|
|
126
|
+
switch (d.type) {
|
|
127
|
+
case "message": {
|
|
128
|
+
const text = squash(String(d.message ?? ""));
|
|
129
|
+
return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
|
|
130
|
+
}
|
|
131
|
+
case "tool-call":
|
|
132
|
+
return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
|
|
133
|
+
case "permission-request":
|
|
134
|
+
return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
|
|
135
|
+
default:
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (b.role === "agent" && b.content?.type === "output") {
|
|
140
|
+
const msg = b.content.data?.message;
|
|
141
|
+
if (msg && Array.isArray(msg.content)) {
|
|
142
|
+
const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
|
|
143
|
+
if (texts.length > 0) {
|
|
144
|
+
const role = msg.role === "user" ? "user" : "assistant";
|
|
145
|
+
return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function formatTranscript(bodies, options = {}) {
|
|
153
|
+
const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
|
|
154
|
+
const lines = [];
|
|
155
|
+
for (const body of bodies) {
|
|
156
|
+
const line = formatMessageBody(body, limits);
|
|
157
|
+
if (line !== null) lines.push(line);
|
|
158
|
+
}
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function asRecord(value) {
|
|
163
|
+
return value && typeof value === "object" ? value : null;
|
|
164
|
+
}
|
|
165
|
+
function sessionEnvelope(body) {
|
|
166
|
+
const b = asRecord(body);
|
|
167
|
+
if (!b || b.role !== "session") return null;
|
|
168
|
+
const env = asRecord(b.content);
|
|
169
|
+
if (!env) return null;
|
|
170
|
+
const ev = asRecord(env.ev);
|
|
171
|
+
return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
|
|
172
|
+
}
|
|
173
|
+
function isUserPrompt(body) {
|
|
174
|
+
const b = asRecord(body);
|
|
175
|
+
return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
|
|
176
|
+
}
|
|
177
|
+
function agentPiece(body) {
|
|
178
|
+
const env = sessionEnvelope(body);
|
|
179
|
+
if (env && env.role !== "user") {
|
|
180
|
+
if (env.ev.t === "text" && !env.ev.thinking) {
|
|
181
|
+
const text = String(env.ev.text ?? "");
|
|
182
|
+
return text.trim().length > 0 ? { kind: "text", text } : null;
|
|
183
|
+
}
|
|
184
|
+
if (env.ev.t === "tool-call-start") return { kind: "tool" };
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const b = asRecord(body);
|
|
188
|
+
if (b?.role === "agent" && b.content?.type === "acp") {
|
|
189
|
+
const d = asRecord(b.content.data);
|
|
190
|
+
if (d?.type === "message") {
|
|
191
|
+
const text = String(d.message ?? "");
|
|
192
|
+
return text.trim().length > 0 ? { kind: "text", text } : null;
|
|
193
|
+
}
|
|
194
|
+
if (d?.type === "tool-call") return { kind: "tool" };
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function userPromptText(body) {
|
|
199
|
+
return isUserPrompt(body) ? body.content.text : null;
|
|
200
|
+
}
|
|
201
|
+
function analyzeLatestTurn(entries, options = {}) {
|
|
202
|
+
const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
|
|
203
|
+
let userIndex = -1;
|
|
204
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
205
|
+
if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
|
|
206
|
+
userIndex = i;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let ended = false;
|
|
211
|
+
let status = null;
|
|
212
|
+
let error = null;
|
|
213
|
+
let texts = [];
|
|
214
|
+
let lastMarker = null;
|
|
215
|
+
let startedAfterPrompt = false;
|
|
216
|
+
const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
|
|
217
|
+
for (let i = userIndex + 1; i < entries.length; i++) {
|
|
218
|
+
const body = entries[i].body;
|
|
219
|
+
if (options.anchor && isUserPrompt(body)) {
|
|
220
|
+
if (ended) break;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const env = sessionEnvelope(body);
|
|
224
|
+
if (env?.ev?.t === "turn-start") {
|
|
225
|
+
lastMarker = "start";
|
|
226
|
+
startedAfterPrompt = true;
|
|
227
|
+
ended = false;
|
|
228
|
+
status = null;
|
|
229
|
+
error = null;
|
|
230
|
+
texts = [];
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (env?.ev?.t === "turn-end") {
|
|
234
|
+
lastMarker = "end";
|
|
235
|
+
if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
|
|
236
|
+
ended = true;
|
|
237
|
+
status = typeof env.ev.status === "string" ? env.ev.status : null;
|
|
238
|
+
error = typeof env.ev.error === "string" ? env.ev.error : null;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const piece = agentPiece(body);
|
|
242
|
+
if (!piece) continue;
|
|
243
|
+
if (piece.kind === "tool") texts = [];
|
|
244
|
+
else texts.push(piece.text);
|
|
245
|
+
}
|
|
246
|
+
if (userIndex < 0) ended = lastMarker === "end";
|
|
247
|
+
return {
|
|
248
|
+
userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
|
|
249
|
+
ended,
|
|
250
|
+
status: ended ? status : null,
|
|
251
|
+
error: ended ? error : null,
|
|
252
|
+
answer: texts.join("\n\n").trim(),
|
|
253
|
+
lastSeq
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
|
|
258
|
+
async function openUserScopedSocket(token, options = {}) {
|
|
259
|
+
const socket = socket_ioClient.io(configuration.configuration.serverUrl, {
|
|
260
|
+
auth: {
|
|
261
|
+
token,
|
|
262
|
+
clientType: "user-scoped",
|
|
263
|
+
happyClient: `cli-session-ops/${configuration.configuration.currentCliVersion}`
|
|
264
|
+
},
|
|
265
|
+
path: "/v1/updates",
|
|
266
|
+
transports: ["websocket"],
|
|
267
|
+
reconnection: false,
|
|
268
|
+
autoConnect: false
|
|
269
|
+
});
|
|
270
|
+
await new Promise((resolve, reject) => {
|
|
271
|
+
const timer = setTimeout(() => {
|
|
272
|
+
socket.close();
|
|
273
|
+
reject(new Error(`Timed out connecting to ${configuration.configuration.serverUrl}`));
|
|
274
|
+
}, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
|
|
275
|
+
socket.once("connect", () => {
|
|
276
|
+
clearTimeout(timer);
|
|
277
|
+
resolve();
|
|
278
|
+
});
|
|
279
|
+
socket.once("connect_error", (error) => {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
socket.close();
|
|
282
|
+
reject(new Error(`Could not connect to ${configuration.configuration.serverUrl}: ${error.message}`));
|
|
283
|
+
});
|
|
284
|
+
socket.connect();
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
async rpcCall(payload, timeoutMs) {
|
|
288
|
+
const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
|
|
289
|
+
return ack;
|
|
290
|
+
},
|
|
291
|
+
close() {
|
|
292
|
+
socket.close();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const RPC_RATE_LIMIT_ERROR = "RPC rate limit reached";
|
|
298
|
+
const RPC_ACCOUNT_RATE_LIMIT_ERROR = "RPC account rate limit reached";
|
|
299
|
+
const RPC_RATE_RETRY_MIN_DELAY_MS = 2e3;
|
|
300
|
+
const RPC_RATE_RETRY_MAX_DELAY_MS = 6e4;
|
|
301
|
+
const RPC_RATE_MAX_RETRIES = 2;
|
|
302
|
+
function isRpcRateLimitAck(ack) {
|
|
303
|
+
if (!ack || typeof ack !== "object") return false;
|
|
304
|
+
const a = ack;
|
|
305
|
+
if (a.ok !== false) return false;
|
|
306
|
+
if (a.code === "rpc_rate_limited" || a.code === "rpc_account_rate_limited") return true;
|
|
307
|
+
return a.error === RPC_RATE_LIMIT_ERROR || a.error === RPC_ACCOUNT_RATE_LIMIT_ERROR;
|
|
308
|
+
}
|
|
309
|
+
function rpcRateRetryDelayMs(attempt, retryAfterMs, random = Math.random()) {
|
|
310
|
+
if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
|
|
311
|
+
return Math.min(RPC_RATE_RETRY_MAX_DELAY_MS, Math.round(retryAfterMs * (1 + random * 0.5)));
|
|
312
|
+
}
|
|
313
|
+
const exponent = Math.max(0, Math.min(attempt, 32) - 1);
|
|
314
|
+
const ceiling = Math.min(RPC_RATE_RETRY_MIN_DELAY_MS * 2 ** exponent, RPC_RATE_RETRY_MAX_DELAY_MS);
|
|
315
|
+
return Math.round(RPC_RATE_RETRY_MIN_DELAY_MS + random * (ceiling - RPC_RATE_RETRY_MIN_DELAY_MS));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const PERMISSION_OPS_CLIENT = "session-ops";
|
|
319
|
+
const PERMISSION_RPC_TIMEOUT_MS = 3e4;
|
|
320
|
+
const SETTLE_TIMEOUT_MS = 5e3;
|
|
321
|
+
const SETTLE_POLL_MS = 500;
|
|
322
|
+
function buildPermissionRpcPayload(requestId, verdict) {
|
|
323
|
+
if (verdict.kind === "approve") {
|
|
324
|
+
return {
|
|
325
|
+
id: requestId,
|
|
326
|
+
approved: true,
|
|
327
|
+
decision: verdict.forSession ? "approved_for_session" : "approved"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
id: requestId,
|
|
332
|
+
approved: false,
|
|
333
|
+
decision: "denied",
|
|
334
|
+
...verdict.reason ? { reason: verdict.reason } : {}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function interpretPermissionAck(ack, decryptResult) {
|
|
338
|
+
if (!ack.ok) {
|
|
339
|
+
const message = ack.error ?? "RPC call failed";
|
|
340
|
+
if (message === "RPC method not available") {
|
|
341
|
+
return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
|
|
342
|
+
}
|
|
343
|
+
if (message === "RPC target disconnected") {
|
|
344
|
+
return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
|
|
345
|
+
}
|
|
346
|
+
if (/timed? ?out/i.test(message)) {
|
|
347
|
+
return { status: "timeout", message };
|
|
348
|
+
}
|
|
349
|
+
if (isRpcRateLimitAck(ack)) {
|
|
350
|
+
return { status: "rate-limited", message, retryAfterMs: rpcRateRetryDelayMs(1, ack.retryAfterMs, 0) };
|
|
351
|
+
}
|
|
352
|
+
return { status: "rejected", message };
|
|
353
|
+
}
|
|
354
|
+
let body = null;
|
|
355
|
+
if (typeof ack.result === "string" && ack.result.length > 0) {
|
|
356
|
+
try {
|
|
357
|
+
body = decryptResult(ack.result);
|
|
358
|
+
} catch {
|
|
359
|
+
body = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (body && typeof body === "object" && typeof body.error === "string") {
|
|
363
|
+
return { status: "handler-error", message: body.error };
|
|
364
|
+
}
|
|
365
|
+
return { status: "acknowledged" };
|
|
366
|
+
}
|
|
367
|
+
function pendingRequestsOf(agentState, now) {
|
|
368
|
+
const requests = agentState?.requests;
|
|
369
|
+
if (!requests || typeof requests !== "object") return [];
|
|
370
|
+
return Object.entries(requests).map(([id, request]) => ({
|
|
371
|
+
id,
|
|
372
|
+
tool: typeof request?.tool === "string" ? request.tool : "unknown",
|
|
373
|
+
...request?.kind ? { kind: request.kind } : {},
|
|
374
|
+
...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
|
|
375
|
+
})).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
|
|
376
|
+
}
|
|
377
|
+
const RPC_DEADLINE_GRACE_MS = 2e3;
|
|
378
|
+
function withDeadline(promise, ms, message) {
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
|
|
381
|
+
promise.then(
|
|
382
|
+
(value) => {
|
|
383
|
+
clearTimeout(timer);
|
|
384
|
+
resolve(value);
|
|
385
|
+
},
|
|
386
|
+
(error) => {
|
|
387
|
+
clearTimeout(timer);
|
|
388
|
+
reject(error);
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
|
|
394
|
+
const now = deps.now ?? Date.now;
|
|
395
|
+
const persisted = (deps.readPersisted ?? persistence.readPersistedSessions)()[sessionId];
|
|
396
|
+
if (!persisted) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`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.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const token = await (deps.bearerToken ?? bearerToken$1)();
|
|
402
|
+
const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
|
|
403
|
+
const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
|
|
404
|
+
if (!before.some((request) => request.id === requestId)) {
|
|
405
|
+
const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
|
|
406
|
+
throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
|
|
407
|
+
}
|
|
408
|
+
const payload = buildPermissionRpcPayload(requestId, verdict);
|
|
409
|
+
const key = authRelay.decodeBase64(persisted.encryptionKey);
|
|
410
|
+
const params = authRelay.encodeBase64(authRelay.encrypt(key, persisted.encryptionVariant, payload));
|
|
411
|
+
const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
|
|
412
|
+
const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
|
|
413
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
414
|
+
let ack;
|
|
415
|
+
try {
|
|
416
|
+
for (let attempt = 1; ; attempt++) {
|
|
417
|
+
try {
|
|
418
|
+
ack = await withDeadline(
|
|
419
|
+
transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
|
|
420
|
+
rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
|
|
421
|
+
`permission RPC did not settle within ${rpcTimeoutMs}ms`
|
|
422
|
+
);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
|
|
425
|
+
}
|
|
426
|
+
if (!isRpcRateLimitAck(ack) || attempt > RPC_RATE_MAX_RETRIES) break;
|
|
427
|
+
const waitMs = rpcRateRetryDelayMs(attempt, ack.retryAfterMs, (deps.random ?? Math.random)());
|
|
428
|
+
deps.onRateLimited?.({ attempt, waitMs, serverError: ack.error ?? "RPC rate limit reached" });
|
|
429
|
+
await sleep(waitMs);
|
|
430
|
+
}
|
|
431
|
+
} finally {
|
|
432
|
+
transport.close();
|
|
433
|
+
}
|
|
434
|
+
const outcome = interpretPermissionAck(ack, (encrypted) => authRelay.decrypt(key, persisted.encryptionVariant, authRelay.decodeBase64(encrypted)));
|
|
435
|
+
if (outcome.status !== "acknowledged") {
|
|
436
|
+
return { sessionId, requestId, payload, outcome };
|
|
437
|
+
}
|
|
438
|
+
const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
|
|
439
|
+
let settled = false;
|
|
440
|
+
for (; ; ) {
|
|
441
|
+
const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
|
|
442
|
+
if (!pending.some((request) => request.id === requestId)) {
|
|
443
|
+
settled = true;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
if (now() >= deadline) break;
|
|
447
|
+
await sleep(SETTLE_POLL_MS);
|
|
448
|
+
}
|
|
449
|
+
return { sessionId, requestId, payload, outcome, settled };
|
|
450
|
+
}
|
|
451
|
+
async function bearerToken$1() {
|
|
452
|
+
const credentials = await persistence.readCredentialsForConfiguredRelay();
|
|
453
|
+
if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
|
|
454
|
+
return credentials.token;
|
|
455
|
+
}
|
|
456
|
+
async function fetchDecryptedAgentState(sessionId, persisted, token) {
|
|
457
|
+
const response = await axios.get(
|
|
458
|
+
`${configuration.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
|
|
459
|
+
{
|
|
460
|
+
headers: {
|
|
461
|
+
"Authorization": `Bearer ${token}`,
|
|
462
|
+
"X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${configuration.configuration.currentCliVersion}`
|
|
463
|
+
},
|
|
464
|
+
timeout: 15e3,
|
|
465
|
+
// 404 is a real answer ("not yours / gone"), not a transport fault.
|
|
466
|
+
validateStatus: (status) => status >= 200 && status < 300 || status === 404
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
if (response.status === 404) {
|
|
470
|
+
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).`);
|
|
471
|
+
}
|
|
472
|
+
const raw = response.data?.session?.agentState;
|
|
473
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
474
|
+
return authRelay.decrypt(authRelay.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, authRelay.decodeBase64(raw));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const SESSION_OPS_CLIENT = "session-ops";
|
|
478
|
+
const DEFAULT_RECENT_LIMIT = 15;
|
|
479
|
+
const MAX_READ_LIMIT = 100;
|
|
480
|
+
function backgroundTasksOf(child) {
|
|
481
|
+
const raw = child.backgroundTasks;
|
|
482
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
483
|
+
const r = raw;
|
|
484
|
+
if (typeof r.count !== "number" || !Array.isArray(r.tasks)) return void 0;
|
|
485
|
+
return {
|
|
486
|
+
count: r.count,
|
|
487
|
+
tasks: r.tasks.filter((task) => !!task && typeof task === "object" && typeof task.id === "string"),
|
|
488
|
+
...typeof r.reportedAt === "number" ? { reportedAt: r.reportedAt } : {},
|
|
489
|
+
...r.stale === true ? { stale: true } : {}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function mergeSessionSummaries(live, persisted, options = {}) {
|
|
493
|
+
const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
|
|
494
|
+
const seen = /* @__PURE__ */ new Set();
|
|
495
|
+
const summaries = [];
|
|
496
|
+
for (const child of live) {
|
|
497
|
+
const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
|
|
498
|
+
if (!id || seen.has(id)) continue;
|
|
499
|
+
seen.add(id);
|
|
500
|
+
summaries.push(toSummary(id, persisted[id], livenessOf(child)));
|
|
501
|
+
}
|
|
502
|
+
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);
|
|
503
|
+
for (const [id, entry] of rest) {
|
|
504
|
+
summaries.push(toSummary(id, entry, { live: false }));
|
|
505
|
+
}
|
|
506
|
+
if (!options.tag) return summaries;
|
|
507
|
+
const wanted = options.tag;
|
|
508
|
+
return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
|
|
509
|
+
}
|
|
510
|
+
function livenessOf(child) {
|
|
511
|
+
const backgroundTasks = backgroundTasksOf(child);
|
|
512
|
+
return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0, ...backgroundTasks ? { backgroundTasks } : {} };
|
|
513
|
+
}
|
|
514
|
+
function sessionLiveness(live, sessionId) {
|
|
515
|
+
const child = live.find((entry) => entry.happySessionId === sessionId);
|
|
516
|
+
return child ? livenessOf(child) : { live: false };
|
|
517
|
+
}
|
|
518
|
+
function toSummary(id, persisted, extra) {
|
|
519
|
+
const meta = persisted?.metadata;
|
|
520
|
+
return {
|
|
521
|
+
id,
|
|
522
|
+
live: extra.live,
|
|
523
|
+
...extra.pid !== void 0 ? { pid: extra.pid } : {},
|
|
524
|
+
...extra.backgroundTasks ? { backgroundTasks: extra.backgroundTasks } : {},
|
|
525
|
+
...meta?.summary?.text ? { title: meta.summary.text } : {},
|
|
526
|
+
...meta?.path ? { cwd: meta.path } : {},
|
|
527
|
+
...meta?.flavor ? { flavor: meta.flavor } : {},
|
|
528
|
+
...meta?.tags?.length ? { tags: [...meta.tags] } : {},
|
|
529
|
+
...meta?.variant ? { variant: meta.variant } : {},
|
|
530
|
+
url: sessionWebUrl(id),
|
|
531
|
+
...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
async function listSessions(options = {}) {
|
|
535
|
+
const live = await controlClient.listDaemonSessions();
|
|
536
|
+
return mergeSessionSummaries(live, persistence.readPersistedSessions(), options);
|
|
537
|
+
}
|
|
538
|
+
function summarizeAccountSession(row, persisted, liveIds, now) {
|
|
539
|
+
const base = {
|
|
540
|
+
id: row.id,
|
|
541
|
+
live: liveIds.has(row.id),
|
|
542
|
+
url: sessionWebUrl(row.id),
|
|
543
|
+
decryptable: false,
|
|
544
|
+
readable: false,
|
|
545
|
+
active: row.active === true,
|
|
546
|
+
archived: typeof row.archivedAt === "number",
|
|
547
|
+
...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
|
|
548
|
+
...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
|
|
549
|
+
attention: false
|
|
550
|
+
};
|
|
551
|
+
if (!persisted) return base;
|
|
552
|
+
const key = authRelay.decodeBase64(persisted.encryptionKey);
|
|
553
|
+
let metadata = null;
|
|
554
|
+
let agentState = null;
|
|
555
|
+
try {
|
|
556
|
+
metadata = row.metadata ? authRelay.decrypt(key, persisted.encryptionVariant, authRelay.decodeBase64(row.metadata)) : null;
|
|
557
|
+
agentState = row.agentState ? authRelay.decrypt(key, persisted.encryptionVariant, authRelay.decodeBase64(row.agentState)) : null;
|
|
558
|
+
} catch {
|
|
559
|
+
return base;
|
|
560
|
+
}
|
|
561
|
+
if (!metadata) return base;
|
|
562
|
+
const meta = metadata;
|
|
563
|
+
const pending = pendingRequestsOf(agentState, now);
|
|
564
|
+
const backgroundTasks = backgroundTaskActivity.backgroundTasksReport(agentState?.backgroundTasks, now, { active: row.active === true });
|
|
565
|
+
return {
|
|
566
|
+
...base,
|
|
567
|
+
decryptable: true,
|
|
568
|
+
readable: true,
|
|
569
|
+
backgroundTasks,
|
|
570
|
+
...meta.summary?.text ? { title: meta.summary.text } : {},
|
|
571
|
+
...meta.path ? { cwd: meta.path } : {},
|
|
572
|
+
...meta.flavor ? { flavor: meta.flavor } : {},
|
|
573
|
+
...meta.tags?.length ? { tags: [...meta.tags] } : {},
|
|
574
|
+
...meta.variant ? { variant: meta.variant } : {},
|
|
575
|
+
...meta.machineId ? { machineId: meta.machineId } : {},
|
|
576
|
+
...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
|
|
577
|
+
pending,
|
|
578
|
+
attention: pending.length > 0
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function orderAccountSessions(summaries, options = {}) {
|
|
582
|
+
const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
|
|
583
|
+
const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
|
|
584
|
+
let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
|
|
585
|
+
const byRank = rank(a) - rank(b);
|
|
586
|
+
if (byRank !== 0) return byRank;
|
|
587
|
+
if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
|
|
588
|
+
return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
|
|
589
|
+
});
|
|
590
|
+
if (options.tag) {
|
|
591
|
+
const wanted = options.tag;
|
|
592
|
+
rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
|
|
593
|
+
}
|
|
594
|
+
const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
|
|
595
|
+
let idleKept = 0;
|
|
596
|
+
return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
|
|
597
|
+
}
|
|
598
|
+
async function listAccountSessions(options = {}) {
|
|
599
|
+
const token = await bearerToken();
|
|
600
|
+
const response = await axios.get(`${configuration.configuration.serverUrl}/v1/sessions`, {
|
|
601
|
+
headers: {
|
|
602
|
+
"Authorization": `Bearer ${token}`,
|
|
603
|
+
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.configuration.currentCliVersion}`
|
|
604
|
+
},
|
|
605
|
+
timeout: 15e3
|
|
606
|
+
});
|
|
607
|
+
const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
|
|
608
|
+
const persisted = persistence.readPersistedSessions();
|
|
609
|
+
const live = await controlClient.listDaemonSessions();
|
|
610
|
+
const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
|
|
611
|
+
const now = Date.now();
|
|
612
|
+
const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
|
|
613
|
+
return orderAccountSessions(summaries, options);
|
|
614
|
+
}
|
|
615
|
+
async function bearerToken() {
|
|
616
|
+
const credentials = await persistence.readCredentialsForConfiguredRelay();
|
|
617
|
+
if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
|
|
618
|
+
return credentials.token;
|
|
619
|
+
}
|
|
620
|
+
function requirePersisted(sessionId) {
|
|
621
|
+
const persisted = persistence.readPersistedSessions()[sessionId];
|
|
622
|
+
if (!persisted) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
return persisted;
|
|
628
|
+
}
|
|
629
|
+
async function readSessionLog(sessionId, persisted, limit) {
|
|
630
|
+
const token = await bearerToken();
|
|
631
|
+
const response = await axios.get(
|
|
632
|
+
`${configuration.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
633
|
+
{
|
|
634
|
+
params: { before_seq: 2147483647, limit },
|
|
635
|
+
headers: {
|
|
636
|
+
"Authorization": `Bearer ${token}`,
|
|
637
|
+
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.configuration.currentCliVersion}`
|
|
638
|
+
},
|
|
639
|
+
timeout: 15e3
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
|
|
643
|
+
messages.reverse();
|
|
644
|
+
const key = authRelay.decodeBase64(persisted.encryptionKey);
|
|
645
|
+
return messages.map((message) => {
|
|
646
|
+
if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
|
|
647
|
+
try {
|
|
648
|
+
return { seq: message.seq, body: authRelay.decrypt(key, persisted.encryptionVariant, authRelay.decodeBase64(message.content.c)) };
|
|
649
|
+
} catch {
|
|
650
|
+
return { seq: message.seq, body: null };
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const TURN_WINDOW = 500;
|
|
655
|
+
async function readSessionTranscript(sessionId, limit, options = {}) {
|
|
656
|
+
const persisted = requirePersisted(sessionId);
|
|
657
|
+
const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
|
|
658
|
+
const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
|
|
659
|
+
const shown = entries.slice(-bounded);
|
|
660
|
+
const live = await controlClient.listDaemonSessions();
|
|
661
|
+
return {
|
|
662
|
+
summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
|
|
663
|
+
messageCount: shown.length,
|
|
664
|
+
transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
|
|
665
|
+
turn: analyzeLatestTurn(entries)
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
async function readSessionMetadata(sessionId, persisted) {
|
|
669
|
+
const token = await bearerToken();
|
|
670
|
+
const response = await axios.get(`${configuration.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
671
|
+
headers: {
|
|
672
|
+
"Authorization": `Bearer ${token}`,
|
|
673
|
+
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.configuration.currentCliVersion}`
|
|
674
|
+
},
|
|
675
|
+
timeout: 15e3
|
|
676
|
+
});
|
|
677
|
+
const ciphertext = response.data?.session?.metadata;
|
|
678
|
+
if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
|
|
679
|
+
return authRelay.decrypt(authRelay.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, authRelay.decodeBase64(ciphertext));
|
|
680
|
+
}
|
|
681
|
+
async function readSessionState(sessionId, persisted) {
|
|
682
|
+
const token = await bearerToken();
|
|
683
|
+
const response = await axios.get(`${configuration.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
684
|
+
headers: {
|
|
685
|
+
"Authorization": `Bearer ${token}`,
|
|
686
|
+
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.configuration.currentCliVersion}`
|
|
687
|
+
},
|
|
688
|
+
timeout: 15e3,
|
|
689
|
+
validateStatus: (status) => status >= 200 && status < 300 || status === 404
|
|
690
|
+
});
|
|
691
|
+
if (response.status === 404) return { found: false, active: false };
|
|
692
|
+
const row = response.data?.session;
|
|
693
|
+
if (!row || typeof row.id !== "string") throw new Error(`Server returned no session row for ${sessionId}`);
|
|
694
|
+
const state = {
|
|
695
|
+
found: true,
|
|
696
|
+
active: row.active === true,
|
|
697
|
+
...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
|
|
698
|
+
archivedAt: typeof row.archivedAt === "number" ? row.archivedAt : null,
|
|
699
|
+
...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {}
|
|
700
|
+
};
|
|
701
|
+
if (persisted && typeof row.metadata === "string" && row.metadata.length > 0) {
|
|
702
|
+
try {
|
|
703
|
+
const metadata = authRelay.decrypt(authRelay.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, authRelay.decodeBase64(row.metadata));
|
|
704
|
+
if (metadata?.machineId) state.machineId = metadata.machineId;
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return state;
|
|
709
|
+
}
|
|
710
|
+
class TurnWaitTimeoutError extends Error {
|
|
711
|
+
constructor(sessionId, timeoutMs, turn) {
|
|
712
|
+
super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
|
|
713
|
+
this.sessionId = sessionId;
|
|
714
|
+
this.timeoutMs = timeoutMs;
|
|
715
|
+
this.turn = turn;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function waitForTurnEnd(sessionId, options) {
|
|
719
|
+
const persisted = requirePersisted(sessionId);
|
|
720
|
+
const pollMs = options.pollMs ?? 3e3;
|
|
721
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
722
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
723
|
+
while (true) {
|
|
724
|
+
const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
|
|
725
|
+
if (turn.ended) return turn;
|
|
726
|
+
if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
|
|
727
|
+
await sleep(pollMs);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async function stopSession(sessionId) {
|
|
731
|
+
return await controlClient.stopDaemonSession(sessionId);
|
|
732
|
+
}
|
|
733
|
+
async function archiveSession(sessionId) {
|
|
734
|
+
const token = await bearerToken();
|
|
735
|
+
await axios.post(
|
|
736
|
+
`${configuration.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
|
|
737
|
+
{},
|
|
738
|
+
{
|
|
739
|
+
headers: {
|
|
740
|
+
"Authorization": `Bearer ${token}`,
|
|
741
|
+
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.configuration.currentCliVersion}`
|
|
742
|
+
},
|
|
743
|
+
timeout: 1e4
|
|
744
|
+
}
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
async function unarchiveSession(sessionId) {
|
|
748
|
+
const token = await bearerToken();
|
|
749
|
+
await axios.post(
|
|
750
|
+
`${configuration.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/unarchive`,
|
|
751
|
+
{},
|
|
752
|
+
{
|
|
753
|
+
headers: {
|
|
754
|
+
"Authorization": `Bearer ${token}`,
|
|
755
|
+
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.configuration.currentCliVersion}`
|
|
756
|
+
},
|
|
757
|
+
timeout: 1e4
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
exports.DEFAULT_RECENT_LIMIT = DEFAULT_RECENT_LIMIT;
|
|
763
|
+
exports.MAX_READ_LIMIT = MAX_READ_LIMIT;
|
|
764
|
+
exports.TURN_WINDOW = TURN_WINDOW;
|
|
765
|
+
exports.TurnWaitTimeoutError = TurnWaitTimeoutError;
|
|
766
|
+
exports.analyzeLatestTurn = analyzeLatestTurn;
|
|
767
|
+
exports.archiveSession = archiveSession;
|
|
768
|
+
exports.listAccountSessions = listAccountSessions;
|
|
769
|
+
exports.listSessions = listSessions;
|
|
770
|
+
exports.openUserScopedSocket = openUserScopedSocket;
|
|
771
|
+
exports.readSessionLog = readSessionLog;
|
|
772
|
+
exports.readSessionMetadata = readSessionMetadata;
|
|
773
|
+
exports.readSessionState = readSessionState;
|
|
774
|
+
exports.readSessionTranscript = readSessionTranscript;
|
|
775
|
+
exports.resolvePermissionRequest = resolvePermissionRequest;
|
|
776
|
+
exports.sendUserMessage = sendUserMessage;
|
|
777
|
+
exports.sessionLiveness = sessionLiveness;
|
|
778
|
+
exports.sessionWebUrl = sessionWebUrl;
|
|
779
|
+
exports.stopSession = stopSession;
|
|
780
|
+
exports.unarchiveSession = unarchiveSession;
|
|
781
|
+
exports.userPromptText = userPromptText;
|
|
782
|
+
exports.waitForSessionKey = waitForSessionKey;
|
|
783
|
+
exports.waitForTurnEnd = waitForTurnEnd;
|