happy-coder 0.9.0-6 → 0.9.1
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/index.cjs +1044 -1089
- package/dist/index.mjs +797 -824
- package/dist/lib.cjs +4 -4
- package/dist/lib.d.cts +48 -3
- package/dist/lib.d.mts +48 -3
- package/dist/lib.mjs +4 -4
- package/dist/{types-DJOX-XG-.mjs → types-BS8Pr3Im.mjs} +588 -204
- package/dist/{types-a-nJyP-e.cjs → types-DNUk09Np.cjs} +486 -71
- package/package.json +5 -5
|
@@ -3,30 +3,177 @@
|
|
|
3
3
|
var axios = require('axios');
|
|
4
4
|
var chalk = require('chalk');
|
|
5
5
|
var fs = require('fs');
|
|
6
|
+
var node_fs = require('node:fs');
|
|
6
7
|
var os = require('node:os');
|
|
7
8
|
var node_path = require('node:path');
|
|
8
9
|
var promises = require('node:fs/promises');
|
|
9
|
-
var node_fs = require('node:fs');
|
|
10
|
-
var node_events = require('node:events');
|
|
11
|
-
var socket_ioClient = require('socket.io-client');
|
|
12
10
|
var z = require('zod');
|
|
13
11
|
var node_crypto = require('node:crypto');
|
|
14
12
|
var tweetnacl = require('tweetnacl');
|
|
13
|
+
var node_events = require('node:events');
|
|
14
|
+
var socket_ioClient = require('socket.io-client');
|
|
15
15
|
var expoServerSdk = require('expo-server-sdk');
|
|
16
16
|
|
|
17
|
+
function _interopNamespaceDefault(e) {
|
|
18
|
+
var n = Object.create(null);
|
|
19
|
+
if (e) {
|
|
20
|
+
Object.keys(e).forEach(function (k) {
|
|
21
|
+
if (k !== 'default') {
|
|
22
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
23
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
get: function () { return e[k]; }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
n.default = e;
|
|
31
|
+
return Object.freeze(n);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
35
|
+
|
|
36
|
+
var name = "happy-coder";
|
|
37
|
+
var version = "0.9.1";
|
|
38
|
+
var description = "Claude Code session sharing CLI";
|
|
39
|
+
var author = "Kirill Dubovitskiy";
|
|
40
|
+
var license = "MIT";
|
|
41
|
+
var type = "module";
|
|
42
|
+
var homepage = "https://github.com/slopus/happy-cli";
|
|
43
|
+
var bugs = "https://github.com/slopus/happy-cli/issues";
|
|
44
|
+
var repository = "slopus/happy-cli";
|
|
45
|
+
var bin = {
|
|
46
|
+
happy: "./bin/happy.mjs"
|
|
47
|
+
};
|
|
48
|
+
var main = "./dist/index.cjs";
|
|
49
|
+
var module$1 = "./dist/index.mjs";
|
|
50
|
+
var types = "./dist/index.d.cts";
|
|
51
|
+
var exports$1 = {
|
|
52
|
+
".": {
|
|
53
|
+
require: {
|
|
54
|
+
types: "./dist/index.d.cts",
|
|
55
|
+
"default": "./dist/index.cjs"
|
|
56
|
+
},
|
|
57
|
+
"import": {
|
|
58
|
+
types: "./dist/index.d.mts",
|
|
59
|
+
"default": "./dist/index.mjs"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"./lib": {
|
|
63
|
+
require: {
|
|
64
|
+
types: "./dist/lib.d.cts",
|
|
65
|
+
"default": "./dist/lib.cjs"
|
|
66
|
+
},
|
|
67
|
+
"import": {
|
|
68
|
+
types: "./dist/lib.d.mts",
|
|
69
|
+
"default": "./dist/lib.mjs"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var files = [
|
|
74
|
+
"dist",
|
|
75
|
+
"bin",
|
|
76
|
+
"scripts",
|
|
77
|
+
"ripgrep",
|
|
78
|
+
"package.json"
|
|
79
|
+
];
|
|
80
|
+
var scripts = {
|
|
81
|
+
"why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started happy",
|
|
82
|
+
typecheck: "tsc --noEmit",
|
|
83
|
+
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
|
|
84
|
+
test: "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
85
|
+
start: "yarn build && ./bin/happy.mjs",
|
|
86
|
+
dev: "yarn build && tsx --env-file .env.dev src/index.ts",
|
|
87
|
+
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
88
|
+
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
89
|
+
prepublishOnly: "yarn build && yarn test",
|
|
90
|
+
release: "release-it"
|
|
91
|
+
};
|
|
92
|
+
var dependencies = {
|
|
93
|
+
"@anthropic-ai/claude-code": "^1.0.89",
|
|
94
|
+
"@anthropic-ai/sdk": "^0.56.0",
|
|
95
|
+
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
96
|
+
"@stablelib/base64": "^2.0.1",
|
|
97
|
+
"@types/http-proxy": "^1.17.16",
|
|
98
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
99
|
+
"@types/react": "^19.1.9",
|
|
100
|
+
axios: "^1.10.0",
|
|
101
|
+
chalk: "^5.4.1",
|
|
102
|
+
"expo-server-sdk": "^3.15.0",
|
|
103
|
+
fastify: "^5.5.0",
|
|
104
|
+
"fastify-type-provider-zod": "4.0.2",
|
|
105
|
+
"http-proxy": "^1.18.1",
|
|
106
|
+
"http-proxy-middleware": "^3.0.5",
|
|
107
|
+
ink: "^6.1.0",
|
|
108
|
+
open: "^10.2.0",
|
|
109
|
+
"qrcode-terminal": "^0.12.0",
|
|
110
|
+
react: "^19.1.1",
|
|
111
|
+
"socket.io-client": "^4.8.1",
|
|
112
|
+
tweetnacl: "^1.0.3",
|
|
113
|
+
zod: "^3.23.8"
|
|
114
|
+
};
|
|
115
|
+
var devDependencies = {
|
|
116
|
+
"@eslint/compat": "^1",
|
|
117
|
+
"@types/node": ">=20",
|
|
118
|
+
"cross-env": "^10.0.0",
|
|
119
|
+
dotenv: "^16.6.1",
|
|
120
|
+
eslint: "^9",
|
|
121
|
+
"eslint-config-prettier": "^10",
|
|
122
|
+
pkgroll: "^2.14.2",
|
|
123
|
+
"release-it": "^19.0.4",
|
|
124
|
+
shx: "^0.3.3",
|
|
125
|
+
"ts-node": "^10",
|
|
126
|
+
tsx: "^4.20.3",
|
|
127
|
+
typescript: "^5",
|
|
128
|
+
vitest: "^3.2.4"
|
|
129
|
+
};
|
|
130
|
+
var resolutions = {
|
|
131
|
+
"whatwg-url": "14.2.0",
|
|
132
|
+
"parse-path": "7.0.3",
|
|
133
|
+
"@types/parse-path": "7.0.3"
|
|
134
|
+
};
|
|
135
|
+
var publishConfig = {
|
|
136
|
+
registry: "https://registry.npmjs.org"
|
|
137
|
+
};
|
|
138
|
+
var packageManager = "yarn@1.22.22";
|
|
139
|
+
var packageJson = {
|
|
140
|
+
name: name,
|
|
141
|
+
version: version,
|
|
142
|
+
description: description,
|
|
143
|
+
author: author,
|
|
144
|
+
license: license,
|
|
145
|
+
type: type,
|
|
146
|
+
homepage: homepage,
|
|
147
|
+
bugs: bugs,
|
|
148
|
+
repository: repository,
|
|
149
|
+
bin: bin,
|
|
150
|
+
main: main,
|
|
151
|
+
module: module$1,
|
|
152
|
+
types: types,
|
|
153
|
+
exports: exports$1,
|
|
154
|
+
files: files,
|
|
155
|
+
scripts: scripts,
|
|
156
|
+
dependencies: dependencies,
|
|
157
|
+
devDependencies: devDependencies,
|
|
158
|
+
resolutions: resolutions,
|
|
159
|
+
publishConfig: publishConfig,
|
|
160
|
+
packageManager: packageManager
|
|
161
|
+
};
|
|
162
|
+
|
|
17
163
|
class Configuration {
|
|
18
164
|
serverUrl;
|
|
19
165
|
isDaemonProcess;
|
|
20
166
|
// Directories and paths (from persistence)
|
|
21
167
|
happyHomeDir;
|
|
22
168
|
logsDir;
|
|
23
|
-
daemonLogsDir;
|
|
24
169
|
settingsFile;
|
|
25
170
|
privateKeyFile;
|
|
26
171
|
daemonStateFile;
|
|
172
|
+
daemonLockFile;
|
|
173
|
+
currentCliVersion;
|
|
27
174
|
isExperimentalEnabled;
|
|
28
175
|
constructor() {
|
|
29
|
-
this.serverUrl = process.env.HAPPY_SERVER_URL || "https://
|
|
176
|
+
this.serverUrl = process.env.HAPPY_SERVER_URL || "https://api.cluster-fluster.com";
|
|
30
177
|
const args = process.argv.slice(2);
|
|
31
178
|
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
32
179
|
if (process.env.HAPPY_HOME_DIR) {
|
|
@@ -36,15 +183,231 @@ class Configuration {
|
|
|
36
183
|
this.happyHomeDir = node_path.join(os.homedir(), ".happy");
|
|
37
184
|
}
|
|
38
185
|
this.logsDir = node_path.join(this.happyHomeDir, "logs");
|
|
39
|
-
this.daemonLogsDir = node_path.join(this.happyHomeDir, "logs-daemon");
|
|
40
186
|
this.settingsFile = node_path.join(this.happyHomeDir, "settings.json");
|
|
41
187
|
this.privateKeyFile = node_path.join(this.happyHomeDir, "access.key");
|
|
42
188
|
this.daemonStateFile = node_path.join(this.happyHomeDir, "daemon.state.json");
|
|
189
|
+
this.daemonLockFile = node_path.join(this.happyHomeDir, "daemon.state.json.lock");
|
|
43
190
|
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
|
|
191
|
+
this.currentCliVersion = packageJson.version;
|
|
192
|
+
if (!node_fs.existsSync(this.happyHomeDir)) {
|
|
193
|
+
node_fs.mkdirSync(this.happyHomeDir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
if (!node_fs.existsSync(this.logsDir)) {
|
|
196
|
+
node_fs.mkdirSync(this.logsDir, { recursive: true });
|
|
197
|
+
}
|
|
44
198
|
}
|
|
45
199
|
}
|
|
46
200
|
const configuration = new Configuration();
|
|
47
201
|
|
|
202
|
+
function encodeBase64(buffer, variant = "base64") {
|
|
203
|
+
if (variant === "base64url") {
|
|
204
|
+
return encodeBase64Url(buffer);
|
|
205
|
+
}
|
|
206
|
+
return Buffer.from(buffer).toString("base64");
|
|
207
|
+
}
|
|
208
|
+
function encodeBase64Url(buffer) {
|
|
209
|
+
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
210
|
+
}
|
|
211
|
+
function decodeBase64(base64, variant = "base64") {
|
|
212
|
+
if (variant === "base64url") {
|
|
213
|
+
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
|
|
214
|
+
return new Uint8Array(Buffer.from(base64Standard, "base64"));
|
|
215
|
+
}
|
|
216
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
217
|
+
}
|
|
218
|
+
function getRandomBytes(size) {
|
|
219
|
+
return new Uint8Array(node_crypto.randomBytes(size));
|
|
220
|
+
}
|
|
221
|
+
function encrypt(data, secret) {
|
|
222
|
+
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
223
|
+
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
224
|
+
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
225
|
+
result.set(nonce);
|
|
226
|
+
result.set(encrypted, nonce.length);
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
function decrypt(data, secret) {
|
|
230
|
+
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
231
|
+
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
232
|
+
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
233
|
+
if (!decrypted) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const defaultSettings = {
|
|
240
|
+
onboardingCompleted: false
|
|
241
|
+
};
|
|
242
|
+
async function readSettings() {
|
|
243
|
+
if (!node_fs.existsSync(configuration.settingsFile)) {
|
|
244
|
+
return { ...defaultSettings };
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const content = await promises.readFile(configuration.settingsFile, "utf8");
|
|
248
|
+
return JSON.parse(content);
|
|
249
|
+
} catch {
|
|
250
|
+
return { ...defaultSettings };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function updateSettings(updater) {
|
|
254
|
+
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
255
|
+
const MAX_LOCK_ATTEMPTS = 50;
|
|
256
|
+
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
257
|
+
const lockFile = configuration.settingsFile + ".lock";
|
|
258
|
+
const tmpFile = configuration.settingsFile + ".tmp";
|
|
259
|
+
let fileHandle;
|
|
260
|
+
let attempts = 0;
|
|
261
|
+
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
262
|
+
try {
|
|
263
|
+
fileHandle = await promises.open(lockFile, node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY);
|
|
264
|
+
break;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
if (err.code === "EEXIST") {
|
|
267
|
+
attempts++;
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
269
|
+
try {
|
|
270
|
+
const stats = await promises.stat(lockFile);
|
|
271
|
+
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
272
|
+
await promises.unlink(lockFile).catch(() => {
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!fileHandle) {
|
|
283
|
+
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const current = await readSettings() || { ...defaultSettings };
|
|
287
|
+
const updated = await updater(current);
|
|
288
|
+
if (!node_fs.existsSync(configuration.happyHomeDir)) {
|
|
289
|
+
await promises.mkdir(configuration.happyHomeDir, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
await promises.writeFile(tmpFile, JSON.stringify(updated, null, 2));
|
|
292
|
+
await promises.rename(tmpFile, configuration.settingsFile);
|
|
293
|
+
return updated;
|
|
294
|
+
} finally {
|
|
295
|
+
await fileHandle.close();
|
|
296
|
+
await promises.unlink(lockFile).catch(() => {
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const credentialsSchema = z__namespace.object({
|
|
301
|
+
secret: z__namespace.string().base64(),
|
|
302
|
+
token: z__namespace.string()
|
|
303
|
+
});
|
|
304
|
+
async function readCredentials() {
|
|
305
|
+
if (!node_fs.existsSync(configuration.privateKeyFile)) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const keyBase64 = await promises.readFile(configuration.privateKeyFile, "utf8");
|
|
310
|
+
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
311
|
+
return {
|
|
312
|
+
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
313
|
+
token: credentials.token
|
|
314
|
+
};
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function writeCredentials(credentials) {
|
|
320
|
+
if (!node_fs.existsSync(configuration.happyHomeDir)) {
|
|
321
|
+
await promises.mkdir(configuration.happyHomeDir, { recursive: true });
|
|
322
|
+
}
|
|
323
|
+
await promises.writeFile(configuration.privateKeyFile, JSON.stringify({
|
|
324
|
+
secret: encodeBase64(credentials.secret),
|
|
325
|
+
token: credentials.token
|
|
326
|
+
}, null, 2));
|
|
327
|
+
}
|
|
328
|
+
async function clearCredentials() {
|
|
329
|
+
if (node_fs.existsSync(configuration.privateKeyFile)) {
|
|
330
|
+
await promises.unlink(configuration.privateKeyFile);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async function clearMachineId() {
|
|
334
|
+
await updateSettings((settings) => ({
|
|
335
|
+
...settings,
|
|
336
|
+
machineId: void 0
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
async function readDaemonState() {
|
|
340
|
+
try {
|
|
341
|
+
if (!node_fs.existsSync(configuration.daemonStateFile)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const content = await promises.readFile(configuration.daemonStateFile, "utf-8");
|
|
345
|
+
return JSON.parse(content);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function writeDaemonState(state) {
|
|
352
|
+
node_fs.writeFileSync(configuration.daemonStateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
353
|
+
}
|
|
354
|
+
async function clearDaemonState() {
|
|
355
|
+
if (node_fs.existsSync(configuration.daemonStateFile)) {
|
|
356
|
+
await promises.unlink(configuration.daemonStateFile);
|
|
357
|
+
}
|
|
358
|
+
if (node_fs.existsSync(configuration.daemonLockFile)) {
|
|
359
|
+
try {
|
|
360
|
+
await promises.unlink(configuration.daemonLockFile);
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
|
|
366
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
367
|
+
try {
|
|
368
|
+
const fileHandle = await promises.open(
|
|
369
|
+
configuration.daemonLockFile,
|
|
370
|
+
node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY
|
|
371
|
+
);
|
|
372
|
+
await fileHandle.writeFile(String(process.pid));
|
|
373
|
+
return fileHandle;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error.code === "EEXIST") {
|
|
376
|
+
try {
|
|
377
|
+
const lockPid = node_fs.readFileSync(configuration.daemonLockFile, "utf-8").trim();
|
|
378
|
+
if (lockPid && !isNaN(Number(lockPid))) {
|
|
379
|
+
try {
|
|
380
|
+
process.kill(Number(lockPid), 0);
|
|
381
|
+
} catch {
|
|
382
|
+
node_fs.unlinkSync(configuration.daemonLockFile);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (attempt === maxAttempts) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
const delayMs = attempt * delayIncrementMs;
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
async function releaseDaemonLock(lockHandle) {
|
|
399
|
+
try {
|
|
400
|
+
await lockHandle.close();
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
if (node_fs.existsSync(configuration.daemonLockFile)) {
|
|
405
|
+
node_fs.unlinkSync(configuration.daemonLockFile);
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
48
411
|
function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
|
|
49
412
|
return date.toLocaleString("sv-SE", {
|
|
50
413
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
@@ -54,7 +417,7 @@ function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
|
|
|
54
417
|
hour: "2-digit",
|
|
55
418
|
minute: "2-digit",
|
|
56
419
|
second: "2-digit"
|
|
57
|
-
}).replace(/[: ]/g, "-").replace(/,/g, "");
|
|
420
|
+
}).replace(/[: ]/g, "-").replace(/,/g, "") + "-pid-" + process.pid;
|
|
58
421
|
}
|
|
59
422
|
function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
|
|
60
423
|
return date.toLocaleTimeString("en-US", {
|
|
@@ -66,17 +429,14 @@ function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
|
|
|
66
429
|
fractionalSecondDigits: 3
|
|
67
430
|
});
|
|
68
431
|
}
|
|
69
|
-
|
|
70
|
-
if (!node_fs.existsSync(configuration.logsDir)) {
|
|
71
|
-
await promises.mkdir(configuration.logsDir, { recursive: true });
|
|
72
|
-
}
|
|
432
|
+
function getSessionLogPath() {
|
|
73
433
|
const timestamp = createTimestampForFilename();
|
|
74
434
|
const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
|
|
75
435
|
return node_path.join(configuration.logsDir, filename);
|
|
76
436
|
}
|
|
77
437
|
class Logger {
|
|
78
|
-
constructor(
|
|
79
|
-
this.
|
|
438
|
+
constructor(logFilePath = getSessionLogPath()) {
|
|
439
|
+
this.logFilePath = logFilePath;
|
|
80
440
|
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && process.env.HAPPY_SERVER_URL) {
|
|
81
441
|
this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPPY_SERVER_URL;
|
|
82
442
|
console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
|
|
@@ -189,25 +549,59 @@ class Logger {
|
|
|
189
549
|
this.sendToRemoteServer(level, message, ...args).catch(() => {
|
|
190
550
|
});
|
|
191
551
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
} catch (appendError) {
|
|
196
|
-
if (process.env.DEBUG) {
|
|
197
|
-
console.error("Failed to append to log file:", appendError);
|
|
198
|
-
throw appendError;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}).catch((error) => {
|
|
552
|
+
try {
|
|
553
|
+
fs.appendFileSync(this.logFilePath, logLine);
|
|
554
|
+
} catch (appendError) {
|
|
202
555
|
if (process.env.DEBUG) {
|
|
203
|
-
console.
|
|
204
|
-
|
|
205
|
-
console.log(prefix, message, ...args);
|
|
556
|
+
console.error("[DEV MODE ONLY THROWING] Failed to append to log file:", appendError);
|
|
557
|
+
throw appendError;
|
|
206
558
|
}
|
|
207
|
-
}
|
|
559
|
+
}
|
|
208
560
|
}
|
|
209
561
|
}
|
|
210
562
|
let logger = new Logger();
|
|
563
|
+
async function listDaemonLogFiles(limit = 50) {
|
|
564
|
+
try {
|
|
565
|
+
const logsDir = configuration.logsDir;
|
|
566
|
+
if (!node_fs.existsSync(logsDir)) {
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
const logs = node_fs.readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
|
|
570
|
+
const fullPath = node_path.join(logsDir, file);
|
|
571
|
+
const stats = node_fs.statSync(fullPath);
|
|
572
|
+
return { file, path: fullPath, modified: stats.mtime };
|
|
573
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
574
|
+
try {
|
|
575
|
+
const state = await readDaemonState();
|
|
576
|
+
if (!state) {
|
|
577
|
+
return logs;
|
|
578
|
+
}
|
|
579
|
+
if (state.daemonLogPath && node_fs.existsSync(state.daemonLogPath)) {
|
|
580
|
+
const stats = node_fs.statSync(state.daemonLogPath);
|
|
581
|
+
const persisted = {
|
|
582
|
+
file: node_path.basename(state.daemonLogPath),
|
|
583
|
+
path: state.daemonLogPath,
|
|
584
|
+
modified: stats.mtime
|
|
585
|
+
};
|
|
586
|
+
const idx = logs.findIndex((l) => l.path === persisted.path);
|
|
587
|
+
if (idx >= 0) {
|
|
588
|
+
const [found] = logs.splice(idx, 1);
|
|
589
|
+
logs.unshift(found);
|
|
590
|
+
} else {
|
|
591
|
+
logs.unshift(persisted);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
return logs.slice(0, Math.max(0, limit));
|
|
597
|
+
} catch {
|
|
598
|
+
return [];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async function getLatestDaemonLog() {
|
|
602
|
+
const [latest] = await listDaemonLogFiles(1);
|
|
603
|
+
return latest || null;
|
|
604
|
+
}
|
|
211
605
|
|
|
212
606
|
const SessionMessageContentSchema = z.z.object({
|
|
213
607
|
c: z.z.string(),
|
|
@@ -266,7 +660,23 @@ z.z.object({
|
|
|
266
660
|
metadata: z.z.any(),
|
|
267
661
|
metadataVersion: z.z.number(),
|
|
268
662
|
agentState: z.z.any().nullable(),
|
|
269
|
-
agentStateVersion: z.z.number()
|
|
663
|
+
agentStateVersion: z.z.number(),
|
|
664
|
+
// Connectivity tracking (from server)
|
|
665
|
+
connectivityStatus: z.z.union([
|
|
666
|
+
z.z.enum(["neverConnected", "online", "offline"]),
|
|
667
|
+
z.z.string()
|
|
668
|
+
// Forward compatibility
|
|
669
|
+
]).optional(),
|
|
670
|
+
connectivityStatusSince: z.z.number().optional(),
|
|
671
|
+
connectivityStatusReason: z.z.string().optional(),
|
|
672
|
+
// State tracking (from server)
|
|
673
|
+
state: z.z.union([
|
|
674
|
+
z.z.enum(["running", "archiveRequested", "archived"]),
|
|
675
|
+
z.z.string()
|
|
676
|
+
// Forward compatibility
|
|
677
|
+
]).optional(),
|
|
678
|
+
stateSince: z.z.number().optional(),
|
|
679
|
+
stateReason: z.z.string().optional()
|
|
270
680
|
});
|
|
271
681
|
z.z.object({
|
|
272
682
|
host: z.z.string(),
|
|
@@ -304,7 +714,23 @@ z.z.object({
|
|
|
304
714
|
active: z.z.boolean(),
|
|
305
715
|
activeAt: z.z.number(),
|
|
306
716
|
createdAt: z.z.number(),
|
|
307
|
-
updatedAt: z.z.number()
|
|
717
|
+
updatedAt: z.z.number(),
|
|
718
|
+
// Connectivity tracking (from server)
|
|
719
|
+
connectivityStatus: z.z.union([
|
|
720
|
+
z.z.enum(["neverConnected", "online", "offline"]),
|
|
721
|
+
z.z.string()
|
|
722
|
+
// Forward compatibility
|
|
723
|
+
]).optional(),
|
|
724
|
+
connectivityStatusSince: z.z.number().optional(),
|
|
725
|
+
connectivityStatusReason: z.z.string().optional(),
|
|
726
|
+
// State tracking (from server)
|
|
727
|
+
state: z.z.union([
|
|
728
|
+
z.z.enum(["running", "archiveRequested", "archived"]),
|
|
729
|
+
z.z.string()
|
|
730
|
+
// Forward compatibility
|
|
731
|
+
]).optional(),
|
|
732
|
+
stateSince: z.z.number().optional(),
|
|
733
|
+
stateReason: z.z.string().optional()
|
|
308
734
|
});
|
|
309
735
|
z.z.object({
|
|
310
736
|
content: SessionMessageContentSchema,
|
|
@@ -364,44 +790,6 @@ const AgentMessageSchema = z.z.object({
|
|
|
364
790
|
});
|
|
365
791
|
z.z.union([UserMessageSchema, AgentMessageSchema]);
|
|
366
792
|
|
|
367
|
-
function encodeBase64(buffer, variant = "base64") {
|
|
368
|
-
if (variant === "base64url") {
|
|
369
|
-
return encodeBase64Url(buffer);
|
|
370
|
-
}
|
|
371
|
-
return Buffer.from(buffer).toString("base64");
|
|
372
|
-
}
|
|
373
|
-
function encodeBase64Url(buffer) {
|
|
374
|
-
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
375
|
-
}
|
|
376
|
-
function decodeBase64(base64, variant = "base64") {
|
|
377
|
-
if (variant === "base64url") {
|
|
378
|
-
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
|
|
379
|
-
return new Uint8Array(Buffer.from(base64Standard, "base64"));
|
|
380
|
-
}
|
|
381
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
382
|
-
}
|
|
383
|
-
function getRandomBytes(size) {
|
|
384
|
-
return new Uint8Array(node_crypto.randomBytes(size));
|
|
385
|
-
}
|
|
386
|
-
function encrypt(data, secret) {
|
|
387
|
-
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
388
|
-
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
389
|
-
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
390
|
-
result.set(nonce);
|
|
391
|
-
result.set(encrypted, nonce.length);
|
|
392
|
-
return result;
|
|
393
|
-
}
|
|
394
|
-
function decrypt(data, secret) {
|
|
395
|
-
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
396
|
-
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
397
|
-
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
398
|
-
if (!decrypted) {
|
|
399
|
-
logger.debug("[ERROR] Decryption failed");
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
403
|
-
}
|
|
404
|
-
|
|
405
793
|
async function delay(ms) {
|
|
406
794
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
407
795
|
}
|
|
@@ -655,6 +1043,9 @@ class ApiSessionClient extends node_events.EventEmitter {
|
|
|
655
1043
|
* Send a ping message to keep the connection alive
|
|
656
1044
|
*/
|
|
657
1045
|
keepAlive(thinking, mode) {
|
|
1046
|
+
if (process.env.DEBUG) {
|
|
1047
|
+
logger.debug(`[API] Sending keep alive message: ${thinking}`);
|
|
1048
|
+
}
|
|
658
1049
|
this.socket.volatile.emit("session-alive", {
|
|
659
1050
|
sid: this.sessionId,
|
|
660
1051
|
time: Date.now(),
|
|
@@ -907,11 +1298,17 @@ class ApiMachineClient {
|
|
|
907
1298
|
if (!session) {
|
|
908
1299
|
throw new Error("Failed to spawn session");
|
|
909
1300
|
}
|
|
910
|
-
|
|
1301
|
+
if (session.error) {
|
|
1302
|
+
throw new Error(session.error);
|
|
1303
|
+
}
|
|
1304
|
+
logger.debug(`[API MACHINE] Spawned session ${session.happySessionId || "WARNING - not session Id recieved in webhook"} with PID ${session.pid}`);
|
|
911
1305
|
if (!session.happySessionId) {
|
|
912
1306
|
throw new Error(`Session spawned (PID ${session.pid}) but no sessionId received from webhook. The session process may still be initializing.`);
|
|
913
1307
|
}
|
|
914
|
-
const response = {
|
|
1308
|
+
const response = {
|
|
1309
|
+
sessionId: session.happySessionId,
|
|
1310
|
+
message: session.message
|
|
1311
|
+
};
|
|
915
1312
|
logger.debug(`[API MACHINE] Sending RPC response:`, response);
|
|
916
1313
|
callback(encodeBase64(encrypt(response, this.secret)));
|
|
917
1314
|
return;
|
|
@@ -997,7 +1394,7 @@ class ApiMachineClient {
|
|
|
997
1394
|
machineId: this.machine.id,
|
|
998
1395
|
time: Date.now()
|
|
999
1396
|
};
|
|
1000
|
-
if (process.env.
|
|
1397
|
+
if (process.env.DEBUG) {
|
|
1001
1398
|
logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
|
|
1002
1399
|
}
|
|
1003
1400
|
this.socket.emit("machine-alive", payload);
|
|
@@ -1025,7 +1422,7 @@ class PushNotificationClient {
|
|
|
1025
1422
|
token;
|
|
1026
1423
|
baseUrl;
|
|
1027
1424
|
expo;
|
|
1028
|
-
constructor(token, baseUrl = "https://
|
|
1425
|
+
constructor(token, baseUrl = "https://api.cluster-fluster.com") {
|
|
1029
1426
|
this.token = token;
|
|
1030
1427
|
this.baseUrl = baseUrl;
|
|
1031
1428
|
this.expo = new expoServerSdk.Expo();
|
|
@@ -1245,6 +1642,11 @@ class ApiClient {
|
|
|
1245
1642
|
timeout: 5e3
|
|
1246
1643
|
}
|
|
1247
1644
|
);
|
|
1645
|
+
if (response.status !== 200) {
|
|
1646
|
+
console.error(chalk.red(`[API] Failed to create machine: ${response.statusText}`));
|
|
1647
|
+
console.log(chalk.yellow(`[API] Failed to create machine: ${response.statusText}, most likely you have re-authenticated, but you still have a machine associated with the old account. Now we are trying to re-associate the machine with the new account. That is not allowed. Please run 'happy doctor clean' to clean up your happy state, and try your original command again. Please create an issue on github if this is causing you problems. We apologize for the inconvenience.`));
|
|
1648
|
+
process.exit(1);
|
|
1649
|
+
}
|
|
1248
1650
|
const raw = response.data.machine;
|
|
1249
1651
|
logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
|
|
1250
1652
|
const machine = {
|
|
@@ -1323,10 +1725,23 @@ exports.ApiClient = ApiClient;
|
|
|
1323
1725
|
exports.ApiSessionClient = ApiSessionClient;
|
|
1324
1726
|
exports.AsyncLock = AsyncLock;
|
|
1325
1727
|
exports.RawJSONLinesSchema = RawJSONLinesSchema;
|
|
1728
|
+
exports.acquireDaemonLock = acquireDaemonLock;
|
|
1326
1729
|
exports.backoff = backoff;
|
|
1730
|
+
exports.clearCredentials = clearCredentials;
|
|
1731
|
+
exports.clearDaemonState = clearDaemonState;
|
|
1732
|
+
exports.clearMachineId = clearMachineId;
|
|
1327
1733
|
exports.configuration = configuration;
|
|
1328
1734
|
exports.decodeBase64 = decodeBase64;
|
|
1329
1735
|
exports.delay = delay;
|
|
1330
1736
|
exports.encodeBase64 = encodeBase64;
|
|
1331
1737
|
exports.encodeBase64Url = encodeBase64Url;
|
|
1738
|
+
exports.getLatestDaemonLog = getLatestDaemonLog;
|
|
1332
1739
|
exports.logger = logger;
|
|
1740
|
+
exports.packageJson = packageJson;
|
|
1741
|
+
exports.readCredentials = readCredentials;
|
|
1742
|
+
exports.readDaemonState = readDaemonState;
|
|
1743
|
+
exports.readSettings = readSettings;
|
|
1744
|
+
exports.releaseDaemonLock = releaseDaemonLock;
|
|
1745
|
+
exports.updateSettings = updateSettings;
|
|
1746
|
+
exports.writeCredentials = writeCredentials;
|
|
1747
|
+
exports.writeDaemonState = writeDaemonState;
|