very-happy-cli 0.2.123 → 0.2.125
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-hM-qHLN3.mjs → AcpBackend-DOQGItcS.mjs} +2 -1
- package/dist/{AcpBackend-DzIvpTxZ.cjs → AcpBackend-ssQpTLks.cjs} +30 -29
- package/dist/{AcpSessionManager-CRPZPHPa.mjs → AcpSessionManager-D4WkNsRs.mjs} +1 -1
- package/dist/{AcpSessionManager-DyU3Ikgj.cjs → AcpSessionManager-DHLc-D50.cjs} +9 -9
- package/dist/agentGuidance-Cg9ZG1pP.cjs +236 -0
- package/dist/agentGuidance-QpYwykb9.mjs +224 -0
- package/dist/codex/happyMcpStdioBridge.cjs +29 -6
- package/dist/codex/happyMcpStdioBridge.mjs +25 -2
- package/dist/{config-4RaHrVSV.cjs → config-BxHOGtEK.cjs} +11 -11
- package/dist/{config-alG86PDN.mjs → config-CZNIz3W0.mjs} +11 -11
- package/dist/{index-Beh0UZf8.cjs → index-CBj99SO6.cjs} +53 -19
- package/dist/{index-36AkXNF2.mjs → index-CrSifgJM.mjs} +2046 -1719
- package/dist/{index-vyYHKKk6.mjs → index-D-BgvQi5.mjs} +8 -8
- package/dist/{index-DWBKJ5zy.cjs → index-D_Rm9dw9.cjs} +8 -8
- package/dist/{index-5b9YhV-1.cjs → index-Do-kjc30.cjs} +2191 -1864
- package/dist/{index-Cg3Vfc00.mjs → index-Jrk6ud3g.mjs} +49 -15
- package/dist/index.cjs +5 -5
- package/dist/index.mjs +5 -5
- package/dist/{installTerminalHooks-DWCa5j7u.cjs → installTerminalHooks-CPsdeOM4.cjs} +9 -9
- package/dist/{installTerminalHooks-DqUPkh0l.mjs → installTerminalHooks-Z1Jb6yjb.mjs} +8 -8
- package/dist/lib.cjs +12 -12
- package/dist/lib.d.cts +3 -0
- package/dist/lib.d.mts +3 -0
- package/dist/lib.mjs +9 -9
- package/dist/limits-DViGHwSj.cjs +1198 -0
- package/dist/limits-FjbPJPsF.mjs +1129 -0
- package/dist/{mcp-BBya5dsZ.cjs → mcp-BbQ5FWcQ.cjs} +11 -10
- package/dist/{mcp-DZIdcsJp.mjs → mcp-zqPJ94Bk.mjs} +7 -6
- package/dist/resources-CVl7NdOB.cjs +83 -0
- package/dist/resources-D-mn7ptO.mjs +80 -0
- package/dist/{runGemini-DmWxHIwS.cjs → runGemini-BnHGYX-H.cjs} +30 -30
- package/dist/{runGemini-BQa2eF3i.mjs → runGemini-Bu49s_bN.mjs} +13 -13
- package/dist/{runOpenClaw-boyJtPBq.cjs → runOpenClaw-3Ld1eoWV.cjs} +24 -24
- package/dist/{runOpenClaw-362eg04u.mjs → runOpenClaw-CznAYPCh.mjs} +9 -9
- package/dist/{send-CT22-8Tp.mjs → send-BevgS1mf.mjs} +10 -10
- package/dist/{send-BINA__Ke.cjs → send-DCzyBxLl.cjs} +10 -10
- package/dist/{sessions-DemgtCkr.mjs → sessions-Ba2oRTmw.mjs} +10 -10
- package/dist/{sessions-DXYxV6Np.cjs → sessions-DSoaBHwA.cjs} +11 -11
- package/dist/{spawn-BtV9dU1i.cjs → spawn-CqMLY6dE.cjs} +21 -21
- package/dist/{spawn-D2egvPgO.mjs → spawn-j9KBfj4e.mjs} +21 -21
- package/dist/teams-C0ggJTJ-.mjs +164 -0
- package/dist/teams-CmU3K9vB.cjs +166 -0
- package/dist/{types-Cd_tUN3U.cjs → types-BGHnQXl5.cjs} +585 -1577
- package/dist/{types-BqW7nEN_.mjs → types-DLDkfrTC.mjs} +198 -1153
- package/package.json +7 -7
- package/dist/agentGuidance-BifkGXdE.cjs +0 -46
- package/dist/agentGuidance-C2_pkgRI.mjs +0 -37
- package/dist/limits-CwFb5S2b.mjs +0 -22
- package/dist/limits-KdWKmwtH.cjs +0 -28
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var axios = require('axios');
|
|
4
|
-
var
|
|
5
|
-
var node_util = require('node:util');
|
|
6
|
-
var node_fs = require('node:fs');
|
|
7
|
-
var os = require('node:os');
|
|
8
|
-
var path = require('node:path');
|
|
9
|
-
var promises = require('node:fs/promises');
|
|
4
|
+
var persistence = require('./limits-DViGHwSj.cjs');
|
|
10
5
|
var node_events = require('node:events');
|
|
11
6
|
var socket_ioClient = require('socket.io-client');
|
|
12
7
|
var z = require('zod');
|
|
13
8
|
var cuid2 = require('@paralleldrive/cuid2');
|
|
14
9
|
var node_crypto = require('node:crypto');
|
|
15
|
-
var tweetnacl = require('tweetnacl');
|
|
16
|
-
var limits = require('./limits-KdWKmwtH.cjs');
|
|
17
10
|
var child_process = require('child_process');
|
|
18
11
|
var util = require('util');
|
|
19
12
|
var fs = require('fs/promises');
|
|
20
13
|
var crypto = require('crypto');
|
|
21
|
-
var path
|
|
14
|
+
var path = require('path');
|
|
22
15
|
var spawn = require('cross-spawn');
|
|
23
16
|
var url = require('url');
|
|
24
|
-
var os
|
|
17
|
+
var os = require('os');
|
|
18
|
+
var promises = require('node:fs/promises');
|
|
19
|
+
var path$1 = require('node:path');
|
|
20
|
+
var os$1 = require('node:os');
|
|
25
21
|
var node_child_process = require('node:child_process');
|
|
26
22
|
var pty = require('node-pty');
|
|
23
|
+
var node_fs = require('node:fs');
|
|
27
24
|
var fs$1 = require('fs');
|
|
25
|
+
var tweetnacl = require('tweetnacl');
|
|
28
26
|
var node_readline = require('node:readline');
|
|
29
27
|
var sandboxRuntime = require('@anthropic-ai/sandbox-runtime');
|
|
30
28
|
var expoServerSdk = require('expo-server-sdk');
|
|
29
|
+
var chalk = require('chalk');
|
|
31
30
|
|
|
32
31
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
33
32
|
function _interopNamespaceDefault(e) {
|
|
@@ -50,599 +49,6 @@ function _interopNamespaceDefault(e) {
|
|
|
50
49
|
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
51
50
|
var pty__namespace = /*#__PURE__*/_interopNamespaceDefault(pty);
|
|
52
51
|
|
|
53
|
-
var name = "very-happy-cli";
|
|
54
|
-
var version = "0.2.123";
|
|
55
|
-
var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
|
|
56
|
-
var author = "Kirill Dubovitskiy";
|
|
57
|
-
var contributors = [
|
|
58
|
-
"Mereith (very-happy-cli fork)"
|
|
59
|
-
];
|
|
60
|
-
var license = "MIT";
|
|
61
|
-
var engines = {
|
|
62
|
-
node: "^20.19.0 || ^22.13.0 || >=24.0.0"
|
|
63
|
-
};
|
|
64
|
-
var type = "module";
|
|
65
|
-
var homepage = "https://github.com/Mereithhh/very-happy";
|
|
66
|
-
var bugs = "https://github.com/Mereithhh/very-happy/issues";
|
|
67
|
-
var repository = {
|
|
68
|
-
type: "git",
|
|
69
|
-
url: "git+https://github.com/Mereithhh/very-happy.git"
|
|
70
|
-
};
|
|
71
|
-
var bin = {
|
|
72
|
-
"very-happy": "./bin/very-happy.mjs",
|
|
73
|
-
"very-happy-mcp": "./bin/very-happy-mcp.mjs"
|
|
74
|
-
};
|
|
75
|
-
var main = "./dist/index.cjs";
|
|
76
|
-
var module$1 = "./dist/index.mjs";
|
|
77
|
-
var types = "./dist/index.d.cts";
|
|
78
|
-
var exports$1 = {
|
|
79
|
-
".": {
|
|
80
|
-
require: {
|
|
81
|
-
types: "./dist/index.d.cts",
|
|
82
|
-
"default": "./dist/index.cjs"
|
|
83
|
-
},
|
|
84
|
-
"import": {
|
|
85
|
-
types: "./dist/index.d.mts",
|
|
86
|
-
"default": "./dist/index.mjs"
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
"./lib": {
|
|
90
|
-
require: {
|
|
91
|
-
types: "./dist/lib.d.cts",
|
|
92
|
-
"default": "./dist/lib.cjs"
|
|
93
|
-
},
|
|
94
|
-
"import": {
|
|
95
|
-
types: "./dist/lib.d.mts",
|
|
96
|
-
"default": "./dist/lib.mjs"
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
"./codex/happyMcpStdioBridge": {
|
|
100
|
-
require: {
|
|
101
|
-
types: "./dist/codex/happyMcpStdioBridge.d.cts",
|
|
102
|
-
"default": "./dist/codex/happyMcpStdioBridge.cjs"
|
|
103
|
-
},
|
|
104
|
-
"import": {
|
|
105
|
-
types: "./dist/codex/happyMcpStdioBridge.d.mts",
|
|
106
|
-
"default": "./dist/codex/happyMcpStdioBridge.mjs"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
var files = [
|
|
111
|
-
"dist",
|
|
112
|
-
"bin",
|
|
113
|
-
"scripts",
|
|
114
|
-
"tools/licenses",
|
|
115
|
-
"package.json",
|
|
116
|
-
"README.md",
|
|
117
|
-
"LICENSE"
|
|
118
|
-
];
|
|
119
|
-
var scripts = {
|
|
120
|
-
typecheck: "tsc --noEmit",
|
|
121
|
-
"bundle:server": "node scripts/bundle-server.cjs",
|
|
122
|
-
"bundle:server:all": "node scripts/bundle-server.cjs --all-platforms",
|
|
123
|
-
"bundle:webapp": "node scripts/bundle-webapp.cjs",
|
|
124
|
-
build: "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc --noEmit && pkgroll",
|
|
125
|
-
test: "pnpm run build && vitest run --project unit",
|
|
126
|
-
"test:integration": "pnpm run build && vitest run --project integration-empty --project integration-plan-mode --project integration-authenticated",
|
|
127
|
-
"cli:install": "node scripts/install-local.cjs",
|
|
128
|
-
prepublishOnly: "pnpm test",
|
|
129
|
-
postinstall: "node scripts/unpack-tools.cjs && node scripts/fix-pty-perms.cjs"
|
|
130
|
-
};
|
|
131
|
-
var dependencies = {
|
|
132
|
-
"@agentclientprotocol/sdk": "^0.14.1",
|
|
133
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.232",
|
|
134
|
-
"@anthropic-ai/sandbox-runtime": "^0.0.37",
|
|
135
|
-
"@modelcontextprotocol/sdk": "1.30.0",
|
|
136
|
-
"@noble/ed25519": "^3.0.0",
|
|
137
|
-
"@noble/hashes": "^2.0.1",
|
|
138
|
-
"@paralleldrive/cuid2": "^2.2.2",
|
|
139
|
-
"@stablelib/base64": "^2.0.1",
|
|
140
|
-
"@stablelib/hex": "^2.0.1",
|
|
141
|
-
"@types/cross-spawn": "^6.0.6",
|
|
142
|
-
"@types/http-proxy": "^1.17.17",
|
|
143
|
-
"@types/react": "^19.2.7",
|
|
144
|
-
"@types/tmp": "^0.2.6",
|
|
145
|
-
axios: "^1.13.2",
|
|
146
|
-
chalk: "^5.6.2",
|
|
147
|
-
"cross-spawn": "^7.0.6",
|
|
148
|
-
"expo-server-sdk": "^3.15.0",
|
|
149
|
-
fastify: "5.8.5",
|
|
150
|
-
"fastify-type-provider-zod": "^6.1.0",
|
|
151
|
-
"http-proxy": "^1.18.1",
|
|
152
|
-
"http-proxy-middleware": "^3.0.7",
|
|
153
|
-
ink: "^6.5.1",
|
|
154
|
-
inquirer: "^13.2.2",
|
|
155
|
-
"node-pty": "^1.0.0",
|
|
156
|
-
open: "^10.2.0",
|
|
157
|
-
"ps-list": "^8.1.1",
|
|
158
|
-
react: "^19.2.0",
|
|
159
|
-
"socket.io-client": "^4.8.1",
|
|
160
|
-
tar: "^7.5.22",
|
|
161
|
-
tmp: "^0.2.7",
|
|
162
|
-
tweetnacl: "^1.0.3",
|
|
163
|
-
ws: "^8.21.3",
|
|
164
|
-
zod: "^4.0.0"
|
|
165
|
-
};
|
|
166
|
-
var optionalDependencies = {
|
|
167
|
-
"very-happy-tools-arm64-darwin": "0.2.123",
|
|
168
|
-
"very-happy-tools-x64-darwin": "0.2.123",
|
|
169
|
-
"very-happy-tools-arm64-linux": "0.2.123",
|
|
170
|
-
"very-happy-tools-x64-linux": "0.2.123",
|
|
171
|
-
"very-happy-tools-arm64-win32": "0.2.123",
|
|
172
|
-
"very-happy-tools-x64-win32": "0.2.123"
|
|
173
|
-
};
|
|
174
|
-
var devDependencies = {
|
|
175
|
-
"@slopus/happy-wire": "workspace:*",
|
|
176
|
-
"@xterm/addon-serialize": "0.13.0",
|
|
177
|
-
"@xterm/headless": "5.5.0",
|
|
178
|
-
"@types/inquirer": "^9.0.9",
|
|
179
|
-
"@types/node": ">=20",
|
|
180
|
-
"@types/ws": "^8.18.1",
|
|
181
|
-
"cross-env": "^10.1.0",
|
|
182
|
-
dotenv: "^16.6.1",
|
|
183
|
-
pkgroll: "^2.14.2",
|
|
184
|
-
"ts-node": "^10",
|
|
185
|
-
tsx: "^4.20.6",
|
|
186
|
-
typescript: "5.9.3",
|
|
187
|
-
vitest: "3.2.6"
|
|
188
|
-
};
|
|
189
|
-
var publishConfig = {
|
|
190
|
-
registry: "https://registry.npmjs.org"
|
|
191
|
-
};
|
|
192
|
-
var packageManager = "pnpm@10.11.0";
|
|
193
|
-
var packageJson = {
|
|
194
|
-
name: name,
|
|
195
|
-
version: version,
|
|
196
|
-
description: description,
|
|
197
|
-
author: author,
|
|
198
|
-
contributors: contributors,
|
|
199
|
-
license: license,
|
|
200
|
-
engines: engines,
|
|
201
|
-
type: type,
|
|
202
|
-
homepage: homepage,
|
|
203
|
-
bugs: bugs,
|
|
204
|
-
repository: repository,
|
|
205
|
-
bin: bin,
|
|
206
|
-
main: main,
|
|
207
|
-
module: module$1,
|
|
208
|
-
types: types,
|
|
209
|
-
exports: exports$1,
|
|
210
|
-
files: files,
|
|
211
|
-
scripts: scripts,
|
|
212
|
-
dependencies: dependencies,
|
|
213
|
-
optionalDependencies: optionalDependencies,
|
|
214
|
-
devDependencies: devDependencies,
|
|
215
|
-
publishConfig: publishConfig,
|
|
216
|
-
packageManager: packageManager
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const PRIVATE_DIRECTORY_MODE = 448;
|
|
220
|
-
const PRIVATE_FILE_MODE = 384;
|
|
221
|
-
function ignoreUnsupportedPermissions(error) {
|
|
222
|
-
if (process.platform !== "win32") throw error;
|
|
223
|
-
}
|
|
224
|
-
function ensurePrivateDirectorySync(path) {
|
|
225
|
-
node_fs.mkdirSync(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
226
|
-
try {
|
|
227
|
-
node_fs.chmodSync(path, PRIVATE_DIRECTORY_MODE);
|
|
228
|
-
} catch (error) {
|
|
229
|
-
ignoreUnsupportedPermissions(error);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
async function ensurePrivateDirectory(path) {
|
|
233
|
-
await promises.mkdir(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
234
|
-
try {
|
|
235
|
-
await promises.chmod(path, PRIVATE_DIRECTORY_MODE);
|
|
236
|
-
} catch (error) {
|
|
237
|
-
ignoreUnsupportedPermissions(error);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function hardenPrivateFileSync(path) {
|
|
241
|
-
try {
|
|
242
|
-
node_fs.chmodSync(path, PRIVATE_FILE_MODE);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
ignoreUnsupportedPermissions(error);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
function hardenPrivateDirectoryFilesSync(path$1) {
|
|
248
|
-
for (const name of node_fs.readdirSync(path$1)) {
|
|
249
|
-
const candidate = path.join(path$1, name);
|
|
250
|
-
try {
|
|
251
|
-
if (node_fs.statSync(candidate).isFile()) hardenPrivateFileSync(candidate);
|
|
252
|
-
} catch {
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
async function hardenPrivateFile(path) {
|
|
257
|
-
try {
|
|
258
|
-
await promises.chmod(path, PRIVATE_FILE_MODE);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
ignoreUnsupportedPermissions(error);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
function writePrivateFileSync(path, data) {
|
|
264
|
-
node_fs.writeFileSync(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
265
|
-
hardenPrivateFileSync(path);
|
|
266
|
-
}
|
|
267
|
-
async function writePrivateFile(path, data) {
|
|
268
|
-
await promises.writeFile(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
269
|
-
await hardenPrivateFile(path);
|
|
270
|
-
}
|
|
271
|
-
function appendPrivateFileSync(path, data) {
|
|
272
|
-
node_fs.appendFileSync(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
273
|
-
hardenPrivateFileSync(path);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const DEFAULT_RELAY_URL = "https://veryhappy.dev";
|
|
277
|
-
function normalizeHttpEndpoint(value, source) {
|
|
278
|
-
const trimmed = value.trim();
|
|
279
|
-
let parsed;
|
|
280
|
-
try {
|
|
281
|
-
parsed = new URL(trimmed);
|
|
282
|
-
} catch {
|
|
283
|
-
throw new Error(`${source} must be an absolute http(s) origin`);
|
|
284
|
-
}
|
|
285
|
-
const onlyOriginPath = /^\/+$/u.test(parsed.pathname);
|
|
286
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username.length > 0 || parsed.password.length > 0 || !onlyOriginPath || parsed.search.length > 0 || parsed.hash.length > 0) {
|
|
287
|
-
throw new Error(`${source} must be an http(s) origin without credentials, path, query, or fragment`);
|
|
288
|
-
}
|
|
289
|
-
return parsed.origin;
|
|
290
|
-
}
|
|
291
|
-
function resolveHttpEndpoint(environmentValue, settingsValue, environmentName, settingsName) {
|
|
292
|
-
if (environmentValue !== void 0) {
|
|
293
|
-
return normalizeHttpEndpoint(environmentValue, environmentName);
|
|
294
|
-
}
|
|
295
|
-
if (settingsValue !== void 0) {
|
|
296
|
-
return normalizeHttpEndpoint(settingsValue, `settings.${settingsName}`);
|
|
297
|
-
}
|
|
298
|
-
return DEFAULT_RELAY_URL;
|
|
299
|
-
}
|
|
300
|
-
class Configuration {
|
|
301
|
-
serverUrl;
|
|
302
|
-
webappUrl;
|
|
303
|
-
isDaemonProcess;
|
|
304
|
-
// Directories and paths (from persistence)
|
|
305
|
-
happyHomeDir;
|
|
306
|
-
logsDir;
|
|
307
|
-
settingsFile;
|
|
308
|
-
privateKeyFile;
|
|
309
|
-
daemonStateFile;
|
|
310
|
-
daemonLockFile;
|
|
311
|
-
sessionsFile;
|
|
312
|
-
currentCliVersion;
|
|
313
|
-
isExperimentalEnabled;
|
|
314
|
-
disableCaffeinate;
|
|
315
|
-
constructor() {
|
|
316
|
-
const args = process.argv.slice(2);
|
|
317
|
-
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
318
|
-
if (process.env.HAPPY_HOME_DIR) {
|
|
319
|
-
const expandedPath = process.env.HAPPY_HOME_DIR.replace(/^~/, os.homedir());
|
|
320
|
-
this.happyHomeDir = expandedPath;
|
|
321
|
-
} else {
|
|
322
|
-
this.happyHomeDir = path.join(os.homedir(), ".happy");
|
|
323
|
-
}
|
|
324
|
-
this.logsDir = path.join(this.happyHomeDir, "logs");
|
|
325
|
-
this.settingsFile = path.join(this.happyHomeDir, "settings.json");
|
|
326
|
-
this.privateKeyFile = path.join(this.happyHomeDir, "access.key");
|
|
327
|
-
this.daemonStateFile = path.join(this.happyHomeDir, "daemon.state.json");
|
|
328
|
-
this.daemonLockFile = path.join(this.happyHomeDir, "daemon.state.json.lock");
|
|
329
|
-
this.sessionsFile = path.join(this.happyHomeDir, "sessions.json");
|
|
330
|
-
try {
|
|
331
|
-
this.serverUrl = resolveHttpEndpoint(
|
|
332
|
-
process.env.HAPPY_SERVER_URL,
|
|
333
|
-
readSettingsStringSync(this.settingsFile, "serverUrl"),
|
|
334
|
-
"HAPPY_SERVER_URL",
|
|
335
|
-
"serverUrl"
|
|
336
|
-
);
|
|
337
|
-
this.webappUrl = resolveHttpEndpoint(
|
|
338
|
-
process.env.HAPPY_WEBAPP_URL,
|
|
339
|
-
readSettingsStringSync(this.settingsFile, "webappUrl"),
|
|
340
|
-
"HAPPY_WEBAPP_URL",
|
|
341
|
-
"webappUrl"
|
|
342
|
-
);
|
|
343
|
-
} catch (error) {
|
|
344
|
-
const message = error instanceof Error ? error.message : "invalid client endpoint";
|
|
345
|
-
process.stderr.write(`[very-happy] Configuration error: ${message}
|
|
346
|
-
`);
|
|
347
|
-
process.exit(1);
|
|
348
|
-
}
|
|
349
|
-
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
|
|
350
|
-
this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.HAPPY_DISABLE_CAFFEINATE?.toLowerCase() || "");
|
|
351
|
-
this.currentCliVersion = packageJson.version;
|
|
352
|
-
const variant = process.env.HAPPY_VARIANT || "stable";
|
|
353
|
-
if (!this.isDaemonProcess && variant === "dev") {
|
|
354
|
-
console.log("\x1B[33m\u{1F527} DEV MODE\x1B[0m - Data: " + this.happyHomeDir);
|
|
355
|
-
}
|
|
356
|
-
ensurePrivateDirectorySync(this.happyHomeDir);
|
|
357
|
-
ensurePrivateDirectorySync(this.logsDir);
|
|
358
|
-
for (const privateFile of [
|
|
359
|
-
this.settingsFile,
|
|
360
|
-
this.privateKeyFile,
|
|
361
|
-
this.daemonStateFile,
|
|
362
|
-
this.daemonLockFile,
|
|
363
|
-
this.sessionsFile
|
|
364
|
-
]) {
|
|
365
|
-
if (node_fs.existsSync(privateFile)) hardenPrivateFileSync(privateFile);
|
|
366
|
-
}
|
|
367
|
-
hardenPrivateDirectoryFilesSync(this.logsDir);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
function readSettingsStringSync(settingsFile, key) {
|
|
371
|
-
try {
|
|
372
|
-
if (!node_fs.existsSync(settingsFile)) return void 0;
|
|
373
|
-
const raw = JSON.parse(node_fs.readFileSync(settingsFile, "utf8"));
|
|
374
|
-
const value = raw?.[key];
|
|
375
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
376
|
-
} catch {
|
|
377
|
-
return void 0;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const configuration = new Configuration();
|
|
381
|
-
|
|
382
|
-
function sensitiveKey(key) {
|
|
383
|
-
const normalized = key.toLowerCase().replace(/[_\-\s]/g, "");
|
|
384
|
-
return /(token|secret|password|authorization|cookie|credential|apikey|privatekey|encryptionkey|accesskey|claimsecret)/.test(normalized);
|
|
385
|
-
}
|
|
386
|
-
function redactLogString(value) {
|
|
387
|
-
return value.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED]").replace(/([?&](?:token|secret|password|api[_-]?key)=)[^&#\s]+/gi, "$1[REDACTED]").replace(/((?:token|secret|password|api[_-]?key)\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]");
|
|
388
|
-
}
|
|
389
|
-
function redactLogValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
390
|
-
if (typeof value === "string") return redactLogString(value);
|
|
391
|
-
if (value === null || typeof value !== "object") return value;
|
|
392
|
-
if (depth >= 6) return "[TRUNCATED]";
|
|
393
|
-
if (seen.has(value)) return "[CIRCULAR]";
|
|
394
|
-
seen.add(value);
|
|
395
|
-
if (value instanceof Error) {
|
|
396
|
-
const error = value;
|
|
397
|
-
return {
|
|
398
|
-
name: error.name,
|
|
399
|
-
message: redactLogString(error.message),
|
|
400
|
-
...typeof error.code === "string" || typeof error.code === "number" ? { code: error.code } : {},
|
|
401
|
-
...typeof (error.status ?? error.response?.status) === "number" ? { status: error.status ?? error.response?.status } : {}
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
if (Array.isArray(value)) return value.map((item) => redactLogValue(item, depth + 1, seen));
|
|
405
|
-
const result = {};
|
|
406
|
-
for (const [key, child] of Object.entries(value)) {
|
|
407
|
-
if (sensitiveKey(key)) {
|
|
408
|
-
result[key] = "[REDACTED]";
|
|
409
|
-
} else if (key === "environmentVariables" && child && typeof child === "object") {
|
|
410
|
-
result[key] = { names: Object.keys(child).sort() };
|
|
411
|
-
} else {
|
|
412
|
-
result[key] = redactLogValue(child, depth + 1, seen);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return result;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
|
|
419
|
-
return date.toLocaleString("sv-SE", {
|
|
420
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
421
|
-
year: "numeric",
|
|
422
|
-
month: "2-digit",
|
|
423
|
-
day: "2-digit",
|
|
424
|
-
hour: "2-digit",
|
|
425
|
-
minute: "2-digit",
|
|
426
|
-
second: "2-digit"
|
|
427
|
-
}).replace(/[: ]/g, "-").replace(/,/g, "") + "-pid-" + process.pid;
|
|
428
|
-
}
|
|
429
|
-
function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
|
|
430
|
-
return date.toLocaleTimeString("en-US", {
|
|
431
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
432
|
-
hour12: false,
|
|
433
|
-
hour: "2-digit",
|
|
434
|
-
minute: "2-digit",
|
|
435
|
-
second: "2-digit",
|
|
436
|
-
fractionalSecondDigits: 3
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
function getSessionLogPath() {
|
|
440
|
-
const timestamp = createTimestampForFilename();
|
|
441
|
-
const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
|
|
442
|
-
return path.join(configuration.logsDir, filename);
|
|
443
|
-
}
|
|
444
|
-
function safeRemoteLogUrl(raw) {
|
|
445
|
-
if (!raw) return void 0;
|
|
446
|
-
try {
|
|
447
|
-
const url = new URL(raw);
|
|
448
|
-
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
449
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return void 0;
|
|
450
|
-
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") return void 0;
|
|
451
|
-
return url.origin;
|
|
452
|
-
} catch {
|
|
453
|
-
return void 0;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
class Logger {
|
|
457
|
-
constructor(logFilePath = getSessionLogPath()) {
|
|
458
|
-
this.logFilePath = logFilePath;
|
|
459
|
-
const remoteUrl = safeRemoteLogUrl(process.env.HAPPY_SERVER_URL);
|
|
460
|
-
const remoteToken = process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING_TOKEN;
|
|
461
|
-
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && remoteUrl && remoteToken && Buffer.byteLength(remoteToken) >= 32) {
|
|
462
|
-
this.dangerouslyUnencryptedServerLoggingUrl = remoteUrl;
|
|
463
|
-
this.dangerouslyUnencryptedServerLoggingToken = remoteToken;
|
|
464
|
-
console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
dangerouslyUnencryptedServerLoggingUrl;
|
|
468
|
-
dangerouslyUnencryptedServerLoggingToken;
|
|
469
|
-
// Use local timezone for simplicity of locating the logs,
|
|
470
|
-
// in practice you will not need absolute timestamps
|
|
471
|
-
localTimezoneTimestamp() {
|
|
472
|
-
return createTimestampForLogEntry();
|
|
473
|
-
}
|
|
474
|
-
debug(message, ...args) {
|
|
475
|
-
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
|
|
476
|
-
}
|
|
477
|
-
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
|
|
478
|
-
if (!process.env.DEBUG) {
|
|
479
|
-
this.debug(`In production, skipping message inspection`);
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
const truncateStrings = (obj) => {
|
|
483
|
-
if (typeof obj === "string") {
|
|
484
|
-
return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
|
|
485
|
-
}
|
|
486
|
-
if (Array.isArray(obj)) {
|
|
487
|
-
const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
|
|
488
|
-
if (obj.length > maxArrayLength) {
|
|
489
|
-
truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
|
|
490
|
-
}
|
|
491
|
-
return truncatedArray;
|
|
492
|
-
}
|
|
493
|
-
if (obj && typeof obj === "object") {
|
|
494
|
-
const result = {};
|
|
495
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
496
|
-
if (key === "usage") {
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
result[key] = truncateStrings(value);
|
|
500
|
-
}
|
|
501
|
-
return result;
|
|
502
|
-
}
|
|
503
|
-
return obj;
|
|
504
|
-
};
|
|
505
|
-
const truncatedObject = truncateStrings(redactLogValue(object));
|
|
506
|
-
const json = JSON.stringify(truncatedObject, null, 2);
|
|
507
|
-
this.logToFile(`[${this.localTimezoneTimestamp()}]`, redactLogString(message), "\n", json);
|
|
508
|
-
}
|
|
509
|
-
info(message, ...args) {
|
|
510
|
-
this.logToConsole("info", "", message, ...args);
|
|
511
|
-
this.debug(message, args);
|
|
512
|
-
}
|
|
513
|
-
infoDeveloper(message, ...args) {
|
|
514
|
-
this.debug(message, ...args);
|
|
515
|
-
if (process.env.DEBUG) {
|
|
516
|
-
this.logToConsole("info", "[DEV]", message, ...args);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
warn(message, ...args) {
|
|
520
|
-
this.logToConsole("warn", "", message, ...args);
|
|
521
|
-
this.debug(`[WARN] ${message}`, ...args);
|
|
522
|
-
}
|
|
523
|
-
getLogPath() {
|
|
524
|
-
return this.logFilePath;
|
|
525
|
-
}
|
|
526
|
-
logToConsole(level, prefix, message, ...args) {
|
|
527
|
-
message = redactLogString(message);
|
|
528
|
-
args = args.map((arg) => redactLogValue(arg));
|
|
529
|
-
switch (level) {
|
|
530
|
-
case "debug": {
|
|
531
|
-
console.log(chalk.gray(prefix), message, ...args);
|
|
532
|
-
break;
|
|
533
|
-
}
|
|
534
|
-
case "error": {
|
|
535
|
-
console.error(chalk.red(prefix), message, ...args);
|
|
536
|
-
break;
|
|
537
|
-
}
|
|
538
|
-
case "info": {
|
|
539
|
-
console.log(chalk.blue(prefix), message, ...args);
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
case "warn": {
|
|
543
|
-
console.log(chalk.yellow(prefix), message, ...args);
|
|
544
|
-
break;
|
|
545
|
-
}
|
|
546
|
-
default: {
|
|
547
|
-
this.debug("Unknown log level:", level);
|
|
548
|
-
console.log(chalk.blue(prefix), message, ...args);
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
async sendToRemoteServer(level, message, ...args) {
|
|
554
|
-
if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
|
|
555
|
-
try {
|
|
556
|
-
await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
|
|
557
|
-
method: "POST",
|
|
558
|
-
headers: {
|
|
559
|
-
"Content-Type": "application/json",
|
|
560
|
-
"Authorization": `Bearer ${this.dangerouslyUnencryptedServerLoggingToken}`
|
|
561
|
-
},
|
|
562
|
-
body: JSON.stringify({
|
|
563
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
564
|
-
level,
|
|
565
|
-
message: `${redactLogString(message)} ${args.map((raw) => {
|
|
566
|
-
const safe = redactLogValue(raw);
|
|
567
|
-
return typeof safe === "object" ? JSON.stringify(safe, null, 2) : String(safe);
|
|
568
|
-
}).join(" ")}`,
|
|
569
|
-
source: "cli",
|
|
570
|
-
platform: process.platform
|
|
571
|
-
})
|
|
572
|
-
});
|
|
573
|
-
} catch (error) {
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
logToFile(prefix, message, ...args) {
|
|
577
|
-
const safeMessage = redactLogString(message);
|
|
578
|
-
const safeArgs = args.map((arg) => redactLogValue(arg));
|
|
579
|
-
const logLine = `${prefix} ${safeMessage} ${safeArgs.map(
|
|
580
|
-
(arg) => typeof arg === "string" ? arg : node_util.inspect(arg, { depth: 5, breakLength: 120 })
|
|
581
|
-
).join(" ")}
|
|
582
|
-
`;
|
|
583
|
-
if (this.dangerouslyUnencryptedServerLoggingUrl) {
|
|
584
|
-
let level = "info";
|
|
585
|
-
if (prefix.includes(this.localTimezoneTimestamp())) {
|
|
586
|
-
level = "debug";
|
|
587
|
-
}
|
|
588
|
-
this.sendToRemoteServer(level, safeMessage, ...safeArgs).catch(() => {
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
try {
|
|
592
|
-
appendPrivateFileSync(this.logFilePath, logLine);
|
|
593
|
-
} catch (appendError) {
|
|
594
|
-
if (process.env.DEBUG) {
|
|
595
|
-
console.error("[DEV MODE ONLY THROWING] Failed to append to log file:", appendError);
|
|
596
|
-
throw appendError;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
let logger = new Logger();
|
|
602
|
-
async function listDaemonLogFiles(limit = 50) {
|
|
603
|
-
try {
|
|
604
|
-
const logsDir = configuration.logsDir;
|
|
605
|
-
if (!node_fs.existsSync(logsDir)) {
|
|
606
|
-
return [];
|
|
607
|
-
}
|
|
608
|
-
const logs = node_fs.readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
|
|
609
|
-
const fullPath = path.join(logsDir, file);
|
|
610
|
-
const stats = node_fs.statSync(fullPath);
|
|
611
|
-
return { file, path: fullPath, modified: stats.mtime };
|
|
612
|
-
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
613
|
-
try {
|
|
614
|
-
const { readDaemonState } = await Promise.resolve().then(function () { return persistence; });
|
|
615
|
-
const state = await readDaemonState();
|
|
616
|
-
if (!state) {
|
|
617
|
-
return logs;
|
|
618
|
-
}
|
|
619
|
-
if (state.daemonLogPath && node_fs.existsSync(state.daemonLogPath)) {
|
|
620
|
-
const stats = node_fs.statSync(state.daemonLogPath);
|
|
621
|
-
const persisted = {
|
|
622
|
-
file: path.basename(state.daemonLogPath),
|
|
623
|
-
path: state.daemonLogPath,
|
|
624
|
-
modified: stats.mtime
|
|
625
|
-
};
|
|
626
|
-
const idx = logs.findIndex((l) => l.path === persisted.path);
|
|
627
|
-
if (idx >= 0) {
|
|
628
|
-
const [found] = logs.splice(idx, 1);
|
|
629
|
-
logs.unshift(found);
|
|
630
|
-
} else {
|
|
631
|
-
logs.unshift(persisted);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
} catch {
|
|
635
|
-
}
|
|
636
|
-
return logs.slice(0, Math.max(0, limit));
|
|
637
|
-
} catch {
|
|
638
|
-
return [];
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
async function getLatestDaemonLog() {
|
|
642
|
-
const [latest] = await listDaemonLogFiles(1);
|
|
643
|
-
return latest || null;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
52
|
const sessionRoleSchema = z__namespace.enum(["user", "agent"]);
|
|
647
53
|
const sessionTextEventSchema = z__namespace.object({
|
|
648
54
|
t: z__namespace.literal("text"),
|
|
@@ -1048,8 +454,34 @@ const ConnectionDiagnosticEventSchema = z.z.object({
|
|
|
1048
454
|
z.z.object({
|
|
1049
455
|
events: z.z.array(ConnectionDiagnosticEventSchema).min(1).max(32)
|
|
1050
456
|
}).strict();
|
|
457
|
+
const id = z.z.string().min(1).max(128);
|
|
458
|
+
const text = z.z.string().min(1).max(32e3);
|
|
459
|
+
const TeamActionSchema = z.z.discriminatedUnion("type", [
|
|
460
|
+
z.z.object({ type: z.z.literal("archive") }),
|
|
461
|
+
z.z.object({ type: z.z.literal("schedule-create"), name: id, botId: id, body: text, runAt: z.z.number().int().positive().max(864e13), intervalMs: z.z.number().int().min(6e4).max(366 * 864e5).optional() }),
|
|
462
|
+
z.z.object({ type: z.z.literal("schedule-pause"), scheduleId: id, version: z.z.number().int().positive() }),
|
|
463
|
+
z.z.object({ type: z.z.literal("schedule-resume"), scheduleId: id, version: z.z.number().int().positive() }),
|
|
464
|
+
z.z.object({ type: z.z.literal("schedule-cancel"), scheduleId: id, version: z.z.number().int().positive() }),
|
|
465
|
+
z.z.object({ type: z.z.literal("reconcile-operation"), operationId: id, claimId: id, note: z.z.string().trim().min(1).max(4e3) }),
|
|
466
|
+
z.z.object({ type: z.z.literal("join"), name: id, sessionId: id, botId: id.optional() }),
|
|
467
|
+
z.z.object({ type: z.z.literal("delegate"), goal: text, acceptance: z.z.array(text).min(1).max(32), parentTaskId: id.optional(), assigneeBotId: id.optional(), botName: id.optional(), directory: z.z.string().min(1).max(4096).optional(), assistant: z.z.enum(["claude", "codex", "pi-acp"]).optional() }),
|
|
468
|
+
z.z.object({ type: z.z.literal("message"), taskId: id, body: text, recipientBotId: id.optional() }),
|
|
469
|
+
z.z.object({ type: z.z.literal("submit"), taskId: id, attemptId: id, goalVersion: z.z.number().int().positive(), result: text }),
|
|
470
|
+
z.z.object({ type: z.z.literal("accept"), taskId: id, attemptId: id, goalVersion: z.z.number().int().positive() }),
|
|
471
|
+
z.z.object({ type: z.z.literal("return"), taskId: id, attemptId: id, goalVersion: z.z.number().int().positive(), reason: text }),
|
|
472
|
+
z.z.object({ type: z.z.literal("cancel"), taskId: id, attemptId: id, goalVersion: z.z.number().int().positive(), reason: text }),
|
|
473
|
+
z.z.object({ type: z.z.literal("handoff"), taskId: id, attemptId: id, goalVersion: z.z.number().int().positive(), assigneeBotId: id }),
|
|
474
|
+
z.z.object({ type: z.z.literal("claim-operation"), operationId: id, machineId: id }),
|
|
475
|
+
z.z.object({ type: z.z.literal("complete-operation"), operationId: id, machineId: id, claimId: id, sessionId: id.optional() }),
|
|
476
|
+
z.z.object({ type: z.z.literal("fail-operation"), operationId: id, machineId: id, claimId: id, error: text, unknown: z.z.boolean().optional() }),
|
|
477
|
+
z.z.object({ type: z.z.literal("session-event"), sessionId: id, event: z.z.enum(["idle", "blocked", "exited"]) }),
|
|
478
|
+
z.z.object({ type: z.z.literal("message-delivered"), messageId: id, recipientBotId: id, generation: z.z.number().int().positive(), sessionId: id })
|
|
479
|
+
]);
|
|
480
|
+
z.z.object({ requestId: id, action: TeamActionSchema });
|
|
481
|
+
z.z.object({ name: id, machineId: id, requestId: id.optional() });
|
|
1051
482
|
|
|
1052
483
|
z.z.object({
|
|
484
|
+
teamsVersion: z.z.number().int().positive().optional(),
|
|
1053
485
|
host: z.z.string(),
|
|
1054
486
|
platform: z.z.string(),
|
|
1055
487
|
happyCliVersion: z.z.string(),
|
|
@@ -1324,125 +756,6 @@ const AgentMessageSchema = z.z.object({
|
|
|
1324
756
|
});
|
|
1325
757
|
z.z.union([UserMessageSchema, AgentMessageSchema]);
|
|
1326
758
|
|
|
1327
|
-
function encodeBase64(buffer, variant = "base64") {
|
|
1328
|
-
if (variant === "base64url") {
|
|
1329
|
-
return encodeBase64Url(buffer);
|
|
1330
|
-
}
|
|
1331
|
-
return Buffer.from(buffer).toString("base64");
|
|
1332
|
-
}
|
|
1333
|
-
function encodeBase64Url(buffer) {
|
|
1334
|
-
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
1335
|
-
}
|
|
1336
|
-
function decodeBase64(base64, variant = "base64") {
|
|
1337
|
-
if (variant === "base64url") {
|
|
1338
|
-
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
|
|
1339
|
-
return new Uint8Array(Buffer.from(base64Standard, "base64"));
|
|
1340
|
-
}
|
|
1341
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
1342
|
-
}
|
|
1343
|
-
function getRandomBytes(size) {
|
|
1344
|
-
return new Uint8Array(node_crypto.randomBytes(size));
|
|
1345
|
-
}
|
|
1346
|
-
function libsodiumPublicKeyFromSecretKey(seed) {
|
|
1347
|
-
const hashedSeed = new Uint8Array(node_crypto.createHash("sha512").update(seed).digest());
|
|
1348
|
-
const secretKey = hashedSeed.slice(0, 32);
|
|
1349
|
-
return new Uint8Array(tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey);
|
|
1350
|
-
}
|
|
1351
|
-
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
1352
|
-
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
1353
|
-
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
|
|
1354
|
-
const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
|
|
1355
|
-
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
|
|
1356
|
-
result.set(ephemeralKeyPair.publicKey, 0);
|
|
1357
|
-
result.set(nonce, ephemeralKeyPair.publicKey.length);
|
|
1358
|
-
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
|
|
1359
|
-
return result;
|
|
1360
|
-
}
|
|
1361
|
-
function encryptLegacy(data, secret) {
|
|
1362
|
-
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
1363
|
-
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
1364
|
-
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
1365
|
-
result.set(nonce);
|
|
1366
|
-
result.set(encrypted, nonce.length);
|
|
1367
|
-
return result;
|
|
1368
|
-
}
|
|
1369
|
-
function decryptLegacy(data, secret) {
|
|
1370
|
-
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
1371
|
-
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
1372
|
-
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
1373
|
-
if (!decrypted) {
|
|
1374
|
-
return null;
|
|
1375
|
-
}
|
|
1376
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
1377
|
-
}
|
|
1378
|
-
function decryptBlob(bundle, key) {
|
|
1379
|
-
if (bundle.length < tweetnacl.secretbox.nonceLength + 16) {
|
|
1380
|
-
return null;
|
|
1381
|
-
}
|
|
1382
|
-
const nonce = bundle.slice(0, tweetnacl.secretbox.nonceLength);
|
|
1383
|
-
const ciphertext = bundle.slice(tweetnacl.secretbox.nonceLength);
|
|
1384
|
-
const decrypted = tweetnacl.secretbox.open(ciphertext, nonce, key);
|
|
1385
|
-
if (!decrypted) {
|
|
1386
|
-
return null;
|
|
1387
|
-
}
|
|
1388
|
-
return new Uint8Array(decrypted);
|
|
1389
|
-
}
|
|
1390
|
-
function encryptWithDataKey(data, dataKey) {
|
|
1391
|
-
const nonce = getRandomBytes(12);
|
|
1392
|
-
const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
|
|
1393
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
1394
|
-
const encrypted = Buffer.concat([
|
|
1395
|
-
cipher.update(plaintext),
|
|
1396
|
-
cipher.final()
|
|
1397
|
-
]);
|
|
1398
|
-
const authTag = cipher.getAuthTag();
|
|
1399
|
-
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
|
|
1400
|
-
bundle.set([0], 0);
|
|
1401
|
-
bundle.set(nonce, 1);
|
|
1402
|
-
bundle.set(new Uint8Array(encrypted), 13);
|
|
1403
|
-
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
1404
|
-
return bundle;
|
|
1405
|
-
}
|
|
1406
|
-
function decryptWithDataKey(bundle, dataKey) {
|
|
1407
|
-
if (bundle.length < 1) {
|
|
1408
|
-
return null;
|
|
1409
|
-
}
|
|
1410
|
-
if (bundle[0] !== 0) {
|
|
1411
|
-
return null;
|
|
1412
|
-
}
|
|
1413
|
-
if (bundle.length < 12 + 16 + 1) {
|
|
1414
|
-
return null;
|
|
1415
|
-
}
|
|
1416
|
-
const nonce = bundle.slice(1, 13);
|
|
1417
|
-
const authTag = bundle.slice(bundle.length - 16);
|
|
1418
|
-
const ciphertext = bundle.slice(13, bundle.length - 16);
|
|
1419
|
-
try {
|
|
1420
|
-
const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
|
|
1421
|
-
decipher.setAuthTag(authTag);
|
|
1422
|
-
const decrypted = Buffer.concat([
|
|
1423
|
-
decipher.update(ciphertext),
|
|
1424
|
-
decipher.final()
|
|
1425
|
-
]);
|
|
1426
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
1427
|
-
} catch (error) {
|
|
1428
|
-
return null;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
function encrypt(key, variant, data) {
|
|
1432
|
-
if (variant === "legacy") {
|
|
1433
|
-
return encryptLegacy(data, key);
|
|
1434
|
-
} else {
|
|
1435
|
-
return encryptWithDataKey(data, key);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
function decrypt(key, variant, data) {
|
|
1439
|
-
if (variant === "legacy") {
|
|
1440
|
-
return decryptLegacy(data, key);
|
|
1441
|
-
} else {
|
|
1442
|
-
return decryptWithDataKey(data, key);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
759
|
async function delay(ms) {
|
|
1447
760
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1448
761
|
}
|
|
@@ -1474,7 +787,7 @@ function createBackoff(opts) {
|
|
|
1474
787
|
}
|
|
1475
788
|
let backoff = createBackoff({
|
|
1476
789
|
onError: (e, failuresCount) => {
|
|
1477
|
-
logger.debug(`[BACKOFF] retry ${failuresCount}:`, e?.message || e);
|
|
790
|
+
persistence.logger.debug(`[BACKOFF] retry ${failuresCount}:`, e?.message || e);
|
|
1478
791
|
}
|
|
1479
792
|
});
|
|
1480
793
|
|
|
@@ -1572,7 +885,7 @@ class RpcHandlerManager {
|
|
|
1572
885
|
this.scopePrefix = config.scopePrefix;
|
|
1573
886
|
this.encryptionKey = config.encryptionKey;
|
|
1574
887
|
this.encryptionVariant = config.encryptionVariant;
|
|
1575
|
-
this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
|
|
888
|
+
this.logger = config.logger || ((msg, data) => persistence.logger.debug(msg, data));
|
|
1576
889
|
}
|
|
1577
890
|
/**
|
|
1578
891
|
* Register an RPC handler for a specific method
|
|
@@ -1600,14 +913,14 @@ class RpcHandlerManager {
|
|
|
1600
913
|
if (!handler) {
|
|
1601
914
|
this.logger("[RPC] [ERROR] Method not found", { method: request.method });
|
|
1602
915
|
const errorResponse = { error: "Method not found" };
|
|
1603
|
-
const encryptedError = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
|
|
916
|
+
const encryptedError = persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
|
|
1604
917
|
return encryptedError;
|
|
1605
918
|
}
|
|
1606
|
-
const decryptedParams = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(request.params));
|
|
919
|
+
const decryptedParams = persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(request.params));
|
|
1607
920
|
this.logger("[RPC] Calling handler", { method: request.method });
|
|
1608
921
|
const result = await handler(decryptedParams);
|
|
1609
922
|
this.logger("[RPC] Handler returned", { method: request.method, hasResult: result !== void 0 });
|
|
1610
|
-
const encryptedResponse = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, result));
|
|
923
|
+
const encryptedResponse = persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, result));
|
|
1611
924
|
this.logger("[RPC] Sending encrypted response", { method: request.method, responseLength: encryptedResponse.length });
|
|
1612
925
|
return encryptedResponse;
|
|
1613
926
|
} catch (error) {
|
|
@@ -1615,7 +928,7 @@ class RpcHandlerManager {
|
|
|
1615
928
|
const errorResponse = {
|
|
1616
929
|
error: error instanceof Error ? error.message : "Unknown error"
|
|
1617
930
|
};
|
|
1618
|
-
return encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
|
|
931
|
+
return persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
|
|
1619
932
|
}
|
|
1620
933
|
}
|
|
1621
934
|
onSocketConnect(socket) {
|
|
@@ -1708,14 +1021,14 @@ class RpcHandlerManager {
|
|
|
1708
1021
|
}
|
|
1709
1022
|
}
|
|
1710
1023
|
|
|
1711
|
-
const __dirname$1 = path
|
|
1024
|
+
const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-BGHnQXl5.cjs', document.baseURI).href))));
|
|
1712
1025
|
function projectPath() {
|
|
1713
|
-
const path = path
|
|
1714
|
-
return path;
|
|
1026
|
+
const path$1 = path.resolve(__dirname$1, "..");
|
|
1027
|
+
return path$1;
|
|
1715
1028
|
}
|
|
1716
1029
|
|
|
1717
1030
|
function run$1(args, options) {
|
|
1718
|
-
const RUNNER_PATH = path
|
|
1031
|
+
const RUNNER_PATH = path.resolve(path.join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
1719
1032
|
return new Promise((resolve2, reject) => {
|
|
1720
1033
|
const child = spawn.spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
|
|
1721
1034
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1744,9 +1057,9 @@ function run$1(args, options) {
|
|
|
1744
1057
|
}
|
|
1745
1058
|
|
|
1746
1059
|
function getBinaryPath() {
|
|
1747
|
-
const platformName = os
|
|
1060
|
+
const platformName = os.platform();
|
|
1748
1061
|
const binaryName = platformName === "win32" ? "difft.exe" : "difft";
|
|
1749
|
-
return path
|
|
1062
|
+
return path.resolve(path.join(projectPath(), "tools", "unpacked", binaryName));
|
|
1750
1063
|
}
|
|
1751
1064
|
function run(args, options) {
|
|
1752
1065
|
const binaryPath = getBinaryPath();
|
|
@@ -1783,9 +1096,9 @@ function run(args, options) {
|
|
|
1783
1096
|
}
|
|
1784
1097
|
|
|
1785
1098
|
function validatePath(targetPath, workingDirectory) {
|
|
1786
|
-
const resolvedTarget = path
|
|
1787
|
-
const resolvedWorkingDir = path
|
|
1788
|
-
if (!resolvedTarget.startsWith(resolvedWorkingDir + path
|
|
1099
|
+
const resolvedTarget = path.resolve(workingDirectory, targetPath);
|
|
1100
|
+
const resolvedWorkingDir = path.resolve(workingDirectory);
|
|
1101
|
+
if (!resolvedTarget.startsWith(resolvedWorkingDir + path.sep) && resolvedTarget !== resolvedWorkingDir) {
|
|
1789
1102
|
return {
|
|
1790
1103
|
valid: false,
|
|
1791
1104
|
resolvedPath: resolvedTarget,
|
|
@@ -1858,9 +1171,9 @@ function createUploadFileHandlers(happyHomeDir, now = Date.now, pathQuoteStyle =
|
|
|
1858
1171
|
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1859
1172
|
await Promise.all(entries.map(async (entry) => {
|
|
1860
1173
|
if (!entry.isFile() || !/^\..+\.[A-Za-z0-9_-]{8,80}\.part$/.test(entry.name)) return;
|
|
1861
|
-
const path = path
|
|
1862
|
-
const fileStat = await fs.stat(path).catch(() => null);
|
|
1863
|
-
if (fileStat && fileStat.mtimeMs < cutoff) await fs.unlink(path).catch(() => {
|
|
1174
|
+
const path$1 = path.join(dir, entry.name);
|
|
1175
|
+
const fileStat = await fs.stat(path$1).catch(() => null);
|
|
1176
|
+
if (fileStat && fileStat.mtimeMs < cutoff) await fs.unlink(path$1).catch(() => {
|
|
1864
1177
|
});
|
|
1865
1178
|
}));
|
|
1866
1179
|
};
|
|
@@ -1868,12 +1181,12 @@ function createUploadFileHandlers(happyHomeDir, now = Date.now, pathQuoteStyle =
|
|
|
1868
1181
|
let tempPath = null;
|
|
1869
1182
|
try {
|
|
1870
1183
|
const buffer = decodeUploadBase64(data.content, MAX_USER_UPLOAD_BYTES);
|
|
1871
|
-
const dir = path
|
|
1184
|
+
const dir = path.join(happyHomeDir, "uploads", safeSegment(data.subdir, "misc", 80));
|
|
1872
1185
|
await fs.mkdir(dir, { recursive: true });
|
|
1873
1186
|
const name = safeFileName(data.name);
|
|
1874
1187
|
const token = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
1875
|
-
const target = path
|
|
1876
|
-
tempPath = path
|
|
1188
|
+
const target = path.join(dir, uniqueFileName(name, token));
|
|
1189
|
+
tempPath = path.join(dir, `.${name}.legacy_${crypto.randomUUID().replace(/-/g, "")}.part`);
|
|
1877
1190
|
await fs.writeFile(tempPath, buffer, { flag: "wx" });
|
|
1878
1191
|
await fs.rename(tempPath, target);
|
|
1879
1192
|
tempPath = null;
|
|
@@ -1894,12 +1207,12 @@ function createUploadFileHandlers(happyHomeDir, now = Date.now, pathQuoteStyle =
|
|
|
1894
1207
|
}
|
|
1895
1208
|
if (active.has(uploadId)) throw new Error("Upload id is already active");
|
|
1896
1209
|
if (active.size >= MAX_ACTIVE_UPLOADS) throw new Error("Too many active uploads");
|
|
1897
|
-
const dir = path
|
|
1210
|
+
const dir = path.join(happyHomeDir, "uploads", safeSegment(data.subdir, "misc", 80));
|
|
1898
1211
|
await fs.mkdir(dir, { recursive: true });
|
|
1899
1212
|
await sweepExpiredFiles(dir);
|
|
1900
1213
|
const name = safeFileName(data.name);
|
|
1901
|
-
const targetPath = path
|
|
1902
|
-
const tempPath = path
|
|
1214
|
+
const targetPath = path.join(dir, uniqueFileName(name, crypto.randomUUID().replace(/-/g, "").slice(0, 12)));
|
|
1215
|
+
const tempPath = path.join(dir, `.${name}.${uploadId}.part`);
|
|
1903
1216
|
await fs.writeFile(tempPath, Buffer.alloc(0), { flag: "wx" });
|
|
1904
1217
|
active.set(uploadId, { tempPath, targetPath, totalSize: data.totalSize, received: 0, touchedAt: now() });
|
|
1905
1218
|
return { success: true, size: 0 };
|
|
@@ -1945,7 +1258,7 @@ function createUploadFileHandlers(happyHomeDir, now = Date.now, pathQuoteStyle =
|
|
|
1945
1258
|
const execAsync = util.promisify(child_process.exec);
|
|
1946
1259
|
function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
1947
1260
|
rpcHandlerManager.registerHandler("bash", async (data) => {
|
|
1948
|
-
logger.debug("Shell command request:", data.command);
|
|
1261
|
+
persistence.logger.debug("Shell command request:", data.command);
|
|
1949
1262
|
if (data.cwd && data.cwd !== "/") {
|
|
1950
1263
|
const validation = validatePath(data.cwd, workingDirectory);
|
|
1951
1264
|
if (!validation.valid) {
|
|
@@ -1961,16 +1274,16 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
1961
1274
|
windowsHide: true
|
|
1962
1275
|
// Prevent cmd.exe popup on Windows for every RPC bash call
|
|
1963
1276
|
};
|
|
1964
|
-
logger.debug("Shell command executing...", { cwd: options.cwd, timeout: options.timeout });
|
|
1277
|
+
persistence.logger.debug("Shell command executing...", { cwd: options.cwd, timeout: options.timeout });
|
|
1965
1278
|
const { stdout, stderr } = await execAsync(data.command, options);
|
|
1966
|
-
logger.debug("Shell command executed, processing result...");
|
|
1279
|
+
persistence.logger.debug("Shell command executed, processing result...");
|
|
1967
1280
|
const result = {
|
|
1968
1281
|
success: true,
|
|
1969
1282
|
stdout: stdout ? stdout.toString() : "",
|
|
1970
1283
|
stderr: stderr ? stderr.toString() : "",
|
|
1971
1284
|
exitCode: 0
|
|
1972
1285
|
};
|
|
1973
|
-
logger.debug("Shell command result:", {
|
|
1286
|
+
persistence.logger.debug("Shell command result:", {
|
|
1974
1287
|
success: true,
|
|
1975
1288
|
exitCode: 0,
|
|
1976
1289
|
stdoutLen: result.stdout.length,
|
|
@@ -1987,7 +1300,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
1987
1300
|
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
1988
1301
|
error: "Command timed out"
|
|
1989
1302
|
};
|
|
1990
|
-
logger.debug("Shell command timed out:", {
|
|
1303
|
+
persistence.logger.debug("Shell command timed out:", {
|
|
1991
1304
|
success: false,
|
|
1992
1305
|
exitCode: result2.exitCode,
|
|
1993
1306
|
error: "Command timed out"
|
|
@@ -2001,7 +1314,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2001
1314
|
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
2002
1315
|
error: execError.message || "Command failed"
|
|
2003
1316
|
};
|
|
2004
|
-
logger.debug("Shell command failed:", {
|
|
1317
|
+
persistence.logger.debug("Shell command failed:", {
|
|
2005
1318
|
success: false,
|
|
2006
1319
|
exitCode: result.exitCode,
|
|
2007
1320
|
error: result.error,
|
|
@@ -2012,7 +1325,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2012
1325
|
}
|
|
2013
1326
|
});
|
|
2014
1327
|
rpcHandlerManager.registerHandler("readFile", async (data) => {
|
|
2015
|
-
logger.debug("Read file request:", data.path);
|
|
1328
|
+
persistence.logger.debug("Read file request:", data.path);
|
|
2016
1329
|
const validation = validatePath(data.path, workingDirectory);
|
|
2017
1330
|
if (!validation.valid) {
|
|
2018
1331
|
return { success: false, error: validation.error };
|
|
@@ -2022,12 +1335,12 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2022
1335
|
const content = buffer.toString("base64");
|
|
2023
1336
|
return { success: true, content };
|
|
2024
1337
|
} catch (error) {
|
|
2025
|
-
logger.debug("Failed to read file:", error);
|
|
1338
|
+
persistence.logger.debug("Failed to read file:", error);
|
|
2026
1339
|
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
2027
1340
|
}
|
|
2028
1341
|
});
|
|
2029
1342
|
rpcHandlerManager.registerHandler("writeFile", async (data) => {
|
|
2030
|
-
logger.debug("Write file request:", data.path);
|
|
1343
|
+
persistence.logger.debug("Write file request:", data.path);
|
|
2031
1344
|
const validation = validatePath(data.path, workingDirectory);
|
|
2032
1345
|
if (!validation.valid) {
|
|
2033
1346
|
return { success: false, error: validation.error };
|
|
@@ -2072,15 +1385,15 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2072
1385
|
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
2073
1386
|
return { success: true, hash };
|
|
2074
1387
|
} catch (error) {
|
|
2075
|
-
logger.debug("Failed to write file:", error);
|
|
1388
|
+
persistence.logger.debug("Failed to write file:", error);
|
|
2076
1389
|
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
2077
1390
|
}
|
|
2078
1391
|
});
|
|
2079
|
-
const uploadHandlers = createUploadFileHandlers(configuration.happyHomeDir);
|
|
1392
|
+
const uploadHandlers = createUploadFileHandlers(persistence.configuration.happyHomeDir);
|
|
2080
1393
|
rpcHandlerManager.registerHandler("uploadFile", uploadHandlers.uploadFile);
|
|
2081
1394
|
rpcHandlerManager.registerHandler("uploadFileChunk", uploadHandlers.uploadFileChunk);
|
|
2082
1395
|
rpcHandlerManager.registerHandler("listDirectory", async (data) => {
|
|
2083
|
-
logger.debug("List directory request:", data.path);
|
|
1396
|
+
persistence.logger.debug("List directory request:", data.path);
|
|
2084
1397
|
const validation = validatePath(data.path, workingDirectory);
|
|
2085
1398
|
if (!validation.valid) {
|
|
2086
1399
|
return { success: false, error: validation.error };
|
|
@@ -2090,7 +1403,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2090
1403
|
const entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
2091
1404
|
const directoryEntries = await Promise.all(
|
|
2092
1405
|
entries.map(async (entry) => {
|
|
2093
|
-
const fullPath = path
|
|
1406
|
+
const fullPath = path.join(directoryPath, entry.name);
|
|
2094
1407
|
let type = "other";
|
|
2095
1408
|
let size;
|
|
2096
1409
|
let modified;
|
|
@@ -2104,7 +1417,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2104
1417
|
size = stats.size;
|
|
2105
1418
|
modified = stats.mtime.getTime();
|
|
2106
1419
|
} catch (error) {
|
|
2107
|
-
logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
1420
|
+
persistence.logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
2108
1421
|
}
|
|
2109
1422
|
return {
|
|
2110
1423
|
name: entry.name,
|
|
@@ -2121,36 +1434,36 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2121
1434
|
});
|
|
2122
1435
|
return { success: true, entries: directoryEntries };
|
|
2123
1436
|
} catch (error) {
|
|
2124
|
-
logger.debug("Failed to list directory:", error);
|
|
1437
|
+
persistence.logger.debug("Failed to list directory:", error);
|
|
2125
1438
|
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
2126
1439
|
}
|
|
2127
1440
|
});
|
|
2128
1441
|
rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
|
|
2129
|
-
logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1442
|
+
persistence.logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
2130
1443
|
const validation = validatePath(data.path, workingDirectory);
|
|
2131
1444
|
if (!validation.valid) {
|
|
2132
1445
|
return { success: false, error: validation.error };
|
|
2133
1446
|
}
|
|
2134
|
-
async function buildTree(path, name, currentDepth) {
|
|
1447
|
+
async function buildTree(path$1, name, currentDepth) {
|
|
2135
1448
|
try {
|
|
2136
|
-
const stats = await fs.stat(path);
|
|
1449
|
+
const stats = await fs.stat(path$1);
|
|
2137
1450
|
const node = {
|
|
2138
1451
|
name,
|
|
2139
|
-
path,
|
|
1452
|
+
path: path$1,
|
|
2140
1453
|
type: stats.isDirectory() ? "directory" : "file",
|
|
2141
1454
|
size: stats.size,
|
|
2142
1455
|
modified: stats.mtime.getTime()
|
|
2143
1456
|
};
|
|
2144
1457
|
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
2145
|
-
const entries = await fs.readdir(path, { withFileTypes: true });
|
|
1458
|
+
const entries = await fs.readdir(path$1, { withFileTypes: true });
|
|
2146
1459
|
const children = [];
|
|
2147
1460
|
await Promise.all(
|
|
2148
1461
|
entries.map(async (entry) => {
|
|
2149
1462
|
if (entry.isSymbolicLink()) {
|
|
2150
|
-
logger.debug(`Skipping symlink: ${path
|
|
1463
|
+
persistence.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
|
|
2151
1464
|
return;
|
|
2152
1465
|
}
|
|
2153
|
-
const childPath = path
|
|
1466
|
+
const childPath = path.join(path$1, entry.name);
|
|
2154
1467
|
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
2155
1468
|
if (childNode) {
|
|
2156
1469
|
children.push(childNode);
|
|
@@ -2166,7 +1479,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2166
1479
|
}
|
|
2167
1480
|
return node;
|
|
2168
1481
|
} catch (error) {
|
|
2169
|
-
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
|
|
1482
|
+
persistence.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
|
|
2170
1483
|
return null;
|
|
2171
1484
|
}
|
|
2172
1485
|
}
|
|
@@ -2182,12 +1495,12 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2182
1495
|
}
|
|
2183
1496
|
return { success: true, tree };
|
|
2184
1497
|
} catch (error) {
|
|
2185
|
-
logger.debug("Failed to get directory tree:", error);
|
|
1498
|
+
persistence.logger.debug("Failed to get directory tree:", error);
|
|
2186
1499
|
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
2187
1500
|
}
|
|
2188
1501
|
});
|
|
2189
1502
|
rpcHandlerManager.registerHandler("ripgrep", async (data) => {
|
|
2190
|
-
logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
1503
|
+
persistence.logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
2191
1504
|
if (data.cwd) {
|
|
2192
1505
|
const validation = validatePath(data.cwd, workingDirectory);
|
|
2193
1506
|
if (!validation.valid) {
|
|
@@ -2204,7 +1517,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2204
1517
|
stderr: result.stderr.toString()
|
|
2205
1518
|
};
|
|
2206
1519
|
} catch (error) {
|
|
2207
|
-
logger.debug("Failed to run ripgrep:", error);
|
|
1520
|
+
persistence.logger.debug("Failed to run ripgrep:", error);
|
|
2208
1521
|
return {
|
|
2209
1522
|
success: false,
|
|
2210
1523
|
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
@@ -2212,7 +1525,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2212
1525
|
}
|
|
2213
1526
|
});
|
|
2214
1527
|
rpcHandlerManager.registerHandler("difftastic", async (data) => {
|
|
2215
|
-
logger.debug("Difftastic request with args:", data.args, "cwd:", data.cwd);
|
|
1528
|
+
persistence.logger.debug("Difftastic request with args:", data.args, "cwd:", data.cwd);
|
|
2216
1529
|
if (data.cwd) {
|
|
2217
1530
|
const validation = validatePath(data.cwd, workingDirectory);
|
|
2218
1531
|
if (!validation.valid) {
|
|
@@ -2229,7 +1542,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2229
1542
|
stderr: result.stderr.toString()
|
|
2230
1543
|
};
|
|
2231
1544
|
} catch (error) {
|
|
2232
|
-
logger.debug("Failed to run difftastic:", error);
|
|
1545
|
+
persistence.logger.debug("Failed to run difftastic:", error);
|
|
2233
1546
|
return {
|
|
2234
1547
|
success: false,
|
|
2235
1548
|
error: error instanceof Error ? error.message : "Failed to run difftastic"
|
|
@@ -2312,7 +1625,7 @@ function calculateCost(usage, modelId) {
|
|
|
2312
1625
|
}
|
|
2313
1626
|
|
|
2314
1627
|
function hasNetworkConnectivity() {
|
|
2315
|
-
const interfaces = os
|
|
1628
|
+
const interfaces = os.networkInterfaces();
|
|
2316
1629
|
for (const name of Object.keys(interfaces)) {
|
|
2317
1630
|
for (const iface of interfaces[name] || []) {
|
|
2318
1631
|
if (!iface.internal && iface.family === "IPv4") return true;
|
|
@@ -3326,13 +2639,13 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3326
2639
|
scopePrefix: this.sessionId,
|
|
3327
2640
|
encryptionKey: this.encryptionKey,
|
|
3328
2641
|
encryptionVariant: this.encryptionVariant,
|
|
3329
|
-
logger: (msg, data) => logger.debug(msg, data)
|
|
2642
|
+
logger: (msg, data) => persistence.logger.debug(msg, data)
|
|
3330
2643
|
});
|
|
3331
2644
|
registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
|
|
3332
2645
|
this.socket = this.createControlSocket();
|
|
3333
2646
|
const controlSocket = this.socket;
|
|
3334
2647
|
this.socket.on("connect", () => {
|
|
3335
|
-
logger.debug("Socket connected successfully");
|
|
2648
|
+
persistence.logger.debug("Socket connected successfully");
|
|
3336
2649
|
if (this.reconnectInterval) {
|
|
3337
2650
|
clearInterval(this.reconnectInterval);
|
|
3338
2651
|
this.reconnectInterval = null;
|
|
@@ -3345,15 +2658,15 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3345
2658
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
3346
2659
|
});
|
|
3347
2660
|
this.socket.on("disconnect", (reason) => {
|
|
3348
|
-
logger.debug(`[API] Socket disconnected: ${reason}`);
|
|
2661
|
+
persistence.logger.debug(`[API] Socket disconnected: ${reason}`);
|
|
3349
2662
|
this.rpcHandlerManager.onSocketDisconnect(controlSocket);
|
|
3350
2663
|
this.startSmartReconnect();
|
|
3351
2664
|
});
|
|
3352
2665
|
this.socket.on("connect_error", (error) => {
|
|
3353
|
-
logger.debug("[API] Socket connection error:", error);
|
|
2666
|
+
persistence.logger.debug("[API] Socket connection error:", error);
|
|
3354
2667
|
this.rpcHandlerManager.onSocketDisconnect(controlSocket);
|
|
3355
2668
|
if (error.message === "Session archived") {
|
|
3356
|
-
logger.debug("[API] Archived session rejected during reconnect; exiting");
|
|
2669
|
+
persistence.logger.debug("[API] Archived session rejected during reconnect; exiting");
|
|
3357
2670
|
this.emit("archived");
|
|
3358
2671
|
return;
|
|
3359
2672
|
}
|
|
@@ -3361,14 +2674,14 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3361
2674
|
});
|
|
3362
2675
|
this.socket.on("session-archive", ({ sessionId }) => {
|
|
3363
2676
|
if (sessionId !== this.sessionId) return;
|
|
3364
|
-
logger.debug("[SOCKET] Server archived this session; exiting...");
|
|
2677
|
+
persistence.logger.debug("[SOCKET] Server archived this session; exiting...");
|
|
3365
2678
|
this.emit("archived");
|
|
3366
2679
|
});
|
|
3367
2680
|
this.socket.on("update", (data) => {
|
|
3368
2681
|
try {
|
|
3369
|
-
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
|
|
2682
|
+
persistence.logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
|
|
3370
2683
|
if (!data.body) {
|
|
3371
|
-
logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
|
|
2684
|
+
persistence.logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
|
|
3372
2685
|
return;
|
|
3373
2686
|
}
|
|
3374
2687
|
if (data.body.t === "new-message") {
|
|
@@ -3386,41 +2699,41 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3386
2699
|
this.lastSeq = messageSeq;
|
|
3387
2700
|
return;
|
|
3388
2701
|
}
|
|
3389
|
-
const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
|
|
3390
|
-
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
2702
|
+
const body = persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(data.body.message.content.c));
|
|
2703
|
+
persistence.logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
3391
2704
|
this.routeIncomingMessage(body, localId);
|
|
3392
2705
|
if (localId) this.rememberRoutedInbound(localId);
|
|
3393
2706
|
this.lastSeq = messageSeq;
|
|
3394
2707
|
} else if (data.body.t === "update-session") {
|
|
3395
2708
|
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
|
|
3396
|
-
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.metadata.value));
|
|
2709
|
+
this.metadata = persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(data.body.metadata.value));
|
|
3397
2710
|
this.metadataVersion = data.body.metadata.version;
|
|
3398
2711
|
const meta = this.metadata;
|
|
3399
2712
|
if (meta?.lifecycleState === "archiveRequested" || meta?.lifecycleState === "archived") {
|
|
3400
2713
|
if (this.ignoreArchiveSignal) {
|
|
3401
|
-
logger.debug(`[SOCKET] Session archived (${meta.lifecycleState}) but suppressed for reconnect`);
|
|
2714
|
+
persistence.logger.debug(`[SOCKET] Session archived (${meta.lifecycleState}) but suppressed for reconnect`);
|
|
3402
2715
|
this.ignoreArchiveSignal = false;
|
|
3403
2716
|
} else {
|
|
3404
|
-
logger.debug(`[SOCKET] Session archived (${meta.lifecycleState}), exiting...`);
|
|
2717
|
+
persistence.logger.debug(`[SOCKET] Session archived (${meta.lifecycleState}), exiting...`);
|
|
3405
2718
|
this.emit("archived");
|
|
3406
2719
|
}
|
|
3407
2720
|
}
|
|
3408
2721
|
}
|
|
3409
2722
|
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
|
|
3410
|
-
this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.agentState.value)) : null;
|
|
2723
|
+
this.agentState = data.body.agentState.value ? persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(data.body.agentState.value)) : null;
|
|
3411
2724
|
this.agentStateVersion = data.body.agentState.version;
|
|
3412
2725
|
}
|
|
3413
2726
|
} else if (data.body.t === "update-machine") {
|
|
3414
|
-
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
|
|
2727
|
+
persistence.logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
|
|
3415
2728
|
} else {
|
|
3416
2729
|
this.emit("message", data.body);
|
|
3417
2730
|
}
|
|
3418
2731
|
} catch (error) {
|
|
3419
|
-
logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
|
|
2732
|
+
persistence.logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
|
|
3420
2733
|
}
|
|
3421
2734
|
});
|
|
3422
2735
|
this.socket.on("error", (error) => {
|
|
3423
|
-
logger.debug("[API] Socket error:", error);
|
|
2736
|
+
persistence.logger.debug("[API] Socket error:", error);
|
|
3424
2737
|
});
|
|
3425
2738
|
this.socket.on("server-draining", (data) => {
|
|
3426
2739
|
const parsed = ReleaseDrainNoticeSchema.safeParse(data);
|
|
@@ -3430,12 +2743,12 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3430
2743
|
this.socket.connect();
|
|
3431
2744
|
}
|
|
3432
2745
|
createControlSocket(handover) {
|
|
3433
|
-
return socket_ioClient.io(configuration.serverUrl, {
|
|
2746
|
+
return socket_ioClient.io(persistence.configuration.serverUrl, {
|
|
3434
2747
|
auth: {
|
|
3435
2748
|
token: this.token,
|
|
3436
2749
|
clientType: "session-scoped",
|
|
3437
2750
|
sessionId: this.sessionId,
|
|
3438
|
-
happyClient: `cli-coding-session/${configuration.currentCliVersion}`,
|
|
2751
|
+
happyClient: `cli-coding-session/${persistence.configuration.currentCliVersion}`,
|
|
3439
2752
|
...handover ? { handoverEpoch: handover.epoch } : {}
|
|
3440
2753
|
},
|
|
3441
2754
|
...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
|
|
@@ -3516,7 +2829,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3516
2829
|
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
3517
2830
|
candidate.close();
|
|
3518
2831
|
if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
|
|
3519
|
-
logger.debug(`[API] Release handover failed: ${error instanceof Error ? error.message : error}`);
|
|
2832
|
+
persistence.logger.debug(`[API] Release handover failed: ${error instanceof Error ? error.message : error}`);
|
|
3520
2833
|
}
|
|
3521
2834
|
}
|
|
3522
2835
|
onUserMessage(callback) {
|
|
@@ -3550,7 +2863,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3550
2863
|
* presigned URL that does not accept extra headers.
|
|
3551
2864
|
*/
|
|
3552
2865
|
async downloadAttachment(ref) {
|
|
3553
|
-
const requestUrl = `${configuration.serverUrl}/v1/sessions/${this.sessionId}/attachments/request-download`;
|
|
2866
|
+
const requestUrl = `${persistence.configuration.serverUrl}/v1/sessions/${this.sessionId}/attachments/request-download`;
|
|
3554
2867
|
const requestRes = await axios.post(
|
|
3555
2868
|
requestUrl,
|
|
3556
2869
|
{ ref },
|
|
@@ -3563,7 +2876,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3563
2876
|
if (typeof downloadUrl !== "string") {
|
|
3564
2877
|
throw new Error("request-download returned no downloadUrl");
|
|
3565
2878
|
}
|
|
3566
|
-
const isServerUrl = downloadUrl.startsWith(configuration.serverUrl);
|
|
2879
|
+
const isServerUrl = downloadUrl.startsWith(persistence.configuration.serverUrl);
|
|
3567
2880
|
const headers = {};
|
|
3568
2881
|
if (isServerUrl) {
|
|
3569
2882
|
headers["Authorization"] = `Bearer ${this.token}`;
|
|
@@ -3585,7 +2898,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3585
2898
|
async downloadAndDecryptAttachment(ref) {
|
|
3586
2899
|
const encrypted = await this.downloadAttachment(ref);
|
|
3587
2900
|
const key = await this.getBlobKey();
|
|
3588
|
-
const decrypted = decryptBlob(encrypted, key);
|
|
2901
|
+
const decrypted = persistence.decryptBlob(encrypted, key);
|
|
3589
2902
|
return decrypted;
|
|
3590
2903
|
}
|
|
3591
2904
|
/**
|
|
@@ -3614,7 +2927,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3614
2927
|
return {
|
|
3615
2928
|
"Authorization": `Bearer ${this.token}`,
|
|
3616
2929
|
"Content-Type": "application/json",
|
|
3617
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
2930
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
3618
2931
|
};
|
|
3619
2932
|
}
|
|
3620
2933
|
routeIncomingMessage(message, sourceLocalId) {
|
|
@@ -3630,7 +2943,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3630
2943
|
}
|
|
3631
2944
|
const fileResult = FileEventMessageSchema.safeParse(message);
|
|
3632
2945
|
if (fileResult.success) {
|
|
3633
|
-
logger.debug(`[API] Received file event: ${fileResult.data.content.data.ev.name} (ref: ${fileResult.data.content.data.ev.ref})`);
|
|
2946
|
+
persistence.logger.debug(`[API] Received file event: ${fileResult.data.content.data.ev.name} (ref: ${fileResult.data.content.data.ev.ref})`);
|
|
3634
2947
|
if (this.pendingFileEventCallback) {
|
|
3635
2948
|
this.pendingFileEventCallback(fileResult.data);
|
|
3636
2949
|
} else {
|
|
@@ -3644,12 +2957,12 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3644
2957
|
const skipRouting = this.skipInitialMessages;
|
|
3645
2958
|
if (skipRouting) {
|
|
3646
2959
|
this.skipInitialMessages = false;
|
|
3647
|
-
logger.debug("[API] Reconnect mode: skipping existing messages, advancing lastSeq");
|
|
2960
|
+
persistence.logger.debug("[API] Reconnect mode: skipping existing messages, advancing lastSeq");
|
|
3648
2961
|
}
|
|
3649
2962
|
let afterSeq = this.lastSeq;
|
|
3650
2963
|
while (true) {
|
|
3651
2964
|
const response = await axios.get(
|
|
3652
|
-
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
|
|
2965
|
+
`${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
|
|
3653
2966
|
{
|
|
3654
2967
|
params: {
|
|
3655
2968
|
after_seq: afterSeq,
|
|
@@ -3676,11 +2989,11 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3676
2989
|
continue;
|
|
3677
2990
|
}
|
|
3678
2991
|
try {
|
|
3679
|
-
const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(message.content.c));
|
|
2992
|
+
const body = persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(message.content.c));
|
|
3680
2993
|
this.routeIncomingMessage(body, message.localId);
|
|
3681
2994
|
if (message.localId) this.rememberRoutedInbound(message.localId);
|
|
3682
2995
|
} catch (error) {
|
|
3683
|
-
logger.debug("[API] Failed to decrypt fetched message", {
|
|
2996
|
+
persistence.logger.debug("[API] Failed to decrypt fetched message", {
|
|
3684
2997
|
sessionId: this.sessionId,
|
|
3685
2998
|
seq: message.seq,
|
|
3686
2999
|
error
|
|
@@ -3690,7 +3003,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3690
3003
|
this.lastSeq = Math.max(this.lastSeq, maxSeq);
|
|
3691
3004
|
const hasMore = !!response.data.hasMore;
|
|
3692
3005
|
if (hasMore && maxSeq === afterSeq) {
|
|
3693
|
-
logger.debug("[API] fetchMessages pagination stalled, stopping to avoid infinite loop", {
|
|
3006
|
+
persistence.logger.debug("[API] fetchMessages pagination stalled, stopping to avoid infinite loop", {
|
|
3694
3007
|
sessionId: this.sessionId,
|
|
3695
3008
|
afterSeq
|
|
3696
3009
|
});
|
|
@@ -3709,7 +3022,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3709
3022
|
const batchSize = Math.min(this.pendingOutbox.length, ApiSessionClient.MAX_OUTBOX_BATCH_SIZE);
|
|
3710
3023
|
const batch = this.pendingOutbox.slice(0, batchSize);
|
|
3711
3024
|
const response = await axios.post(
|
|
3712
|
-
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
|
|
3025
|
+
`${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
|
|
3713
3026
|
{
|
|
3714
3027
|
messages: batch
|
|
3715
3028
|
},
|
|
@@ -3744,7 +3057,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3744
3057
|
if (!machineId || this.relaySocket?.connected) return;
|
|
3745
3058
|
try {
|
|
3746
3059
|
const response = await fetch(
|
|
3747
|
-
`${configuration.serverUrl}/v1/relays/sessions/${encodeURIComponent(this.sessionId)}/machines/${encodeURIComponent(machineId)}`,
|
|
3060
|
+
`${persistence.configuration.serverUrl}/v1/relays/sessions/${encodeURIComponent(this.sessionId)}/machines/${encodeURIComponent(machineId)}`,
|
|
3748
3061
|
{ headers: this.authHeaders(), cache: "no-store" }
|
|
3749
3062
|
);
|
|
3750
3063
|
if (!response.ok) return;
|
|
@@ -3763,7 +3076,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3763
3076
|
relaySocket.on("connect", () => {
|
|
3764
3077
|
if (this.relaySocket !== relaySocket) return;
|
|
3765
3078
|
this.rpcHandlerManager.onSocketConnect(relaySocket);
|
|
3766
|
-
logger.debug(`[API] Session connected to regional relay ${assignment.relayId}`);
|
|
3079
|
+
persistence.logger.debug(`[API] Session connected to regional relay ${assignment.relayId}`);
|
|
3767
3080
|
});
|
|
3768
3081
|
relaySocket.on("rpc-request", async (data, callback) => {
|
|
3769
3082
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
@@ -3785,7 +3098,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3785
3098
|
for (const item of batch) this.rememberDirectInbound(item.localId);
|
|
3786
3099
|
try {
|
|
3787
3100
|
const persisted = await axios.post(
|
|
3788
|
-
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
|
|
3101
|
+
`${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
|
|
3789
3102
|
{ messages: batch },
|
|
3790
3103
|
{ headers: this.authHeaders(), timeout: 6e4 }
|
|
3791
3104
|
);
|
|
@@ -3797,7 +3110,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3797
3110
|
this.lastSeq = stored.reduce((max, message) => Math.max(max, message.seq), this.lastSeq);
|
|
3798
3111
|
for (const item of batch) {
|
|
3799
3112
|
if (this.routedInboundLocalIds.has(item.localId)) continue;
|
|
3800
|
-
const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(item.content));
|
|
3113
|
+
const body = persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(item.content));
|
|
3801
3114
|
this.routeIncomingMessage(body, item.localId);
|
|
3802
3115
|
this.rememberRoutedInbound(item.localId);
|
|
3803
3116
|
}
|
|
@@ -3811,7 +3124,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3811
3124
|
if (this.relaySocket === relaySocket) this.rpcHandlerManager.onSocketDisconnect(relaySocket);
|
|
3812
3125
|
});
|
|
3813
3126
|
} catch (error) {
|
|
3814
|
-
logger.debug(`[API] Session regional relay unavailable: ${error instanceof Error ? error.message : error}`);
|
|
3127
|
+
persistence.logger.debug(`[API] Session regional relay unavailable: ${error instanceof Error ? error.message : error}`);
|
|
3815
3128
|
}
|
|
3816
3129
|
}
|
|
3817
3130
|
pushCommittedMessagesToRelay(batch, messages) {
|
|
@@ -3831,7 +3144,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3831
3144
|
}
|
|
3832
3145
|
}
|
|
3833
3146
|
enqueueMessage(content, invalidate = true, localId) {
|
|
3834
|
-
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
|
|
3147
|
+
const encrypted = persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, content));
|
|
3835
3148
|
this.pendingOutbox.push({
|
|
3836
3149
|
content: encrypted,
|
|
3837
3150
|
// Deterministic localIds (terminal mirror, B-105) ride the server's
|
|
@@ -3873,7 +3186,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3873
3186
|
while (!reachedBoundary && pages < MAX_SCAN_PAGES) {
|
|
3874
3187
|
pages += 1;
|
|
3875
3188
|
const response = await axios.get(
|
|
3876
|
-
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
|
|
3189
|
+
`${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
|
|
3877
3190
|
{
|
|
3878
3191
|
params: { before_seq: beforeSeq, limit: 100 },
|
|
3879
3192
|
headers: this.authHeaders(),
|
|
@@ -3885,9 +3198,9 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3885
3198
|
const records = messages.map((message) => {
|
|
3886
3199
|
let body = null;
|
|
3887
3200
|
try {
|
|
3888
|
-
body = message.content?.t === "encrypted" ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(message.content.c)) : null;
|
|
3201
|
+
body = message.content?.t === "encrypted" ? persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(message.content.c)) : null;
|
|
3889
3202
|
} catch (error) {
|
|
3890
|
-
logger.debug("[API] Failed to decrypt during undelivered scan:", error);
|
|
3203
|
+
persistence.logger.debug("[API] Failed to decrypt during undelivered scan:", error);
|
|
3891
3204
|
}
|
|
3892
3205
|
return { seq: message.seq, localId: message.localId, body };
|
|
3893
3206
|
});
|
|
@@ -3900,11 +3213,11 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3900
3213
|
beforeSeq = oldest;
|
|
3901
3214
|
}
|
|
3902
3215
|
} catch (error) {
|
|
3903
|
-
logger.debug("[API] cancelUndeliveredQueuedInputs scan failed:", error);
|
|
3216
|
+
persistence.logger.debug("[API] cancelUndeliveredQueuedInputs scan failed:", error);
|
|
3904
3217
|
return;
|
|
3905
3218
|
}
|
|
3906
3219
|
if (!reachedBoundary) {
|
|
3907
|
-
logger.debug("[API] cancelUndeliveredQueuedInputs found no turn boundary; leaving messages as-is");
|
|
3220
|
+
persistence.logger.debug("[API] cancelUndeliveredQueuedInputs found no turn boundary; leaving messages as-is");
|
|
3908
3221
|
return;
|
|
3909
3222
|
}
|
|
3910
3223
|
undelivered.reverse();
|
|
@@ -3926,7 +3239,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3926
3239
|
const eventId = typeof body.uuid === "string" ? body.uuid : typeof body.message.id === "string" ? body.message.id : void 0;
|
|
3927
3240
|
this.sendUsageData(body.message.usage, body.message.model, eventId);
|
|
3928
3241
|
} catch (error) {
|
|
3929
|
-
logger.debug("[SOCKET] Failed to send usage data:", error);
|
|
3242
|
+
persistence.logger.debug("[SOCKET] Failed to send usage data:", error);
|
|
3930
3243
|
}
|
|
3931
3244
|
}
|
|
3932
3245
|
if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
|
|
@@ -3993,7 +3306,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
3993
3306
|
sentFrom: "cli"
|
|
3994
3307
|
}
|
|
3995
3308
|
};
|
|
3996
|
-
logger.debug(`[SOCKET] Sending ACP message from ${provider}:`, { type: body.type, hasMessage: "message" in body });
|
|
3309
|
+
persistence.logger.debug(`[SOCKET] Sending ACP message from ${provider}:`, { type: body.type, hasMessage: "message" in body });
|
|
3997
3310
|
this.enqueueMessage(content);
|
|
3998
3311
|
}
|
|
3999
3312
|
sendSessionEvent(event, id) {
|
|
@@ -4012,7 +3325,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4012
3325
|
*/
|
|
4013
3326
|
keepAlive(thinking, mode) {
|
|
4014
3327
|
if (process.env.DEBUG) {
|
|
4015
|
-
logger.debug(`[API] Sending keep alive message: ${thinking}`);
|
|
3328
|
+
persistence.logger.debug(`[API] Sending keep alive message: ${thinking}`);
|
|
4016
3329
|
}
|
|
4017
3330
|
this.socket.volatile.emit("session-alive", {
|
|
4018
3331
|
sid: this.sessionId,
|
|
@@ -4041,11 +3354,11 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4041
3354
|
// The frame object, NOT a JSON string: `encrypt` serializes
|
|
4042
3355
|
// internally, so pre-stringifying would double-encode and
|
|
4043
3356
|
// escape every quote in the streamed text.
|
|
4044
|
-
payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, frame)),
|
|
3357
|
+
payload: persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, frame)),
|
|
4045
3358
|
enc: true
|
|
4046
3359
|
});
|
|
4047
3360
|
} catch (error) {
|
|
4048
|
-
logger.debug(`[API] Failed to relay stream frame: ${error}`);
|
|
3361
|
+
persistence.logger.debug(`[API] Failed to relay stream frame: ${error}`);
|
|
4049
3362
|
}
|
|
4050
3363
|
}
|
|
4051
3364
|
/**
|
|
@@ -4062,12 +3375,12 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4062
3375
|
* Returns delivery info for the MCP tool to report back to the model.
|
|
4063
3376
|
*/
|
|
4064
3377
|
pushClipboard(text) {
|
|
4065
|
-
const prepared =
|
|
3378
|
+
const prepared = persistence.prepareClipboardText(text);
|
|
4066
3379
|
if (!this.socket.connected) {
|
|
4067
3380
|
return { delivered: false, truncated: prepared.truncated, totalBytes: prepared.totalBytes };
|
|
4068
3381
|
}
|
|
4069
3382
|
this.socket.emit("clipboard-push", {
|
|
4070
|
-
payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, prepared.text)),
|
|
3383
|
+
payload: persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, prepared.text)),
|
|
4071
3384
|
enc: true,
|
|
4072
3385
|
truncated: prepared.truncated,
|
|
4073
3386
|
totalBytes: prepared.totalBytes
|
|
@@ -4093,7 +3406,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4093
3406
|
return { delivered: false, error: "not connected to the server" };
|
|
4094
3407
|
}
|
|
4095
3408
|
this.socket.emit("file-preview-push", {
|
|
4096
|
-
payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, path)),
|
|
3409
|
+
payload: persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, path)),
|
|
4097
3410
|
enc: true,
|
|
4098
3411
|
mode
|
|
4099
3412
|
});
|
|
@@ -4133,7 +3446,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4133
3446
|
output: this.claudeUsageTotals.costOutput
|
|
4134
3447
|
}
|
|
4135
3448
|
};
|
|
4136
|
-
logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
|
|
3449
|
+
persistence.logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
|
|
4137
3450
|
this.socket.emit("usage-report", usageReport);
|
|
4138
3451
|
}
|
|
4139
3452
|
/** Save a provider's cumulative session snapshot under one upsert key. */
|
|
@@ -4148,7 +3461,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4148
3461
|
// The Web treats this sentinel as unknown, not as real $0.00.
|
|
4149
3462
|
cost: { total: 0 }
|
|
4150
3463
|
};
|
|
4151
|
-
logger.debugLargeJson("[SOCKET] Sending agent usage snapshot:", usageReport);
|
|
3464
|
+
persistence.logger.debugLargeJson("[SOCKET] Sending agent usage snapshot:", usageReport);
|
|
4152
3465
|
this.socket.emit("usage-report", usageReport);
|
|
4153
3466
|
return true;
|
|
4154
3467
|
}
|
|
@@ -4181,15 +3494,15 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4181
3494
|
let rateRefusals = 0;
|
|
4182
3495
|
await backoff(async () => {
|
|
4183
3496
|
let updated = handler(this.metadata);
|
|
4184
|
-
const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) });
|
|
3497
|
+
const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, updated)) });
|
|
4185
3498
|
if (answer.result === "success") {
|
|
4186
|
-
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
|
|
3499
|
+
this.metadata = persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(answer.metadata));
|
|
4187
3500
|
this.metadataVersion = answer.version;
|
|
4188
3501
|
rateRefusals = 0;
|
|
4189
3502
|
} else if (answer.result === "version-mismatch") {
|
|
4190
3503
|
if (answer.version > this.metadataVersion) {
|
|
4191
3504
|
this.metadataVersion = answer.version;
|
|
4192
|
-
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
|
|
3505
|
+
this.metadata = persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(answer.metadata));
|
|
4193
3506
|
}
|
|
4194
3507
|
throw new Error("Metadata version mismatch");
|
|
4195
3508
|
} else if (answer.result === "error") {
|
|
@@ -4200,7 +3513,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4200
3513
|
code: answer.error,
|
|
4201
3514
|
attempt: rateRefusals,
|
|
4202
3515
|
sleep: delay,
|
|
4203
|
-
log: (message) => logger.debug(message)
|
|
3516
|
+
log: (message) => persistence.logger.debug(message)
|
|
4204
3517
|
});
|
|
4205
3518
|
}
|
|
4206
3519
|
throw new Error("Metadata update failed");
|
|
@@ -4213,21 +3526,21 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4213
3526
|
* @param handler - Handler function that returns the updated agent state
|
|
4214
3527
|
*/
|
|
4215
3528
|
updateAgentState(handler) {
|
|
4216
|
-
logger.debugLargeJson("Updating agent state", this.agentState);
|
|
3529
|
+
persistence.logger.debugLargeJson("Updating agent state", this.agentState);
|
|
4217
3530
|
this.agentStateLock.inLock(async () => {
|
|
4218
3531
|
let rateRefusals = 0;
|
|
4219
3532
|
await backoff(async () => {
|
|
4220
3533
|
let updated = handler(this.agentState || {});
|
|
4221
|
-
const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) : null });
|
|
3534
|
+
const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, updated)) : null });
|
|
4222
3535
|
if (answer.result === "success") {
|
|
4223
|
-
this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
|
|
3536
|
+
this.agentState = answer.agentState ? persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(answer.agentState)) : null;
|
|
4224
3537
|
this.agentStateVersion = answer.version;
|
|
4225
3538
|
rateRefusals = 0;
|
|
4226
|
-
logger.debug("Agent state updated", this.agentState);
|
|
3539
|
+
persistence.logger.debug("Agent state updated", this.agentState);
|
|
4227
3540
|
} else if (answer.result === "version-mismatch") {
|
|
4228
3541
|
if (answer.version > this.agentStateVersion) {
|
|
4229
3542
|
this.agentStateVersion = answer.version;
|
|
4230
|
-
this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
|
|
3543
|
+
this.agentState = answer.agentState ? persistence.decrypt(this.encryptionKey, this.encryptionVariant, persistence.decodeBase64(answer.agentState)) : null;
|
|
4231
3544
|
}
|
|
4232
3545
|
throw new Error("Agent state version mismatch");
|
|
4233
3546
|
} else if (answer.result === "error") {
|
|
@@ -4238,7 +3551,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4238
3551
|
code: answer.error,
|
|
4239
3552
|
attempt: rateRefusals,
|
|
4240
3553
|
sleep: delay,
|
|
4241
|
-
log: (message) => logger.debug(message)
|
|
3554
|
+
log: (message) => persistence.logger.debug(message)
|
|
4242
3555
|
});
|
|
4243
3556
|
}
|
|
4244
3557
|
throw new Error("Agent state update failed");
|
|
@@ -4267,7 +3580,7 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4267
3580
|
});
|
|
4268
3581
|
}
|
|
4269
3582
|
async close() {
|
|
4270
|
-
logger.debug("[API] socket.close() called");
|
|
3583
|
+
persistence.logger.debug("[API] socket.close() called");
|
|
4271
3584
|
this.sendSync.stop();
|
|
4272
3585
|
this.receiveSync.stop();
|
|
4273
3586
|
if (this.reconnectInterval) {
|
|
@@ -4287,14 +3600,14 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
4287
3600
|
return;
|
|
4288
3601
|
}
|
|
4289
3602
|
if (!shouldReconnect()) {
|
|
4290
|
-
logger.debug("[API] Still not ready to reconnect");
|
|
3603
|
+
persistence.logger.debug("[API] Still not ready to reconnect");
|
|
4291
3604
|
return;
|
|
4292
3605
|
}
|
|
4293
|
-
logger.debug("[API] Attempting reconnect");
|
|
3606
|
+
persistence.logger.debug("[API] Attempting reconnect");
|
|
4294
3607
|
this.socket.connect();
|
|
4295
3608
|
}, 3e3);
|
|
4296
3609
|
if (shouldReconnect()) {
|
|
4297
|
-
logger.debug("[API] Network up + lid open \u2014 reconnecting in 1s");
|
|
3610
|
+
persistence.logger.debug("[API] Network up + lid open \u2014 reconnecting in 1s");
|
|
4298
3611
|
setTimeout(() => {
|
|
4299
3612
|
if (!this.socket.connected) this.socket.connect();
|
|
4300
3613
|
}, 1e3);
|
|
@@ -4445,12 +3758,12 @@ function normalizeFsPath(input, homeDir) {
|
|
|
4445
3758
|
if (p === "~") {
|
|
4446
3759
|
p = homeDir;
|
|
4447
3760
|
} else if (p.startsWith("~/")) {
|
|
4448
|
-
p = path.join(homeDir, p.slice(2));
|
|
3761
|
+
p = path$1.join(homeDir, p.slice(2));
|
|
4449
3762
|
}
|
|
4450
|
-
if (!path.isAbsolute(p)) {
|
|
4451
|
-
p = path.join(homeDir, p);
|
|
3763
|
+
if (!path$1.isAbsolute(p)) {
|
|
3764
|
+
p = path$1.join(homeDir, p);
|
|
4452
3765
|
}
|
|
4453
|
-
return path.resolve(p);
|
|
3766
|
+
return path$1.resolve(p);
|
|
4454
3767
|
}
|
|
4455
3768
|
function isBinaryContent(bytes) {
|
|
4456
3769
|
const n = Math.min(bytes.length, BINARY_SNIFF_BYTES);
|
|
@@ -4490,13 +3803,13 @@ function fsError(error) {
|
|
|
4490
3803
|
if (error instanceof Error) return error;
|
|
4491
3804
|
return new Error("fs-failed");
|
|
4492
3805
|
}
|
|
4493
|
-
async function fsList(inputPath, homeDir = os.homedir()) {
|
|
4494
|
-
const path
|
|
3806
|
+
async function fsList(inputPath, homeDir = os$1.homedir()) {
|
|
3807
|
+
const path = normalizeFsPath(inputPath, homeDir);
|
|
4495
3808
|
let dirents;
|
|
4496
3809
|
try {
|
|
4497
|
-
const st = await promises.stat(path
|
|
3810
|
+
const st = await promises.stat(path);
|
|
4498
3811
|
if (!st.isDirectory()) throw new Error("not-a-directory");
|
|
4499
|
-
dirents = await promises.readdir(path
|
|
3812
|
+
dirents = await promises.readdir(path, { withFileTypes: true });
|
|
4500
3813
|
} catch (error) {
|
|
4501
3814
|
throw fsError(error);
|
|
4502
3815
|
}
|
|
@@ -4504,7 +3817,7 @@ async function fsList(inputPath, homeDir = os.homedir()) {
|
|
|
4504
3817
|
dirents.map(async (d) => {
|
|
4505
3818
|
const entry = { name: d.name, type: entryTypeOf(d) };
|
|
4506
3819
|
try {
|
|
4507
|
-
const s = await promises.lstat(path.join(path
|
|
3820
|
+
const s = await promises.lstat(path$1.join(path, d.name));
|
|
4508
3821
|
entry.size = s.size;
|
|
4509
3822
|
entry.mtimeMs = Math.round(s.mtimeMs);
|
|
4510
3823
|
} catch {
|
|
@@ -4515,12 +3828,12 @@ async function fsList(inputPath, homeDir = os.homedir()) {
|
|
|
4515
3828
|
entries.sort(compareFsEntries);
|
|
4516
3829
|
const truncated = entries.length > FS_LIST_MAX_ENTRIES;
|
|
4517
3830
|
return {
|
|
4518
|
-
path
|
|
3831
|
+
path,
|
|
4519
3832
|
entries: truncated ? entries.slice(0, FS_LIST_MAX_ENTRIES) : entries,
|
|
4520
3833
|
truncated
|
|
4521
3834
|
};
|
|
4522
3835
|
}
|
|
4523
|
-
async function fsRead(inputPath, options = {}, homeDir = os.homedir()) {
|
|
3836
|
+
async function fsRead(inputPath, options = {}, homeDir = os$1.homedir()) {
|
|
4524
3837
|
const path = normalizeFsPath(inputPath, homeDir);
|
|
4525
3838
|
const limit = clampReadLimit(options.maxBytes);
|
|
4526
3839
|
const offset = clampReadOffset(options.offset);
|
|
@@ -4552,400 +3865,15 @@ async function fsRead(inputPath, options = {}, homeDir = os.homedir()) {
|
|
|
4552
3865
|
}
|
|
4553
3866
|
function registerFsHandlers(rpcHandlerManager) {
|
|
4554
3867
|
rpcHandlerManager.registerHandler("fs-list", async (params) => {
|
|
4555
|
-
logger.debug("[FS RPC] fs-list", params?.path);
|
|
3868
|
+
persistence.logger.debug("[FS RPC] fs-list", params?.path);
|
|
4556
3869
|
return fsList(params?.path);
|
|
4557
3870
|
});
|
|
4558
3871
|
rpcHandlerManager.registerHandler("fs-read", async (params) => {
|
|
4559
|
-
logger.debug("[FS RPC] fs-read", params?.path);
|
|
3872
|
+
persistence.logger.debug("[FS RPC] fs-read", params?.path);
|
|
4560
3873
|
return fsRead(params?.path, { maxBytes: params?.maxBytes, allowBinary: params?.allowBinary === true, offset: params?.offset });
|
|
4561
3874
|
});
|
|
4562
3875
|
}
|
|
4563
3876
|
|
|
4564
|
-
const DEFAULT_SERVER_URL = "https://veryhappy.dev";
|
|
4565
|
-
const LEGACY_CLOUD_SERVER_URL = "https://happy.mereith.com";
|
|
4566
|
-
const CLOUD_SERVER_URLS = [DEFAULT_SERVER_URL, LEGACY_CLOUD_SERVER_URL];
|
|
4567
|
-
function sameRelay(left, right) {
|
|
4568
|
-
try {
|
|
4569
|
-
return new URL(left).origin === new URL(right).origin;
|
|
4570
|
-
} catch {
|
|
4571
|
-
return left.replace(/\/+$/, "") === right.replace(/\/+$/, "");
|
|
4572
|
-
}
|
|
4573
|
-
}
|
|
4574
|
-
function credentialRelayProblem(credentialServerUrl, configuredServerUrl) {
|
|
4575
|
-
if (credentialServerUrl) {
|
|
4576
|
-
return sameRelay(credentialServerUrl, configuredServerUrl) ? void 0 : `Credentials belong to ${credentialServerUrl}, but the active relay is ${configuredServerUrl}.`;
|
|
4577
|
-
}
|
|
4578
|
-
if (CLOUD_SERVER_URLS.some((cloudUrl) => sameRelay(configuredServerUrl, cloudUrl))) {
|
|
4579
|
-
return void 0;
|
|
4580
|
-
}
|
|
4581
|
-
return `These credentials predate relay tracking, so they cannot be safely reused with ${configuredServerUrl}.`;
|
|
4582
|
-
}
|
|
4583
|
-
|
|
4584
|
-
const SandboxConfigSchema = z__namespace.object({
|
|
4585
|
-
enabled: z__namespace.boolean().default(false),
|
|
4586
|
-
workspaceRoot: z__namespace.string().optional(),
|
|
4587
|
-
sessionIsolation: z__namespace.enum(["strict", "workspace", "custom"]).default("workspace"),
|
|
4588
|
-
customWritePaths: z__namespace.array(z__namespace.string()).default([]),
|
|
4589
|
-
denyReadPaths: z__namespace.array(z__namespace.string()).default(["~/.ssh", "~/.aws", "~/.gnupg"]),
|
|
4590
|
-
extraWritePaths: z__namespace.array(z__namespace.string()).default(["/tmp"]),
|
|
4591
|
-
denyWritePaths: z__namespace.array(z__namespace.string()).default([".env"]),
|
|
4592
|
-
networkMode: z__namespace.enum(["blocked", "allowed", "custom"]).default("allowed"),
|
|
4593
|
-
allowedDomains: z__namespace.array(z__namespace.string()).default([]),
|
|
4594
|
-
deniedDomains: z__namespace.array(z__namespace.string()).default([]),
|
|
4595
|
-
allowLocalBinding: z__namespace.boolean().default(true)
|
|
4596
|
-
});
|
|
4597
|
-
const SUPPORTED_SCHEMA_VERSION = 2;
|
|
4598
|
-
const defaultSettings = {
|
|
4599
|
-
schemaVersion: SUPPORTED_SCHEMA_VERSION,
|
|
4600
|
-
onboardingCompleted: false,
|
|
4601
|
-
sandboxConfig: void 0
|
|
4602
|
-
};
|
|
4603
|
-
function migrateSettings(raw, fromVersion) {
|
|
4604
|
-
let migrated = { ...raw };
|
|
4605
|
-
return migrated;
|
|
4606
|
-
}
|
|
4607
|
-
async function readSettingsOutcome() {
|
|
4608
|
-
if (!node_fs.existsSync(configuration.settingsFile)) {
|
|
4609
|
-
return { kind: "absent" };
|
|
4610
|
-
}
|
|
4611
|
-
try {
|
|
4612
|
-
return { kind: "ok", settings: await parseSettingsFile() };
|
|
4613
|
-
} catch (error) {
|
|
4614
|
-
return { kind: "unreadable", error: error?.message ?? String(error) };
|
|
4615
|
-
}
|
|
4616
|
-
}
|
|
4617
|
-
async function readSettings() {
|
|
4618
|
-
const outcome = await readSettingsOutcome();
|
|
4619
|
-
if (outcome.kind === "ok") return outcome.settings;
|
|
4620
|
-
if (outcome.kind === "unreadable") {
|
|
4621
|
-
logger.warn(`Failed to read settings: ${outcome.error}`);
|
|
4622
|
-
}
|
|
4623
|
-
return { ...defaultSettings };
|
|
4624
|
-
}
|
|
4625
|
-
async function parseSettingsFile() {
|
|
4626
|
-
const content = await promises.readFile(configuration.settingsFile, "utf8");
|
|
4627
|
-
const raw = JSON.parse(content);
|
|
4628
|
-
const schemaVersion = raw.schemaVersion ?? 1;
|
|
4629
|
-
if (schemaVersion > SUPPORTED_SCHEMA_VERSION) {
|
|
4630
|
-
logger.warn(
|
|
4631
|
-
`\u26A0\uFE0F Settings schema v${schemaVersion} > supported v${SUPPORTED_SCHEMA_VERSION}. Update happy-cli for full functionality.`
|
|
4632
|
-
);
|
|
4633
|
-
}
|
|
4634
|
-
const migrated = migrateSettings(raw);
|
|
4635
|
-
if (migrated.sandboxConfig !== void 0) {
|
|
4636
|
-
try {
|
|
4637
|
-
migrated.sandboxConfig = SandboxConfigSchema.parse(migrated.sandboxConfig);
|
|
4638
|
-
} catch (error) {
|
|
4639
|
-
logger.warn(`\u26A0\uFE0F Invalid sandbox config - skipping. Error: ${error.message}`);
|
|
4640
|
-
migrated.sandboxConfig = void 0;
|
|
4641
|
-
}
|
|
4642
|
-
}
|
|
4643
|
-
return { ...defaultSettings, ...migrated };
|
|
4644
|
-
}
|
|
4645
|
-
async function writeSettings(settings) {
|
|
4646
|
-
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
4647
|
-
const settingsWithVersion = {
|
|
4648
|
-
...settings,
|
|
4649
|
-
schemaVersion: settings.schemaVersion ?? SUPPORTED_SCHEMA_VERSION
|
|
4650
|
-
};
|
|
4651
|
-
await writePrivateFile(configuration.settingsFile, JSON.stringify(settingsWithVersion, null, 2));
|
|
4652
|
-
}
|
|
4653
|
-
async function updateSettings(updater) {
|
|
4654
|
-
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
4655
|
-
const MAX_LOCK_ATTEMPTS = 50;
|
|
4656
|
-
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
4657
|
-
const lockFile = configuration.settingsFile + ".lock";
|
|
4658
|
-
const tmpFile = configuration.settingsFile + ".tmp";
|
|
4659
|
-
let fileHandle;
|
|
4660
|
-
let attempts = 0;
|
|
4661
|
-
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
4662
|
-
try {
|
|
4663
|
-
fileHandle = await promises.open(lockFile, node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY, PRIVATE_FILE_MODE);
|
|
4664
|
-
break;
|
|
4665
|
-
} catch (err) {
|
|
4666
|
-
if (err.code === "EEXIST") {
|
|
4667
|
-
attempts++;
|
|
4668
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
4669
|
-
try {
|
|
4670
|
-
const stats = await promises.stat(lockFile);
|
|
4671
|
-
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
4672
|
-
await promises.unlink(lockFile).catch(() => {
|
|
4673
|
-
});
|
|
4674
|
-
}
|
|
4675
|
-
} catch {
|
|
4676
|
-
}
|
|
4677
|
-
} else {
|
|
4678
|
-
throw err;
|
|
4679
|
-
}
|
|
4680
|
-
}
|
|
4681
|
-
}
|
|
4682
|
-
if (!fileHandle) {
|
|
4683
|
-
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
4684
|
-
}
|
|
4685
|
-
try {
|
|
4686
|
-
const outcome = await readSettingsOutcome();
|
|
4687
|
-
if (outcome.kind === "unreadable") {
|
|
4688
|
-
throw new Error(
|
|
4689
|
-
`Settings file ${configuration.settingsFile} exists but could not be read (${outcome.error}). Refusing to overwrite it \u2014 fix or remove the file (a removed file is recreated with defaults).`
|
|
4690
|
-
);
|
|
4691
|
-
}
|
|
4692
|
-
const current = outcome.kind === "ok" ? outcome.settings : { ...defaultSettings };
|
|
4693
|
-
const updated = await updater(current);
|
|
4694
|
-
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
4695
|
-
await writePrivateFile(tmpFile, JSON.stringify(updated, null, 2));
|
|
4696
|
-
await promises.rename(tmpFile, configuration.settingsFile);
|
|
4697
|
-
await hardenPrivateFile(configuration.settingsFile);
|
|
4698
|
-
return updated;
|
|
4699
|
-
} finally {
|
|
4700
|
-
await fileHandle.close();
|
|
4701
|
-
await promises.unlink(lockFile).catch(() => {
|
|
4702
|
-
});
|
|
4703
|
-
}
|
|
4704
|
-
}
|
|
4705
|
-
const credentialsSchema = z__namespace.object({
|
|
4706
|
-
token: z__namespace.string(),
|
|
4707
|
-
authServerUrl: z__namespace.string().optional(),
|
|
4708
|
-
secret: z__namespace.string().base64().nullish(),
|
|
4709
|
-
// Legacy
|
|
4710
|
-
encryption: z__namespace.object({
|
|
4711
|
-
publicKey: z__namespace.string().base64(),
|
|
4712
|
-
machineKey: z__namespace.string().base64()
|
|
4713
|
-
}).nullish()
|
|
4714
|
-
});
|
|
4715
|
-
async function readCredentials() {
|
|
4716
|
-
if (!node_fs.existsSync(configuration.privateKeyFile)) {
|
|
4717
|
-
return null;
|
|
4718
|
-
}
|
|
4719
|
-
try {
|
|
4720
|
-
const keyBase64 = await promises.readFile(configuration.privateKeyFile, "utf8");
|
|
4721
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
4722
|
-
if (credentials.secret) {
|
|
4723
|
-
return {
|
|
4724
|
-
token: credentials.token,
|
|
4725
|
-
authServerUrl: credentials.authServerUrl,
|
|
4726
|
-
encryption: {
|
|
4727
|
-
type: "legacy",
|
|
4728
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64"))
|
|
4729
|
-
}
|
|
4730
|
-
};
|
|
4731
|
-
} else if (credentials.encryption) {
|
|
4732
|
-
return {
|
|
4733
|
-
token: credentials.token,
|
|
4734
|
-
authServerUrl: credentials.authServerUrl,
|
|
4735
|
-
encryption: {
|
|
4736
|
-
type: "dataKey",
|
|
4737
|
-
publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
|
|
4738
|
-
machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
|
|
4739
|
-
}
|
|
4740
|
-
};
|
|
4741
|
-
}
|
|
4742
|
-
} catch {
|
|
4743
|
-
return null;
|
|
4744
|
-
}
|
|
4745
|
-
return null;
|
|
4746
|
-
}
|
|
4747
|
-
async function readCredentialsForConfiguredRelay() {
|
|
4748
|
-
const credentials = await readCredentials();
|
|
4749
|
-
if (!credentials) return null;
|
|
4750
|
-
const relayProblem = credentialRelayProblem(credentials.authServerUrl, configuration.serverUrl);
|
|
4751
|
-
if (relayProblem) {
|
|
4752
|
-
throw new Error(
|
|
4753
|
-
`${relayProblem} Use a separate HAPPY_HOME_DIR for this relay, or run \`very-happy auth login --force\` before making relay requests.`
|
|
4754
|
-
);
|
|
4755
|
-
}
|
|
4756
|
-
return credentials;
|
|
4757
|
-
}
|
|
4758
|
-
async function writeCredentialsLegacy(credentials) {
|
|
4759
|
-
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
4760
|
-
await writePrivateFile(configuration.privateKeyFile, JSON.stringify({
|
|
4761
|
-
secret: encodeBase64(credentials.secret),
|
|
4762
|
-
token: credentials.token,
|
|
4763
|
-
authServerUrl: configuration.serverUrl
|
|
4764
|
-
}, null, 2));
|
|
4765
|
-
}
|
|
4766
|
-
async function writeCredentialsDataKey(credentials) {
|
|
4767
|
-
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
4768
|
-
await writePrivateFile(configuration.privateKeyFile, JSON.stringify({
|
|
4769
|
-
encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
|
|
4770
|
-
token: credentials.token,
|
|
4771
|
-
authServerUrl: configuration.serverUrl
|
|
4772
|
-
}, null, 2));
|
|
4773
|
-
}
|
|
4774
|
-
async function clearCredentials() {
|
|
4775
|
-
if (node_fs.existsSync(configuration.privateKeyFile)) {
|
|
4776
|
-
await promises.unlink(configuration.privateKeyFile);
|
|
4777
|
-
}
|
|
4778
|
-
}
|
|
4779
|
-
async function clearMachineId() {
|
|
4780
|
-
await updateSettings((settings) => ({
|
|
4781
|
-
...settings,
|
|
4782
|
-
machineId: void 0
|
|
4783
|
-
}));
|
|
4784
|
-
}
|
|
4785
|
-
async function readDaemonState() {
|
|
4786
|
-
try {
|
|
4787
|
-
if (!node_fs.existsSync(configuration.daemonStateFile)) {
|
|
4788
|
-
return null;
|
|
4789
|
-
}
|
|
4790
|
-
const content = await promises.readFile(configuration.daemonStateFile, "utf-8");
|
|
4791
|
-
return JSON.parse(content);
|
|
4792
|
-
} catch (error) {
|
|
4793
|
-
console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
|
|
4794
|
-
return null;
|
|
4795
|
-
}
|
|
4796
|
-
}
|
|
4797
|
-
function writeDaemonState(state) {
|
|
4798
|
-
writePrivateFileSync(configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
4799
|
-
}
|
|
4800
|
-
async function clearDaemonState() {
|
|
4801
|
-
if (node_fs.existsSync(configuration.daemonStateFile)) {
|
|
4802
|
-
await promises.unlink(configuration.daemonStateFile);
|
|
4803
|
-
}
|
|
4804
|
-
if (node_fs.existsSync(configuration.daemonLockFile)) {
|
|
4805
|
-
try {
|
|
4806
|
-
await promises.unlink(configuration.daemonLockFile);
|
|
4807
|
-
} catch {
|
|
4808
|
-
}
|
|
4809
|
-
}
|
|
4810
|
-
}
|
|
4811
|
-
async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
|
|
4812
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4813
|
-
try {
|
|
4814
|
-
const fileHandle = await promises.open(
|
|
4815
|
-
configuration.daemonLockFile,
|
|
4816
|
-
node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY,
|
|
4817
|
-
PRIVATE_FILE_MODE
|
|
4818
|
-
);
|
|
4819
|
-
await fileHandle.writeFile(String(process.pid));
|
|
4820
|
-
return fileHandle;
|
|
4821
|
-
} catch (error) {
|
|
4822
|
-
if (error.code === "EEXIST") {
|
|
4823
|
-
try {
|
|
4824
|
-
const lockPid = node_fs.readFileSync(configuration.daemonLockFile, "utf-8").trim();
|
|
4825
|
-
if (lockPid && !isNaN(Number(lockPid))) {
|
|
4826
|
-
try {
|
|
4827
|
-
process.kill(Number(lockPid), 0);
|
|
4828
|
-
} catch {
|
|
4829
|
-
node_fs.unlinkSync(configuration.daemonLockFile);
|
|
4830
|
-
continue;
|
|
4831
|
-
}
|
|
4832
|
-
}
|
|
4833
|
-
} catch {
|
|
4834
|
-
}
|
|
4835
|
-
}
|
|
4836
|
-
if (attempt === maxAttempts) {
|
|
4837
|
-
return null;
|
|
4838
|
-
}
|
|
4839
|
-
const delayMs = attempt * delayIncrementMs;
|
|
4840
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4841
|
-
}
|
|
4842
|
-
}
|
|
4843
|
-
return null;
|
|
4844
|
-
}
|
|
4845
|
-
async function releaseDaemonLock(lockHandle) {
|
|
4846
|
-
try {
|
|
4847
|
-
await lockHandle.close();
|
|
4848
|
-
} catch {
|
|
4849
|
-
}
|
|
4850
|
-
try {
|
|
4851
|
-
if (node_fs.existsSync(configuration.daemonLockFile)) {
|
|
4852
|
-
node_fs.unlinkSync(configuration.daemonLockFile);
|
|
4853
|
-
}
|
|
4854
|
-
} catch {
|
|
4855
|
-
}
|
|
4856
|
-
}
|
|
4857
|
-
const SESSION_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
4858
|
-
function readPersistedSessions() {
|
|
4859
|
-
try {
|
|
4860
|
-
if (!node_fs.existsSync(configuration.sessionsFile)) return {};
|
|
4861
|
-
const data = JSON.parse(node_fs.readFileSync(configuration.sessionsFile, "utf-8"));
|
|
4862
|
-
if (!data?.sessions || typeof data.sessions !== "object") return {};
|
|
4863
|
-
const now = Date.now();
|
|
4864
|
-
const sessions = {};
|
|
4865
|
-
for (const [id, session] of Object.entries(data.sessions)) {
|
|
4866
|
-
if (now - session.savedAt < SESSION_MAX_AGE_MS) {
|
|
4867
|
-
sessions[id] = session;
|
|
4868
|
-
}
|
|
4869
|
-
}
|
|
4870
|
-
return sessions;
|
|
4871
|
-
} catch {
|
|
4872
|
-
return {};
|
|
4873
|
-
}
|
|
4874
|
-
}
|
|
4875
|
-
function readTrackedClaudeSessionIds() {
|
|
4876
|
-
try {
|
|
4877
|
-
if (!node_fs.existsSync(configuration.sessionsFile)) return [];
|
|
4878
|
-
const data = JSON.parse(node_fs.readFileSync(configuration.sessionsFile, "utf-8"));
|
|
4879
|
-
if (!data?.sessions || typeof data.sessions !== "object") return [];
|
|
4880
|
-
const ids = /* @__PURE__ */ new Set();
|
|
4881
|
-
for (const session of Object.values(data.sessions)) {
|
|
4882
|
-
const metadata = (session ?? {}).metadata;
|
|
4883
|
-
for (const id of [metadata?.claudeSessionId, metadata?.importedFromClaudeSessionId]) {
|
|
4884
|
-
if (typeof id === "string" && id.length > 0) ids.add(id.toLowerCase());
|
|
4885
|
-
}
|
|
4886
|
-
}
|
|
4887
|
-
return [...ids];
|
|
4888
|
-
} catch {
|
|
4889
|
-
return [];
|
|
4890
|
-
}
|
|
4891
|
-
}
|
|
4892
|
-
function persistSession(sessionId, session) {
|
|
4893
|
-
try {
|
|
4894
|
-
const existing = readPersistedSessions();
|
|
4895
|
-
existing[sessionId] = session;
|
|
4896
|
-
const tmpFile = configuration.sessionsFile + ".tmp";
|
|
4897
|
-
writePrivateFileSync(tmpFile, JSON.stringify({ sessions: existing }, null, 2));
|
|
4898
|
-
node_fs.renameSync(tmpFile, configuration.sessionsFile);
|
|
4899
|
-
hardenPrivateFileSync(configuration.sessionsFile);
|
|
4900
|
-
} catch (error) {
|
|
4901
|
-
logger.debug(`[PERSISTENCE] Failed to persist session ${sessionId}:`, error);
|
|
4902
|
-
}
|
|
4903
|
-
}
|
|
4904
|
-
function deletePersistedSessions(sessionIds) {
|
|
4905
|
-
try {
|
|
4906
|
-
const existing = readPersistedSessions();
|
|
4907
|
-
let changed = false;
|
|
4908
|
-
for (const id of sessionIds) {
|
|
4909
|
-
if (id in existing) {
|
|
4910
|
-
delete existing[id];
|
|
4911
|
-
changed = true;
|
|
4912
|
-
}
|
|
4913
|
-
}
|
|
4914
|
-
if (!changed) return;
|
|
4915
|
-
const tmpFile = configuration.sessionsFile + ".tmp";
|
|
4916
|
-
writePrivateFileSync(tmpFile, JSON.stringify({ sessions: existing }, null, 2));
|
|
4917
|
-
node_fs.renameSync(tmpFile, configuration.sessionsFile);
|
|
4918
|
-
hardenPrivateFileSync(configuration.sessionsFile);
|
|
4919
|
-
} catch (error) {
|
|
4920
|
-
logger.debug(`[PERSISTENCE] Failed to delete persisted sessions:`, error);
|
|
4921
|
-
}
|
|
4922
|
-
}
|
|
4923
|
-
|
|
4924
|
-
var persistence = /*#__PURE__*/Object.freeze({
|
|
4925
|
-
__proto__: null,
|
|
4926
|
-
SUPPORTED_SCHEMA_VERSION: SUPPORTED_SCHEMA_VERSION,
|
|
4927
|
-
SandboxConfigSchema: SandboxConfigSchema,
|
|
4928
|
-
acquireDaemonLock: acquireDaemonLock,
|
|
4929
|
-
clearCredentials: clearCredentials,
|
|
4930
|
-
clearDaemonState: clearDaemonState,
|
|
4931
|
-
clearMachineId: clearMachineId,
|
|
4932
|
-
deletePersistedSessions: deletePersistedSessions,
|
|
4933
|
-
persistSession: persistSession,
|
|
4934
|
-
readCredentials: readCredentials,
|
|
4935
|
-
readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay,
|
|
4936
|
-
readDaemonState: readDaemonState,
|
|
4937
|
-
readPersistedSessions: readPersistedSessions,
|
|
4938
|
-
readSettings: readSettings,
|
|
4939
|
-
readSettingsOutcome: readSettingsOutcome,
|
|
4940
|
-
readTrackedClaudeSessionIds: readTrackedClaudeSessionIds,
|
|
4941
|
-
releaseDaemonLock: releaseDaemonLock,
|
|
4942
|
-
updateSettings: updateSettings,
|
|
4943
|
-
writeCredentialsDataKey: writeCredentialsDataKey,
|
|
4944
|
-
writeCredentialsLegacy: writeCredentialsLegacy,
|
|
4945
|
-
writeDaemonState: writeDaemonState,
|
|
4946
|
-
writeSettings: writeSettings
|
|
4947
|
-
});
|
|
4948
|
-
|
|
4949
3877
|
const TODO_LIST_MAX_ITEMS = 500;
|
|
4950
3878
|
const TODO_TITLE_MAX_CHARS = 500;
|
|
4951
3879
|
const FIELD_MAX_CHARS = 200;
|
|
@@ -5011,7 +3939,7 @@ function buildProviderArgv(config, verb, operand) {
|
|
|
5011
3939
|
const DEFAULT_TIMEOUT_MS = 2e4;
|
|
5012
3940
|
const MAX_STDOUT_BYTES = 2 * 1024 * 1024;
|
|
5013
3941
|
async function readProviderConfig() {
|
|
5014
|
-
const settings = await readSettings();
|
|
3942
|
+
const settings = await persistence.readSettings();
|
|
5015
3943
|
const raw = settings?.todoProvider;
|
|
5016
3944
|
if (!raw || typeof raw !== "object") return null;
|
|
5017
3945
|
const cfg = raw;
|
|
@@ -5090,7 +4018,7 @@ function registerTodoHandlers(manager) {
|
|
|
5090
4018
|
if ("error" in parsed) {
|
|
5091
4019
|
throw new Error(`bad-output: ${parsed.error}`);
|
|
5092
4020
|
}
|
|
5093
|
-
logger.debug(`[todo] list \u2192 ${parsed.items.length} items (dropped ${parsed.dropped})`);
|
|
4021
|
+
persistence.logger.debug(`[todo] list \u2192 ${parsed.items.length} items (dropped ${parsed.dropped})`);
|
|
5094
4022
|
return { ...parsed, machineConfigured: true };
|
|
5095
4023
|
});
|
|
5096
4024
|
manager.registerHandler("todo-complete", async (params) => {
|
|
@@ -5304,14 +4232,14 @@ function scrubTmuxClientEnv(env) {
|
|
|
5304
4232
|
}
|
|
5305
4233
|
|
|
5306
4234
|
function getClaudeConfigDir() {
|
|
5307
|
-
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
4235
|
+
return process.env.CLAUDE_CONFIG_DIR || path$1.join(os$1.homedir(), ".claude");
|
|
5308
4236
|
}
|
|
5309
4237
|
function getClaudeProjectsRoot() {
|
|
5310
|
-
return path.join(getClaudeConfigDir(), "projects");
|
|
4238
|
+
return path$1.join(getClaudeConfigDir(), "projects");
|
|
5311
4239
|
}
|
|
5312
4240
|
function getProjectPath(workingDirectory) {
|
|
5313
|
-
const projectId = path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
5314
|
-
return path.join(getClaudeProjectsRoot(), projectId);
|
|
4241
|
+
const projectId = path$1.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
4242
|
+
return path$1.join(getClaudeProjectsRoot(), projectId);
|
|
5315
4243
|
}
|
|
5316
4244
|
|
|
5317
4245
|
var xtermHeadless = {};
|
|
@@ -9656,18 +8584,18 @@ class ControlClient {
|
|
|
9656
8584
|
});
|
|
9657
8585
|
this.child.stdout.on("data", (chunk) => this.consume(this.decoder.push(chunk)));
|
|
9658
8586
|
this.child.stderr.on("data", (chunk) => {
|
|
9659
|
-
logger.debug(`[CONTROL ${tmuxSession}] stderr: ${chunk.toString("utf8").trim()}`);
|
|
8587
|
+
persistence.logger.debug(`[CONTROL ${tmuxSession}] stderr: ${chunk.toString("utf8").trim()}`);
|
|
9660
8588
|
});
|
|
9661
8589
|
this.child.stdin.on("error", (e) => {
|
|
9662
|
-
logger.debug(`[CONTROL ${tmuxSession}] stdin error (session gone?): ${e}`);
|
|
8590
|
+
persistence.logger.debug(`[CONTROL ${tmuxSession}] stdin error (session gone?): ${e}`);
|
|
9663
8591
|
});
|
|
9664
8592
|
this.child.stdout.on("error", (e) => {
|
|
9665
|
-
logger.debug(`[CONTROL ${tmuxSession}] stdout error: ${e}`);
|
|
8593
|
+
persistence.logger.debug(`[CONTROL ${tmuxSession}] stdout error: ${e}`);
|
|
9666
8594
|
});
|
|
9667
8595
|
this.child.stderr.on("error", () => {
|
|
9668
8596
|
});
|
|
9669
8597
|
this.child.on("error", (e) => {
|
|
9670
|
-
logger.debug(`[CONTROL ${tmuxSession}] spawn error: ${e}`);
|
|
8598
|
+
persistence.logger.debug(`[CONTROL ${tmuxSession}] spawn error: ${e}`);
|
|
9671
8599
|
this.failAll(new Error(`control client failed: ${e}`));
|
|
9672
8600
|
});
|
|
9673
8601
|
this.child.on("exit", (code, signal) => {
|
|
@@ -9704,7 +8632,7 @@ class ControlClient {
|
|
|
9704
8632
|
if (!ev.solicited) continue;
|
|
9705
8633
|
const pending = this.queue.shift();
|
|
9706
8634
|
if (!pending) {
|
|
9707
|
-
logger.debug(`[CONTROL ${this.tmuxSession}] unmatched block cmd=${ev.cmdNum}`);
|
|
8635
|
+
persistence.logger.debug(`[CONTROL ${this.tmuxSession}] unmatched block cmd=${ev.cmdNum}`);
|
|
9708
8636
|
continue;
|
|
9709
8637
|
}
|
|
9710
8638
|
clearTimeout(pending.timer);
|
|
@@ -9713,7 +8641,7 @@ class ControlClient {
|
|
|
9713
8641
|
try {
|
|
9714
8642
|
pending.onBlock?.(ev.body);
|
|
9715
8643
|
} catch (e) {
|
|
9716
|
-
logger.debug(`[CONTROL ${this.tmuxSession}] onBlock threw: ${e}`);
|
|
8644
|
+
persistence.logger.debug(`[CONTROL ${this.tmuxSession}] onBlock threw: ${e}`);
|
|
9717
8645
|
}
|
|
9718
8646
|
pending.resolve(ev.body);
|
|
9719
8647
|
}
|
|
@@ -9723,7 +8651,7 @@ class ControlClient {
|
|
|
9723
8651
|
this.handlers.onNotification(ev.name, ev.args);
|
|
9724
8652
|
continue;
|
|
9725
8653
|
}
|
|
9726
|
-
logger.debug(`[CONTROL ${this.tmuxSession}] protocol error ${ev.reason}: ${ev.detail}`);
|
|
8654
|
+
persistence.logger.debug(`[CONTROL ${this.tmuxSession}] protocol error ${ev.reason}: ${ev.detail}`);
|
|
9727
8655
|
}
|
|
9728
8656
|
}
|
|
9729
8657
|
failAll(err) {
|
|
@@ -9784,7 +8712,7 @@ class ControlClient {
|
|
|
9784
8712
|
try {
|
|
9785
8713
|
this.child.stdin.write(text);
|
|
9786
8714
|
} catch (e) {
|
|
9787
|
-
logger.debug(`[CONTROL ${this.tmuxSession}] write failed (session gone?): ${e}`);
|
|
8715
|
+
persistence.logger.debug(`[CONTROL ${this.tmuxSession}] write failed (session gone?): ${e}`);
|
|
9788
8716
|
}
|
|
9789
8717
|
}
|
|
9790
8718
|
/** SIGTERM → grace → SIGKILL (rule 3). Resolves once the child is gone. */
|
|
@@ -9794,7 +8722,7 @@ class ControlClient {
|
|
|
9794
8722
|
this.stopping = true;
|
|
9795
8723
|
return new Promise((resolve) => {
|
|
9796
8724
|
const killer = setTimeout(() => {
|
|
9797
|
-
logger.debug(`[CONTROL ${this.tmuxSession}] SIGTERM did not land in ${graceMs}ms \u2014 SIGKILL`);
|
|
8725
|
+
persistence.logger.debug(`[CONTROL ${this.tmuxSession}] SIGTERM did not land in ${graceMs}ms \u2014 SIGKILL`);
|
|
9798
8726
|
try {
|
|
9799
8727
|
this.child.kill("SIGKILL");
|
|
9800
8728
|
} catch {
|
|
@@ -10389,14 +9317,14 @@ function sendTerminalNotification(opts) {
|
|
|
10389
9317
|
headers: {
|
|
10390
9318
|
"Authorization": `Bearer ${opts.token}`,
|
|
10391
9319
|
"Content-Type": "application/json",
|
|
10392
|
-
"X-Happy-Client": `cli-daemon/${configuration.currentCliVersion}`
|
|
9320
|
+
"X-Happy-Client": `cli-daemon/${persistence.configuration.currentCliVersion}`
|
|
10393
9321
|
},
|
|
10394
9322
|
timeout: 15e3
|
|
10395
9323
|
}
|
|
10396
9324
|
).then(() => {
|
|
10397
|
-
logger.debug(`[WEB TERMINAL] notify sent (event=${opts.event}, link=${opts.link})`);
|
|
9325
|
+
persistence.logger.debug(`[WEB TERMINAL] notify sent (event=${opts.event}, link=${opts.link})`);
|
|
10398
9326
|
}).catch((error) => {
|
|
10399
|
-
logger.debug("[WEB TERMINAL] notify failed:", error);
|
|
9327
|
+
persistence.logger.debug("[WEB TERMINAL] notify failed:", error);
|
|
10400
9328
|
});
|
|
10401
9329
|
}
|
|
10402
9330
|
|
|
@@ -10543,7 +9471,7 @@ function pruneTombstones(map, now, ttlMs = TOMBSTONE_TTL_MS) {
|
|
|
10543
9471
|
return out;
|
|
10544
9472
|
}
|
|
10545
9473
|
function tombstoneFile() {
|
|
10546
|
-
return path.join(configuration.happyHomeDir, "terminal-tombstones.json");
|
|
9474
|
+
return path$1.join(persistence.configuration.happyHomeDir, "terminal-tombstones.json");
|
|
10547
9475
|
}
|
|
10548
9476
|
function loadTombstones() {
|
|
10549
9477
|
try {
|
|
@@ -10555,14 +9483,14 @@ function loadTombstones() {
|
|
|
10555
9483
|
}
|
|
10556
9484
|
function saveTombstones(map) {
|
|
10557
9485
|
try {
|
|
10558
|
-
node_fs.mkdirSync(configuration.happyHomeDir, { recursive: true });
|
|
9486
|
+
node_fs.mkdirSync(persistence.configuration.happyHomeDir, { recursive: true });
|
|
10559
9487
|
node_fs.writeFileSync(tombstoneFile(), JSON.stringify(pruneTombstones(map, Date.now())));
|
|
10560
9488
|
} catch (e) {
|
|
10561
|
-
logger.debug(`[WEB TERMINAL] tombstone save failed: ${e}`);
|
|
9489
|
+
persistence.logger.debug(`[WEB TERMINAL] tombstone save failed: ${e}`);
|
|
10562
9490
|
}
|
|
10563
9491
|
}
|
|
10564
9492
|
function closedTerminalsFile() {
|
|
10565
|
-
return path.join(configuration.happyHomeDir, "closed-terminals.json");
|
|
9493
|
+
return path$1.join(persistence.configuration.happyHomeDir, "closed-terminals.json");
|
|
10566
9494
|
}
|
|
10567
9495
|
function loadClosedTerminals() {
|
|
10568
9496
|
try {
|
|
@@ -10573,14 +9501,14 @@ function loadClosedTerminals() {
|
|
|
10573
9501
|
}
|
|
10574
9502
|
function saveClosedTerminals(list) {
|
|
10575
9503
|
try {
|
|
10576
|
-
node_fs.mkdirSync(configuration.happyHomeDir, { recursive: true });
|
|
9504
|
+
node_fs.mkdirSync(persistence.configuration.happyHomeDir, { recursive: true });
|
|
10577
9505
|
node_fs.writeFileSync(closedTerminalsFile(), JSON.stringify(list));
|
|
10578
9506
|
} catch (e) {
|
|
10579
|
-
logger.debug(`[WEB TERMINAL] closed-terminals save failed: ${e}`);
|
|
9507
|
+
persistence.logger.debug(`[WEB TERMINAL] closed-terminals save failed: ${e}`);
|
|
10580
9508
|
}
|
|
10581
9509
|
}
|
|
10582
9510
|
function liveTerminalsFile() {
|
|
10583
|
-
return path.join(configuration.happyHomeDir, "live-terminals.json");
|
|
9511
|
+
return path$1.join(persistence.configuration.happyHomeDir, "live-terminals.json");
|
|
10584
9512
|
}
|
|
10585
9513
|
function loadLiveSnapshot() {
|
|
10586
9514
|
try {
|
|
@@ -10591,10 +9519,10 @@ function loadLiveSnapshot() {
|
|
|
10591
9519
|
}
|
|
10592
9520
|
function saveLiveSnapshot(map) {
|
|
10593
9521
|
try {
|
|
10594
|
-
node_fs.mkdirSync(configuration.happyHomeDir, { recursive: true });
|
|
9522
|
+
node_fs.mkdirSync(persistence.configuration.happyHomeDir, { recursive: true });
|
|
10595
9523
|
node_fs.writeFileSync(liveTerminalsFile(), JSON.stringify(serializeLiveSnapshot(map)));
|
|
10596
9524
|
} catch (e) {
|
|
10597
|
-
logger.debug(`[WEB TERMINAL] live-terminals save failed: ${e}`);
|
|
9525
|
+
persistence.logger.debug(`[WEB TERMINAL] live-terminals save failed: ${e}`);
|
|
10598
9526
|
}
|
|
10599
9527
|
}
|
|
10600
9528
|
const TERMINAL_OPEN_TIMEOUT = "terminal-open-timeout";
|
|
@@ -10855,7 +9783,7 @@ function tmuxSupportsEnvFlag() {
|
|
|
10855
9783
|
}
|
|
10856
9784
|
const PASTE_FILE_TTL_MS = 5e3;
|
|
10857
9785
|
function pasteSpoolDir() {
|
|
10858
|
-
const dir = path.join(configuration.happyHomeDir, "paste-spool");
|
|
9786
|
+
const dir = path$1.join(persistence.configuration.happyHomeDir, "paste-spool");
|
|
10859
9787
|
try {
|
|
10860
9788
|
node_fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10861
9789
|
} catch {
|
|
@@ -10906,7 +9834,7 @@ function ptyEnv() {
|
|
|
10906
9834
|
if (typeof v === "string") env[k] = v;
|
|
10907
9835
|
}
|
|
10908
9836
|
scrubTmuxClientEnv(env);
|
|
10909
|
-
const local = `${os.homedir()}/.local/bin`;
|
|
9837
|
+
const local = `${os$1.homedir()}/.local/bin`;
|
|
10910
9838
|
if (!(env.PATH || "").split(":").includes(local)) {
|
|
10911
9839
|
env.PATH = `${local}:${env.PATH || ""}`;
|
|
10912
9840
|
}
|
|
@@ -11382,7 +10310,7 @@ class WebTerminalManager {
|
|
|
11382
10310
|
try {
|
|
11383
10311
|
this.emit("terminal-activity", { terminals: updates });
|
|
11384
10312
|
} catch (e) {
|
|
11385
|
-
logger.debug(`[WEB TERMINAL] activity emit failed: ${e}`);
|
|
10313
|
+
persistence.logger.debug(`[WEB TERMINAL] activity emit failed: ${e}`);
|
|
11386
10314
|
}
|
|
11387
10315
|
}
|
|
11388
10316
|
/** Seed the change signature from a list the caller already delivered by
|
|
@@ -11472,7 +10400,7 @@ class WebTerminalManager {
|
|
|
11472
10400
|
this.lastListSignature = sig;
|
|
11473
10401
|
cb(list);
|
|
11474
10402
|
} catch (e) {
|
|
11475
|
-
logger.debug(`[WEB TERMINAL] list track tick failed: ${e}`);
|
|
10403
|
+
persistence.logger.debug(`[WEB TERMINAL] list track tick failed: ${e}`);
|
|
11476
10404
|
}
|
|
11477
10405
|
}
|
|
11478
10406
|
/** Closed-record diff (B-084): a terminal id that was in the last observed
|
|
@@ -11500,7 +10428,7 @@ class WebTerminalManager {
|
|
|
11500
10428
|
next.set(id, info);
|
|
11501
10429
|
continue;
|
|
11502
10430
|
}
|
|
11503
|
-
persisted ??= readPersistedSessions();
|
|
10431
|
+
persisted ??= persistence.readPersistedSessions();
|
|
11504
10432
|
const mirror = pickMirrorForTerminal(persisted, id);
|
|
11505
10433
|
this.closedTerminals = appendClosedTerminal(this.closedTerminals, {
|
|
11506
10434
|
id,
|
|
@@ -11518,11 +10446,11 @@ class WebTerminalManager {
|
|
|
11518
10446
|
closedAt: now
|
|
11519
10447
|
});
|
|
11520
10448
|
changed = true;
|
|
11521
|
-
logger.debug(`[WEB TERMINAL] recorded closed terminal ${id}`);
|
|
10449
|
+
persistence.logger.debug(`[WEB TERMINAL] recorded closed terminal ${id}`);
|
|
11522
10450
|
try {
|
|
11523
10451
|
this.onTerminalClosedCb?.(id);
|
|
11524
10452
|
} catch (e) {
|
|
11525
|
-
logger.debug(`[WEB TERMINAL] terminal-closed callback failed: ${e}`);
|
|
10453
|
+
persistence.logger.debug(`[WEB TERMINAL] terminal-closed callback failed: ${e}`);
|
|
11526
10454
|
}
|
|
11527
10455
|
}
|
|
11528
10456
|
const pruned = pruneClosedAgainstLive(this.closedTerminals, liveIds);
|
|
@@ -11554,7 +10482,7 @@ class WebTerminalManager {
|
|
|
11554
10482
|
for (const [id, info] of snapshot) {
|
|
11555
10483
|
if (liveIds.has(id)) continue;
|
|
11556
10484
|
if (this.closedTerminals.some((r) => r.id === id)) continue;
|
|
11557
|
-
persisted ??= readPersistedSessions();
|
|
10485
|
+
persisted ??= persistence.readPersistedSessions();
|
|
11558
10486
|
const mirror = pickMirrorForTerminal(persisted, id);
|
|
11559
10487
|
this.closedTerminals = appendClosedTerminal(this.closedTerminals, {
|
|
11560
10488
|
id,
|
|
@@ -11583,7 +10511,7 @@ class WebTerminalManager {
|
|
|
11583
10511
|
}
|
|
11584
10512
|
if (recorded > 0) {
|
|
11585
10513
|
saveClosedTerminals(this.closedTerminals);
|
|
11586
|
-
logger.debug(`[WEB TERMINAL] daemon gap: archived ${recorded} terminal(s) from the persisted snapshot`);
|
|
10514
|
+
persistence.logger.debug(`[WEB TERMINAL] daemon gap: archived ${recorded} terminal(s) from the persisted snapshot`);
|
|
11587
10515
|
}
|
|
11588
10516
|
if (candidates.length > 0) void this.autoRestore(candidates, liveIds);
|
|
11589
10517
|
}
|
|
@@ -11601,7 +10529,7 @@ class WebTerminalManager {
|
|
|
11601
10529
|
this.autoRestoreDone = true;
|
|
11602
10530
|
try {
|
|
11603
10531
|
if (!isTmuxAvailable()) return;
|
|
11604
|
-
const settings = await readSettings();
|
|
10532
|
+
const settings = await persistence.readSettings();
|
|
11605
10533
|
const config = resolveAutoRestoreConfig(settings);
|
|
11606
10534
|
const selection = selectAutoRestore(candidates, {
|
|
11607
10535
|
now: Date.now(),
|
|
@@ -11617,7 +10545,7 @@ class WebTerminalManager {
|
|
|
11617
10545
|
});
|
|
11618
10546
|
for (const plan of selection.plans) {
|
|
11619
10547
|
if (this.shuttingDown) {
|
|
11620
|
-
logger.debug("[WEB TERMINAL] auto-restore aborted: daemon shutting down");
|
|
10548
|
+
persistence.logger.debug("[WEB TERMINAL] auto-restore aborted: daemon shutting down");
|
|
11621
10549
|
return;
|
|
11622
10550
|
}
|
|
11623
10551
|
const ok = this.restoreOneTerminal(plan);
|
|
@@ -11630,15 +10558,15 @@ class WebTerminalManager {
|
|
|
11630
10558
|
}
|
|
11631
10559
|
const line = autoRestoreSummary(selection);
|
|
11632
10560
|
if (line) {
|
|
11633
|
-
logger.info(`[WEB TERMINAL] auto-restore: ${line}`);
|
|
10561
|
+
persistence.logger.info(`[WEB TERMINAL] auto-restore: ${line}`);
|
|
11634
10562
|
try {
|
|
11635
10563
|
this.onAutoRestoreSummaryCb?.(line);
|
|
11636
10564
|
} catch (e) {
|
|
11637
|
-
logger.debug(`[WEB TERMINAL] auto-restore report failed: ${e}`);
|
|
10565
|
+
persistence.logger.debug(`[WEB TERMINAL] auto-restore report failed: ${e}`);
|
|
11638
10566
|
}
|
|
11639
10567
|
}
|
|
11640
10568
|
} catch (e) {
|
|
11641
|
-
logger.debug(`[WEB TERMINAL] auto-restore failed: ${e}`);
|
|
10569
|
+
persistence.logger.debug(`[WEB TERMINAL] auto-restore failed: ${e}`);
|
|
11642
10570
|
}
|
|
11643
10571
|
}
|
|
11644
10572
|
/**
|
|
@@ -11684,17 +10612,17 @@ class WebTerminalManager {
|
|
|
11684
10612
|
return false;
|
|
11685
10613
|
}
|
|
11686
10614
|
},
|
|
11687
|
-
conversationExists: (cwd, claudeSessionId) => node_fs.existsSync(path.join(getProjectPath(cwd), `${claudeSessionId}.jsonl`)),
|
|
10615
|
+
conversationExists: (cwd, claudeSessionId) => node_fs.existsSync(path$1.join(getProjectPath(cwd), `${claudeSessionId}.jsonl`)),
|
|
11688
10616
|
// B-273: only consulted for records that were attach terminals.
|
|
11689
10617
|
// Unbounded: the panel's 50-row cap must not hide a restore target.
|
|
11690
10618
|
userSessions: record.attachTmux ? this.listUserTmuxSessions(Infinity) : [],
|
|
11691
10619
|
attachSocket: process.env[VH_TMUX_SOCKET_ENV],
|
|
11692
|
-
homeDir: os.homedir()
|
|
10620
|
+
homeDir: os$1.homedir()
|
|
11693
10621
|
});
|
|
11694
10622
|
if (plan.kind === "already-live") return { type: "success", terminalId };
|
|
11695
10623
|
if (plan.kind === "error") return { type: "error", reason: plan.reason };
|
|
11696
10624
|
if (!this.createDetachedTerminal(plan)) return { type: "error", reason: "create-failed" };
|
|
11697
|
-
logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.attachTmux ? ` (attached tmux session ${plan.attachTmux})` : plan.command ? " (conversation resumed)" : ""}`);
|
|
10625
|
+
persistence.logger.info(`[WEB TERMINAL] restored closed terminal ${terminalId} in ${plan.cwd}${plan.attachTmux ? ` (attached tmux session ${plan.attachTmux})` : plan.command ? " (conversation resumed)" : ""}`);
|
|
11698
10626
|
this.kickListRefresh();
|
|
11699
10627
|
return { type: "success", terminalId };
|
|
11700
10628
|
}
|
|
@@ -11716,7 +10644,7 @@ class WebTerminalManager {
|
|
|
11716
10644
|
"-e",
|
|
11717
10645
|
`VH_TERMINAL_ID=${plan.terminalId}`,
|
|
11718
10646
|
"-e",
|
|
11719
|
-
`VH_HAPPY_HOME_DIR=${configuration.happyHomeDir}`
|
|
10647
|
+
`VH_HAPPY_HOME_DIR=${persistence.configuration.happyHomeDir}`
|
|
11720
10648
|
] : [];
|
|
11721
10649
|
const { cols, rows } = coldCreateGeometry(plan);
|
|
11722
10650
|
const created = node_child_process.spawnSync(
|
|
@@ -11730,7 +10658,7 @@ class WebTerminalManager {
|
|
|
11730
10658
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env }
|
|
11731
10659
|
).status === 0;
|
|
11732
10660
|
if (!live) {
|
|
11733
|
-
logger.debug(`[WEB TERMINAL] auto-restore: tmux create failed for ${plan.terminalId}`);
|
|
10661
|
+
persistence.logger.debug(`[WEB TERMINAL] auto-restore: tmux create failed for ${plan.terminalId}`);
|
|
11734
10662
|
return false;
|
|
11735
10663
|
}
|
|
11736
10664
|
if (plan.title) {
|
|
@@ -11767,7 +10695,7 @@ class WebTerminalManager {
|
|
|
11767
10695
|
node_child_process.spawnSync("tmux", tmuxArgs(argv), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
11768
10696
|
}
|
|
11769
10697
|
}
|
|
11770
|
-
logger.info(`[WEB TERMINAL] cold-created ${plan.terminalId} in ${plan.cwd}`);
|
|
10698
|
+
persistence.logger.info(`[WEB TERMINAL] cold-created ${plan.terminalId} in ${plan.cwd}`);
|
|
11771
10699
|
return true;
|
|
11772
10700
|
}
|
|
11773
10701
|
/** Mirror the in-memory cache to disk, but only when something a restart
|
|
@@ -11801,7 +10729,7 @@ class WebTerminalManager {
|
|
|
11801
10729
|
try {
|
|
11802
10730
|
this.onNotify({ terminalId: item.id, title: item.title || "Terminal", event });
|
|
11803
10731
|
} catch (e) {
|
|
11804
|
-
logger.debug(`[WEB TERMINAL] notify callback failed: ${e}`);
|
|
10732
|
+
persistence.logger.debug(`[WEB TERMINAL] notify callback failed: ${e}`);
|
|
11805
10733
|
}
|
|
11806
10734
|
}
|
|
11807
10735
|
}
|
|
@@ -11815,7 +10743,7 @@ class WebTerminalManager {
|
|
|
11815
10743
|
this.snapshots.sweep(now);
|
|
11816
10744
|
for (const [id, session] of [...this.terminals]) {
|
|
11817
10745
|
if (session.subscribers === 0 && now - session.lastTouch > SESSION_IDLE_MS) {
|
|
11818
|
-
logger.debug(`[WEB TERMINAL] reaping orphaned idle session ${id} (idle ${Math.round((now - session.lastTouch) / 6e4)}m)`);
|
|
10746
|
+
persistence.logger.debug(`[WEB TERMINAL] reaping orphaned idle session ${id} (idle ${Math.round((now - session.lastTouch) / 6e4)}m)`);
|
|
11819
10747
|
this.detach(id);
|
|
11820
10748
|
}
|
|
11821
10749
|
}
|
|
@@ -11841,8 +10769,8 @@ class WebTerminalManager {
|
|
|
11841
10769
|
}
|
|
11842
10770
|
const victimId = unwatchedVictim ?? anyVictim;
|
|
11843
10771
|
if (!victimId) break;
|
|
11844
|
-
if (!unwatchedVictim) logger.debug(`[WEB TERMINAL] pty cap reached with all sessions claiming watchers; force-evicting LRU ${victimId}`);
|
|
11845
|
-
else logger.debug(`[WEB TERMINAL] pty cap reached (${this.terminals.size}); evicting LRU ${victimId}`);
|
|
10772
|
+
if (!unwatchedVictim) persistence.logger.debug(`[WEB TERMINAL] pty cap reached with all sessions claiming watchers; force-evicting LRU ${victimId}`);
|
|
10773
|
+
else persistence.logger.debug(`[WEB TERMINAL] pty cap reached (${this.terminals.size}); evicting LRU ${victimId}`);
|
|
11846
10774
|
this.detach(victimId);
|
|
11847
10775
|
}
|
|
11848
10776
|
}
|
|
@@ -11867,7 +10795,7 @@ class WebTerminalManager {
|
|
|
11867
10795
|
async open(opts) {
|
|
11868
10796
|
const cols = Math.max(2, Math.floor(opts.cols ?? 80));
|
|
11869
10797
|
const rows = Math.max(2, Math.floor(opts.rows ?? 24));
|
|
11870
|
-
const cwd = opts.cwd && opts.cwd.length > 0 ? opts.cwd : os.homedir();
|
|
10798
|
+
const cwd = opts.cwd && opts.cwd.length > 0 ? opts.cwd : os$1.homedir();
|
|
11871
10799
|
const id = opts.terminalId && /^[a-zA-Z0-9_-]{1,64}$/.test(opts.terminalId) ? opts.terminalId : node_crypto.randomBytes(5).toString("hex");
|
|
11872
10800
|
const lines = opts.streamMode === "lines";
|
|
11873
10801
|
const existing = this.terminals.get(id);
|
|
@@ -11878,7 +10806,7 @@ class WebTerminalManager {
|
|
|
11878
10806
|
this.applyResize(existing, cols, rows);
|
|
11879
10807
|
if (opts.fromSeq !== void 0 && existing.ringCovers(opts.fromSeq)) {
|
|
11880
10808
|
const chunks = existing.chunksAfter(opts.fromSeq);
|
|
11881
|
-
logger.debug(`[WEB TERMINAL] re-subscribed ${id} (subs=${existing.subscribers}, mode=replay, seq=${existing.seq})`);
|
|
10809
|
+
persistence.logger.debug(`[WEB TERMINAL] re-subscribed ${id} (subs=${existing.subscribers}, mode=replay, seq=${existing.seq})`);
|
|
11882
10810
|
return {
|
|
11883
10811
|
terminalId: id,
|
|
11884
10812
|
tmuxSession: existing.tmuxSession,
|
|
@@ -11897,7 +10825,7 @@ class WebTerminalManager {
|
|
|
11897
10825
|
return { ...await this.linesSnapshotResponse(id, existing), ...echo2 };
|
|
11898
10826
|
}
|
|
11899
10827
|
const state = existing.subscribeState(opts.fromSeq);
|
|
11900
|
-
logger.debug(`[WEB TERMINAL] re-subscribed ${id} (subs=${existing.subscribers}, mode=${state.mode}, seq=${state.seq})`);
|
|
10828
|
+
persistence.logger.debug(`[WEB TERMINAL] re-subscribed ${id} (subs=${existing.subscribers}, mode=${state.mode}, seq=${state.seq})`);
|
|
11901
10829
|
return { terminalId: id, tmuxSession: existing.tmuxSession, ...state, ...echo2 };
|
|
11902
10830
|
}
|
|
11903
10831
|
if (opts.attachOnly || opts.resub) {
|
|
@@ -11914,7 +10842,7 @@ class WebTerminalManager {
|
|
|
11914
10842
|
{ stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() }
|
|
11915
10843
|
).status === 0;
|
|
11916
10844
|
if (!alive) {
|
|
11917
|
-
logger.debug(`[WEB TERMINAL] refused stale-client resurrection of tombstoned ${id}`);
|
|
10845
|
+
persistence.logger.debug(`[WEB TERMINAL] refused stale-client resurrection of tombstoned ${id}`);
|
|
11918
10846
|
throw new Error("terminal-gone");
|
|
11919
10847
|
}
|
|
11920
10848
|
}
|
|
@@ -11936,7 +10864,7 @@ class WebTerminalManager {
|
|
|
11936
10864
|
"-e",
|
|
11937
10865
|
`VH_TERMINAL_ID=${id}`,
|
|
11938
10866
|
"-e",
|
|
11939
|
-
`VH_HAPPY_HOME_DIR=${configuration.happyHomeDir}`
|
|
10867
|
+
`VH_HAPPY_HOME_DIR=${persistence.configuration.happyHomeDir}`
|
|
11940
10868
|
] : [];
|
|
11941
10869
|
const attachOnly = !!(opts.attachOnly || opts.resub);
|
|
11942
10870
|
const attachTarget = !attachOnly && opts.attachTmux ? this.resolveAttachTarget(opts.attachTmux) : void 0;
|
|
@@ -12022,7 +10950,7 @@ class WebTerminalManager {
|
|
|
12022
10950
|
).status === 0;
|
|
12023
10951
|
if (!alive) {
|
|
12024
10952
|
if (attachTarget) throw new Error("tmux-unavailable");
|
|
12025
|
-
logger.info(`[WEB TERMINAL] tmux session creation unavailable; using direct PTY for ${id}`);
|
|
10953
|
+
persistence.logger.info(`[WEB TERMINAL] tmux session creation unavailable; using direct PTY for ${id}`);
|
|
12026
10954
|
tmuxSession = void 0;
|
|
12027
10955
|
}
|
|
12028
10956
|
}
|
|
@@ -12060,11 +10988,11 @@ class WebTerminalManager {
|
|
|
12060
10988
|
created.announcedRows = size.rows;
|
|
12061
10989
|
const marker = created.ingest(geometryMarker(size.cols, size.rows));
|
|
12062
10990
|
this.emit("terminal-output", { terminalId: id, data: marker.data, seq: marker.seq });
|
|
12063
|
-
logger.debug(`[WEB TERMINAL] ${id} pane geometry \u2192 ${size.cols}x${size.rows} (seq=${marker.seq})`);
|
|
10991
|
+
persistence.logger.debug(`[WEB TERMINAL] ${id} pane geometry \u2192 ${size.cols}x${size.rows} (seq=${marker.seq})`);
|
|
12064
10992
|
return;
|
|
12065
10993
|
}
|
|
12066
10994
|
if (name === "exit") {
|
|
12067
|
-
logger.debug(`[WEB TERMINAL] control client for ${id} got %exit (${args2})`);
|
|
10995
|
+
persistence.logger.debug(`[WEB TERMINAL] control client for ${id} got %exit (${args2})`);
|
|
12068
10996
|
}
|
|
12069
10997
|
},
|
|
12070
10998
|
onExit: (code) => {
|
|
@@ -12113,7 +11041,7 @@ class WebTerminalManager {
|
|
|
12113
11041
|
if (attachedTmux) session.attachedTmux = attachedTmux;
|
|
12114
11042
|
this.terminals.set(id, session);
|
|
12115
11043
|
session.onTitleChange(() => this.kickListRefresh());
|
|
12116
|
-
logger.debug(`[WEB TERMINAL] opened ${id} (${tmuxSession ? "control" : "pty"}) ${cols}x${rows} cwd=${cwd}`);
|
|
11044
|
+
persistence.logger.debug(`[WEB TERMINAL] opened ${id} (${tmuxSession ? "control" : "pty"}) ${cols}x${rows} cwd=${cwd}`);
|
|
12117
11045
|
this.kickListRefresh();
|
|
12118
11046
|
const echo = attachedTmux ? { attachedTmux } : {};
|
|
12119
11047
|
if (session.transport.kind !== "control") {
|
|
@@ -12147,7 +11075,7 @@ class WebTerminalManager {
|
|
|
12147
11075
|
} catch {
|
|
12148
11076
|
}
|
|
12149
11077
|
}
|
|
12150
|
-
logger.debug(`[WEB TERMINAL] ${tmuxSession} attaches user tmux session ${attachTarget.id} (${attachTarget.name})`);
|
|
11078
|
+
persistence.logger.debug(`[WEB TERMINAL] ${tmuxSession} attaches user tmux session ${attachTarget.id} (${attachTarget.name})`);
|
|
12151
11079
|
return { id: attachTarget.id, name: attachTarget.name };
|
|
12152
11080
|
}
|
|
12153
11081
|
const startup = normalizeStartupCommand(startupCommand);
|
|
@@ -12156,7 +11084,7 @@ class WebTerminalManager {
|
|
|
12156
11084
|
for (const a of startupInjectionArgs(tmuxSession, startup)) {
|
|
12157
11085
|
node_child_process.spawnSync("tmux", tmuxArgs(a), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env });
|
|
12158
11086
|
}
|
|
12159
|
-
logger.debug(`[WEB TERMINAL] injected startup command into new session ${tmuxSession}`);
|
|
11087
|
+
persistence.logger.debug(`[WEB TERMINAL] injected startup command into new session ${tmuxSession}`);
|
|
12160
11088
|
} catch {
|
|
12161
11089
|
}
|
|
12162
11090
|
}
|
|
@@ -12206,7 +11134,7 @@ class WebTerminalManager {
|
|
|
12206
11134
|
);
|
|
12207
11135
|
this.lastUserSessionsProbe = { status: r.status, error: r.error?.message, stderr: (r.stderr || "").toString().trim() || void 0, stdoutBytes: typeof r.stdout === "string" ? r.stdout.length : 0 };
|
|
12208
11136
|
if (r.status !== 0 || typeof r.stdout !== "string") {
|
|
12209
|
-
logger.debug(`[WEB TERMINAL] list-tmux-sessions failed: ${JSON.stringify(this.lastUserSessionsProbe)}`);
|
|
11137
|
+
persistence.logger.debug(`[WEB TERMINAL] list-tmux-sessions failed: ${JSON.stringify(this.lastUserSessionsProbe)}`);
|
|
12210
11138
|
return [];
|
|
12211
11139
|
}
|
|
12212
11140
|
return parseUserSessions(r.stdout, max);
|
|
@@ -12253,7 +11181,7 @@ class WebTerminalManager {
|
|
|
12253
11181
|
try {
|
|
12254
11182
|
bodies = await client.send(requests, CAPTURE_TIMEOUT_MS);
|
|
12255
11183
|
} catch (e) {
|
|
12256
|
-
logger.debug(`[WEB TERMINAL] capture batch failed for ${session.id}: ${e}`);
|
|
11184
|
+
persistence.logger.debug(`[WEB TERMINAL] capture batch failed for ${session.id}: ${e}`);
|
|
12257
11185
|
if (fresh) {
|
|
12258
11186
|
this.terminals.delete(session.id);
|
|
12259
11187
|
session.dispose();
|
|
@@ -12277,7 +11205,7 @@ class WebTerminalManager {
|
|
|
12277
11205
|
}
|
|
12278
11206
|
/** Build the lines-mode open response from a completed capture. */
|
|
12279
11207
|
linesResponse(id, session, restored) {
|
|
12280
|
-
logger.debug(`[WEB TERMINAL] lines open ${id}: seq=${restored.seqAtAnchor} small=${restored.small.length}B full=${restored.full.length}B pages=${restored.totalPages} alt=${restored.alternateOn}`);
|
|
11208
|
+
persistence.logger.debug(`[WEB TERMINAL] lines open ${id}: seq=${restored.seqAtAnchor} small=${restored.small.length}B full=${restored.full.length}B pages=${restored.totalPages} alt=${restored.alternateOn}`);
|
|
12281
11209
|
return {
|
|
12282
11210
|
terminalId: id,
|
|
12283
11211
|
tmuxSession: session.tmuxSession,
|
|
@@ -12332,7 +11260,7 @@ class WebTerminalManager {
|
|
|
12332
11260
|
const target = session.paneId ?? `=${session.tmuxSession}:`;
|
|
12333
11261
|
const { commands, dropped } = encodeTerminalWrite(text, target, { normalizeKeyNames: true });
|
|
12334
11262
|
if (dropped) {
|
|
12335
|
-
logger.debug(`[WEB TERMINAL] dropped terminal auto-reply for ${terminalId}`);
|
|
11263
|
+
persistence.logger.debug(`[WEB TERMINAL] dropped terminal auto-reply for ${terminalId}`);
|
|
12336
11264
|
return;
|
|
12337
11265
|
}
|
|
12338
11266
|
if (commands.length === 0) return;
|
|
@@ -12393,7 +11321,7 @@ class WebTerminalManager {
|
|
|
12393
11321
|
try {
|
|
12394
11322
|
session.transport.pty.resize(c, r);
|
|
12395
11323
|
} catch (e) {
|
|
12396
|
-
logger.debug(`[WEB TERMINAL] resize ${session.id} failed: ${e}`);
|
|
11324
|
+
persistence.logger.debug(`[WEB TERMINAL] resize ${session.id} failed: ${e}`);
|
|
12397
11325
|
}
|
|
12398
11326
|
} else if (c !== session.cols || r !== session.rows) {
|
|
12399
11327
|
session.transport.client.sendFireAndForget([
|
|
@@ -12414,7 +11342,7 @@ class WebTerminalManager {
|
|
|
12414
11342
|
if (!session) return;
|
|
12415
11343
|
session.subscribers = Math.max(0, session.subscribers - 1);
|
|
12416
11344
|
session.lastTouch = Date.now();
|
|
12417
|
-
logger.debug(`[WEB TERMINAL] unsubscribed ${terminalId} (subs=${session.subscribers})`);
|
|
11345
|
+
persistence.logger.debug(`[WEB TERMINAL] unsubscribed ${terminalId} (subs=${session.subscribers})`);
|
|
12418
11346
|
}
|
|
12419
11347
|
/** Detach the pty (its single tmux client) without killing the tmux session.
|
|
12420
11348
|
* Used by the reaper / cap eviction. Reopening the id respawns a pty and
|
|
@@ -12426,7 +11354,7 @@ class WebTerminalManager {
|
|
|
12426
11354
|
this.snapshots.drop(terminalId);
|
|
12427
11355
|
this.captureInFlight.delete(terminalId);
|
|
12428
11356
|
session.dispose();
|
|
12429
|
-
logger.debug(`[WEB TERMINAL] detached ${terminalId} (tmux session survives)`);
|
|
11357
|
+
persistence.logger.debug(`[WEB TERMINAL] detached ${terminalId} (tmux session survives)`);
|
|
12430
11358
|
}
|
|
12431
11359
|
/** Stop every live session (test teardown / daemon shutdown). Control
|
|
12432
11360
|
* clients get the SIGTERM→SIGKILL discipline, so no tmux child is left
|
|
@@ -12450,20 +11378,20 @@ class WebTerminalManager {
|
|
|
12450
11378
|
const killed = node_child_process.spawnSync("tmux", tmuxArgs(["kill-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
|
|
12451
11379
|
const verified = node_child_process.spawnSync("tmux", tmuxArgs(["has-session", "-t", target]), { stdio: "ignore", env: ptyEnv() });
|
|
12452
11380
|
if (!tmuxKillVerified(killed, verified)) {
|
|
12453
|
-
logger.debug(`[WEB TERMINAL] failed to verify tmux session removal ${target}`);
|
|
11381
|
+
persistence.logger.debug(`[WEB TERMINAL] failed to verify tmux session removal ${target}`);
|
|
12454
11382
|
return false;
|
|
12455
11383
|
}
|
|
12456
11384
|
if (opts?.alsoAttached && info?.attachTmux) {
|
|
12457
11385
|
const matches = this.listUserTmuxSessions(Infinity).filter((s) => s.name === info.attachTmux);
|
|
12458
11386
|
if (matches.length === 1) {
|
|
12459
11387
|
node_child_process.spawnSync("tmux", tmuxArgs(["kill-session", "-t", matches[0].id]), { stdio: "ignore", timeout: TMUX_PROBE_TIMEOUT_MS, env: ptyEnv() });
|
|
12460
|
-
logger.info(`[WEB TERMINAL] killed attached user tmux session ${matches[0].id} (${info.attachTmux}) with ${terminalId}`);
|
|
11388
|
+
persistence.logger.info(`[WEB TERMINAL] killed attached user tmux session ${matches[0].id} (${info.attachTmux}) with ${terminalId}`);
|
|
12461
11389
|
} else {
|
|
12462
|
-
logger.debug(`[WEB TERMINAL] alsoAttached: no unique session named ${info.attachTmux} (${matches.length} matches)`);
|
|
11390
|
+
persistence.logger.debug(`[WEB TERMINAL] alsoAttached: no unique session named ${info.attachTmux} (${matches.length} matches)`);
|
|
12463
11391
|
}
|
|
12464
11392
|
}
|
|
12465
11393
|
if (info) {
|
|
12466
|
-
const mirror = pickMirrorForTerminal(readPersistedSessions(), terminalId);
|
|
11394
|
+
const mirror = pickMirrorForTerminal(persistence.readPersistedSessions(), terminalId);
|
|
12467
11395
|
this.recordClosed({
|
|
12468
11396
|
id: terminalId,
|
|
12469
11397
|
title: info.title,
|
|
@@ -12482,13 +11410,13 @@ class WebTerminalManager {
|
|
|
12482
11410
|
try {
|
|
12483
11411
|
this.onTerminalClosedCb?.(terminalId);
|
|
12484
11412
|
} catch (e) {
|
|
12485
|
-
logger.debug(`[WEB TERMINAL] terminal-closed callback failed: ${e}`);
|
|
11413
|
+
persistence.logger.debug(`[WEB TERMINAL] terminal-closed callback failed: ${e}`);
|
|
12486
11414
|
}
|
|
12487
11415
|
this.detach(terminalId);
|
|
12488
11416
|
this.tombstones[terminalId] = Date.now();
|
|
12489
11417
|
saveTombstones(this.tombstones);
|
|
12490
11418
|
this.notifyTracker.remove(terminalId);
|
|
12491
|
-
logger.debug(`[WEB TERMINAL] killed session vh-${terminalId}`);
|
|
11419
|
+
persistence.logger.debug(`[WEB TERMINAL] killed session vh-${terminalId}`);
|
|
12492
11420
|
this.kickListRefresh();
|
|
12493
11421
|
return true;
|
|
12494
11422
|
}
|
|
@@ -12526,7 +11454,7 @@ class WebTerminalManager {
|
|
|
12526
11454
|
{ encoding: "utf8", env }
|
|
12527
11455
|
);
|
|
12528
11456
|
if (r.status !== 0 || !r.stdout) return [];
|
|
12529
|
-
const hostname = os.hostname();
|
|
11457
|
+
const hostname = os$1.hostname();
|
|
12530
11458
|
const out = [];
|
|
12531
11459
|
for (const line of r.stdout.split("\n")) {
|
|
12532
11460
|
const s = parseSessionListLine(line);
|
|
@@ -12758,7 +11686,7 @@ class WebTerminalManager {
|
|
|
12758
11686
|
|
|
12759
11687
|
const TMUX_TIMEOUT_MS = 3e3;
|
|
12760
11688
|
const VH_LIST_SESSIONS_FORMAT = LIST_SESSIONS_FORMAT;
|
|
12761
|
-
function parseVhTerminals(stdout, hostname = os.hostname()) {
|
|
11689
|
+
function parseVhTerminals(stdout, hostname = os$1.hostname()) {
|
|
12762
11690
|
const out = [];
|
|
12763
11691
|
for (const line of stdout.split("\n")) {
|
|
12764
11692
|
const s = parseSessionListLine(line);
|
|
@@ -12897,7 +11825,7 @@ function decideBackfill(messages, maxLines) {
|
|
|
12897
11825
|
}
|
|
12898
11826
|
|
|
12899
11827
|
function detectCLIAvailability() {
|
|
12900
|
-
const isWindows = os
|
|
11828
|
+
const isWindows = os.platform() === "win32";
|
|
12901
11829
|
if (isWindows) {
|
|
12902
11830
|
return detectWindows();
|
|
12903
11831
|
}
|
|
@@ -12922,14 +11850,14 @@ function commandExistsWindows(name) {
|
|
|
12922
11850
|
const SAFE_COMMAND_NAME = /^[A-Za-z0-9._-]+$/;
|
|
12923
11851
|
function commandOnPath(name) {
|
|
12924
11852
|
if (!SAFE_COMMAND_NAME.test(name)) return false;
|
|
12925
|
-
return os
|
|
11853
|
+
return os.platform() === "win32" ? commandExistsWindows(name) : commandExists(name);
|
|
12926
11854
|
}
|
|
12927
11855
|
function detectPosix() {
|
|
12928
11856
|
const claude = commandExists("claude");
|
|
12929
11857
|
const codex = commandExists("codex");
|
|
12930
11858
|
const gemini = commandExists("gemini");
|
|
12931
11859
|
const openclawCommand = commandExists("openclaw");
|
|
12932
|
-
const openclawConfig = fs$1.existsSync(path
|
|
11860
|
+
const openclawConfig = fs$1.existsSync(path.join(os.homedir(), ".openclaw", "openclaw.json"));
|
|
12933
11861
|
const openclawEnv = !!process.env.OPENCLAW_GATEWAY_URL;
|
|
12934
11862
|
const openclaw = openclawCommand || openclawConfig || openclawEnv;
|
|
12935
11863
|
const pi = commandExists("pi") && commandExists("pi-acp");
|
|
@@ -12941,7 +11869,7 @@ function detectWindows() {
|
|
|
12941
11869
|
const codex = checkCommand("codex");
|
|
12942
11870
|
const gemini = checkCommand("gemini");
|
|
12943
11871
|
const openclawCommand = checkCommand("openclaw");
|
|
12944
|
-
const openclawConfig = fs$1.existsSync(path
|
|
11872
|
+
const openclawConfig = fs$1.existsSync(path.join(process.env.USERPROFILE || os.homedir(), ".openclaw", "openclaw.json"));
|
|
12945
11873
|
const openclawEnv = !!process.env.OPENCLAW_GATEWAY_URL;
|
|
12946
11874
|
const openclaw = openclawCommand || openclawConfig || openclawEnv;
|
|
12947
11875
|
const pi = checkCommand("pi") && checkCommand("pi-acp");
|
|
@@ -12984,10 +11912,10 @@ function deriveContentKeyPair(secret) {
|
|
|
12984
11912
|
secretKey: keyPair.secretKey
|
|
12985
11913
|
};
|
|
12986
11914
|
}
|
|
12987
|
-
function getLocalHappyAgentCredentialPath(happyHomeDir = configuration.happyHomeDir) {
|
|
12988
|
-
return path.join(happyHomeDir, "agent.key");
|
|
11915
|
+
function getLocalHappyAgentCredentialPath(happyHomeDir = persistence.configuration.happyHomeDir) {
|
|
11916
|
+
return path$1.join(happyHomeDir, "agent.key");
|
|
12989
11917
|
}
|
|
12990
|
-
function readLocalHappyAgentCredentials(happyHomeDir = configuration.happyHomeDir, serverUrl = configuration.serverUrl) {
|
|
11918
|
+
function readLocalHappyAgentCredentials(happyHomeDir = persistence.configuration.happyHomeDir, serverUrl = persistence.configuration.serverUrl) {
|
|
12991
11919
|
const credentialPath = getLocalHappyAgentCredentialPath(happyHomeDir);
|
|
12992
11920
|
if (!node_fs.existsSync(credentialPath)) {
|
|
12993
11921
|
return null;
|
|
@@ -12995,8 +11923,8 @@ function readLocalHappyAgentCredentials(happyHomeDir = configuration.happyHomeDi
|
|
|
12995
11923
|
try {
|
|
12996
11924
|
const parsed = AgentCredentialsSchema.parse(JSON.parse(node_fs.readFileSync(credentialPath, "utf8")));
|
|
12997
11925
|
if (!parsed.authServerUrl) return null;
|
|
12998
|
-
if (credentialRelayProblem(parsed.authServerUrl, serverUrl)) return null;
|
|
12999
|
-
const secret = decodeBase64(parsed.secret);
|
|
11926
|
+
if (persistence.credentialRelayProblem(parsed.authServerUrl, serverUrl)) return null;
|
|
11927
|
+
const secret = persistence.decodeBase64(parsed.secret);
|
|
13000
11928
|
return {
|
|
13001
11929
|
token: parsed.token,
|
|
13002
11930
|
authServerUrl: parsed.authServerUrl,
|
|
@@ -13007,10 +11935,10 @@ function readLocalHappyAgentCredentials(happyHomeDir = configuration.happyHomeDi
|
|
|
13007
11935
|
return null;
|
|
13008
11936
|
}
|
|
13009
11937
|
}
|
|
13010
|
-
function hasLocalHappyAgentAuth(happyHomeDir = configuration.happyHomeDir) {
|
|
11938
|
+
function hasLocalHappyAgentAuth(happyHomeDir = persistence.configuration.happyHomeDir) {
|
|
13011
11939
|
return readLocalHappyAgentCredentials(happyHomeDir) !== null;
|
|
13012
11940
|
}
|
|
13013
|
-
function detectResumeSupport(happyHomeDir = configuration.happyHomeDir) {
|
|
11941
|
+
function detectResumeSupport(happyHomeDir = persistence.configuration.happyHomeDir) {
|
|
13014
11942
|
const happyAgentAuthenticated = hasLocalHappyAgentAuth(happyHomeDir);
|
|
13015
11943
|
return {
|
|
13016
11944
|
rpcAvailable: happyAgentAuthenticated,
|
|
@@ -13043,7 +11971,7 @@ async function listClaudeProjectDirs(projectsRoot) {
|
|
|
13043
11971
|
}
|
|
13044
11972
|
const dirs = [];
|
|
13045
11973
|
for (const name of names) {
|
|
13046
|
-
const full = path.join(projectsRoot, name);
|
|
11974
|
+
const full = path$1.join(projectsRoot, name);
|
|
13047
11975
|
try {
|
|
13048
11976
|
if ((await promises.stat(full)).isDirectory()) dirs.push(full);
|
|
13049
11977
|
} catch {
|
|
@@ -13064,11 +11992,11 @@ async function collectCandidates(projectDirs, exclude) {
|
|
|
13064
11992
|
if (!name.endsWith(".jsonl")) continue;
|
|
13065
11993
|
const id = name.slice(0, -".jsonl".length);
|
|
13066
11994
|
if (!UUID_RE$1.test(id) || exclude.has(id.toLowerCase())) continue;
|
|
13067
|
-
const path
|
|
11995
|
+
const path = path$1.join(dir, name);
|
|
13068
11996
|
try {
|
|
13069
|
-
const s = await promises.stat(path
|
|
11997
|
+
const s = await promises.stat(path);
|
|
13070
11998
|
if (!s.isFile() || s.size === 0) continue;
|
|
13071
|
-
out.push({ path
|
|
11999
|
+
out.push({ path, id, updatedAt: s.mtimeMs, sizeBytes: s.size });
|
|
13072
12000
|
} catch {
|
|
13073
12001
|
}
|
|
13074
12002
|
}
|
|
@@ -13213,7 +12141,7 @@ class ForkSourceMissingError extends Error {
|
|
|
13213
12141
|
}
|
|
13214
12142
|
}
|
|
13215
12143
|
function jsonlPath(projectDir, sessionId) {
|
|
13216
|
-
return path.join(projectDir, `${sessionId}.jsonl`);
|
|
12144
|
+
return path$1.join(projectDir, `${sessionId}.jsonl`);
|
|
13217
12145
|
}
|
|
13218
12146
|
function isUserPrompt(parsed) {
|
|
13219
12147
|
if (!parsed || parsed.type !== "user") return false;
|
|
@@ -13221,13 +12149,25 @@ function isUserPrompt(parsed) {
|
|
|
13221
12149
|
const content = parsed.message?.content;
|
|
13222
12150
|
return typeof content === "string";
|
|
13223
12151
|
}
|
|
12152
|
+
function hasPromptAttachments(parsed) {
|
|
12153
|
+
const content = parsed.message?.content;
|
|
12154
|
+
return typeof content === "string" && content.includes("<attached_files>") || Array.isArray(content) && content.some((block) => block?.type !== "text" || typeof block.text === "string" && block.text.includes("<attached_files>"));
|
|
12155
|
+
}
|
|
12156
|
+
function userPromptText(parsed) {
|
|
12157
|
+
if (parsed?.type !== "user" || parsed.isSidechain || parsed.isMeta || parsed.isSynthetic) return null;
|
|
12158
|
+
const content = parsed.message?.content;
|
|
12159
|
+
if (typeof content === "string") return content.trim() ? content : null;
|
|
12160
|
+
if (!Array.isArray(content) || content.some((block) => block?.type === "tool_result")) return null;
|
|
12161
|
+
const text = content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
12162
|
+
return text.trim() ? text : null;
|
|
12163
|
+
}
|
|
13224
12164
|
async function discardForkedSession(projectDir, claudeSessionId) {
|
|
13225
12165
|
try {
|
|
13226
12166
|
await promises.unlink(jsonlPath(projectDir, claudeSessionId));
|
|
13227
|
-
logger.debug(`[CLAUDE FORK] Discarded unused fork ${claudeSessionId}`);
|
|
12167
|
+
persistence.logger.debug(`[CLAUDE FORK] Discarded unused fork ${claudeSessionId}`);
|
|
13228
12168
|
} catch (error) {
|
|
13229
12169
|
if (error.code !== "ENOENT") {
|
|
13230
|
-
logger.debug(`[CLAUDE FORK] Failed to discard fork ${claudeSessionId}:`, error);
|
|
12170
|
+
persistence.logger.debug(`[CLAUDE FORK] Failed to discard fork ${claudeSessionId}:`, error);
|
|
13231
12171
|
}
|
|
13232
12172
|
}
|
|
13233
12173
|
}
|
|
@@ -13243,7 +12183,7 @@ async function forkSession(projectDir, sourceClaudeSessionId) {
|
|
|
13243
12183
|
}
|
|
13244
12184
|
throw error;
|
|
13245
12185
|
}
|
|
13246
|
-
logger.debug(`[CLAUDE FORK] Forked ${sourceClaudeSessionId} -> ${newId}`);
|
|
12186
|
+
persistence.logger.debug(`[CLAUDE FORK] Forked ${sourceClaudeSessionId} -> ${newId}`);
|
|
13247
12187
|
return newId;
|
|
13248
12188
|
}
|
|
13249
12189
|
async function forkAndTruncateSession(projectDir, sourceClaudeSessionId, cutAfterUuid) {
|
|
@@ -13265,7 +12205,7 @@ async function forkAndTruncateSession(projectDir, sourceClaudeSessionId, cutAfte
|
|
|
13265
12205
|
try {
|
|
13266
12206
|
parsed = JSON.parse(line);
|
|
13267
12207
|
} catch {
|
|
13268
|
-
logger.debug(`[CLAUDE FORK] Skipped malformed JSON line during truncation copy`);
|
|
12208
|
+
persistence.logger.debug(`[CLAUDE FORK] Skipped malformed JSON line during truncation copy`);
|
|
13269
12209
|
}
|
|
13270
12210
|
if (foundMarker && parsed && isUserPrompt(parsed)) {
|
|
13271
12211
|
break;
|
|
@@ -13297,7 +12237,7 @@ async function forkAndTruncateSession(projectDir, sourceClaudeSessionId, cutAfte
|
|
|
13297
12237
|
throw new ForkTruncateUuidNotFoundError(cutAfterUuid, src);
|
|
13298
12238
|
}
|
|
13299
12239
|
await promises.rename(tempDst, finalDst);
|
|
13300
|
-
logger.debug(`[CLAUDE FORK] Forked ${sourceClaudeSessionId} -> ${newId}, cut after ${cutAfterUuid}`);
|
|
12240
|
+
persistence.logger.debug(`[CLAUDE FORK] Forked ${sourceClaudeSessionId} -> ${newId}, cut after ${cutAfterUuid}`);
|
|
13301
12241
|
return newId;
|
|
13302
12242
|
}
|
|
13303
12243
|
async function listClaudeRewindPoints(projectDir, claudeSessionId) {
|
|
@@ -13320,23 +12260,68 @@ async function listClaudeRewindPoints(projectDir, claudeSessionId) {
|
|
|
13320
12260
|
} catch {
|
|
13321
12261
|
continue;
|
|
13322
12262
|
}
|
|
13323
|
-
|
|
13324
|
-
if (
|
|
12263
|
+
const content = userPromptText(parsed);
|
|
12264
|
+
if (content === null) continue;
|
|
13325
12265
|
if (typeof parsed.uuid !== "string" || parsed.uuid.length === 0) continue;
|
|
13326
|
-
const content = parsed.message?.content;
|
|
13327
|
-
if (typeof content !== "string") continue;
|
|
13328
|
-
const trimmed = content.trim();
|
|
13329
|
-
if (trimmed.length === 0) continue;
|
|
13330
12266
|
const timestampRaw = parsed.timestamp;
|
|
13331
12267
|
const timestamp = typeof timestampRaw === "string" ? Date.parse(timestampRaw) : typeof timestampRaw === "number" ? timestampRaw : Date.now();
|
|
13332
12268
|
points.push({
|
|
13333
12269
|
uuid: parsed.uuid,
|
|
13334
12270
|
text: content,
|
|
12271
|
+
...hasPromptAttachments(parsed) ? { hasAttachments: true } : {},
|
|
13335
12272
|
timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
|
|
13336
12273
|
});
|
|
13337
12274
|
}
|
|
13338
12275
|
return points;
|
|
13339
12276
|
}
|
|
12277
|
+
async function forkBeforeUserMessage(projectDir, sourceClaudeSessionId, cutBeforeUuid) {
|
|
12278
|
+
const src = jsonlPath(projectDir, sourceClaudeSessionId);
|
|
12279
|
+
let raw;
|
|
12280
|
+
try {
|
|
12281
|
+
raw = await promises.readFile(src, "utf-8");
|
|
12282
|
+
} catch (error) {
|
|
12283
|
+
if (error.code === "ENOENT") throw new ForkSourceMissingError(src);
|
|
12284
|
+
throw error;
|
|
12285
|
+
}
|
|
12286
|
+
const lines = raw.split("\n");
|
|
12287
|
+
let hasPreviousConversation = false;
|
|
12288
|
+
let hasPreviousPrompt = false;
|
|
12289
|
+
let cut = -1;
|
|
12290
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12291
|
+
let entry;
|
|
12292
|
+
try {
|
|
12293
|
+
entry = JSON.parse(lines[i]);
|
|
12294
|
+
} catch {
|
|
12295
|
+
continue;
|
|
12296
|
+
}
|
|
12297
|
+
if (userPromptText(entry) !== null && entry.uuid === cutBeforeUuid) {
|
|
12298
|
+
if (hasPromptAttachments(entry)) throw new Error("Cannot edit and rerun a message with attachments");
|
|
12299
|
+
cut = i;
|
|
12300
|
+
break;
|
|
12301
|
+
}
|
|
12302
|
+
if (!entry?.isSidechain && !entry?.isMeta && !entry?.isSynthetic && (entry?.type === "user" || entry?.type === "assistant")) {
|
|
12303
|
+
hasPreviousConversation = true;
|
|
12304
|
+
const content = entry.message?.content;
|
|
12305
|
+
if (entry.type === "user" && (typeof content === "string" || Array.isArray(content) && content.length > 0 && !content.some((block) => block?.type === "tool_result"))) {
|
|
12306
|
+
hasPreviousPrompt = true;
|
|
12307
|
+
}
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
12310
|
+
if (cut < 0) throw new ForkTruncateUuidNotFoundError(cutBeforeUuid, src);
|
|
12311
|
+
if (!hasPreviousConversation) return null;
|
|
12312
|
+
if (!hasPreviousPrompt) throw new Error("Cannot safely rewind: earlier history has no user prompt");
|
|
12313
|
+
const newId = node_crypto.randomUUID();
|
|
12314
|
+
const destination = jsonlPath(projectDir, newId);
|
|
12315
|
+
const temporary = `${destination}.tmp-${process.pid}`;
|
|
12316
|
+
try {
|
|
12317
|
+
await promises.writeFile(temporary, lines.slice(0, cut).join("\n") + "\n", { encoding: "utf-8", flag: "wx" });
|
|
12318
|
+
await promises.rename(temporary, destination);
|
|
12319
|
+
} catch (error) {
|
|
12320
|
+
await promises.unlink(temporary).catch(() => void 0);
|
|
12321
|
+
throw error;
|
|
12322
|
+
}
|
|
12323
|
+
return newId;
|
|
12324
|
+
}
|
|
13340
12325
|
|
|
13341
12326
|
function serializedByteLength(value) {
|
|
13342
12327
|
try {
|
|
@@ -13389,11 +12374,11 @@ function errorLogMetadata(error) {
|
|
|
13389
12374
|
}
|
|
13390
12375
|
|
|
13391
12376
|
function expandPath(pathValue, sessionPath) {
|
|
13392
|
-
const expandedHome = pathValue.replace(/^~(?=\/|$)/, os.homedir());
|
|
13393
|
-
if (path.isAbsolute(expandedHome)) {
|
|
12377
|
+
const expandedHome = pathValue.replace(/^~(?=\/|$)/, os$1.homedir());
|
|
12378
|
+
if (path$1.isAbsolute(expandedHome)) {
|
|
13394
12379
|
return expandedHome;
|
|
13395
12380
|
}
|
|
13396
|
-
return path.resolve(sessionPath, expandedHome);
|
|
12381
|
+
return path$1.resolve(sessionPath, expandedHome);
|
|
13397
12382
|
}
|
|
13398
12383
|
function resolvePaths(paths, sessionPath) {
|
|
13399
12384
|
return paths.map((pathValue) => expandPath(pathValue, sessionPath));
|
|
@@ -13415,10 +12400,10 @@ function buildSandboxRuntimeConfig(sandboxConfig, sessionPath) {
|
|
|
13415
12400
|
const allowWrite = (() => {
|
|
13416
12401
|
switch (sandboxConfig.sessionIsolation) {
|
|
13417
12402
|
case "strict":
|
|
13418
|
-
return uniquePaths([path.resolve(sessionPath), ...extraWritePaths, ...sharedAgentStatePaths]);
|
|
12403
|
+
return uniquePaths([path$1.resolve(sessionPath), ...extraWritePaths, ...sharedAgentStatePaths]);
|
|
13419
12404
|
case "workspace": {
|
|
13420
|
-
const workspaceRoot = sandboxConfig.workspaceRoot ? expandPath(sandboxConfig.workspaceRoot, sessionPath) : path.resolve(sessionPath);
|
|
13421
|
-
return uniquePaths([workspaceRoot, path.resolve(sessionPath), ...extraWritePaths, ...sharedAgentStatePaths]);
|
|
12405
|
+
const workspaceRoot = sandboxConfig.workspaceRoot ? expandPath(sandboxConfig.workspaceRoot, sessionPath) : path$1.resolve(sessionPath);
|
|
12406
|
+
return uniquePaths([workspaceRoot, path$1.resolve(sessionPath), ...extraWritePaths, ...sharedAgentStatePaths]);
|
|
13422
12407
|
}
|
|
13423
12408
|
case "custom":
|
|
13424
12409
|
return uniquePaths([
|
|
@@ -13780,9 +12765,9 @@ class CodexAppServerClient {
|
|
|
13780
12765
|
command = wrapped.command;
|
|
13781
12766
|
args = wrapped.args;
|
|
13782
12767
|
this.sandboxEnabled = true;
|
|
13783
|
-
logger.info(`[CodexAppServer] Sandbox enabled`);
|
|
12768
|
+
persistence.logger.info(`[CodexAppServer] Sandbox enabled`);
|
|
13784
12769
|
} catch (error) {
|
|
13785
|
-
logger.warn("[CodexAppServer] Failed to initialize sandbox; continuing without.", error);
|
|
12770
|
+
persistence.logger.warn("[CodexAppServer] Failed to initialize sandbox; continuing without.", error);
|
|
13786
12771
|
this.sandboxCleanup = null;
|
|
13787
12772
|
}
|
|
13788
12773
|
}
|
|
@@ -13799,7 +12784,7 @@ class CodexAppServerClient {
|
|
|
13799
12784
|
if (this.sandboxEnabled) {
|
|
13800
12785
|
env.CODEX_SANDBOX = "seatbelt";
|
|
13801
12786
|
}
|
|
13802
|
-
logger.debug(`[CodexAppServer] Spawning: ${command} ${args.join(" ")}`);
|
|
12787
|
+
persistence.logger.debug(`[CodexAppServer] Spawning: ${command} ${args.join(" ")}`);
|
|
13803
12788
|
const epoch = ++this.processEpoch;
|
|
13804
12789
|
const proc = spawn.spawn(command, args, {
|
|
13805
12790
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -13808,12 +12793,12 @@ class CodexAppServerClient {
|
|
|
13808
12793
|
});
|
|
13809
12794
|
this.process = proc;
|
|
13810
12795
|
proc.on("error", (err) => {
|
|
13811
|
-
logger.debug("[CodexAppServer] Process error:", err);
|
|
12796
|
+
persistence.logger.debug("[CodexAppServer] Process error:", err);
|
|
13812
12797
|
});
|
|
13813
12798
|
proc.on("exit", (code, signal) => {
|
|
13814
|
-
logger.debug(`[CodexAppServer] Process exited: code=${code} signal=${signal}`);
|
|
12799
|
+
persistence.logger.debug(`[CodexAppServer] Process exited: code=${code} signal=${signal}`);
|
|
13815
12800
|
if (this.process !== proc || this.processEpoch !== epoch) {
|
|
13816
|
-
logger.debug("[CodexAppServer] Ignoring stale process exit");
|
|
12801
|
+
persistence.logger.debug("[CodexAppServer] Ignoring stale process exit");
|
|
13817
12802
|
return;
|
|
13818
12803
|
}
|
|
13819
12804
|
this.connected = false;
|
|
@@ -13827,7 +12812,7 @@ class CodexAppServerClient {
|
|
|
13827
12812
|
proc.stderr?.on("data", (chunk) => {
|
|
13828
12813
|
if (this.process !== proc || this.processEpoch !== epoch) return;
|
|
13829
12814
|
const text = chunk.toString().trim();
|
|
13830
|
-
if (text) logger.debug("[CodexAppServer:stderr]", contentLogMetadata(text));
|
|
12815
|
+
if (text) persistence.logger.debug("[CodexAppServer:stderr]", contentLogMetadata(text));
|
|
13831
12816
|
});
|
|
13832
12817
|
this.readline = node_readline.createInterface({ input: proc.stdout });
|
|
13833
12818
|
this.readline.on("line", (line) => {
|
|
@@ -13838,7 +12823,7 @@ class CodexAppServerClient {
|
|
|
13838
12823
|
clientInfo: {
|
|
13839
12824
|
name: "happy-codex",
|
|
13840
12825
|
title: "Happy Codex Client",
|
|
13841
|
-
version: packageJson.version
|
|
12826
|
+
version: persistence.packageJson.version
|
|
13842
12827
|
},
|
|
13843
12828
|
capabilities: {
|
|
13844
12829
|
experimentalApi: true
|
|
@@ -13847,14 +12832,14 @@ class CodexAppServerClient {
|
|
|
13847
12832
|
await this.request("initialize", initParams);
|
|
13848
12833
|
this.notify("initialized");
|
|
13849
12834
|
this.connected = true;
|
|
13850
|
-
logger.debug("[CodexAppServer] Connected and initialized");
|
|
12835
|
+
persistence.logger.debug("[CodexAppServer] Connected and initialized");
|
|
13851
12836
|
}
|
|
13852
12837
|
async disconnectInternal(opts) {
|
|
13853
12838
|
if (!this.connected && !this.process) return;
|
|
13854
12839
|
const proc = this.process;
|
|
13855
12840
|
const pid = proc?.pid;
|
|
13856
12841
|
const epoch = this.processEpoch;
|
|
13857
|
-
logger.debug(`[CodexAppServer] Disconnecting; pid=${pid ?? "none"}`);
|
|
12842
|
+
persistence.logger.debug(`[CodexAppServer] Disconnecting; pid=${pid ?? "none"}`);
|
|
13858
12843
|
this.readline?.close();
|
|
13859
12844
|
this.readline = null;
|
|
13860
12845
|
try {
|
|
@@ -13895,7 +12880,7 @@ class CodexAppServerClient {
|
|
|
13895
12880
|
this.sandboxCleanup = null;
|
|
13896
12881
|
}
|
|
13897
12882
|
this.sandboxEnabled = false;
|
|
13898
|
-
logger.debug("[CodexAppServer] Disconnected");
|
|
12883
|
+
persistence.logger.debug("[CodexAppServer] Disconnected");
|
|
13899
12884
|
}
|
|
13900
12885
|
async disconnect() {
|
|
13901
12886
|
await this.disconnectInternal();
|
|
@@ -13933,7 +12918,7 @@ class CodexAppServerClient {
|
|
|
13933
12918
|
this._threadId = result.thread.id;
|
|
13934
12919
|
this._turnId = null;
|
|
13935
12920
|
this.rememberThreadDefaults(opts);
|
|
13936
|
-
logger.debug("[CodexAppServer] Thread started:", this._threadId);
|
|
12921
|
+
persistence.logger.debug("[CodexAppServer] Thread started:", this._threadId);
|
|
13937
12922
|
return { threadId: result.thread.id, model: result.model };
|
|
13938
12923
|
}
|
|
13939
12924
|
async resumeThread(opts) {
|
|
@@ -13964,7 +12949,7 @@ class CodexAppServerClient {
|
|
|
13964
12949
|
sandbox: opts?.sandbox ?? defaults.sandbox,
|
|
13965
12950
|
mcpServers: opts?.mcpServers ?? defaults.mcpServers
|
|
13966
12951
|
});
|
|
13967
|
-
logger.debug("[CodexAppServer] Thread resumed:", this._threadId);
|
|
12952
|
+
persistence.logger.debug("[CodexAppServer] Thread resumed:", this._threadId);
|
|
13968
12953
|
return { threadId: result.thread.id, model: result.model };
|
|
13969
12954
|
}
|
|
13970
12955
|
async forkThread(opts) {
|
|
@@ -13992,7 +12977,7 @@ class CodexAppServerClient {
|
|
|
13992
12977
|
sandbox: opts.sandbox ?? defaults.sandbox,
|
|
13993
12978
|
mcpServers: opts.mcpServers ?? defaults.mcpServers
|
|
13994
12979
|
});
|
|
13995
|
-
logger.debug("[CodexAppServer] Thread forked:", opts.threadId, "->", this._threadId);
|
|
12980
|
+
persistence.logger.debug("[CodexAppServer] Thread forked:", opts.threadId, "->", this._threadId);
|
|
13996
12981
|
return { threadId: result.thread.id, model: result.model, thread: result.thread };
|
|
13997
12982
|
}
|
|
13998
12983
|
async readThread(opts) {
|
|
@@ -14027,7 +13012,7 @@ class CodexAppServerClient {
|
|
|
14027
13012
|
await this.resumeThread({ threadId });
|
|
14028
13013
|
return true;
|
|
14029
13014
|
} catch (error) {
|
|
14030
|
-
logger.warn("[CodexAppServer] Failed to resume thread after reconnect", error);
|
|
13015
|
+
persistence.logger.warn("[CodexAppServer] Failed to resume thread after reconnect", error);
|
|
14031
13016
|
this._threadId = null;
|
|
14032
13017
|
this.threadDefaults = null;
|
|
14033
13018
|
return false;
|
|
@@ -14054,7 +13039,7 @@ class CodexAppServerClient {
|
|
|
14054
13039
|
const pending = this.pendingTurnCompletion;
|
|
14055
13040
|
if (!pending) return;
|
|
14056
13041
|
if (pending.turnId && turnId && pending.turnId !== turnId) {
|
|
14057
|
-
logger.debug(
|
|
13042
|
+
persistence.logger.debug(
|
|
14058
13043
|
`[CodexAppServer] Ignoring ${source} for turn ${turnId}; awaiting ${pending.turnId}`
|
|
14059
13044
|
);
|
|
14060
13045
|
return;
|
|
@@ -14093,7 +13078,7 @@ class CodexAppServerClient {
|
|
|
14093
13078
|
if (!shouldForceRestart) {
|
|
14094
13079
|
return { hadActiveTurn: true, aborted: false, forcedRestart: false, resumedThread: false };
|
|
14095
13080
|
}
|
|
14096
|
-
logger.warn(`[CodexAppServer] interrupt did not settle turn in ${gracePeriodMs}ms; force-restarting app-server`);
|
|
13081
|
+
persistence.logger.warn(`[CodexAppServer] interrupt did not settle turn in ${gracePeriodMs}ms; force-restarting app-server`);
|
|
14097
13082
|
const pendingTurnId = this.pendingTurnCompletion?.turnId ?? this._turnId;
|
|
14098
13083
|
if (this.pendingTurnCompletion) {
|
|
14099
13084
|
this.eventHandler?.({
|
|
@@ -14167,7 +13152,7 @@ class CodexAppServerClient {
|
|
|
14167
13152
|
};
|
|
14168
13153
|
timer = setTimeout(() => {
|
|
14169
13154
|
if (this.pendingTurnCompletion) {
|
|
14170
|
-
logger.warn(`[CodexAppServer] Turn timed out after ${timeoutMs}ms \u2014 treating as abort`);
|
|
13155
|
+
persistence.logger.warn(`[CodexAppServer] Turn timed out after ${timeoutMs}ms \u2014 treating as abort`);
|
|
14171
13156
|
this.resolvePendingTurn(true);
|
|
14172
13157
|
}
|
|
14173
13158
|
}, timeoutMs);
|
|
@@ -14186,7 +13171,7 @@ class CodexAppServerClient {
|
|
|
14186
13171
|
async interruptTurn() {
|
|
14187
13172
|
if (!this._threadId) return;
|
|
14188
13173
|
if (!this._turnId) {
|
|
14189
|
-
logger.debug("[CodexAppServer] interruptTurn: no active turnId, skipping");
|
|
13174
|
+
persistence.logger.debug("[CodexAppServer] interruptTurn: no active turnId, skipping");
|
|
14190
13175
|
return;
|
|
14191
13176
|
}
|
|
14192
13177
|
const params = {
|
|
@@ -14197,7 +13182,7 @@ class CodexAppServerClient {
|
|
|
14197
13182
|
try {
|
|
14198
13183
|
await this.request("turn/interrupt", params);
|
|
14199
13184
|
} catch (err) {
|
|
14200
|
-
logger.debug("[CodexAppServer] interruptTurn error (may be expected):", err);
|
|
13185
|
+
persistence.logger.debug("[CodexAppServer] interruptTurn error (may be expected):", err);
|
|
14201
13186
|
} finally {
|
|
14202
13187
|
this.pendingInterrupt = null;
|
|
14203
13188
|
}
|
|
@@ -14210,7 +13195,7 @@ class CodexAppServerClient {
|
|
|
14210
13195
|
return this._threadId !== null;
|
|
14211
13196
|
}
|
|
14212
13197
|
clearThreadState() {
|
|
14213
|
-
logger.debug(
|
|
13198
|
+
persistence.logger.debug(
|
|
14214
13199
|
`[CodexAppServer] Clearing thread state: thread=${this._threadId ?? "none"} turn=${this._turnId ?? "none"}`
|
|
14215
13200
|
);
|
|
14216
13201
|
this.resolvePendingTurn(true);
|
|
@@ -14249,7 +13234,7 @@ class CodexAppServerClient {
|
|
|
14249
13234
|
});
|
|
14250
13235
|
const msg = { jsonrpc: "2.0", id, method, params };
|
|
14251
13236
|
const line = JSON.stringify(msg) + "\n";
|
|
14252
|
-
logger.debug(`[CodexAppServer] \u2192 ${method} (id=${id})`);
|
|
13237
|
+
persistence.logger.debug(`[CodexAppServer] \u2192 ${method} (id=${id})`);
|
|
14253
13238
|
this.process.stdin.write(line);
|
|
14254
13239
|
});
|
|
14255
13240
|
}
|
|
@@ -14257,13 +13242,13 @@ class CodexAppServerClient {
|
|
|
14257
13242
|
if (!this.process?.stdin?.writable) return;
|
|
14258
13243
|
const msg = { jsonrpc: "2.0", method, params };
|
|
14259
13244
|
this.process.stdin.write(JSON.stringify(msg) + "\n");
|
|
14260
|
-
logger.debug(`[CodexAppServer] \u2192 ${method} (notification)`);
|
|
13245
|
+
persistence.logger.debug(`[CodexAppServer] \u2192 ${method} (notification)`);
|
|
14261
13246
|
}
|
|
14262
13247
|
respond(id, result) {
|
|
14263
13248
|
if (!this.process?.stdin?.writable) return;
|
|
14264
13249
|
const msg = { jsonrpc: "2.0", id, result };
|
|
14265
13250
|
this.process.stdin.write(JSON.stringify(msg) + "\n");
|
|
14266
|
-
logger.debug(`[CodexAppServer] \u2192 response (id=${id})`);
|
|
13251
|
+
persistence.logger.debug(`[CodexAppServer] \u2192 response (id=${id})`);
|
|
14267
13252
|
}
|
|
14268
13253
|
handleLine(line, sourceEpoch = this.processEpoch) {
|
|
14269
13254
|
if (sourceEpoch !== this.processEpoch) {
|
|
@@ -14274,14 +13259,14 @@ class CodexAppServerClient {
|
|
|
14274
13259
|
try {
|
|
14275
13260
|
msg = JSON.parse(line);
|
|
14276
13261
|
} catch {
|
|
14277
|
-
logger.debug("[CodexAppServer] Non-JSON line received:", contentLogMetadata(line));
|
|
13262
|
+
persistence.logger.debug("[CodexAppServer] Non-JSON line received:", contentLogMetadata(line));
|
|
14278
13263
|
return;
|
|
14279
13264
|
}
|
|
14280
13265
|
if (msg.id != null && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
14281
13266
|
const pending = this.pending.get(msg.id);
|
|
14282
13267
|
if (pending) {
|
|
14283
13268
|
if (pending.epoch !== sourceEpoch) {
|
|
14284
|
-
logger.debug(`[CodexAppServer] Ignoring response from stale epoch for id=${msg.id}`);
|
|
13269
|
+
persistence.logger.debug(`[CodexAppServer] Ignoring response from stale epoch for id=${msg.id}`);
|
|
14285
13270
|
return;
|
|
14286
13271
|
}
|
|
14287
13272
|
this.pending.delete(msg.id);
|
|
@@ -14295,7 +13280,7 @@ class CodexAppServerClient {
|
|
|
14295
13280
|
}
|
|
14296
13281
|
if (msg.id != null && msg.method) {
|
|
14297
13282
|
this.handleServerRequest(msg.id, msg.method, msg.params).catch((err) => {
|
|
14298
|
-
logger.debug("[CodexAppServer] Error handling server request:", errorLogMetadata(err));
|
|
13283
|
+
persistence.logger.debug("[CodexAppServer] Error handling server request:", errorLogMetadata(err));
|
|
14299
13284
|
});
|
|
14300
13285
|
return;
|
|
14301
13286
|
}
|
|
@@ -14303,7 +13288,7 @@ class CodexAppServerClient {
|
|
|
14303
13288
|
this.handleNotification(msg.method, msg.params);
|
|
14304
13289
|
return;
|
|
14305
13290
|
}
|
|
14306
|
-
logger.debug("[CodexAppServer] Unhandled message:", contentLogMetadata(msg));
|
|
13291
|
+
persistence.logger.debug("[CodexAppServer] Unhandled message:", contentLogMetadata(msg));
|
|
14307
13292
|
}
|
|
14308
13293
|
/**
|
|
14309
13294
|
* Map our internal ReviewDecision to the wire format the server expects.
|
|
@@ -14415,7 +13400,7 @@ class CodexAppServerClient {
|
|
|
14415
13400
|
this.respond(id, { decision: this.mapDecisionToWire(decision, legacy) });
|
|
14416
13401
|
return;
|
|
14417
13402
|
}
|
|
14418
|
-
logger.debug(`[CodexAppServer] Unknown server request: ${method}`);
|
|
13403
|
+
persistence.logger.debug(`[CodexAppServer] Unknown server request: ${method}`);
|
|
14419
13404
|
this.respond(id, {});
|
|
14420
13405
|
}
|
|
14421
13406
|
async handleApproval(params) {
|
|
@@ -14423,7 +13408,7 @@ class CodexAppServerClient {
|
|
|
14423
13408
|
try {
|
|
14424
13409
|
return await this.approvalHandler(params);
|
|
14425
13410
|
} catch (err) {
|
|
14426
|
-
logger.debug("[CodexAppServer] Approval handler error:", err);
|
|
13411
|
+
persistence.logger.debug("[CodexAppServer] Approval handler error:", err);
|
|
14427
13412
|
return "denied";
|
|
14428
13413
|
}
|
|
14429
13414
|
}
|
|
@@ -14457,11 +13442,11 @@ class CodexAppServerClient {
|
|
|
14457
13442
|
return;
|
|
14458
13443
|
}
|
|
14459
13444
|
if (this.handleRawNotification(method, params)) {
|
|
14460
|
-
logger.debug(`[CodexAppServer] Raw notification: ${method}`);
|
|
13445
|
+
persistence.logger.debug(`[CodexAppServer] Raw notification: ${method}`);
|
|
14461
13446
|
return;
|
|
14462
13447
|
}
|
|
14463
13448
|
if (method === "thread/started" || method === "turn/started" || method === "turn/completed" || method === "thread/status/changed") {
|
|
14464
|
-
logger.debug(`[CodexAppServer] Lifecycle notification: ${method}`);
|
|
13449
|
+
persistence.logger.debug(`[CodexAppServer] Lifecycle notification: ${method}`);
|
|
14465
13450
|
if (method === "turn/started") {
|
|
14466
13451
|
const turnId = this.extractTurnId(params);
|
|
14467
13452
|
if (turnId) {
|
|
@@ -14480,10 +13465,10 @@ class CodexAppServerClient {
|
|
|
14480
13465
|
return;
|
|
14481
13466
|
}
|
|
14482
13467
|
if (method === "mcpServer/startupStatus/updated") {
|
|
14483
|
-
logger.debug(`[CodexAppServer] mcpServer startup status:`, params);
|
|
13468
|
+
persistence.logger.debug(`[CodexAppServer] mcpServer startup status:`, params);
|
|
14484
13469
|
return;
|
|
14485
13470
|
}
|
|
14486
|
-
logger.debug(`[CodexAppServer] Notification: ${method}`);
|
|
13471
|
+
persistence.logger.debug(`[CodexAppServer] Notification: ${method}`);
|
|
14487
13472
|
}
|
|
14488
13473
|
}
|
|
14489
13474
|
|
|
@@ -14613,6 +13598,14 @@ class CodexForkRewindPointNotFoundError extends Error {
|
|
|
14613
13598
|
this.name = "CodexForkRewindPointNotFoundError";
|
|
14614
13599
|
}
|
|
14615
13600
|
}
|
|
13601
|
+
function hasUserAttachments(item) {
|
|
13602
|
+
const content = item.content;
|
|
13603
|
+
return Array.isArray(content) && content.some((part) => !part || typeof part !== "object" || part.type !== "text" || typeof part.text === "string" && part.text.includes("<attached_files>"));
|
|
13604
|
+
}
|
|
13605
|
+
function rejectAttachedTarget(thread, itemId) {
|
|
13606
|
+
const item = thread.turns?.flatMap((turn) => turn.items ?? []).find((item2) => item2.id === itemId);
|
|
13607
|
+
if (item && hasUserAttachments(item)) throw new Error("Cannot edit and rerun a message with attachments");
|
|
13608
|
+
}
|
|
14616
13609
|
function textFromUserItem(item) {
|
|
14617
13610
|
if (item.type !== "userMessage") {
|
|
14618
13611
|
return null;
|
|
@@ -14639,6 +13632,7 @@ function listCodexRewindPoints(thread) {
|
|
|
14639
13632
|
points.push({
|
|
14640
13633
|
itemId: item.id,
|
|
14641
13634
|
text,
|
|
13635
|
+
...hasUserAttachments(item) ? { hasAttachments: true } : {},
|
|
14642
13636
|
timestamp: timestampFromTurn(turn)
|
|
14643
13637
|
});
|
|
14644
13638
|
}
|
|
@@ -14696,6 +13690,37 @@ async function forkCodexThread(client, opts) {
|
|
|
14696
13690
|
newCodexThreadId: forked.threadId
|
|
14697
13691
|
};
|
|
14698
13692
|
}
|
|
13693
|
+
async function forkCodexBeforeUserMessage(client, opts) {
|
|
13694
|
+
const { thread: source } = await client.readThread({ threadId: opts.threadId, includeTurns: true });
|
|
13695
|
+
rejectAttachedTarget(source, opts.cutBeforeItemId);
|
|
13696
|
+
const sourceCut = findCutTurn(source, opts.cutBeforeItemId);
|
|
13697
|
+
if (!sourceCut) throw new CodexForkRewindPointNotFoundError(opts.cutBeforeItemId, opts.threadId);
|
|
13698
|
+
if ((source.turns?.[sourceCut.index].items ?? []).findIndex((item) => item.id === opts.cutBeforeItemId) !== 0) {
|
|
13699
|
+
throw new Error("Cannot safely rewind a user message inside an existing turn");
|
|
13700
|
+
}
|
|
13701
|
+
if (sourceCut.index === 0) return { type: "success", startFresh: true };
|
|
13702
|
+
const forked = await client.forkThread({ threadId: opts.threadId, cwd: opts.cwd });
|
|
13703
|
+
if (forked.threadId === opts.threadId) throw new Error("Codex did not create an independent fork");
|
|
13704
|
+
rejectAttachedTarget(forked.thread, opts.cutBeforeItemId);
|
|
13705
|
+
const cut = findCutTurn(forked.thread, opts.cutBeforeItemId);
|
|
13706
|
+
if (!cut) throw new CodexForkRewindPointNotFoundError(opts.cutBeforeItemId, opts.threadId);
|
|
13707
|
+
const turns = forked.thread.turns ?? [];
|
|
13708
|
+
const items = turns[cut.index].items ?? [];
|
|
13709
|
+
if (items.findIndex((item) => item.id === opts.cutBeforeItemId) !== 0) {
|
|
13710
|
+
throw new Error("Cannot safely rewind a user message inside an existing turn");
|
|
13711
|
+
}
|
|
13712
|
+
if (cut.index === 0) return { type: "success", startFresh: true };
|
|
13713
|
+
const expectedTurnIds = turns.slice(0, cut.index).map((turn) => turn.id);
|
|
13714
|
+
const { thread } = await client.rollbackThread({
|
|
13715
|
+
threadId: forked.threadId,
|
|
13716
|
+
numTurns: turns.length - cut.index
|
|
13717
|
+
});
|
|
13718
|
+
const remaining = thread.turns ?? [];
|
|
13719
|
+
if (remaining.length !== cut.index || remaining.some((turn, index) => turn.id !== expectedTurnIds[index])) {
|
|
13720
|
+
throw new Error("Codex rollback did not preserve the expected history");
|
|
13721
|
+
}
|
|
13722
|
+
return { type: "success", newCodexThreadId: forked.threadId };
|
|
13723
|
+
}
|
|
14699
13724
|
|
|
14700
13725
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14701
13726
|
function requireNonEmptyString(value, name) {
|
|
@@ -14722,7 +13747,7 @@ class ApiMachineClient {
|
|
|
14722
13747
|
scopePrefix: this.machine.id,
|
|
14723
13748
|
encryptionKey: this.machine.encryptionKey,
|
|
14724
13749
|
encryptionVariant: this.machine.encryptionVariant,
|
|
14725
|
-
logger: (msg, data) => logger.debug(msg, data)
|
|
13750
|
+
logger: (msg, data) => persistence.logger.debug(msg, data)
|
|
14726
13751
|
});
|
|
14727
13752
|
registerCommonHandlers(this.rpcHandlerManager, process.cwd());
|
|
14728
13753
|
registerFsHandlers(this.rpcHandlerManager);
|
|
@@ -14780,7 +13805,7 @@ class ApiMachineClient {
|
|
|
14780
13805
|
if (this.relaySocket?.connected) this.relaySocket.emit(event, out);
|
|
14781
13806
|
}, (n) => {
|
|
14782
13807
|
sendTerminalNotification({
|
|
14783
|
-
baseUrl: configuration.serverUrl,
|
|
13808
|
+
baseUrl: persistence.configuration.serverUrl,
|
|
14784
13809
|
token: this.token,
|
|
14785
13810
|
title: n.title,
|
|
14786
13811
|
message: terminalNotifyMessage(n.event),
|
|
@@ -14810,12 +13835,12 @@ class ApiMachineClient {
|
|
|
14810
13835
|
* Encrypted with the per-machine key; the server relays without reading.
|
|
14811
13836
|
*/
|
|
14812
13837
|
pushClipboard(text) {
|
|
14813
|
-
const prepared =
|
|
13838
|
+
const prepared = persistence.prepareClipboardText(text);
|
|
14814
13839
|
if (!this.socket?.connected) {
|
|
14815
13840
|
return { delivered: false, truncated: prepared.truncated, totalBytes: prepared.totalBytes, error: "daemon is not connected to the server" };
|
|
14816
13841
|
}
|
|
14817
13842
|
this.socket.emit("clipboard-push", {
|
|
14818
|
-
payload: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, prepared.text)),
|
|
13843
|
+
payload: persistence.encodeBase64(persistence.encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, prepared.text)),
|
|
14819
13844
|
enc: true,
|
|
14820
13845
|
truncated: prepared.truncated,
|
|
14821
13846
|
totalBytes: prepared.totalBytes
|
|
@@ -14826,7 +13851,7 @@ class ApiMachineClient {
|
|
|
14826
13851
|
* as the live output stream) → base64 ciphertext. Used for both live output
|
|
14827
13852
|
* and the open-terminal snapshot/replay payloads. */
|
|
14828
13853
|
encTerminalData(dataBase64) {
|
|
14829
|
-
return encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, dataBase64));
|
|
13854
|
+
return persistence.encodeBase64(persistence.encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, dataBase64));
|
|
14830
13855
|
}
|
|
14831
13856
|
setCliUpdateRetryHandler(handler) {
|
|
14832
13857
|
this.rpcHandlerManager.registerHandler("cli-update-retry", (params) => handler(params && typeof params === "object" ? params.version : void 0));
|
|
@@ -14836,7 +13861,7 @@ class ApiMachineClient {
|
|
|
14836
13861
|
setCliUpdateState(state) {
|
|
14837
13862
|
this.cliUpdateState = state;
|
|
14838
13863
|
if (!this.socket?.connected) return;
|
|
14839
|
-
this.cliUpdatePushChain = this.cliUpdatePushChain.then(() => this.updateDaemonState((current) => withCurrentCliUpdateState(current, this.cliUpdateState))).catch((error) => logger.debug("[API MACHINE] Failed to publish CLI update policy:", error));
|
|
13864
|
+
this.cliUpdatePushChain = this.cliUpdatePushChain.then(() => this.updateDaemonState((current) => withCurrentCliUpdateState(current, this.cliUpdateState))).catch((error) => persistence.logger.debug("[API MACHINE] Failed to publish CLI update policy:", error));
|
|
14840
13865
|
}
|
|
14841
13866
|
/**
|
|
14842
13867
|
* B-276: publish the daemon-context Claude auth preflight into daemonState
|
|
@@ -14853,7 +13878,7 @@ class ApiMachineClient {
|
|
|
14853
13878
|
status: current?.status ?? "running",
|
|
14854
13879
|
claudeAuth: state
|
|
14855
13880
|
}))).then(() => this.machine.daemonState?.claudeAuth?.checkedAt === state.checkedAt && this.machine.daemonState?.claudeAuth?.daemonPid === state.daemonPid).catch((error) => {
|
|
14856
|
-
logger.debug("[API MACHINE] Failed to publish Claude auth state:", error);
|
|
13881
|
+
persistence.logger.debug("[API MACHINE] Failed to publish Claude auth state:", error);
|
|
14857
13882
|
return false;
|
|
14858
13883
|
});
|
|
14859
13884
|
this.cliUpdatePushChain = attempt.then(() => void 0);
|
|
@@ -14890,17 +13915,17 @@ class ApiMachineClient {
|
|
|
14890
13915
|
this.listTrackedSessionIds = listTrackedSessionIds ?? null;
|
|
14891
13916
|
this.rpcHandlerManager.registerHandler("spawn-happy-session", async (params) => {
|
|
14892
13917
|
const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, environmentVariables, token, resumeClaudeSessionId, resumeCodexThreadId, parentSessionId, forkedFromMessageId, importedFromClaudeSessionId, variant, forceNew, permissionMode } = params || {};
|
|
14893
|
-
logger.debug("[API MACHINE] Spawning session:", summarizeSpawnSessionForLog(params));
|
|
13918
|
+
persistence.logger.debug("[API MACHINE] Spawning session:", summarizeSpawnSessionForLog(params));
|
|
14894
13919
|
if (!directory && variant !== "assistant") {
|
|
14895
13920
|
throw new Error("Directory is required");
|
|
14896
13921
|
}
|
|
14897
13922
|
const result = await spawnSession({ directory: directory || "", sessionId, machineId, approvedNewDirectoryCreation, agent, environmentVariables, token, resumeClaudeSessionId, resumeCodexThreadId, parentSessionId, forkedFromMessageId, importedFromClaudeSessionId: typeof importedFromClaudeSessionId === "string" ? importedFromClaudeSessionId : void 0, variant: variant === "assistant" ? "assistant" : void 0, forceNew: forceNew === true, permissionMode: typeof permissionMode === "string" ? permissionMode : void 0 });
|
|
14898
13923
|
switch (result.type) {
|
|
14899
13924
|
case "success":
|
|
14900
|
-
logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
|
|
13925
|
+
persistence.logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
|
|
14901
13926
|
return { type: "success", sessionId: result.sessionId };
|
|
14902
13927
|
case "requestToApproveDirectoryCreation":
|
|
14903
|
-
logger.debug(`[API MACHINE] Requesting directory creation approval for: ${result.directory}`);
|
|
13928
|
+
persistence.logger.debug(`[API MACHINE] Requesting directory creation approval for: ${result.directory}`);
|
|
14904
13929
|
return { type: "requestToApproveDirectoryCreation", directory: result.directory };
|
|
14905
13930
|
case "error":
|
|
14906
13931
|
throw new Error(result.errorMessage);
|
|
@@ -14948,7 +13973,7 @@ class ApiMachineClient {
|
|
|
14948
13973
|
await discardForkedSession(projectDir, newClaudeSessionId);
|
|
14949
13974
|
return result;
|
|
14950
13975
|
}
|
|
14951
|
-
logger.debug(`[API MACHINE] Imported Claude conversation ${claudeSessionId} as session ${result.sessionId}`);
|
|
13976
|
+
persistence.logger.debug(`[API MACHINE] Imported Claude conversation ${claudeSessionId} as session ${result.sessionId}`);
|
|
14952
13977
|
return { type: "success", sessionId: result.sessionId, newClaudeSessionId };
|
|
14953
13978
|
});
|
|
14954
13979
|
this.syncResumeSessionRpcRegistration();
|
|
@@ -15073,7 +14098,7 @@ class ApiMachineClient {
|
|
|
15073
14098
|
if (!success) {
|
|
15074
14099
|
throw new Error("Session not found or failed to stop");
|
|
15075
14100
|
}
|
|
15076
|
-
logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
|
|
14101
|
+
persistence.logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
|
|
15077
14102
|
return { message: "Session stopped" };
|
|
15078
14103
|
});
|
|
15079
14104
|
this.rpcHandlerManager.registerHandler("claude-fork-session", async (params) => {
|
|
@@ -15150,7 +14175,7 @@ class ApiMachineClient {
|
|
|
15150
14175
|
const projectDirs = typeof directory === "string" ? [getProjectPath(directory)] : await listClaudeProjectDirs(getClaudeProjectsRoot());
|
|
15151
14176
|
const excludeIds = [
|
|
15152
14177
|
...Array.isArray(exclude) ? exclude.filter((id) => typeof id === "string" && UUID_RE.test(id)) : [],
|
|
15153
|
-
...readTrackedClaudeSessionIds()
|
|
14178
|
+
...persistence.readTrackedClaudeSessionIds()
|
|
15154
14179
|
];
|
|
15155
14180
|
const result = await listClaudeSessionHistory({
|
|
15156
14181
|
projectDirs,
|
|
@@ -15200,10 +14225,30 @@ class ApiMachineClient {
|
|
|
15200
14225
|
throw error;
|
|
15201
14226
|
}
|
|
15202
14227
|
});
|
|
14228
|
+
this.rpcHandlerManager.registerHandler("claude-rewind-before-message", async (params) => {
|
|
14229
|
+
const directory = requireNonEmptyString(params?.directory, "directory");
|
|
14230
|
+
const claudeSessionId = requireNonEmptyString(params?.claudeSessionId, "claudeSessionId");
|
|
14231
|
+
const cutBeforeUuid = requireNonEmptyString(params?.cutBeforeUuid, "cutBeforeUuid");
|
|
14232
|
+
if (!UUID_RE.test(claudeSessionId) || !UUID_RE.test(cutBeforeUuid)) {
|
|
14233
|
+
throw new Error("claudeSessionId and cutBeforeUuid must be valid UUIDs");
|
|
14234
|
+
}
|
|
14235
|
+
const newClaudeSessionId = await forkBeforeUserMessage(getProjectPath(directory), claudeSessionId, cutBeforeUuid);
|
|
14236
|
+
return newClaudeSessionId === null ? { type: "success", startFresh: true } : { type: "success", newClaudeSessionId };
|
|
14237
|
+
});
|
|
14238
|
+
this.rpcHandlerManager.registerHandler("codex-rewind-before-message", async (params) => {
|
|
14239
|
+
const directory = requireNonEmptyString(params?.directory, "directory");
|
|
14240
|
+
const codexThreadId = requireNonEmptyString(params?.codexThreadId, "codexThreadId");
|
|
14241
|
+
const cutBeforeItemId = requireNonEmptyString(params?.cutBeforeItemId, "cutBeforeItemId");
|
|
14242
|
+
return withCodexAppServerClient((client) => forkCodexBeforeUserMessage(client, {
|
|
14243
|
+
threadId: codexThreadId,
|
|
14244
|
+
cwd: directory,
|
|
14245
|
+
cutBeforeItemId
|
|
14246
|
+
}));
|
|
14247
|
+
});
|
|
15203
14248
|
this.rpcHandlerManager.registerHandler("stop-daemon", () => {
|
|
15204
|
-
logger.debug("[API MACHINE] Received stop-daemon RPC request");
|
|
14249
|
+
persistence.logger.debug("[API MACHINE] Received stop-daemon RPC request");
|
|
15205
14250
|
setTimeout(() => {
|
|
15206
|
-
logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
|
|
14251
|
+
persistence.logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
|
|
15207
14252
|
requestShutdown();
|
|
15208
14253
|
}, 100);
|
|
15209
14254
|
return { message: "Daemon stop request acknowledged, starting shutdown sequence..." };
|
|
@@ -15286,7 +14331,7 @@ class ApiMachineClient {
|
|
|
15286
14331
|
wireAutoRestoreReport() {
|
|
15287
14332
|
this.webTerminal.setOnAutoRestoreSummary((line) => {
|
|
15288
14333
|
sendTerminalNotification({
|
|
15289
|
-
baseUrl: configuration.serverUrl,
|
|
14334
|
+
baseUrl: persistence.configuration.serverUrl,
|
|
15290
14335
|
token: this.token,
|
|
15291
14336
|
title: "Terminals restored",
|
|
15292
14337
|
message: line,
|
|
@@ -15322,7 +14367,7 @@ class ApiMachineClient {
|
|
|
15322
14367
|
try {
|
|
15323
14368
|
this.mirrorListObserver?.(terminals);
|
|
15324
14369
|
} catch (err) {
|
|
15325
|
-
logger.debug("[API MACHINE] mirror list observer failed:", err);
|
|
14370
|
+
persistence.logger.debug("[API MACHINE] mirror list observer failed:", err);
|
|
15326
14371
|
}
|
|
15327
14372
|
this.latestTerminalList = terminals;
|
|
15328
14373
|
if (!this.socket?.connected) return;
|
|
@@ -15341,9 +14386,9 @@ class ApiMachineClient {
|
|
|
15341
14386
|
// Read at write time — same freshness rule as the list.
|
|
15342
14387
|
closedTerminals: this.webTerminal.getClosedTerminals()
|
|
15343
14388
|
}));
|
|
15344
|
-
logger.debug(`[API MACHINE] Pushed terminal list (${list.length} terminals)`);
|
|
14389
|
+
persistence.logger.debug(`[API MACHINE] Pushed terminal list (${list.length} terminals)`);
|
|
15345
14390
|
}).catch((err) => {
|
|
15346
|
-
logger.debug("[API MACHINE] Terminal list push failed:", err);
|
|
14391
|
+
persistence.logger.debug("[API MACHINE] Terminal list push failed:", err);
|
|
15347
14392
|
});
|
|
15348
14393
|
}
|
|
15349
14394
|
/**
|
|
@@ -15357,18 +14402,18 @@ class ApiMachineClient {
|
|
|
15357
14402
|
const updated = handler(this.machine.metadata);
|
|
15358
14403
|
const answer = await this.socket.emitWithAck("machine-update-metadata", {
|
|
15359
14404
|
machineId: this.machine.id,
|
|
15360
|
-
metadata: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
|
|
14405
|
+
metadata: persistence.encodeBase64(persistence.encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
|
|
15361
14406
|
expectedVersion: this.machine.metadataVersion
|
|
15362
14407
|
});
|
|
15363
14408
|
if (answer.result === "success") {
|
|
15364
|
-
this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
|
|
14409
|
+
this.machine.metadata = persistence.decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, persistence.decodeBase64(answer.metadata));
|
|
15365
14410
|
this.machine.metadataVersion = answer.version;
|
|
15366
14411
|
rateRefusals = 0;
|
|
15367
|
-
logger.debug("[API MACHINE] Metadata updated successfully");
|
|
14412
|
+
persistence.logger.debug("[API MACHINE] Metadata updated successfully");
|
|
15368
14413
|
} else if (answer.result === "version-mismatch") {
|
|
15369
14414
|
if (answer.version > this.machine.metadataVersion) {
|
|
15370
14415
|
this.machine.metadataVersion = answer.version;
|
|
15371
|
-
this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
|
|
14416
|
+
this.machine.metadata = persistence.decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, persistence.decodeBase64(answer.metadata));
|
|
15372
14417
|
}
|
|
15373
14418
|
throw new Error("Metadata version mismatch");
|
|
15374
14419
|
} else if (answer.result === "error") {
|
|
@@ -15379,7 +14424,7 @@ class ApiMachineClient {
|
|
|
15379
14424
|
code: answer.error,
|
|
15380
14425
|
attempt: rateRefusals,
|
|
15381
14426
|
sleep: delay,
|
|
15382
|
-
log: (message) => logger.debug(message)
|
|
14427
|
+
log: (message) => persistence.logger.debug(message)
|
|
15383
14428
|
});
|
|
15384
14429
|
}
|
|
15385
14430
|
throw new Error("Machine metadata update failed");
|
|
@@ -15396,18 +14441,18 @@ class ApiMachineClient {
|
|
|
15396
14441
|
const updated = handler(this.machine.daemonState);
|
|
15397
14442
|
const answer = await this.socket.emitWithAck("machine-update-state", {
|
|
15398
14443
|
machineId: this.machine.id,
|
|
15399
|
-
daemonState: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
|
|
14444
|
+
daemonState: persistence.encodeBase64(persistence.encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
|
|
15400
14445
|
expectedVersion: this.machine.daemonStateVersion
|
|
15401
14446
|
});
|
|
15402
14447
|
if (answer.result === "success") {
|
|
15403
|
-
this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
|
|
14448
|
+
this.machine.daemonState = persistence.decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, persistence.decodeBase64(answer.daemonState));
|
|
15404
14449
|
this.machine.daemonStateVersion = answer.version;
|
|
15405
14450
|
rateRefusals = 0;
|
|
15406
|
-
logger.debug("[API MACHINE] Daemon state updated successfully");
|
|
14451
|
+
persistence.logger.debug("[API MACHINE] Daemon state updated successfully");
|
|
15407
14452
|
} else if (answer.result === "version-mismatch") {
|
|
15408
14453
|
if (answer.version > this.machine.daemonStateVersion) {
|
|
15409
14454
|
this.machine.daemonStateVersion = answer.version;
|
|
15410
|
-
this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
|
|
14455
|
+
this.machine.daemonState = persistence.decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, persistence.decodeBase64(answer.daemonState));
|
|
15411
14456
|
}
|
|
15412
14457
|
throw new Error("Daemon state version mismatch");
|
|
15413
14458
|
} else if (answer.result === "error") {
|
|
@@ -15418,7 +14463,7 @@ class ApiMachineClient {
|
|
|
15418
14463
|
code: answer.error,
|
|
15419
14464
|
attempt: rateRefusals,
|
|
15420
14465
|
sleep: delay,
|
|
15421
|
-
log: (message) => logger.debug(message)
|
|
14466
|
+
log: (message) => persistence.logger.debug(message)
|
|
15422
14467
|
});
|
|
15423
14468
|
}
|
|
15424
14469
|
throw new Error("Daemon state update failed");
|
|
@@ -15426,8 +14471,8 @@ class ApiMachineClient {
|
|
|
15426
14471
|
});
|
|
15427
14472
|
}
|
|
15428
14473
|
connect() {
|
|
15429
|
-
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
15430
|
-
logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
|
|
14474
|
+
const serverUrl = persistence.configuration.serverUrl.replace(/^http/, "ws");
|
|
14475
|
+
persistence.logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
|
|
15431
14476
|
this.socket = this.createControlSocket();
|
|
15432
14477
|
const controlSocket = this.socket;
|
|
15433
14478
|
this.bindRealtimeHandlers(controlSocket, "control");
|
|
@@ -15436,7 +14481,7 @@ class ApiMachineClient {
|
|
|
15436
14481
|
this.activateControlSocket(controlSocket);
|
|
15437
14482
|
});
|
|
15438
14483
|
controlSocket.on("disconnect", (reason) => {
|
|
15439
|
-
logger.debug(`[API MACHINE] Disconnected from server \u2014 reason: ${reason}`);
|
|
14484
|
+
persistence.logger.debug(`[API MACHINE] Disconnected from server \u2014 reason: ${reason}`);
|
|
15440
14485
|
this.rpcHandlerManager.onSocketDisconnect(controlSocket);
|
|
15441
14486
|
if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
|
|
15442
14487
|
this.stopKeepAlive();
|
|
@@ -15445,14 +14490,14 @@ class ApiMachineClient {
|
|
|
15445
14490
|
this.bindControlDataHandlers(controlSocket);
|
|
15446
14491
|
}
|
|
15447
14492
|
createControlSocket(handover) {
|
|
15448
|
-
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
14493
|
+
const serverUrl = persistence.configuration.serverUrl.replace(/^http/, "ws");
|
|
15449
14494
|
return socket_ioClient.io(serverUrl, {
|
|
15450
14495
|
transports: ["websocket"],
|
|
15451
14496
|
auth: {
|
|
15452
14497
|
token: this.token,
|
|
15453
14498
|
clientType: "machine-scoped",
|
|
15454
14499
|
machineId: this.machine.id,
|
|
15455
|
-
happyClient: `cli-daemon/${configuration.currentCliVersion}`,
|
|
14500
|
+
happyClient: `cli-daemon/${persistence.configuration.currentCliVersion}`,
|
|
15456
14501
|
...handover ? { handoverEpoch: handover.epoch } : {}
|
|
15457
14502
|
},
|
|
15458
14503
|
...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
|
|
@@ -15462,7 +14507,7 @@ class ApiMachineClient {
|
|
|
15462
14507
|
});
|
|
15463
14508
|
}
|
|
15464
14509
|
activateControlSocket(socket, rpcAlreadyRegistered = false) {
|
|
15465
|
-
logger.debug("[API MACHINE] Connected to server");
|
|
14510
|
+
persistence.logger.debug("[API MACHINE] Connected to server");
|
|
15466
14511
|
if (this.reconnectInterval) {
|
|
15467
14512
|
clearInterval(this.reconnectInterval);
|
|
15468
14513
|
this.reconnectInterval = null;
|
|
@@ -15508,25 +14553,25 @@ class ApiMachineClient {
|
|
|
15508
14553
|
if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
15509
14554
|
const update = data.body;
|
|
15510
14555
|
if (update.metadata) {
|
|
15511
|
-
logger.debug("[API MACHINE] Received external metadata update");
|
|
15512
|
-
this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.metadata.value));
|
|
14556
|
+
persistence.logger.debug("[API MACHINE] Received external metadata update");
|
|
14557
|
+
this.machine.metadata = persistence.decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, persistence.decodeBase64(update.metadata.value));
|
|
15513
14558
|
this.machine.metadataVersion = update.metadata.version;
|
|
15514
14559
|
}
|
|
15515
14560
|
if (update.daemonState) {
|
|
15516
|
-
logger.debug("[API MACHINE] Received external daemon state update");
|
|
15517
|
-
this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.daemonState.value));
|
|
14561
|
+
persistence.logger.debug("[API MACHINE] Received external daemon state update");
|
|
14562
|
+
this.machine.daemonState = persistence.decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, persistence.decodeBase64(update.daemonState.value));
|
|
15518
14563
|
this.machine.daemonStateVersion = update.daemonState.version;
|
|
15519
14564
|
}
|
|
15520
14565
|
} else {
|
|
15521
|
-
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
14566
|
+
persistence.logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
15522
14567
|
}
|
|
15523
14568
|
});
|
|
15524
14569
|
socket.on("connect_error", (error) => {
|
|
15525
|
-
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
14570
|
+
persistence.logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
15526
14571
|
if (this.socket === socket) this.startSmartReconnect();
|
|
15527
14572
|
});
|
|
15528
14573
|
socket.io.on("error", (error) => {
|
|
15529
|
-
logger.debug("[API MACHINE] Socket error:", error);
|
|
14574
|
+
persistence.logger.debug("[API MACHINE] Socket error:", error);
|
|
15530
14575
|
});
|
|
15531
14576
|
socket.on("server-draining", (data) => {
|
|
15532
14577
|
if (this.socket !== socket) return;
|
|
@@ -15596,7 +14641,7 @@ class ApiMachineClient {
|
|
|
15596
14641
|
this.rpcHandlerManager.onSocketDisconnect(candidate);
|
|
15597
14642
|
candidate.close();
|
|
15598
14643
|
if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
|
|
15599
|
-
logger.debug(`[API MACHINE] Release handover failed: ${error instanceof Error ? error.message : error}`);
|
|
14644
|
+
persistence.logger.debug(`[API MACHINE] Release handover failed: ${error instanceof Error ? error.message : error}`);
|
|
15600
14645
|
}
|
|
15601
14646
|
}
|
|
15602
14647
|
startKeepAlive() {
|
|
@@ -15607,7 +14652,7 @@ class ApiMachineClient {
|
|
|
15607
14652
|
time: Date.now()
|
|
15608
14653
|
};
|
|
15609
14654
|
if (process.env.DEBUG) {
|
|
15610
|
-
logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
|
|
14655
|
+
persistence.logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
|
|
15611
14656
|
}
|
|
15612
14657
|
this.socket.emit("machine-alive", payload);
|
|
15613
14658
|
void this.refreshRelayConnection();
|
|
@@ -15629,15 +14674,15 @@ class ApiMachineClient {
|
|
|
15629
14674
|
cliAvailability: newAvailability,
|
|
15630
14675
|
resumeSupport: { ...newResumeSupport, rpcAvailable: !!this.resumeSessionHandler }
|
|
15631
14676
|
})).catch((err) => {
|
|
15632
|
-
logger.debug("[API MACHINE] Failed to update machine capabilities:", err);
|
|
14677
|
+
persistence.logger.debug("[API MACHINE] Failed to update machine capabilities:", err);
|
|
15633
14678
|
});
|
|
15634
14679
|
}
|
|
15635
14680
|
}, 2e4);
|
|
15636
|
-
logger.debug("[API MACHINE] Keep-alive started (20s interval)");
|
|
14681
|
+
persistence.logger.debug("[API MACHINE] Keep-alive started (20s interval)");
|
|
15637
14682
|
}
|
|
15638
14683
|
bindRpcRequestHandler(socket, transport) {
|
|
15639
14684
|
socket.on("rpc-request", async (data, callback) => {
|
|
15640
|
-
logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
|
|
14685
|
+
persistence.logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
|
|
15641
14686
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
15642
14687
|
});
|
|
15643
14688
|
}
|
|
@@ -15645,7 +14690,7 @@ class ApiMachineClient {
|
|
|
15645
14690
|
socket.on("terminal-input", (data) => {
|
|
15646
14691
|
let payload = data.data;
|
|
15647
14692
|
if (data.enc) {
|
|
15648
|
-
const dec = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(data.data));
|
|
14693
|
+
const dec = persistence.decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, persistence.decodeBase64(data.data));
|
|
15649
14694
|
if (typeof dec !== "string") return;
|
|
15650
14695
|
payload = dec;
|
|
15651
14696
|
}
|
|
@@ -15656,7 +14701,7 @@ class ApiMachineClient {
|
|
|
15656
14701
|
socket.on("session-archive", (data) => {
|
|
15657
14702
|
if (typeof data?.sessionId !== "string") return;
|
|
15658
14703
|
const stopped = this.stopSessionHandler?.(data.sessionId) ?? false;
|
|
15659
|
-
logger.debug(`[API MACHINE] Server archived session ${data.sessionId}; local process stopped=${stopped}`);
|
|
14704
|
+
persistence.logger.debug(`[API MACHINE] Server archived session ${data.sessionId}; local process stopped=${stopped}`);
|
|
15660
14705
|
});
|
|
15661
14706
|
}
|
|
15662
14707
|
unbindMachineCommandHandlers(socket) {
|
|
@@ -15676,12 +14721,12 @@ class ApiMachineClient {
|
|
|
15676
14721
|
if (tracked.length === 0) return;
|
|
15677
14722
|
try {
|
|
15678
14723
|
for (let offset = 0; offset < tracked.length; offset += 500) {
|
|
15679
|
-
const response = await fetch(`${configuration.serverUrl}/v1/sessions/archive-status`, {
|
|
14724
|
+
const response = await fetch(`${persistence.configuration.serverUrl}/v1/sessions/archive-status`, {
|
|
15680
14725
|
method: "POST",
|
|
15681
14726
|
headers: {
|
|
15682
14727
|
"Authorization": `Bearer ${this.token}`,
|
|
15683
14728
|
"Content-Type": "application/json",
|
|
15684
|
-
"X-Happy-Client": `cli-daemon/${configuration.currentCliVersion}`
|
|
14729
|
+
"X-Happy-Client": `cli-daemon/${persistence.configuration.currentCliVersion}`
|
|
15685
14730
|
},
|
|
15686
14731
|
body: JSON.stringify({ sessionIds: tracked.slice(offset, offset + 500) })
|
|
15687
14732
|
});
|
|
@@ -15694,14 +14739,14 @@ class ApiMachineClient {
|
|
|
15694
14739
|
}
|
|
15695
14740
|
}
|
|
15696
14741
|
} catch (error) {
|
|
15697
|
-
logger.debug(`[API MACHINE] Archived-session reconciliation failed: ${error instanceof Error ? error.message : error}`);
|
|
14742
|
+
persistence.logger.debug(`[API MACHINE] Archived-session reconciliation failed: ${error instanceof Error ? error.message : error}`);
|
|
15698
14743
|
}
|
|
15699
14744
|
}
|
|
15700
14745
|
async refreshRelayConnection() {
|
|
15701
14746
|
if (this.relayRefreshInFlight) return this.relayRefreshInFlight;
|
|
15702
14747
|
this.relayRefreshInFlight = (async () => {
|
|
15703
14748
|
const selected = await discoverAndClaimRelay({
|
|
15704
|
-
controlUrl: configuration.serverUrl,
|
|
14749
|
+
controlUrl: persistence.configuration.serverUrl,
|
|
15705
14750
|
token: this.token,
|
|
15706
14751
|
machineId: this.machine.id,
|
|
15707
14752
|
connectedRelayId: this.relaySocket?.connected ? this.relayAssignment?.relayId : void 0,
|
|
@@ -15715,7 +14760,7 @@ class ApiMachineClient {
|
|
|
15715
14760
|
const previousUrl = this.relayAssignment?.url;
|
|
15716
14761
|
this.relayAssignment = selected.assignment;
|
|
15717
14762
|
const probe = selected.probes.find((item) => item.relayId === selected.assignment.relayId);
|
|
15718
|
-
logger.debug(`[API MACHINE] Regional relay selected: ${selected.assignment.relayId} (${selected.assignment.region}), RTT ${probe ? Math.round(probe.rttMs) : "unknown"}ms`);
|
|
14763
|
+
persistence.logger.debug(`[API MACHINE] Regional relay selected: ${selected.assignment.relayId} (${selected.assignment.region}), RTT ${probe ? Math.round(probe.rttMs) : "unknown"}ms`);
|
|
15719
14764
|
if (this.relaySocket && previousUrl === selected.assignment.url) {
|
|
15720
14765
|
this.relaySocket.auth = { token: selected.assignment.token };
|
|
15721
14766
|
if (this.relaySocket.connected) return;
|
|
@@ -15739,7 +14784,7 @@ class ApiMachineClient {
|
|
|
15739
14784
|
this.rpcHandlerManager.onSocketConnect(relaySocket);
|
|
15740
14785
|
this.syncResumeSessionRpcRegistration();
|
|
15741
14786
|
this.syncRestartSessionRpcRegistration();
|
|
15742
|
-
logger.debug(`[API MACHINE] Connected to regional relay ${selected.assignment.relayId}`);
|
|
14787
|
+
persistence.logger.debug(`[API MACHINE] Connected to regional relay ${selected.assignment.relayId}`);
|
|
15743
14788
|
});
|
|
15744
14789
|
relaySocket.on("disconnect", () => {
|
|
15745
14790
|
this.relaySwitchTracker = null;
|
|
@@ -15760,14 +14805,14 @@ class ApiMachineClient {
|
|
|
15760
14805
|
return;
|
|
15761
14806
|
}
|
|
15762
14807
|
if (!shouldReconnect()) {
|
|
15763
|
-
logger.debug("[API MACHINE] Still not ready to reconnect");
|
|
14808
|
+
persistence.logger.debug("[API MACHINE] Still not ready to reconnect");
|
|
15764
14809
|
return;
|
|
15765
14810
|
}
|
|
15766
|
-
logger.debug("[API MACHINE] Attempting reconnect");
|
|
14811
|
+
persistence.logger.debug("[API MACHINE] Attempting reconnect");
|
|
15767
14812
|
this.socket.connect();
|
|
15768
14813
|
}, 3e3);
|
|
15769
14814
|
if (shouldReconnect()) {
|
|
15770
|
-
logger.debug("[API MACHINE] Network up + lid open \u2014 reconnecting in 1s");
|
|
14815
|
+
persistence.logger.debug("[API MACHINE] Network up + lid open \u2014 reconnecting in 1s");
|
|
15771
14816
|
setTimeout(() => {
|
|
15772
14817
|
if (!this.socket.connected) this.socket.connect();
|
|
15773
14818
|
}, 1e3);
|
|
@@ -15777,11 +14822,11 @@ class ApiMachineClient {
|
|
|
15777
14822
|
if (this.keepAliveInterval) {
|
|
15778
14823
|
clearInterval(this.keepAliveInterval);
|
|
15779
14824
|
this.keepAliveInterval = null;
|
|
15780
|
-
logger.debug("[API MACHINE] Keep-alive stopped");
|
|
14825
|
+
persistence.logger.debug("[API MACHINE] Keep-alive stopped");
|
|
15781
14826
|
}
|
|
15782
14827
|
}
|
|
15783
14828
|
shutdown() {
|
|
15784
|
-
logger.debug("[API MACHINE] Shutting down");
|
|
14829
|
+
persistence.logger.debug("[API MACHINE] Shutting down");
|
|
15785
14830
|
this.webTerminal.stopListTracking();
|
|
15786
14831
|
this.stopKeepAlive();
|
|
15787
14832
|
if (this.reconnectInterval) {
|
|
@@ -15790,13 +14835,13 @@ class ApiMachineClient {
|
|
|
15790
14835
|
}
|
|
15791
14836
|
if (this.socket) {
|
|
15792
14837
|
this.socket.close();
|
|
15793
|
-
logger.debug("[API MACHINE] Socket closed");
|
|
14838
|
+
persistence.logger.debug("[API MACHINE] Socket closed");
|
|
15794
14839
|
}
|
|
15795
14840
|
if (this.relaySocket) {
|
|
15796
14841
|
this.rpcHandlerManager.onSocketDisconnect(this.relaySocket);
|
|
15797
14842
|
this.relaySocket.close();
|
|
15798
14843
|
this.relaySocket = null;
|
|
15799
|
-
logger.debug("[API MACHINE] Regional relay socket closed");
|
|
14844
|
+
persistence.logger.debug("[API MACHINE] Regional relay socket closed");
|
|
15800
14845
|
}
|
|
15801
14846
|
}
|
|
15802
14847
|
}
|
|
@@ -15866,24 +14911,24 @@ class PushNotificationClient {
|
|
|
15866
14911
|
headers: {
|
|
15867
14912
|
"Authorization": `Bearer ${this.token}`,
|
|
15868
14913
|
"Content-Type": "application/json",
|
|
15869
|
-
"X-Happy-Client": `cli-daemon/${configuration.currentCliVersion}`
|
|
14914
|
+
"X-Happy-Client": `cli-daemon/${persistence.configuration.currentCliVersion}`
|
|
15870
14915
|
}
|
|
15871
14916
|
}
|
|
15872
14917
|
);
|
|
15873
|
-
logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
|
|
14918
|
+
persistence.logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
|
|
15874
14919
|
response.data.tokens.forEach((token, index) => {
|
|
15875
|
-
logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
|
|
14920
|
+
persistence.logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
|
|
15876
14921
|
});
|
|
15877
14922
|
return response.data.tokens;
|
|
15878
14923
|
} catch (error) {
|
|
15879
|
-
logger.debug(`[PUSH] [ERROR] Failed to fetch push tokens (attempt ${attempt}/${maxAttempts}):`, error);
|
|
14924
|
+
persistence.logger.debug(`[PUSH] [ERROR] Failed to fetch push tokens (attempt ${attempt}/${maxAttempts}):`, error);
|
|
15880
14925
|
if (attempt < maxAttempts) {
|
|
15881
14926
|
const delay = 1e3 * Math.pow(2, attempt - 1);
|
|
15882
14927
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
15883
14928
|
}
|
|
15884
14929
|
}
|
|
15885
14930
|
}
|
|
15886
|
-
logger.debug("[PUSH] [ERROR] All push token fetch attempts failed");
|
|
14931
|
+
persistence.logger.debug("[PUSH] [ERROR] All push token fetch attempts failed");
|
|
15887
14932
|
return [];
|
|
15888
14933
|
}
|
|
15889
14934
|
/**
|
|
@@ -15891,7 +14936,7 @@ class PushNotificationClient {
|
|
|
15891
14936
|
* @param messages - Array of push messages to send
|
|
15892
14937
|
*/
|
|
15893
14938
|
async sendPushNotifications(messages) {
|
|
15894
|
-
logger.debug(`Sending ${messages.length} push notifications`);
|
|
14939
|
+
persistence.logger.debug(`Sending ${messages.length} push notifications`);
|
|
15895
14940
|
const validMessages = messages.filter((message) => {
|
|
15896
14941
|
if (Array.isArray(message.to)) {
|
|
15897
14942
|
return message.to.every((token) => expoServerSdk.Expo.isExpoPushToken(token));
|
|
@@ -15899,7 +14944,7 @@ class PushNotificationClient {
|
|
|
15899
14944
|
return expoServerSdk.Expo.isExpoPushToken(message.to);
|
|
15900
14945
|
});
|
|
15901
14946
|
if (validMessages.length === 0) {
|
|
15902
|
-
logger.debug("No valid Expo push tokens found");
|
|
14947
|
+
persistence.logger.debug("No valid Expo push tokens found");
|
|
15903
14948
|
return;
|
|
15904
14949
|
}
|
|
15905
14950
|
const chunks = this.expo.chunkPushNotifications(validMessages);
|
|
@@ -15913,7 +14958,7 @@ class PushNotificationClient {
|
|
|
15913
14958
|
const errors = ticketChunk.filter((ticket) => ticket.status === "error");
|
|
15914
14959
|
if (errors.length > 0) {
|
|
15915
14960
|
const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
|
|
15916
|
-
logger.debug("[PUSH] Some notifications failed:", errorDetails);
|
|
14961
|
+
persistence.logger.debug("[PUSH] Some notifications failed:", errorDetails);
|
|
15917
14962
|
}
|
|
15918
14963
|
if (errors.length === ticketChunk.length) {
|
|
15919
14964
|
throw new Error("All push notifications in chunk failed");
|
|
@@ -15922,7 +14967,7 @@ class PushNotificationClient {
|
|
|
15922
14967
|
} catch (error) {
|
|
15923
14968
|
const elapsed = Date.now() - startTime;
|
|
15924
14969
|
if (elapsed >= timeout) {
|
|
15925
|
-
logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
|
|
14970
|
+
persistence.logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
|
|
15926
14971
|
break;
|
|
15927
14972
|
}
|
|
15928
14973
|
attempt++;
|
|
@@ -15930,13 +14975,13 @@ class PushNotificationClient {
|
|
|
15930
14975
|
const remainingTime = timeout - elapsed;
|
|
15931
14976
|
const waitTime = Math.min(delay, remainingTime);
|
|
15932
14977
|
if (waitTime > 0) {
|
|
15933
|
-
logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
|
|
14978
|
+
persistence.logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
|
|
15934
14979
|
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
15935
14980
|
}
|
|
15936
14981
|
}
|
|
15937
14982
|
}
|
|
15938
14983
|
}
|
|
15939
|
-
logger.debug(`Push notifications sent successfully`);
|
|
14984
|
+
persistence.logger.debug(`Push notifications sent successfully`);
|
|
15940
14985
|
}
|
|
15941
14986
|
/**
|
|
15942
14987
|
* Send a push notification to all registered devices for the user
|
|
@@ -15945,25 +14990,25 @@ class PushNotificationClient {
|
|
|
15945
14990
|
* @param data - Additional data to send with the notification
|
|
15946
14991
|
*/
|
|
15947
14992
|
sendToAllDevices(title, body, data) {
|
|
15948
|
-
logger.debug("[PUSH] sendToAllDevices called:", {
|
|
14993
|
+
persistence.logger.debug("[PUSH] sendToAllDevices called:", {
|
|
15949
14994
|
title: contentLogMetadata(title),
|
|
15950
14995
|
body: contentLogMetadata(body),
|
|
15951
14996
|
dataKeyCount: data ? Object.keys(data).length : 0
|
|
15952
14997
|
});
|
|
15953
14998
|
(async () => {
|
|
15954
14999
|
try {
|
|
15955
|
-
logger.debug("[PUSH] Fetching push tokens...");
|
|
15000
|
+
persistence.logger.debug("[PUSH] Fetching push tokens...");
|
|
15956
15001
|
const tokens = await this.fetchPushTokens();
|
|
15957
|
-
logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
|
|
15002
|
+
persistence.logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
|
|
15958
15003
|
tokens.forEach((token, index) => {
|
|
15959
|
-
logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
|
|
15004
|
+
persistence.logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
|
|
15960
15005
|
});
|
|
15961
15006
|
if (tokens.length === 0) {
|
|
15962
|
-
logger.debug("No push tokens found for user");
|
|
15007
|
+
persistence.logger.debug("No push tokens found for user");
|
|
15963
15008
|
return;
|
|
15964
15009
|
}
|
|
15965
15010
|
const messages = tokens.map((token, index) => {
|
|
15966
|
-
logger.debug(`[PUSH] Creating message ${index + 1} for token`);
|
|
15011
|
+
persistence.logger.debug(`[PUSH] Creating message ${index + 1} for token`);
|
|
15967
15012
|
return {
|
|
15968
15013
|
to: token.token,
|
|
15969
15014
|
title,
|
|
@@ -15976,11 +15021,11 @@ class PushNotificationClient {
|
|
|
15976
15021
|
priority: "high"
|
|
15977
15022
|
};
|
|
15978
15023
|
});
|
|
15979
|
-
logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
|
|
15024
|
+
persistence.logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
|
|
15980
15025
|
await this.sendPushNotifications(messages);
|
|
15981
|
-
logger.debug("[PUSH] Push notifications sent successfully");
|
|
15026
|
+
persistence.logger.debug("[PUSH] Push notifications sent successfully");
|
|
15982
15027
|
} catch (error) {
|
|
15983
|
-
logger.debug("[PUSH] Error sending to all devices:", error);
|
|
15028
|
+
persistence.logger.debug("[PUSH] Error sending to all devices:", error);
|
|
15984
15029
|
}
|
|
15985
15030
|
})();
|
|
15986
15031
|
}
|
|
@@ -16002,7 +15047,7 @@ class PushNotificationClient {
|
|
|
16002
15047
|
};
|
|
16003
15048
|
const sessionId = typeof params.data?.sessionId === "string" ? params.data.sessionId : null;
|
|
16004
15049
|
if (!sessionId) {
|
|
16005
|
-
logger.debug("[PUSH] sendSessionNotification: missing sessionId, falling back to direct send");
|
|
15050
|
+
persistence.logger.debug("[PUSH] sendSessionNotification: missing sessionId, falling back to direct send");
|
|
16006
15051
|
this.sendToAllDevices(title, body, payloadData);
|
|
16007
15052
|
return;
|
|
16008
15053
|
}
|
|
@@ -16020,14 +15065,14 @@ class PushNotificationClient {
|
|
|
16020
15065
|
headers: {
|
|
16021
15066
|
"Authorization": `Bearer ${this.token}`,
|
|
16022
15067
|
"Content-Type": "application/json",
|
|
16023
|
-
"X-Happy-Client": `cli-daemon/${configuration.currentCliVersion}`
|
|
15068
|
+
"X-Happy-Client": `cli-daemon/${persistence.configuration.currentCliVersion}`
|
|
16024
15069
|
},
|
|
16025
15070
|
timeout: 15e3
|
|
16026
15071
|
}
|
|
16027
15072
|
);
|
|
16028
|
-
logger.debug(`[PUSH] sendSessionNotification dispatched via server (kind=${params.kind})`);
|
|
15073
|
+
persistence.logger.debug(`[PUSH] sendSessionNotification dispatched via server (kind=${params.kind})`);
|
|
16029
15074
|
} catch (error) {
|
|
16030
|
-
logger.debug("[PUSH] sendSessionNotification failed:", error);
|
|
15075
|
+
persistence.logger.debug("[PUSH] sendSessionNotification failed:", error);
|
|
16031
15076
|
}
|
|
16032
15077
|
})();
|
|
16033
15078
|
}
|
|
@@ -16045,7 +15090,7 @@ function startOfflineReconnection(config) {
|
|
|
16045
15090
|
validateStatus: (status) => status < 500,
|
|
16046
15091
|
// 4xx = server is up, 5xx = server error
|
|
16047
15092
|
headers: {
|
|
16048
|
-
"X-Happy-Client": `cli-daemon/${configuration.currentCliVersion}`
|
|
15093
|
+
"X-Happy-Client": `cli-daemon/${persistence.configuration.currentCliVersion}`
|
|
16049
15094
|
}
|
|
16050
15095
|
});
|
|
16051
15096
|
};
|
|
@@ -16061,16 +15106,16 @@ function startOfflineReconnection(config) {
|
|
|
16061
15106
|
if (cancelled) return;
|
|
16062
15107
|
reconnected = true;
|
|
16063
15108
|
config.onNotify("\u2705 Reconnected! Session syncing in background.");
|
|
16064
|
-
logger.debug("[OfflineReconnection] Successfully reconnected");
|
|
15109
|
+
persistence.logger.debug("[OfflineReconnection] Successfully reconnected");
|
|
16065
15110
|
} catch (e) {
|
|
16066
15111
|
if (axios.isAxiosError(e) && e.response?.status === 401) {
|
|
16067
|
-
logger.debug("[OfflineReconnection] Authentication error, stopping retries");
|
|
15112
|
+
persistence.logger.debug("[OfflineReconnection] Authentication error, stopping retries");
|
|
16068
15113
|
config.onNotify("\u274C Authentication failed. Please re-authenticate with `very-happy auth`.");
|
|
16069
15114
|
return;
|
|
16070
15115
|
}
|
|
16071
15116
|
failureCount++;
|
|
16072
15117
|
const delay = retryDelayMs(failureCount);
|
|
16073
|
-
logger.debug(`[OfflineReconnection] Attempt ${failureCount} failed, retrying in ${delay}ms`);
|
|
15118
|
+
persistence.logger.debug(`[OfflineReconnection] Attempt ${failureCount} failed, retrying in ${delay}ms`);
|
|
16074
15119
|
if (!cancelled) {
|
|
16075
15120
|
timeoutId = setTimeout(attemptReconnect, delay);
|
|
16076
15121
|
}
|
|
@@ -16164,7 +15209,7 @@ const connectionState = new OfflineState();
|
|
|
16164
15209
|
|
|
16165
15210
|
function accountBoxPublicKeyFromCredentials(credential) {
|
|
16166
15211
|
if (credential.encryption.type === "legacy") {
|
|
16167
|
-
return libsodiumPublicKeyFromSecretKey(credential.encryption.secret);
|
|
15212
|
+
return persistence.libsodiumPublicKeyFromSecretKey(credential.encryption.secret);
|
|
16168
15213
|
}
|
|
16169
15214
|
return credential.encryption.publicKey;
|
|
16170
15215
|
}
|
|
@@ -16231,7 +15276,7 @@ class NotificationProducer {
|
|
|
16231
15276
|
const now = Date.now();
|
|
16232
15277
|
const last = this.lastEmittedAt.get(repeatKey);
|
|
16233
15278
|
if (last !== void 0 && now - last < DEDUP_WINDOW_MS) {
|
|
16234
|
-
logger.debug(`[notif] Suppressing duplicate ${notifType} for ${this.sessionId}`);
|
|
15279
|
+
persistence.logger.debug(`[notif] Suppressing duplicate ${notifType} for ${this.sessionId}`);
|
|
16235
15280
|
return;
|
|
16236
15281
|
}
|
|
16237
15282
|
this.lastEmittedAt.set(repeatKey, now);
|
|
@@ -16244,10 +15289,10 @@ class NotificationProducer {
|
|
|
16244
15289
|
let enc;
|
|
16245
15290
|
try {
|
|
16246
15291
|
const plaintext = new TextEncoder().encode(JSON.stringify(body));
|
|
16247
|
-
const bundle = libsodiumEncryptForPublicKey(plaintext, this.accountBoxPubKey);
|
|
16248
|
-
enc = encodeBase64(bundle);
|
|
15292
|
+
const bundle = persistence.libsodiumEncryptForPublicKey(plaintext, this.accountBoxPubKey);
|
|
15293
|
+
enc = persistence.encodeBase64(bundle);
|
|
16249
15294
|
} catch (err) {
|
|
16250
|
-
logger.debug("[notif] Failed to encrypt notification body:", err);
|
|
15295
|
+
persistence.logger.debug("[notif] Failed to encrypt notification body:", err);
|
|
16251
15296
|
return;
|
|
16252
15297
|
}
|
|
16253
15298
|
void this.post(notifType, enc, repeatKey);
|
|
@@ -16255,7 +15300,7 @@ class NotificationProducer {
|
|
|
16255
15300
|
async post(notifType, enc, repeatKey) {
|
|
16256
15301
|
try {
|
|
16257
15302
|
await axios.post(
|
|
16258
|
-
`${configuration.serverUrl}/v1/feed`,
|
|
15303
|
+
`${persistence.configuration.serverUrl}/v1/feed`,
|
|
16259
15304
|
{
|
|
16260
15305
|
notifType,
|
|
16261
15306
|
sessionId: this.sessionId,
|
|
@@ -16266,14 +15311,14 @@ class NotificationProducer {
|
|
|
16266
15311
|
headers: {
|
|
16267
15312
|
"Authorization": `Bearer ${this.token}`,
|
|
16268
15313
|
"Content-Type": "application/json",
|
|
16269
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15314
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16270
15315
|
},
|
|
16271
15316
|
timeout: 5e3
|
|
16272
15317
|
}
|
|
16273
15318
|
);
|
|
16274
|
-
logger.debug(`[notif] Posted ${notifType} for session ${this.sessionId}`);
|
|
15319
|
+
persistence.logger.debug(`[notif] Posted ${notifType} for session ${this.sessionId}`);
|
|
16275
15320
|
} catch (err) {
|
|
16276
|
-
logger.debug(`[notif] Failed to post ${notifType}:`, err);
|
|
15321
|
+
persistence.logger.debug(`[notif] Failed to post ${notifType}:`, err);
|
|
16277
15322
|
}
|
|
16278
15323
|
}
|
|
16279
15324
|
/** Short, human-friendly label for the project/session for use in titles. */
|
|
@@ -16311,7 +15356,7 @@ class ApiClient {
|
|
|
16311
15356
|
pushClient;
|
|
16312
15357
|
constructor(credential) {
|
|
16313
15358
|
this.credential = credential;
|
|
16314
|
-
this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
|
|
15359
|
+
this.pushClient = new PushNotificationClient(credential.token, persistence.configuration.serverUrl);
|
|
16315
15360
|
}
|
|
16316
15361
|
/**
|
|
16317
15362
|
* Create a new session or load existing one with the given tag
|
|
@@ -16321,9 +15366,9 @@ class ApiClient {
|
|
|
16321
15366
|
let encryptionKey;
|
|
16322
15367
|
let encryptionVariant;
|
|
16323
15368
|
if (this.credential.encryption.type === "dataKey") {
|
|
16324
|
-
encryptionKey = getRandomBytes(32);
|
|
15369
|
+
encryptionKey = persistence.getRandomBytes(32);
|
|
16325
15370
|
encryptionVariant = "dataKey";
|
|
16326
|
-
let encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
|
|
15371
|
+
let encryptedDataKey = persistence.libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
|
|
16327
15372
|
dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
|
|
16328
15373
|
dataEncryptionKey.set([0], 0);
|
|
16329
15374
|
dataEncryptionKey.set(encryptedDataKey, 1);
|
|
@@ -16333,38 +15378,38 @@ class ApiClient {
|
|
|
16333
15378
|
}
|
|
16334
15379
|
try {
|
|
16335
15380
|
const response = await axios.post(
|
|
16336
|
-
`${configuration.serverUrl}/v1/sessions`,
|
|
15381
|
+
`${persistence.configuration.serverUrl}/v1/sessions`,
|
|
16337
15382
|
{
|
|
16338
15383
|
tag: opts.tag,
|
|
16339
|
-
metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
|
|
16340
|
-
agentState: opts.state ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.state)) : null,
|
|
16341
|
-
dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : null
|
|
15384
|
+
metadata: persistence.encodeBase64(persistence.encrypt(encryptionKey, encryptionVariant, opts.metadata)),
|
|
15385
|
+
agentState: opts.state ? persistence.encodeBase64(persistence.encrypt(encryptionKey, encryptionVariant, opts.state)) : null,
|
|
15386
|
+
dataEncryptionKey: dataEncryptionKey ? persistence.encodeBase64(dataEncryptionKey) : null
|
|
16342
15387
|
},
|
|
16343
15388
|
{
|
|
16344
15389
|
headers: {
|
|
16345
15390
|
"Authorization": `Bearer ${this.credential.token}`,
|
|
16346
15391
|
"Content-Type": "application/json",
|
|
16347
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15392
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16348
15393
|
},
|
|
16349
15394
|
timeout: 6e4
|
|
16350
15395
|
// 1 minute timeout for very bad network connections
|
|
16351
15396
|
}
|
|
16352
15397
|
);
|
|
16353
|
-
logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
|
|
15398
|
+
persistence.logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
|
|
16354
15399
|
let raw = response.data.session;
|
|
16355
15400
|
let session = {
|
|
16356
15401
|
id: raw.id,
|
|
16357
15402
|
seq: raw.seq,
|
|
16358
|
-
metadata: decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)),
|
|
15403
|
+
metadata: persistence.decrypt(encryptionKey, encryptionVariant, persistence.decodeBase64(raw.metadata)),
|
|
16359
15404
|
metadataVersion: raw.metadataVersion,
|
|
16360
|
-
agentState: raw.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.agentState)) : null,
|
|
15405
|
+
agentState: raw.agentState ? persistence.decrypt(encryptionKey, encryptionVariant, persistence.decodeBase64(raw.agentState)) : null,
|
|
16361
15406
|
agentStateVersion: raw.agentStateVersion,
|
|
16362
15407
|
encryptionKey,
|
|
16363
15408
|
encryptionVariant
|
|
16364
15409
|
};
|
|
16365
15410
|
return session;
|
|
16366
15411
|
} catch (error) {
|
|
16367
|
-
logger.debug("[API] [ERROR] Failed to get or create session:", error);
|
|
15412
|
+
persistence.logger.debug("[API] [ERROR] Failed to get or create session:", error);
|
|
16368
15413
|
if (error && typeof error === "object" && "code" in error) {
|
|
16369
15414
|
const errorCode = error.code;
|
|
16370
15415
|
if (isNetworkError(errorCode)) {
|
|
@@ -16372,7 +15417,7 @@ class ApiClient {
|
|
|
16372
15417
|
operation: "Session creation",
|
|
16373
15418
|
caller: "api.getOrCreateSession",
|
|
16374
15419
|
errorCode,
|
|
16375
|
-
url: `${configuration.serverUrl}/v1/sessions`
|
|
15420
|
+
url: `${persistence.configuration.serverUrl}/v1/sessions`
|
|
16376
15421
|
});
|
|
16377
15422
|
return null;
|
|
16378
15423
|
}
|
|
@@ -16382,7 +15427,7 @@ class ApiClient {
|
|
|
16382
15427
|
connectionState.fail({
|
|
16383
15428
|
operation: "Session creation",
|
|
16384
15429
|
errorCode: "404",
|
|
16385
|
-
url: `${configuration.serverUrl}/v1/sessions`
|
|
15430
|
+
url: `${persistence.configuration.serverUrl}/v1/sessions`
|
|
16386
15431
|
});
|
|
16387
15432
|
return null;
|
|
16388
15433
|
}
|
|
@@ -16392,7 +15437,7 @@ class ApiClient {
|
|
|
16392
15437
|
connectionState.fail({
|
|
16393
15438
|
operation: "Session creation",
|
|
16394
15439
|
errorCode: String(status),
|
|
16395
|
-
url: `${configuration.serverUrl}/v1/sessions`,
|
|
15440
|
+
url: `${persistence.configuration.serverUrl}/v1/sessions`,
|
|
16396
15441
|
details: ["Server encountered an error, will retry automatically"]
|
|
16397
15442
|
});
|
|
16398
15443
|
return null;
|
|
@@ -16412,7 +15457,7 @@ class ApiClient {
|
|
|
16412
15457
|
if (this.credential.encryption.type === "dataKey") {
|
|
16413
15458
|
encryptionVariant = "dataKey";
|
|
16414
15459
|
encryptionKey = this.credential.encryption.machineKey;
|
|
16415
|
-
let encryptedDataKey = libsodiumEncryptForPublicKey(this.credential.encryption.machineKey, this.credential.encryption.publicKey);
|
|
15460
|
+
let encryptedDataKey = persistence.libsodiumEncryptForPublicKey(this.credential.encryption.machineKey, this.credential.encryption.publicKey);
|
|
16416
15461
|
dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
|
|
16417
15462
|
dataEncryptionKey.set([0], 0);
|
|
16418
15463
|
dataEncryptionKey.set(encryptedDataKey, 1);
|
|
@@ -16431,32 +15476,32 @@ class ApiClient {
|
|
|
16431
15476
|
});
|
|
16432
15477
|
try {
|
|
16433
15478
|
const response = await axios.post(
|
|
16434
|
-
`${configuration.serverUrl}/v1/machines`,
|
|
15479
|
+
`${persistence.configuration.serverUrl}/v1/machines`,
|
|
16435
15480
|
{
|
|
16436
15481
|
id: opts.machineId,
|
|
16437
|
-
metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
|
|
16438
|
-
daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.daemonState)) : void 0,
|
|
16439
|
-
dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : void 0
|
|
15482
|
+
metadata: persistence.encodeBase64(persistence.encrypt(encryptionKey, encryptionVariant, opts.metadata)),
|
|
15483
|
+
daemonState: opts.daemonState ? persistence.encodeBase64(persistence.encrypt(encryptionKey, encryptionVariant, opts.daemonState)) : void 0,
|
|
15484
|
+
dataEncryptionKey: dataEncryptionKey ? persistence.encodeBase64(dataEncryptionKey) : void 0
|
|
16440
15485
|
},
|
|
16441
15486
|
{
|
|
16442
15487
|
headers: {
|
|
16443
15488
|
"Authorization": `Bearer ${this.credential.token}`,
|
|
16444
15489
|
"Content-Type": "application/json",
|
|
16445
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15490
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16446
15491
|
},
|
|
16447
15492
|
timeout: 6e4
|
|
16448
15493
|
// 1 minute timeout for very bad network connections
|
|
16449
15494
|
}
|
|
16450
15495
|
);
|
|
16451
15496
|
const raw = response.data.machine;
|
|
16452
|
-
logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
|
|
15497
|
+
persistence.logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
|
|
16453
15498
|
const machine = {
|
|
16454
15499
|
id: raw.id,
|
|
16455
15500
|
encryptionKey,
|
|
16456
15501
|
encryptionVariant,
|
|
16457
|
-
metadata: raw.metadata ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)) : null,
|
|
15502
|
+
metadata: raw.metadata ? persistence.decrypt(encryptionKey, encryptionVariant, persistence.decodeBase64(raw.metadata)) : null,
|
|
16458
15503
|
metadataVersion: raw.metadataVersion || 0,
|
|
16459
|
-
daemonState: raw.daemonState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.daemonState)) : null,
|
|
15504
|
+
daemonState: raw.daemonState ? persistence.decrypt(encryptionKey, encryptionVariant, persistence.decodeBase64(raw.daemonState)) : null,
|
|
16460
15505
|
daemonStateVersion: raw.daemonStateVersion || 0
|
|
16461
15506
|
};
|
|
16462
15507
|
return machine;
|
|
@@ -16466,7 +15511,7 @@ class ApiClient {
|
|
|
16466
15511
|
operation: "Machine registration",
|
|
16467
15512
|
caller: "api.getOrCreateMachine",
|
|
16468
15513
|
errorCode: error.code,
|
|
16469
|
-
url: `${configuration.serverUrl}/v1/machines`
|
|
15514
|
+
url: `${persistence.configuration.serverUrl}/v1/machines`
|
|
16470
15515
|
});
|
|
16471
15516
|
return createMinimalMachine();
|
|
16472
15517
|
}
|
|
@@ -16497,7 +15542,7 @@ class ApiClient {
|
|
|
16497
15542
|
connectionState.fail({
|
|
16498
15543
|
operation: "Machine registration",
|
|
16499
15544
|
errorCode: "429",
|
|
16500
|
-
url: `${configuration.serverUrl}/v1/machines`,
|
|
15545
|
+
url: `${persistence.configuration.serverUrl}/v1/machines`,
|
|
16501
15546
|
details: ["Relay write quota is cooling down; realtime connection will retry automatically"]
|
|
16502
15547
|
});
|
|
16503
15548
|
return createMinimalMachine();
|
|
@@ -16506,7 +15551,7 @@ class ApiClient {
|
|
|
16506
15551
|
connectionState.fail({
|
|
16507
15552
|
operation: "Machine registration",
|
|
16508
15553
|
errorCode: String(status),
|
|
16509
|
-
url: `${configuration.serverUrl}/v1/machines`,
|
|
15554
|
+
url: `${persistence.configuration.serverUrl}/v1/machines`,
|
|
16510
15555
|
details: ["Server encountered an error, will retry automatically"]
|
|
16511
15556
|
});
|
|
16512
15557
|
return createMinimalMachine();
|
|
@@ -16515,7 +15560,7 @@ class ApiClient {
|
|
|
16515
15560
|
connectionState.fail({
|
|
16516
15561
|
operation: "Machine registration",
|
|
16517
15562
|
errorCode: "404",
|
|
16518
|
-
url: `${configuration.serverUrl}/v1/machines`
|
|
15563
|
+
url: `${persistence.configuration.serverUrl}/v1/machines`
|
|
16519
15564
|
});
|
|
16520
15565
|
return createMinimalMachine();
|
|
16521
15566
|
}
|
|
@@ -16554,11 +15599,11 @@ class ApiClient {
|
|
|
16554
15599
|
async kvGet(key) {
|
|
16555
15600
|
try {
|
|
16556
15601
|
const response = await axios.get(
|
|
16557
|
-
`${configuration.serverUrl}/v1/kv/${encodeURIComponent(key)}`,
|
|
15602
|
+
`${persistence.configuration.serverUrl}/v1/kv/${encodeURIComponent(key)}`,
|
|
16558
15603
|
{
|
|
16559
15604
|
headers: {
|
|
16560
15605
|
"Authorization": `Bearer ${this.credential.token}`,
|
|
16561
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15606
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16562
15607
|
},
|
|
16563
15608
|
timeout: 5e3,
|
|
16564
15609
|
validateStatus: (s) => s === 200 || s === 404
|
|
@@ -16567,7 +15612,7 @@ class ApiClient {
|
|
|
16567
15612
|
if (response.status === 404) return null;
|
|
16568
15613
|
return response.data;
|
|
16569
15614
|
} catch (error) {
|
|
16570
|
-
logger.debug(`[API] kvGet(${key}) failed:`, error);
|
|
15615
|
+
persistence.logger.debug(`[API] kvGet(${key}) failed:`, error);
|
|
16571
15616
|
return null;
|
|
16572
15617
|
}
|
|
16573
15618
|
}
|
|
@@ -16578,7 +15623,7 @@ class ApiClient {
|
|
|
16578
15623
|
async registerVendorToken(vendor, apiKey) {
|
|
16579
15624
|
try {
|
|
16580
15625
|
const response = await axios.post(
|
|
16581
|
-
`${configuration.serverUrl}/v1/connect/${vendor}/register`,
|
|
15626
|
+
`${persistence.configuration.serverUrl}/v1/connect/${vendor}/register`,
|
|
16582
15627
|
{
|
|
16583
15628
|
token: JSON.stringify(apiKey)
|
|
16584
15629
|
},
|
|
@@ -16586,7 +15631,7 @@ class ApiClient {
|
|
|
16586
15631
|
headers: {
|
|
16587
15632
|
"Authorization": `Bearer ${this.credential.token}`,
|
|
16588
15633
|
"Content-Type": "application/json",
|
|
16589
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15634
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16590
15635
|
},
|
|
16591
15636
|
timeout: 5e3
|
|
16592
15637
|
}
|
|
@@ -16594,9 +15639,9 @@ class ApiClient {
|
|
|
16594
15639
|
if (response.status !== 200 && response.status !== 201) {
|
|
16595
15640
|
throw new Error(`Server returned status ${response.status}`);
|
|
16596
15641
|
}
|
|
16597
|
-
logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
|
|
15642
|
+
persistence.logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
|
|
16598
15643
|
} catch (error) {
|
|
16599
|
-
logger.debug(`[API] [ERROR] Failed to register vendor token:`, redactLogValue(error));
|
|
15644
|
+
persistence.logger.debug(`[API] [ERROR] Failed to register vendor token:`, persistence.redactLogValue(error));
|
|
16600
15645
|
throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
16601
15646
|
}
|
|
16602
15647
|
}
|
|
@@ -16607,24 +15652,24 @@ class ApiClient {
|
|
|
16607
15652
|
async getVendorToken(vendor) {
|
|
16608
15653
|
try {
|
|
16609
15654
|
const response = await axios.get(
|
|
16610
|
-
`${configuration.serverUrl}/v1/connect/${vendor}/token`,
|
|
15655
|
+
`${persistence.configuration.serverUrl}/v1/connect/${vendor}/token`,
|
|
16611
15656
|
{
|
|
16612
15657
|
headers: {
|
|
16613
15658
|
"Authorization": `Bearer ${this.credential.token}`,
|
|
16614
15659
|
"Content-Type": "application/json",
|
|
16615
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15660
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16616
15661
|
},
|
|
16617
15662
|
timeout: 5e3
|
|
16618
15663
|
}
|
|
16619
15664
|
);
|
|
16620
15665
|
if (response.status === 404) {
|
|
16621
|
-
logger.debug(`[API] No vendor token found for ${vendor}`);
|
|
15666
|
+
persistence.logger.debug(`[API] No vendor token found for ${vendor}`);
|
|
16622
15667
|
return null;
|
|
16623
15668
|
}
|
|
16624
15669
|
if (response.status !== 200) {
|
|
16625
15670
|
throw new Error(`Server returned status ${response.status}`);
|
|
16626
15671
|
}
|
|
16627
|
-
logger.debug(`[API] Raw vendor token response:`, {
|
|
15672
|
+
persistence.logger.debug(`[API] Raw vendor token response:`, {
|
|
16628
15673
|
status: response.status,
|
|
16629
15674
|
dataKeys: Object.keys(response.data || {}),
|
|
16630
15675
|
hasToken: "token" in (response.data || {}),
|
|
@@ -16636,37 +15681,37 @@ class ApiClient {
|
|
|
16636
15681
|
try {
|
|
16637
15682
|
tokenData = JSON.parse(response.data.token);
|
|
16638
15683
|
} catch (parseError) {
|
|
16639
|
-
logger.debug(`[API] Failed to parse token as JSON, using as string:`, parseError);
|
|
15684
|
+
persistence.logger.debug(`[API] Failed to parse token as JSON, using as string:`, parseError);
|
|
16640
15685
|
tokenData = response.data.token;
|
|
16641
15686
|
}
|
|
16642
15687
|
} else if (response.data.token !== null) {
|
|
16643
15688
|
tokenData = response.data.token;
|
|
16644
15689
|
} else {
|
|
16645
|
-
logger.debug(`[API] Token is null for ${vendor}, treating as not found`);
|
|
15690
|
+
persistence.logger.debug(`[API] Token is null for ${vendor}, treating as not found`);
|
|
16646
15691
|
return null;
|
|
16647
15692
|
}
|
|
16648
15693
|
} else if (response.data && typeof response.data === "object") {
|
|
16649
15694
|
if (response.data.token === null && Object.keys(response.data).length === 1) {
|
|
16650
|
-
logger.debug(`[API] Response contains only null token for ${vendor}, treating as not found`);
|
|
15695
|
+
persistence.logger.debug(`[API] Response contains only null token for ${vendor}, treating as not found`);
|
|
16651
15696
|
return null;
|
|
16652
15697
|
}
|
|
16653
15698
|
tokenData = response.data;
|
|
16654
15699
|
}
|
|
16655
15700
|
if (tokenData === null || tokenData && typeof tokenData === "object" && tokenData.token === null && Object.keys(tokenData).length === 1) {
|
|
16656
|
-
logger.debug(`[API] Token data is null for ${vendor}`);
|
|
15701
|
+
persistence.logger.debug(`[API] Token data is null for ${vendor}`);
|
|
16657
15702
|
return null;
|
|
16658
15703
|
}
|
|
16659
|
-
logger.debug(`[API] Vendor token for ${vendor} retrieved successfully`, {
|
|
15704
|
+
persistence.logger.debug(`[API] Vendor token for ${vendor} retrieved successfully`, {
|
|
16660
15705
|
tokenDataType: typeof tokenData,
|
|
16661
15706
|
tokenDataKeys: tokenData && typeof tokenData === "object" ? Object.keys(tokenData) : "not an object"
|
|
16662
15707
|
});
|
|
16663
15708
|
return tokenData;
|
|
16664
15709
|
} catch (error) {
|
|
16665
15710
|
if (error.response?.status === 404) {
|
|
16666
|
-
logger.debug(`[API] No vendor token found for ${vendor}`);
|
|
15711
|
+
persistence.logger.debug(`[API] No vendor token found for ${vendor}`);
|
|
16667
15712
|
return null;
|
|
16668
15713
|
}
|
|
16669
|
-
logger.debug(`[API] [ERROR] Failed to get vendor token:`, redactLogValue(error));
|
|
15714
|
+
persistence.logger.debug(`[API] [ERROR] Failed to get vendor token:`, persistence.redactLogValue(error));
|
|
16670
15715
|
return null;
|
|
16671
15716
|
}
|
|
16672
15717
|
}
|
|
@@ -16684,19 +15729,19 @@ class ApiClient {
|
|
|
16684
15729
|
async deactivateSession(sessionId) {
|
|
16685
15730
|
try {
|
|
16686
15731
|
const response = await axios.post(
|
|
16687
|
-
`${configuration.serverUrl}/v1/sessions/${sessionId}/archive`,
|
|
15732
|
+
`${persistence.configuration.serverUrl}/v1/sessions/${sessionId}/archive`,
|
|
16688
15733
|
{},
|
|
16689
15734
|
{
|
|
16690
15735
|
headers: {
|
|
16691
15736
|
"Authorization": `Bearer ${this.credential.token}`,
|
|
16692
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15737
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16693
15738
|
},
|
|
16694
15739
|
timeout: 3e3
|
|
16695
15740
|
}
|
|
16696
15741
|
);
|
|
16697
15742
|
return response.status >= 200 && response.status < 300;
|
|
16698
15743
|
} catch (error) {
|
|
16699
|
-
logger.debug("[API] deactivateSession failed:", error);
|
|
15744
|
+
persistence.logger.debug("[API] deactivateSession failed:", error);
|
|
16700
15745
|
return false;
|
|
16701
15746
|
}
|
|
16702
15747
|
}
|
|
@@ -16715,11 +15760,11 @@ class ApiClient {
|
|
|
16715
15760
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
16716
15761
|
try {
|
|
16717
15762
|
const response = await axios.get(
|
|
16718
|
-
`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
|
|
15763
|
+
`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
|
|
16719
15764
|
{
|
|
16720
15765
|
headers: {
|
|
16721
15766
|
"Authorization": `Bearer ${this.credential.token}`,
|
|
16722
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15767
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16723
15768
|
},
|
|
16724
15769
|
timeout: opts?.timeoutMs ?? 4e3,
|
|
16725
15770
|
validateStatus: () => true
|
|
@@ -16733,17 +15778,17 @@ class ApiClient {
|
|
|
16733
15778
|
if (!row || typeof row.seq !== "number" || typeof row.metadata !== "string") {
|
|
16734
15779
|
return { ok: false, reason: "unsupported" };
|
|
16735
15780
|
}
|
|
16736
|
-
const metadata = decrypt(encryptionKey, encryptionVariant, decodeBase64(row.metadata));
|
|
15781
|
+
const metadata = persistence.decrypt(encryptionKey, encryptionVariant, persistence.decodeBase64(row.metadata));
|
|
16737
15782
|
if (!metadata) return { ok: false, reason: "unavailable" };
|
|
16738
|
-
const agentState = row.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(row.agentState)) : null;
|
|
15783
|
+
const agentState = row.agentState ? persistence.decrypt(encryptionKey, encryptionVariant, persistence.decodeBase64(row.agentState)) : null;
|
|
16739
15784
|
return {
|
|
16740
15785
|
ok: true,
|
|
16741
15786
|
session: { seq: row.seq, metadata, metadataVersion: row.metadataVersion, agentState, agentStateVersion: row.agentStateVersion }
|
|
16742
15787
|
};
|
|
16743
15788
|
}
|
|
16744
|
-
logger.debug(`[API] getSession ${sessionId}: status ${response.status} (attempt ${attempt + 1}/${attempts})`);
|
|
15789
|
+
persistence.logger.debug(`[API] getSession ${sessionId}: status ${response.status} (attempt ${attempt + 1}/${attempts})`);
|
|
16745
15790
|
} catch (error) {
|
|
16746
|
-
logger.debug(`[API] getSession ${sessionId} failed (attempt ${attempt + 1}/${attempts}):`, error);
|
|
15791
|
+
persistence.logger.debug(`[API] getSession ${sessionId} failed (attempt ${attempt + 1}/${attempts}):`, error);
|
|
16747
15792
|
}
|
|
16748
15793
|
const delayMs = delays[Math.min(attempt, delays.length - 1)];
|
|
16749
15794
|
if (attempt < attempts - 1 && delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
@@ -16756,12 +15801,12 @@ class ApiClient {
|
|
|
16756
15801
|
async reactivateSession(sessionId) {
|
|
16757
15802
|
try {
|
|
16758
15803
|
const response = await axios.post(
|
|
16759
|
-
`${configuration.serverUrl}/v1/sessions/${sessionId}/unarchive`,
|
|
15804
|
+
`${persistence.configuration.serverUrl}/v1/sessions/${sessionId}/unarchive`,
|
|
16760
15805
|
{},
|
|
16761
15806
|
{
|
|
16762
15807
|
headers: {
|
|
16763
15808
|
"Authorization": `Bearer ${this.credential.token}`,
|
|
16764
|
-
"X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
|
|
15809
|
+
"X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
|
|
16765
15810
|
},
|
|
16766
15811
|
timeout: 3e3,
|
|
16767
15812
|
validateStatus: (status) => status >= 200 && status < 300 || status === 404
|
|
@@ -16769,7 +15814,7 @@ class ApiClient {
|
|
|
16769
15814
|
);
|
|
16770
15815
|
return response.status >= 200 && response.status < 300 || response.status === 404;
|
|
16771
15816
|
} catch (error) {
|
|
16772
|
-
logger.debug("[API] reactivateSession failed:", error);
|
|
15817
|
+
persistence.logger.debug("[API] reactivateSession failed:", error);
|
|
16773
15818
|
return false;
|
|
16774
15819
|
}
|
|
16775
15820
|
}
|
|
@@ -16853,67 +15898,36 @@ exports.MAX_CHAT_ATTACHMENT_SOURCE_BYTES = MAX_CHAT_ATTACHMENT_SOURCE_BYTES;
|
|
|
16853
15898
|
exports.MIRROR_BACKFILL_LINES_DEFAULT = MIRROR_BACKFILL_LINES_DEFAULT;
|
|
16854
15899
|
exports.RawJSONLinesSchema = RawJSONLinesSchema;
|
|
16855
15900
|
exports.STREAM_DELTA_MAX_CHARS = STREAM_DELTA_MAX_CHARS;
|
|
16856
|
-
exports.SandboxConfigSchema = SandboxConfigSchema;
|
|
16857
15901
|
exports.TITLE_PROMPT_PREFIX = TITLE_PROMPT_PREFIX;
|
|
16858
|
-
exports.acquireDaemonLock = acquireDaemonLock;
|
|
16859
15902
|
exports.api = api;
|
|
16860
|
-
exports.clearCredentials = clearCredentials;
|
|
16861
|
-
exports.clearDaemonState = clearDaemonState;
|
|
16862
|
-
exports.clearMachineId = clearMachineId;
|
|
16863
15903
|
exports.commandOnPath = commandOnPath;
|
|
16864
15904
|
exports.compareExactVersions = compareExactVersions;
|
|
16865
|
-
exports.configuration = configuration;
|
|
16866
15905
|
exports.connectionState = connectionState;
|
|
16867
15906
|
exports.contentLogMetadata = contentLogMetadata;
|
|
16868
15907
|
exports.createEnvelope = createEnvelope;
|
|
16869
|
-
exports.credentialRelayProblem = credentialRelayProblem;
|
|
16870
15908
|
exports.decideBackfill = decideBackfill;
|
|
16871
15909
|
exports.decideMirrorBinding = decideMirrorBinding;
|
|
16872
|
-
exports.decodeBase64 = decodeBase64;
|
|
16873
|
-
exports.decrypt = decrypt;
|
|
16874
|
-
exports.decryptLegacy = decryptLegacy;
|
|
16875
|
-
exports.decryptWithDataKey = decryptWithDataKey;
|
|
16876
15910
|
exports.delay = delay;
|
|
16877
|
-
exports.deletePersistedSessions = deletePersistedSessions;
|
|
16878
15911
|
exports.deriveLocalCliUpdateSummary = deriveLocalCliUpdateSummary;
|
|
16879
15912
|
exports.detectCLIAvailability = detectCLIAvailability;
|
|
16880
15913
|
exports.detectResumeSupport = detectResumeSupport;
|
|
16881
|
-
exports.encodeBase64 = encodeBase64;
|
|
16882
|
-
exports.encodeBase64Url = encodeBase64Url;
|
|
16883
|
-
exports.encrypt = encrypt;
|
|
16884
|
-
exports.ensurePrivateDirectory = ensurePrivateDirectory;
|
|
16885
|
-
exports.ensurePrivateDirectorySync = ensurePrivateDirectorySync;
|
|
16886
15914
|
exports.errorLogMetadata = errorLogMetadata;
|
|
16887
15915
|
exports.extractCompleteLines = extractCompleteLines;
|
|
16888
15916
|
exports.fetchCliUpdateState = fetchCliUpdateState;
|
|
16889
|
-
exports.getLatestDaemonLog = getLatestDaemonLog;
|
|
16890
15917
|
exports.getLocalHappyAgentCredentialPath = getLocalHappyAgentCredentialPath;
|
|
16891
15918
|
exports.getProjectPath = getProjectPath;
|
|
16892
|
-
exports.hardenPrivateFile = hardenPrivateFile;
|
|
16893
|
-
exports.hardenPrivateFileSync = hardenPrivateFileSync;
|
|
16894
15919
|
exports.initializeSandbox = initializeSandbox;
|
|
16895
15920
|
exports.isClaudeSessionId = isClaudeSessionId;
|
|
16896
15921
|
exports.isValidSessionId = isValidSessionId;
|
|
16897
15922
|
exports.isValidTerminalId = isValidTerminalId;
|
|
16898
15923
|
exports.listVhTerminals = listVhTerminals;
|
|
16899
|
-
exports.logger = logger;
|
|
16900
15924
|
exports.mirrorLineKey = mirrorLineKey;
|
|
16901
15925
|
exports.mirrorLocalId = mirrorLocalId;
|
|
16902
|
-
exports.packageJson = packageJson;
|
|
16903
15926
|
exports.parseExactVersion = parseExactVersion;
|
|
16904
15927
|
exports.parseTerminalHookPayload = parseTerminalHookPayload;
|
|
16905
|
-
exports.persistSession = persistSession;
|
|
16906
|
-
exports.persistence = persistence;
|
|
16907
15928
|
exports.projectPath = projectPath;
|
|
16908
|
-
exports.readCredentials = readCredentials;
|
|
16909
|
-
exports.readCredentialsForConfiguredRelay = readCredentialsForConfiguredRelay;
|
|
16910
|
-
exports.readDaemonState = readDaemonState;
|
|
16911
15929
|
exports.readLocalHappyAgentCredentials = readLocalHappyAgentCredentials;
|
|
16912
|
-
exports.readPersistedSessions = readPersistedSessions;
|
|
16913
|
-
exports.readSettings = readSettings;
|
|
16914
|
-
exports.readSettingsOutcome = readSettingsOutcome;
|
|
16915
15930
|
exports.readVhTerminal = readVhTerminal;
|
|
16916
|
-
exports.releaseDaemonLock = releaseDaemonLock;
|
|
16917
15931
|
exports.resolveCliUpdateCheckInterval = resolveCliUpdateCheckInterval;
|
|
16918
15932
|
exports.scrubTmuxClientEnv = scrubTmuxClientEnv;
|
|
16919
15933
|
exports.sendToVhTerminal = sendToVhTerminal;
|
|
@@ -16922,10 +15936,4 @@ exports.startOfflineReconnection = startOfflineReconnection;
|
|
|
16922
15936
|
exports.streamKeyOf = streamKeyOf;
|
|
16923
15937
|
exports.summarizeSpawnSessionForLog = summarizeSpawnSessionForLog;
|
|
16924
15938
|
exports.tmuxArgs = tmuxArgs;
|
|
16925
|
-
exports.updateSettings = updateSettings;
|
|
16926
15939
|
exports.wrapCommand = wrapCommand;
|
|
16927
|
-
exports.writeCredentialsDataKey = writeCredentialsDataKey;
|
|
16928
|
-
exports.writeCredentialsLegacy = writeCredentialsLegacy;
|
|
16929
|
-
exports.writeDaemonState = writeDaemonState;
|
|
16930
|
-
exports.writePrivateFileSync = writePrivateFileSync;
|
|
16931
|
-
exports.writeSettings = writeSettings;
|