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,625 +1,31 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import
|
|
3
|
-
import { inspect } from 'node:util';
|
|
4
|
-
import { mkdirSync, chmodSync, readdirSync, statSync, appendFileSync, writeFileSync, existsSync, readFileSync, constants, unlinkSync, renameSync, accessSync, createReadStream, createWriteStream } from 'node:fs';
|
|
5
|
-
import os$1, { homedir } from 'node:os';
|
|
6
|
-
import { join, basename, isAbsolute, resolve as resolve$1 } from 'node:path';
|
|
7
|
-
import { mkdir, chmod, writeFile, stat as stat$1, readdir as readdir$1, lstat, open, unlink as unlink$1, readFile as readFile$1, rename as rename$1, copyFile } from 'node:fs/promises';
|
|
2
|
+
import { l as logger, g as encodeBase64, j as encrypt, k as decrypt, m as decodeBase64, c as configuration, o as decryptBlob, q as prepareClipboardText, f as readSettings, r as readPersistedSessions, s as credentialRelayProblem, p as packageJson, t as readTrackedClaudeSessionIds, u as libsodiumEncryptForPublicKey, v as libsodiumPublicKeyFromSecretKey, x as getRandomBytes, y as redactLogValue } from './limits-FjbPJPsF.mjs';
|
|
8
3
|
import { EventEmitter } from 'node:events';
|
|
9
4
|
import { io } from 'socket.io-client';
|
|
10
5
|
import * as z from 'zod';
|
|
11
6
|
import { z as z$1 } from 'zod';
|
|
12
7
|
import { isCuid, createId } from '@paralleldrive/cuid2';
|
|
13
|
-
import {
|
|
14
|
-
import tweetnacl from 'tweetnacl';
|
|
15
|
-
import { p as prepareClipboardText } from './limits-CwFb5S2b.mjs';
|
|
8
|
+
import { createHmac, randomUUID as randomUUID$1, randomBytes, createHash as createHash$1 } from 'node:crypto';
|
|
16
9
|
import { spawn as spawn$1, exec, execSync } from 'child_process';
|
|
17
10
|
import { promisify } from 'util';
|
|
18
|
-
import { mkdir
|
|
19
|
-
import { randomUUID, createHash
|
|
20
|
-
import { dirname, resolve, join
|
|
11
|
+
import { mkdir, writeFile, appendFile, stat, rename, unlink, readdir, readFile } from 'fs/promises';
|
|
12
|
+
import { randomUUID, createHash } from 'crypto';
|
|
13
|
+
import { dirname, resolve, join, sep } from 'path';
|
|
21
14
|
import { spawn } from 'cross-spawn';
|
|
22
15
|
import { fileURLToPath } from 'url';
|
|
23
16
|
import os, { platform } from 'os';
|
|
17
|
+
import { stat as stat$1, readdir as readdir$1, lstat, open, copyFile, unlink as unlink$1, readFile as readFile$1, rename as rename$1, writeFile as writeFile$1 } from 'node:fs/promises';
|
|
18
|
+
import { join as join$1, isAbsolute, resolve as resolve$1 } from 'node:path';
|
|
19
|
+
import os$1, { homedir } from 'node:os';
|
|
24
20
|
import { spawn as spawn$2, spawnSync, execSync as execSync$1 } from 'node:child_process';
|
|
25
21
|
import * as pty from 'node-pty';
|
|
22
|
+
import { existsSync, statSync, writeFileSync, unlinkSync, readFileSync, mkdirSync, accessSync, constants, createReadStream, createWriteStream } from 'node:fs';
|
|
26
23
|
import { existsSync as existsSync$1 } from 'fs';
|
|
24
|
+
import tweetnacl from 'tweetnacl';
|
|
27
25
|
import { createInterface } from 'node:readline';
|
|
28
26
|
import { SandboxManager } from '@anthropic-ai/sandbox-runtime';
|
|
29
27
|
import { Expo } from 'expo-server-sdk';
|
|
30
|
-
|
|
31
|
-
var name = "very-happy-cli";
|
|
32
|
-
var version = "0.2.123";
|
|
33
|
-
var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
|
|
34
|
-
var author = "Kirill Dubovitskiy";
|
|
35
|
-
var contributors = [
|
|
36
|
-
"Mereith (very-happy-cli fork)"
|
|
37
|
-
];
|
|
38
|
-
var license = "MIT";
|
|
39
|
-
var engines = {
|
|
40
|
-
node: "^20.19.0 || ^22.13.0 || >=24.0.0"
|
|
41
|
-
};
|
|
42
|
-
var type = "module";
|
|
43
|
-
var homepage = "https://github.com/Mereithhh/very-happy";
|
|
44
|
-
var bugs = "https://github.com/Mereithhh/very-happy/issues";
|
|
45
|
-
var repository = {
|
|
46
|
-
type: "git",
|
|
47
|
-
url: "git+https://github.com/Mereithhh/very-happy.git"
|
|
48
|
-
};
|
|
49
|
-
var bin = {
|
|
50
|
-
"very-happy": "./bin/very-happy.mjs",
|
|
51
|
-
"very-happy-mcp": "./bin/very-happy-mcp.mjs"
|
|
52
|
-
};
|
|
53
|
-
var main = "./dist/index.cjs";
|
|
54
|
-
var module$1 = "./dist/index.mjs";
|
|
55
|
-
var types = "./dist/index.d.cts";
|
|
56
|
-
var exports$1 = {
|
|
57
|
-
".": {
|
|
58
|
-
require: {
|
|
59
|
-
types: "./dist/index.d.cts",
|
|
60
|
-
"default": "./dist/index.cjs"
|
|
61
|
-
},
|
|
62
|
-
"import": {
|
|
63
|
-
types: "./dist/index.d.mts",
|
|
64
|
-
"default": "./dist/index.mjs"
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
"./lib": {
|
|
68
|
-
require: {
|
|
69
|
-
types: "./dist/lib.d.cts",
|
|
70
|
-
"default": "./dist/lib.cjs"
|
|
71
|
-
},
|
|
72
|
-
"import": {
|
|
73
|
-
types: "./dist/lib.d.mts",
|
|
74
|
-
"default": "./dist/lib.mjs"
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
"./codex/happyMcpStdioBridge": {
|
|
78
|
-
require: {
|
|
79
|
-
types: "./dist/codex/happyMcpStdioBridge.d.cts",
|
|
80
|
-
"default": "./dist/codex/happyMcpStdioBridge.cjs"
|
|
81
|
-
},
|
|
82
|
-
"import": {
|
|
83
|
-
types: "./dist/codex/happyMcpStdioBridge.d.mts",
|
|
84
|
-
"default": "./dist/codex/happyMcpStdioBridge.mjs"
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
var files = [
|
|
89
|
-
"dist",
|
|
90
|
-
"bin",
|
|
91
|
-
"scripts",
|
|
92
|
-
"tools/licenses",
|
|
93
|
-
"package.json",
|
|
94
|
-
"README.md",
|
|
95
|
-
"LICENSE"
|
|
96
|
-
];
|
|
97
|
-
var scripts = {
|
|
98
|
-
typecheck: "tsc --noEmit",
|
|
99
|
-
"bundle:server": "node scripts/bundle-server.cjs",
|
|
100
|
-
"bundle:server:all": "node scripts/bundle-server.cjs --all-platforms",
|
|
101
|
-
"bundle:webapp": "node scripts/bundle-webapp.cjs",
|
|
102
|
-
build: "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc --noEmit && pkgroll",
|
|
103
|
-
test: "pnpm run build && vitest run --project unit",
|
|
104
|
-
"test:integration": "pnpm run build && vitest run --project integration-empty --project integration-plan-mode --project integration-authenticated",
|
|
105
|
-
"cli:install": "node scripts/install-local.cjs",
|
|
106
|
-
prepublishOnly: "pnpm test",
|
|
107
|
-
postinstall: "node scripts/unpack-tools.cjs && node scripts/fix-pty-perms.cjs"
|
|
108
|
-
};
|
|
109
|
-
var dependencies = {
|
|
110
|
-
"@agentclientprotocol/sdk": "^0.14.1",
|
|
111
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.232",
|
|
112
|
-
"@anthropic-ai/sandbox-runtime": "^0.0.37",
|
|
113
|
-
"@modelcontextprotocol/sdk": "1.30.0",
|
|
114
|
-
"@noble/ed25519": "^3.0.0",
|
|
115
|
-
"@noble/hashes": "^2.0.1",
|
|
116
|
-
"@paralleldrive/cuid2": "^2.2.2",
|
|
117
|
-
"@stablelib/base64": "^2.0.1",
|
|
118
|
-
"@stablelib/hex": "^2.0.1",
|
|
119
|
-
"@types/cross-spawn": "^6.0.6",
|
|
120
|
-
"@types/http-proxy": "^1.17.17",
|
|
121
|
-
"@types/react": "^19.2.7",
|
|
122
|
-
"@types/tmp": "^0.2.6",
|
|
123
|
-
axios: "^1.13.2",
|
|
124
|
-
chalk: "^5.6.2",
|
|
125
|
-
"cross-spawn": "^7.0.6",
|
|
126
|
-
"expo-server-sdk": "^3.15.0",
|
|
127
|
-
fastify: "5.8.5",
|
|
128
|
-
"fastify-type-provider-zod": "^6.1.0",
|
|
129
|
-
"http-proxy": "^1.18.1",
|
|
130
|
-
"http-proxy-middleware": "^3.0.7",
|
|
131
|
-
ink: "^6.5.1",
|
|
132
|
-
inquirer: "^13.2.2",
|
|
133
|
-
"node-pty": "^1.0.0",
|
|
134
|
-
open: "^10.2.0",
|
|
135
|
-
"ps-list": "^8.1.1",
|
|
136
|
-
react: "^19.2.0",
|
|
137
|
-
"socket.io-client": "^4.8.1",
|
|
138
|
-
tar: "^7.5.22",
|
|
139
|
-
tmp: "^0.2.7",
|
|
140
|
-
tweetnacl: "^1.0.3",
|
|
141
|
-
ws: "^8.21.3",
|
|
142
|
-
zod: "^4.0.0"
|
|
143
|
-
};
|
|
144
|
-
var optionalDependencies = {
|
|
145
|
-
"very-happy-tools-arm64-darwin": "0.2.123",
|
|
146
|
-
"very-happy-tools-x64-darwin": "0.2.123",
|
|
147
|
-
"very-happy-tools-arm64-linux": "0.2.123",
|
|
148
|
-
"very-happy-tools-x64-linux": "0.2.123",
|
|
149
|
-
"very-happy-tools-arm64-win32": "0.2.123",
|
|
150
|
-
"very-happy-tools-x64-win32": "0.2.123"
|
|
151
|
-
};
|
|
152
|
-
var devDependencies = {
|
|
153
|
-
"@slopus/happy-wire": "workspace:*",
|
|
154
|
-
"@xterm/addon-serialize": "0.13.0",
|
|
155
|
-
"@xterm/headless": "5.5.0",
|
|
156
|
-
"@types/inquirer": "^9.0.9",
|
|
157
|
-
"@types/node": ">=20",
|
|
158
|
-
"@types/ws": "^8.18.1",
|
|
159
|
-
"cross-env": "^10.1.0",
|
|
160
|
-
dotenv: "^16.6.1",
|
|
161
|
-
pkgroll: "^2.14.2",
|
|
162
|
-
"ts-node": "^10",
|
|
163
|
-
tsx: "^4.20.6",
|
|
164
|
-
typescript: "5.9.3",
|
|
165
|
-
vitest: "3.2.6"
|
|
166
|
-
};
|
|
167
|
-
var publishConfig = {
|
|
168
|
-
registry: "https://registry.npmjs.org"
|
|
169
|
-
};
|
|
170
|
-
var packageManager = "pnpm@10.11.0";
|
|
171
|
-
var packageJson = {
|
|
172
|
-
name: name,
|
|
173
|
-
version: version,
|
|
174
|
-
description: description,
|
|
175
|
-
author: author,
|
|
176
|
-
contributors: contributors,
|
|
177
|
-
license: license,
|
|
178
|
-
engines: engines,
|
|
179
|
-
type: type,
|
|
180
|
-
homepage: homepage,
|
|
181
|
-
bugs: bugs,
|
|
182
|
-
repository: repository,
|
|
183
|
-
bin: bin,
|
|
184
|
-
main: main,
|
|
185
|
-
module: module$1,
|
|
186
|
-
types: types,
|
|
187
|
-
exports: exports$1,
|
|
188
|
-
files: files,
|
|
189
|
-
scripts: scripts,
|
|
190
|
-
dependencies: dependencies,
|
|
191
|
-
optionalDependencies: optionalDependencies,
|
|
192
|
-
devDependencies: devDependencies,
|
|
193
|
-
publishConfig: publishConfig,
|
|
194
|
-
packageManager: packageManager
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const PRIVATE_DIRECTORY_MODE = 448;
|
|
198
|
-
const PRIVATE_FILE_MODE = 384;
|
|
199
|
-
function ignoreUnsupportedPermissions(error) {
|
|
200
|
-
if (process.platform !== "win32") throw error;
|
|
201
|
-
}
|
|
202
|
-
function ensurePrivateDirectorySync(path) {
|
|
203
|
-
mkdirSync(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
204
|
-
try {
|
|
205
|
-
chmodSync(path, PRIVATE_DIRECTORY_MODE);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
ignoreUnsupportedPermissions(error);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
async function ensurePrivateDirectory(path) {
|
|
211
|
-
await mkdir(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
212
|
-
try {
|
|
213
|
-
await chmod(path, PRIVATE_DIRECTORY_MODE);
|
|
214
|
-
} catch (error) {
|
|
215
|
-
ignoreUnsupportedPermissions(error);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
function hardenPrivateFileSync(path) {
|
|
219
|
-
try {
|
|
220
|
-
chmodSync(path, PRIVATE_FILE_MODE);
|
|
221
|
-
} catch (error) {
|
|
222
|
-
ignoreUnsupportedPermissions(error);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
function hardenPrivateDirectoryFilesSync(path) {
|
|
226
|
-
for (const name of readdirSync(path)) {
|
|
227
|
-
const candidate = join(path, name);
|
|
228
|
-
try {
|
|
229
|
-
if (statSync(candidate).isFile()) hardenPrivateFileSync(candidate);
|
|
230
|
-
} catch {
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
async function hardenPrivateFile(path) {
|
|
235
|
-
try {
|
|
236
|
-
await chmod(path, PRIVATE_FILE_MODE);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
ignoreUnsupportedPermissions(error);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function writePrivateFileSync(path, data) {
|
|
242
|
-
writeFileSync(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
243
|
-
hardenPrivateFileSync(path);
|
|
244
|
-
}
|
|
245
|
-
async function writePrivateFile(path, data) {
|
|
246
|
-
await writeFile(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
247
|
-
await hardenPrivateFile(path);
|
|
248
|
-
}
|
|
249
|
-
function appendPrivateFileSync(path, data) {
|
|
250
|
-
appendFileSync(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
251
|
-
hardenPrivateFileSync(path);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const DEFAULT_RELAY_URL = "https://veryhappy.dev";
|
|
255
|
-
function normalizeHttpEndpoint(value, source) {
|
|
256
|
-
const trimmed = value.trim();
|
|
257
|
-
let parsed;
|
|
258
|
-
try {
|
|
259
|
-
parsed = new URL(trimmed);
|
|
260
|
-
} catch {
|
|
261
|
-
throw new Error(`${source} must be an absolute http(s) origin`);
|
|
262
|
-
}
|
|
263
|
-
const onlyOriginPath = /^\/+$/u.test(parsed.pathname);
|
|
264
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username.length > 0 || parsed.password.length > 0 || !onlyOriginPath || parsed.search.length > 0 || parsed.hash.length > 0) {
|
|
265
|
-
throw new Error(`${source} must be an http(s) origin without credentials, path, query, or fragment`);
|
|
266
|
-
}
|
|
267
|
-
return parsed.origin;
|
|
268
|
-
}
|
|
269
|
-
function resolveHttpEndpoint(environmentValue, settingsValue, environmentName, settingsName) {
|
|
270
|
-
if (environmentValue !== void 0) {
|
|
271
|
-
return normalizeHttpEndpoint(environmentValue, environmentName);
|
|
272
|
-
}
|
|
273
|
-
if (settingsValue !== void 0) {
|
|
274
|
-
return normalizeHttpEndpoint(settingsValue, `settings.${settingsName}`);
|
|
275
|
-
}
|
|
276
|
-
return DEFAULT_RELAY_URL;
|
|
277
|
-
}
|
|
278
|
-
class Configuration {
|
|
279
|
-
serverUrl;
|
|
280
|
-
webappUrl;
|
|
281
|
-
isDaemonProcess;
|
|
282
|
-
// Directories and paths (from persistence)
|
|
283
|
-
happyHomeDir;
|
|
284
|
-
logsDir;
|
|
285
|
-
settingsFile;
|
|
286
|
-
privateKeyFile;
|
|
287
|
-
daemonStateFile;
|
|
288
|
-
daemonLockFile;
|
|
289
|
-
sessionsFile;
|
|
290
|
-
currentCliVersion;
|
|
291
|
-
isExperimentalEnabled;
|
|
292
|
-
disableCaffeinate;
|
|
293
|
-
constructor() {
|
|
294
|
-
const args = process.argv.slice(2);
|
|
295
|
-
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
296
|
-
if (process.env.HAPPY_HOME_DIR) {
|
|
297
|
-
const expandedPath = process.env.HAPPY_HOME_DIR.replace(/^~/, homedir());
|
|
298
|
-
this.happyHomeDir = expandedPath;
|
|
299
|
-
} else {
|
|
300
|
-
this.happyHomeDir = join(homedir(), ".happy");
|
|
301
|
-
}
|
|
302
|
-
this.logsDir = join(this.happyHomeDir, "logs");
|
|
303
|
-
this.settingsFile = join(this.happyHomeDir, "settings.json");
|
|
304
|
-
this.privateKeyFile = join(this.happyHomeDir, "access.key");
|
|
305
|
-
this.daemonStateFile = join(this.happyHomeDir, "daemon.state.json");
|
|
306
|
-
this.daemonLockFile = join(this.happyHomeDir, "daemon.state.json.lock");
|
|
307
|
-
this.sessionsFile = join(this.happyHomeDir, "sessions.json");
|
|
308
|
-
try {
|
|
309
|
-
this.serverUrl = resolveHttpEndpoint(
|
|
310
|
-
process.env.HAPPY_SERVER_URL,
|
|
311
|
-
readSettingsStringSync(this.settingsFile, "serverUrl"),
|
|
312
|
-
"HAPPY_SERVER_URL",
|
|
313
|
-
"serverUrl"
|
|
314
|
-
);
|
|
315
|
-
this.webappUrl = resolveHttpEndpoint(
|
|
316
|
-
process.env.HAPPY_WEBAPP_URL,
|
|
317
|
-
readSettingsStringSync(this.settingsFile, "webappUrl"),
|
|
318
|
-
"HAPPY_WEBAPP_URL",
|
|
319
|
-
"webappUrl"
|
|
320
|
-
);
|
|
321
|
-
} catch (error) {
|
|
322
|
-
const message = error instanceof Error ? error.message : "invalid client endpoint";
|
|
323
|
-
process.stderr.write(`[very-happy] Configuration error: ${message}
|
|
324
|
-
`);
|
|
325
|
-
process.exit(1);
|
|
326
|
-
}
|
|
327
|
-
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
|
|
328
|
-
this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.HAPPY_DISABLE_CAFFEINATE?.toLowerCase() || "");
|
|
329
|
-
this.currentCliVersion = packageJson.version;
|
|
330
|
-
const variant = process.env.HAPPY_VARIANT || "stable";
|
|
331
|
-
if (!this.isDaemonProcess && variant === "dev") {
|
|
332
|
-
console.log("\x1B[33m\u{1F527} DEV MODE\x1B[0m - Data: " + this.happyHomeDir);
|
|
333
|
-
}
|
|
334
|
-
ensurePrivateDirectorySync(this.happyHomeDir);
|
|
335
|
-
ensurePrivateDirectorySync(this.logsDir);
|
|
336
|
-
for (const privateFile of [
|
|
337
|
-
this.settingsFile,
|
|
338
|
-
this.privateKeyFile,
|
|
339
|
-
this.daemonStateFile,
|
|
340
|
-
this.daemonLockFile,
|
|
341
|
-
this.sessionsFile
|
|
342
|
-
]) {
|
|
343
|
-
if (existsSync(privateFile)) hardenPrivateFileSync(privateFile);
|
|
344
|
-
}
|
|
345
|
-
hardenPrivateDirectoryFilesSync(this.logsDir);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
function readSettingsStringSync(settingsFile, key) {
|
|
349
|
-
try {
|
|
350
|
-
if (!existsSync(settingsFile)) return void 0;
|
|
351
|
-
const raw = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
352
|
-
const value = raw?.[key];
|
|
353
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
354
|
-
} catch {
|
|
355
|
-
return void 0;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
const configuration = new Configuration();
|
|
359
|
-
|
|
360
|
-
function sensitiveKey(key) {
|
|
361
|
-
const normalized = key.toLowerCase().replace(/[_\-\s]/g, "");
|
|
362
|
-
return /(token|secret|password|authorization|cookie|credential|apikey|privatekey|encryptionkey|accesskey|claimsecret)/.test(normalized);
|
|
363
|
-
}
|
|
364
|
-
function redactLogString(value) {
|
|
365
|
-
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]");
|
|
366
|
-
}
|
|
367
|
-
function redactLogValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
368
|
-
if (typeof value === "string") return redactLogString(value);
|
|
369
|
-
if (value === null || typeof value !== "object") return value;
|
|
370
|
-
if (depth >= 6) return "[TRUNCATED]";
|
|
371
|
-
if (seen.has(value)) return "[CIRCULAR]";
|
|
372
|
-
seen.add(value);
|
|
373
|
-
if (value instanceof Error) {
|
|
374
|
-
const error = value;
|
|
375
|
-
return {
|
|
376
|
-
name: error.name,
|
|
377
|
-
message: redactLogString(error.message),
|
|
378
|
-
...typeof error.code === "string" || typeof error.code === "number" ? { code: error.code } : {},
|
|
379
|
-
...typeof (error.status ?? error.response?.status) === "number" ? { status: error.status ?? error.response?.status } : {}
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
if (Array.isArray(value)) return value.map((item) => redactLogValue(item, depth + 1, seen));
|
|
383
|
-
const result = {};
|
|
384
|
-
for (const [key, child] of Object.entries(value)) {
|
|
385
|
-
if (sensitiveKey(key)) {
|
|
386
|
-
result[key] = "[REDACTED]";
|
|
387
|
-
} else if (key === "environmentVariables" && child && typeof child === "object") {
|
|
388
|
-
result[key] = { names: Object.keys(child).sort() };
|
|
389
|
-
} else {
|
|
390
|
-
result[key] = redactLogValue(child, depth + 1, seen);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return result;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
|
|
397
|
-
return date.toLocaleString("sv-SE", {
|
|
398
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
399
|
-
year: "numeric",
|
|
400
|
-
month: "2-digit",
|
|
401
|
-
day: "2-digit",
|
|
402
|
-
hour: "2-digit",
|
|
403
|
-
minute: "2-digit",
|
|
404
|
-
second: "2-digit"
|
|
405
|
-
}).replace(/[: ]/g, "-").replace(/,/g, "") + "-pid-" + process.pid;
|
|
406
|
-
}
|
|
407
|
-
function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
|
|
408
|
-
return date.toLocaleTimeString("en-US", {
|
|
409
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
410
|
-
hour12: false,
|
|
411
|
-
hour: "2-digit",
|
|
412
|
-
minute: "2-digit",
|
|
413
|
-
second: "2-digit",
|
|
414
|
-
fractionalSecondDigits: 3
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
function getSessionLogPath() {
|
|
418
|
-
const timestamp = createTimestampForFilename();
|
|
419
|
-
const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
|
|
420
|
-
return join(configuration.logsDir, filename);
|
|
421
|
-
}
|
|
422
|
-
function safeRemoteLogUrl(raw) {
|
|
423
|
-
if (!raw) return void 0;
|
|
424
|
-
try {
|
|
425
|
-
const url = new URL(raw);
|
|
426
|
-
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
427
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return void 0;
|
|
428
|
-
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") return void 0;
|
|
429
|
-
return url.origin;
|
|
430
|
-
} catch {
|
|
431
|
-
return void 0;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
class Logger {
|
|
435
|
-
constructor(logFilePath = getSessionLogPath()) {
|
|
436
|
-
this.logFilePath = logFilePath;
|
|
437
|
-
const remoteUrl = safeRemoteLogUrl(process.env.HAPPY_SERVER_URL);
|
|
438
|
-
const remoteToken = process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING_TOKEN;
|
|
439
|
-
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && remoteUrl && remoteToken && Buffer.byteLength(remoteToken) >= 32) {
|
|
440
|
-
this.dangerouslyUnencryptedServerLoggingUrl = remoteUrl;
|
|
441
|
-
this.dangerouslyUnencryptedServerLoggingToken = remoteToken;
|
|
442
|
-
console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
dangerouslyUnencryptedServerLoggingUrl;
|
|
446
|
-
dangerouslyUnencryptedServerLoggingToken;
|
|
447
|
-
// Use local timezone for simplicity of locating the logs,
|
|
448
|
-
// in practice you will not need absolute timestamps
|
|
449
|
-
localTimezoneTimestamp() {
|
|
450
|
-
return createTimestampForLogEntry();
|
|
451
|
-
}
|
|
452
|
-
debug(message, ...args) {
|
|
453
|
-
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
|
|
454
|
-
}
|
|
455
|
-
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
|
|
456
|
-
if (!process.env.DEBUG) {
|
|
457
|
-
this.debug(`In production, skipping message inspection`);
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
const truncateStrings = (obj) => {
|
|
461
|
-
if (typeof obj === "string") {
|
|
462
|
-
return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
|
|
463
|
-
}
|
|
464
|
-
if (Array.isArray(obj)) {
|
|
465
|
-
const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
|
|
466
|
-
if (obj.length > maxArrayLength) {
|
|
467
|
-
truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
|
|
468
|
-
}
|
|
469
|
-
return truncatedArray;
|
|
470
|
-
}
|
|
471
|
-
if (obj && typeof obj === "object") {
|
|
472
|
-
const result = {};
|
|
473
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
474
|
-
if (key === "usage") {
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
result[key] = truncateStrings(value);
|
|
478
|
-
}
|
|
479
|
-
return result;
|
|
480
|
-
}
|
|
481
|
-
return obj;
|
|
482
|
-
};
|
|
483
|
-
const truncatedObject = truncateStrings(redactLogValue(object));
|
|
484
|
-
const json = JSON.stringify(truncatedObject, null, 2);
|
|
485
|
-
this.logToFile(`[${this.localTimezoneTimestamp()}]`, redactLogString(message), "\n", json);
|
|
486
|
-
}
|
|
487
|
-
info(message, ...args) {
|
|
488
|
-
this.logToConsole("info", "", message, ...args);
|
|
489
|
-
this.debug(message, args);
|
|
490
|
-
}
|
|
491
|
-
infoDeveloper(message, ...args) {
|
|
492
|
-
this.debug(message, ...args);
|
|
493
|
-
if (process.env.DEBUG) {
|
|
494
|
-
this.logToConsole("info", "[DEV]", message, ...args);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
warn(message, ...args) {
|
|
498
|
-
this.logToConsole("warn", "", message, ...args);
|
|
499
|
-
this.debug(`[WARN] ${message}`, ...args);
|
|
500
|
-
}
|
|
501
|
-
getLogPath() {
|
|
502
|
-
return this.logFilePath;
|
|
503
|
-
}
|
|
504
|
-
logToConsole(level, prefix, message, ...args) {
|
|
505
|
-
message = redactLogString(message);
|
|
506
|
-
args = args.map((arg) => redactLogValue(arg));
|
|
507
|
-
switch (level) {
|
|
508
|
-
case "debug": {
|
|
509
|
-
console.log(chalk.gray(prefix), message, ...args);
|
|
510
|
-
break;
|
|
511
|
-
}
|
|
512
|
-
case "error": {
|
|
513
|
-
console.error(chalk.red(prefix), message, ...args);
|
|
514
|
-
break;
|
|
515
|
-
}
|
|
516
|
-
case "info": {
|
|
517
|
-
console.log(chalk.blue(prefix), message, ...args);
|
|
518
|
-
break;
|
|
519
|
-
}
|
|
520
|
-
case "warn": {
|
|
521
|
-
console.log(chalk.yellow(prefix), message, ...args);
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
default: {
|
|
525
|
-
this.debug("Unknown log level:", level);
|
|
526
|
-
console.log(chalk.blue(prefix), message, ...args);
|
|
527
|
-
break;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
async sendToRemoteServer(level, message, ...args) {
|
|
532
|
-
if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
|
|
533
|
-
try {
|
|
534
|
-
await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
|
|
535
|
-
method: "POST",
|
|
536
|
-
headers: {
|
|
537
|
-
"Content-Type": "application/json",
|
|
538
|
-
"Authorization": `Bearer ${this.dangerouslyUnencryptedServerLoggingToken}`
|
|
539
|
-
},
|
|
540
|
-
body: JSON.stringify({
|
|
541
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
542
|
-
level,
|
|
543
|
-
message: `${redactLogString(message)} ${args.map((raw) => {
|
|
544
|
-
const safe = redactLogValue(raw);
|
|
545
|
-
return typeof safe === "object" ? JSON.stringify(safe, null, 2) : String(safe);
|
|
546
|
-
}).join(" ")}`,
|
|
547
|
-
source: "cli",
|
|
548
|
-
platform: process.platform
|
|
549
|
-
})
|
|
550
|
-
});
|
|
551
|
-
} catch (error) {
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
logToFile(prefix, message, ...args) {
|
|
555
|
-
const safeMessage = redactLogString(message);
|
|
556
|
-
const safeArgs = args.map((arg) => redactLogValue(arg));
|
|
557
|
-
const logLine = `${prefix} ${safeMessage} ${safeArgs.map(
|
|
558
|
-
(arg) => typeof arg === "string" ? arg : inspect(arg, { depth: 5, breakLength: 120 })
|
|
559
|
-
).join(" ")}
|
|
560
|
-
`;
|
|
561
|
-
if (this.dangerouslyUnencryptedServerLoggingUrl) {
|
|
562
|
-
let level = "info";
|
|
563
|
-
if (prefix.includes(this.localTimezoneTimestamp())) {
|
|
564
|
-
level = "debug";
|
|
565
|
-
}
|
|
566
|
-
this.sendToRemoteServer(level, safeMessage, ...safeArgs).catch(() => {
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
try {
|
|
570
|
-
appendPrivateFileSync(this.logFilePath, logLine);
|
|
571
|
-
} catch (appendError) {
|
|
572
|
-
if (process.env.DEBUG) {
|
|
573
|
-
console.error("[DEV MODE ONLY THROWING] Failed to append to log file:", appendError);
|
|
574
|
-
throw appendError;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
let logger = new Logger();
|
|
580
|
-
async function listDaemonLogFiles(limit = 50) {
|
|
581
|
-
try {
|
|
582
|
-
const logsDir = configuration.logsDir;
|
|
583
|
-
if (!existsSync(logsDir)) {
|
|
584
|
-
return [];
|
|
585
|
-
}
|
|
586
|
-
const logs = readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
|
|
587
|
-
const fullPath = join(logsDir, file);
|
|
588
|
-
const stats = statSync(fullPath);
|
|
589
|
-
return { file, path: fullPath, modified: stats.mtime };
|
|
590
|
-
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
591
|
-
try {
|
|
592
|
-
const { readDaemonState } = await Promise.resolve().then(function () { return persistence; });
|
|
593
|
-
const state = await readDaemonState();
|
|
594
|
-
if (!state) {
|
|
595
|
-
return logs;
|
|
596
|
-
}
|
|
597
|
-
if (state.daemonLogPath && existsSync(state.daemonLogPath)) {
|
|
598
|
-
const stats = statSync(state.daemonLogPath);
|
|
599
|
-
const persisted = {
|
|
600
|
-
file: basename(state.daemonLogPath),
|
|
601
|
-
path: state.daemonLogPath,
|
|
602
|
-
modified: stats.mtime
|
|
603
|
-
};
|
|
604
|
-
const idx = logs.findIndex((l) => l.path === persisted.path);
|
|
605
|
-
if (idx >= 0) {
|
|
606
|
-
const [found] = logs.splice(idx, 1);
|
|
607
|
-
logs.unshift(found);
|
|
608
|
-
} else {
|
|
609
|
-
logs.unshift(persisted);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
} catch {
|
|
613
|
-
}
|
|
614
|
-
return logs.slice(0, Math.max(0, limit));
|
|
615
|
-
} catch {
|
|
616
|
-
return [];
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
async function getLatestDaemonLog() {
|
|
620
|
-
const [latest] = await listDaemonLogFiles(1);
|
|
621
|
-
return latest || null;
|
|
622
|
-
}
|
|
28
|
+
import chalk from 'chalk';
|
|
623
29
|
|
|
624
30
|
const sessionRoleSchema = z.enum(["user", "agent"]);
|
|
625
31
|
const sessionTextEventSchema = z.object({
|
|
@@ -1026,8 +432,34 @@ const ConnectionDiagnosticEventSchema = z$1.object({
|
|
|
1026
432
|
z$1.object({
|
|
1027
433
|
events: z$1.array(ConnectionDiagnosticEventSchema).min(1).max(32)
|
|
1028
434
|
}).strict();
|
|
435
|
+
const id = z$1.string().min(1).max(128);
|
|
436
|
+
const text = z$1.string().min(1).max(32e3);
|
|
437
|
+
const TeamActionSchema = z$1.discriminatedUnion("type", [
|
|
438
|
+
z$1.object({ type: z$1.literal("archive") }),
|
|
439
|
+
z$1.object({ type: z$1.literal("schedule-create"), name: id, botId: id, body: text, runAt: z$1.number().int().positive().max(864e13), intervalMs: z$1.number().int().min(6e4).max(366 * 864e5).optional() }),
|
|
440
|
+
z$1.object({ type: z$1.literal("schedule-pause"), scheduleId: id, version: z$1.number().int().positive() }),
|
|
441
|
+
z$1.object({ type: z$1.literal("schedule-resume"), scheduleId: id, version: z$1.number().int().positive() }),
|
|
442
|
+
z$1.object({ type: z$1.literal("schedule-cancel"), scheduleId: id, version: z$1.number().int().positive() }),
|
|
443
|
+
z$1.object({ type: z$1.literal("reconcile-operation"), operationId: id, claimId: id, note: z$1.string().trim().min(1).max(4e3) }),
|
|
444
|
+
z$1.object({ type: z$1.literal("join"), name: id, sessionId: id, botId: id.optional() }),
|
|
445
|
+
z$1.object({ type: z$1.literal("delegate"), goal: text, acceptance: z$1.array(text).min(1).max(32), parentTaskId: id.optional(), assigneeBotId: id.optional(), botName: id.optional(), directory: z$1.string().min(1).max(4096).optional(), assistant: z$1.enum(["claude", "codex", "pi-acp"]).optional() }),
|
|
446
|
+
z$1.object({ type: z$1.literal("message"), taskId: id, body: text, recipientBotId: id.optional() }),
|
|
447
|
+
z$1.object({ type: z$1.literal("submit"), taskId: id, attemptId: id, goalVersion: z$1.number().int().positive(), result: text }),
|
|
448
|
+
z$1.object({ type: z$1.literal("accept"), taskId: id, attemptId: id, goalVersion: z$1.number().int().positive() }),
|
|
449
|
+
z$1.object({ type: z$1.literal("return"), taskId: id, attemptId: id, goalVersion: z$1.number().int().positive(), reason: text }),
|
|
450
|
+
z$1.object({ type: z$1.literal("cancel"), taskId: id, attemptId: id, goalVersion: z$1.number().int().positive(), reason: text }),
|
|
451
|
+
z$1.object({ type: z$1.literal("handoff"), taskId: id, attemptId: id, goalVersion: z$1.number().int().positive(), assigneeBotId: id }),
|
|
452
|
+
z$1.object({ type: z$1.literal("claim-operation"), operationId: id, machineId: id }),
|
|
453
|
+
z$1.object({ type: z$1.literal("complete-operation"), operationId: id, machineId: id, claimId: id, sessionId: id.optional() }),
|
|
454
|
+
z$1.object({ type: z$1.literal("fail-operation"), operationId: id, machineId: id, claimId: id, error: text, unknown: z$1.boolean().optional() }),
|
|
455
|
+
z$1.object({ type: z$1.literal("session-event"), sessionId: id, event: z$1.enum(["idle", "blocked", "exited"]) }),
|
|
456
|
+
z$1.object({ type: z$1.literal("message-delivered"), messageId: id, recipientBotId: id, generation: z$1.number().int().positive(), sessionId: id })
|
|
457
|
+
]);
|
|
458
|
+
z$1.object({ requestId: id, action: TeamActionSchema });
|
|
459
|
+
z$1.object({ name: id, machineId: id, requestId: id.optional() });
|
|
1029
460
|
|
|
1030
461
|
z$1.object({
|
|
462
|
+
teamsVersion: z$1.number().int().positive().optional(),
|
|
1031
463
|
host: z$1.string(),
|
|
1032
464
|
platform: z$1.string(),
|
|
1033
465
|
happyCliVersion: z$1.string(),
|
|
@@ -1302,125 +734,6 @@ const AgentMessageSchema = z$1.object({
|
|
|
1302
734
|
});
|
|
1303
735
|
z$1.union([UserMessageSchema, AgentMessageSchema]);
|
|
1304
736
|
|
|
1305
|
-
function encodeBase64(buffer, variant = "base64") {
|
|
1306
|
-
if (variant === "base64url") {
|
|
1307
|
-
return encodeBase64Url(buffer);
|
|
1308
|
-
}
|
|
1309
|
-
return Buffer.from(buffer).toString("base64");
|
|
1310
|
-
}
|
|
1311
|
-
function encodeBase64Url(buffer) {
|
|
1312
|
-
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
1313
|
-
}
|
|
1314
|
-
function decodeBase64(base64, variant = "base64") {
|
|
1315
|
-
if (variant === "base64url") {
|
|
1316
|
-
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
|
|
1317
|
-
return new Uint8Array(Buffer.from(base64Standard, "base64"));
|
|
1318
|
-
}
|
|
1319
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
1320
|
-
}
|
|
1321
|
-
function getRandomBytes(size) {
|
|
1322
|
-
return new Uint8Array(randomBytes(size));
|
|
1323
|
-
}
|
|
1324
|
-
function libsodiumPublicKeyFromSecretKey(seed) {
|
|
1325
|
-
const hashedSeed = new Uint8Array(createHash("sha512").update(seed).digest());
|
|
1326
|
-
const secretKey = hashedSeed.slice(0, 32);
|
|
1327
|
-
return new Uint8Array(tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey);
|
|
1328
|
-
}
|
|
1329
|
-
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
1330
|
-
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
1331
|
-
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
|
|
1332
|
-
const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
|
|
1333
|
-
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
|
|
1334
|
-
result.set(ephemeralKeyPair.publicKey, 0);
|
|
1335
|
-
result.set(nonce, ephemeralKeyPair.publicKey.length);
|
|
1336
|
-
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
|
|
1337
|
-
return result;
|
|
1338
|
-
}
|
|
1339
|
-
function encryptLegacy(data, secret) {
|
|
1340
|
-
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
1341
|
-
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
1342
|
-
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
1343
|
-
result.set(nonce);
|
|
1344
|
-
result.set(encrypted, nonce.length);
|
|
1345
|
-
return result;
|
|
1346
|
-
}
|
|
1347
|
-
function decryptLegacy(data, secret) {
|
|
1348
|
-
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
1349
|
-
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
1350
|
-
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
1351
|
-
if (!decrypted) {
|
|
1352
|
-
return null;
|
|
1353
|
-
}
|
|
1354
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
1355
|
-
}
|
|
1356
|
-
function decryptBlob(bundle, key) {
|
|
1357
|
-
if (bundle.length < tweetnacl.secretbox.nonceLength + 16) {
|
|
1358
|
-
return null;
|
|
1359
|
-
}
|
|
1360
|
-
const nonce = bundle.slice(0, tweetnacl.secretbox.nonceLength);
|
|
1361
|
-
const ciphertext = bundle.slice(tweetnacl.secretbox.nonceLength);
|
|
1362
|
-
const decrypted = tweetnacl.secretbox.open(ciphertext, nonce, key);
|
|
1363
|
-
if (!decrypted) {
|
|
1364
|
-
return null;
|
|
1365
|
-
}
|
|
1366
|
-
return new Uint8Array(decrypted);
|
|
1367
|
-
}
|
|
1368
|
-
function encryptWithDataKey(data, dataKey) {
|
|
1369
|
-
const nonce = getRandomBytes(12);
|
|
1370
|
-
const cipher = createCipheriv("aes-256-gcm", dataKey, nonce);
|
|
1371
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
1372
|
-
const encrypted = Buffer.concat([
|
|
1373
|
-
cipher.update(plaintext),
|
|
1374
|
-
cipher.final()
|
|
1375
|
-
]);
|
|
1376
|
-
const authTag = cipher.getAuthTag();
|
|
1377
|
-
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
|
|
1378
|
-
bundle.set([0], 0);
|
|
1379
|
-
bundle.set(nonce, 1);
|
|
1380
|
-
bundle.set(new Uint8Array(encrypted), 13);
|
|
1381
|
-
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
1382
|
-
return bundle;
|
|
1383
|
-
}
|
|
1384
|
-
function decryptWithDataKey(bundle, dataKey) {
|
|
1385
|
-
if (bundle.length < 1) {
|
|
1386
|
-
return null;
|
|
1387
|
-
}
|
|
1388
|
-
if (bundle[0] !== 0) {
|
|
1389
|
-
return null;
|
|
1390
|
-
}
|
|
1391
|
-
if (bundle.length < 12 + 16 + 1) {
|
|
1392
|
-
return null;
|
|
1393
|
-
}
|
|
1394
|
-
const nonce = bundle.slice(1, 13);
|
|
1395
|
-
const authTag = bundle.slice(bundle.length - 16);
|
|
1396
|
-
const ciphertext = bundle.slice(13, bundle.length - 16);
|
|
1397
|
-
try {
|
|
1398
|
-
const decipher = createDecipheriv("aes-256-gcm", dataKey, nonce);
|
|
1399
|
-
decipher.setAuthTag(authTag);
|
|
1400
|
-
const decrypted = Buffer.concat([
|
|
1401
|
-
decipher.update(ciphertext),
|
|
1402
|
-
decipher.final()
|
|
1403
|
-
]);
|
|
1404
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
return null;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
function encrypt(key, variant, data) {
|
|
1410
|
-
if (variant === "legacy") {
|
|
1411
|
-
return encryptLegacy(data, key);
|
|
1412
|
-
} else {
|
|
1413
|
-
return encryptWithDataKey(data, key);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
function decrypt(key, variant, data) {
|
|
1417
|
-
if (variant === "legacy") {
|
|
1418
|
-
return decryptLegacy(data, key);
|
|
1419
|
-
} else {
|
|
1420
|
-
return decryptWithDataKey(data, key);
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
737
|
async function delay(ms) {
|
|
1425
738
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1426
739
|
}
|
|
@@ -1693,7 +1006,7 @@ function projectPath() {
|
|
|
1693
1006
|
}
|
|
1694
1007
|
|
|
1695
1008
|
function run$1(args, options) {
|
|
1696
|
-
const RUNNER_PATH = resolve(join
|
|
1009
|
+
const RUNNER_PATH = resolve(join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
1697
1010
|
return new Promise((resolve2, reject) => {
|
|
1698
1011
|
const child = spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
|
|
1699
1012
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1724,7 +1037,7 @@ function run$1(args, options) {
|
|
|
1724
1037
|
function getBinaryPath() {
|
|
1725
1038
|
const platformName = platform();
|
|
1726
1039
|
const binaryName = platformName === "win32" ? "difft.exe" : "difft";
|
|
1727
|
-
return resolve(join
|
|
1040
|
+
return resolve(join(projectPath(), "tools", "unpacked", binaryName));
|
|
1728
1041
|
}
|
|
1729
1042
|
function run(args, options) {
|
|
1730
1043
|
const binaryPath = getBinaryPath();
|
|
@@ -1836,7 +1149,7 @@ function createUploadFileHandlers(happyHomeDir, now = Date.now, pathQuoteStyle =
|
|
|
1836
1149
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1837
1150
|
await Promise.all(entries.map(async (entry) => {
|
|
1838
1151
|
if (!entry.isFile() || !/^\..+\.[A-Za-z0-9_-]{8,80}\.part$/.test(entry.name)) return;
|
|
1839
|
-
const path = join
|
|
1152
|
+
const path = join(dir, entry.name);
|
|
1840
1153
|
const fileStat = await stat(path).catch(() => null);
|
|
1841
1154
|
if (fileStat && fileStat.mtimeMs < cutoff) await unlink(path).catch(() => {
|
|
1842
1155
|
});
|
|
@@ -1846,13 +1159,13 @@ function createUploadFileHandlers(happyHomeDir, now = Date.now, pathQuoteStyle =
|
|
|
1846
1159
|
let tempPath = null;
|
|
1847
1160
|
try {
|
|
1848
1161
|
const buffer = decodeUploadBase64(data.content, MAX_USER_UPLOAD_BYTES);
|
|
1849
|
-
const dir = join
|
|
1850
|
-
await mkdir
|
|
1162
|
+
const dir = join(happyHomeDir, "uploads", safeSegment(data.subdir, "misc", 80));
|
|
1163
|
+
await mkdir(dir, { recursive: true });
|
|
1851
1164
|
const name = safeFileName(data.name);
|
|
1852
1165
|
const token = randomUUID().replace(/-/g, "").slice(0, 12);
|
|
1853
|
-
const target = join
|
|
1854
|
-
tempPath = join
|
|
1855
|
-
await writeFile
|
|
1166
|
+
const target = join(dir, uniqueFileName(name, token));
|
|
1167
|
+
tempPath = join(dir, `.${name}.legacy_${randomUUID().replace(/-/g, "")}.part`);
|
|
1168
|
+
await writeFile(tempPath, buffer, { flag: "wx" });
|
|
1856
1169
|
await rename(tempPath, target);
|
|
1857
1170
|
tempPath = null;
|
|
1858
1171
|
return { success: true, path: target, pathQuoteStyle, size: buffer.length };
|
|
@@ -1872,13 +1185,13 @@ function createUploadFileHandlers(happyHomeDir, now = Date.now, pathQuoteStyle =
|
|
|
1872
1185
|
}
|
|
1873
1186
|
if (active.has(uploadId)) throw new Error("Upload id is already active");
|
|
1874
1187
|
if (active.size >= MAX_ACTIVE_UPLOADS) throw new Error("Too many active uploads");
|
|
1875
|
-
const dir = join
|
|
1876
|
-
await mkdir
|
|
1188
|
+
const dir = join(happyHomeDir, "uploads", safeSegment(data.subdir, "misc", 80));
|
|
1189
|
+
await mkdir(dir, { recursive: true });
|
|
1877
1190
|
await sweepExpiredFiles(dir);
|
|
1878
1191
|
const name = safeFileName(data.name);
|
|
1879
|
-
const targetPath = join
|
|
1880
|
-
const tempPath = join
|
|
1881
|
-
await writeFile
|
|
1192
|
+
const targetPath = join(dir, uniqueFileName(name, randomUUID().replace(/-/g, "").slice(0, 12)));
|
|
1193
|
+
const tempPath = join(dir, `.${name}.${uploadId}.part`);
|
|
1194
|
+
await writeFile(tempPath, Buffer.alloc(0), { flag: "wx" });
|
|
1882
1195
|
active.set(uploadId, { tempPath, targetPath, totalSize: data.totalSize, received: 0, touchedAt: now() });
|
|
1883
1196
|
return { success: true, size: 0 };
|
|
1884
1197
|
}
|
|
@@ -2014,7 +1327,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2014
1327
|
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
2015
1328
|
try {
|
|
2016
1329
|
const existingBuffer = await readFile(validation.resolvedPath);
|
|
2017
|
-
const existingHash = createHash
|
|
1330
|
+
const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
|
|
2018
1331
|
if (existingHash !== data.expectedHash) {
|
|
2019
1332
|
return {
|
|
2020
1333
|
success: false,
|
|
@@ -2046,8 +1359,8 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2046
1359
|
}
|
|
2047
1360
|
}
|
|
2048
1361
|
const buffer = Buffer.from(data.content, "base64");
|
|
2049
|
-
await writeFile
|
|
2050
|
-
const hash = createHash
|
|
1362
|
+
await writeFile(validation.resolvedPath, buffer);
|
|
1363
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
2051
1364
|
return { success: true, hash };
|
|
2052
1365
|
} catch (error) {
|
|
2053
1366
|
logger.debug("Failed to write file:", error);
|
|
@@ -2068,7 +1381,7 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2068
1381
|
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
2069
1382
|
const directoryEntries = await Promise.all(
|
|
2070
1383
|
entries.map(async (entry) => {
|
|
2071
|
-
const fullPath = join
|
|
1384
|
+
const fullPath = join(directoryPath, entry.name);
|
|
2072
1385
|
let type = "other";
|
|
2073
1386
|
let size;
|
|
2074
1387
|
let modified;
|
|
@@ -2125,10 +1438,10 @@ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
|
|
|
2125
1438
|
await Promise.all(
|
|
2126
1439
|
entries.map(async (entry) => {
|
|
2127
1440
|
if (entry.isSymbolicLink()) {
|
|
2128
|
-
logger.debug(`Skipping symlink: ${join
|
|
1441
|
+
logger.debug(`Skipping symlink: ${join(path, entry.name)}`);
|
|
2129
1442
|
return;
|
|
2130
1443
|
}
|
|
2131
|
-
const childPath = join
|
|
1444
|
+
const childPath = join(path, entry.name);
|
|
2132
1445
|
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
2133
1446
|
if (childNode) {
|
|
2134
1447
|
children.push(childNode);
|
|
@@ -4423,10 +3736,10 @@ function normalizeFsPath(input, homeDir) {
|
|
|
4423
3736
|
if (p === "~") {
|
|
4424
3737
|
p = homeDir;
|
|
4425
3738
|
} else if (p.startsWith("~/")) {
|
|
4426
|
-
p = join(homeDir, p.slice(2));
|
|
3739
|
+
p = join$1(homeDir, p.slice(2));
|
|
4427
3740
|
}
|
|
4428
3741
|
if (!isAbsolute(p)) {
|
|
4429
|
-
p = join(homeDir, p);
|
|
3742
|
+
p = join$1(homeDir, p);
|
|
4430
3743
|
}
|
|
4431
3744
|
return resolve$1(p);
|
|
4432
3745
|
}
|
|
@@ -4482,7 +3795,7 @@ async function fsList(inputPath, homeDir = homedir()) {
|
|
|
4482
3795
|
dirents.map(async (d) => {
|
|
4483
3796
|
const entry = { name: d.name, type: entryTypeOf(d) };
|
|
4484
3797
|
try {
|
|
4485
|
-
const s = await lstat(join(path, d.name));
|
|
3798
|
+
const s = await lstat(join$1(path, d.name));
|
|
4486
3799
|
entry.size = s.size;
|
|
4487
3800
|
entry.mtimeMs = Math.round(s.mtimeMs);
|
|
4488
3801
|
} catch {
|
|
@@ -4539,391 +3852,6 @@ function registerFsHandlers(rpcHandlerManager) {
|
|
|
4539
3852
|
});
|
|
4540
3853
|
}
|
|
4541
3854
|
|
|
4542
|
-
const DEFAULT_SERVER_URL = "https://veryhappy.dev";
|
|
4543
|
-
const LEGACY_CLOUD_SERVER_URL = "https://happy.mereith.com";
|
|
4544
|
-
const CLOUD_SERVER_URLS = [DEFAULT_SERVER_URL, LEGACY_CLOUD_SERVER_URL];
|
|
4545
|
-
function sameRelay(left, right) {
|
|
4546
|
-
try {
|
|
4547
|
-
return new URL(left).origin === new URL(right).origin;
|
|
4548
|
-
} catch {
|
|
4549
|
-
return left.replace(/\/+$/, "") === right.replace(/\/+$/, "");
|
|
4550
|
-
}
|
|
4551
|
-
}
|
|
4552
|
-
function credentialRelayProblem(credentialServerUrl, configuredServerUrl) {
|
|
4553
|
-
if (credentialServerUrl) {
|
|
4554
|
-
return sameRelay(credentialServerUrl, configuredServerUrl) ? void 0 : `Credentials belong to ${credentialServerUrl}, but the active relay is ${configuredServerUrl}.`;
|
|
4555
|
-
}
|
|
4556
|
-
if (CLOUD_SERVER_URLS.some((cloudUrl) => sameRelay(configuredServerUrl, cloudUrl))) {
|
|
4557
|
-
return void 0;
|
|
4558
|
-
}
|
|
4559
|
-
return `These credentials predate relay tracking, so they cannot be safely reused with ${configuredServerUrl}.`;
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
|
-
const SandboxConfigSchema = z.object({
|
|
4563
|
-
enabled: z.boolean().default(false),
|
|
4564
|
-
workspaceRoot: z.string().optional(),
|
|
4565
|
-
sessionIsolation: z.enum(["strict", "workspace", "custom"]).default("workspace"),
|
|
4566
|
-
customWritePaths: z.array(z.string()).default([]),
|
|
4567
|
-
denyReadPaths: z.array(z.string()).default(["~/.ssh", "~/.aws", "~/.gnupg"]),
|
|
4568
|
-
extraWritePaths: z.array(z.string()).default(["/tmp"]),
|
|
4569
|
-
denyWritePaths: z.array(z.string()).default([".env"]),
|
|
4570
|
-
networkMode: z.enum(["blocked", "allowed", "custom"]).default("allowed"),
|
|
4571
|
-
allowedDomains: z.array(z.string()).default([]),
|
|
4572
|
-
deniedDomains: z.array(z.string()).default([]),
|
|
4573
|
-
allowLocalBinding: z.boolean().default(true)
|
|
4574
|
-
});
|
|
4575
|
-
const SUPPORTED_SCHEMA_VERSION = 2;
|
|
4576
|
-
const defaultSettings = {
|
|
4577
|
-
schemaVersion: SUPPORTED_SCHEMA_VERSION,
|
|
4578
|
-
onboardingCompleted: false,
|
|
4579
|
-
sandboxConfig: void 0
|
|
4580
|
-
};
|
|
4581
|
-
function migrateSettings(raw, fromVersion) {
|
|
4582
|
-
let migrated = { ...raw };
|
|
4583
|
-
return migrated;
|
|
4584
|
-
}
|
|
4585
|
-
async function readSettingsOutcome() {
|
|
4586
|
-
if (!existsSync(configuration.settingsFile)) {
|
|
4587
|
-
return { kind: "absent" };
|
|
4588
|
-
}
|
|
4589
|
-
try {
|
|
4590
|
-
return { kind: "ok", settings: await parseSettingsFile() };
|
|
4591
|
-
} catch (error) {
|
|
4592
|
-
return { kind: "unreadable", error: error?.message ?? String(error) };
|
|
4593
|
-
}
|
|
4594
|
-
}
|
|
4595
|
-
async function readSettings() {
|
|
4596
|
-
const outcome = await readSettingsOutcome();
|
|
4597
|
-
if (outcome.kind === "ok") return outcome.settings;
|
|
4598
|
-
if (outcome.kind === "unreadable") {
|
|
4599
|
-
logger.warn(`Failed to read settings: ${outcome.error}`);
|
|
4600
|
-
}
|
|
4601
|
-
return { ...defaultSettings };
|
|
4602
|
-
}
|
|
4603
|
-
async function parseSettingsFile() {
|
|
4604
|
-
const content = await readFile$1(configuration.settingsFile, "utf8");
|
|
4605
|
-
const raw = JSON.parse(content);
|
|
4606
|
-
const schemaVersion = raw.schemaVersion ?? 1;
|
|
4607
|
-
if (schemaVersion > SUPPORTED_SCHEMA_VERSION) {
|
|
4608
|
-
logger.warn(
|
|
4609
|
-
`\u26A0\uFE0F Settings schema v${schemaVersion} > supported v${SUPPORTED_SCHEMA_VERSION}. Update happy-cli for full functionality.`
|
|
4610
|
-
);
|
|
4611
|
-
}
|
|
4612
|
-
const migrated = migrateSettings(raw);
|
|
4613
|
-
if (migrated.sandboxConfig !== void 0) {
|
|
4614
|
-
try {
|
|
4615
|
-
migrated.sandboxConfig = SandboxConfigSchema.parse(migrated.sandboxConfig);
|
|
4616
|
-
} catch (error) {
|
|
4617
|
-
logger.warn(`\u26A0\uFE0F Invalid sandbox config - skipping. Error: ${error.message}`);
|
|
4618
|
-
migrated.sandboxConfig = void 0;
|
|
4619
|
-
}
|
|
4620
|
-
}
|
|
4621
|
-
return { ...defaultSettings, ...migrated };
|
|
4622
|
-
}
|
|
4623
|
-
async function writeSettings(settings) {
|
|
4624
|
-
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
4625
|
-
const settingsWithVersion = {
|
|
4626
|
-
...settings,
|
|
4627
|
-
schemaVersion: settings.schemaVersion ?? SUPPORTED_SCHEMA_VERSION
|
|
4628
|
-
};
|
|
4629
|
-
await writePrivateFile(configuration.settingsFile, JSON.stringify(settingsWithVersion, null, 2));
|
|
4630
|
-
}
|
|
4631
|
-
async function updateSettings(updater) {
|
|
4632
|
-
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
4633
|
-
const MAX_LOCK_ATTEMPTS = 50;
|
|
4634
|
-
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
4635
|
-
const lockFile = configuration.settingsFile + ".lock";
|
|
4636
|
-
const tmpFile = configuration.settingsFile + ".tmp";
|
|
4637
|
-
let fileHandle;
|
|
4638
|
-
let attempts = 0;
|
|
4639
|
-
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
4640
|
-
try {
|
|
4641
|
-
fileHandle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, PRIVATE_FILE_MODE);
|
|
4642
|
-
break;
|
|
4643
|
-
} catch (err) {
|
|
4644
|
-
if (err.code === "EEXIST") {
|
|
4645
|
-
attempts++;
|
|
4646
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
4647
|
-
try {
|
|
4648
|
-
const stats = await stat$1(lockFile);
|
|
4649
|
-
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
4650
|
-
await unlink$1(lockFile).catch(() => {
|
|
4651
|
-
});
|
|
4652
|
-
}
|
|
4653
|
-
} catch {
|
|
4654
|
-
}
|
|
4655
|
-
} else {
|
|
4656
|
-
throw err;
|
|
4657
|
-
}
|
|
4658
|
-
}
|
|
4659
|
-
}
|
|
4660
|
-
if (!fileHandle) {
|
|
4661
|
-
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
4662
|
-
}
|
|
4663
|
-
try {
|
|
4664
|
-
const outcome = await readSettingsOutcome();
|
|
4665
|
-
if (outcome.kind === "unreadable") {
|
|
4666
|
-
throw new Error(
|
|
4667
|
-
`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).`
|
|
4668
|
-
);
|
|
4669
|
-
}
|
|
4670
|
-
const current = outcome.kind === "ok" ? outcome.settings : { ...defaultSettings };
|
|
4671
|
-
const updated = await updater(current);
|
|
4672
|
-
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
4673
|
-
await writePrivateFile(tmpFile, JSON.stringify(updated, null, 2));
|
|
4674
|
-
await rename$1(tmpFile, configuration.settingsFile);
|
|
4675
|
-
await hardenPrivateFile(configuration.settingsFile);
|
|
4676
|
-
return updated;
|
|
4677
|
-
} finally {
|
|
4678
|
-
await fileHandle.close();
|
|
4679
|
-
await unlink$1(lockFile).catch(() => {
|
|
4680
|
-
});
|
|
4681
|
-
}
|
|
4682
|
-
}
|
|
4683
|
-
const credentialsSchema = z.object({
|
|
4684
|
-
token: z.string(),
|
|
4685
|
-
authServerUrl: z.string().optional(),
|
|
4686
|
-
secret: z.string().base64().nullish(),
|
|
4687
|
-
// Legacy
|
|
4688
|
-
encryption: z.object({
|
|
4689
|
-
publicKey: z.string().base64(),
|
|
4690
|
-
machineKey: z.string().base64()
|
|
4691
|
-
}).nullish()
|
|
4692
|
-
});
|
|
4693
|
-
async function readCredentials() {
|
|
4694
|
-
if (!existsSync(configuration.privateKeyFile)) {
|
|
4695
|
-
return null;
|
|
4696
|
-
}
|
|
4697
|
-
try {
|
|
4698
|
-
const keyBase64 = await readFile$1(configuration.privateKeyFile, "utf8");
|
|
4699
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
4700
|
-
if (credentials.secret) {
|
|
4701
|
-
return {
|
|
4702
|
-
token: credentials.token,
|
|
4703
|
-
authServerUrl: credentials.authServerUrl,
|
|
4704
|
-
encryption: {
|
|
4705
|
-
type: "legacy",
|
|
4706
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64"))
|
|
4707
|
-
}
|
|
4708
|
-
};
|
|
4709
|
-
} else if (credentials.encryption) {
|
|
4710
|
-
return {
|
|
4711
|
-
token: credentials.token,
|
|
4712
|
-
authServerUrl: credentials.authServerUrl,
|
|
4713
|
-
encryption: {
|
|
4714
|
-
type: "dataKey",
|
|
4715
|
-
publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
|
|
4716
|
-
machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
|
|
4717
|
-
}
|
|
4718
|
-
};
|
|
4719
|
-
}
|
|
4720
|
-
} catch {
|
|
4721
|
-
return null;
|
|
4722
|
-
}
|
|
4723
|
-
return null;
|
|
4724
|
-
}
|
|
4725
|
-
async function readCredentialsForConfiguredRelay() {
|
|
4726
|
-
const credentials = await readCredentials();
|
|
4727
|
-
if (!credentials) return null;
|
|
4728
|
-
const relayProblem = credentialRelayProblem(credentials.authServerUrl, configuration.serverUrl);
|
|
4729
|
-
if (relayProblem) {
|
|
4730
|
-
throw new Error(
|
|
4731
|
-
`${relayProblem} Use a separate HAPPY_HOME_DIR for this relay, or run \`very-happy auth login --force\` before making relay requests.`
|
|
4732
|
-
);
|
|
4733
|
-
}
|
|
4734
|
-
return credentials;
|
|
4735
|
-
}
|
|
4736
|
-
async function writeCredentialsLegacy(credentials) {
|
|
4737
|
-
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
4738
|
-
await writePrivateFile(configuration.privateKeyFile, JSON.stringify({
|
|
4739
|
-
secret: encodeBase64(credentials.secret),
|
|
4740
|
-
token: credentials.token,
|
|
4741
|
-
authServerUrl: configuration.serverUrl
|
|
4742
|
-
}, null, 2));
|
|
4743
|
-
}
|
|
4744
|
-
async function writeCredentialsDataKey(credentials) {
|
|
4745
|
-
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
4746
|
-
await writePrivateFile(configuration.privateKeyFile, JSON.stringify({
|
|
4747
|
-
encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
|
|
4748
|
-
token: credentials.token,
|
|
4749
|
-
authServerUrl: configuration.serverUrl
|
|
4750
|
-
}, null, 2));
|
|
4751
|
-
}
|
|
4752
|
-
async function clearCredentials() {
|
|
4753
|
-
if (existsSync(configuration.privateKeyFile)) {
|
|
4754
|
-
await unlink$1(configuration.privateKeyFile);
|
|
4755
|
-
}
|
|
4756
|
-
}
|
|
4757
|
-
async function clearMachineId() {
|
|
4758
|
-
await updateSettings((settings) => ({
|
|
4759
|
-
...settings,
|
|
4760
|
-
machineId: void 0
|
|
4761
|
-
}));
|
|
4762
|
-
}
|
|
4763
|
-
async function readDaemonState() {
|
|
4764
|
-
try {
|
|
4765
|
-
if (!existsSync(configuration.daemonStateFile)) {
|
|
4766
|
-
return null;
|
|
4767
|
-
}
|
|
4768
|
-
const content = await readFile$1(configuration.daemonStateFile, "utf-8");
|
|
4769
|
-
return JSON.parse(content);
|
|
4770
|
-
} catch (error) {
|
|
4771
|
-
console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
|
|
4772
|
-
return null;
|
|
4773
|
-
}
|
|
4774
|
-
}
|
|
4775
|
-
function writeDaemonState(state) {
|
|
4776
|
-
writePrivateFileSync(configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
4777
|
-
}
|
|
4778
|
-
async function clearDaemonState() {
|
|
4779
|
-
if (existsSync(configuration.daemonStateFile)) {
|
|
4780
|
-
await unlink$1(configuration.daemonStateFile);
|
|
4781
|
-
}
|
|
4782
|
-
if (existsSync(configuration.daemonLockFile)) {
|
|
4783
|
-
try {
|
|
4784
|
-
await unlink$1(configuration.daemonLockFile);
|
|
4785
|
-
} catch {
|
|
4786
|
-
}
|
|
4787
|
-
}
|
|
4788
|
-
}
|
|
4789
|
-
async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
|
|
4790
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4791
|
-
try {
|
|
4792
|
-
const fileHandle = await open(
|
|
4793
|
-
configuration.daemonLockFile,
|
|
4794
|
-
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
|
|
4795
|
-
PRIVATE_FILE_MODE
|
|
4796
|
-
);
|
|
4797
|
-
await fileHandle.writeFile(String(process.pid));
|
|
4798
|
-
return fileHandle;
|
|
4799
|
-
} catch (error) {
|
|
4800
|
-
if (error.code === "EEXIST") {
|
|
4801
|
-
try {
|
|
4802
|
-
const lockPid = readFileSync(configuration.daemonLockFile, "utf-8").trim();
|
|
4803
|
-
if (lockPid && !isNaN(Number(lockPid))) {
|
|
4804
|
-
try {
|
|
4805
|
-
process.kill(Number(lockPid), 0);
|
|
4806
|
-
} catch {
|
|
4807
|
-
unlinkSync(configuration.daemonLockFile);
|
|
4808
|
-
continue;
|
|
4809
|
-
}
|
|
4810
|
-
}
|
|
4811
|
-
} catch {
|
|
4812
|
-
}
|
|
4813
|
-
}
|
|
4814
|
-
if (attempt === maxAttempts) {
|
|
4815
|
-
return null;
|
|
4816
|
-
}
|
|
4817
|
-
const delayMs = attempt * delayIncrementMs;
|
|
4818
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4819
|
-
}
|
|
4820
|
-
}
|
|
4821
|
-
return null;
|
|
4822
|
-
}
|
|
4823
|
-
async function releaseDaemonLock(lockHandle) {
|
|
4824
|
-
try {
|
|
4825
|
-
await lockHandle.close();
|
|
4826
|
-
} catch {
|
|
4827
|
-
}
|
|
4828
|
-
try {
|
|
4829
|
-
if (existsSync(configuration.daemonLockFile)) {
|
|
4830
|
-
unlinkSync(configuration.daemonLockFile);
|
|
4831
|
-
}
|
|
4832
|
-
} catch {
|
|
4833
|
-
}
|
|
4834
|
-
}
|
|
4835
|
-
const SESSION_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
4836
|
-
function readPersistedSessions() {
|
|
4837
|
-
try {
|
|
4838
|
-
if (!existsSync(configuration.sessionsFile)) return {};
|
|
4839
|
-
const data = JSON.parse(readFileSync(configuration.sessionsFile, "utf-8"));
|
|
4840
|
-
if (!data?.sessions || typeof data.sessions !== "object") return {};
|
|
4841
|
-
const now = Date.now();
|
|
4842
|
-
const sessions = {};
|
|
4843
|
-
for (const [id, session] of Object.entries(data.sessions)) {
|
|
4844
|
-
if (now - session.savedAt < SESSION_MAX_AGE_MS) {
|
|
4845
|
-
sessions[id] = session;
|
|
4846
|
-
}
|
|
4847
|
-
}
|
|
4848
|
-
return sessions;
|
|
4849
|
-
} catch {
|
|
4850
|
-
return {};
|
|
4851
|
-
}
|
|
4852
|
-
}
|
|
4853
|
-
function readTrackedClaudeSessionIds() {
|
|
4854
|
-
try {
|
|
4855
|
-
if (!existsSync(configuration.sessionsFile)) return [];
|
|
4856
|
-
const data = JSON.parse(readFileSync(configuration.sessionsFile, "utf-8"));
|
|
4857
|
-
if (!data?.sessions || typeof data.sessions !== "object") return [];
|
|
4858
|
-
const ids = /* @__PURE__ */ new Set();
|
|
4859
|
-
for (const session of Object.values(data.sessions)) {
|
|
4860
|
-
const metadata = (session ?? {}).metadata;
|
|
4861
|
-
for (const id of [metadata?.claudeSessionId, metadata?.importedFromClaudeSessionId]) {
|
|
4862
|
-
if (typeof id === "string" && id.length > 0) ids.add(id.toLowerCase());
|
|
4863
|
-
}
|
|
4864
|
-
}
|
|
4865
|
-
return [...ids];
|
|
4866
|
-
} catch {
|
|
4867
|
-
return [];
|
|
4868
|
-
}
|
|
4869
|
-
}
|
|
4870
|
-
function persistSession(sessionId, session) {
|
|
4871
|
-
try {
|
|
4872
|
-
const existing = readPersistedSessions();
|
|
4873
|
-
existing[sessionId] = session;
|
|
4874
|
-
const tmpFile = configuration.sessionsFile + ".tmp";
|
|
4875
|
-
writePrivateFileSync(tmpFile, JSON.stringify({ sessions: existing }, null, 2));
|
|
4876
|
-
renameSync(tmpFile, configuration.sessionsFile);
|
|
4877
|
-
hardenPrivateFileSync(configuration.sessionsFile);
|
|
4878
|
-
} catch (error) {
|
|
4879
|
-
logger.debug(`[PERSISTENCE] Failed to persist session ${sessionId}:`, error);
|
|
4880
|
-
}
|
|
4881
|
-
}
|
|
4882
|
-
function deletePersistedSessions(sessionIds) {
|
|
4883
|
-
try {
|
|
4884
|
-
const existing = readPersistedSessions();
|
|
4885
|
-
let changed = false;
|
|
4886
|
-
for (const id of sessionIds) {
|
|
4887
|
-
if (id in existing) {
|
|
4888
|
-
delete existing[id];
|
|
4889
|
-
changed = true;
|
|
4890
|
-
}
|
|
4891
|
-
}
|
|
4892
|
-
if (!changed) return;
|
|
4893
|
-
const tmpFile = configuration.sessionsFile + ".tmp";
|
|
4894
|
-
writePrivateFileSync(tmpFile, JSON.stringify({ sessions: existing }, null, 2));
|
|
4895
|
-
renameSync(tmpFile, configuration.sessionsFile);
|
|
4896
|
-
hardenPrivateFileSync(configuration.sessionsFile);
|
|
4897
|
-
} catch (error) {
|
|
4898
|
-
logger.debug(`[PERSISTENCE] Failed to delete persisted sessions:`, error);
|
|
4899
|
-
}
|
|
4900
|
-
}
|
|
4901
|
-
|
|
4902
|
-
var persistence = /*#__PURE__*/Object.freeze({
|
|
4903
|
-
__proto__: null,
|
|
4904
|
-
SUPPORTED_SCHEMA_VERSION: SUPPORTED_SCHEMA_VERSION,
|
|
4905
|
-
SandboxConfigSchema: SandboxConfigSchema,
|
|
4906
|
-
acquireDaemonLock: acquireDaemonLock,
|
|
4907
|
-
clearCredentials: clearCredentials,
|
|
4908
|
-
clearDaemonState: clearDaemonState,
|
|
4909
|
-
clearMachineId: clearMachineId,
|
|
4910
|
-
deletePersistedSessions: deletePersistedSessions,
|
|
4911
|
-
persistSession: persistSession,
|
|
4912
|
-
readCredentials: readCredentials,
|
|
4913
|
-
readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay,
|
|
4914
|
-
readDaemonState: readDaemonState,
|
|
4915
|
-
readPersistedSessions: readPersistedSessions,
|
|
4916
|
-
readSettings: readSettings,
|
|
4917
|
-
readSettingsOutcome: readSettingsOutcome,
|
|
4918
|
-
readTrackedClaudeSessionIds: readTrackedClaudeSessionIds,
|
|
4919
|
-
releaseDaemonLock: releaseDaemonLock,
|
|
4920
|
-
updateSettings: updateSettings,
|
|
4921
|
-
writeCredentialsDataKey: writeCredentialsDataKey,
|
|
4922
|
-
writeCredentialsLegacy: writeCredentialsLegacy,
|
|
4923
|
-
writeDaemonState: writeDaemonState,
|
|
4924
|
-
writeSettings: writeSettings
|
|
4925
|
-
});
|
|
4926
|
-
|
|
4927
3855
|
const TODO_LIST_MAX_ITEMS = 500;
|
|
4928
3856
|
const TODO_TITLE_MAX_CHARS = 500;
|
|
4929
3857
|
const FIELD_MAX_CHARS = 200;
|
|
@@ -5282,14 +4210,14 @@ function scrubTmuxClientEnv(env) {
|
|
|
5282
4210
|
}
|
|
5283
4211
|
|
|
5284
4212
|
function getClaudeConfigDir() {
|
|
5285
|
-
return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
4213
|
+
return process.env.CLAUDE_CONFIG_DIR || join$1(homedir(), ".claude");
|
|
5286
4214
|
}
|
|
5287
4215
|
function getClaudeProjectsRoot() {
|
|
5288
|
-
return join(getClaudeConfigDir(), "projects");
|
|
4216
|
+
return join$1(getClaudeConfigDir(), "projects");
|
|
5289
4217
|
}
|
|
5290
4218
|
function getProjectPath(workingDirectory) {
|
|
5291
4219
|
const projectId = resolve$1(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
5292
|
-
return join(getClaudeProjectsRoot(), projectId);
|
|
4220
|
+
return join$1(getClaudeProjectsRoot(), projectId);
|
|
5293
4221
|
}
|
|
5294
4222
|
|
|
5295
4223
|
var xtermHeadless = {};
|
|
@@ -10521,7 +9449,7 @@ function pruneTombstones(map, now, ttlMs = TOMBSTONE_TTL_MS) {
|
|
|
10521
9449
|
return out;
|
|
10522
9450
|
}
|
|
10523
9451
|
function tombstoneFile() {
|
|
10524
|
-
return join(configuration.happyHomeDir, "terminal-tombstones.json");
|
|
9452
|
+
return join$1(configuration.happyHomeDir, "terminal-tombstones.json");
|
|
10525
9453
|
}
|
|
10526
9454
|
function loadTombstones() {
|
|
10527
9455
|
try {
|
|
@@ -10540,7 +9468,7 @@ function saveTombstones(map) {
|
|
|
10540
9468
|
}
|
|
10541
9469
|
}
|
|
10542
9470
|
function closedTerminalsFile() {
|
|
10543
|
-
return join(configuration.happyHomeDir, "closed-terminals.json");
|
|
9471
|
+
return join$1(configuration.happyHomeDir, "closed-terminals.json");
|
|
10544
9472
|
}
|
|
10545
9473
|
function loadClosedTerminals() {
|
|
10546
9474
|
try {
|
|
@@ -10558,7 +9486,7 @@ function saveClosedTerminals(list) {
|
|
|
10558
9486
|
}
|
|
10559
9487
|
}
|
|
10560
9488
|
function liveTerminalsFile() {
|
|
10561
|
-
return join(configuration.happyHomeDir, "live-terminals.json");
|
|
9489
|
+
return join$1(configuration.happyHomeDir, "live-terminals.json");
|
|
10562
9490
|
}
|
|
10563
9491
|
function loadLiveSnapshot() {
|
|
10564
9492
|
try {
|
|
@@ -10833,7 +9761,7 @@ function tmuxSupportsEnvFlag() {
|
|
|
10833
9761
|
}
|
|
10834
9762
|
const PASTE_FILE_TTL_MS = 5e3;
|
|
10835
9763
|
function pasteSpoolDir() {
|
|
10836
|
-
const dir = join(configuration.happyHomeDir, "paste-spool");
|
|
9764
|
+
const dir = join$1(configuration.happyHomeDir, "paste-spool");
|
|
10837
9765
|
try {
|
|
10838
9766
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10839
9767
|
} catch {
|
|
@@ -11662,7 +10590,7 @@ class WebTerminalManager {
|
|
|
11662
10590
|
return false;
|
|
11663
10591
|
}
|
|
11664
10592
|
},
|
|
11665
|
-
conversationExists: (cwd, claudeSessionId) => existsSync(join(getProjectPath(cwd), `${claudeSessionId}.jsonl`)),
|
|
10593
|
+
conversationExists: (cwd, claudeSessionId) => existsSync(join$1(getProjectPath(cwd), `${claudeSessionId}.jsonl`)),
|
|
11666
10594
|
// B-273: only consulted for records that were attach terminals.
|
|
11667
10595
|
// Unbounded: the panel's 50-row cap must not hide a restore target.
|
|
11668
10596
|
userSessions: record.attachTmux ? this.listUserTmuxSessions(Infinity) : [],
|
|
@@ -12907,7 +11835,7 @@ function detectPosix() {
|
|
|
12907
11835
|
const codex = commandExists("codex");
|
|
12908
11836
|
const gemini = commandExists("gemini");
|
|
12909
11837
|
const openclawCommand = commandExists("openclaw");
|
|
12910
|
-
const openclawConfig = existsSync$1(join
|
|
11838
|
+
const openclawConfig = existsSync$1(join(os.homedir(), ".openclaw", "openclaw.json"));
|
|
12911
11839
|
const openclawEnv = !!process.env.OPENCLAW_GATEWAY_URL;
|
|
12912
11840
|
const openclaw = openclawCommand || openclawConfig || openclawEnv;
|
|
12913
11841
|
const pi = commandExists("pi") && commandExists("pi-acp");
|
|
@@ -12919,7 +11847,7 @@ function detectWindows() {
|
|
|
12919
11847
|
const codex = checkCommand("codex");
|
|
12920
11848
|
const gemini = checkCommand("gemini");
|
|
12921
11849
|
const openclawCommand = checkCommand("openclaw");
|
|
12922
|
-
const openclawConfig = existsSync$1(join
|
|
11850
|
+
const openclawConfig = existsSync$1(join(process.env.USERPROFILE || os.homedir(), ".openclaw", "openclaw.json"));
|
|
12923
11851
|
const openclawEnv = !!process.env.OPENCLAW_GATEWAY_URL;
|
|
12924
11852
|
const openclaw = openclawCommand || openclawConfig || openclawEnv;
|
|
12925
11853
|
const pi = checkCommand("pi") && checkCommand("pi-acp");
|
|
@@ -12954,7 +11882,7 @@ function deriveKey(master, usage, path) {
|
|
|
12954
11882
|
}
|
|
12955
11883
|
function deriveContentKeyPair(secret) {
|
|
12956
11884
|
const seed = deriveKey(secret, "Happy EnCoder", ["content"]);
|
|
12957
|
-
const hashedSeed = new Uint8Array(createHash("sha512").update(seed).digest());
|
|
11885
|
+
const hashedSeed = new Uint8Array(createHash$1("sha512").update(seed).digest());
|
|
12958
11886
|
const secretKey = hashedSeed.slice(0, 32);
|
|
12959
11887
|
const keyPair = tweetnacl.box.keyPair.fromSecretKey(secretKey);
|
|
12960
11888
|
return {
|
|
@@ -12963,7 +11891,7 @@ function deriveContentKeyPair(secret) {
|
|
|
12963
11891
|
};
|
|
12964
11892
|
}
|
|
12965
11893
|
function getLocalHappyAgentCredentialPath(happyHomeDir = configuration.happyHomeDir) {
|
|
12966
|
-
return join(happyHomeDir, "agent.key");
|
|
11894
|
+
return join$1(happyHomeDir, "agent.key");
|
|
12967
11895
|
}
|
|
12968
11896
|
function readLocalHappyAgentCredentials(happyHomeDir = configuration.happyHomeDir, serverUrl = configuration.serverUrl) {
|
|
12969
11897
|
const credentialPath = getLocalHappyAgentCredentialPath(happyHomeDir);
|
|
@@ -13021,7 +11949,7 @@ async function listClaudeProjectDirs(projectsRoot) {
|
|
|
13021
11949
|
}
|
|
13022
11950
|
const dirs = [];
|
|
13023
11951
|
for (const name of names) {
|
|
13024
|
-
const full = join(projectsRoot, name);
|
|
11952
|
+
const full = join$1(projectsRoot, name);
|
|
13025
11953
|
try {
|
|
13026
11954
|
if ((await stat$1(full)).isDirectory()) dirs.push(full);
|
|
13027
11955
|
} catch {
|
|
@@ -13042,7 +11970,7 @@ async function collectCandidates(projectDirs, exclude) {
|
|
|
13042
11970
|
if (!name.endsWith(".jsonl")) continue;
|
|
13043
11971
|
const id = name.slice(0, -".jsonl".length);
|
|
13044
11972
|
if (!UUID_RE$1.test(id) || exclude.has(id.toLowerCase())) continue;
|
|
13045
|
-
const path = join(dir, name);
|
|
11973
|
+
const path = join$1(dir, name);
|
|
13046
11974
|
try {
|
|
13047
11975
|
const s = await stat$1(path);
|
|
13048
11976
|
if (!s.isFile() || s.size === 0) continue;
|
|
@@ -13191,7 +12119,7 @@ class ForkSourceMissingError extends Error {
|
|
|
13191
12119
|
}
|
|
13192
12120
|
}
|
|
13193
12121
|
function jsonlPath(projectDir, sessionId) {
|
|
13194
|
-
return join(projectDir, `${sessionId}.jsonl`);
|
|
12122
|
+
return join$1(projectDir, `${sessionId}.jsonl`);
|
|
13195
12123
|
}
|
|
13196
12124
|
function isUserPrompt(parsed) {
|
|
13197
12125
|
if (!parsed || parsed.type !== "user") return false;
|
|
@@ -13199,6 +12127,18 @@ function isUserPrompt(parsed) {
|
|
|
13199
12127
|
const content = parsed.message?.content;
|
|
13200
12128
|
return typeof content === "string";
|
|
13201
12129
|
}
|
|
12130
|
+
function hasPromptAttachments(parsed) {
|
|
12131
|
+
const content = parsed.message?.content;
|
|
12132
|
+
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>"));
|
|
12133
|
+
}
|
|
12134
|
+
function userPromptText(parsed) {
|
|
12135
|
+
if (parsed?.type !== "user" || parsed.isSidechain || parsed.isMeta || parsed.isSynthetic) return null;
|
|
12136
|
+
const content = parsed.message?.content;
|
|
12137
|
+
if (typeof content === "string") return content.trim() ? content : null;
|
|
12138
|
+
if (!Array.isArray(content) || content.some((block) => block?.type === "tool_result")) return null;
|
|
12139
|
+
const text = content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
12140
|
+
return text.trim() ? text : null;
|
|
12141
|
+
}
|
|
13202
12142
|
async function discardForkedSession(projectDir, claudeSessionId) {
|
|
13203
12143
|
try {
|
|
13204
12144
|
await unlink$1(jsonlPath(projectDir, claudeSessionId));
|
|
@@ -13298,23 +12238,68 @@ async function listClaudeRewindPoints(projectDir, claudeSessionId) {
|
|
|
13298
12238
|
} catch {
|
|
13299
12239
|
continue;
|
|
13300
12240
|
}
|
|
13301
|
-
|
|
13302
|
-
if (
|
|
12241
|
+
const content = userPromptText(parsed);
|
|
12242
|
+
if (content === null) continue;
|
|
13303
12243
|
if (typeof parsed.uuid !== "string" || parsed.uuid.length === 0) continue;
|
|
13304
|
-
const content = parsed.message?.content;
|
|
13305
|
-
if (typeof content !== "string") continue;
|
|
13306
|
-
const trimmed = content.trim();
|
|
13307
|
-
if (trimmed.length === 0) continue;
|
|
13308
12244
|
const timestampRaw = parsed.timestamp;
|
|
13309
12245
|
const timestamp = typeof timestampRaw === "string" ? Date.parse(timestampRaw) : typeof timestampRaw === "number" ? timestampRaw : Date.now();
|
|
13310
12246
|
points.push({
|
|
13311
12247
|
uuid: parsed.uuid,
|
|
13312
12248
|
text: content,
|
|
12249
|
+
...hasPromptAttachments(parsed) ? { hasAttachments: true } : {},
|
|
13313
12250
|
timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
|
|
13314
12251
|
});
|
|
13315
12252
|
}
|
|
13316
12253
|
return points;
|
|
13317
12254
|
}
|
|
12255
|
+
async function forkBeforeUserMessage(projectDir, sourceClaudeSessionId, cutBeforeUuid) {
|
|
12256
|
+
const src = jsonlPath(projectDir, sourceClaudeSessionId);
|
|
12257
|
+
let raw;
|
|
12258
|
+
try {
|
|
12259
|
+
raw = await readFile$1(src, "utf-8");
|
|
12260
|
+
} catch (error) {
|
|
12261
|
+
if (error.code === "ENOENT") throw new ForkSourceMissingError(src);
|
|
12262
|
+
throw error;
|
|
12263
|
+
}
|
|
12264
|
+
const lines = raw.split("\n");
|
|
12265
|
+
let hasPreviousConversation = false;
|
|
12266
|
+
let hasPreviousPrompt = false;
|
|
12267
|
+
let cut = -1;
|
|
12268
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12269
|
+
let entry;
|
|
12270
|
+
try {
|
|
12271
|
+
entry = JSON.parse(lines[i]);
|
|
12272
|
+
} catch {
|
|
12273
|
+
continue;
|
|
12274
|
+
}
|
|
12275
|
+
if (userPromptText(entry) !== null && entry.uuid === cutBeforeUuid) {
|
|
12276
|
+
if (hasPromptAttachments(entry)) throw new Error("Cannot edit and rerun a message with attachments");
|
|
12277
|
+
cut = i;
|
|
12278
|
+
break;
|
|
12279
|
+
}
|
|
12280
|
+
if (!entry?.isSidechain && !entry?.isMeta && !entry?.isSynthetic && (entry?.type === "user" || entry?.type === "assistant")) {
|
|
12281
|
+
hasPreviousConversation = true;
|
|
12282
|
+
const content = entry.message?.content;
|
|
12283
|
+
if (entry.type === "user" && (typeof content === "string" || Array.isArray(content) && content.length > 0 && !content.some((block) => block?.type === "tool_result"))) {
|
|
12284
|
+
hasPreviousPrompt = true;
|
|
12285
|
+
}
|
|
12286
|
+
}
|
|
12287
|
+
}
|
|
12288
|
+
if (cut < 0) throw new ForkTruncateUuidNotFoundError(cutBeforeUuid, src);
|
|
12289
|
+
if (!hasPreviousConversation) return null;
|
|
12290
|
+
if (!hasPreviousPrompt) throw new Error("Cannot safely rewind: earlier history has no user prompt");
|
|
12291
|
+
const newId = randomUUID$1();
|
|
12292
|
+
const destination = jsonlPath(projectDir, newId);
|
|
12293
|
+
const temporary = `${destination}.tmp-${process.pid}`;
|
|
12294
|
+
try {
|
|
12295
|
+
await writeFile$1(temporary, lines.slice(0, cut).join("\n") + "\n", { encoding: "utf-8", flag: "wx" });
|
|
12296
|
+
await rename$1(temporary, destination);
|
|
12297
|
+
} catch (error) {
|
|
12298
|
+
await unlink$1(temporary).catch(() => void 0);
|
|
12299
|
+
throw error;
|
|
12300
|
+
}
|
|
12301
|
+
return newId;
|
|
12302
|
+
}
|
|
13318
12303
|
|
|
13319
12304
|
function serializedByteLength(value) {
|
|
13320
12305
|
try {
|
|
@@ -14591,6 +13576,14 @@ class CodexForkRewindPointNotFoundError extends Error {
|
|
|
14591
13576
|
this.name = "CodexForkRewindPointNotFoundError";
|
|
14592
13577
|
}
|
|
14593
13578
|
}
|
|
13579
|
+
function hasUserAttachments(item) {
|
|
13580
|
+
const content = item.content;
|
|
13581
|
+
return Array.isArray(content) && content.some((part) => !part || typeof part !== "object" || part.type !== "text" || typeof part.text === "string" && part.text.includes("<attached_files>"));
|
|
13582
|
+
}
|
|
13583
|
+
function rejectAttachedTarget(thread, itemId) {
|
|
13584
|
+
const item = thread.turns?.flatMap((turn) => turn.items ?? []).find((item2) => item2.id === itemId);
|
|
13585
|
+
if (item && hasUserAttachments(item)) throw new Error("Cannot edit and rerun a message with attachments");
|
|
13586
|
+
}
|
|
14594
13587
|
function textFromUserItem(item) {
|
|
14595
13588
|
if (item.type !== "userMessage") {
|
|
14596
13589
|
return null;
|
|
@@ -14617,6 +13610,7 @@ function listCodexRewindPoints(thread) {
|
|
|
14617
13610
|
points.push({
|
|
14618
13611
|
itemId: item.id,
|
|
14619
13612
|
text,
|
|
13613
|
+
...hasUserAttachments(item) ? { hasAttachments: true } : {},
|
|
14620
13614
|
timestamp: timestampFromTurn(turn)
|
|
14621
13615
|
});
|
|
14622
13616
|
}
|
|
@@ -14674,6 +13668,37 @@ async function forkCodexThread(client, opts) {
|
|
|
14674
13668
|
newCodexThreadId: forked.threadId
|
|
14675
13669
|
};
|
|
14676
13670
|
}
|
|
13671
|
+
async function forkCodexBeforeUserMessage(client, opts) {
|
|
13672
|
+
const { thread: source } = await client.readThread({ threadId: opts.threadId, includeTurns: true });
|
|
13673
|
+
rejectAttachedTarget(source, opts.cutBeforeItemId);
|
|
13674
|
+
const sourceCut = findCutTurn(source, opts.cutBeforeItemId);
|
|
13675
|
+
if (!sourceCut) throw new CodexForkRewindPointNotFoundError(opts.cutBeforeItemId, opts.threadId);
|
|
13676
|
+
if ((source.turns?.[sourceCut.index].items ?? []).findIndex((item) => item.id === opts.cutBeforeItemId) !== 0) {
|
|
13677
|
+
throw new Error("Cannot safely rewind a user message inside an existing turn");
|
|
13678
|
+
}
|
|
13679
|
+
if (sourceCut.index === 0) return { type: "success", startFresh: true };
|
|
13680
|
+
const forked = await client.forkThread({ threadId: opts.threadId, cwd: opts.cwd });
|
|
13681
|
+
if (forked.threadId === opts.threadId) throw new Error("Codex did not create an independent fork");
|
|
13682
|
+
rejectAttachedTarget(forked.thread, opts.cutBeforeItemId);
|
|
13683
|
+
const cut = findCutTurn(forked.thread, opts.cutBeforeItemId);
|
|
13684
|
+
if (!cut) throw new CodexForkRewindPointNotFoundError(opts.cutBeforeItemId, opts.threadId);
|
|
13685
|
+
const turns = forked.thread.turns ?? [];
|
|
13686
|
+
const items = turns[cut.index].items ?? [];
|
|
13687
|
+
if (items.findIndex((item) => item.id === opts.cutBeforeItemId) !== 0) {
|
|
13688
|
+
throw new Error("Cannot safely rewind a user message inside an existing turn");
|
|
13689
|
+
}
|
|
13690
|
+
if (cut.index === 0) return { type: "success", startFresh: true };
|
|
13691
|
+
const expectedTurnIds = turns.slice(0, cut.index).map((turn) => turn.id);
|
|
13692
|
+
const { thread } = await client.rollbackThread({
|
|
13693
|
+
threadId: forked.threadId,
|
|
13694
|
+
numTurns: turns.length - cut.index
|
|
13695
|
+
});
|
|
13696
|
+
const remaining = thread.turns ?? [];
|
|
13697
|
+
if (remaining.length !== cut.index || remaining.some((turn, index) => turn.id !== expectedTurnIds[index])) {
|
|
13698
|
+
throw new Error("Codex rollback did not preserve the expected history");
|
|
13699
|
+
}
|
|
13700
|
+
return { type: "success", newCodexThreadId: forked.threadId };
|
|
13701
|
+
}
|
|
14677
13702
|
|
|
14678
13703
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14679
13704
|
function requireNonEmptyString(value, name) {
|
|
@@ -15178,6 +14203,26 @@ class ApiMachineClient {
|
|
|
15178
14203
|
throw error;
|
|
15179
14204
|
}
|
|
15180
14205
|
});
|
|
14206
|
+
this.rpcHandlerManager.registerHandler("claude-rewind-before-message", async (params) => {
|
|
14207
|
+
const directory = requireNonEmptyString(params?.directory, "directory");
|
|
14208
|
+
const claudeSessionId = requireNonEmptyString(params?.claudeSessionId, "claudeSessionId");
|
|
14209
|
+
const cutBeforeUuid = requireNonEmptyString(params?.cutBeforeUuid, "cutBeforeUuid");
|
|
14210
|
+
if (!UUID_RE.test(claudeSessionId) || !UUID_RE.test(cutBeforeUuid)) {
|
|
14211
|
+
throw new Error("claudeSessionId and cutBeforeUuid must be valid UUIDs");
|
|
14212
|
+
}
|
|
14213
|
+
const newClaudeSessionId = await forkBeforeUserMessage(getProjectPath(directory), claudeSessionId, cutBeforeUuid);
|
|
14214
|
+
return newClaudeSessionId === null ? { type: "success", startFresh: true } : { type: "success", newClaudeSessionId };
|
|
14215
|
+
});
|
|
14216
|
+
this.rpcHandlerManager.registerHandler("codex-rewind-before-message", async (params) => {
|
|
14217
|
+
const directory = requireNonEmptyString(params?.directory, "directory");
|
|
14218
|
+
const codexThreadId = requireNonEmptyString(params?.codexThreadId, "codexThreadId");
|
|
14219
|
+
const cutBeforeItemId = requireNonEmptyString(params?.cutBeforeItemId, "cutBeforeItemId");
|
|
14220
|
+
return withCodexAppServerClient((client) => forkCodexBeforeUserMessage(client, {
|
|
14221
|
+
threadId: codexThreadId,
|
|
14222
|
+
cwd: directory,
|
|
14223
|
+
cutBeforeItemId
|
|
14224
|
+
}));
|
|
14225
|
+
});
|
|
15181
14226
|
this.rpcHandlerManager.registerHandler("stop-daemon", () => {
|
|
15182
14227
|
logger.debug("[API MACHINE] Received stop-daemon RPC request");
|
|
15183
14228
|
setTimeout(() => {
|
|
@@ -16820,4 +15865,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
|
|
|
16820
15865
|
}).passthrough()
|
|
16821
15866
|
]);
|
|
16822
15867
|
|
|
16823
|
-
export {
|
|
15868
|
+
export { ApiClient as A, MIRROR_BACKFILL_LINES_DEFAULT as B, mirrorLineKey as C, mirrorLocalId as D, detectCLIAvailability as E, detectResumeSupport as F, summarizeSpawnSessionForLog as G, shellescape as H, InvalidateSync as I, fetchCliUpdateState as J, resolveCliUpdateCheckInterval as K, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, listVhTerminals as N, isValidTerminalId as O, readVhTerminal as P, sendToVhTerminal as Q, RawJSONLinesSchema as R, STREAM_DELTA_MAX_CHARS as S, TITLE_PROMPT_PREFIX as T, BOARD_ANALYZER_PROMPT_PREFIX as U, startOfflineReconnection as V, CodexAppServerClient as W, api as X, ApiSessionClient as a, contentLogMetadata as b, createEnvelope as c, delay as d, errorLogMetadata as e, connectionState as f, getProjectPath as g, initializeSandbox as h, isValidSessionId as i, AsyncLock as j, commandOnPath as k, deriveLocalCliUpdateSummary as l, isClaudeSessionId as m, getLocalHappyAgentCredentialPath as n, parseExactVersion as o, projectPath as p, compareExactVersions as q, readLocalHappyAgentCredentials as r, streamKeyOf as s, extractCompleteLines as t, decideBackfill as u, parseTerminalHookPayload as v, wrapCommand as w, tmuxArgs as x, scrubTmuxClientEnv as y, decideMirrorBinding as z };
|