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
|
@@ -0,0 +1,1198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('node:fs/promises');
|
|
4
|
+
var node_fs = require('node:fs');
|
|
5
|
+
var os = require('node:os');
|
|
6
|
+
var path = require('node:path');
|
|
7
|
+
var z = require('zod');
|
|
8
|
+
var node_crypto = require('node:crypto');
|
|
9
|
+
var tweetnacl = require('tweetnacl');
|
|
10
|
+
var chalk = require('chalk');
|
|
11
|
+
var node_util = require('node:util');
|
|
12
|
+
|
|
13
|
+
function _interopNamespaceDefault(e) {
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
31
|
+
|
|
32
|
+
var name = "very-happy-cli";
|
|
33
|
+
var version = "0.2.125";
|
|
34
|
+
var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
|
|
35
|
+
var author = "Kirill Dubovitskiy";
|
|
36
|
+
var contributors = [
|
|
37
|
+
"Mereith (very-happy-cli fork)"
|
|
38
|
+
];
|
|
39
|
+
var license = "MIT";
|
|
40
|
+
var engines = {
|
|
41
|
+
node: "^20.19.0 || ^22.13.0 || >=24.0.0"
|
|
42
|
+
};
|
|
43
|
+
var type = "module";
|
|
44
|
+
var homepage = "https://github.com/Mereithhh/very-happy";
|
|
45
|
+
var bugs = "https://github.com/Mereithhh/very-happy/issues";
|
|
46
|
+
var repository = {
|
|
47
|
+
type: "git",
|
|
48
|
+
url: "git+https://github.com/Mereithhh/very-happy.git"
|
|
49
|
+
};
|
|
50
|
+
var bin = {
|
|
51
|
+
"very-happy": "./bin/very-happy.mjs",
|
|
52
|
+
"very-happy-mcp": "./bin/very-happy-mcp.mjs"
|
|
53
|
+
};
|
|
54
|
+
var main = "./dist/index.cjs";
|
|
55
|
+
var module$1 = "./dist/index.mjs";
|
|
56
|
+
var types = "./dist/index.d.cts";
|
|
57
|
+
var exports$1 = {
|
|
58
|
+
".": {
|
|
59
|
+
require: {
|
|
60
|
+
types: "./dist/index.d.cts",
|
|
61
|
+
"default": "./dist/index.cjs"
|
|
62
|
+
},
|
|
63
|
+
"import": {
|
|
64
|
+
types: "./dist/index.d.mts",
|
|
65
|
+
"default": "./dist/index.mjs"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"./lib": {
|
|
69
|
+
require: {
|
|
70
|
+
types: "./dist/lib.d.cts",
|
|
71
|
+
"default": "./dist/lib.cjs"
|
|
72
|
+
},
|
|
73
|
+
"import": {
|
|
74
|
+
types: "./dist/lib.d.mts",
|
|
75
|
+
"default": "./dist/lib.mjs"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"./codex/happyMcpStdioBridge": {
|
|
79
|
+
require: {
|
|
80
|
+
types: "./dist/codex/happyMcpStdioBridge.d.cts",
|
|
81
|
+
"default": "./dist/codex/happyMcpStdioBridge.cjs"
|
|
82
|
+
},
|
|
83
|
+
"import": {
|
|
84
|
+
types: "./dist/codex/happyMcpStdioBridge.d.mts",
|
|
85
|
+
"default": "./dist/codex/happyMcpStdioBridge.mjs"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var files = [
|
|
90
|
+
"dist",
|
|
91
|
+
"bin",
|
|
92
|
+
"scripts",
|
|
93
|
+
"tools/licenses",
|
|
94
|
+
"package.json",
|
|
95
|
+
"README.md",
|
|
96
|
+
"LICENSE"
|
|
97
|
+
];
|
|
98
|
+
var scripts = {
|
|
99
|
+
typecheck: "tsc --noEmit",
|
|
100
|
+
"bundle:server": "node scripts/bundle-server.cjs",
|
|
101
|
+
"bundle:server:all": "node scripts/bundle-server.cjs --all-platforms",
|
|
102
|
+
"bundle:webapp": "node scripts/bundle-webapp.cjs",
|
|
103
|
+
build: "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc --noEmit && pkgroll",
|
|
104
|
+
test: "pnpm run build && vitest run --project unit",
|
|
105
|
+
"test:integration": "pnpm run build && vitest run --project integration-empty --project integration-plan-mode --project integration-authenticated",
|
|
106
|
+
"cli:install": "node scripts/install-local.cjs",
|
|
107
|
+
prepublishOnly: "pnpm test",
|
|
108
|
+
postinstall: "node scripts/unpack-tools.cjs && node scripts/fix-pty-perms.cjs"
|
|
109
|
+
};
|
|
110
|
+
var dependencies = {
|
|
111
|
+
"@agentclientprotocol/sdk": "^0.14.1",
|
|
112
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.232",
|
|
113
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.37",
|
|
114
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
115
|
+
"@noble/ed25519": "^3.0.0",
|
|
116
|
+
"@noble/hashes": "^2.0.1",
|
|
117
|
+
"@paralleldrive/cuid2": "^2.2.2",
|
|
118
|
+
"@stablelib/base64": "^2.0.1",
|
|
119
|
+
"@stablelib/hex": "^2.0.1",
|
|
120
|
+
"@types/cross-spawn": "^6.0.6",
|
|
121
|
+
"@types/http-proxy": "^1.17.17",
|
|
122
|
+
"@types/react": "^19.2.7",
|
|
123
|
+
"@types/tmp": "^0.2.6",
|
|
124
|
+
axios: "^1.13.2",
|
|
125
|
+
chalk: "^5.6.2",
|
|
126
|
+
"cross-spawn": "^7.0.6",
|
|
127
|
+
"expo-server-sdk": "^3.15.0",
|
|
128
|
+
fastify: "5.8.5",
|
|
129
|
+
"fastify-type-provider-zod": "^6.1.0",
|
|
130
|
+
"http-proxy": "^1.18.1",
|
|
131
|
+
"http-proxy-middleware": "^3.0.7",
|
|
132
|
+
ink: "^6.5.1",
|
|
133
|
+
inquirer: "^13.2.2",
|
|
134
|
+
"node-pty": "^1.0.0",
|
|
135
|
+
open: "^10.2.0",
|
|
136
|
+
"ps-list": "^8.1.1",
|
|
137
|
+
react: "^19.2.0",
|
|
138
|
+
"socket.io-client": "^4.8.1",
|
|
139
|
+
tar: "^7.5.22",
|
|
140
|
+
tmp: "^0.2.7",
|
|
141
|
+
tweetnacl: "^1.0.3",
|
|
142
|
+
ws: "^8.21.3",
|
|
143
|
+
zod: "^4.0.0"
|
|
144
|
+
};
|
|
145
|
+
var optionalDependencies = {
|
|
146
|
+
"very-happy-tools-arm64-darwin": "0.2.125",
|
|
147
|
+
"very-happy-tools-x64-darwin": "0.2.125",
|
|
148
|
+
"very-happy-tools-arm64-linux": "0.2.125",
|
|
149
|
+
"very-happy-tools-x64-linux": "0.2.125",
|
|
150
|
+
"very-happy-tools-arm64-win32": "0.2.125",
|
|
151
|
+
"very-happy-tools-x64-win32": "0.2.125"
|
|
152
|
+
};
|
|
153
|
+
var devDependencies = {
|
|
154
|
+
"@slopus/happy-wire": "workspace:*",
|
|
155
|
+
"@xterm/addon-serialize": "0.13.0",
|
|
156
|
+
"@xterm/headless": "5.5.0",
|
|
157
|
+
"@types/inquirer": "^9.0.9",
|
|
158
|
+
"@types/node": ">=20",
|
|
159
|
+
"@types/ws": "^8.18.1",
|
|
160
|
+
"cross-env": "^10.1.0",
|
|
161
|
+
dotenv: "^16.6.1",
|
|
162
|
+
pkgroll: "^2.14.2",
|
|
163
|
+
"ts-node": "^10",
|
|
164
|
+
tsx: "^4.20.6",
|
|
165
|
+
typescript: "5.9.3",
|
|
166
|
+
vitest: "3.2.6"
|
|
167
|
+
};
|
|
168
|
+
var publishConfig = {
|
|
169
|
+
registry: "https://registry.npmjs.org"
|
|
170
|
+
};
|
|
171
|
+
var packageManager = "pnpm@10.11.0";
|
|
172
|
+
var packageJson = {
|
|
173
|
+
name: name,
|
|
174
|
+
version: version,
|
|
175
|
+
description: description,
|
|
176
|
+
author: author,
|
|
177
|
+
contributors: contributors,
|
|
178
|
+
license: license,
|
|
179
|
+
engines: engines,
|
|
180
|
+
type: type,
|
|
181
|
+
homepage: homepage,
|
|
182
|
+
bugs: bugs,
|
|
183
|
+
repository: repository,
|
|
184
|
+
bin: bin,
|
|
185
|
+
main: main,
|
|
186
|
+
module: module$1,
|
|
187
|
+
types: types,
|
|
188
|
+
exports: exports$1,
|
|
189
|
+
files: files,
|
|
190
|
+
scripts: scripts,
|
|
191
|
+
dependencies: dependencies,
|
|
192
|
+
optionalDependencies: optionalDependencies,
|
|
193
|
+
devDependencies: devDependencies,
|
|
194
|
+
publishConfig: publishConfig,
|
|
195
|
+
packageManager: packageManager
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const PRIVATE_DIRECTORY_MODE = 448;
|
|
199
|
+
const PRIVATE_FILE_MODE = 384;
|
|
200
|
+
function ignoreUnsupportedPermissions(error) {
|
|
201
|
+
if (process.platform !== "win32") throw error;
|
|
202
|
+
}
|
|
203
|
+
function ensurePrivateDirectorySync(path) {
|
|
204
|
+
node_fs.mkdirSync(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
205
|
+
try {
|
|
206
|
+
node_fs.chmodSync(path, PRIVATE_DIRECTORY_MODE);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
ignoreUnsupportedPermissions(error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function ensurePrivateDirectory(path) {
|
|
212
|
+
await promises.mkdir(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
213
|
+
try {
|
|
214
|
+
await promises.chmod(path, PRIVATE_DIRECTORY_MODE);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
ignoreUnsupportedPermissions(error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function hardenPrivateFileSync(path) {
|
|
220
|
+
try {
|
|
221
|
+
node_fs.chmodSync(path, PRIVATE_FILE_MODE);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
ignoreUnsupportedPermissions(error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function hardenPrivateDirectoryFilesSync(path$1) {
|
|
227
|
+
for (const name of node_fs.readdirSync(path$1)) {
|
|
228
|
+
const candidate = path.join(path$1, name);
|
|
229
|
+
try {
|
|
230
|
+
if (node_fs.statSync(candidate).isFile()) hardenPrivateFileSync(candidate);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function hardenPrivateFile(path) {
|
|
236
|
+
try {
|
|
237
|
+
await promises.chmod(path, PRIVATE_FILE_MODE);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
ignoreUnsupportedPermissions(error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function writePrivateFileSync(path, data) {
|
|
243
|
+
node_fs.writeFileSync(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
244
|
+
hardenPrivateFileSync(path);
|
|
245
|
+
}
|
|
246
|
+
async function writePrivateFile(path, data) {
|
|
247
|
+
await promises.writeFile(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
248
|
+
await hardenPrivateFile(path);
|
|
249
|
+
}
|
|
250
|
+
function appendPrivateFileSync(path, data) {
|
|
251
|
+
node_fs.appendFileSync(path, data, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
252
|
+
hardenPrivateFileSync(path);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const DEFAULT_RELAY_URL = "https://veryhappy.dev";
|
|
256
|
+
function normalizeHttpEndpoint(value, source) {
|
|
257
|
+
const trimmed = value.trim();
|
|
258
|
+
let parsed;
|
|
259
|
+
try {
|
|
260
|
+
parsed = new URL(trimmed);
|
|
261
|
+
} catch {
|
|
262
|
+
throw new Error(`${source} must be an absolute http(s) origin`);
|
|
263
|
+
}
|
|
264
|
+
const onlyOriginPath = /^\/+$/u.test(parsed.pathname);
|
|
265
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username.length > 0 || parsed.password.length > 0 || !onlyOriginPath || parsed.search.length > 0 || parsed.hash.length > 0) {
|
|
266
|
+
throw new Error(`${source} must be an http(s) origin without credentials, path, query, or fragment`);
|
|
267
|
+
}
|
|
268
|
+
return parsed.origin;
|
|
269
|
+
}
|
|
270
|
+
function resolveHttpEndpoint(environmentValue, settingsValue, environmentName, settingsName) {
|
|
271
|
+
if (environmentValue !== void 0) {
|
|
272
|
+
return normalizeHttpEndpoint(environmentValue, environmentName);
|
|
273
|
+
}
|
|
274
|
+
if (settingsValue !== void 0) {
|
|
275
|
+
return normalizeHttpEndpoint(settingsValue, `settings.${settingsName}`);
|
|
276
|
+
}
|
|
277
|
+
return DEFAULT_RELAY_URL;
|
|
278
|
+
}
|
|
279
|
+
class Configuration {
|
|
280
|
+
serverUrl;
|
|
281
|
+
webappUrl;
|
|
282
|
+
isDaemonProcess;
|
|
283
|
+
// Directories and paths (from persistence)
|
|
284
|
+
happyHomeDir;
|
|
285
|
+
logsDir;
|
|
286
|
+
settingsFile;
|
|
287
|
+
privateKeyFile;
|
|
288
|
+
daemonStateFile;
|
|
289
|
+
daemonLockFile;
|
|
290
|
+
sessionsFile;
|
|
291
|
+
currentCliVersion;
|
|
292
|
+
isExperimentalEnabled;
|
|
293
|
+
disableCaffeinate;
|
|
294
|
+
constructor() {
|
|
295
|
+
const args = process.argv.slice(2);
|
|
296
|
+
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
297
|
+
if (process.env.HAPPY_HOME_DIR) {
|
|
298
|
+
const expandedPath = process.env.HAPPY_HOME_DIR.replace(/^~/, os.homedir());
|
|
299
|
+
this.happyHomeDir = expandedPath;
|
|
300
|
+
} else {
|
|
301
|
+
this.happyHomeDir = path.join(os.homedir(), ".happy");
|
|
302
|
+
}
|
|
303
|
+
this.logsDir = path.join(this.happyHomeDir, "logs");
|
|
304
|
+
this.settingsFile = path.join(this.happyHomeDir, "settings.json");
|
|
305
|
+
this.privateKeyFile = path.join(this.happyHomeDir, "access.key");
|
|
306
|
+
this.daemonStateFile = path.join(this.happyHomeDir, "daemon.state.json");
|
|
307
|
+
this.daemonLockFile = path.join(this.happyHomeDir, "daemon.state.json.lock");
|
|
308
|
+
this.sessionsFile = path.join(this.happyHomeDir, "sessions.json");
|
|
309
|
+
try {
|
|
310
|
+
this.serverUrl = resolveHttpEndpoint(
|
|
311
|
+
process.env.HAPPY_SERVER_URL,
|
|
312
|
+
readSettingsStringSync(this.settingsFile, "serverUrl"),
|
|
313
|
+
"HAPPY_SERVER_URL",
|
|
314
|
+
"serverUrl"
|
|
315
|
+
);
|
|
316
|
+
this.webappUrl = resolveHttpEndpoint(
|
|
317
|
+
process.env.HAPPY_WEBAPP_URL,
|
|
318
|
+
readSettingsStringSync(this.settingsFile, "webappUrl"),
|
|
319
|
+
"HAPPY_WEBAPP_URL",
|
|
320
|
+
"webappUrl"
|
|
321
|
+
);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const message = error instanceof Error ? error.message : "invalid client endpoint";
|
|
324
|
+
process.stderr.write(`[very-happy] Configuration error: ${message}
|
|
325
|
+
`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
|
|
329
|
+
this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.HAPPY_DISABLE_CAFFEINATE?.toLowerCase() || "");
|
|
330
|
+
this.currentCliVersion = packageJson.version;
|
|
331
|
+
const variant = process.env.HAPPY_VARIANT || "stable";
|
|
332
|
+
if (!this.isDaemonProcess && variant === "dev") {
|
|
333
|
+
console.log("\x1B[33m\u{1F527} DEV MODE\x1B[0m - Data: " + this.happyHomeDir);
|
|
334
|
+
}
|
|
335
|
+
ensurePrivateDirectorySync(this.happyHomeDir);
|
|
336
|
+
ensurePrivateDirectorySync(this.logsDir);
|
|
337
|
+
for (const privateFile of [
|
|
338
|
+
this.settingsFile,
|
|
339
|
+
this.privateKeyFile,
|
|
340
|
+
this.daemonStateFile,
|
|
341
|
+
this.daemonLockFile,
|
|
342
|
+
this.sessionsFile
|
|
343
|
+
]) {
|
|
344
|
+
if (node_fs.existsSync(privateFile)) hardenPrivateFileSync(privateFile);
|
|
345
|
+
}
|
|
346
|
+
hardenPrivateDirectoryFilesSync(this.logsDir);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function readSettingsStringSync(settingsFile, key) {
|
|
350
|
+
try {
|
|
351
|
+
if (!node_fs.existsSync(settingsFile)) return void 0;
|
|
352
|
+
const raw = JSON.parse(node_fs.readFileSync(settingsFile, "utf8"));
|
|
353
|
+
const value = raw?.[key];
|
|
354
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
355
|
+
} catch {
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const configuration = new Configuration();
|
|
360
|
+
|
|
361
|
+
function encodeBase64(buffer, variant = "base64") {
|
|
362
|
+
if (variant === "base64url") {
|
|
363
|
+
return encodeBase64Url(buffer);
|
|
364
|
+
}
|
|
365
|
+
return Buffer.from(buffer).toString("base64");
|
|
366
|
+
}
|
|
367
|
+
function encodeBase64Url(buffer) {
|
|
368
|
+
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
369
|
+
}
|
|
370
|
+
function decodeBase64(base64, variant = "base64") {
|
|
371
|
+
if (variant === "base64url") {
|
|
372
|
+
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
|
|
373
|
+
return new Uint8Array(Buffer.from(base64Standard, "base64"));
|
|
374
|
+
}
|
|
375
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
376
|
+
}
|
|
377
|
+
function getRandomBytes(size) {
|
|
378
|
+
return new Uint8Array(node_crypto.randomBytes(size));
|
|
379
|
+
}
|
|
380
|
+
function libsodiumPublicKeyFromSecretKey(seed) {
|
|
381
|
+
const hashedSeed = new Uint8Array(node_crypto.createHash("sha512").update(seed).digest());
|
|
382
|
+
const secretKey = hashedSeed.slice(0, 32);
|
|
383
|
+
return new Uint8Array(tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey);
|
|
384
|
+
}
|
|
385
|
+
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
386
|
+
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
387
|
+
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
|
|
388
|
+
const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
|
|
389
|
+
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
|
|
390
|
+
result.set(ephemeralKeyPair.publicKey, 0);
|
|
391
|
+
result.set(nonce, ephemeralKeyPair.publicKey.length);
|
|
392
|
+
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
function encryptLegacy(data, secret) {
|
|
396
|
+
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
397
|
+
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
398
|
+
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
399
|
+
result.set(nonce);
|
|
400
|
+
result.set(encrypted, nonce.length);
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
function decryptLegacy(data, secret) {
|
|
404
|
+
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
405
|
+
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
406
|
+
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
407
|
+
if (!decrypted) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
411
|
+
}
|
|
412
|
+
function decryptBlob(bundle, key) {
|
|
413
|
+
if (bundle.length < tweetnacl.secretbox.nonceLength + 16) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const nonce = bundle.slice(0, tweetnacl.secretbox.nonceLength);
|
|
417
|
+
const ciphertext = bundle.slice(tweetnacl.secretbox.nonceLength);
|
|
418
|
+
const decrypted = tweetnacl.secretbox.open(ciphertext, nonce, key);
|
|
419
|
+
if (!decrypted) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
return new Uint8Array(decrypted);
|
|
423
|
+
}
|
|
424
|
+
function encryptWithDataKey(data, dataKey) {
|
|
425
|
+
const nonce = getRandomBytes(12);
|
|
426
|
+
const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
|
|
427
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
428
|
+
const encrypted = Buffer.concat([
|
|
429
|
+
cipher.update(plaintext),
|
|
430
|
+
cipher.final()
|
|
431
|
+
]);
|
|
432
|
+
const authTag = cipher.getAuthTag();
|
|
433
|
+
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
|
|
434
|
+
bundle.set([0], 0);
|
|
435
|
+
bundle.set(nonce, 1);
|
|
436
|
+
bundle.set(new Uint8Array(encrypted), 13);
|
|
437
|
+
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
|
|
438
|
+
return bundle;
|
|
439
|
+
}
|
|
440
|
+
function decryptWithDataKey(bundle, dataKey) {
|
|
441
|
+
if (bundle.length < 1) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
if (bundle[0] !== 0) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
if (bundle.length < 12 + 16 + 1) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
const nonce = bundle.slice(1, 13);
|
|
451
|
+
const authTag = bundle.slice(bundle.length - 16);
|
|
452
|
+
const ciphertext = bundle.slice(13, bundle.length - 16);
|
|
453
|
+
try {
|
|
454
|
+
const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
|
|
455
|
+
decipher.setAuthTag(authTag);
|
|
456
|
+
const decrypted = Buffer.concat([
|
|
457
|
+
decipher.update(ciphertext),
|
|
458
|
+
decipher.final()
|
|
459
|
+
]);
|
|
460
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function encrypt(key, variant, data) {
|
|
466
|
+
if (variant === "legacy") {
|
|
467
|
+
return encryptLegacy(data, key);
|
|
468
|
+
} else {
|
|
469
|
+
return encryptWithDataKey(data, key);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function decrypt(key, variant, data) {
|
|
473
|
+
if (variant === "legacy") {
|
|
474
|
+
return decryptLegacy(data, key);
|
|
475
|
+
} else {
|
|
476
|
+
return decryptWithDataKey(data, key);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function sensitiveKey(key) {
|
|
481
|
+
const normalized = key.toLowerCase().replace(/[_\-\s]/g, "");
|
|
482
|
+
return /(token|secret|password|authorization|cookie|credential|apikey|privatekey|encryptionkey|accesskey|claimsecret)/.test(normalized);
|
|
483
|
+
}
|
|
484
|
+
function redactLogString(value) {
|
|
485
|
+
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]");
|
|
486
|
+
}
|
|
487
|
+
function redactLogValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
488
|
+
if (typeof value === "string") return redactLogString(value);
|
|
489
|
+
if (value === null || typeof value !== "object") return value;
|
|
490
|
+
if (depth >= 6) return "[TRUNCATED]";
|
|
491
|
+
if (seen.has(value)) return "[CIRCULAR]";
|
|
492
|
+
seen.add(value);
|
|
493
|
+
if (value instanceof Error) {
|
|
494
|
+
const error = value;
|
|
495
|
+
return {
|
|
496
|
+
name: error.name,
|
|
497
|
+
message: redactLogString(error.message),
|
|
498
|
+
...typeof error.code === "string" || typeof error.code === "number" ? { code: error.code } : {},
|
|
499
|
+
...typeof (error.status ?? error.response?.status) === "number" ? { status: error.status ?? error.response?.status } : {}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (Array.isArray(value)) return value.map((item) => redactLogValue(item, depth + 1, seen));
|
|
503
|
+
const result = {};
|
|
504
|
+
for (const [key, child] of Object.entries(value)) {
|
|
505
|
+
if (sensitiveKey(key)) {
|
|
506
|
+
result[key] = "[REDACTED]";
|
|
507
|
+
} else if (key === "environmentVariables" && child && typeof child === "object") {
|
|
508
|
+
result[key] = { names: Object.keys(child).sort() };
|
|
509
|
+
} else {
|
|
510
|
+
result[key] = redactLogValue(child, depth + 1, seen);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
|
|
517
|
+
return date.toLocaleString("sv-SE", {
|
|
518
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
519
|
+
year: "numeric",
|
|
520
|
+
month: "2-digit",
|
|
521
|
+
day: "2-digit",
|
|
522
|
+
hour: "2-digit",
|
|
523
|
+
minute: "2-digit",
|
|
524
|
+
second: "2-digit"
|
|
525
|
+
}).replace(/[: ]/g, "-").replace(/,/g, "") + "-pid-" + process.pid;
|
|
526
|
+
}
|
|
527
|
+
function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
|
|
528
|
+
return date.toLocaleTimeString("en-US", {
|
|
529
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
530
|
+
hour12: false,
|
|
531
|
+
hour: "2-digit",
|
|
532
|
+
minute: "2-digit",
|
|
533
|
+
second: "2-digit",
|
|
534
|
+
fractionalSecondDigits: 3
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function getSessionLogPath() {
|
|
538
|
+
const timestamp = createTimestampForFilename();
|
|
539
|
+
const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
|
|
540
|
+
return path.join(configuration.logsDir, filename);
|
|
541
|
+
}
|
|
542
|
+
function safeRemoteLogUrl(raw) {
|
|
543
|
+
if (!raw) return void 0;
|
|
544
|
+
try {
|
|
545
|
+
const url = new URL(raw);
|
|
546
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
547
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return void 0;
|
|
548
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") return void 0;
|
|
549
|
+
return url.origin;
|
|
550
|
+
} catch {
|
|
551
|
+
return void 0;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
class Logger {
|
|
555
|
+
constructor(logFilePath = getSessionLogPath()) {
|
|
556
|
+
this.logFilePath = logFilePath;
|
|
557
|
+
const remoteUrl = safeRemoteLogUrl(process.env.HAPPY_SERVER_URL);
|
|
558
|
+
const remoteToken = process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING_TOKEN;
|
|
559
|
+
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && remoteUrl && remoteToken && Buffer.byteLength(remoteToken) >= 32) {
|
|
560
|
+
this.dangerouslyUnencryptedServerLoggingUrl = remoteUrl;
|
|
561
|
+
this.dangerouslyUnencryptedServerLoggingToken = remoteToken;
|
|
562
|
+
console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
dangerouslyUnencryptedServerLoggingUrl;
|
|
566
|
+
dangerouslyUnencryptedServerLoggingToken;
|
|
567
|
+
// Use local timezone for simplicity of locating the logs,
|
|
568
|
+
// in practice you will not need absolute timestamps
|
|
569
|
+
localTimezoneTimestamp() {
|
|
570
|
+
return createTimestampForLogEntry();
|
|
571
|
+
}
|
|
572
|
+
debug(message, ...args) {
|
|
573
|
+
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
|
|
574
|
+
}
|
|
575
|
+
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
|
|
576
|
+
if (!process.env.DEBUG) {
|
|
577
|
+
this.debug(`In production, skipping message inspection`);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const truncateStrings = (obj) => {
|
|
581
|
+
if (typeof obj === "string") {
|
|
582
|
+
return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
|
|
583
|
+
}
|
|
584
|
+
if (Array.isArray(obj)) {
|
|
585
|
+
const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
|
|
586
|
+
if (obj.length > maxArrayLength) {
|
|
587
|
+
truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
|
|
588
|
+
}
|
|
589
|
+
return truncatedArray;
|
|
590
|
+
}
|
|
591
|
+
if (obj && typeof obj === "object") {
|
|
592
|
+
const result = {};
|
|
593
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
594
|
+
if (key === "usage") {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
result[key] = truncateStrings(value);
|
|
598
|
+
}
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
return obj;
|
|
602
|
+
};
|
|
603
|
+
const truncatedObject = truncateStrings(redactLogValue(object));
|
|
604
|
+
const json = JSON.stringify(truncatedObject, null, 2);
|
|
605
|
+
this.logToFile(`[${this.localTimezoneTimestamp()}]`, redactLogString(message), "\n", json);
|
|
606
|
+
}
|
|
607
|
+
info(message, ...args) {
|
|
608
|
+
this.logToConsole("info", "", message, ...args);
|
|
609
|
+
this.debug(message, args);
|
|
610
|
+
}
|
|
611
|
+
infoDeveloper(message, ...args) {
|
|
612
|
+
this.debug(message, ...args);
|
|
613
|
+
if (process.env.DEBUG) {
|
|
614
|
+
this.logToConsole("info", "[DEV]", message, ...args);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
warn(message, ...args) {
|
|
618
|
+
this.logToConsole("warn", "", message, ...args);
|
|
619
|
+
this.debug(`[WARN] ${message}`, ...args);
|
|
620
|
+
}
|
|
621
|
+
getLogPath() {
|
|
622
|
+
return this.logFilePath;
|
|
623
|
+
}
|
|
624
|
+
logToConsole(level, prefix, message, ...args) {
|
|
625
|
+
message = redactLogString(message);
|
|
626
|
+
args = args.map((arg) => redactLogValue(arg));
|
|
627
|
+
switch (level) {
|
|
628
|
+
case "debug": {
|
|
629
|
+
console.log(chalk.gray(prefix), message, ...args);
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case "error": {
|
|
633
|
+
console.error(chalk.red(prefix), message, ...args);
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
case "info": {
|
|
637
|
+
console.log(chalk.blue(prefix), message, ...args);
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case "warn": {
|
|
641
|
+
console.log(chalk.yellow(prefix), message, ...args);
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
default: {
|
|
645
|
+
this.debug("Unknown log level:", level);
|
|
646
|
+
console.log(chalk.blue(prefix), message, ...args);
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async sendToRemoteServer(level, message, ...args) {
|
|
652
|
+
if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
|
|
653
|
+
try {
|
|
654
|
+
await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
|
|
655
|
+
method: "POST",
|
|
656
|
+
headers: {
|
|
657
|
+
"Content-Type": "application/json",
|
|
658
|
+
"Authorization": `Bearer ${this.dangerouslyUnencryptedServerLoggingToken}`
|
|
659
|
+
},
|
|
660
|
+
body: JSON.stringify({
|
|
661
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
662
|
+
level,
|
|
663
|
+
message: `${redactLogString(message)} ${args.map((raw) => {
|
|
664
|
+
const safe = redactLogValue(raw);
|
|
665
|
+
return typeof safe === "object" ? JSON.stringify(safe, null, 2) : String(safe);
|
|
666
|
+
}).join(" ")}`,
|
|
667
|
+
source: "cli",
|
|
668
|
+
platform: process.platform
|
|
669
|
+
})
|
|
670
|
+
});
|
|
671
|
+
} catch (error) {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
logToFile(prefix, message, ...args) {
|
|
675
|
+
const safeMessage = redactLogString(message);
|
|
676
|
+
const safeArgs = args.map((arg) => redactLogValue(arg));
|
|
677
|
+
const logLine = `${prefix} ${safeMessage} ${safeArgs.map(
|
|
678
|
+
(arg) => typeof arg === "string" ? arg : node_util.inspect(arg, { depth: 5, breakLength: 120 })
|
|
679
|
+
).join(" ")}
|
|
680
|
+
`;
|
|
681
|
+
if (this.dangerouslyUnencryptedServerLoggingUrl) {
|
|
682
|
+
let level = "info";
|
|
683
|
+
if (prefix.includes(this.localTimezoneTimestamp())) {
|
|
684
|
+
level = "debug";
|
|
685
|
+
}
|
|
686
|
+
this.sendToRemoteServer(level, safeMessage, ...safeArgs).catch(() => {
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
appendPrivateFileSync(this.logFilePath, logLine);
|
|
691
|
+
} catch (appendError) {
|
|
692
|
+
if (process.env.DEBUG) {
|
|
693
|
+
console.error("[DEV MODE ONLY THROWING] Failed to append to log file:", appendError);
|
|
694
|
+
throw appendError;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
let logger = new Logger();
|
|
700
|
+
async function listDaemonLogFiles(limit = 50) {
|
|
701
|
+
try {
|
|
702
|
+
const logsDir = configuration.logsDir;
|
|
703
|
+
if (!node_fs.existsSync(logsDir)) {
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
const logs = node_fs.readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
|
|
707
|
+
const fullPath = path.join(logsDir, file);
|
|
708
|
+
const stats = node_fs.statSync(fullPath);
|
|
709
|
+
return { file, path: fullPath, modified: stats.mtime };
|
|
710
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
711
|
+
try {
|
|
712
|
+
const { readDaemonState } = await Promise.resolve().then(function () { return persistence; });
|
|
713
|
+
const state = await readDaemonState();
|
|
714
|
+
if (!state) {
|
|
715
|
+
return logs;
|
|
716
|
+
}
|
|
717
|
+
if (state.daemonLogPath && node_fs.existsSync(state.daemonLogPath)) {
|
|
718
|
+
const stats = node_fs.statSync(state.daemonLogPath);
|
|
719
|
+
const persisted = {
|
|
720
|
+
file: path.basename(state.daemonLogPath),
|
|
721
|
+
path: state.daemonLogPath,
|
|
722
|
+
modified: stats.mtime
|
|
723
|
+
};
|
|
724
|
+
const idx = logs.findIndex((l) => l.path === persisted.path);
|
|
725
|
+
if (idx >= 0) {
|
|
726
|
+
const [found] = logs.splice(idx, 1);
|
|
727
|
+
logs.unshift(found);
|
|
728
|
+
} else {
|
|
729
|
+
logs.unshift(persisted);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
return logs.slice(0, Math.max(0, limit));
|
|
735
|
+
} catch {
|
|
736
|
+
return [];
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function getLatestDaemonLog() {
|
|
740
|
+
const [latest] = await listDaemonLogFiles(1);
|
|
741
|
+
return latest || null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const DEFAULT_SERVER_URL = "https://veryhappy.dev";
|
|
745
|
+
const LEGACY_CLOUD_SERVER_URL = "https://happy.mereith.com";
|
|
746
|
+
const CLOUD_SERVER_URLS = [DEFAULT_SERVER_URL, LEGACY_CLOUD_SERVER_URL];
|
|
747
|
+
function sameRelay(left, right) {
|
|
748
|
+
try {
|
|
749
|
+
return new URL(left).origin === new URL(right).origin;
|
|
750
|
+
} catch {
|
|
751
|
+
return left.replace(/\/+$/, "") === right.replace(/\/+$/, "");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function credentialRelayProblem(credentialServerUrl, configuredServerUrl) {
|
|
755
|
+
if (credentialServerUrl) {
|
|
756
|
+
return sameRelay(credentialServerUrl, configuredServerUrl) ? void 0 : `Credentials belong to ${credentialServerUrl}, but the active relay is ${configuredServerUrl}.`;
|
|
757
|
+
}
|
|
758
|
+
if (CLOUD_SERVER_URLS.some((cloudUrl) => sameRelay(configuredServerUrl, cloudUrl))) {
|
|
759
|
+
return void 0;
|
|
760
|
+
}
|
|
761
|
+
return `These credentials predate relay tracking, so they cannot be safely reused with ${configuredServerUrl}.`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const SandboxConfigSchema = z__namespace.object({
|
|
765
|
+
enabled: z__namespace.boolean().default(false),
|
|
766
|
+
workspaceRoot: z__namespace.string().optional(),
|
|
767
|
+
sessionIsolation: z__namespace.enum(["strict", "workspace", "custom"]).default("workspace"),
|
|
768
|
+
customWritePaths: z__namespace.array(z__namespace.string()).default([]),
|
|
769
|
+
denyReadPaths: z__namespace.array(z__namespace.string()).default(["~/.ssh", "~/.aws", "~/.gnupg"]),
|
|
770
|
+
extraWritePaths: z__namespace.array(z__namespace.string()).default(["/tmp"]),
|
|
771
|
+
denyWritePaths: z__namespace.array(z__namespace.string()).default([".env"]),
|
|
772
|
+
networkMode: z__namespace.enum(["blocked", "allowed", "custom"]).default("allowed"),
|
|
773
|
+
allowedDomains: z__namespace.array(z__namespace.string()).default([]),
|
|
774
|
+
deniedDomains: z__namespace.array(z__namespace.string()).default([]),
|
|
775
|
+
allowLocalBinding: z__namespace.boolean().default(true)
|
|
776
|
+
});
|
|
777
|
+
const SUPPORTED_SCHEMA_VERSION = 2;
|
|
778
|
+
const defaultSettings = {
|
|
779
|
+
schemaVersion: SUPPORTED_SCHEMA_VERSION,
|
|
780
|
+
onboardingCompleted: false,
|
|
781
|
+
sandboxConfig: void 0
|
|
782
|
+
};
|
|
783
|
+
function migrateSettings(raw, fromVersion) {
|
|
784
|
+
let migrated = { ...raw };
|
|
785
|
+
return migrated;
|
|
786
|
+
}
|
|
787
|
+
async function readSettingsOutcome() {
|
|
788
|
+
if (!node_fs.existsSync(configuration.settingsFile)) {
|
|
789
|
+
return { kind: "absent" };
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
return { kind: "ok", settings: await parseSettingsFile() };
|
|
793
|
+
} catch (error) {
|
|
794
|
+
return { kind: "unreadable", error: error?.message ?? String(error) };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function readSettings() {
|
|
798
|
+
const outcome = await readSettingsOutcome();
|
|
799
|
+
if (outcome.kind === "ok") return outcome.settings;
|
|
800
|
+
if (outcome.kind === "unreadable") {
|
|
801
|
+
logger.warn(`Failed to read settings: ${outcome.error}`);
|
|
802
|
+
}
|
|
803
|
+
return { ...defaultSettings };
|
|
804
|
+
}
|
|
805
|
+
async function parseSettingsFile() {
|
|
806
|
+
const content = await promises.readFile(configuration.settingsFile, "utf8");
|
|
807
|
+
const raw = JSON.parse(content);
|
|
808
|
+
const schemaVersion = raw.schemaVersion ?? 1;
|
|
809
|
+
if (schemaVersion > SUPPORTED_SCHEMA_VERSION) {
|
|
810
|
+
logger.warn(
|
|
811
|
+
`\u26A0\uFE0F Settings schema v${schemaVersion} > supported v${SUPPORTED_SCHEMA_VERSION}. Update happy-cli for full functionality.`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
const migrated = migrateSettings(raw);
|
|
815
|
+
if (migrated.sandboxConfig !== void 0) {
|
|
816
|
+
try {
|
|
817
|
+
migrated.sandboxConfig = SandboxConfigSchema.parse(migrated.sandboxConfig);
|
|
818
|
+
} catch (error) {
|
|
819
|
+
logger.warn(`\u26A0\uFE0F Invalid sandbox config - skipping. Error: ${error.message}`);
|
|
820
|
+
migrated.sandboxConfig = void 0;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return { ...defaultSettings, ...migrated };
|
|
824
|
+
}
|
|
825
|
+
async function writeSettings(settings) {
|
|
826
|
+
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
827
|
+
const settingsWithVersion = {
|
|
828
|
+
...settings,
|
|
829
|
+
schemaVersion: settings.schemaVersion ?? SUPPORTED_SCHEMA_VERSION
|
|
830
|
+
};
|
|
831
|
+
await writePrivateFile(configuration.settingsFile, JSON.stringify(settingsWithVersion, null, 2));
|
|
832
|
+
}
|
|
833
|
+
async function updateSettings(updater) {
|
|
834
|
+
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
835
|
+
const MAX_LOCK_ATTEMPTS = 50;
|
|
836
|
+
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
837
|
+
const lockFile = configuration.settingsFile + ".lock";
|
|
838
|
+
const tmpFile = configuration.settingsFile + ".tmp";
|
|
839
|
+
let fileHandle;
|
|
840
|
+
let attempts = 0;
|
|
841
|
+
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
842
|
+
try {
|
|
843
|
+
fileHandle = await promises.open(lockFile, node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY, PRIVATE_FILE_MODE);
|
|
844
|
+
break;
|
|
845
|
+
} catch (err) {
|
|
846
|
+
if (err.code === "EEXIST") {
|
|
847
|
+
attempts++;
|
|
848
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
849
|
+
try {
|
|
850
|
+
const stats = await promises.stat(lockFile);
|
|
851
|
+
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
852
|
+
await promises.unlink(lockFile).catch(() => {
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
} else {
|
|
858
|
+
throw err;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (!fileHandle) {
|
|
863
|
+
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const outcome = await readSettingsOutcome();
|
|
867
|
+
if (outcome.kind === "unreadable") {
|
|
868
|
+
throw new Error(
|
|
869
|
+
`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).`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const current = outcome.kind === "ok" ? outcome.settings : { ...defaultSettings };
|
|
873
|
+
const updated = await updater(current);
|
|
874
|
+
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
875
|
+
await writePrivateFile(tmpFile, JSON.stringify(updated, null, 2));
|
|
876
|
+
await promises.rename(tmpFile, configuration.settingsFile);
|
|
877
|
+
await hardenPrivateFile(configuration.settingsFile);
|
|
878
|
+
return updated;
|
|
879
|
+
} finally {
|
|
880
|
+
await fileHandle.close();
|
|
881
|
+
await promises.unlink(lockFile).catch(() => {
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const credentialsSchema = z__namespace.object({
|
|
886
|
+
token: z__namespace.string(),
|
|
887
|
+
authServerUrl: z__namespace.string().optional(),
|
|
888
|
+
secret: z__namespace.string().base64().nullish(),
|
|
889
|
+
// Legacy
|
|
890
|
+
encryption: z__namespace.object({
|
|
891
|
+
publicKey: z__namespace.string().base64(),
|
|
892
|
+
machineKey: z__namespace.string().base64()
|
|
893
|
+
}).nullish()
|
|
894
|
+
});
|
|
895
|
+
async function readCredentials() {
|
|
896
|
+
if (!node_fs.existsSync(configuration.privateKeyFile)) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
const keyBase64 = await promises.readFile(configuration.privateKeyFile, "utf8");
|
|
901
|
+
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
902
|
+
if (credentials.secret) {
|
|
903
|
+
return {
|
|
904
|
+
token: credentials.token,
|
|
905
|
+
authServerUrl: credentials.authServerUrl,
|
|
906
|
+
encryption: {
|
|
907
|
+
type: "legacy",
|
|
908
|
+
secret: new Uint8Array(Buffer.from(credentials.secret, "base64"))
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
} else if (credentials.encryption) {
|
|
912
|
+
return {
|
|
913
|
+
token: credentials.token,
|
|
914
|
+
authServerUrl: credentials.authServerUrl,
|
|
915
|
+
encryption: {
|
|
916
|
+
type: "dataKey",
|
|
917
|
+
publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
|
|
918
|
+
machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
} catch {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
async function readCredentialsForConfiguredRelay() {
|
|
928
|
+
const credentials = await readCredentials();
|
|
929
|
+
if (!credentials) return null;
|
|
930
|
+
const relayProblem = credentialRelayProblem(credentials.authServerUrl, configuration.serverUrl);
|
|
931
|
+
if (relayProblem) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
`${relayProblem} Use a separate HAPPY_HOME_DIR for this relay, or run \`very-happy auth login --force\` before making relay requests.`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return credentials;
|
|
937
|
+
}
|
|
938
|
+
async function writeCredentialsLegacy(credentials) {
|
|
939
|
+
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
940
|
+
await writePrivateFile(configuration.privateKeyFile, JSON.stringify({
|
|
941
|
+
secret: encodeBase64(credentials.secret),
|
|
942
|
+
token: credentials.token,
|
|
943
|
+
authServerUrl: configuration.serverUrl
|
|
944
|
+
}, null, 2));
|
|
945
|
+
}
|
|
946
|
+
async function writeCredentialsDataKey(credentials) {
|
|
947
|
+
await ensurePrivateDirectory(configuration.happyHomeDir);
|
|
948
|
+
await writePrivateFile(configuration.privateKeyFile, JSON.stringify({
|
|
949
|
+
encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
|
|
950
|
+
token: credentials.token,
|
|
951
|
+
authServerUrl: configuration.serverUrl
|
|
952
|
+
}, null, 2));
|
|
953
|
+
}
|
|
954
|
+
async function clearCredentials() {
|
|
955
|
+
if (node_fs.existsSync(configuration.privateKeyFile)) {
|
|
956
|
+
await promises.unlink(configuration.privateKeyFile);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
async function clearMachineId() {
|
|
960
|
+
await updateSettings((settings) => ({
|
|
961
|
+
...settings,
|
|
962
|
+
machineId: void 0
|
|
963
|
+
}));
|
|
964
|
+
}
|
|
965
|
+
async function readDaemonState() {
|
|
966
|
+
try {
|
|
967
|
+
if (!node_fs.existsSync(configuration.daemonStateFile)) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const content = await promises.readFile(configuration.daemonStateFile, "utf-8");
|
|
971
|
+
return JSON.parse(content);
|
|
972
|
+
} catch (error) {
|
|
973
|
+
console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function writeDaemonState(state) {
|
|
978
|
+
writePrivateFileSync(configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
979
|
+
}
|
|
980
|
+
async function clearDaemonState() {
|
|
981
|
+
if (node_fs.existsSync(configuration.daemonStateFile)) {
|
|
982
|
+
await promises.unlink(configuration.daemonStateFile);
|
|
983
|
+
}
|
|
984
|
+
if (node_fs.existsSync(configuration.daemonLockFile)) {
|
|
985
|
+
try {
|
|
986
|
+
await promises.unlink(configuration.daemonLockFile);
|
|
987
|
+
} catch {
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
|
|
992
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
993
|
+
try {
|
|
994
|
+
const fileHandle = await promises.open(
|
|
995
|
+
configuration.daemonLockFile,
|
|
996
|
+
node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY,
|
|
997
|
+
PRIVATE_FILE_MODE
|
|
998
|
+
);
|
|
999
|
+
await fileHandle.writeFile(String(process.pid));
|
|
1000
|
+
return fileHandle;
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
if (error.code === "EEXIST") {
|
|
1003
|
+
try {
|
|
1004
|
+
const lockPid = node_fs.readFileSync(configuration.daemonLockFile, "utf-8").trim();
|
|
1005
|
+
if (lockPid && !isNaN(Number(lockPid))) {
|
|
1006
|
+
try {
|
|
1007
|
+
process.kill(Number(lockPid), 0);
|
|
1008
|
+
} catch {
|
|
1009
|
+
node_fs.unlinkSync(configuration.daemonLockFile);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (attempt === maxAttempts) {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
const delayMs = attempt * delayIncrementMs;
|
|
1020
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
async function releaseDaemonLock(lockHandle) {
|
|
1026
|
+
try {
|
|
1027
|
+
await lockHandle.close();
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
try {
|
|
1031
|
+
if (node_fs.existsSync(configuration.daemonLockFile)) {
|
|
1032
|
+
node_fs.unlinkSync(configuration.daemonLockFile);
|
|
1033
|
+
}
|
|
1034
|
+
} catch {
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const SESSION_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
1038
|
+
function readPersistedSessions() {
|
|
1039
|
+
try {
|
|
1040
|
+
if (!node_fs.existsSync(configuration.sessionsFile)) return {};
|
|
1041
|
+
const data = JSON.parse(node_fs.readFileSync(configuration.sessionsFile, "utf-8"));
|
|
1042
|
+
if (!data?.sessions || typeof data.sessions !== "object") return {};
|
|
1043
|
+
const now = Date.now();
|
|
1044
|
+
const sessions = {};
|
|
1045
|
+
for (const [id, session] of Object.entries(data.sessions)) {
|
|
1046
|
+
if (now - session.savedAt < SESSION_MAX_AGE_MS) {
|
|
1047
|
+
sessions[id] = session;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return sessions;
|
|
1051
|
+
} catch {
|
|
1052
|
+
return {};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function readTrackedClaudeSessionIds() {
|
|
1056
|
+
try {
|
|
1057
|
+
if (!node_fs.existsSync(configuration.sessionsFile)) return [];
|
|
1058
|
+
const data = JSON.parse(node_fs.readFileSync(configuration.sessionsFile, "utf-8"));
|
|
1059
|
+
if (!data?.sessions || typeof data.sessions !== "object") return [];
|
|
1060
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1061
|
+
for (const session of Object.values(data.sessions)) {
|
|
1062
|
+
const metadata = (session ?? {}).metadata;
|
|
1063
|
+
for (const id of [metadata?.claudeSessionId, metadata?.importedFromClaudeSessionId]) {
|
|
1064
|
+
if (typeof id === "string" && id.length > 0) ids.add(id.toLowerCase());
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return [...ids];
|
|
1068
|
+
} catch {
|
|
1069
|
+
return [];
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function persistSession(sessionId, session) {
|
|
1073
|
+
try {
|
|
1074
|
+
const existing = readPersistedSessions();
|
|
1075
|
+
existing[sessionId] = session;
|
|
1076
|
+
const tmpFile = configuration.sessionsFile + ".tmp";
|
|
1077
|
+
writePrivateFileSync(tmpFile, JSON.stringify({ sessions: existing }, null, 2));
|
|
1078
|
+
node_fs.renameSync(tmpFile, configuration.sessionsFile);
|
|
1079
|
+
hardenPrivateFileSync(configuration.sessionsFile);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
logger.debug(`[PERSISTENCE] Failed to persist session ${sessionId}:`, error);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function deletePersistedSessions(sessionIds) {
|
|
1085
|
+
try {
|
|
1086
|
+
const existing = readPersistedSessions();
|
|
1087
|
+
let changed = false;
|
|
1088
|
+
for (const id of sessionIds) {
|
|
1089
|
+
if (id in existing) {
|
|
1090
|
+
delete existing[id];
|
|
1091
|
+
changed = true;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (!changed) return;
|
|
1095
|
+
const tmpFile = configuration.sessionsFile + ".tmp";
|
|
1096
|
+
writePrivateFileSync(tmpFile, JSON.stringify({ sessions: existing }, null, 2));
|
|
1097
|
+
node_fs.renameSync(tmpFile, configuration.sessionsFile);
|
|
1098
|
+
hardenPrivateFileSync(configuration.sessionsFile);
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
logger.debug(`[PERSISTENCE] Failed to delete persisted sessions:`, error);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
var persistence = /*#__PURE__*/Object.freeze({
|
|
1105
|
+
__proto__: null,
|
|
1106
|
+
SUPPORTED_SCHEMA_VERSION: SUPPORTED_SCHEMA_VERSION,
|
|
1107
|
+
SandboxConfigSchema: SandboxConfigSchema,
|
|
1108
|
+
acquireDaemonLock: acquireDaemonLock,
|
|
1109
|
+
clearCredentials: clearCredentials,
|
|
1110
|
+
clearDaemonState: clearDaemonState,
|
|
1111
|
+
clearMachineId: clearMachineId,
|
|
1112
|
+
deletePersistedSessions: deletePersistedSessions,
|
|
1113
|
+
persistSession: persistSession,
|
|
1114
|
+
readCredentials: readCredentials,
|
|
1115
|
+
readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay,
|
|
1116
|
+
readDaemonState: readDaemonState,
|
|
1117
|
+
readPersistedSessions: readPersistedSessions,
|
|
1118
|
+
readSettings: readSettings,
|
|
1119
|
+
readSettingsOutcome: readSettingsOutcome,
|
|
1120
|
+
readTrackedClaudeSessionIds: readTrackedClaudeSessionIds,
|
|
1121
|
+
releaseDaemonLock: releaseDaemonLock,
|
|
1122
|
+
updateSettings: updateSettings,
|
|
1123
|
+
writeCredentialsDataKey: writeCredentialsDataKey,
|
|
1124
|
+
writeCredentialsLegacy: writeCredentialsLegacy,
|
|
1125
|
+
writeDaemonState: writeDaemonState,
|
|
1126
|
+
writeSettings: writeSettings
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
const CLIPBOARD_MAX_BYTES = 256 * 1024;
|
|
1130
|
+
const CLIPBOARD_TOOL_NAME = "copy_to_clipboard";
|
|
1131
|
+
const CLIPBOARD_TOOL_TITLE = "Copy to User Clipboard";
|
|
1132
|
+
const CLIPBOARD_TOOL_DESCRIPTION = `Copy text to the clipboard of the device the user is currently viewing this session from (their web browser, possibly a phone). Use this whenever the user wants a piece of text handed to them \u2014 not only when they literally say "clipboard". Trigger phrases include: "copy X", "copy that for me", "give me X", "send me X", "I need X on my phone", "\u590D\u5236\u7ED9\u6211", "\u628A X \u590D\u5236\u4E00\u4E0B", "\u53D1\u6211", "\u7ED9\u6211\u4E00\u4EFD", "\u590D\u5236\u5230\u526A\u5207\u677F/\u526A\u8D34\u677F". Typical payloads: command output, code snippets, tokens/keys, URLs, file contents, commit messages. The text is delivered to every device where the user has the web client open. Payloads larger than ${CLIPBOARD_MAX_BYTES / 1024}KB are truncated.`;
|
|
1133
|
+
function prepareClipboardText(input) {
|
|
1134
|
+
const totalBytes = Buffer.byteLength(input, "utf8");
|
|
1135
|
+
if (totalBytes <= CLIPBOARD_MAX_BYTES) {
|
|
1136
|
+
return { text: input, truncated: false, totalBytes };
|
|
1137
|
+
}
|
|
1138
|
+
const sliced = Buffer.from(input, "utf8").subarray(0, CLIPBOARD_MAX_BYTES).toString("utf8");
|
|
1139
|
+
let text = sliced;
|
|
1140
|
+
if (text.endsWith("\uFFFD")) {
|
|
1141
|
+
text = text.slice(0, -1);
|
|
1142
|
+
}
|
|
1143
|
+
const lastCode = text.charCodeAt(text.length - 1);
|
|
1144
|
+
if (lastCode >= 55296 && lastCode <= 56319) {
|
|
1145
|
+
text = text.slice(0, -1);
|
|
1146
|
+
}
|
|
1147
|
+
return { text, truncated: true, totalBytes };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
exports.CLIPBOARD_MAX_BYTES = CLIPBOARD_MAX_BYTES;
|
|
1151
|
+
exports.CLIPBOARD_TOOL_DESCRIPTION = CLIPBOARD_TOOL_DESCRIPTION;
|
|
1152
|
+
exports.CLIPBOARD_TOOL_NAME = CLIPBOARD_TOOL_NAME;
|
|
1153
|
+
exports.CLIPBOARD_TOOL_TITLE = CLIPBOARD_TOOL_TITLE;
|
|
1154
|
+
exports.SandboxConfigSchema = SandboxConfigSchema;
|
|
1155
|
+
exports.acquireDaemonLock = acquireDaemonLock;
|
|
1156
|
+
exports.clearCredentials = clearCredentials;
|
|
1157
|
+
exports.clearDaemonState = clearDaemonState;
|
|
1158
|
+
exports.clearMachineId = clearMachineId;
|
|
1159
|
+
exports.configuration = configuration;
|
|
1160
|
+
exports.credentialRelayProblem = credentialRelayProblem;
|
|
1161
|
+
exports.decodeBase64 = decodeBase64;
|
|
1162
|
+
exports.decrypt = decrypt;
|
|
1163
|
+
exports.decryptBlob = decryptBlob;
|
|
1164
|
+
exports.decryptLegacy = decryptLegacy;
|
|
1165
|
+
exports.decryptWithDataKey = decryptWithDataKey;
|
|
1166
|
+
exports.deletePersistedSessions = deletePersistedSessions;
|
|
1167
|
+
exports.encodeBase64 = encodeBase64;
|
|
1168
|
+
exports.encodeBase64Url = encodeBase64Url;
|
|
1169
|
+
exports.encrypt = encrypt;
|
|
1170
|
+
exports.ensurePrivateDirectory = ensurePrivateDirectory;
|
|
1171
|
+
exports.ensurePrivateDirectorySync = ensurePrivateDirectorySync;
|
|
1172
|
+
exports.getLatestDaemonLog = getLatestDaemonLog;
|
|
1173
|
+
exports.getRandomBytes = getRandomBytes;
|
|
1174
|
+
exports.hardenPrivateFile = hardenPrivateFile;
|
|
1175
|
+
exports.hardenPrivateFileSync = hardenPrivateFileSync;
|
|
1176
|
+
exports.libsodiumEncryptForPublicKey = libsodiumEncryptForPublicKey;
|
|
1177
|
+
exports.libsodiumPublicKeyFromSecretKey = libsodiumPublicKeyFromSecretKey;
|
|
1178
|
+
exports.logger = logger;
|
|
1179
|
+
exports.normalizeHttpEndpoint = normalizeHttpEndpoint;
|
|
1180
|
+
exports.packageJson = packageJson;
|
|
1181
|
+
exports.persistSession = persistSession;
|
|
1182
|
+
exports.persistence = persistence;
|
|
1183
|
+
exports.prepareClipboardText = prepareClipboardText;
|
|
1184
|
+
exports.readCredentials = readCredentials;
|
|
1185
|
+
exports.readCredentialsForConfiguredRelay = readCredentialsForConfiguredRelay;
|
|
1186
|
+
exports.readDaemonState = readDaemonState;
|
|
1187
|
+
exports.readPersistedSessions = readPersistedSessions;
|
|
1188
|
+
exports.readSettings = readSettings;
|
|
1189
|
+
exports.readSettingsOutcome = readSettingsOutcome;
|
|
1190
|
+
exports.readTrackedClaudeSessionIds = readTrackedClaudeSessionIds;
|
|
1191
|
+
exports.redactLogValue = redactLogValue;
|
|
1192
|
+
exports.releaseDaemonLock = releaseDaemonLock;
|
|
1193
|
+
exports.updateSettings = updateSettings;
|
|
1194
|
+
exports.writeCredentialsDataKey = writeCredentialsDataKey;
|
|
1195
|
+
exports.writeCredentialsLegacy = writeCredentialsLegacy;
|
|
1196
|
+
exports.writeDaemonState = writeDaemonState;
|
|
1197
|
+
exports.writePrivateFileSync = writePrivateFileSync;
|
|
1198
|
+
exports.writeSettings = writeSettings;
|