happy-coder 0.6.3 → 0.7.1-beta.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 +1623 -774
- package/dist/index.mjs +1631 -782
- package/dist/lib.cjs +3 -11
- package/dist/lib.d.cts +162 -14
- package/dist/lib.d.mts +162 -14
- package/dist/lib.mjs +1 -1
- package/dist/{types-Dz5kZrVh.mjs → types-BZC9-exR.mjs} +413 -43
- package/dist/{types-BDtHM1DY.cjs → types-CzvFvJwf.cjs} +458 -89
- package/package.json +16 -8
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var types$1 = require('./types-
|
|
4
|
+
var types$1 = require('./types-CzvFvJwf.cjs');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
6
|
var node_child_process = require('node:child_process');
|
|
7
7
|
var node_path = require('node:path');
|
|
@@ -16,8 +16,8 @@ var promises = require('fs/promises');
|
|
|
16
16
|
var ink = require('ink');
|
|
17
17
|
var React = require('react');
|
|
18
18
|
var axios = require('axios');
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
require('node:events');
|
|
20
|
+
require('socket.io-client');
|
|
21
21
|
var tweetnacl = require('tweetnacl');
|
|
22
22
|
require('expo-server-sdk');
|
|
23
23
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
@@ -29,8 +29,10 @@ var util = require('util');
|
|
|
29
29
|
var crypto = require('crypto');
|
|
30
30
|
var qrcode = require('qrcode-terminal');
|
|
31
31
|
var open = require('open');
|
|
32
|
-
var
|
|
32
|
+
var fastify = require('fastify');
|
|
33
|
+
var fastifyTypeProviderZod = require('fastify-type-provider-zod');
|
|
33
34
|
var os$1 = require('os');
|
|
35
|
+
var fs = require('fs');
|
|
34
36
|
|
|
35
37
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
36
38
|
function _interopNamespaceDefault(e) {
|
|
@@ -93,11 +95,19 @@ class Session {
|
|
|
93
95
|
onSessionFound = (sessionId) => {
|
|
94
96
|
this.sessionId = sessionId;
|
|
95
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* Clear the current session ID (used by /clear command)
|
|
100
|
+
*/
|
|
101
|
+
clearSessionId = () => {
|
|
102
|
+
this.sessionId = null;
|
|
103
|
+
types$1.logger.debug("[Session] Session ID cleared");
|
|
104
|
+
};
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
function getProjectPath(workingDirectory) {
|
|
99
108
|
const projectId = node_path.resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
|
|
100
|
-
|
|
109
|
+
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || node_path.join(os.homedir(), ".claude");
|
|
110
|
+
return node_path.join(claudeConfigDir, "projects", projectId);
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
function claudeCheckSession(sessionId, path) {
|
|
@@ -1237,6 +1247,50 @@ class PushableAsyncIterable {
|
|
|
1237
1247
|
}
|
|
1238
1248
|
}
|
|
1239
1249
|
|
|
1250
|
+
function parseCompact(message) {
|
|
1251
|
+
const trimmed = message.trim();
|
|
1252
|
+
if (trimmed === "/compact") {
|
|
1253
|
+
return {
|
|
1254
|
+
isCompact: true,
|
|
1255
|
+
originalMessage: trimmed
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
if (trimmed.startsWith("/compact ")) {
|
|
1259
|
+
return {
|
|
1260
|
+
isCompact: true,
|
|
1261
|
+
originalMessage: trimmed
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
return {
|
|
1265
|
+
isCompact: false,
|
|
1266
|
+
originalMessage: message
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
function parseClear(message) {
|
|
1270
|
+
const trimmed = message.trim();
|
|
1271
|
+
return {
|
|
1272
|
+
isClear: trimmed === "/clear"
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function parseSpecialCommand(message) {
|
|
1276
|
+
const compactResult = parseCompact(message);
|
|
1277
|
+
if (compactResult.isCompact) {
|
|
1278
|
+
return {
|
|
1279
|
+
type: "compact",
|
|
1280
|
+
originalMessage: compactResult.originalMessage
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
const clearResult = parseClear(message);
|
|
1284
|
+
if (clearResult.isClear) {
|
|
1285
|
+
return {
|
|
1286
|
+
type: "clear"
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
type: null
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1240
1294
|
async function claudeRemote(opts) {
|
|
1241
1295
|
let startFrom = opts.sessionId;
|
|
1242
1296
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
@@ -1270,6 +1324,21 @@ async function claudeRemote(opts) {
|
|
|
1270
1324
|
sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
|
|
1271
1325
|
}
|
|
1272
1326
|
types$1.logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}, model: ${opts.model || "default"}, fallbackModel: ${opts.fallbackModel || "none"}, customSystemPrompt: ${opts.customSystemPrompt ? "set" : "none"}, appendSystemPrompt: ${opts.appendSystemPrompt ? "set" : "none"}, allowedTools: ${opts.allowedTools ? opts.allowedTools.join(",") : "none"}, disallowedTools: ${opts.disallowedTools ? opts.disallowedTools.join(",") : "none"}`);
|
|
1327
|
+
const specialCommand = parseSpecialCommand(opts.message);
|
|
1328
|
+
if (specialCommand.type === "clear") {
|
|
1329
|
+
types$1.logger.debug("[claudeRemote] /clear command detected - should not reach here, handled in start.ts");
|
|
1330
|
+
if (opts.onCompletionEvent) {
|
|
1331
|
+
opts.onCompletionEvent("Context was reset");
|
|
1332
|
+
}
|
|
1333
|
+
if (opts.onSessionReset) {
|
|
1334
|
+
opts.onSessionReset();
|
|
1335
|
+
}
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (specialCommand.type === "compact") {
|
|
1339
|
+
types$1.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
|
|
1340
|
+
}
|
|
1341
|
+
const isCompactCommand = specialCommand.type === "compact";
|
|
1273
1342
|
let message = new PushableAsyncIterable();
|
|
1274
1343
|
message.push({
|
|
1275
1344
|
type: "user",
|
|
@@ -1309,10 +1378,22 @@ async function claudeRemote(opts) {
|
|
|
1309
1378
|
types$1.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
|
|
1310
1379
|
opts.onSessionFound(systemInit.session_id);
|
|
1311
1380
|
}
|
|
1381
|
+
if (isCompactCommand) {
|
|
1382
|
+
types$1.logger.debug("[claudeRemote] Compaction started");
|
|
1383
|
+
if (opts.onCompletionEvent) {
|
|
1384
|
+
opts.onCompletionEvent("Compaction started");
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1312
1387
|
}
|
|
1313
1388
|
if (message2.type === "result") {
|
|
1314
1389
|
updateThinking(false);
|
|
1315
1390
|
types$1.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
1391
|
+
if (isCompactCommand) {
|
|
1392
|
+
types$1.logger.debug("[claudeRemote] Compaction completed");
|
|
1393
|
+
if (opts.onCompletionEvent) {
|
|
1394
|
+
opts.onCompletionEvent("Compaction completed");
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1316
1397
|
return;
|
|
1317
1398
|
}
|
|
1318
1399
|
if (message2.type === "user") {
|
|
@@ -1973,31 +2054,39 @@ class SDKToLogConverter {
|
|
|
1973
2054
|
}
|
|
1974
2055
|
|
|
1975
2056
|
async function claudeRemoteLauncher(session) {
|
|
2057
|
+
types$1.logger.debug("[claudeRemoteLauncher] Starting remote launcher");
|
|
2058
|
+
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
2059
|
+
types$1.logger.debug(`[claudeRemoteLauncher] TTY available: ${hasTTY}`);
|
|
1976
2060
|
let messageBuffer = new MessageBuffer();
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2061
|
+
let inkInstance = null;
|
|
2062
|
+
if (hasTTY) {
|
|
2063
|
+
console.clear();
|
|
2064
|
+
inkInstance = ink.render(React.createElement(RemoteModeDisplay, {
|
|
2065
|
+
messageBuffer,
|
|
2066
|
+
logPath: process.env.DEBUG ? session.logPath : void 0,
|
|
2067
|
+
onExit: async () => {
|
|
2068
|
+
types$1.logger.debug("[remote]: Exiting client via Ctrl-C");
|
|
2069
|
+
if (!exitReason) {
|
|
2070
|
+
exitReason = "exit";
|
|
2071
|
+
}
|
|
2072
|
+
await abort();
|
|
2073
|
+
},
|
|
2074
|
+
onSwitchToLocal: () => {
|
|
2075
|
+
types$1.logger.debug("[remote]: Switching to local mode via double space");
|
|
2076
|
+
doSwitch();
|
|
1985
2077
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2078
|
+
}), {
|
|
2079
|
+
exitOnCtrlC: false,
|
|
2080
|
+
patchConsole: false
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
if (hasTTY) {
|
|
2084
|
+
process.stdin.resume();
|
|
2085
|
+
if (process.stdin.isTTY) {
|
|
2086
|
+
process.stdin.setRawMode(true);
|
|
1991
2087
|
}
|
|
1992
|
-
|
|
1993
|
-
exitOnCtrlC: false,
|
|
1994
|
-
patchConsole: false
|
|
1995
|
-
});
|
|
1996
|
-
process.stdin.resume();
|
|
1997
|
-
if (process.stdin.isTTY) {
|
|
1998
|
-
process.stdin.setRawMode(true);
|
|
2088
|
+
process.stdin.setEncoding("utf8");
|
|
1999
2089
|
}
|
|
2000
|
-
process.stdin.setEncoding("utf8");
|
|
2001
2090
|
const scanner = await createSessionScanner({
|
|
2002
2091
|
sessionId: session.sessionId,
|
|
2003
2092
|
workingDirectory: session.path,
|
|
@@ -2177,6 +2266,14 @@ async function claudeRemoteLauncher(session) {
|
|
|
2177
2266
|
claudeEnvVars: session.claudeEnvVars,
|
|
2178
2267
|
claudeArgs: session.claudeArgs,
|
|
2179
2268
|
onMessage,
|
|
2269
|
+
onCompletionEvent: (message) => {
|
|
2270
|
+
types$1.logger.debug(`[remote]: Completion event: ${message}`);
|
|
2271
|
+
session.client.sendSessionEvent({ type: "message", message });
|
|
2272
|
+
},
|
|
2273
|
+
onSessionReset: () => {
|
|
2274
|
+
types$1.logger.debug("[remote]: Session reset");
|
|
2275
|
+
session.clearSessionId();
|
|
2276
|
+
},
|
|
2180
2277
|
signal: abortController.signal
|
|
2181
2278
|
});
|
|
2182
2279
|
if (!exitReason && abortController.signal.aborted) {
|
|
@@ -2209,7 +2306,9 @@ async function claudeRemoteLauncher(session) {
|
|
|
2209
2306
|
if (process.stdin.isTTY) {
|
|
2210
2307
|
process.stdin.setRawMode(false);
|
|
2211
2308
|
}
|
|
2212
|
-
inkInstance
|
|
2309
|
+
if (inkInstance) {
|
|
2310
|
+
inkInstance.unmount();
|
|
2311
|
+
}
|
|
2213
2312
|
messageBuffer.clear();
|
|
2214
2313
|
if (abortFuture) {
|
|
2215
2314
|
abortFuture.resolve(void 0);
|
|
@@ -2233,6 +2332,9 @@ async function loop(opts) {
|
|
|
2233
2332
|
messageQueue: opts.messageQueue,
|
|
2234
2333
|
onModeChange: opts.onModeChange
|
|
2235
2334
|
});
|
|
2335
|
+
if (opts.onSessionReady) {
|
|
2336
|
+
opts.onSessionReady(session);
|
|
2337
|
+
}
|
|
2236
2338
|
let mode = opts.startingMode ?? "local";
|
|
2237
2339
|
while (true) {
|
|
2238
2340
|
types$1.logger.debug(`[loop] Iteration with mode: ${mode}`);
|
|
@@ -2262,7 +2364,7 @@ async function loop(opts) {
|
|
|
2262
2364
|
}
|
|
2263
2365
|
|
|
2264
2366
|
var name = "happy-coder";
|
|
2265
|
-
var version = "0.
|
|
2367
|
+
var version = "0.7.1-beta.1";
|
|
2266
2368
|
var description = "Claude Code session sharing CLI";
|
|
2267
2369
|
var author = "Kirill Dubovitskiy";
|
|
2268
2370
|
var license = "MIT";
|
|
@@ -2306,15 +2408,21 @@ var files = [
|
|
|
2306
2408
|
"package.json"
|
|
2307
2409
|
];
|
|
2308
2410
|
var scripts = {
|
|
2309
|
-
|
|
2411
|
+
"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",
|
|
2412
|
+
typecheck: "tsc --noEmit",
|
|
2413
|
+
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
|
|
2414
|
+
test: "yarn build && vitest run",
|
|
2310
2415
|
"test:watch": "vitest",
|
|
2311
|
-
|
|
2416
|
+
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2417
|
+
dev: "yarn build && npx tsx src/index.ts",
|
|
2418
|
+
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2419
|
+
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2312
2420
|
prepublishOnly: "yarn build && yarn test",
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
"
|
|
2316
|
-
"
|
|
2317
|
-
"publish
|
|
2421
|
+
"minor:publish": "yarn build && npm version minor && npm publish",
|
|
2422
|
+
"patch:publish": "yarn build && npm version patch && npm publish",
|
|
2423
|
+
"version:prerelease": "yarn build && npm version prerelease --preid=beta",
|
|
2424
|
+
"publish:prerelease": "npm publish --tag beta",
|
|
2425
|
+
"beta:publish": "yarn version:prerelease && yarn publish:prerelease"
|
|
2318
2426
|
};
|
|
2319
2427
|
var dependencies = {
|
|
2320
2428
|
"@anthropic-ai/claude-code": "^1.0.73",
|
|
@@ -2327,6 +2435,8 @@ var dependencies = {
|
|
|
2327
2435
|
axios: "^1.10.0",
|
|
2328
2436
|
chalk: "^5.4.1",
|
|
2329
2437
|
"expo-server-sdk": "^3.15.0",
|
|
2438
|
+
fastify: "^5.5.0",
|
|
2439
|
+
"fastify-type-provider-zod": "4.0.2",
|
|
2330
2440
|
"http-proxy": "^1.18.1",
|
|
2331
2441
|
"http-proxy-middleware": "^3.0.5",
|
|
2332
2442
|
ink: "^6.1.0",
|
|
@@ -2416,8 +2526,8 @@ function registerHandlers(session) {
|
|
|
2416
2526
|
const { stdout, stderr } = await execAsync(data.command, options);
|
|
2417
2527
|
return {
|
|
2418
2528
|
success: true,
|
|
2419
|
-
stdout: stdout
|
|
2420
|
-
stderr: stderr
|
|
2529
|
+
stdout: stdout ? stdout.toString() : "",
|
|
2530
|
+
stderr: stderr ? stderr.toString() : "",
|
|
2421
2531
|
exitCode: 0
|
|
2422
2532
|
};
|
|
2423
2533
|
} catch (error) {
|
|
@@ -2433,8 +2543,8 @@ function registerHandlers(session) {
|
|
|
2433
2543
|
}
|
|
2434
2544
|
return {
|
|
2435
2545
|
success: false,
|
|
2436
|
-
stdout: execError.stdout
|
|
2437
|
-
stderr: execError.stderr
|
|
2546
|
+
stdout: execError.stdout ? execError.stdout.toString() : "",
|
|
2547
|
+
stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
|
|
2438
2548
|
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
2439
2549
|
error: execError.message || "Command failed"
|
|
2440
2550
|
};
|
|
@@ -2601,8 +2711,8 @@ function registerHandlers(session) {
|
|
|
2601
2711
|
return {
|
|
2602
2712
|
success: true,
|
|
2603
2713
|
exitCode: result.exitCode,
|
|
2604
|
-
stdout: result.stdout,
|
|
2605
|
-
stderr: result.stderr
|
|
2714
|
+
stdout: result.stdout.toString(),
|
|
2715
|
+
stderr: result.stderr.toString()
|
|
2606
2716
|
};
|
|
2607
2717
|
} catch (error) {
|
|
2608
2718
|
types$1.logger.debug("Failed to run ripgrep:", error);
|
|
@@ -2612,6 +2722,26 @@ function registerHandlers(session) {
|
|
|
2612
2722
|
};
|
|
2613
2723
|
}
|
|
2614
2724
|
});
|
|
2725
|
+
session.setHandler("killSession", async () => {
|
|
2726
|
+
types$1.logger.debug("Kill session request received");
|
|
2727
|
+
try {
|
|
2728
|
+
const response = {
|
|
2729
|
+
success: true,
|
|
2730
|
+
message: "Session termination acknowledged, exiting in 100ms"
|
|
2731
|
+
};
|
|
2732
|
+
setTimeout(() => {
|
|
2733
|
+
types$1.logger.debug("[KILL SESSION] Exiting process as requested");
|
|
2734
|
+
process.exit(0);
|
|
2735
|
+
}, 100);
|
|
2736
|
+
return response;
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
types$1.logger.debug("Failed to kill session:", error);
|
|
2739
|
+
return {
|
|
2740
|
+
success: false,
|
|
2741
|
+
message: error instanceof Error ? error.message : "Failed to kill session"
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
});
|
|
2615
2745
|
}
|
|
2616
2746
|
|
|
2617
2747
|
const defaultSettings = {
|
|
@@ -2628,11 +2758,52 @@ async function readSettings() {
|
|
|
2628
2758
|
return { ...defaultSettings };
|
|
2629
2759
|
}
|
|
2630
2760
|
}
|
|
2631
|
-
async function
|
|
2632
|
-
|
|
2633
|
-
|
|
2761
|
+
async function updateSettings(updater) {
|
|
2762
|
+
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
2763
|
+
const MAX_LOCK_ATTEMPTS = 50;
|
|
2764
|
+
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
2765
|
+
const lockFile = types$1.configuration.settingsFile + ".lock";
|
|
2766
|
+
const tmpFile = types$1.configuration.settingsFile + ".tmp";
|
|
2767
|
+
let fileHandle;
|
|
2768
|
+
let attempts = 0;
|
|
2769
|
+
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
2770
|
+
try {
|
|
2771
|
+
fileHandle = await promises$1.open(lockFile, node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY);
|
|
2772
|
+
break;
|
|
2773
|
+
} catch (err) {
|
|
2774
|
+
if (err.code === "EEXIST") {
|
|
2775
|
+
attempts++;
|
|
2776
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
2777
|
+
try {
|
|
2778
|
+
const stats = await promises$1.stat(lockFile);
|
|
2779
|
+
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
2780
|
+
await promises$1.unlink(lockFile).catch(() => {
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
} catch {
|
|
2784
|
+
}
|
|
2785
|
+
} else {
|
|
2786
|
+
throw err;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
if (!fileHandle) {
|
|
2791
|
+
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
2792
|
+
}
|
|
2793
|
+
try {
|
|
2794
|
+
const current = await readSettings() || { ...defaultSettings };
|
|
2795
|
+
const updated = await updater(current);
|
|
2796
|
+
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
2797
|
+
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
2798
|
+
}
|
|
2799
|
+
await promises$1.writeFile(tmpFile, JSON.stringify(updated, null, 2));
|
|
2800
|
+
await promises$1.rename(tmpFile, types$1.configuration.settingsFile);
|
|
2801
|
+
return updated;
|
|
2802
|
+
} finally {
|
|
2803
|
+
await fileHandle.close();
|
|
2804
|
+
await promises$1.unlink(lockFile).catch(() => {
|
|
2805
|
+
});
|
|
2634
2806
|
}
|
|
2635
|
-
await promises$1.writeFile(types$1.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
2636
2807
|
}
|
|
2637
2808
|
const credentialsSchema = z__namespace.object({
|
|
2638
2809
|
secret: z__namespace.string().base64(),
|
|
@@ -2654,14 +2825,47 @@ async function readCredentials() {
|
|
|
2654
2825
|
}
|
|
2655
2826
|
}
|
|
2656
2827
|
async function writeCredentials(credentials) {
|
|
2657
|
-
if (!node_fs.existsSync(types$1.configuration.
|
|
2658
|
-
await promises$1.mkdir(types$1.configuration.
|
|
2828
|
+
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
2829
|
+
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
2659
2830
|
}
|
|
2660
2831
|
await promises$1.writeFile(types$1.configuration.privateKeyFile, JSON.stringify({
|
|
2661
2832
|
secret: types$1.encodeBase64(credentials.secret),
|
|
2662
2833
|
token: credentials.token
|
|
2663
2834
|
}, null, 2));
|
|
2664
2835
|
}
|
|
2836
|
+
async function clearCredentials() {
|
|
2837
|
+
if (node_fs.existsSync(types$1.configuration.privateKeyFile)) {
|
|
2838
|
+
await promises$1.unlink(types$1.configuration.privateKeyFile);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
async function clearMachineId() {
|
|
2842
|
+
await updateSettings((settings) => ({
|
|
2843
|
+
...settings,
|
|
2844
|
+
machineId: void 0
|
|
2845
|
+
}));
|
|
2846
|
+
}
|
|
2847
|
+
async function readDaemonState() {
|
|
2848
|
+
try {
|
|
2849
|
+
if (!node_fs.existsSync(types$1.configuration.daemonStateFile)) {
|
|
2850
|
+
return null;
|
|
2851
|
+
}
|
|
2852
|
+
const content = await promises$1.readFile(types$1.configuration.daemonStateFile, "utf-8");
|
|
2853
|
+
return JSON.parse(content);
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
return null;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
async function writeDaemonState(state) {
|
|
2859
|
+
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
2860
|
+
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
2861
|
+
}
|
|
2862
|
+
await promises$1.writeFile(types$1.configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
2863
|
+
}
|
|
2864
|
+
async function clearDaemonState() {
|
|
2865
|
+
if (node_fs.existsSync(types$1.configuration.daemonStateFile)) {
|
|
2866
|
+
await promises$1.unlink(types$1.configuration.daemonStateFile);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2665
2869
|
|
|
2666
2870
|
class MessageQueue2 {
|
|
2667
2871
|
constructor(modeHasher) {
|
|
@@ -2669,6 +2873,7 @@ class MessageQueue2 {
|
|
|
2669
2873
|
types$1.logger.debug(`[MessageQueue2] Initialized`);
|
|
2670
2874
|
}
|
|
2671
2875
|
queue = [];
|
|
2876
|
+
// Made public for testing
|
|
2672
2877
|
waiter = null;
|
|
2673
2878
|
closed = false;
|
|
2674
2879
|
onMessageHandler = null;
|
|
@@ -2690,7 +2895,8 @@ class MessageQueue2 {
|
|
|
2690
2895
|
this.queue.push({
|
|
2691
2896
|
message,
|
|
2692
2897
|
mode,
|
|
2693
|
-
modeHash
|
|
2898
|
+
modeHash,
|
|
2899
|
+
isolate: false
|
|
2694
2900
|
});
|
|
2695
2901
|
if (this.onMessageHandler) {
|
|
2696
2902
|
this.onMessageHandler(message, mode);
|
|
@@ -2703,6 +2909,62 @@ class MessageQueue2 {
|
|
|
2703
2909
|
}
|
|
2704
2910
|
types$1.logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
|
|
2705
2911
|
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Push a message immediately without batching delay.
|
|
2914
|
+
* Does not clear the queue or enforce isolation.
|
|
2915
|
+
*/
|
|
2916
|
+
pushImmediate(message, mode) {
|
|
2917
|
+
if (this.closed) {
|
|
2918
|
+
throw new Error("Cannot push to closed queue");
|
|
2919
|
+
}
|
|
2920
|
+
const modeHash = this.modeHasher(mode);
|
|
2921
|
+
types$1.logger.debug(`[MessageQueue2] pushImmediate() called with mode hash: ${modeHash}`);
|
|
2922
|
+
this.queue.push({
|
|
2923
|
+
message,
|
|
2924
|
+
mode,
|
|
2925
|
+
modeHash,
|
|
2926
|
+
isolate: false
|
|
2927
|
+
});
|
|
2928
|
+
if (this.onMessageHandler) {
|
|
2929
|
+
this.onMessageHandler(message, mode);
|
|
2930
|
+
}
|
|
2931
|
+
if (this.waiter) {
|
|
2932
|
+
types$1.logger.debug(`[MessageQueue2] Notifying waiter for immediate message`);
|
|
2933
|
+
const waiter = this.waiter;
|
|
2934
|
+
this.waiter = null;
|
|
2935
|
+
waiter(true);
|
|
2936
|
+
}
|
|
2937
|
+
types$1.logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* Push a message that must be processed in complete isolation.
|
|
2941
|
+
* Clears any pending messages and ensures this message is never batched with others.
|
|
2942
|
+
* Used for special commands that require dedicated processing.
|
|
2943
|
+
*/
|
|
2944
|
+
pushIsolateAndClear(message, mode) {
|
|
2945
|
+
if (this.closed) {
|
|
2946
|
+
throw new Error("Cannot push to closed queue");
|
|
2947
|
+
}
|
|
2948
|
+
const modeHash = this.modeHasher(mode);
|
|
2949
|
+
types$1.logger.debug(`[MessageQueue2] pushIsolateAndClear() called with mode hash: ${modeHash} - clearing ${this.queue.length} pending messages`);
|
|
2950
|
+
this.queue = [];
|
|
2951
|
+
this.queue.push({
|
|
2952
|
+
message,
|
|
2953
|
+
mode,
|
|
2954
|
+
modeHash,
|
|
2955
|
+
isolate: true
|
|
2956
|
+
});
|
|
2957
|
+
if (this.onMessageHandler) {
|
|
2958
|
+
this.onMessageHandler(message, mode);
|
|
2959
|
+
}
|
|
2960
|
+
if (this.waiter) {
|
|
2961
|
+
types$1.logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
|
|
2962
|
+
const waiter = this.waiter;
|
|
2963
|
+
this.waiter = null;
|
|
2964
|
+
waiter(true);
|
|
2965
|
+
}
|
|
2966
|
+
types$1.logger.debug(`[MessageQueue2] pushIsolateAndClear() completed. Queue size: ${this.queue.length}`);
|
|
2967
|
+
}
|
|
2706
2968
|
/**
|
|
2707
2969
|
* Push a message to the beginning of the queue with a mode.
|
|
2708
2970
|
*/
|
|
@@ -2715,7 +2977,8 @@ class MessageQueue2 {
|
|
|
2715
2977
|
this.queue.unshift({
|
|
2716
2978
|
message,
|
|
2717
2979
|
mode,
|
|
2718
|
-
modeHash
|
|
2980
|
+
modeHash,
|
|
2981
|
+
isolate: false
|
|
2719
2982
|
});
|
|
2720
2983
|
if (this.onMessageHandler) {
|
|
2721
2984
|
this.onMessageHandler(message, mode);
|
|
@@ -2779,7 +3042,7 @@ class MessageQueue2 {
|
|
|
2779
3042
|
return this.collectBatch();
|
|
2780
3043
|
}
|
|
2781
3044
|
/**
|
|
2782
|
-
* Collect a batch of messages with the same mode
|
|
3045
|
+
* Collect a batch of messages with the same mode, respecting isolation requirements
|
|
2783
3046
|
*/
|
|
2784
3047
|
collectBatch() {
|
|
2785
3048
|
if (this.queue.length === 0) {
|
|
@@ -2789,12 +3052,18 @@ class MessageQueue2 {
|
|
|
2789
3052
|
const sameModeMessages = [];
|
|
2790
3053
|
let mode = firstItem.mode;
|
|
2791
3054
|
const targetModeHash = firstItem.modeHash;
|
|
2792
|
-
|
|
3055
|
+
if (firstItem.isolate) {
|
|
2793
3056
|
const item = this.queue.shift();
|
|
2794
3057
|
sameModeMessages.push(item.message);
|
|
3058
|
+
types$1.logger.debug(`[MessageQueue2] Collected isolated message with mode hash: ${targetModeHash}`);
|
|
3059
|
+
} else {
|
|
3060
|
+
while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash && !this.queue[0].isolate) {
|
|
3061
|
+
const item = this.queue.shift();
|
|
3062
|
+
sameModeMessages.push(item.message);
|
|
3063
|
+
}
|
|
3064
|
+
types$1.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
|
|
2795
3065
|
}
|
|
2796
3066
|
const combinedMessage = sameModeMessages.join("\n");
|
|
2797
|
-
types$1.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
|
|
2798
3067
|
return {
|
|
2799
3068
|
message: combinedMessage,
|
|
2800
3069
|
mode
|
|
@@ -2959,8 +3228,13 @@ function startCaffeinate() {
|
|
|
2959
3228
|
return false;
|
|
2960
3229
|
}
|
|
2961
3230
|
}
|
|
3231
|
+
let isStopping = false;
|
|
2962
3232
|
function stopCaffeinate() {
|
|
3233
|
+
if (isStopping) {
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
2963
3236
|
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3237
|
+
isStopping = true;
|
|
2964
3238
|
types$1.logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
|
|
2965
3239
|
try {
|
|
2966
3240
|
caffeinateProcess.kill("SIGTERM");
|
|
@@ -2970,9 +3244,11 @@ function stopCaffeinate() {
|
|
|
2970
3244
|
caffeinateProcess.kill("SIGKILL");
|
|
2971
3245
|
}
|
|
2972
3246
|
caffeinateProcess = null;
|
|
3247
|
+
isStopping = false;
|
|
2973
3248
|
}, 1e3);
|
|
2974
3249
|
} catch (error) {
|
|
2975
3250
|
types$1.logger.debug("[caffeinate] Error stopping caffeinate:", error);
|
|
3251
|
+
isStopping = false;
|
|
2976
3252
|
}
|
|
2977
3253
|
}
|
|
2978
3254
|
}
|
|
@@ -3047,97 +3323,505 @@ function extractSDKMetadataAsync(onComplete) {
|
|
|
3047
3323
|
});
|
|
3048
3324
|
}
|
|
3049
3325
|
|
|
3050
|
-
async function
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
options.startingMode = "remote";
|
|
3056
|
-
}
|
|
3057
|
-
const api = new types$1.ApiClient(credentials.token, credentials.secret);
|
|
3058
|
-
let state = {};
|
|
3059
|
-
const settings = await readSettings() || { };
|
|
3060
|
-
let metadata = {
|
|
3061
|
-
path: workingDirectory,
|
|
3062
|
-
host: os.hostname(),
|
|
3063
|
-
version: packageJson.version,
|
|
3064
|
-
os: os.platform(),
|
|
3065
|
-
machineId: settings.machineId,
|
|
3066
|
-
homeDir: os.homedir()
|
|
3067
|
-
};
|
|
3068
|
-
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
3069
|
-
types$1.logger.debug(`Session created: ${response.id}`);
|
|
3070
|
-
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
3071
|
-
types$1.logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
3072
|
-
try {
|
|
3073
|
-
api.session(response).updateMetadata((currentMetadata) => ({
|
|
3074
|
-
...currentMetadata,
|
|
3075
|
-
tools: sdkMetadata.tools,
|
|
3076
|
-
slashCommands: sdkMetadata.slashCommands
|
|
3077
|
-
}));
|
|
3078
|
-
types$1.logger.debug("[start] Session metadata updated with SDK capabilities");
|
|
3079
|
-
} catch (error) {
|
|
3080
|
-
types$1.logger.debug("[start] Failed to update session metadata:", error);
|
|
3326
|
+
async function isDaemonRunning() {
|
|
3327
|
+
try {
|
|
3328
|
+
const state = await getDaemonState();
|
|
3329
|
+
if (!state) {
|
|
3330
|
+
return false;
|
|
3081
3331
|
}
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3332
|
+
const isRunning = await isDaemonProcessRunning(state.pid);
|
|
3333
|
+
if (!isRunning) {
|
|
3334
|
+
types$1.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3335
|
+
await cleanupDaemonState();
|
|
3336
|
+
return false;
|
|
3337
|
+
}
|
|
3338
|
+
return true;
|
|
3339
|
+
} catch (error) {
|
|
3340
|
+
types$1.logger.debug("[DAEMON RUN] Error checking daemon status", error);
|
|
3341
|
+
return false;
|
|
3085
3342
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
}));
|
|
3094
|
-
const caffeinateStarted = startCaffeinate();
|
|
3095
|
-
if (caffeinateStarted) {
|
|
3096
|
-
types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
|
|
3343
|
+
}
|
|
3344
|
+
async function getDaemonState() {
|
|
3345
|
+
try {
|
|
3346
|
+
return await readDaemonState();
|
|
3347
|
+
} catch (error) {
|
|
3348
|
+
types$1.logger.debug("[DAEMON RUN] Error reading daemon metadata", error);
|
|
3349
|
+
return null;
|
|
3097
3350
|
}
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3351
|
+
}
|
|
3352
|
+
async function isDaemonProcessRunning(pid) {
|
|
3353
|
+
try {
|
|
3354
|
+
process.kill(pid, 0);
|
|
3355
|
+
return true;
|
|
3356
|
+
} catch {
|
|
3357
|
+
return false;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
async function cleanupDaemonState() {
|
|
3361
|
+
try {
|
|
3362
|
+
await clearDaemonState();
|
|
3363
|
+
types$1.logger.debug("[DAEMON RUN] Daemon state file removed");
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
types$1.logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
function findAllHappyProcesses() {
|
|
3369
|
+
try {
|
|
3370
|
+
const output = node_child_process.execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3371
|
+
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3372
|
+
const allProcesses = [];
|
|
3373
|
+
for (const line of lines) {
|
|
3374
|
+
const parts = line.trim().split(/\s+/);
|
|
3375
|
+
if (parts.length < 11) continue;
|
|
3376
|
+
const pid = parseInt(parts[1]);
|
|
3377
|
+
const command = parts.slice(10).join(" ");
|
|
3378
|
+
let type = "unknown";
|
|
3379
|
+
if (pid === process.pid) {
|
|
3380
|
+
type = "current";
|
|
3381
|
+
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3382
|
+
type = "daemon";
|
|
3383
|
+
} else if (command.includes("--started-by daemon")) {
|
|
3384
|
+
type = "daemon-spawned-session";
|
|
3385
|
+
} else if (command.includes("doctor")) {
|
|
3386
|
+
type = "doctor";
|
|
3115
3387
|
} else {
|
|
3116
|
-
|
|
3388
|
+
type = "user-session";
|
|
3117
3389
|
}
|
|
3118
|
-
|
|
3119
|
-
types$1.logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
|
|
3390
|
+
allProcesses.push({ pid, command, type });
|
|
3120
3391
|
}
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3392
|
+
try {
|
|
3393
|
+
const devOutput = node_child_process.execSync('ps aux | grep -E "(tsx.*src/index.ts|yarn.*tsx)" | grep -v grep', { encoding: "utf8" });
|
|
3394
|
+
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3395
|
+
for (const line of devLines) {
|
|
3396
|
+
const parts = line.trim().split(/\s+/);
|
|
3397
|
+
if (parts.length < 11) continue;
|
|
3398
|
+
const pid = parseInt(parts[1]);
|
|
3399
|
+
const command = parts.slice(10).join(" ");
|
|
3400
|
+
let workingDir = "";
|
|
3401
|
+
try {
|
|
3402
|
+
const pwdOutput = node_child_process.execSync(`pwdx ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
3403
|
+
workingDir = pwdOutput.replace(`${pid}:`, "").trim();
|
|
3404
|
+
} catch {
|
|
3405
|
+
}
|
|
3406
|
+
if (workingDir.includes("happy-cli")) {
|
|
3407
|
+
allProcesses.push({ pid, command, type: "dev-session" });
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
} catch {
|
|
3128
3411
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3412
|
+
return allProcesses;
|
|
3413
|
+
} catch (error) {
|
|
3414
|
+
return [];
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
function findRunawayHappyProcesses() {
|
|
3418
|
+
try {
|
|
3419
|
+
const output = node_child_process.execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3420
|
+
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3421
|
+
const processes = [];
|
|
3422
|
+
for (const line of lines) {
|
|
3423
|
+
const parts = line.trim().split(/\s+/);
|
|
3424
|
+
if (parts.length < 11) continue;
|
|
3425
|
+
const pid = parseInt(parts[1]);
|
|
3426
|
+
const command = parts.slice(10).join(" ");
|
|
3427
|
+
if (pid === process.pid) continue;
|
|
3428
|
+
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3429
|
+
processes.push({ pid, command });
|
|
3430
|
+
}
|
|
3136
3431
|
}
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3432
|
+
return processes;
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
return [];
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
async function killRunawayHappyProcesses() {
|
|
3438
|
+
const runawayProcesses = findRunawayHappyProcesses();
|
|
3439
|
+
const errors = [];
|
|
3440
|
+
let killed = 0;
|
|
3441
|
+
for (const { pid, command } of runawayProcesses) {
|
|
3442
|
+
try {
|
|
3443
|
+
process.kill(pid, "SIGTERM");
|
|
3444
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3445
|
+
try {
|
|
3446
|
+
process.kill(pid, 0);
|
|
3447
|
+
console.log(`Process PID ${pid} ignored SIGTERM, using SIGKILL`);
|
|
3448
|
+
process.kill(pid, "SIGKILL");
|
|
3449
|
+
} catch {
|
|
3450
|
+
}
|
|
3451
|
+
killed++;
|
|
3452
|
+
console.log(`Killed runaway process PID ${pid}: ${command}`);
|
|
3453
|
+
} catch (error) {
|
|
3454
|
+
errors.push({ pid, error: error.message });
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
return { killed, errors };
|
|
3458
|
+
}
|
|
3459
|
+
async function stopDaemon() {
|
|
3460
|
+
try {
|
|
3461
|
+
stopCaffeinate();
|
|
3462
|
+
types$1.logger.debug("Stopped sleep prevention");
|
|
3463
|
+
const state = await getDaemonState();
|
|
3464
|
+
if (!state) {
|
|
3465
|
+
types$1.logger.debug("No daemon state found");
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
types$1.logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3469
|
+
try {
|
|
3470
|
+
const { stopDaemonHttp } = await Promise.resolve().then(function () { return controlClient; });
|
|
3471
|
+
await stopDaemonHttp();
|
|
3472
|
+
await waitForProcessDeath(state.pid, 5e3);
|
|
3473
|
+
types$1.logger.debug("Daemon stopped gracefully via HTTP");
|
|
3474
|
+
return;
|
|
3475
|
+
} catch (error) {
|
|
3476
|
+
types$1.logger.debug("HTTP stop failed, will force kill", error);
|
|
3477
|
+
}
|
|
3478
|
+
try {
|
|
3479
|
+
process.kill(state.pid, "SIGKILL");
|
|
3480
|
+
types$1.logger.debug("Force killed daemon");
|
|
3481
|
+
} catch (error) {
|
|
3482
|
+
types$1.logger.debug("Daemon already dead");
|
|
3483
|
+
}
|
|
3484
|
+
} catch (error) {
|
|
3485
|
+
types$1.logger.debug("Error stopping daemon", error);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
async function waitForProcessDeath(pid, timeout) {
|
|
3489
|
+
const start = Date.now();
|
|
3490
|
+
while (Date.now() - start < timeout) {
|
|
3491
|
+
try {
|
|
3492
|
+
process.kill(pid, 0);
|
|
3493
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3494
|
+
} catch {
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
throw new Error("Process did not die within timeout");
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
var utils = /*#__PURE__*/Object.freeze({
|
|
3502
|
+
__proto__: null,
|
|
3503
|
+
cleanupDaemonState: cleanupDaemonState,
|
|
3504
|
+
findAllHappyProcesses: findAllHappyProcesses,
|
|
3505
|
+
findRunawayHappyProcesses: findRunawayHappyProcesses,
|
|
3506
|
+
getDaemonState: getDaemonState,
|
|
3507
|
+
isDaemonRunning: isDaemonRunning,
|
|
3508
|
+
killRunawayHappyProcesses: killRunawayHappyProcesses,
|
|
3509
|
+
stopDaemon: stopDaemon
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
function getEnvironmentInfo() {
|
|
3513
|
+
return {
|
|
3514
|
+
PWD: process.env.PWD,
|
|
3515
|
+
HAPPY_HOME_DIR: process.env.HAPPY_HOME_DIR,
|
|
3516
|
+
HAPPY_SERVER_URL: process.env.HAPPY_SERVER_URL,
|
|
3517
|
+
HAPPY_PROJECT_ROOT: process.env.HAPPY_PROJECT_ROOT,
|
|
3518
|
+
DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING: process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING,
|
|
3519
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
3520
|
+
DEBUG: process.env.DEBUG,
|
|
3521
|
+
workingDirectory: process.cwd(),
|
|
3522
|
+
processArgv: process.argv,
|
|
3523
|
+
happyDir: types$1.configuration?.happyHomeDir,
|
|
3524
|
+
serverUrl: types$1.configuration?.serverUrl,
|
|
3525
|
+
logsDir: types$1.configuration?.logsDir
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
function getLogFiles(logDir) {
|
|
3529
|
+
if (!node_fs.existsSync(logDir)) {
|
|
3530
|
+
return [];
|
|
3531
|
+
}
|
|
3532
|
+
try {
|
|
3533
|
+
return node_fs.readdirSync(logDir).filter((file) => file.endsWith(".log")).map((file) => {
|
|
3534
|
+
const path = node_path.join(logDir, file);
|
|
3535
|
+
const stats = node_fs.statSync(path);
|
|
3536
|
+
return { file, path, modified: stats.mtime };
|
|
3537
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, 10);
|
|
3538
|
+
} catch {
|
|
3539
|
+
return [];
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
async function runDoctorCommand() {
|
|
3543
|
+
console.log(chalk.bold.cyan("\n\u{1FA7A} Happy CLI Doctor\n"));
|
|
3544
|
+
console.log(chalk.bold("\u{1F4CB} Basic Information"));
|
|
3545
|
+
console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
|
|
3546
|
+
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3547
|
+
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3548
|
+
console.log("");
|
|
3549
|
+
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3550
|
+
console.log(`Happy Home: ${chalk.blue(types$1.configuration.happyHomeDir)}`);
|
|
3551
|
+
console.log(`Server URL: ${chalk.blue(types$1.configuration.serverUrl)}`);
|
|
3552
|
+
console.log(`Logs Dir: ${chalk.blue(types$1.configuration.logsDir)}`);
|
|
3553
|
+
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3554
|
+
const env = getEnvironmentInfo();
|
|
3555
|
+
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3556
|
+
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3557
|
+
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3558
|
+
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3559
|
+
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3560
|
+
try {
|
|
3561
|
+
const settings = await readSettings();
|
|
3562
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3563
|
+
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3564
|
+
} catch (error) {
|
|
3565
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3566
|
+
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3567
|
+
}
|
|
3568
|
+
console.log(chalk.bold("\n\u{1F510} Authentication"));
|
|
3569
|
+
try {
|
|
3570
|
+
const credentials = await readCredentials();
|
|
3571
|
+
if (credentials) {
|
|
3572
|
+
console.log(chalk.green("\u2713 Authenticated (credentials found)"));
|
|
3573
|
+
} else {
|
|
3574
|
+
console.log(chalk.yellow("\u26A0\uFE0F Not authenticated (no credentials)"));
|
|
3575
|
+
}
|
|
3576
|
+
} catch (error) {
|
|
3577
|
+
console.log(chalk.red("\u274C Error reading credentials"));
|
|
3578
|
+
}
|
|
3579
|
+
console.log(chalk.bold("\n\u{1F916} Daemon Status"));
|
|
3580
|
+
try {
|
|
3581
|
+
const isRunning = await isDaemonRunning();
|
|
3582
|
+
const state = await getDaemonState();
|
|
3583
|
+
if (isRunning && state) {
|
|
3584
|
+
console.log(chalk.green("\u2713 Daemon is running"));
|
|
3585
|
+
console.log(` PID: ${state.pid}`);
|
|
3586
|
+
console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
|
|
3587
|
+
console.log(` CLI Version: ${state.startedWithCliVersion}`);
|
|
3588
|
+
if (state.httpPort) {
|
|
3589
|
+
console.log(` HTTP Port: ${state.httpPort}`);
|
|
3590
|
+
}
|
|
3591
|
+
} else if (state && !isRunning) {
|
|
3592
|
+
console.log(chalk.yellow("\u26A0\uFE0F Daemon state exists but process not running (stale)"));
|
|
3593
|
+
} else {
|
|
3594
|
+
console.log(chalk.red("\u274C Daemon is not running"));
|
|
3595
|
+
}
|
|
3596
|
+
if (state) {
|
|
3597
|
+
console.log(chalk.bold("\n\u{1F4C4} Daemon State:"));
|
|
3598
|
+
console.log(chalk.blue(`Location: ${types$1.configuration.daemonStateFile}`));
|
|
3599
|
+
console.log(chalk.gray(JSON.stringify(state, null, 2)));
|
|
3600
|
+
}
|
|
3601
|
+
const allProcesses = findAllHappyProcesses();
|
|
3602
|
+
if (allProcesses.length > 0) {
|
|
3603
|
+
console.log(chalk.bold("\n\u{1F50D} All Happy CLI Processes"));
|
|
3604
|
+
const grouped = allProcesses.reduce((groups, process2) => {
|
|
3605
|
+
if (!groups[process2.type]) groups[process2.type] = [];
|
|
3606
|
+
groups[process2.type].push(process2);
|
|
3607
|
+
return groups;
|
|
3608
|
+
}, {});
|
|
3609
|
+
Object.entries(grouped).forEach(([type, processes]) => {
|
|
3610
|
+
const typeLabels = {
|
|
3611
|
+
"current": "\u{1F4CD} Current Process",
|
|
3612
|
+
"daemon": "\u{1F916} Daemon",
|
|
3613
|
+
"daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
|
|
3614
|
+
"user-session": "\u{1F464} User Sessions",
|
|
3615
|
+
"dev-daemon": "\u{1F6E0}\uFE0F Dev Daemon",
|
|
3616
|
+
"dev-session": "\u{1F6E0}\uFE0F Dev Sessions",
|
|
3617
|
+
"dev-doctor": "\u{1F6E0}\uFE0F Dev Doctor",
|
|
3618
|
+
"dev-related": "\u{1F6E0}\uFE0F Dev Related",
|
|
3619
|
+
"doctor": "\u{1FA7A} Doctor",
|
|
3620
|
+
"unknown": "\u2753 Unknown"
|
|
3621
|
+
};
|
|
3622
|
+
console.log(chalk.blue(`
|
|
3623
|
+
${typeLabels[type] || type}:`));
|
|
3624
|
+
processes.forEach(({ pid, command }) => {
|
|
3625
|
+
const color = type === "current" ? chalk.green : type.startsWith("dev") ? chalk.cyan : type.includes("daemon") ? chalk.blue : chalk.gray;
|
|
3626
|
+
console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
3627
|
+
});
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
const runawayProcesses = findRunawayHappyProcesses();
|
|
3631
|
+
if (runawayProcesses.length > 0) {
|
|
3632
|
+
console.log(chalk.bold("\n\u{1F6A8} Runaway Happy processes detected"));
|
|
3633
|
+
console.log(chalk.gray("These processes were left running after daemon crashes."));
|
|
3634
|
+
runawayProcesses.forEach(({ pid, command }) => {
|
|
3635
|
+
console.log(` ${chalk.yellow(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
3636
|
+
});
|
|
3637
|
+
console.log(chalk.blue("\nTo clean up: happy daemon kill-runaway"));
|
|
3638
|
+
}
|
|
3639
|
+
if (allProcesses.length > 1) {
|
|
3640
|
+
console.log(chalk.bold("\n\u{1F4A1} Process Management"));
|
|
3641
|
+
console.log(chalk.gray("To kill runaway processes: happy daemon kill-runaway"));
|
|
3642
|
+
}
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
console.log(chalk.red("\u274C Error checking daemon status"));
|
|
3645
|
+
}
|
|
3646
|
+
console.log(chalk.bold("\n\u{1F4DD} Log Files"));
|
|
3647
|
+
const mainLogs = getLogFiles(types$1.configuration.logsDir);
|
|
3648
|
+
if (mainLogs.length > 0) {
|
|
3649
|
+
console.log(chalk.blue("\nMain Logs:"));
|
|
3650
|
+
mainLogs.forEach(({ file, path, modified }) => {
|
|
3651
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3652
|
+
console.log(chalk.gray(` ${path}`));
|
|
3653
|
+
});
|
|
3654
|
+
} else {
|
|
3655
|
+
console.log(chalk.yellow("No main log files found"));
|
|
3656
|
+
}
|
|
3657
|
+
const daemonLogs = mainLogs.filter(({ file }) => file.includes("daemon"));
|
|
3658
|
+
if (daemonLogs.length > 0) {
|
|
3659
|
+
console.log(chalk.blue("\nDaemon Logs:"));
|
|
3660
|
+
daemonLogs.forEach(({ file, path, modified }) => {
|
|
3661
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3662
|
+
console.log(chalk.gray(` ${path}`));
|
|
3663
|
+
});
|
|
3664
|
+
} else {
|
|
3665
|
+
console.log(chalk.yellow("No daemon log files found"));
|
|
3666
|
+
}
|
|
3667
|
+
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
3668
|
+
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
3669
|
+
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
3670
|
+
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
async function daemonPost(path, body) {
|
|
3674
|
+
const state = await getDaemonState();
|
|
3675
|
+
if (!state?.httpPort) {
|
|
3676
|
+
throw new Error("No daemon running");
|
|
3677
|
+
}
|
|
3678
|
+
try {
|
|
3679
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
3680
|
+
method: "POST",
|
|
3681
|
+
headers: { "Content-Type": "application/json" },
|
|
3682
|
+
body: JSON.stringify(body || {}),
|
|
3683
|
+
signal: AbortSignal.timeout(5e3)
|
|
3684
|
+
});
|
|
3685
|
+
if (!response.ok) {
|
|
3686
|
+
throw new Error(`HTTP ${response.status}`);
|
|
3687
|
+
}
|
|
3688
|
+
return await response.json();
|
|
3689
|
+
} catch (error) {
|
|
3690
|
+
types$1.logger.debug(`[CONTROL CLIENT] Request failed: ${path}`, error);
|
|
3691
|
+
throw error;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
3695
|
+
await daemonPost("/session-started", {
|
|
3696
|
+
sessionId,
|
|
3697
|
+
metadata
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
async function listDaemonSessions() {
|
|
3701
|
+
const result = await daemonPost("/list");
|
|
3702
|
+
return result.children || [];
|
|
3703
|
+
}
|
|
3704
|
+
async function stopDaemonSession(sessionId) {
|
|
3705
|
+
const result = await daemonPost("/stop-session", { sessionId });
|
|
3706
|
+
return result.success || false;
|
|
3707
|
+
}
|
|
3708
|
+
async function stopDaemonHttp() {
|
|
3709
|
+
await daemonPost("/stop");
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
var controlClient = /*#__PURE__*/Object.freeze({
|
|
3713
|
+
__proto__: null,
|
|
3714
|
+
listDaemonSessions: listDaemonSessions,
|
|
3715
|
+
notifyDaemonSessionStarted: notifyDaemonSessionStarted,
|
|
3716
|
+
stopDaemonHttp: stopDaemonHttp,
|
|
3717
|
+
stopDaemonSession: stopDaemonSession
|
|
3718
|
+
});
|
|
3719
|
+
|
|
3720
|
+
async function start(credentials, options = {}) {
|
|
3721
|
+
const workingDirectory = process.cwd();
|
|
3722
|
+
const sessionTag = node_crypto.randomUUID();
|
|
3723
|
+
types$1.logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
|
|
3724
|
+
types$1.logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
|
|
3725
|
+
if (options.startedBy === "daemon" && options.startingMode === "local") {
|
|
3726
|
+
types$1.logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
3727
|
+
options.startingMode = "remote";
|
|
3728
|
+
}
|
|
3729
|
+
const api = new types$1.ApiClient(credentials.token, credentials.secret);
|
|
3730
|
+
let state = {};
|
|
3731
|
+
const settings = await readSettings();
|
|
3732
|
+
const machineId = settings?.machineId || "unknown";
|
|
3733
|
+
types$1.logger.debug(`Using machineId: ${machineId}`);
|
|
3734
|
+
let metadata = {
|
|
3735
|
+
path: workingDirectory,
|
|
3736
|
+
host: os.hostname(),
|
|
3737
|
+
version: packageJson.version,
|
|
3738
|
+
os: os.platform(),
|
|
3739
|
+
machineId,
|
|
3740
|
+
homeDir: os.homedir(),
|
|
3741
|
+
happyHomeDir: types$1.configuration.happyHomeDir,
|
|
3742
|
+
startedFromDaemon: options.startedBy === "daemon",
|
|
3743
|
+
hostPid: process.pid,
|
|
3744
|
+
startedBy: options.startedBy || "terminal"
|
|
3745
|
+
};
|
|
3746
|
+
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
3747
|
+
types$1.logger.debug(`Session created: ${response.id}`);
|
|
3748
|
+
try {
|
|
3749
|
+
const daemonState = await getDaemonState();
|
|
3750
|
+
if (daemonState?.httpPort) {
|
|
3751
|
+
await notifyDaemonSessionStarted(response.id, metadata);
|
|
3752
|
+
types$1.logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
3753
|
+
}
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
types$1.logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
3756
|
+
}
|
|
3757
|
+
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
3758
|
+
types$1.logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
3759
|
+
try {
|
|
3760
|
+
api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
|
|
3761
|
+
...currentMetadata,
|
|
3762
|
+
tools: sdkMetadata.tools,
|
|
3763
|
+
slashCommands: sdkMetadata.slashCommands
|
|
3764
|
+
}));
|
|
3765
|
+
types$1.logger.debug("[start] Session metadata updated with SDK capabilities");
|
|
3766
|
+
} catch (error) {
|
|
3767
|
+
types$1.logger.debug("[start] Failed to update session metadata:", error);
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
const session = api.sessionSyncClient(response);
|
|
3771
|
+
const logPath = await types$1.logger.logFilePathPromise;
|
|
3772
|
+
types$1.logger.infoDeveloper(`Session: ${response.id}`);
|
|
3773
|
+
types$1.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
3774
|
+
session.updateAgentState((currentState) => ({
|
|
3775
|
+
...currentState,
|
|
3776
|
+
controlledByUser: options.startingMode !== "remote"
|
|
3777
|
+
}));
|
|
3778
|
+
const caffeinateStarted = startCaffeinate();
|
|
3779
|
+
if (caffeinateStarted) {
|
|
3780
|
+
types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
|
|
3781
|
+
}
|
|
3782
|
+
const messageQueue = new MessageQueue2((mode) => hashObject(mode));
|
|
3783
|
+
registerHandlers(session);
|
|
3784
|
+
let currentPermissionMode = options.permissionMode;
|
|
3785
|
+
let currentModel = options.model;
|
|
3786
|
+
let currentFallbackModel = void 0;
|
|
3787
|
+
let currentCustomSystemPrompt = void 0;
|
|
3788
|
+
let currentAppendSystemPrompt = void 0;
|
|
3789
|
+
let currentAllowedTools = void 0;
|
|
3790
|
+
let currentDisallowedTools = void 0;
|
|
3791
|
+
session.onUserMessage((message) => {
|
|
3792
|
+
let messagePermissionMode = currentPermissionMode;
|
|
3793
|
+
if (message.meta?.permissionMode) {
|
|
3794
|
+
const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
|
|
3795
|
+
if (validModes.includes(message.meta.permissionMode)) {
|
|
3796
|
+
messagePermissionMode = message.meta.permissionMode;
|
|
3797
|
+
currentPermissionMode = messagePermissionMode;
|
|
3798
|
+
types$1.logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
3799
|
+
} else {
|
|
3800
|
+
types$1.logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
3801
|
+
}
|
|
3802
|
+
} else {
|
|
3803
|
+
types$1.logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
|
|
3804
|
+
}
|
|
3805
|
+
let messageModel = currentModel;
|
|
3806
|
+
if (message.meta?.hasOwnProperty("model")) {
|
|
3807
|
+
messageModel = message.meta.model || void 0;
|
|
3808
|
+
currentModel = messageModel;
|
|
3809
|
+
types$1.logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
|
|
3810
|
+
} else {
|
|
3811
|
+
types$1.logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
|
|
3812
|
+
}
|
|
3813
|
+
let messageCustomSystemPrompt = currentCustomSystemPrompt;
|
|
3814
|
+
if (message.meta?.hasOwnProperty("customSystemPrompt")) {
|
|
3815
|
+
messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
|
|
3816
|
+
currentCustomSystemPrompt = messageCustomSystemPrompt;
|
|
3817
|
+
types$1.logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
|
|
3818
|
+
} else {
|
|
3819
|
+
types$1.logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
|
|
3820
|
+
}
|
|
3821
|
+
let messageFallbackModel = currentFallbackModel;
|
|
3822
|
+
if (message.meta?.hasOwnProperty("fallbackModel")) {
|
|
3823
|
+
messageFallbackModel = message.meta.fallbackModel || void 0;
|
|
3824
|
+
currentFallbackModel = messageFallbackModel;
|
|
3141
3825
|
types$1.logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
|
|
3142
3826
|
} else {
|
|
3143
3827
|
types$1.logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
|
|
@@ -3166,6 +3850,37 @@ async function start(credentials, options = {}) {
|
|
|
3166
3850
|
} else {
|
|
3167
3851
|
types$1.logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
|
|
3168
3852
|
}
|
|
3853
|
+
const specialCommand = parseSpecialCommand(message.content.text);
|
|
3854
|
+
if (specialCommand.type === "compact") {
|
|
3855
|
+
types$1.logger.debug("[start] Detected /compact command");
|
|
3856
|
+
const enhancedMode2 = {
|
|
3857
|
+
permissionMode: messagePermissionMode || "default",
|
|
3858
|
+
model: messageModel,
|
|
3859
|
+
fallbackModel: messageFallbackModel,
|
|
3860
|
+
customSystemPrompt: messageCustomSystemPrompt,
|
|
3861
|
+
appendSystemPrompt: messageAppendSystemPrompt,
|
|
3862
|
+
allowedTools: messageAllowedTools,
|
|
3863
|
+
disallowedTools: messageDisallowedTools
|
|
3864
|
+
};
|
|
3865
|
+
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
3866
|
+
types$1.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
if (specialCommand.type === "clear") {
|
|
3870
|
+
types$1.logger.debug("[start] Detected /clear command");
|
|
3871
|
+
const enhancedMode2 = {
|
|
3872
|
+
permissionMode: messagePermissionMode || "default",
|
|
3873
|
+
model: messageModel,
|
|
3874
|
+
fallbackModel: messageFallbackModel,
|
|
3875
|
+
customSystemPrompt: messageCustomSystemPrompt,
|
|
3876
|
+
appendSystemPrompt: messageAppendSystemPrompt,
|
|
3877
|
+
allowedTools: messageAllowedTools,
|
|
3878
|
+
disallowedTools: messageDisallowedTools
|
|
3879
|
+
};
|
|
3880
|
+
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
3881
|
+
types$1.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3169
3884
|
const enhancedMode = {
|
|
3170
3885
|
permissionMode: messagePermissionMode || "default",
|
|
3171
3886
|
model: messageModel,
|
|
@@ -3178,6 +3893,32 @@ async function start(credentials, options = {}) {
|
|
|
3178
3893
|
messageQueue.push(message.content.text, enhancedMode);
|
|
3179
3894
|
types$1.logger.debugLargeJson("User message pushed to queue:", message);
|
|
3180
3895
|
});
|
|
3896
|
+
const cleanup = async () => {
|
|
3897
|
+
types$1.logger.debug("[START] Received termination signal, cleaning up...");
|
|
3898
|
+
try {
|
|
3899
|
+
if (session) {
|
|
3900
|
+
session.sendSessionDeath();
|
|
3901
|
+
await session.flush();
|
|
3902
|
+
await session.close();
|
|
3903
|
+
}
|
|
3904
|
+
stopCaffeinate();
|
|
3905
|
+
types$1.logger.debug("[START] Cleanup complete, exiting");
|
|
3906
|
+
process.exit(0);
|
|
3907
|
+
} catch (error) {
|
|
3908
|
+
types$1.logger.debug("[START] Error during cleanup:", error);
|
|
3909
|
+
process.exit(1);
|
|
3910
|
+
}
|
|
3911
|
+
};
|
|
3912
|
+
process.on("SIGTERM", cleanup);
|
|
3913
|
+
process.on("SIGINT", cleanup);
|
|
3914
|
+
process.on("uncaughtException", (error) => {
|
|
3915
|
+
types$1.logger.debug("[START] Uncaught exception:", error);
|
|
3916
|
+
cleanup();
|
|
3917
|
+
});
|
|
3918
|
+
process.on("unhandledRejection", (reason) => {
|
|
3919
|
+
types$1.logger.debug("[START] Unhandled rejection:", reason);
|
|
3920
|
+
cleanup();
|
|
3921
|
+
});
|
|
3181
3922
|
await loop({
|
|
3182
3923
|
path: workingDirectory,
|
|
3183
3924
|
model: options.model,
|
|
@@ -3192,6 +3933,8 @@ async function start(credentials, options = {}) {
|
|
|
3192
3933
|
controlledByUser: newMode === "local"
|
|
3193
3934
|
}));
|
|
3194
3935
|
},
|
|
3936
|
+
onSessionReady: (sessionInstance) => {
|
|
3937
|
+
},
|
|
3195
3938
|
mcpServers: {},
|
|
3196
3939
|
session,
|
|
3197
3940
|
claudeEnvVars: options.claudeEnvVars,
|
|
@@ -3285,10 +4028,14 @@ async function doAuth() {
|
|
|
3285
4028
|
const secret = new Uint8Array(node_crypto.randomBytes(32));
|
|
3286
4029
|
const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
|
|
3287
4030
|
try {
|
|
4031
|
+
console.log(`[AUTH DEBUG] Sending auth request to: ${types$1.configuration.serverUrl}/v1/auth/request`);
|
|
4032
|
+
console.log(`[AUTH DEBUG] Public key: ${types$1.encodeBase64(keypair.publicKey).substring(0, 20)}...`);
|
|
3288
4033
|
await axios.post(`${types$1.configuration.serverUrl}/v1/auth/request`, {
|
|
3289
4034
|
publicKey: types$1.encodeBase64(keypair.publicKey)
|
|
3290
4035
|
});
|
|
4036
|
+
console.log(`[AUTH DEBUG] Auth request sent successfully`);
|
|
3291
4037
|
} catch (error) {
|
|
4038
|
+
console.log(`[AUTH DEBUG] Failed to send auth request:`, error);
|
|
3292
4039
|
console.log("Failed to create authentication request, please try again later.");
|
|
3293
4040
|
return null;
|
|
3294
4041
|
}
|
|
@@ -3405,550 +4152,375 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
3405
4152
|
}
|
|
3406
4153
|
return decrypted;
|
|
3407
4154
|
}
|
|
4155
|
+
async function authAndSetupMachineIfNeeded() {
|
|
4156
|
+
types$1.logger.debug("[AUTH] Starting auth and machine setup...");
|
|
4157
|
+
let credentials = await readCredentials();
|
|
4158
|
+
if (!credentials) {
|
|
4159
|
+
types$1.logger.debug("[AUTH] No credentials found, starting authentication flow...");
|
|
4160
|
+
const authResult = await doAuth();
|
|
4161
|
+
if (!authResult) {
|
|
4162
|
+
throw new Error("Authentication failed or was cancelled");
|
|
4163
|
+
}
|
|
4164
|
+
credentials = authResult;
|
|
4165
|
+
} else {
|
|
4166
|
+
types$1.logger.debug("[AUTH] Using existing credentials");
|
|
4167
|
+
}
|
|
4168
|
+
const settings = await updateSettings(async (s) => {
|
|
4169
|
+
if (!s.machineId) {
|
|
4170
|
+
return {
|
|
4171
|
+
...s,
|
|
4172
|
+
machineId: node_crypto.randomUUID()
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
return s;
|
|
4176
|
+
});
|
|
4177
|
+
types$1.logger.debug(`[AUTH] Machine ID: ${settings.machineId}`);
|
|
4178
|
+
return { credentials, machineId: settings.machineId };
|
|
4179
|
+
}
|
|
3408
4180
|
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
this.secret = secret;
|
|
3421
|
-
this.machineIdentity = machineIdentity;
|
|
3422
|
-
types$1.logger.debug(`[DAEMON SESSION] Connecting to server: ${types$1.configuration.serverUrl}`);
|
|
3423
|
-
const socket = socket_ioClient.io(types$1.configuration.serverUrl, {
|
|
3424
|
-
auth: {
|
|
3425
|
-
token: this.token,
|
|
3426
|
-
clientType: "machine-scoped",
|
|
3427
|
-
machineId: this.machineIdentity.machineId
|
|
3428
|
-
},
|
|
3429
|
-
path: "/v1/updates",
|
|
3430
|
-
reconnection: true,
|
|
3431
|
-
reconnectionAttempts: Infinity,
|
|
3432
|
-
reconnectionDelay: 1e3,
|
|
3433
|
-
reconnectionDelayMax: 5e3,
|
|
3434
|
-
transports: ["websocket"],
|
|
3435
|
-
withCredentials: true,
|
|
3436
|
-
autoConnect: false
|
|
3437
|
-
});
|
|
3438
|
-
socket.on("connect", async () => {
|
|
3439
|
-
types$1.logger.debug("[DAEMON SESSION] Socket connected");
|
|
3440
|
-
types$1.logger.debug(`[DAEMON SESSION] Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
|
|
3441
|
-
if (!this.machineRegistered) {
|
|
3442
|
-
await this.registerMachine();
|
|
3443
|
-
}
|
|
3444
|
-
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
3445
|
-
socket.emit("rpc-register", { method: rpcMethod });
|
|
3446
|
-
types$1.logger.debug(`[DAEMON SESSION] Emitted RPC registration: ${rpcMethod}`);
|
|
3447
|
-
this.emit("connected");
|
|
3448
|
-
this.startKeepAlive();
|
|
4181
|
+
function startDaemonControlServer({
|
|
4182
|
+
getChildren,
|
|
4183
|
+
stopSession,
|
|
4184
|
+
spawnSession,
|
|
4185
|
+
requestShutdown,
|
|
4186
|
+
onHappySessionWebhook
|
|
4187
|
+
}) {
|
|
4188
|
+
return new Promise((resolve) => {
|
|
4189
|
+
const app = fastify({
|
|
4190
|
+
logger: false
|
|
4191
|
+
// We use our own logger
|
|
3449
4192
|
});
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
const args = [
|
|
3461
|
-
"--daemon-spawn",
|
|
3462
|
-
"--happy-starting-mode",
|
|
3463
|
-
"remote"
|
|
3464
|
-
// ALWAYS force remote mode for daemon spawns
|
|
3465
|
-
];
|
|
3466
|
-
if (types$1.configuration.installationLocation === "local") {
|
|
3467
|
-
args.push("--local");
|
|
3468
|
-
}
|
|
3469
|
-
types$1.logger.debug(`[DAEMON SESSION] Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
|
|
3470
|
-
const happyBinPath = path.join(projectPath(), "bin", "happy.mjs");
|
|
3471
|
-
types$1.logger.debug(`[DAEMON SESSION] Using happy binary at: ${happyBinPath}`);
|
|
3472
|
-
const executable = happyBinPath;
|
|
3473
|
-
const spawnArgs = args;
|
|
3474
|
-
types$1.logger.debug(`[DAEMON SESSION] Spawn: executable=${executable}, args=${JSON.stringify(spawnArgs)}, cwd=${directory}`);
|
|
3475
|
-
const happyProcess = child_process.spawn(executable, spawnArgs, {
|
|
3476
|
-
cwd: directory,
|
|
3477
|
-
detached: true,
|
|
3478
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3479
|
-
// We need stdout
|
|
3480
|
-
});
|
|
3481
|
-
this.spawnedProcesses.add(happyProcess);
|
|
3482
|
-
this.updateChildPidsInMetadata();
|
|
3483
|
-
let sessionId = null;
|
|
3484
|
-
let output = "";
|
|
3485
|
-
let timeoutId = null;
|
|
3486
|
-
const cleanup = () => {
|
|
3487
|
-
happyProcess.stdout.removeAllListeners("data");
|
|
3488
|
-
happyProcess.stderr.removeAllListeners("data");
|
|
3489
|
-
happyProcess.removeAllListeners("error");
|
|
3490
|
-
happyProcess.removeAllListeners("exit");
|
|
3491
|
-
if (timeoutId) {
|
|
3492
|
-
clearTimeout(timeoutId);
|
|
3493
|
-
timeoutId = null;
|
|
3494
|
-
}
|
|
3495
|
-
};
|
|
3496
|
-
happyProcess.stdout.on("data", (data2) => {
|
|
3497
|
-
output += data2.toString();
|
|
3498
|
-
const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
|
|
3499
|
-
if (match && !sessionId) {
|
|
3500
|
-
sessionId = match[1];
|
|
3501
|
-
types$1.logger.debug(`[DAEMON SESSION] Session spawned successfully: ${sessionId}`);
|
|
3502
|
-
callback({ sessionId });
|
|
3503
|
-
cleanup();
|
|
3504
|
-
happyProcess.unref();
|
|
3505
|
-
}
|
|
3506
|
-
});
|
|
3507
|
-
happyProcess.stderr.on("data", (data2) => {
|
|
3508
|
-
types$1.logger.debug(`[DAEMON SESSION] Spawned process stderr: ${data2.toString()}`);
|
|
3509
|
-
});
|
|
3510
|
-
happyProcess.on("error", (error) => {
|
|
3511
|
-
types$1.logger.debug("[DAEMON SESSION] Error spawning session:", error);
|
|
3512
|
-
if (!sessionId) {
|
|
3513
|
-
callback({ error: `Failed to spawn: ${error.message}` });
|
|
3514
|
-
cleanup();
|
|
3515
|
-
this.spawnedProcesses.delete(happyProcess);
|
|
3516
|
-
}
|
|
3517
|
-
});
|
|
3518
|
-
happyProcess.on("exit", (code, signal) => {
|
|
3519
|
-
types$1.logger.debug(`[DAEMON SESSION] Spawned process exited with code ${code}, signal ${signal}`);
|
|
3520
|
-
this.spawnedProcesses.delete(happyProcess);
|
|
3521
|
-
this.updateChildPidsInMetadata();
|
|
3522
|
-
if (!sessionId) {
|
|
3523
|
-
callback({ error: `Process exited before session ID received` });
|
|
3524
|
-
cleanup();
|
|
3525
|
-
}
|
|
3526
|
-
});
|
|
3527
|
-
timeoutId = setTimeout(() => {
|
|
3528
|
-
if (!sessionId) {
|
|
3529
|
-
types$1.logger.debug("[DAEMON SESSION] Timeout waiting for session ID");
|
|
3530
|
-
callback({ error: "Timeout waiting for session" });
|
|
3531
|
-
cleanup();
|
|
3532
|
-
happyProcess.kill();
|
|
3533
|
-
this.spawnedProcesses.delete(happyProcess);
|
|
3534
|
-
this.updateChildPidsInMetadata();
|
|
3535
|
-
}
|
|
3536
|
-
}, 1e4);
|
|
3537
|
-
} catch (error) {
|
|
3538
|
-
types$1.logger.debug("[DAEMON SESSION] Error spawning session:", error);
|
|
3539
|
-
callback({ error: error instanceof Error ? error.message : "Unknown error" });
|
|
3540
|
-
}
|
|
3541
|
-
} else {
|
|
3542
|
-
types$1.logger.debug(`[DAEMON SESSION] Unknown RPC method: ${data.method}`);
|
|
3543
|
-
callback({ error: `Unknown method: ${data.method}` });
|
|
4193
|
+
app.setValidatorCompiler(fastifyTypeProviderZod.validatorCompiler);
|
|
4194
|
+
app.setSerializerCompiler(fastifyTypeProviderZod.serializerCompiler);
|
|
4195
|
+
const typed = app.withTypeProvider();
|
|
4196
|
+
typed.post("/session-started", {
|
|
4197
|
+
schema: {
|
|
4198
|
+
body: z.z.object({
|
|
4199
|
+
sessionId: z.z.string(),
|
|
4200
|
+
metadata: z.z.any()
|
|
4201
|
+
// Metadata type from API
|
|
4202
|
+
})
|
|
3544
4203
|
}
|
|
4204
|
+
}, async (request, reply) => {
|
|
4205
|
+
const { sessionId, metadata } = request.body;
|
|
4206
|
+
types$1.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4207
|
+
onHappySessionWebhook(sessionId, metadata);
|
|
4208
|
+
return { status: "ok" };
|
|
3545
4209
|
});
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
});
|
|
3551
|
-
socket.on("reconnect", () => {
|
|
3552
|
-
types$1.logger.debug("[DAEMON SESSION] Reconnected to server");
|
|
3553
|
-
const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
|
|
3554
|
-
socket.emit("rpc-register", { method: rpcMethod });
|
|
3555
|
-
types$1.logger.debug(`[DAEMON SESSION] Re-registered RPC method: ${rpcMethod}`);
|
|
3556
|
-
});
|
|
3557
|
-
socket.on("rpc-registered", (data) => {
|
|
3558
|
-
types$1.logger.debug(`[DAEMON SESSION] RPC registration confirmed: ${data.method}`);
|
|
4210
|
+
typed.post("/list", async (request, reply) => {
|
|
4211
|
+
const children = getChildren();
|
|
4212
|
+
types$1.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4213
|
+
return { children };
|
|
3559
4214
|
});
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
});
|
|
3566
|
-
socket.onAny((event, ...args) => {
|
|
3567
|
-
if (!event.startsWith("session-alive") && event !== "ephemeral") {
|
|
3568
|
-
types$1.logger.debug(`[DAEMON SESSION] Socket event: ${event}, args: ${JSON.stringify(args)}`);
|
|
4215
|
+
typed.post("/stop-session", {
|
|
4216
|
+
schema: {
|
|
4217
|
+
body: z.z.object({
|
|
4218
|
+
sessionId: z.z.string()
|
|
4219
|
+
})
|
|
3569
4220
|
}
|
|
4221
|
+
}, async (request, reply) => {
|
|
4222
|
+
const { sessionId } = request.body;
|
|
4223
|
+
types$1.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4224
|
+
const success = stopSession(sessionId);
|
|
4225
|
+
return { success };
|
|
3570
4226
|
});
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
4227
|
+
typed.post("/spawn-session", {
|
|
4228
|
+
schema: {
|
|
4229
|
+
body: z.z.object({
|
|
4230
|
+
directory: z.z.string(),
|
|
4231
|
+
sessionId: z.z.string().optional()
|
|
4232
|
+
})
|
|
4233
|
+
}
|
|
4234
|
+
}, async (request, reply) => {
|
|
4235
|
+
const { directory, sessionId } = request.body;
|
|
4236
|
+
types$1.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4237
|
+
const session = await spawnSession(directory, sessionId);
|
|
4238
|
+
if (session) {
|
|
4239
|
+
return {
|
|
4240
|
+
success: true,
|
|
4241
|
+
pid: session.pid,
|
|
4242
|
+
sessionId: session.happySessionId || "pending"
|
|
4243
|
+
};
|
|
4244
|
+
} else {
|
|
4245
|
+
reply.code(500);
|
|
4246
|
+
return { error: "Failed to spawn session" };
|
|
4247
|
+
}
|
|
3574
4248
|
});
|
|
3575
|
-
|
|
3576
|
-
types$1.logger.debug(
|
|
4249
|
+
typed.post("/stop", async (request, reply) => {
|
|
4250
|
+
types$1.logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4251
|
+
setTimeout(() => {
|
|
4252
|
+
types$1.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
4253
|
+
requestShutdown();
|
|
4254
|
+
}, 50);
|
|
4255
|
+
return { status: "stopping" };
|
|
3577
4256
|
});
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
break;
|
|
3583
|
-
case "status":
|
|
3584
|
-
this.emit("status-request");
|
|
3585
|
-
break;
|
|
4257
|
+
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4258
|
+
if (err) {
|
|
4259
|
+
types$1.logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4260
|
+
throw err;
|
|
3586
4261
|
}
|
|
4262
|
+
const port = parseInt(address.split(":").pop());
|
|
4263
|
+
types$1.logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
4264
|
+
resolve({
|
|
4265
|
+
port,
|
|
4266
|
+
stop: async () => {
|
|
4267
|
+
types$1.logger.debug("[CONTROL SERVER] Stopping server");
|
|
4268
|
+
await app.close();
|
|
4269
|
+
types$1.logger.debug("[CONTROL SERVER] Server stopped");
|
|
4270
|
+
}
|
|
4271
|
+
});
|
|
3587
4272
|
});
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
4273
|
+
});
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
async function startDaemon() {
|
|
4277
|
+
types$1.logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4278
|
+
types$1.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4279
|
+
const runningDaemon = await getDaemonState();
|
|
4280
|
+
if (runningDaemon) {
|
|
3591
4281
|
try {
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
const encrypted = types$1.encrypt(JSON.stringify(metadata), this.secret);
|
|
3599
|
-
const encryptedMetadata = types$1.encodeBase64(encrypted);
|
|
3600
|
-
const response = await fetch(`${types$1.configuration.serverUrl}/v1/machines`, {
|
|
3601
|
-
method: "POST",
|
|
3602
|
-
headers: {
|
|
3603
|
-
"Authorization": `Bearer ${this.token}`,
|
|
3604
|
-
"Content-Type": "application/json"
|
|
3605
|
-
},
|
|
3606
|
-
body: JSON.stringify({
|
|
3607
|
-
id: this.machineIdentity.machineId,
|
|
3608
|
-
metadata: encryptedMetadata
|
|
3609
|
-
})
|
|
3610
|
-
});
|
|
3611
|
-
if (response.ok) {
|
|
3612
|
-
types$1.logger.debug("[DAEMON SESSION] Machine registered/updated successfully");
|
|
3613
|
-
this.machineRegistered = true;
|
|
3614
|
-
} else {
|
|
3615
|
-
types$1.logger.debug(`[DAEMON SESSION] Failed to register machine: ${response.status}`);
|
|
3616
|
-
}
|
|
3617
|
-
} catch (error) {
|
|
3618
|
-
types$1.logger.debug("[DAEMON SESSION] Failed to register machine:", error);
|
|
4282
|
+
process.kill(runningDaemon.pid, 0);
|
|
4283
|
+
types$1.logger.debug("[DAEMON RUN] Daemon already running");
|
|
4284
|
+
process.exit(0);
|
|
4285
|
+
} catch {
|
|
4286
|
+
types$1.logger.debug("[DAEMON RUN] Stale state found, cleaning up");
|
|
4287
|
+
await cleanupDaemonState();
|
|
3619
4288
|
}
|
|
3620
4289
|
}
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
const payload = {
|
|
3625
|
-
machineId: this.machineIdentity.machineId,
|
|
3626
|
-
time: Date.now()
|
|
3627
|
-
};
|
|
3628
|
-
types$1.logger.debugLargeJson(`[DAEMON SESSION] Emitting machine-alive`, payload);
|
|
3629
|
-
this.socket.emit("machine-alive", payload);
|
|
3630
|
-
}, 2e4);
|
|
3631
|
-
}
|
|
3632
|
-
stopKeepAlive() {
|
|
3633
|
-
if (this.keepAliveInterval) {
|
|
3634
|
-
clearInterval(this.keepAliveInterval);
|
|
3635
|
-
this.keepAliveInterval = null;
|
|
3636
|
-
}
|
|
4290
|
+
const caffeinateStarted = startCaffeinate();
|
|
4291
|
+
if (caffeinateStarted) {
|
|
4292
|
+
types$1.logger.debug("[DAEMON RUN] Sleep prevention enabled");
|
|
3637
4293
|
}
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
4294
|
+
try {
|
|
4295
|
+
const { credentials, machineId } = await authAndSetupMachineIfNeeded();
|
|
4296
|
+
types$1.logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
4297
|
+
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
4298
|
+
const pidToAwaiter = /* @__PURE__ */ new Map();
|
|
4299
|
+
const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
|
|
4300
|
+
const onHappySessionWebhook = (sessionId, sessionMetadata) => {
|
|
4301
|
+
const pid = sessionMetadata.hostPid;
|
|
4302
|
+
if (!pid) {
|
|
4303
|
+
types$1.logger.debug(`[DAEMON RUN] Session webhook missing hostPid for session ${sessionId}`);
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
types$1.logger.debug(`[DAEMON RUN] Session webhook: ${sessionId}, PID: ${pid}, started by: ${sessionMetadata.startedBy || "unknown"}`);
|
|
4307
|
+
const existingSession = pidToTrackedSession.get(pid);
|
|
4308
|
+
if (existingSession && existingSession.startedBy === "daemon") {
|
|
4309
|
+
existingSession.happySessionId = sessionId;
|
|
4310
|
+
existingSession.happySessionMetadataFromLocalWebhook = sessionMetadata;
|
|
4311
|
+
types$1.logger.debug(`[DAEMON RUN] Updated daemon-spawned session ${sessionId} with metadata`);
|
|
4312
|
+
const awaiter = pidToAwaiter.get(pid);
|
|
4313
|
+
if (awaiter) {
|
|
4314
|
+
pidToAwaiter.delete(pid);
|
|
4315
|
+
awaiter(existingSession);
|
|
4316
|
+
types$1.logger.debug(`[DAEMON RUN] Resolved session awaiter for PID ${pid}`);
|
|
4317
|
+
}
|
|
4318
|
+
} else if (!existingSession) {
|
|
4319
|
+
const trackedSession = {
|
|
4320
|
+
startedBy: "happy directly - likely by user from terminal",
|
|
4321
|
+
happySessionId: sessionId,
|
|
4322
|
+
happySessionMetadataFromLocalWebhook: sessionMetadata,
|
|
4323
|
+
pid
|
|
4324
|
+
};
|
|
4325
|
+
pidToTrackedSession.set(pid, trackedSession);
|
|
4326
|
+
types$1.logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
|
|
3646
4327
|
}
|
|
3647
|
-
} catch (error) {
|
|
3648
|
-
types$1.logger.debug("[DAEMON SESSION] Error updating child PIDs in metadata:", error);
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
connect() {
|
|
3652
|
-
this.socket.connect();
|
|
3653
|
-
}
|
|
3654
|
-
updateMetadata(updates) {
|
|
3655
|
-
const metadata = {
|
|
3656
|
-
host: this.machineIdentity.machineHost,
|
|
3657
|
-
platform: this.machineIdentity.platform,
|
|
3658
|
-
happyCliVersion: updates.happyCliVersion || this.machineIdentity.happyCliVersion,
|
|
3659
|
-
happyHomeDirectory: updates.happyHomeDirectory || this.machineIdentity.happyHomeDirectory
|
|
3660
4328
|
};
|
|
3661
|
-
const
|
|
3662
|
-
const encryptedMetadata = types$1.encodeBase64(encrypted);
|
|
3663
|
-
this.socket.emit("update-machine", { metadata: encryptedMetadata });
|
|
3664
|
-
}
|
|
3665
|
-
shutdown() {
|
|
3666
|
-
types$1.logger.debug(`[DAEMON SESSION] Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
|
|
3667
|
-
for (const process of this.spawnedProcesses) {
|
|
4329
|
+
const spawnSession = async (directory, sessionId) => {
|
|
3668
4330
|
try {
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
4331
|
+
const happyBinPath = path.join(projectPath(), "bin", "happy.mjs");
|
|
4332
|
+
const args = [
|
|
4333
|
+
"--happy-starting-mode",
|
|
4334
|
+
"remote",
|
|
4335
|
+
"--started-by",
|
|
4336
|
+
"daemon"
|
|
4337
|
+
];
|
|
4338
|
+
const fullCommand = `${happyBinPath} ${args.join(" ")}`;
|
|
4339
|
+
types$1.logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
|
|
4340
|
+
const happyProcess = child_process.spawn(happyBinPath, args, {
|
|
4341
|
+
cwd: directory,
|
|
4342
|
+
detached: true,
|
|
4343
|
+
// Sessions stay alive when daemon stops
|
|
4344
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4345
|
+
// Capture stdout/stderr for debugging
|
|
4346
|
+
// env is inherited automatically from parent process
|
|
4347
|
+
});
|
|
4348
|
+
happyProcess.stdout?.on("data", (data) => {
|
|
4349
|
+
types$1.logger.debug(`[DAEMON RUN] Child stdout: ${data.toString()}`);
|
|
4350
|
+
});
|
|
4351
|
+
happyProcess.stderr?.on("data", (data) => {
|
|
4352
|
+
types$1.logger.debug(`[DAEMON RUN] Child stderr: ${data.toString()}`);
|
|
4353
|
+
});
|
|
4354
|
+
if (!happyProcess.pid) {
|
|
4355
|
+
types$1.logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4356
|
+
return null;
|
|
4357
|
+
}
|
|
4358
|
+
types$1.logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
|
|
4359
|
+
const trackedSession = {
|
|
4360
|
+
startedBy: "daemon",
|
|
4361
|
+
pid: happyProcess.pid,
|
|
4362
|
+
childProcess: happyProcess
|
|
4363
|
+
};
|
|
4364
|
+
pidToTrackedSession.set(happyProcess.pid, trackedSession);
|
|
4365
|
+
happyProcess.on("exit", (code, signal) => {
|
|
4366
|
+
types$1.logger.debug(`[DAEMON RUN] Child PID ${happyProcess.pid} exited with code ${code}, signal ${signal}`);
|
|
4367
|
+
if (happyProcess.pid) {
|
|
4368
|
+
onChildExited(happyProcess.pid);
|
|
3675
4369
|
}
|
|
3676
|
-
}
|
|
4370
|
+
});
|
|
4371
|
+
happyProcess.on("error", (error) => {
|
|
4372
|
+
types$1.logger.debug(`[DAEMON RUN] Child process error:`, error);
|
|
4373
|
+
if (happyProcess.pid) {
|
|
4374
|
+
onChildExited(happyProcess.pid);
|
|
4375
|
+
}
|
|
4376
|
+
});
|
|
4377
|
+
types$1.logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${happyProcess.pid}`);
|
|
4378
|
+
return new Promise((resolve, reject) => {
|
|
4379
|
+
const timeout = setTimeout(() => {
|
|
4380
|
+
pidToAwaiter.delete(happyProcess.pid);
|
|
4381
|
+
types$1.logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4382
|
+
resolve(trackedSession);
|
|
4383
|
+
}, 1e4);
|
|
4384
|
+
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4385
|
+
clearTimeout(timeout);
|
|
4386
|
+
types$1.logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4387
|
+
resolve(completedSession);
|
|
4388
|
+
});
|
|
4389
|
+
});
|
|
3677
4390
|
} catch (error) {
|
|
3678
|
-
types$1.logger.debug(
|
|
4391
|
+
types$1.logger.debug("[DAEMON RUN] Failed to spawn session:", error);
|
|
4392
|
+
return null;
|
|
3679
4393
|
}
|
|
3680
|
-
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
types$1.logger.debug(`[DAEMON RUN] Daemon version mismatch (running: ${runningDaemon.version}, current: ${packageJson.version}), restarting...`);
|
|
3700
|
-
await stopDaemon();
|
|
3701
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3702
|
-
} else if (await isDaemonProcessRunning(runningDaemon.pid)) {
|
|
3703
|
-
types$1.logger.debug("[DAEMON RUN] Happy daemon is already running with correct version");
|
|
3704
|
-
process.exit(0);
|
|
3705
|
-
} else {
|
|
3706
|
-
types$1.logger.debug("[DAEMON RUN] Stale daemon metadata found, cleaning up");
|
|
3707
|
-
await cleanupDaemonMetadata();
|
|
3708
|
-
}
|
|
3709
|
-
}
|
|
3710
|
-
const oldMetadata = await getDaemonMetadata();
|
|
3711
|
-
if (oldMetadata && oldMetadata.childPids && oldMetadata.childPids.length > 0) {
|
|
3712
|
-
types$1.logger.debug(`[DAEMON RUN] Found ${oldMetadata.childPids.length} potential orphaned child processes from previous run`);
|
|
3713
|
-
for (const childPid of oldMetadata.childPids) {
|
|
3714
|
-
try {
|
|
3715
|
-
process.kill(childPid, 0);
|
|
3716
|
-
const isHappy = await isProcessHappyChild(childPid);
|
|
3717
|
-
if (isHappy) {
|
|
3718
|
-
types$1.logger.debug(`[DAEMON RUN] Killing orphaned happy process ${childPid}`);
|
|
3719
|
-
process.kill(childPid, "SIGTERM");
|
|
3720
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3721
|
-
try {
|
|
3722
|
-
process.kill(childPid, 0);
|
|
3723
|
-
process.kill(childPid, "SIGKILL");
|
|
3724
|
-
} catch {
|
|
4394
|
+
};
|
|
4395
|
+
const stopSession = (sessionId) => {
|
|
4396
|
+
types$1.logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
|
|
4397
|
+
for (const [pid, session] of pidToTrackedSession.entries()) {
|
|
4398
|
+
if (session.happySessionId === sessionId || sessionId.startsWith("PID-") && pid === parseInt(sessionId.replace("PID-", ""))) {
|
|
4399
|
+
if (session.startedBy === "daemon" && session.childProcess) {
|
|
4400
|
+
try {
|
|
4401
|
+
session.childProcess.kill("SIGTERM");
|
|
4402
|
+
types$1.logger.debug(`[DAEMON RUN] Sent SIGTERM to daemon-spawned session ${sessionId}`);
|
|
4403
|
+
} catch (error) {
|
|
4404
|
+
types$1.logger.debug(`[DAEMON RUN] Failed to kill session ${sessionId}:`, error);
|
|
4405
|
+
}
|
|
4406
|
+
} else {
|
|
4407
|
+
try {
|
|
4408
|
+
process.kill(pid, "SIGTERM");
|
|
4409
|
+
types$1.logger.debug(`[DAEMON RUN] Sent SIGTERM to external session PID ${pid}`);
|
|
4410
|
+
} catch (error) {
|
|
4411
|
+
types$1.logger.debug(`[DAEMON RUN] Failed to kill external session PID ${pid}:`, error);
|
|
4412
|
+
}
|
|
3725
4413
|
}
|
|
4414
|
+
pidToTrackedSession.delete(pid);
|
|
4415
|
+
types$1.logger.debug(`[DAEMON RUN] Removed session ${sessionId} from tracking`);
|
|
4416
|
+
return true;
|
|
3726
4417
|
}
|
|
3727
|
-
} catch {
|
|
3728
|
-
types$1.logger.debug(`[DAEMON RUN] Process ${childPid} doesn't exist (already dead)`);
|
|
3729
4418
|
}
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
writeDaemonMetadata();
|
|
3733
|
-
types$1.logger.debug("[DAEMON RUN] Daemon metadata written");
|
|
3734
|
-
const caffeinateStarted = startCaffeinate();
|
|
3735
|
-
if (caffeinateStarted) {
|
|
3736
|
-
types$1.logger.debug("[DAEMON RUN] Sleep prevention enabled for daemon");
|
|
3737
|
-
}
|
|
3738
|
-
try {
|
|
3739
|
-
const settings = await readSettings() || { onboardingCompleted: false };
|
|
3740
|
-
if (!settings.machineId) {
|
|
3741
|
-
settings.machineId = crypto.randomUUID();
|
|
3742
|
-
settings.machineHost = os$1.hostname();
|
|
3743
|
-
await writeSettings(settings);
|
|
3744
|
-
}
|
|
3745
|
-
const machineIdentity = {
|
|
3746
|
-
machineId: settings.machineId,
|
|
3747
|
-
machineHost: settings.machineHost || os$1.hostname(),
|
|
3748
|
-
platform: process.platform,
|
|
3749
|
-
happyCliVersion: packageJson.version,
|
|
3750
|
-
happyHomeDirectory: process.cwd()
|
|
4419
|
+
types$1.logger.debug(`[DAEMON RUN] Session ${sessionId} not found`);
|
|
4420
|
+
return false;
|
|
3751
4421
|
};
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
}
|
|
3760
|
-
}
|
|
3761
|
-
const { token, secret } = credentials;
|
|
3762
|
-
const daemon = new ApiDaemonSession(
|
|
3763
|
-
token,
|
|
3764
|
-
secret,
|
|
3765
|
-
machineIdentity
|
|
3766
|
-
);
|
|
3767
|
-
daemon.on("connected", () => {
|
|
3768
|
-
types$1.logger.debug("[DAEMON RUN] Connected to server event received");
|
|
3769
|
-
});
|
|
3770
|
-
daemon.on("disconnected", () => {
|
|
3771
|
-
types$1.logger.debug("[DAEMON RUN] Disconnected from server event received");
|
|
4422
|
+
const onChildExited = (pid) => {
|
|
4423
|
+
types$1.logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
|
|
4424
|
+
pidToTrackedSession.delete(pid);
|
|
4425
|
+
};
|
|
4426
|
+
let requestShutdown;
|
|
4427
|
+
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
4428
|
+
requestShutdown = resolve;
|
|
3772
4429
|
});
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
4430
|
+
const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
|
|
4431
|
+
getChildren: getCurrentChildren,
|
|
4432
|
+
stopSession,
|
|
4433
|
+
spawnSession,
|
|
4434
|
+
requestShutdown: () => requestShutdown("happy-cli"),
|
|
4435
|
+
onHappySessionWebhook
|
|
3778
4436
|
});
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
4437
|
+
const fileState = {
|
|
4438
|
+
pid: process.pid,
|
|
4439
|
+
httpPort: controlPort,
|
|
4440
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4441
|
+
startedWithCliVersion: packageJson.version
|
|
4442
|
+
};
|
|
4443
|
+
await writeDaemonState(fileState);
|
|
4444
|
+
types$1.logger.debug("[DAEMON RUN] Daemon state written");
|
|
4445
|
+
const initialMetadata = {
|
|
4446
|
+
host: os$1.hostname(),
|
|
4447
|
+
platform: os$1.platform(),
|
|
4448
|
+
happyCliVersion: packageJson.version,
|
|
4449
|
+
homeDir: os$1.homedir(),
|
|
4450
|
+
happyHomeDir: types$1.configuration.happyHomeDir
|
|
4451
|
+
};
|
|
4452
|
+
const initialDaemonState = {
|
|
4453
|
+
status: "offline",
|
|
4454
|
+
pid: process.pid,
|
|
4455
|
+
httpPort: controlPort,
|
|
4456
|
+
startedAt: Date.now()
|
|
4457
|
+
};
|
|
4458
|
+
const api = new types$1.ApiClient(credentials.token, credentials.secret);
|
|
4459
|
+
const machine = await api.createOrReturnExistingAsIs({
|
|
4460
|
+
machineId,
|
|
4461
|
+
metadata: initialMetadata,
|
|
4462
|
+
daemonState: initialDaemonState
|
|
3788
4463
|
});
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
process.exit(0);
|
|
4464
|
+
types$1.logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
|
|
4465
|
+
const apiMachine = api.machineSyncClient(machine);
|
|
4466
|
+
apiMachine.setRPCHandlers({
|
|
4467
|
+
spawnSession,
|
|
4468
|
+
stopSession,
|
|
4469
|
+
requestShutdown: () => requestShutdown("happy-app")
|
|
3796
4470
|
});
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
types$1.logger.debug("[DAEMON RUN] [isDaemonRunning] Checking if daemon is running...");
|
|
3809
|
-
const metadata = await getDaemonMetadata();
|
|
3810
|
-
if (!metadata) {
|
|
3811
|
-
types$1.logger.debug("[DAEMON RUN] [isDaemonRunning] No daemon metadata found");
|
|
3812
|
-
return false;
|
|
3813
|
-
}
|
|
3814
|
-
types$1.logger.debug("[DAEMON RUN] [isDaemonRunning] Daemon metadata exists");
|
|
3815
|
-
types$1.logger.debug("[DAEMON RUN] [isDaemonRunning] PID from metadata:", metadata.pid);
|
|
3816
|
-
const isRunning = await isDaemonProcessRunning(metadata.pid);
|
|
3817
|
-
if (!isRunning) {
|
|
3818
|
-
types$1.logger.debug("[DAEMON RUN] [isDaemonRunning] Process not running, cleaning up stale metadata");
|
|
3819
|
-
await cleanupDaemonMetadata();
|
|
3820
|
-
return false;
|
|
3821
|
-
}
|
|
3822
|
-
return true;
|
|
3823
|
-
} catch (error) {
|
|
3824
|
-
types$1.logger.debug("[DAEMON RUN] [isDaemonRunning] Error:", error);
|
|
3825
|
-
types$1.logger.debug("Error checking daemon status", error);
|
|
3826
|
-
return false;
|
|
3827
|
-
}
|
|
3828
|
-
}
|
|
3829
|
-
async function isDaemonProcessRunning(pid) {
|
|
3830
|
-
try {
|
|
3831
|
-
process.kill(pid, 0);
|
|
3832
|
-
types$1.logger.debug("[DAEMON RUN] Process exists, checking if it's a happy daemon...");
|
|
3833
|
-
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
3834
|
-
types$1.logger.debug("[DAEMON RUN] isHappyDaemon:", isHappyDaemon);
|
|
3835
|
-
return isHappyDaemon;
|
|
3836
|
-
} catch (error) {
|
|
3837
|
-
return false;
|
|
3838
|
-
}
|
|
3839
|
-
}
|
|
3840
|
-
function writeDaemonMetadata(childPids) {
|
|
3841
|
-
const happyDir = path.join(os$1.homedir(), ".happy");
|
|
3842
|
-
if (!fs.existsSync(happyDir)) {
|
|
3843
|
-
fs.mkdirSync(happyDir, { recursive: true });
|
|
3844
|
-
}
|
|
3845
|
-
const metadata = {
|
|
3846
|
-
pid: process.pid,
|
|
3847
|
-
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3848
|
-
version: packageJson.version,
|
|
3849
|
-
...childPids
|
|
3850
|
-
};
|
|
3851
|
-
fs.writeFileSync(types$1.configuration.daemonMetadataFile, JSON.stringify(metadata, null, 2));
|
|
3852
|
-
}
|
|
3853
|
-
async function getDaemonMetadata() {
|
|
3854
|
-
try {
|
|
3855
|
-
if (!fs.existsSync(types$1.configuration.daemonMetadataFile)) {
|
|
3856
|
-
return null;
|
|
3857
|
-
}
|
|
3858
|
-
const content = fs.readFileSync(types$1.configuration.daemonMetadataFile, "utf-8");
|
|
3859
|
-
return JSON.parse(content);
|
|
3860
|
-
} catch (error) {
|
|
3861
|
-
types$1.logger.debug("Error reading daemon metadata", error);
|
|
3862
|
-
return null;
|
|
3863
|
-
}
|
|
3864
|
-
}
|
|
3865
|
-
async function cleanupDaemonMetadata() {
|
|
3866
|
-
try {
|
|
3867
|
-
if (fs.existsSync(types$1.configuration.daemonMetadataFile)) {
|
|
3868
|
-
fs.unlinkSync(types$1.configuration.daemonMetadataFile);
|
|
3869
|
-
}
|
|
3870
|
-
} catch (error) {
|
|
3871
|
-
types$1.logger.debug("Error cleaning up daemon metadata", error);
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
|
-
async function stopDaemon() {
|
|
3875
|
-
try {
|
|
3876
|
-
stopCaffeinate();
|
|
3877
|
-
types$1.logger.debug("Stopped sleep prevention");
|
|
3878
|
-
const metadata = await getDaemonMetadata();
|
|
3879
|
-
if (metadata) {
|
|
3880
|
-
types$1.logger.debug(`Stopping daemon with PID ${metadata.pid}`);
|
|
3881
|
-
try {
|
|
3882
|
-
process.kill(metadata.pid, "SIGTERM");
|
|
3883
|
-
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3884
|
-
try {
|
|
3885
|
-
process.kill(metadata.pid, 0);
|
|
3886
|
-
types$1.logger.debug("Daemon still running, force killing...");
|
|
3887
|
-
process.kill(metadata.pid, "SIGKILL");
|
|
3888
|
-
} catch {
|
|
3889
|
-
types$1.logger.debug("Daemon exited cleanly");
|
|
3890
|
-
}
|
|
3891
|
-
} catch (error) {
|
|
3892
|
-
types$1.logger.debug("Daemon process already dead or inaccessible", error);
|
|
4471
|
+
apiMachine.connect();
|
|
4472
|
+
const cleanupAndShutdown = async (source) => {
|
|
4473
|
+
types$1.logger.debug(`[DAEMON RUN] Starting cleanup (source: ${source})...`);
|
|
4474
|
+
if (apiMachine) {
|
|
4475
|
+
await apiMachine.updateDaemonState((state) => ({
|
|
4476
|
+
...state,
|
|
4477
|
+
status: "shutting-down",
|
|
4478
|
+
shutdownRequestedAt: Date.now(),
|
|
4479
|
+
shutdownSource: source === "happy-app" ? "mobile-app" : source === "happy-cli" ? "cli" : source
|
|
4480
|
+
}));
|
|
4481
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3893
4482
|
}
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
types$1.logger.debug(`Checking for ${metadata.childPids.length} potential orphaned child processes...`);
|
|
3897
|
-
for (const childPid of metadata.childPids) {
|
|
3898
|
-
try {
|
|
3899
|
-
process.kill(childPid, 0);
|
|
3900
|
-
const isHappy = await isProcessHappyChild(childPid);
|
|
3901
|
-
if (isHappy) {
|
|
3902
|
-
types$1.logger.debug(`Killing orphaned happy process ${childPid}`);
|
|
3903
|
-
process.kill(childPid, "SIGTERM");
|
|
3904
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3905
|
-
try {
|
|
3906
|
-
process.kill(childPid, 0);
|
|
3907
|
-
process.kill(childPid, "SIGKILL");
|
|
3908
|
-
} catch {
|
|
3909
|
-
}
|
|
3910
|
-
}
|
|
3911
|
-
} catch {
|
|
3912
|
-
}
|
|
3913
|
-
}
|
|
4483
|
+
if (apiMachine) {
|
|
4484
|
+
apiMachine.shutdown();
|
|
3914
4485
|
}
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4486
|
+
types$1.logger.debug("[DAEMON RUN] Machine session shutdown");
|
|
4487
|
+
await stopControlServer();
|
|
4488
|
+
types$1.logger.debug("[DAEMON RUN] Control server stopped");
|
|
4489
|
+
await cleanupDaemonState();
|
|
4490
|
+
types$1.logger.debug("[DAEMON RUN] State cleaned up");
|
|
4491
|
+
stopCaffeinate();
|
|
4492
|
+
types$1.logger.debug("[DAEMON RUN] Caffeinate stopped");
|
|
4493
|
+
process.exit(0);
|
|
4494
|
+
};
|
|
4495
|
+
process.on("SIGINT", () => {
|
|
4496
|
+
types$1.logger.debug("[DAEMON RUN] Received SIGINT");
|
|
4497
|
+
cleanupAndShutdown("os-signal");
|
|
3927
4498
|
});
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
4499
|
+
process.on("SIGTERM", () => {
|
|
4500
|
+
types$1.logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4501
|
+
cleanupAndShutdown("os-signal");
|
|
3931
4502
|
});
|
|
3932
|
-
|
|
3933
|
-
|
|
4503
|
+
process.on("uncaughtException", (error) => {
|
|
4504
|
+
types$1.logger.debug("[DAEMON RUN] Uncaught exception - cleaning up before crash", error);
|
|
4505
|
+
cleanupAndShutdown("unknown");
|
|
3934
4506
|
});
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
return new Promise((resolve) => {
|
|
3939
|
-
const ps = child_process.spawn("ps", ["-p", pid.toString(), "-o", "command="]);
|
|
3940
|
-
let output = "";
|
|
3941
|
-
ps.stdout.on("data", (data) => {
|
|
3942
|
-
output += data.toString();
|
|
4507
|
+
process.on("unhandledRejection", (reason) => {
|
|
4508
|
+
types$1.logger.debug("[DAEMON RUN] Unhandled rejection - cleaning up before crash", reason);
|
|
4509
|
+
cleanupAndShutdown("unknown");
|
|
3943
4510
|
});
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
resolve(isHappyChild);
|
|
4511
|
+
process.on("exit", () => {
|
|
4512
|
+
types$1.logger.debug("[DAEMON RUN] Process exit, not killing any children");
|
|
3947
4513
|
});
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
});
|
|
3951
|
-
|
|
4514
|
+
types$1.logger.debug("[DAEMON RUN] Daemon started successfully");
|
|
4515
|
+
const shutdownSource = await resolvesWhenShutdownRequested;
|
|
4516
|
+
types$1.logger.debug(`[DAEMON RUN] Shutdown requested (source: ${shutdownSource})`);
|
|
4517
|
+
await cleanupAndShutdown(shutdownSource);
|
|
4518
|
+
} catch (error) {
|
|
4519
|
+
types$1.logger.debug("[DAEMON RUN] Failed to start daemon", error);
|
|
4520
|
+
await cleanupDaemonState();
|
|
4521
|
+
stopCaffeinate();
|
|
4522
|
+
process.exit(1);
|
|
4523
|
+
}
|
|
3952
4524
|
}
|
|
3953
4525
|
|
|
3954
4526
|
function trimIdent(text) {
|
|
@@ -3972,7 +4544,6 @@ function trimIdent(text) {
|
|
|
3972
4544
|
|
|
3973
4545
|
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
3974
4546
|
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
3975
|
-
const USER_HOME = process.env.HOME || process.env.USERPROFILE;
|
|
3976
4547
|
async function install$1() {
|
|
3977
4548
|
try {
|
|
3978
4549
|
if (fs.existsSync(PLIST_FILE$1)) {
|
|
@@ -4009,10 +4580,10 @@ async function install$1() {
|
|
|
4009
4580
|
<true/>
|
|
4010
4581
|
|
|
4011
4582
|
<key>StandardErrorPath</key>
|
|
4012
|
-
<string>${
|
|
4583
|
+
<string>${os$1.homedir()}/.happy/daemon.err</string>
|
|
4013
4584
|
|
|
4014
4585
|
<key>StandardOutPath</key>
|
|
4015
|
-
<string>${
|
|
4586
|
+
<string>${os$1.homedir()}/.happy/daemon.log</string>
|
|
4016
4587
|
|
|
4017
4588
|
<key>WorkingDirectory</key>
|
|
4018
4589
|
<string>/tmp</string>
|
|
@@ -4076,16 +4647,249 @@ async function uninstall() {
|
|
|
4076
4647
|
await uninstall$1();
|
|
4077
4648
|
}
|
|
4078
4649
|
|
|
4650
|
+
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
4651
|
+
function bytesToBase32(bytes) {
|
|
4652
|
+
let result = "";
|
|
4653
|
+
let buffer = 0;
|
|
4654
|
+
let bufferLength = 0;
|
|
4655
|
+
for (const byte of bytes) {
|
|
4656
|
+
buffer = buffer << 8 | byte;
|
|
4657
|
+
bufferLength += 8;
|
|
4658
|
+
while (bufferLength >= 5) {
|
|
4659
|
+
bufferLength -= 5;
|
|
4660
|
+
result += BASE32_ALPHABET[buffer >> bufferLength & 31];
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
if (bufferLength > 0) {
|
|
4664
|
+
result += BASE32_ALPHABET[buffer << 5 - bufferLength & 31];
|
|
4665
|
+
}
|
|
4666
|
+
return result;
|
|
4667
|
+
}
|
|
4668
|
+
function formatSecretKeyForBackup(secretBytes) {
|
|
4669
|
+
const base32 = bytesToBase32(secretBytes);
|
|
4670
|
+
const groups = [];
|
|
4671
|
+
for (let i = 0; i < base32.length; i += 5) {
|
|
4672
|
+
groups.push(base32.slice(i, i + 5));
|
|
4673
|
+
}
|
|
4674
|
+
return groups.join("-");
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
async function handleAuthCommand(args) {
|
|
4678
|
+
const subcommand = args[0];
|
|
4679
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
4680
|
+
showAuthHelp();
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
switch (subcommand) {
|
|
4684
|
+
case "login":
|
|
4685
|
+
await handleAuthLogin(args.slice(1));
|
|
4686
|
+
break;
|
|
4687
|
+
case "logout":
|
|
4688
|
+
await handleAuthLogout();
|
|
4689
|
+
break;
|
|
4690
|
+
case "show-backup":
|
|
4691
|
+
await handleAuthShowBackup();
|
|
4692
|
+
break;
|
|
4693
|
+
case "status":
|
|
4694
|
+
await handleAuthStatus();
|
|
4695
|
+
break;
|
|
4696
|
+
default:
|
|
4697
|
+
console.error(chalk.red(`Unknown auth subcommand: ${subcommand}`));
|
|
4698
|
+
showAuthHelp();
|
|
4699
|
+
process.exit(1);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
function showAuthHelp() {
|
|
4703
|
+
console.log(`
|
|
4704
|
+
${chalk.bold("happy auth")} - Authentication management
|
|
4705
|
+
|
|
4706
|
+
${chalk.bold("Usage:")}
|
|
4707
|
+
happy auth login [--force] Authenticate with Happy
|
|
4708
|
+
happy auth logout Remove authentication and machine data
|
|
4709
|
+
happy auth status Show authentication status
|
|
4710
|
+
happy auth show-backup Display backup key for mobile/web clients
|
|
4711
|
+
happy auth help Show this help message
|
|
4712
|
+
|
|
4713
|
+
${chalk.bold("Options:")}
|
|
4714
|
+
--force Clear credentials, machine ID, and stop daemon before re-auth
|
|
4715
|
+
|
|
4716
|
+
${chalk.bold("Examples:")}
|
|
4717
|
+
happy auth login Authenticate if not already logged in
|
|
4718
|
+
happy auth login --force Force re-authentication (complete reset)
|
|
4719
|
+
happy auth status Check authentication and machine status
|
|
4720
|
+
happy auth show-backup Get backup key to link other devices
|
|
4721
|
+
happy auth logout Remove all authentication data
|
|
4722
|
+
|
|
4723
|
+
${chalk.bold("Notes:")}
|
|
4724
|
+
\u2022 Use 'auth login --force' when you need to re-register your machine
|
|
4725
|
+
\u2022 'auth show-backup' displays the key format expected by mobile/web clients
|
|
4726
|
+
\u2022 The backup key allows linking multiple devices to the same account
|
|
4727
|
+
`);
|
|
4728
|
+
}
|
|
4729
|
+
async function handleAuthLogin(args) {
|
|
4730
|
+
const forceAuth = args.includes("--force") || args.includes("-f");
|
|
4731
|
+
if (forceAuth) {
|
|
4732
|
+
console.log(chalk.yellow("Force authentication requested."));
|
|
4733
|
+
console.log(chalk.gray("This will:"));
|
|
4734
|
+
console.log(chalk.gray(" \u2022 Clear existing credentials"));
|
|
4735
|
+
console.log(chalk.gray(" \u2022 Clear machine ID"));
|
|
4736
|
+
console.log(chalk.gray(" \u2022 Stop daemon if running"));
|
|
4737
|
+
console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
|
|
4738
|
+
try {
|
|
4739
|
+
types$1.logger.debug("Stopping daemon for force auth...");
|
|
4740
|
+
await stopDaemon();
|
|
4741
|
+
console.log(chalk.gray("\u2713 Stopped daemon"));
|
|
4742
|
+
} catch (error) {
|
|
4743
|
+
types$1.logger.debug("Daemon was not running or failed to stop:", error);
|
|
4744
|
+
}
|
|
4745
|
+
await clearCredentials();
|
|
4746
|
+
console.log(chalk.gray("\u2713 Cleared credentials"));
|
|
4747
|
+
await clearMachineId();
|
|
4748
|
+
console.log(chalk.gray("\u2713 Cleared machine ID"));
|
|
4749
|
+
console.log("");
|
|
4750
|
+
}
|
|
4751
|
+
if (!forceAuth) {
|
|
4752
|
+
const existingCreds = await readCredentials();
|
|
4753
|
+
const settings = await readSettings();
|
|
4754
|
+
if (existingCreds && settings?.machineId) {
|
|
4755
|
+
console.log(chalk.green("\u2713 Already authenticated"));
|
|
4756
|
+
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
4757
|
+
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
4758
|
+
console.log(chalk.gray(` Use 'happy auth login --force' to re-authenticate`));
|
|
4759
|
+
return;
|
|
4760
|
+
} else if (existingCreds && !settings?.machineId) {
|
|
4761
|
+
console.log(chalk.yellow("\u26A0\uFE0F Credentials exist but machine ID is missing"));
|
|
4762
|
+
console.log(chalk.gray(" This can happen if --auth flag was used previously"));
|
|
4763
|
+
console.log(chalk.gray(" Fixing by setting up machine...\n"));
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
try {
|
|
4767
|
+
const result = await authAndSetupMachineIfNeeded();
|
|
4768
|
+
console.log(chalk.green("\n\u2713 Authentication successful"));
|
|
4769
|
+
console.log(chalk.gray(` Machine ID: ${result.machineId}`));
|
|
4770
|
+
} catch (error) {
|
|
4771
|
+
console.error(chalk.red("Authentication failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
4772
|
+
process.exit(1);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
async function handleAuthLogout() {
|
|
4776
|
+
const happyDir = types$1.configuration.happyHomeDir;
|
|
4777
|
+
const credentials = await readCredentials();
|
|
4778
|
+
if (!credentials) {
|
|
4779
|
+
console.log(chalk.yellow("Not currently authenticated"));
|
|
4780
|
+
return;
|
|
4781
|
+
}
|
|
4782
|
+
console.log(chalk.blue("This will log you out of Happy"));
|
|
4783
|
+
console.log(chalk.yellow("\u26A0\uFE0F You will need to re-authenticate to use Happy again"));
|
|
4784
|
+
const rl = node_readline.createInterface({
|
|
4785
|
+
input: process.stdin,
|
|
4786
|
+
output: process.stdout
|
|
4787
|
+
});
|
|
4788
|
+
const answer = await new Promise((resolve) => {
|
|
4789
|
+
rl.question(chalk.yellow("Are you sure you want to log out? (y/N): "), resolve);
|
|
4790
|
+
});
|
|
4791
|
+
rl.close();
|
|
4792
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
4793
|
+
try {
|
|
4794
|
+
try {
|
|
4795
|
+
await stopDaemon();
|
|
4796
|
+
console.log(chalk.gray("Stopped daemon"));
|
|
4797
|
+
} catch {
|
|
4798
|
+
}
|
|
4799
|
+
if (node_fs.existsSync(happyDir)) {
|
|
4800
|
+
node_fs.rmSync(happyDir, { recursive: true, force: true });
|
|
4801
|
+
}
|
|
4802
|
+
console.log(chalk.green("\u2713 Successfully logged out"));
|
|
4803
|
+
console.log(chalk.gray(' Run "happy auth login" to authenticate again'));
|
|
4804
|
+
} catch (error) {
|
|
4805
|
+
throw new Error(`Failed to logout: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4806
|
+
}
|
|
4807
|
+
} else {
|
|
4808
|
+
console.log(chalk.blue("Logout cancelled"));
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
async function handleAuthShowBackup() {
|
|
4812
|
+
const credentials = await readCredentials();
|
|
4813
|
+
const settings = await readSettings();
|
|
4814
|
+
if (!credentials) {
|
|
4815
|
+
console.log(chalk.yellow("Not authenticated"));
|
|
4816
|
+
console.log(chalk.gray('Run "happy auth login" to authenticate first'));
|
|
4817
|
+
return;
|
|
4818
|
+
}
|
|
4819
|
+
const formattedBackupKey = formatSecretKeyForBackup(credentials.secret);
|
|
4820
|
+
console.log(chalk.bold("\n\u{1F4F1} Backup Key\n"));
|
|
4821
|
+
console.log(chalk.cyan("Your backup key:"));
|
|
4822
|
+
console.log(chalk.bold(formattedBackupKey));
|
|
4823
|
+
console.log("");
|
|
4824
|
+
console.log(chalk.cyan("Machine Information:"));
|
|
4825
|
+
console.log(` Machine ID: ${settings?.machineId || "not set"}`);
|
|
4826
|
+
console.log(` Host: ${os.hostname()}`);
|
|
4827
|
+
console.log("");
|
|
4828
|
+
console.log(chalk.bold("How to use this backup key:"));
|
|
4829
|
+
console.log(chalk.gray("\u2022 In Happy mobile app: Go to restore/link device and enter this key"));
|
|
4830
|
+
console.log(chalk.gray("\u2022 This key format matches what the mobile app expects"));
|
|
4831
|
+
console.log(chalk.gray("\u2022 You can type it with or without dashes - the app will normalize it"));
|
|
4832
|
+
console.log(chalk.gray("\u2022 Common typos (0\u2192O, 1\u2192I) are automatically corrected"));
|
|
4833
|
+
console.log("");
|
|
4834
|
+
console.log(chalk.yellow("\u26A0\uFE0F Keep this key secure - it provides full access to your account"));
|
|
4835
|
+
}
|
|
4836
|
+
async function handleAuthStatus() {
|
|
4837
|
+
const credentials = await readCredentials();
|
|
4838
|
+
const settings = await readSettings();
|
|
4839
|
+
console.log(chalk.bold("\nAuthentication Status\n"));
|
|
4840
|
+
if (!credentials) {
|
|
4841
|
+
console.log(chalk.red("\u2717 Not authenticated"));
|
|
4842
|
+
console.log(chalk.gray(' Run "happy auth login" to authenticate'));
|
|
4843
|
+
return;
|
|
4844
|
+
}
|
|
4845
|
+
console.log(chalk.green("\u2713 Authenticated"));
|
|
4846
|
+
const tokenPreview = credentials.token.substring(0, 30) + "...";
|
|
4847
|
+
console.log(chalk.gray(` Token: ${tokenPreview}`));
|
|
4848
|
+
if (settings?.machineId) {
|
|
4849
|
+
console.log(chalk.green("\u2713 Machine registered"));
|
|
4850
|
+
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
4851
|
+
console.log(chalk.gray(` Host: ${os.hostname()}`));
|
|
4852
|
+
} else {
|
|
4853
|
+
console.log(chalk.yellow("\u26A0\uFE0F Machine not registered"));
|
|
4854
|
+
console.log(chalk.gray(' Run "happy auth login --force" to fix this'));
|
|
4855
|
+
}
|
|
4856
|
+
console.log(chalk.gray(`
|
|
4857
|
+
Data directory: ${types$1.configuration.happyHomeDir}`));
|
|
4858
|
+
try {
|
|
4859
|
+
const { isDaemonRunning } = await Promise.resolve().then(function () { return utils; });
|
|
4860
|
+
const running = await isDaemonRunning();
|
|
4861
|
+
if (running) {
|
|
4862
|
+
console.log(chalk.green("\u2713 Daemon running"));
|
|
4863
|
+
} else {
|
|
4864
|
+
console.log(chalk.gray("\u2717 Daemon not running"));
|
|
4865
|
+
}
|
|
4866
|
+
} catch {
|
|
4867
|
+
console.log(chalk.gray("\u2717 Daemon not running"));
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4079
4871
|
(async () => {
|
|
4080
4872
|
const args = process.argv.slice(2);
|
|
4081
|
-
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
4082
|
-
types$1.initializeConfiguration(installationLocation);
|
|
4083
|
-
types$1.initLoggerWithGlobalConfiguration();
|
|
4084
4873
|
types$1.logger.debug("Starting happy CLI with args: ", process.argv);
|
|
4085
4874
|
const subcommand = args[0];
|
|
4086
|
-
if (subcommand === "
|
|
4875
|
+
if (subcommand === "doctor") {
|
|
4876
|
+
await runDoctorCommand();
|
|
4877
|
+
return;
|
|
4878
|
+
} else if (subcommand === "auth") {
|
|
4087
4879
|
try {
|
|
4088
|
-
await
|
|
4880
|
+
await handleAuthCommand(args.slice(1));
|
|
4881
|
+
} catch (error) {
|
|
4882
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
4883
|
+
if (process.env.DEBUG) {
|
|
4884
|
+
console.error(error);
|
|
4885
|
+
}
|
|
4886
|
+
process.exit(1);
|
|
4887
|
+
}
|
|
4888
|
+
return;
|
|
4889
|
+
} else if (subcommand === "logout") {
|
|
4890
|
+
console.log(chalk.yellow('Note: "happy logout" is deprecated. Use "happy auth logout" instead.\n'));
|
|
4891
|
+
try {
|
|
4892
|
+
await handleAuthCommand(["logout"]);
|
|
4089
4893
|
} catch (error) {
|
|
4090
4894
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
4091
4895
|
if (process.env.DEBUG) {
|
|
@@ -4107,12 +4911,92 @@ async function uninstall() {
|
|
|
4107
4911
|
return;
|
|
4108
4912
|
} else if (subcommand === "daemon") {
|
|
4109
4913
|
const daemonSubcommand = args[1];
|
|
4110
|
-
if (daemonSubcommand === "
|
|
4914
|
+
if (daemonSubcommand === "list") {
|
|
4915
|
+
try {
|
|
4916
|
+
const sessions = await listDaemonSessions();
|
|
4917
|
+
if (sessions.length === 0) {
|
|
4918
|
+
console.log("No active sessions");
|
|
4919
|
+
} else {
|
|
4920
|
+
console.log("Active sessions:");
|
|
4921
|
+
const cleanSessions = sessions.map((s) => ({
|
|
4922
|
+
pid: s.pid,
|
|
4923
|
+
sessionId: s.happySessionId || `PID-${s.pid}`,
|
|
4924
|
+
startedBy: s.startedBy,
|
|
4925
|
+
directory: s.happySessionMetadataFromLocalWebhook?.directory || "unknown"
|
|
4926
|
+
}));
|
|
4927
|
+
console.log(JSON.stringify(cleanSessions, null, 2));
|
|
4928
|
+
}
|
|
4929
|
+
} catch (error) {
|
|
4930
|
+
console.log("No daemon running");
|
|
4931
|
+
}
|
|
4932
|
+
return;
|
|
4933
|
+
} else if (daemonSubcommand === "stop-session") {
|
|
4934
|
+
const sessionId = args[2];
|
|
4935
|
+
if (!sessionId) {
|
|
4936
|
+
console.error("Session ID required");
|
|
4937
|
+
process.exit(1);
|
|
4938
|
+
}
|
|
4939
|
+
try {
|
|
4940
|
+
const success = await stopDaemonSession(sessionId);
|
|
4941
|
+
console.log(success ? "Session stopped" : "Failed to stop session");
|
|
4942
|
+
} catch (error) {
|
|
4943
|
+
console.log("No daemon running");
|
|
4944
|
+
}
|
|
4945
|
+
return;
|
|
4946
|
+
} else if (daemonSubcommand === "start") {
|
|
4947
|
+
const happyBinPath = node_path.join(projectPath(), "bin", "happy.mjs");
|
|
4948
|
+
const child = child_process.spawn(happyBinPath, ["daemon", "start-sync"], {
|
|
4949
|
+
detached: true,
|
|
4950
|
+
stdio: "ignore",
|
|
4951
|
+
env: process.env
|
|
4952
|
+
});
|
|
4953
|
+
child.unref();
|
|
4954
|
+
let started = false;
|
|
4955
|
+
for (let i = 0; i < 50; i++) {
|
|
4956
|
+
if (await isDaemonRunning()) {
|
|
4957
|
+
started = true;
|
|
4958
|
+
break;
|
|
4959
|
+
}
|
|
4960
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4961
|
+
}
|
|
4962
|
+
if (started) {
|
|
4963
|
+
console.log("Daemon started successfully");
|
|
4964
|
+
} else {
|
|
4965
|
+
console.error("Failed to start daemon");
|
|
4966
|
+
process.exit(1);
|
|
4967
|
+
}
|
|
4968
|
+
process.exit(0);
|
|
4969
|
+
} else if (daemonSubcommand === "start-sync") {
|
|
4111
4970
|
await startDaemon();
|
|
4112
4971
|
process.exit(0);
|
|
4113
4972
|
} else if (daemonSubcommand === "stop") {
|
|
4114
4973
|
await stopDaemon();
|
|
4115
4974
|
process.exit(0);
|
|
4975
|
+
} else if (daemonSubcommand === "status") {
|
|
4976
|
+
const state = await getDaemonState();
|
|
4977
|
+
if (!state) {
|
|
4978
|
+
console.log("Daemon is not running");
|
|
4979
|
+
} else {
|
|
4980
|
+
const isRunning = await isDaemonRunning();
|
|
4981
|
+
if (isRunning) {
|
|
4982
|
+
console.log("Daemon is running");
|
|
4983
|
+
console.log(` PID: ${state.pid}`);
|
|
4984
|
+
console.log(` Port: ${state.httpPort}`);
|
|
4985
|
+
console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
|
|
4986
|
+
console.log(` CLI Version: ${state.startedWithCliVersion}`);
|
|
4987
|
+
} else {
|
|
4988
|
+
console.log("Daemon state file exists but daemon is not running (stale)");
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
process.exit(0);
|
|
4992
|
+
} else if (daemonSubcommand === "kill-runaway") {
|
|
4993
|
+
const { killRunawayHappyProcesses } = await Promise.resolve().then(function () { return utils; });
|
|
4994
|
+
const result = await killRunawayHappyProcesses();
|
|
4995
|
+
console.log(`Killed ${result.killed} runaway processes`);
|
|
4996
|
+
if (result.errors.length > 0) {
|
|
4997
|
+
console.log("Errors:", result.errors);
|
|
4998
|
+
}
|
|
4999
|
+
process.exit(0);
|
|
4116
5000
|
} else if (daemonSubcommand === "install") {
|
|
4117
5001
|
try {
|
|
4118
5002
|
await install();
|
|
@@ -4132,13 +5016,16 @@ async function uninstall() {
|
|
|
4132
5016
|
${chalk.bold("happy daemon")} - Daemon management
|
|
4133
5017
|
|
|
4134
5018
|
${chalk.bold("Usage:")}
|
|
4135
|
-
happy daemon start
|
|
4136
|
-
happy daemon stop
|
|
4137
|
-
|
|
4138
|
-
|
|
5019
|
+
happy daemon start Start the daemon (detached)
|
|
5020
|
+
happy daemon stop Stop the daemon (sessions stay alive)
|
|
5021
|
+
happy daemon stop --kill-managed Stop daemon and kill managed sessions
|
|
5022
|
+
happy daemon status Show daemon status
|
|
5023
|
+
happy daemon list List active sessions
|
|
5024
|
+
happy daemon stop-session <id> Stop a specific session
|
|
5025
|
+
happy daemon kill-runaway Kill all runaway Happy processes
|
|
4139
5026
|
|
|
4140
|
-
${chalk.bold("Note:")} The daemon runs in the background and
|
|
4141
|
-
|
|
5027
|
+
${chalk.bold("Note:")} The daemon runs in the background and manages Claude sessions.
|
|
5028
|
+
Sessions spawned by the daemon will continue running after daemon stops unless --kill-managed is used.
|
|
4142
5029
|
`);
|
|
4143
5030
|
}
|
|
4144
5031
|
return;
|
|
@@ -4147,104 +5034,105 @@ Currently only supported on macOS.
|
|
|
4147
5034
|
let showHelp = false;
|
|
4148
5035
|
let showVersion = false;
|
|
4149
5036
|
let forceAuth = false;
|
|
5037
|
+
let forceAuthNew = false;
|
|
5038
|
+
const unknownArgs = [];
|
|
4150
5039
|
for (let i = 0; i < args.length; i++) {
|
|
4151
5040
|
const arg = args[i];
|
|
4152
|
-
if (arg === "
|
|
5041
|
+
if (arg === "--help") {
|
|
4153
5042
|
showHelp = true;
|
|
4154
|
-
} else if (arg === "
|
|
5043
|
+
} else if (arg === "--version") {
|
|
4155
5044
|
showVersion = true;
|
|
4156
5045
|
} else if (arg === "--auth" || arg === "--login") {
|
|
4157
5046
|
forceAuth = true;
|
|
4158
|
-
} else if (arg === "-
|
|
4159
|
-
|
|
4160
|
-
} else if (arg === "
|
|
4161
|
-
options.permissionMode = z.z.enum(["default", "acceptEdits", "bypassPermissions", "plan"]).parse(args[++i]);
|
|
4162
|
-
} else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
|
|
5047
|
+
} else if (arg === "--force-auth") {
|
|
5048
|
+
forceAuthNew = true;
|
|
5049
|
+
} else if (arg === "--happy-starting-mode") {
|
|
4163
5050
|
options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
|
|
4164
|
-
} else if (arg === "--
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
console.error(chalk.red(`Invalid environment variable format: ${envVar}. Use KEY=VALUE`));
|
|
4169
|
-
process.exit(1);
|
|
4170
|
-
}
|
|
4171
|
-
options.claudeEnvVars = { ...options.claudeEnvVars, [key]: value };
|
|
4172
|
-
} else if (arg === "--claude-arg") {
|
|
4173
|
-
const claudeArg = args[++i];
|
|
4174
|
-
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
4175
|
-
} else if (arg === "--daemon-spawn") {
|
|
4176
|
-
options.daemonSpawn = true;
|
|
5051
|
+
} else if (arg === "--yolo") {
|
|
5052
|
+
unknownArgs.push("--dangerously-skip-permissions");
|
|
5053
|
+
} else if (arg === "--started-by") {
|
|
5054
|
+
options.startedBy = args[++i];
|
|
4177
5055
|
} else {
|
|
4178
|
-
|
|
4179
|
-
|
|
5056
|
+
unknownArgs.push(arg);
|
|
5057
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
5058
|
+
unknownArgs.push(args[++i]);
|
|
5059
|
+
}
|
|
4180
5060
|
}
|
|
4181
5061
|
}
|
|
5062
|
+
if (unknownArgs.length > 0) {
|
|
5063
|
+
options.claudeArgs = [...options.claudeArgs || [], ...unknownArgs];
|
|
5064
|
+
}
|
|
4182
5065
|
if (showHelp) {
|
|
4183
5066
|
console.log(`
|
|
4184
5067
|
${chalk.bold("happy")} - Claude Code On the Go
|
|
4185
5068
|
|
|
4186
5069
|
${chalk.bold("Usage:")}
|
|
4187
|
-
happy [options]
|
|
4188
|
-
happy
|
|
4189
|
-
happy
|
|
4190
|
-
happy daemon
|
|
4191
|
-
|
|
4192
|
-
${chalk.bold("Options:")}
|
|
4193
|
-
-h, --help Show this help message
|
|
4194
|
-
-v, --version Show version
|
|
4195
|
-
-m, --model <model> Claude model to use (default: sonnet)
|
|
4196
|
-
-p, --permission-mode Permission mode: default, acceptEdits, bypassPermissions, or plan
|
|
4197
|
-
--auth, --login Force re-authentication
|
|
4198
|
-
--claude-env KEY=VALUE Set environment variable for Claude Code
|
|
4199
|
-
--claude-arg ARG Pass additional argument to Claude CLI
|
|
5070
|
+
happy [options] Start Claude with mobile control
|
|
5071
|
+
happy auth Manage authentication
|
|
5072
|
+
happy notify Send push notification
|
|
5073
|
+
happy daemon Manage background service
|
|
4200
5074
|
|
|
4201
|
-
|
|
4202
|
-
--
|
|
4203
|
-
--
|
|
4204
|
-
--
|
|
4205
|
-
--happy-daemon-uninstall Uninstall daemon from startup
|
|
5075
|
+
${chalk.bold("Happy Options:")}
|
|
5076
|
+
--help Show this help message
|
|
5077
|
+
--yolo Skip all permissions (--dangerously-skip-permissions)
|
|
5078
|
+
--force-auth Force re-authentication
|
|
4206
5079
|
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
4210
|
-
You will require re-login each time you run this in a new directory.
|
|
4211
|
-
--happy-starting-mode <interactive|remote>
|
|
4212
|
-
Set the starting mode for new sessions (default: remote)
|
|
4213
|
-
--happy-server-url <url>
|
|
4214
|
-
Set the server URL (overrides HANDY_SERVER_URL environment variable)
|
|
5080
|
+
${chalk.bold("\u{1F3AF} Happy supports ALL Claude options!")}
|
|
5081
|
+
Use any claude flag exactly as you normally would.
|
|
4215
5082
|
|
|
4216
5083
|
${chalk.bold("Examples:")}
|
|
4217
|
-
happy Start
|
|
4218
|
-
happy
|
|
4219
|
-
happy
|
|
4220
|
-
happy
|
|
4221
|
-
happy
|
|
4222
|
-
happy
|
|
4223
|
-
Set environment variable for Claude Code
|
|
4224
|
-
happy --claude-arg --option
|
|
4225
|
-
Pass argument to Claude CLI
|
|
4226
|
-
happy logout Logs out of your account and removes data directory
|
|
5084
|
+
happy Start session
|
|
5085
|
+
happy --yolo Start without permissions
|
|
5086
|
+
happy --verbose Enable verbose mode
|
|
5087
|
+
happy -c Continue last conversation
|
|
5088
|
+
happy auth login Authenticate
|
|
5089
|
+
happy notify -p "Done!" Send notification
|
|
4227
5090
|
|
|
4228
|
-
|
|
4229
|
-
${chalk.bold("
|
|
4230
|
-
|
|
5091
|
+
${chalk.bold("Happy is a wrapper around Claude Code that enables remote control via mobile app.")}
|
|
5092
|
+
${chalk.bold('Use "happy daemon" for background service management.')}
|
|
5093
|
+
|
|
5094
|
+
${chalk.gray("\u2500".repeat(60))}
|
|
5095
|
+
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
4231
5096
|
`);
|
|
5097
|
+
const { execSync } = await import('child_process');
|
|
5098
|
+
try {
|
|
5099
|
+
const claudeHelp = execSync("claude --help", { encoding: "utf8" });
|
|
5100
|
+
console.log(claudeHelp);
|
|
5101
|
+
} catch (e) {
|
|
5102
|
+
console.log(chalk.yellow("Could not retrieve claude help. Make sure claude is installed."));
|
|
5103
|
+
}
|
|
4232
5104
|
process.exit(0);
|
|
4233
5105
|
}
|
|
4234
5106
|
if (showVersion) {
|
|
4235
5107
|
console.log(packageJson.version);
|
|
4236
5108
|
process.exit(0);
|
|
4237
5109
|
}
|
|
4238
|
-
let credentials
|
|
4239
|
-
if (
|
|
4240
|
-
|
|
5110
|
+
let credentials;
|
|
5111
|
+
if (forceAuthNew) {
|
|
5112
|
+
console.log(chalk.yellow("Force authentication requested..."));
|
|
5113
|
+
try {
|
|
5114
|
+
await stopDaemon();
|
|
5115
|
+
} catch {
|
|
5116
|
+
}
|
|
5117
|
+
await clearCredentials();
|
|
5118
|
+
await clearMachineId();
|
|
5119
|
+
const result = await authAndSetupMachineIfNeeded();
|
|
5120
|
+
credentials = result.credentials;
|
|
5121
|
+
} else if (forceAuth) {
|
|
5122
|
+
console.log(chalk.yellow('Note: --auth is deprecated. Use "happy auth login" or --force-auth instead.\n'));
|
|
5123
|
+
const res = await doAuth();
|
|
4241
5124
|
if (!res) {
|
|
4242
5125
|
process.exit(1);
|
|
4243
5126
|
}
|
|
4244
|
-
|
|
5127
|
+
await writeCredentials(res);
|
|
5128
|
+
const result = await authAndSetupMachineIfNeeded();
|
|
5129
|
+
credentials = result.credentials;
|
|
5130
|
+
} else {
|
|
5131
|
+
const result = await authAndSetupMachineIfNeeded();
|
|
5132
|
+
credentials = result.credentials;
|
|
4245
5133
|
}
|
|
4246
|
-
|
|
4247
|
-
if (settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
5134
|
+
let settings = await readSettings();
|
|
5135
|
+
if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
4248
5136
|
console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
|
|
4249
5137
|
const rl = node_readline.createInterface({
|
|
4250
5138
|
input: process.stdin,
|
|
@@ -4259,39 +5147,28 @@ TODO: exec cluade --help and show inline here
|
|
|
4259
5147
|
});
|
|
4260
5148
|
rl.close();
|
|
4261
5149
|
const shouldAutoStart = answer.toLowerCase() !== "n";
|
|
4262
|
-
settings
|
|
5150
|
+
settings = await updateSettings((settings2) => ({
|
|
5151
|
+
...settings2,
|
|
5152
|
+
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5153
|
+
}));
|
|
4263
5154
|
if (shouldAutoStart) {
|
|
4264
5155
|
console.log(chalk.green("\u2713 Happy will start the background service automatically"));
|
|
4265
5156
|
console.log(chalk.gray(" The service will run whenever you use the happy command"));
|
|
4266
5157
|
} else {
|
|
4267
5158
|
console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
|
|
4268
5159
|
}
|
|
4269
|
-
await writeSettings(settings);
|
|
4270
5160
|
}
|
|
4271
|
-
if (settings.daemonAutoStartWhenRunningHappy) {
|
|
5161
|
+
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
4272
5162
|
types$1.logger.debug("Starting Happy background service...");
|
|
4273
5163
|
if (!await isDaemonRunning()) {
|
|
4274
|
-
const
|
|
4275
|
-
const
|
|
4276
|
-
const daemonArgs = ["daemon", "start"];
|
|
4277
|
-
if (installationLocation === "local") {
|
|
4278
|
-
daemonArgs.push("--local");
|
|
4279
|
-
}
|
|
4280
|
-
let executable, args2;
|
|
4281
|
-
if (runningFromBuiltBinary) {
|
|
4282
|
-
executable = happyPath;
|
|
4283
|
-
args2 = daemonArgs;
|
|
4284
|
-
} else {
|
|
4285
|
-
executable = "npx";
|
|
4286
|
-
args2 = ["tsx", happyPath, ...daemonArgs];
|
|
4287
|
-
}
|
|
4288
|
-
const daemonProcess = child_process.spawn(executable, args2, {
|
|
5164
|
+
const happyBinPath = node_path.join(projectPath(), "bin", "happy.mjs");
|
|
5165
|
+
const daemonProcess = child_process.spawn(happyBinPath, ["daemon", "start-sync"], {
|
|
4289
5166
|
detached: true,
|
|
4290
|
-
stdio:
|
|
4291
|
-
|
|
5167
|
+
stdio: "ignore",
|
|
5168
|
+
env: process.env
|
|
4292
5169
|
});
|
|
4293
5170
|
daemonProcess.unref();
|
|
4294
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
5171
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4295
5172
|
}
|
|
4296
5173
|
}
|
|
4297
5174
|
try {
|
|
@@ -4305,34 +5182,6 @@ TODO: exec cluade --help and show inline here
|
|
|
4305
5182
|
}
|
|
4306
5183
|
}
|
|
4307
5184
|
})();
|
|
4308
|
-
async function cleanKey() {
|
|
4309
|
-
const happyDir = types$1.configuration.happyDir;
|
|
4310
|
-
if (!node_fs.existsSync(happyDir)) {
|
|
4311
|
-
console.log(chalk.yellow("No happy data directory found at:"), happyDir);
|
|
4312
|
-
return;
|
|
4313
|
-
}
|
|
4314
|
-
console.log(chalk.blue("Found happy data directory at:"), happyDir);
|
|
4315
|
-
console.log(chalk.yellow("\u26A0\uFE0F This will remove all authentication data and require reconnecting your phone."));
|
|
4316
|
-
const rl = node_readline.createInterface({
|
|
4317
|
-
input: process.stdin,
|
|
4318
|
-
output: process.stdout
|
|
4319
|
-
});
|
|
4320
|
-
const answer = await new Promise((resolve) => {
|
|
4321
|
-
rl.question(chalk.yellow("Are you sure you want to remove the happy data directory? (y/N): "), resolve);
|
|
4322
|
-
});
|
|
4323
|
-
rl.close();
|
|
4324
|
-
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
4325
|
-
try {
|
|
4326
|
-
node_fs.rmSync(happyDir, { recursive: true, force: true });
|
|
4327
|
-
console.log(chalk.green("\u2713 Happy data directory removed successfully"));
|
|
4328
|
-
console.log(chalk.blue("\u2139\uFE0F You will need to reconnect your phone on the next session"));
|
|
4329
|
-
} catch (error) {
|
|
4330
|
-
throw new Error(`Failed to remove data directory: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4331
|
-
}
|
|
4332
|
-
} else {
|
|
4333
|
-
console.log(chalk.blue("Operation cancelled"));
|
|
4334
|
-
}
|
|
4335
|
-
}
|
|
4336
5185
|
async function handleNotifyCommand(args) {
|
|
4337
5186
|
let message = "";
|
|
4338
5187
|
let title = "";
|