happy-coder 0.9.0-6 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1044 -1089
- package/dist/index.mjs +797 -824
- package/dist/lib.cjs +4 -4
- package/dist/lib.d.cts +48 -3
- package/dist/lib.d.mts +48 -3
- package/dist/lib.mjs +4 -4
- package/dist/{types-DJOX-XG-.mjs → types-BS8Pr3Im.mjs} +588 -204
- package/dist/{types-a-nJyP-e.cjs → types-DNUk09Np.cjs} +486 -71
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var
|
|
4
|
+
var os = require('node:os');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
|
+
var types = require('./types-DNUk09Np.cjs');
|
|
6
7
|
var node_child_process = require('node:child_process');
|
|
7
8
|
var node_path = require('node:path');
|
|
8
9
|
var node_readline = require('node:readline');
|
|
9
10
|
var node_url = require('node:url');
|
|
10
11
|
var node_fs = require('node:fs');
|
|
11
|
-
var os = require('node:os');
|
|
12
12
|
var path = require('path');
|
|
13
13
|
var url = require('url');
|
|
14
|
-
var promises
|
|
15
|
-
var
|
|
14
|
+
var promises = require('node:fs/promises');
|
|
15
|
+
var fs = require('fs/promises');
|
|
16
16
|
var ink = require('ink');
|
|
17
17
|
var React = require('react');
|
|
18
18
|
var axios = require('axios');
|
|
@@ -23,37 +23,18 @@ require('expo-server-sdk');
|
|
|
23
23
|
var child_process = require('child_process');
|
|
24
24
|
var util = require('util');
|
|
25
25
|
var crypto = require('crypto');
|
|
26
|
-
var z = require('zod');
|
|
27
|
-
var fastify = require('fastify');
|
|
28
|
-
var fastifyTypeProviderZod = require('fastify-type-provider-zod');
|
|
29
26
|
var os$1 = require('os');
|
|
30
27
|
var qrcode = require('qrcode-terminal');
|
|
31
28
|
var open = require('open');
|
|
29
|
+
var fastify = require('fastify');
|
|
30
|
+
var z = require('zod');
|
|
31
|
+
var fastifyTypeProviderZod = require('fastify-type-provider-zod');
|
|
32
|
+
var fs$1 = require('fs');
|
|
32
33
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
33
34
|
var node_http = require('node:http');
|
|
34
35
|
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
35
|
-
var fs = require('fs');
|
|
36
36
|
|
|
37
37
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
38
|
-
function _interopNamespaceDefault(e) {
|
|
39
|
-
var n = Object.create(null);
|
|
40
|
-
if (e) {
|
|
41
|
-
Object.keys(e).forEach(function (k) {
|
|
42
|
-
if (k !== 'default') {
|
|
43
|
-
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
44
|
-
Object.defineProperty(n, k, d.get ? d : {
|
|
45
|
-
enumerable: true,
|
|
46
|
-
get: function () { return e[k]; }
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
n.default = e;
|
|
52
|
-
return Object.freeze(n);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
56
|
-
|
|
57
38
|
class Session {
|
|
58
39
|
path;
|
|
59
40
|
logPath;
|
|
@@ -62,6 +43,7 @@ class Session {
|
|
|
62
43
|
queue;
|
|
63
44
|
claudeEnvVars;
|
|
64
45
|
claudeArgs;
|
|
46
|
+
// Made mutable to allow filtering
|
|
65
47
|
mcpServers;
|
|
66
48
|
allowedTools;
|
|
67
49
|
_onModeChange;
|
|
@@ -96,13 +78,45 @@ class Session {
|
|
|
96
78
|
};
|
|
97
79
|
onSessionFound = (sessionId) => {
|
|
98
80
|
this.sessionId = sessionId;
|
|
81
|
+
this.client.updateMetadata((metadata) => ({
|
|
82
|
+
...metadata,
|
|
83
|
+
claudeSessionId: sessionId
|
|
84
|
+
}));
|
|
85
|
+
types.logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
|
|
99
86
|
};
|
|
100
87
|
/**
|
|
101
88
|
* Clear the current session ID (used by /clear command)
|
|
102
89
|
*/
|
|
103
90
|
clearSessionId = () => {
|
|
104
91
|
this.sessionId = null;
|
|
105
|
-
types
|
|
92
|
+
types.logger.debug("[Session] Session ID cleared");
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Consume one-time Claude flags from claudeArgs after Claude spawn
|
|
96
|
+
* Currently handles: --resume (with or without session ID)
|
|
97
|
+
*/
|
|
98
|
+
consumeOneTimeFlags = () => {
|
|
99
|
+
if (!this.claudeArgs) return;
|
|
100
|
+
const filteredArgs = [];
|
|
101
|
+
for (let i = 0; i < this.claudeArgs.length; i++) {
|
|
102
|
+
if (this.claudeArgs[i] === "--resume") {
|
|
103
|
+
if (i + 1 < this.claudeArgs.length) {
|
|
104
|
+
const nextArg = this.claudeArgs[i + 1];
|
|
105
|
+
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
106
|
+
i++;
|
|
107
|
+
types.logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
|
|
108
|
+
} else {
|
|
109
|
+
types.logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
types.logger.debug("[Session] Consumed --resume flag (no session ID)");
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
filteredArgs.push(this.claudeArgs[i]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
|
|
119
|
+
types.logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
|
|
106
120
|
};
|
|
107
121
|
}
|
|
108
122
|
|
|
@@ -117,7 +131,7 @@ function claudeCheckSession(sessionId, path) {
|
|
|
117
131
|
const sessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
118
132
|
const sessionExists = node_fs.existsSync(sessionFile);
|
|
119
133
|
if (!sessionExists) {
|
|
120
|
-
types
|
|
134
|
+
types.logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
|
|
121
135
|
return false;
|
|
122
136
|
}
|
|
123
137
|
const sessionData = node_fs.readFileSync(sessionFile, "utf-8").split("\n");
|
|
@@ -158,6 +172,16 @@ function trimIdent(text) {
|
|
|
158
172
|
|
|
159
173
|
const systemPrompt = trimIdent(`
|
|
160
174
|
ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
|
|
175
|
+
|
|
176
|
+
When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
|
|
177
|
+
|
|
178
|
+
<main commit message>
|
|
179
|
+
|
|
180
|
+
Generated with [Claude Code](https://claude.ai/code)
|
|
181
|
+
via [Happy](https://happy.engineering)
|
|
182
|
+
|
|
183
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
184
|
+
Co-Authored-By: Happy <yesreply@happy.engineering>
|
|
161
185
|
`);
|
|
162
186
|
|
|
163
187
|
node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
@@ -170,7 +194,7 @@ async function claudeLocal(opts) {
|
|
|
170
194
|
const detectedIdsFileSystem = /* @__PURE__ */ new Set();
|
|
171
195
|
watcher.on("change", (event, filename) => {
|
|
172
196
|
if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
|
|
173
|
-
types
|
|
197
|
+
types.logger.debug("change", event, filename);
|
|
174
198
|
const sessionId = filename.replace(".jsonl", "");
|
|
175
199
|
if (detectedIdsFileSystem.has(sessionId)) {
|
|
176
200
|
return;
|
|
@@ -194,7 +218,7 @@ async function claudeLocal(opts) {
|
|
|
194
218
|
const updateThinking = (newThinking) => {
|
|
195
219
|
if (thinking !== newThinking) {
|
|
196
220
|
thinking = newThinking;
|
|
197
|
-
types
|
|
221
|
+
types.logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
|
|
198
222
|
if (opts.onThinkingChange) {
|
|
199
223
|
opts.onThinkingChange(thinking);
|
|
200
224
|
}
|
|
@@ -272,10 +296,10 @@ async function claudeLocal(opts) {
|
|
|
272
296
|
}
|
|
273
297
|
break;
|
|
274
298
|
default:
|
|
275
|
-
types
|
|
299
|
+
types.logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
|
|
276
300
|
}
|
|
277
301
|
} catch (e) {
|
|
278
|
-
types
|
|
302
|
+
types.logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
|
|
279
303
|
}
|
|
280
304
|
});
|
|
281
305
|
rl.on("error", (err) => {
|
|
@@ -379,7 +403,7 @@ class InvalidateSync {
|
|
|
379
403
|
this._pendings = [];
|
|
380
404
|
};
|
|
381
405
|
_doSync = async () => {
|
|
382
|
-
await types
|
|
406
|
+
await types.backoff(async () => {
|
|
383
407
|
if (this._stopped) {
|
|
384
408
|
return;
|
|
385
409
|
}
|
|
@@ -404,21 +428,21 @@ function startFileWatcher(file, onFileChange) {
|
|
|
404
428
|
void (async () => {
|
|
405
429
|
while (true) {
|
|
406
430
|
try {
|
|
407
|
-
types
|
|
408
|
-
const watcher =
|
|
431
|
+
types.logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
|
|
432
|
+
const watcher = fs.watch(file, { persistent: true, signal: abortController.signal });
|
|
409
433
|
for await (const event of watcher) {
|
|
410
434
|
if (abortController.signal.aborted) {
|
|
411
435
|
return;
|
|
412
436
|
}
|
|
413
|
-
types
|
|
437
|
+
types.logger.debug(`[FILE_WATCHER] File changed: ${file}`);
|
|
414
438
|
onFileChange(file);
|
|
415
439
|
}
|
|
416
440
|
} catch (e) {
|
|
417
441
|
if (abortController.signal.aborted) {
|
|
418
442
|
return;
|
|
419
443
|
}
|
|
420
|
-
types
|
|
421
|
-
await types
|
|
444
|
+
types.logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
|
|
445
|
+
await types.delay(1e3);
|
|
422
446
|
}
|
|
423
447
|
}
|
|
424
448
|
})();
|
|
@@ -488,21 +512,21 @@ async function createSessionScanner(opts) {
|
|
|
488
512
|
},
|
|
489
513
|
onNewSession: (sessionId) => {
|
|
490
514
|
if (currentSessionId === sessionId) {
|
|
491
|
-
types
|
|
515
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
|
|
492
516
|
return;
|
|
493
517
|
}
|
|
494
518
|
if (finishedSessions.has(sessionId)) {
|
|
495
|
-
types
|
|
519
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
|
|
496
520
|
return;
|
|
497
521
|
}
|
|
498
522
|
if (pendingSessions.has(sessionId)) {
|
|
499
|
-
types
|
|
523
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
|
|
500
524
|
return;
|
|
501
525
|
}
|
|
502
526
|
if (currentSessionId) {
|
|
503
527
|
pendingSessions.add(currentSessionId);
|
|
504
528
|
}
|
|
505
|
-
types
|
|
529
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
|
|
506
530
|
currentSessionId = sessionId;
|
|
507
531
|
sync.invalidate();
|
|
508
532
|
}
|
|
@@ -523,12 +547,12 @@ function messageKey(message) {
|
|
|
523
547
|
}
|
|
524
548
|
async function readSessionLog(projectDir, sessionId) {
|
|
525
549
|
const expectedSessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
526
|
-
types
|
|
550
|
+
types.logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
|
|
527
551
|
let file;
|
|
528
552
|
try {
|
|
529
|
-
file = await promises
|
|
553
|
+
file = await promises.readFile(expectedSessionFile, "utf-8");
|
|
530
554
|
} catch (error) {
|
|
531
|
-
types
|
|
555
|
+
types.logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
|
|
532
556
|
return [];
|
|
533
557
|
}
|
|
534
558
|
let lines = file.split("\n");
|
|
@@ -539,14 +563,14 @@ async function readSessionLog(projectDir, sessionId) {
|
|
|
539
563
|
continue;
|
|
540
564
|
}
|
|
541
565
|
let message = JSON.parse(l);
|
|
542
|
-
let parsed = types
|
|
566
|
+
let parsed = types.RawJSONLinesSchema.safeParse(message);
|
|
543
567
|
if (!parsed.success) {
|
|
544
|
-
types
|
|
568
|
+
types.logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
|
|
545
569
|
continue;
|
|
546
570
|
}
|
|
547
571
|
messages.push(parsed.data);
|
|
548
572
|
} catch (e) {
|
|
549
|
-
types
|
|
573
|
+
types.logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
550
574
|
continue;
|
|
551
575
|
}
|
|
552
576
|
}
|
|
@@ -574,7 +598,7 @@ async function claudeLocalLauncher(session) {
|
|
|
574
598
|
await exutFuture.promise;
|
|
575
599
|
}
|
|
576
600
|
async function doAbort() {
|
|
577
|
-
types
|
|
601
|
+
types.logger.debug("[local]: doAbort");
|
|
578
602
|
if (!exitReason) {
|
|
579
603
|
exitReason = "switch";
|
|
580
604
|
}
|
|
@@ -582,7 +606,7 @@ async function claudeLocalLauncher(session) {
|
|
|
582
606
|
await abort();
|
|
583
607
|
}
|
|
584
608
|
async function doSwitch() {
|
|
585
|
-
types
|
|
609
|
+
types.logger.debug("[local]: doSwitch");
|
|
586
610
|
if (!exitReason) {
|
|
587
611
|
exitReason = "switch";
|
|
588
612
|
}
|
|
@@ -604,7 +628,7 @@ async function claudeLocalLauncher(session) {
|
|
|
604
628
|
if (exitReason) {
|
|
605
629
|
return exitReason;
|
|
606
630
|
}
|
|
607
|
-
types
|
|
631
|
+
types.logger.debug("[local]: launch");
|
|
608
632
|
try {
|
|
609
633
|
await claudeLocal({
|
|
610
634
|
path: session.path,
|
|
@@ -617,12 +641,13 @@ async function claudeLocalLauncher(session) {
|
|
|
617
641
|
mcpServers: session.mcpServers,
|
|
618
642
|
allowedTools: session.allowedTools
|
|
619
643
|
});
|
|
644
|
+
session.consumeOneTimeFlags();
|
|
620
645
|
if (!exitReason) {
|
|
621
646
|
exitReason = "exit";
|
|
622
647
|
break;
|
|
623
648
|
}
|
|
624
649
|
} catch (e) {
|
|
625
|
-
types
|
|
650
|
+
types.logger.debug("[local]: launch error", e);
|
|
626
651
|
if (!exitReason) {
|
|
627
652
|
session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
628
653
|
continue;
|
|
@@ -630,7 +655,7 @@ async function claudeLocalLauncher(session) {
|
|
|
630
655
|
break;
|
|
631
656
|
}
|
|
632
657
|
}
|
|
633
|
-
types
|
|
658
|
+
types.logger.debug("[local]: launch done");
|
|
634
659
|
}
|
|
635
660
|
} finally {
|
|
636
661
|
exutFuture.resolve(void 0);
|
|
@@ -910,7 +935,7 @@ function getDefaultClaudeCodePath() {
|
|
|
910
935
|
}
|
|
911
936
|
function logDebug(message) {
|
|
912
937
|
if (process.env.DEBUG) {
|
|
913
|
-
types
|
|
938
|
+
types.logger.debug(message);
|
|
914
939
|
console.log(message);
|
|
915
940
|
}
|
|
916
941
|
}
|
|
@@ -989,7 +1014,7 @@ class Query {
|
|
|
989
1014
|
}
|
|
990
1015
|
this.inputStream.enqueue(message);
|
|
991
1016
|
} catch (e) {
|
|
992
|
-
types
|
|
1017
|
+
types.logger.debug(line);
|
|
993
1018
|
}
|
|
994
1019
|
}
|
|
995
1020
|
}
|
|
@@ -1406,10 +1431,10 @@ async function awaitFileExist(file, timeout = 1e4) {
|
|
|
1406
1431
|
const startTime = Date.now();
|
|
1407
1432
|
while (Date.now() - startTime < timeout) {
|
|
1408
1433
|
try {
|
|
1409
|
-
await
|
|
1434
|
+
await fs.access(file);
|
|
1410
1435
|
return true;
|
|
1411
1436
|
} catch (e) {
|
|
1412
|
-
await types
|
|
1437
|
+
await types.delay(1e3);
|
|
1413
1438
|
}
|
|
1414
1439
|
}
|
|
1415
1440
|
return false;
|
|
@@ -1420,6 +1445,26 @@ async function claudeRemote(opts) {
|
|
|
1420
1445
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
1421
1446
|
startFrom = null;
|
|
1422
1447
|
}
|
|
1448
|
+
if (!startFrom && opts.claudeArgs) {
|
|
1449
|
+
for (let i = 0; i < opts.claudeArgs.length; i++) {
|
|
1450
|
+
if (opts.claudeArgs[i] === "--resume") {
|
|
1451
|
+
if (i + 1 < opts.claudeArgs.length) {
|
|
1452
|
+
const nextArg = opts.claudeArgs[i + 1];
|
|
1453
|
+
if (!nextArg.startsWith("-") && nextArg.includes("-")) {
|
|
1454
|
+
startFrom = nextArg;
|
|
1455
|
+
types.logger.debug(`[claudeRemote] Found --resume with session ID: ${startFrom}`);
|
|
1456
|
+
break;
|
|
1457
|
+
} else {
|
|
1458
|
+
types.logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
} else {
|
|
1462
|
+
types.logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1423
1468
|
if (opts.claudeEnvVars) {
|
|
1424
1469
|
Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
|
|
1425
1470
|
process.env[key] = value;
|
|
@@ -1441,7 +1486,7 @@ async function claudeRemote(opts) {
|
|
|
1441
1486
|
}
|
|
1442
1487
|
let isCompactCommand = false;
|
|
1443
1488
|
if (specialCommand.type === "compact") {
|
|
1444
|
-
types
|
|
1489
|
+
types.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
|
|
1445
1490
|
isCompactCommand = true;
|
|
1446
1491
|
if (opts.onCompletionEvent) {
|
|
1447
1492
|
opts.onCompletionEvent("Compaction started");
|
|
@@ -1470,7 +1515,7 @@ async function claudeRemote(opts) {
|
|
|
1470
1515
|
const updateThinking = (newThinking) => {
|
|
1471
1516
|
if (thinking !== newThinking) {
|
|
1472
1517
|
thinking = newThinking;
|
|
1473
|
-
types
|
|
1518
|
+
types.logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
|
|
1474
1519
|
if (opts.onThinkingChange) {
|
|
1475
1520
|
opts.onThinkingChange(thinking);
|
|
1476
1521
|
}
|
|
@@ -1490,26 +1535,26 @@ async function claudeRemote(opts) {
|
|
|
1490
1535
|
});
|
|
1491
1536
|
updateThinking(true);
|
|
1492
1537
|
try {
|
|
1493
|
-
types
|
|
1538
|
+
types.logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
1494
1539
|
for await (const message of response) {
|
|
1495
|
-
types
|
|
1540
|
+
types.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
1496
1541
|
opts.onMessage(message);
|
|
1497
1542
|
if (message.type === "system" && message.subtype === "init") {
|
|
1498
1543
|
updateThinking(true);
|
|
1499
1544
|
const systemInit = message;
|
|
1500
1545
|
if (systemInit.session_id) {
|
|
1501
|
-
types
|
|
1546
|
+
types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
|
|
1502
1547
|
const projectDir = getProjectPath(opts.path);
|
|
1503
1548
|
const found = await awaitFileExist(node_path.join(projectDir, `${systemInit.session_id}.jsonl`));
|
|
1504
|
-
types
|
|
1549
|
+
types.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
|
|
1505
1550
|
opts.onSessionFound(systemInit.session_id);
|
|
1506
1551
|
}
|
|
1507
1552
|
}
|
|
1508
1553
|
if (message.type === "result") {
|
|
1509
1554
|
updateThinking(false);
|
|
1510
|
-
types
|
|
1555
|
+
types.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
1511
1556
|
if (isCompactCommand) {
|
|
1512
|
-
types
|
|
1557
|
+
types.logger.debug("[claudeRemote] Compaction completed");
|
|
1513
1558
|
if (opts.onCompletionEvent) {
|
|
1514
1559
|
opts.onCompletionEvent("Compaction completed");
|
|
1515
1560
|
}
|
|
@@ -1529,7 +1574,7 @@ async function claudeRemote(opts) {
|
|
|
1529
1574
|
if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
|
|
1530
1575
|
for (let c of msg.message.content) {
|
|
1531
1576
|
if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
|
|
1532
|
-
types
|
|
1577
|
+
types.logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
|
|
1533
1578
|
return;
|
|
1534
1579
|
}
|
|
1535
1580
|
}
|
|
@@ -1538,7 +1583,7 @@ async function claudeRemote(opts) {
|
|
|
1538
1583
|
}
|
|
1539
1584
|
} catch (e) {
|
|
1540
1585
|
if (e instanceof AbortError) {
|
|
1541
|
-
types
|
|
1586
|
+
types.logger.debug(`[claudeRemote] Aborted`);
|
|
1542
1587
|
} else {
|
|
1543
1588
|
throw e;
|
|
1544
1589
|
}
|
|
@@ -1658,9 +1703,9 @@ class PermissionHandler {
|
|
|
1658
1703
|
this.permissionMode = response.mode;
|
|
1659
1704
|
}
|
|
1660
1705
|
if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
|
|
1661
|
-
types
|
|
1706
|
+
types.logger.debug("Plan mode result received", response);
|
|
1662
1707
|
if (response.approved) {
|
|
1663
|
-
types
|
|
1708
|
+
types.logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
|
|
1664
1709
|
if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
|
|
1665
1710
|
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
|
|
1666
1711
|
} else {
|
|
@@ -1703,7 +1748,7 @@ class PermissionHandler {
|
|
|
1703
1748
|
}
|
|
1704
1749
|
let toolCallId = this.resolveToolCallId(toolName, input);
|
|
1705
1750
|
if (!toolCallId) {
|
|
1706
|
-
await types
|
|
1751
|
+
await types.delay(1e3);
|
|
1707
1752
|
toolCallId = this.resolveToolCallId(toolName, input);
|
|
1708
1753
|
if (!toolCallId) {
|
|
1709
1754
|
throw new Error(`Could not resolve tool call ID for ${toolName}`);
|
|
@@ -1757,7 +1802,7 @@ class PermissionHandler {
|
|
|
1757
1802
|
}
|
|
1758
1803
|
}
|
|
1759
1804
|
}));
|
|
1760
|
-
types
|
|
1805
|
+
types.logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
|
|
1761
1806
|
});
|
|
1762
1807
|
}
|
|
1763
1808
|
/**
|
|
@@ -1879,11 +1924,11 @@ class PermissionHandler {
|
|
|
1879
1924
|
*/
|
|
1880
1925
|
setupClientHandler() {
|
|
1881
1926
|
this.session.client.setHandler("permission", async (message) => {
|
|
1882
|
-
types
|
|
1927
|
+
types.logger.debug(`Permission response: ${JSON.stringify(message)}`);
|
|
1883
1928
|
const id = message.id;
|
|
1884
1929
|
const pending = this.pendingRequests.get(id);
|
|
1885
1930
|
if (!pending) {
|
|
1886
|
-
types
|
|
1931
|
+
types.logger.debug("Permission request not found or already resolved");
|
|
1887
1932
|
return;
|
|
1888
1933
|
}
|
|
1889
1934
|
this.responses.set(id, { ...message, receivedAt: Date.now() });
|
|
@@ -1921,7 +1966,7 @@ class PermissionHandler {
|
|
|
1921
1966
|
}
|
|
1922
1967
|
|
|
1923
1968
|
function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
|
|
1924
|
-
types
|
|
1969
|
+
types.logger.debugLargeJson("[CLAUDE INK] Message from remote mode:", message);
|
|
1925
1970
|
switch (message.type) {
|
|
1926
1971
|
case "system": {
|
|
1927
1972
|
const sysMsg = message;
|
|
@@ -2016,7 +2061,7 @@ function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
|
|
|
2016
2061
|
} else if (resultMsg.subtype === "error_during_execution") {
|
|
2017
2062
|
messageBuffer.addMessage("\u274C Error during execution", "result");
|
|
2018
2063
|
messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns before error`, "status");
|
|
2019
|
-
types
|
|
2064
|
+
types.logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
|
|
2020
2065
|
}
|
|
2021
2066
|
break;
|
|
2022
2067
|
}
|
|
@@ -2263,7 +2308,7 @@ class OutgoingMessageQueue {
|
|
|
2263
2308
|
}
|
|
2264
2309
|
queue = [];
|
|
2265
2310
|
nextId = 1;
|
|
2266
|
-
lock = new types
|
|
2311
|
+
lock = new types.AsyncLock();
|
|
2267
2312
|
processTimer;
|
|
2268
2313
|
delayTimers = /* @__PURE__ */ new Map();
|
|
2269
2314
|
/**
|
|
@@ -2395,9 +2440,9 @@ class OutgoingMessageQueue {
|
|
|
2395
2440
|
}
|
|
2396
2441
|
|
|
2397
2442
|
async function claudeRemoteLauncher(session) {
|
|
2398
|
-
types
|
|
2443
|
+
types.logger.debug("[claudeRemoteLauncher] Starting remote launcher");
|
|
2399
2444
|
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
2400
|
-
types
|
|
2445
|
+
types.logger.debug(`[claudeRemoteLauncher] TTY available: ${hasTTY}`);
|
|
2401
2446
|
let messageBuffer = new MessageBuffer();
|
|
2402
2447
|
let inkInstance = null;
|
|
2403
2448
|
if (hasTTY) {
|
|
@@ -2406,14 +2451,14 @@ async function claudeRemoteLauncher(session) {
|
|
|
2406
2451
|
messageBuffer,
|
|
2407
2452
|
logPath: process.env.DEBUG ? session.logPath : void 0,
|
|
2408
2453
|
onExit: async () => {
|
|
2409
|
-
types
|
|
2454
|
+
types.logger.debug("[remote]: Exiting client via Ctrl-C");
|
|
2410
2455
|
if (!exitReason) {
|
|
2411
2456
|
exitReason = "exit";
|
|
2412
2457
|
}
|
|
2413
2458
|
await abort();
|
|
2414
2459
|
},
|
|
2415
2460
|
onSwitchToLocal: () => {
|
|
2416
|
-
types
|
|
2461
|
+
types.logger.debug("[remote]: Switching to local mode via double space");
|
|
2417
2462
|
doSwitch();
|
|
2418
2463
|
}
|
|
2419
2464
|
}), {
|
|
@@ -2438,11 +2483,11 @@ async function claudeRemoteLauncher(session) {
|
|
|
2438
2483
|
await abortFuture?.promise;
|
|
2439
2484
|
}
|
|
2440
2485
|
async function doAbort() {
|
|
2441
|
-
types
|
|
2486
|
+
types.logger.debug("[remote]: doAbort");
|
|
2442
2487
|
await abort();
|
|
2443
2488
|
}
|
|
2444
2489
|
async function doSwitch() {
|
|
2445
|
-
types
|
|
2490
|
+
types.logger.debug("[remote]: doSwitch");
|
|
2446
2491
|
if (!exitReason) {
|
|
2447
2492
|
exitReason = "switch";
|
|
2448
2493
|
}
|
|
@@ -2472,7 +2517,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2472
2517
|
if (umessage.message.content && Array.isArray(umessage.message.content)) {
|
|
2473
2518
|
for (let c of umessage.message.content) {
|
|
2474
2519
|
if (c.type === "tool_use" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
|
|
2475
|
-
types
|
|
2520
|
+
types.logger.debug("[remote]: detected plan mode tool call " + c.id);
|
|
2476
2521
|
planModeToolCalls.add(c.id);
|
|
2477
2522
|
}
|
|
2478
2523
|
}
|
|
@@ -2483,7 +2528,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2483
2528
|
if (umessage.message.content && Array.isArray(umessage.message.content)) {
|
|
2484
2529
|
for (let c of umessage.message.content) {
|
|
2485
2530
|
if (c.type === "tool_use") {
|
|
2486
|
-
types
|
|
2531
|
+
types.logger.debug("[remote]: detected tool use " + c.id + " parent: " + umessage.parent_tool_use_id);
|
|
2487
2532
|
ongoingToolCalls.set(c.id, { parentToolCallId: umessage.parent_tool_use_id ?? null });
|
|
2488
2533
|
}
|
|
2489
2534
|
}
|
|
@@ -2511,8 +2556,8 @@ async function claudeRemoteLauncher(session) {
|
|
|
2511
2556
|
content: umessage.message.content.map((c) => {
|
|
2512
2557
|
if (c.type === "tool_result" && c.tool_use_id && planModeToolCalls.has(c.tool_use_id)) {
|
|
2513
2558
|
if (c.content === PLAN_FAKE_REJECT) {
|
|
2514
|
-
types
|
|
2515
|
-
types
|
|
2559
|
+
types.logger.debug("[remote]: hack plan mode exit");
|
|
2560
|
+
types.logger.debugLargeJson("[remote]: hack plan mode exit", c);
|
|
2516
2561
|
return {
|
|
2517
2562
|
...c,
|
|
2518
2563
|
is_error: false,
|
|
@@ -2597,7 +2642,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2597
2642
|
try {
|
|
2598
2643
|
let pending = null;
|
|
2599
2644
|
while (!exitReason) {
|
|
2600
|
-
types
|
|
2645
|
+
types.logger.debug("[remote]: launch");
|
|
2601
2646
|
messageBuffer.addMessage("\u2550".repeat(40), "status");
|
|
2602
2647
|
messageBuffer.addMessage("Starting new Claude session...", "status");
|
|
2603
2648
|
const controller = new AbortController();
|
|
@@ -2608,7 +2653,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2608
2653
|
let modeHash = null;
|
|
2609
2654
|
let mode = null;
|
|
2610
2655
|
try {
|
|
2611
|
-
await claudeRemote({
|
|
2656
|
+
const remoteResult = await claudeRemote({
|
|
2612
2657
|
sessionId: session.sessionId,
|
|
2613
2658
|
path: session.path,
|
|
2614
2659
|
allowedTools: session.allowedTools ?? [],
|
|
@@ -2627,7 +2672,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2627
2672
|
let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
|
|
2628
2673
|
if (msg) {
|
|
2629
2674
|
if (modeHash && msg.hash !== modeHash || msg.isolate) {
|
|
2630
|
-
types
|
|
2675
|
+
types.logger.debug("[remote]: mode has changed, pending message");
|
|
2631
2676
|
pending = msg;
|
|
2632
2677
|
return null;
|
|
2633
2678
|
}
|
|
@@ -2650,11 +2695,11 @@ async function claudeRemoteLauncher(session) {
|
|
|
2650
2695
|
claudeArgs: session.claudeArgs,
|
|
2651
2696
|
onMessage,
|
|
2652
2697
|
onCompletionEvent: (message) => {
|
|
2653
|
-
types
|
|
2698
|
+
types.logger.debug(`[remote]: Completion event: ${message}`);
|
|
2654
2699
|
session.client.sendSessionEvent({ type: "message", message });
|
|
2655
2700
|
},
|
|
2656
2701
|
onSessionReset: () => {
|
|
2657
|
-
types
|
|
2702
|
+
types.logger.debug("[remote]: Session reset");
|
|
2658
2703
|
session.clearSessionId();
|
|
2659
2704
|
},
|
|
2660
2705
|
onReady: () => {
|
|
@@ -2669,33 +2714,34 @@ async function claudeRemoteLauncher(session) {
|
|
|
2669
2714
|
},
|
|
2670
2715
|
signal: abortController.signal
|
|
2671
2716
|
});
|
|
2717
|
+
session.consumeOneTimeFlags();
|
|
2672
2718
|
if (!exitReason && abortController.signal.aborted) {
|
|
2673
2719
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
2674
2720
|
}
|
|
2675
2721
|
} catch (e) {
|
|
2676
|
-
types
|
|
2722
|
+
types.logger.debug("[remote]: launch error", e);
|
|
2677
2723
|
if (!exitReason) {
|
|
2678
2724
|
session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
2679
2725
|
continue;
|
|
2680
2726
|
}
|
|
2681
2727
|
} finally {
|
|
2682
|
-
types
|
|
2728
|
+
types.logger.debug("[remote]: launch finally");
|
|
2683
2729
|
for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
|
|
2684
2730
|
const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
|
|
2685
2731
|
if (converted) {
|
|
2686
|
-
types
|
|
2732
|
+
types.logger.debug("[remote]: terminating tool call " + toolCallId + " parent: " + parentToolCallId);
|
|
2687
2733
|
session.client.sendClaudeSessionMessage(converted);
|
|
2688
2734
|
}
|
|
2689
2735
|
}
|
|
2690
2736
|
ongoingToolCalls.clear();
|
|
2691
|
-
types
|
|
2737
|
+
types.logger.debug("[remote]: flushing message queue");
|
|
2692
2738
|
await messageQueue.flush();
|
|
2693
2739
|
messageQueue.destroy();
|
|
2694
|
-
types
|
|
2740
|
+
types.logger.debug("[remote]: message queue flushed");
|
|
2695
2741
|
abortController = null;
|
|
2696
2742
|
abortFuture?.resolve(void 0);
|
|
2697
2743
|
abortFuture = null;
|
|
2698
|
-
types
|
|
2744
|
+
types.logger.debug("[remote]: launch done");
|
|
2699
2745
|
permissionHandler.reset();
|
|
2700
2746
|
modeHash = null;
|
|
2701
2747
|
mode = null;
|
|
@@ -2719,7 +2765,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2719
2765
|
}
|
|
2720
2766
|
|
|
2721
2767
|
async function loop(opts) {
|
|
2722
|
-
const logPath =
|
|
2768
|
+
const logPath = types.logger.logFilePath;
|
|
2723
2769
|
let session = new Session({
|
|
2724
2770
|
api: opts.api,
|
|
2725
2771
|
client: opts.session,
|
|
@@ -2738,7 +2784,7 @@ async function loop(opts) {
|
|
|
2738
2784
|
}
|
|
2739
2785
|
let mode = opts.startingMode ?? "local";
|
|
2740
2786
|
while (true) {
|
|
2741
|
-
types
|
|
2787
|
+
types.logger.debug(`[loop] Iteration with mode: ${mode}`);
|
|
2742
2788
|
if (mode === "local") {
|
|
2743
2789
|
let reason = await claudeLocalLauncher(session);
|
|
2744
2790
|
if (reason === "exit") {
|
|
@@ -2764,133 +2810,6 @@ async function loop(opts) {
|
|
|
2764
2810
|
}
|
|
2765
2811
|
}
|
|
2766
2812
|
|
|
2767
|
-
var name = "happy-coder";
|
|
2768
|
-
var version = "0.9.0-6";
|
|
2769
|
-
var description = "Claude Code session sharing CLI";
|
|
2770
|
-
var author = "Kirill Dubovitskiy";
|
|
2771
|
-
var license = "MIT";
|
|
2772
|
-
var type = "module";
|
|
2773
|
-
var homepage = "https://github.com/slopus/happy-cli";
|
|
2774
|
-
var bugs = "https://github.com/slopus/happy-cli/issues";
|
|
2775
|
-
var repository = "slopus/happy-cli";
|
|
2776
|
-
var bin = {
|
|
2777
|
-
happy: "./bin/happy.mjs"
|
|
2778
|
-
};
|
|
2779
|
-
var main = "./dist/index.cjs";
|
|
2780
|
-
var module$1 = "./dist/index.mjs";
|
|
2781
|
-
var types = "./dist/index.d.cts";
|
|
2782
|
-
var exports$1 = {
|
|
2783
|
-
".": {
|
|
2784
|
-
require: {
|
|
2785
|
-
types: "./dist/index.d.cts",
|
|
2786
|
-
"default": "./dist/index.cjs"
|
|
2787
|
-
},
|
|
2788
|
-
"import": {
|
|
2789
|
-
types: "./dist/index.d.mts",
|
|
2790
|
-
"default": "./dist/index.mjs"
|
|
2791
|
-
}
|
|
2792
|
-
},
|
|
2793
|
-
"./lib": {
|
|
2794
|
-
require: {
|
|
2795
|
-
types: "./dist/lib.d.cts",
|
|
2796
|
-
"default": "./dist/lib.cjs"
|
|
2797
|
-
},
|
|
2798
|
-
"import": {
|
|
2799
|
-
types: "./dist/lib.d.mts",
|
|
2800
|
-
"default": "./dist/lib.mjs"
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
};
|
|
2804
|
-
var files = [
|
|
2805
|
-
"dist",
|
|
2806
|
-
"bin",
|
|
2807
|
-
"scripts",
|
|
2808
|
-
"ripgrep",
|
|
2809
|
-
"package.json"
|
|
2810
|
-
];
|
|
2811
|
-
var scripts = {
|
|
2812
|
-
"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",
|
|
2813
|
-
typecheck: "tsc --noEmit",
|
|
2814
|
-
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
|
|
2815
|
-
test: "yarn build && vitest run",
|
|
2816
|
-
"test:watch": "vitest",
|
|
2817
|
-
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2818
|
-
dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
|
|
2819
|
-
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2820
|
-
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2821
|
-
prepublishOnly: "yarn build && yarn test",
|
|
2822
|
-
release: "release-it"
|
|
2823
|
-
};
|
|
2824
|
-
var dependencies = {
|
|
2825
|
-
"@anthropic-ai/claude-code": "^1.0.89",
|
|
2826
|
-
"@anthropic-ai/sdk": "^0.56.0",
|
|
2827
|
-
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
2828
|
-
"@stablelib/base64": "^2.0.1",
|
|
2829
|
-
"@types/http-proxy": "^1.17.16",
|
|
2830
|
-
"@types/qrcode-terminal": "^0.12.2",
|
|
2831
|
-
"@types/react": "^19.1.9",
|
|
2832
|
-
axios: "^1.10.0",
|
|
2833
|
-
chalk: "^5.4.1",
|
|
2834
|
-
"expo-server-sdk": "^3.15.0",
|
|
2835
|
-
fastify: "^5.5.0",
|
|
2836
|
-
"fastify-type-provider-zod": "4.0.2",
|
|
2837
|
-
"http-proxy": "^1.18.1",
|
|
2838
|
-
"http-proxy-middleware": "^3.0.5",
|
|
2839
|
-
ink: "^6.1.0",
|
|
2840
|
-
open: "^10.2.0",
|
|
2841
|
-
"qrcode-terminal": "^0.12.0",
|
|
2842
|
-
react: "^19.1.1",
|
|
2843
|
-
"socket.io-client": "^4.8.1",
|
|
2844
|
-
tweetnacl: "^1.0.3",
|
|
2845
|
-
zod: "^3.23.8"
|
|
2846
|
-
};
|
|
2847
|
-
var devDependencies = {
|
|
2848
|
-
"@eslint/compat": "^1",
|
|
2849
|
-
"@types/node": ">=20",
|
|
2850
|
-
"cross-env": "^10.0.0",
|
|
2851
|
-
eslint: "^9",
|
|
2852
|
-
"eslint-config-prettier": "^10",
|
|
2853
|
-
pkgroll: "^2.14.2",
|
|
2854
|
-
"release-it": "^19.0.4",
|
|
2855
|
-
shx: "^0.3.3",
|
|
2856
|
-
"ts-node": "^10",
|
|
2857
|
-
tsx: "^4.20.3",
|
|
2858
|
-
typescript: "^5",
|
|
2859
|
-
vitest: "^3.2.4"
|
|
2860
|
-
};
|
|
2861
|
-
var resolutions = {
|
|
2862
|
-
"whatwg-url": "14.2.0",
|
|
2863
|
-
"parse-path": "7.0.3",
|
|
2864
|
-
"@types/parse-path": "7.0.3"
|
|
2865
|
-
};
|
|
2866
|
-
var publishConfig = {
|
|
2867
|
-
registry: "https://registry.npmjs.org"
|
|
2868
|
-
};
|
|
2869
|
-
var packageManager = "yarn@1.22.22";
|
|
2870
|
-
var packageJson = {
|
|
2871
|
-
name: name,
|
|
2872
|
-
version: version,
|
|
2873
|
-
description: description,
|
|
2874
|
-
author: author,
|
|
2875
|
-
license: license,
|
|
2876
|
-
type: type,
|
|
2877
|
-
homepage: homepage,
|
|
2878
|
-
bugs: bugs,
|
|
2879
|
-
repository: repository,
|
|
2880
|
-
bin: bin,
|
|
2881
|
-
main: main,
|
|
2882
|
-
module: module$1,
|
|
2883
|
-
types: types,
|
|
2884
|
-
exports: exports$1,
|
|
2885
|
-
files: files,
|
|
2886
|
-
scripts: scripts,
|
|
2887
|
-
dependencies: dependencies,
|
|
2888
|
-
devDependencies: devDependencies,
|
|
2889
|
-
resolutions: resolutions,
|
|
2890
|
-
publishConfig: publishConfig,
|
|
2891
|
-
packageManager: packageManager
|
|
2892
|
-
};
|
|
2893
|
-
|
|
2894
2813
|
function run(args, options) {
|
|
2895
2814
|
const RUNNER_PATH = path.resolve(path.join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
2896
2815
|
return new Promise((resolve2, reject) => {
|
|
@@ -2922,7 +2841,7 @@ function run(args, options) {
|
|
|
2922
2841
|
const execAsync = util.promisify(child_process.exec);
|
|
2923
2842
|
function registerHandlers(session) {
|
|
2924
2843
|
session.setHandler("bash", async (data) => {
|
|
2925
|
-
types
|
|
2844
|
+
types.logger.debug("Shell command request:", data.command);
|
|
2926
2845
|
try {
|
|
2927
2846
|
const options = {
|
|
2928
2847
|
cwd: data.cwd,
|
|
@@ -2957,22 +2876,22 @@ function registerHandlers(session) {
|
|
|
2957
2876
|
}
|
|
2958
2877
|
});
|
|
2959
2878
|
session.setHandler("readFile", async (data) => {
|
|
2960
|
-
types
|
|
2879
|
+
types.logger.debug("Read file request:", data.path);
|
|
2961
2880
|
try {
|
|
2962
|
-
const buffer = await
|
|
2881
|
+
const buffer = await fs.readFile(data.path);
|
|
2963
2882
|
const content = buffer.toString("base64");
|
|
2964
2883
|
return { success: true, content };
|
|
2965
2884
|
} catch (error) {
|
|
2966
|
-
types
|
|
2885
|
+
types.logger.debug("Failed to read file:", error);
|
|
2967
2886
|
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
2968
2887
|
}
|
|
2969
2888
|
});
|
|
2970
2889
|
session.setHandler("writeFile", async (data) => {
|
|
2971
|
-
types
|
|
2890
|
+
types.logger.debug("Write file request:", data.path);
|
|
2972
2891
|
try {
|
|
2973
2892
|
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
2974
2893
|
try {
|
|
2975
|
-
const existingBuffer = await
|
|
2894
|
+
const existingBuffer = await fs.readFile(data.path);
|
|
2976
2895
|
const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
|
|
2977
2896
|
if (existingHash !== data.expectedHash) {
|
|
2978
2897
|
return {
|
|
@@ -2992,7 +2911,7 @@ function registerHandlers(session) {
|
|
|
2992
2911
|
}
|
|
2993
2912
|
} else {
|
|
2994
2913
|
try {
|
|
2995
|
-
await
|
|
2914
|
+
await fs.stat(data.path);
|
|
2996
2915
|
return {
|
|
2997
2916
|
success: false,
|
|
2998
2917
|
error: "File already exists but was expected to be new"
|
|
@@ -3005,18 +2924,18 @@ function registerHandlers(session) {
|
|
|
3005
2924
|
}
|
|
3006
2925
|
}
|
|
3007
2926
|
const buffer = Buffer.from(data.content, "base64");
|
|
3008
|
-
await
|
|
2927
|
+
await fs.writeFile(data.path, buffer);
|
|
3009
2928
|
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
3010
2929
|
return { success: true, hash };
|
|
3011
2930
|
} catch (error) {
|
|
3012
|
-
types
|
|
2931
|
+
types.logger.debug("Failed to write file:", error);
|
|
3013
2932
|
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
3014
2933
|
}
|
|
3015
2934
|
});
|
|
3016
2935
|
session.setHandler("listDirectory", async (data) => {
|
|
3017
|
-
types
|
|
2936
|
+
types.logger.debug("List directory request:", data.path);
|
|
3018
2937
|
try {
|
|
3019
|
-
const entries = await
|
|
2938
|
+
const entries = await fs.readdir(data.path, { withFileTypes: true });
|
|
3020
2939
|
const directoryEntries = await Promise.all(
|
|
3021
2940
|
entries.map(async (entry) => {
|
|
3022
2941
|
const fullPath = path.join(data.path, entry.name);
|
|
@@ -3029,11 +2948,11 @@ function registerHandlers(session) {
|
|
|
3029
2948
|
type = "file";
|
|
3030
2949
|
}
|
|
3031
2950
|
try {
|
|
3032
|
-
const stats = await
|
|
2951
|
+
const stats = await fs.stat(fullPath);
|
|
3033
2952
|
size = stats.size;
|
|
3034
2953
|
modified = stats.mtime.getTime();
|
|
3035
2954
|
} catch (error) {
|
|
3036
|
-
types
|
|
2955
|
+
types.logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
3037
2956
|
}
|
|
3038
2957
|
return {
|
|
3039
2958
|
name: entry.name,
|
|
@@ -3050,15 +2969,15 @@ function registerHandlers(session) {
|
|
|
3050
2969
|
});
|
|
3051
2970
|
return { success: true, entries: directoryEntries };
|
|
3052
2971
|
} catch (error) {
|
|
3053
|
-
types
|
|
2972
|
+
types.logger.debug("Failed to list directory:", error);
|
|
3054
2973
|
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
3055
2974
|
}
|
|
3056
2975
|
});
|
|
3057
2976
|
session.setHandler("getDirectoryTree", async (data) => {
|
|
3058
|
-
types
|
|
2977
|
+
types.logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
3059
2978
|
async function buildTree(path$1, name, currentDepth) {
|
|
3060
2979
|
try {
|
|
3061
|
-
const stats = await
|
|
2980
|
+
const stats = await fs.stat(path$1);
|
|
3062
2981
|
const node = {
|
|
3063
2982
|
name,
|
|
3064
2983
|
path: path$1,
|
|
@@ -3067,12 +2986,12 @@ function registerHandlers(session) {
|
|
|
3067
2986
|
modified: stats.mtime.getTime()
|
|
3068
2987
|
};
|
|
3069
2988
|
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
3070
|
-
const entries = await
|
|
2989
|
+
const entries = await fs.readdir(path$1, { withFileTypes: true });
|
|
3071
2990
|
const children = [];
|
|
3072
2991
|
await Promise.all(
|
|
3073
2992
|
entries.map(async (entry) => {
|
|
3074
2993
|
if (entry.isSymbolicLink()) {
|
|
3075
|
-
types
|
|
2994
|
+
types.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
|
|
3076
2995
|
return;
|
|
3077
2996
|
}
|
|
3078
2997
|
const childPath = path.join(path$1, entry.name);
|
|
@@ -3091,7 +3010,7 @@ function registerHandlers(session) {
|
|
|
3091
3010
|
}
|
|
3092
3011
|
return node;
|
|
3093
3012
|
} catch (error) {
|
|
3094
|
-
types
|
|
3013
|
+
types.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
|
|
3095
3014
|
return null;
|
|
3096
3015
|
}
|
|
3097
3016
|
}
|
|
@@ -3106,12 +3025,12 @@ function registerHandlers(session) {
|
|
|
3106
3025
|
}
|
|
3107
3026
|
return { success: true, tree };
|
|
3108
3027
|
} catch (error) {
|
|
3109
|
-
types
|
|
3028
|
+
types.logger.debug("Failed to get directory tree:", error);
|
|
3110
3029
|
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
3111
3030
|
}
|
|
3112
3031
|
});
|
|
3113
3032
|
session.setHandler("ripgrep", async (data) => {
|
|
3114
|
-
types
|
|
3033
|
+
types.logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
3115
3034
|
try {
|
|
3116
3035
|
const result = await run(data.args, { cwd: data.cwd });
|
|
3117
3036
|
return {
|
|
@@ -3121,7 +3040,7 @@ function registerHandlers(session) {
|
|
|
3121
3040
|
stderr: result.stderr.toString()
|
|
3122
3041
|
};
|
|
3123
3042
|
} catch (error) {
|
|
3124
|
-
types
|
|
3043
|
+
types.logger.debug("Failed to run ripgrep:", error);
|
|
3125
3044
|
return {
|
|
3126
3045
|
success: false,
|
|
3127
3046
|
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
@@ -3129,19 +3048,19 @@ function registerHandlers(session) {
|
|
|
3129
3048
|
}
|
|
3130
3049
|
});
|
|
3131
3050
|
session.setHandler("killSession", async () => {
|
|
3132
|
-
types
|
|
3051
|
+
types.logger.debug("Kill session request received");
|
|
3133
3052
|
try {
|
|
3134
3053
|
const response = {
|
|
3135
3054
|
success: true,
|
|
3136
3055
|
message: "Session termination acknowledged, exiting in 100ms"
|
|
3137
3056
|
};
|
|
3138
3057
|
setTimeout(() => {
|
|
3139
|
-
types
|
|
3058
|
+
types.logger.debug("[KILL SESSION] Exiting process as requested");
|
|
3140
3059
|
process.exit(0);
|
|
3141
3060
|
}, 100);
|
|
3142
3061
|
return response;
|
|
3143
3062
|
} catch (error) {
|
|
3144
|
-
types
|
|
3063
|
+
types.logger.debug("Failed to kill session:", error);
|
|
3145
3064
|
return {
|
|
3146
3065
|
success: false,
|
|
3147
3066
|
message: error instanceof Error ? error.message : "Failed to kill session"
|
|
@@ -3150,129 +3069,6 @@ function registerHandlers(session) {
|
|
|
3150
3069
|
});
|
|
3151
3070
|
}
|
|
3152
3071
|
|
|
3153
|
-
const defaultSettings = {
|
|
3154
|
-
onboardingCompleted: false
|
|
3155
|
-
};
|
|
3156
|
-
async function readSettings() {
|
|
3157
|
-
if (!node_fs.existsSync(types$1.configuration.settingsFile)) {
|
|
3158
|
-
return { ...defaultSettings };
|
|
3159
|
-
}
|
|
3160
|
-
try {
|
|
3161
|
-
const content = await promises$1.readFile(types$1.configuration.settingsFile, "utf8");
|
|
3162
|
-
return JSON.parse(content);
|
|
3163
|
-
} catch {
|
|
3164
|
-
return { ...defaultSettings };
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
async function updateSettings(updater) {
|
|
3168
|
-
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
3169
|
-
const MAX_LOCK_ATTEMPTS = 50;
|
|
3170
|
-
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
3171
|
-
const lockFile = types$1.configuration.settingsFile + ".lock";
|
|
3172
|
-
const tmpFile = types$1.configuration.settingsFile + ".tmp";
|
|
3173
|
-
let fileHandle;
|
|
3174
|
-
let attempts = 0;
|
|
3175
|
-
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
3176
|
-
try {
|
|
3177
|
-
fileHandle = await promises$1.open(lockFile, node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY);
|
|
3178
|
-
break;
|
|
3179
|
-
} catch (err) {
|
|
3180
|
-
if (err.code === "EEXIST") {
|
|
3181
|
-
attempts++;
|
|
3182
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
3183
|
-
try {
|
|
3184
|
-
const stats = await promises$1.stat(lockFile);
|
|
3185
|
-
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
3186
|
-
await promises$1.unlink(lockFile).catch(() => {
|
|
3187
|
-
});
|
|
3188
|
-
}
|
|
3189
|
-
} catch {
|
|
3190
|
-
}
|
|
3191
|
-
} else {
|
|
3192
|
-
throw err;
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
if (!fileHandle) {
|
|
3197
|
-
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
3198
|
-
}
|
|
3199
|
-
try {
|
|
3200
|
-
const current = await readSettings() || { ...defaultSettings };
|
|
3201
|
-
const updated = await updater(current);
|
|
3202
|
-
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
3203
|
-
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
3204
|
-
}
|
|
3205
|
-
await promises$1.writeFile(tmpFile, JSON.stringify(updated, null, 2));
|
|
3206
|
-
await promises$1.rename(tmpFile, types$1.configuration.settingsFile);
|
|
3207
|
-
return updated;
|
|
3208
|
-
} finally {
|
|
3209
|
-
await fileHandle.close();
|
|
3210
|
-
await promises$1.unlink(lockFile).catch(() => {
|
|
3211
|
-
});
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
const credentialsSchema = z__namespace.object({
|
|
3215
|
-
secret: z__namespace.string().base64(),
|
|
3216
|
-
token: z__namespace.string()
|
|
3217
|
-
});
|
|
3218
|
-
async function readCredentials() {
|
|
3219
|
-
if (!node_fs.existsSync(types$1.configuration.privateKeyFile)) {
|
|
3220
|
-
return null;
|
|
3221
|
-
}
|
|
3222
|
-
try {
|
|
3223
|
-
const keyBase64 = await promises$1.readFile(types$1.configuration.privateKeyFile, "utf8");
|
|
3224
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
3225
|
-
return {
|
|
3226
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
3227
|
-
token: credentials.token
|
|
3228
|
-
};
|
|
3229
|
-
} catch {
|
|
3230
|
-
return null;
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
async function writeCredentials(credentials) {
|
|
3234
|
-
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
3235
|
-
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
3236
|
-
}
|
|
3237
|
-
await promises$1.writeFile(types$1.configuration.privateKeyFile, JSON.stringify({
|
|
3238
|
-
secret: types$1.encodeBase64(credentials.secret),
|
|
3239
|
-
token: credentials.token
|
|
3240
|
-
}, null, 2));
|
|
3241
|
-
}
|
|
3242
|
-
async function clearCredentials() {
|
|
3243
|
-
if (node_fs.existsSync(types$1.configuration.privateKeyFile)) {
|
|
3244
|
-
await promises$1.unlink(types$1.configuration.privateKeyFile);
|
|
3245
|
-
}
|
|
3246
|
-
}
|
|
3247
|
-
async function clearMachineId() {
|
|
3248
|
-
await updateSettings((settings) => ({
|
|
3249
|
-
...settings,
|
|
3250
|
-
machineId: void 0
|
|
3251
|
-
}));
|
|
3252
|
-
}
|
|
3253
|
-
async function readDaemonState() {
|
|
3254
|
-
try {
|
|
3255
|
-
if (!node_fs.existsSync(types$1.configuration.daemonStateFile)) {
|
|
3256
|
-
return null;
|
|
3257
|
-
}
|
|
3258
|
-
const content = await promises$1.readFile(types$1.configuration.daemonStateFile, "utf-8");
|
|
3259
|
-
return JSON.parse(content);
|
|
3260
|
-
} catch (error) {
|
|
3261
|
-
return null;
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
async function writeDaemonState(state) {
|
|
3265
|
-
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
3266
|
-
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
3267
|
-
}
|
|
3268
|
-
await promises$1.writeFile(types$1.configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
3269
|
-
}
|
|
3270
|
-
async function clearDaemonState() {
|
|
3271
|
-
if (node_fs.existsSync(types$1.configuration.daemonStateFile)) {
|
|
3272
|
-
await promises$1.unlink(types$1.configuration.daemonStateFile);
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
|
|
3276
3072
|
class MessageQueue2 {
|
|
3277
3073
|
queue = [];
|
|
3278
3074
|
// Made public for testing
|
|
@@ -3283,7 +3079,7 @@ class MessageQueue2 {
|
|
|
3283
3079
|
constructor(modeHasher, onMessageHandler = null) {
|
|
3284
3080
|
this.modeHasher = modeHasher;
|
|
3285
3081
|
this.onMessageHandler = onMessageHandler;
|
|
3286
|
-
types
|
|
3082
|
+
types.logger.debug(`[MessageQueue2] Initialized`);
|
|
3287
3083
|
}
|
|
3288
3084
|
/**
|
|
3289
3085
|
* Set a handler that will be called when a message arrives
|
|
@@ -3299,7 +3095,7 @@ class MessageQueue2 {
|
|
|
3299
3095
|
throw new Error("Cannot push to closed queue");
|
|
3300
3096
|
}
|
|
3301
3097
|
const modeHash = this.modeHasher(mode);
|
|
3302
|
-
types
|
|
3098
|
+
types.logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
|
|
3303
3099
|
this.queue.push({
|
|
3304
3100
|
message,
|
|
3305
3101
|
mode,
|
|
@@ -3310,12 +3106,12 @@ class MessageQueue2 {
|
|
|
3310
3106
|
this.onMessageHandler(message, mode);
|
|
3311
3107
|
}
|
|
3312
3108
|
if (this.waiter) {
|
|
3313
|
-
types
|
|
3109
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter`);
|
|
3314
3110
|
const waiter = this.waiter;
|
|
3315
3111
|
this.waiter = null;
|
|
3316
3112
|
waiter(true);
|
|
3317
3113
|
}
|
|
3318
|
-
types
|
|
3114
|
+
types.logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
|
|
3319
3115
|
}
|
|
3320
3116
|
/**
|
|
3321
3117
|
* Push a message immediately without batching delay.
|
|
@@ -3326,7 +3122,7 @@ class MessageQueue2 {
|
|
|
3326
3122
|
throw new Error("Cannot push to closed queue");
|
|
3327
3123
|
}
|
|
3328
3124
|
const modeHash = this.modeHasher(mode);
|
|
3329
|
-
types
|
|
3125
|
+
types.logger.debug(`[MessageQueue2] pushImmediate() called with mode hash: ${modeHash}`);
|
|
3330
3126
|
this.queue.push({
|
|
3331
3127
|
message,
|
|
3332
3128
|
mode,
|
|
@@ -3337,12 +3133,12 @@ class MessageQueue2 {
|
|
|
3337
3133
|
this.onMessageHandler(message, mode);
|
|
3338
3134
|
}
|
|
3339
3135
|
if (this.waiter) {
|
|
3340
|
-
types
|
|
3136
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter for immediate message`);
|
|
3341
3137
|
const waiter = this.waiter;
|
|
3342
3138
|
this.waiter = null;
|
|
3343
3139
|
waiter(true);
|
|
3344
3140
|
}
|
|
3345
|
-
types
|
|
3141
|
+
types.logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
|
|
3346
3142
|
}
|
|
3347
3143
|
/**
|
|
3348
3144
|
* Push a message that must be processed in complete isolation.
|
|
@@ -3354,7 +3150,7 @@ class MessageQueue2 {
|
|
|
3354
3150
|
throw new Error("Cannot push to closed queue");
|
|
3355
3151
|
}
|
|
3356
3152
|
const modeHash = this.modeHasher(mode);
|
|
3357
|
-
types
|
|
3153
|
+
types.logger.debug(`[MessageQueue2] pushIsolateAndClear() called with mode hash: ${modeHash} - clearing ${this.queue.length} pending messages`);
|
|
3358
3154
|
this.queue = [];
|
|
3359
3155
|
this.queue.push({
|
|
3360
3156
|
message,
|
|
@@ -3366,12 +3162,12 @@ class MessageQueue2 {
|
|
|
3366
3162
|
this.onMessageHandler(message, mode);
|
|
3367
3163
|
}
|
|
3368
3164
|
if (this.waiter) {
|
|
3369
|
-
types
|
|
3165
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
|
|
3370
3166
|
const waiter = this.waiter;
|
|
3371
3167
|
this.waiter = null;
|
|
3372
3168
|
waiter(true);
|
|
3373
3169
|
}
|
|
3374
|
-
types
|
|
3170
|
+
types.logger.debug(`[MessageQueue2] pushIsolateAndClear() completed. Queue size: ${this.queue.length}`);
|
|
3375
3171
|
}
|
|
3376
3172
|
/**
|
|
3377
3173
|
* Push a message to the beginning of the queue with a mode.
|
|
@@ -3381,7 +3177,7 @@ class MessageQueue2 {
|
|
|
3381
3177
|
throw new Error("Cannot unshift to closed queue");
|
|
3382
3178
|
}
|
|
3383
3179
|
const modeHash = this.modeHasher(mode);
|
|
3384
|
-
types
|
|
3180
|
+
types.logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
|
|
3385
3181
|
this.queue.unshift({
|
|
3386
3182
|
message,
|
|
3387
3183
|
mode,
|
|
@@ -3392,18 +3188,18 @@ class MessageQueue2 {
|
|
|
3392
3188
|
this.onMessageHandler(message, mode);
|
|
3393
3189
|
}
|
|
3394
3190
|
if (this.waiter) {
|
|
3395
|
-
types
|
|
3191
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter`);
|
|
3396
3192
|
const waiter = this.waiter;
|
|
3397
3193
|
this.waiter = null;
|
|
3398
3194
|
waiter(true);
|
|
3399
3195
|
}
|
|
3400
|
-
types
|
|
3196
|
+
types.logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
|
|
3401
3197
|
}
|
|
3402
3198
|
/**
|
|
3403
3199
|
* Reset the queue - clears all messages and resets to empty state
|
|
3404
3200
|
*/
|
|
3405
3201
|
reset() {
|
|
3406
|
-
types
|
|
3202
|
+
types.logger.debug(`[MessageQueue2] reset() called. Clearing ${this.queue.length} messages`);
|
|
3407
3203
|
this.queue = [];
|
|
3408
3204
|
this.closed = false;
|
|
3409
3205
|
this.waiter = null;
|
|
@@ -3412,7 +3208,7 @@ class MessageQueue2 {
|
|
|
3412
3208
|
* Close the queue - no more messages can be pushed
|
|
3413
3209
|
*/
|
|
3414
3210
|
close() {
|
|
3415
|
-
types
|
|
3211
|
+
types.logger.debug(`[MessageQueue2] close() called`);
|
|
3416
3212
|
this.closed = true;
|
|
3417
3213
|
if (this.waiter) {
|
|
3418
3214
|
const waiter = this.waiter;
|
|
@@ -3464,13 +3260,13 @@ class MessageQueue2 {
|
|
|
3464
3260
|
if (firstItem.isolate) {
|
|
3465
3261
|
const item = this.queue.shift();
|
|
3466
3262
|
sameModeMessages.push(item.message);
|
|
3467
|
-
types
|
|
3263
|
+
types.logger.debug(`[MessageQueue2] Collected isolated message with mode hash: ${targetModeHash}`);
|
|
3468
3264
|
} else {
|
|
3469
3265
|
while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash && !this.queue[0].isolate) {
|
|
3470
3266
|
const item = this.queue.shift();
|
|
3471
3267
|
sameModeMessages.push(item.message);
|
|
3472
3268
|
}
|
|
3473
|
-
types
|
|
3269
|
+
types.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
|
|
3474
3270
|
}
|
|
3475
3271
|
const combinedMessage = sameModeMessages.join("\n");
|
|
3476
3272
|
return {
|
|
@@ -3488,7 +3284,7 @@ class MessageQueue2 {
|
|
|
3488
3284
|
let abortHandler = null;
|
|
3489
3285
|
if (abortSignal) {
|
|
3490
3286
|
abortHandler = () => {
|
|
3491
|
-
types
|
|
3287
|
+
types.logger.debug("[MessageQueue2] Wait aborted");
|
|
3492
3288
|
if (this.waiter === waiterFunc) {
|
|
3493
3289
|
this.waiter = null;
|
|
3494
3290
|
}
|
|
@@ -3517,7 +3313,7 @@ class MessageQueue2 {
|
|
|
3517
3313
|
return;
|
|
3518
3314
|
}
|
|
3519
3315
|
this.waiter = waiterFunc;
|
|
3520
|
-
types
|
|
3316
|
+
types.logger.debug("[MessageQueue2] Waiting for messages...");
|
|
3521
3317
|
});
|
|
3522
3318
|
}
|
|
3523
3319
|
}
|
|
@@ -3611,11 +3407,11 @@ function hashObject(obj, options, encoding = "hex") {
|
|
|
3611
3407
|
let caffeinateProcess = null;
|
|
3612
3408
|
function startCaffeinate() {
|
|
3613
3409
|
if (process.platform !== "darwin") {
|
|
3614
|
-
types
|
|
3410
|
+
types.logger.debug("[caffeinate] Not on macOS, skipping caffeinate");
|
|
3615
3411
|
return false;
|
|
3616
3412
|
}
|
|
3617
3413
|
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3618
|
-
types
|
|
3414
|
+
types.logger.debug("[caffeinate] Caffeinate already running");
|
|
3619
3415
|
return true;
|
|
3620
3416
|
}
|
|
3621
3417
|
try {
|
|
@@ -3624,41 +3420,41 @@ function startCaffeinate() {
|
|
|
3624
3420
|
detached: false
|
|
3625
3421
|
});
|
|
3626
3422
|
caffeinateProcess.on("error", (error) => {
|
|
3627
|
-
types
|
|
3423
|
+
types.logger.debug("[caffeinate] Error starting caffeinate:", error);
|
|
3628
3424
|
caffeinateProcess = null;
|
|
3629
3425
|
});
|
|
3630
3426
|
caffeinateProcess.on("exit", (code, signal) => {
|
|
3631
|
-
types
|
|
3427
|
+
types.logger.debug(`[caffeinate] Process exited with code ${code}, signal ${signal}`);
|
|
3632
3428
|
caffeinateProcess = null;
|
|
3633
3429
|
});
|
|
3634
|
-
types
|
|
3430
|
+
types.logger.debug(`[caffeinate] Started with PID ${caffeinateProcess.pid}`);
|
|
3635
3431
|
setupCleanupHandlers();
|
|
3636
3432
|
return true;
|
|
3637
3433
|
} catch (error) {
|
|
3638
|
-
types
|
|
3434
|
+
types.logger.debug("[caffeinate] Failed to start caffeinate:", error);
|
|
3639
3435
|
return false;
|
|
3640
3436
|
}
|
|
3641
3437
|
}
|
|
3642
3438
|
let isStopping = false;
|
|
3643
|
-
function stopCaffeinate() {
|
|
3439
|
+
async function stopCaffeinate() {
|
|
3644
3440
|
if (isStopping) {
|
|
3441
|
+
types.logger.debug("[caffeinate] Already stopping, skipping");
|
|
3645
3442
|
return;
|
|
3646
3443
|
}
|
|
3647
3444
|
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3648
3445
|
isStopping = true;
|
|
3649
|
-
types
|
|
3446
|
+
types.logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
|
|
3650
3447
|
try {
|
|
3651
3448
|
caffeinateProcess.kill("SIGTERM");
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
}, 1e3);
|
|
3449
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3450
|
+
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3451
|
+
types.logger.debug("[caffeinate] Force killing caffeinate process");
|
|
3452
|
+
caffeinateProcess.kill("SIGKILL");
|
|
3453
|
+
}
|
|
3454
|
+
caffeinateProcess = null;
|
|
3455
|
+
isStopping = false;
|
|
3660
3456
|
} catch (error) {
|
|
3661
|
-
types
|
|
3457
|
+
types.logger.debug("[caffeinate] Error stopping caffeinate:", error);
|
|
3662
3458
|
isStopping = false;
|
|
3663
3459
|
}
|
|
3664
3460
|
}
|
|
@@ -3678,21 +3474,19 @@ function setupCleanupHandlers() {
|
|
|
3678
3474
|
process.on("SIGUSR1", cleanup);
|
|
3679
3475
|
process.on("SIGUSR2", cleanup);
|
|
3680
3476
|
process.on("uncaughtException", (error) => {
|
|
3681
|
-
types
|
|
3477
|
+
types.logger.debug("[caffeinate] Uncaught exception, cleaning up:", error);
|
|
3682
3478
|
cleanup();
|
|
3683
|
-
process.exit(1);
|
|
3684
3479
|
});
|
|
3685
3480
|
process.on("unhandledRejection", (reason, promise) => {
|
|
3686
|
-
types
|
|
3481
|
+
types.logger.debug("[caffeinate] Unhandled rejection, cleaning up:", reason);
|
|
3687
3482
|
cleanup();
|
|
3688
|
-
process.exit(1);
|
|
3689
3483
|
});
|
|
3690
3484
|
}
|
|
3691
3485
|
|
|
3692
3486
|
async function extractSDKMetadata() {
|
|
3693
3487
|
const abortController = new AbortController();
|
|
3694
3488
|
try {
|
|
3695
|
-
types
|
|
3489
|
+
types.logger.debug("[metadataExtractor] Starting SDK metadata extraction");
|
|
3696
3490
|
const sdkQuery = query({
|
|
3697
3491
|
prompt: "hello",
|
|
3698
3492
|
options: {
|
|
@@ -3708,19 +3502,19 @@ async function extractSDKMetadata() {
|
|
|
3708
3502
|
tools: systemMessage.tools,
|
|
3709
3503
|
slashCommands: systemMessage.slash_commands
|
|
3710
3504
|
};
|
|
3711
|
-
types
|
|
3505
|
+
types.logger.debug("[metadataExtractor] Captured SDK metadata:", metadata);
|
|
3712
3506
|
abortController.abort();
|
|
3713
3507
|
return metadata;
|
|
3714
3508
|
}
|
|
3715
3509
|
}
|
|
3716
|
-
types
|
|
3510
|
+
types.logger.debug("[metadataExtractor] No init message received from SDK");
|
|
3717
3511
|
return {};
|
|
3718
3512
|
} catch (error) {
|
|
3719
3513
|
if (error instanceof Error && error.name === "AbortError") {
|
|
3720
|
-
types
|
|
3514
|
+
types.logger.debug("[metadataExtractor] SDK query aborted after capturing metadata");
|
|
3721
3515
|
return {};
|
|
3722
3516
|
}
|
|
3723
|
-
types
|
|
3517
|
+
types.logger.debug("[metadataExtractor] Error extracting SDK metadata:", error);
|
|
3724
3518
|
return {};
|
|
3725
3519
|
}
|
|
3726
3520
|
}
|
|
@@ -3730,93 +3524,208 @@ function extractSDKMetadataAsync(onComplete) {
|
|
|
3730
3524
|
onComplete(metadata);
|
|
3731
3525
|
}
|
|
3732
3526
|
}).catch((error) => {
|
|
3733
|
-
types
|
|
3527
|
+
types.logger.debug("[metadataExtractor] Async extraction failed:", error);
|
|
3734
3528
|
});
|
|
3735
3529
|
}
|
|
3736
3530
|
|
|
3737
|
-
async function
|
|
3531
|
+
async function daemonPost(path, body) {
|
|
3532
|
+
const state = await types.readDaemonState();
|
|
3533
|
+
if (!state?.httpPort) {
|
|
3534
|
+
const errorMessage = "No daemon running, no state file found";
|
|
3535
|
+
types.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3536
|
+
return {
|
|
3537
|
+
error: errorMessage
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3738
3540
|
try {
|
|
3739
|
-
|
|
3740
|
-
if (!state) {
|
|
3741
|
-
return false;
|
|
3742
|
-
}
|
|
3743
|
-
const isRunning = await isDaemonProcessRunning(state.pid);
|
|
3744
|
-
if (!isRunning) {
|
|
3745
|
-
types$1.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3746
|
-
await cleanupDaemonState();
|
|
3747
|
-
return false;
|
|
3748
|
-
}
|
|
3749
|
-
return true;
|
|
3541
|
+
process.kill(state.pid, 0);
|
|
3750
3542
|
} catch (error) {
|
|
3751
|
-
|
|
3752
|
-
|
|
3543
|
+
const errorMessage = "Daemon is not running, file is stale";
|
|
3544
|
+
types.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3545
|
+
return {
|
|
3546
|
+
error: errorMessage
|
|
3547
|
+
};
|
|
3753
3548
|
}
|
|
3754
|
-
}
|
|
3755
|
-
async function getDaemonState() {
|
|
3756
3549
|
try {
|
|
3757
|
-
|
|
3550
|
+
const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 1e4;
|
|
3551
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
3552
|
+
method: "POST",
|
|
3553
|
+
headers: { "Content-Type": "application/json" },
|
|
3554
|
+
body: JSON.stringify(body || {}),
|
|
3555
|
+
// Mostly increased for stress test
|
|
3556
|
+
signal: AbortSignal.timeout(timeout)
|
|
3557
|
+
});
|
|
3558
|
+
if (!response.ok) {
|
|
3559
|
+
const errorMessage = `Request failed: ${path}, HTTP ${response.status}`;
|
|
3560
|
+
types.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3561
|
+
return {
|
|
3562
|
+
error: errorMessage
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
return await response.json();
|
|
3758
3566
|
} catch (error) {
|
|
3759
|
-
|
|
3760
|
-
|
|
3567
|
+
const errorMessage = `Request failed: ${path}, ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
3568
|
+
types.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3569
|
+
return {
|
|
3570
|
+
error: errorMessage
|
|
3571
|
+
};
|
|
3761
3572
|
}
|
|
3762
3573
|
}
|
|
3763
|
-
async function
|
|
3574
|
+
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
3575
|
+
return await daemonPost("/session-started", {
|
|
3576
|
+
sessionId,
|
|
3577
|
+
metadata
|
|
3578
|
+
});
|
|
3579
|
+
}
|
|
3580
|
+
async function listDaemonSessions() {
|
|
3581
|
+
const result = await daemonPost("/list");
|
|
3582
|
+
return result.children || [];
|
|
3583
|
+
}
|
|
3584
|
+
async function stopDaemonSession(sessionId) {
|
|
3585
|
+
const result = await daemonPost("/stop-session", { sessionId });
|
|
3586
|
+
return result.success || false;
|
|
3587
|
+
}
|
|
3588
|
+
async function stopDaemonHttp() {
|
|
3589
|
+
await daemonPost("/stop");
|
|
3590
|
+
}
|
|
3591
|
+
async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
3592
|
+
const state = await types.readDaemonState();
|
|
3593
|
+
if (!state) {
|
|
3594
|
+
return false;
|
|
3595
|
+
}
|
|
3764
3596
|
try {
|
|
3765
|
-
process.kill(pid, 0);
|
|
3597
|
+
process.kill(state.pid, 0);
|
|
3766
3598
|
return true;
|
|
3767
3599
|
} catch {
|
|
3600
|
+
types.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3601
|
+
await cleanupDaemonState();
|
|
3602
|
+
return false;
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
async function isDaemonRunningSameVersion() {
|
|
3606
|
+
types.logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
3607
|
+
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3608
|
+
if (!runningDaemon) {
|
|
3609
|
+
types.logger.debug("[DAEMON CONTROL] No daemon running, returning false");
|
|
3610
|
+
return false;
|
|
3611
|
+
}
|
|
3612
|
+
const state = await types.readDaemonState();
|
|
3613
|
+
if (!state) {
|
|
3614
|
+
types.logger.debug("[DAEMON CONTROL] No daemon state found, returning false");
|
|
3615
|
+
return false;
|
|
3616
|
+
}
|
|
3617
|
+
try {
|
|
3618
|
+
types.logger.debug(`[DAEMON CONTROL] Current CLI version: ${types.configuration.currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
3619
|
+
return types.configuration.currentCliVersion === state.startedWithCliVersion;
|
|
3620
|
+
} catch (error) {
|
|
3621
|
+
types.logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
3768
3622
|
return false;
|
|
3769
3623
|
}
|
|
3770
3624
|
}
|
|
3771
3625
|
async function cleanupDaemonState() {
|
|
3772
3626
|
try {
|
|
3773
|
-
await clearDaemonState();
|
|
3774
|
-
types
|
|
3627
|
+
await types.clearDaemonState();
|
|
3628
|
+
types.logger.debug("[DAEMON RUN] Daemon state file removed");
|
|
3629
|
+
} catch (error) {
|
|
3630
|
+
types.logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
async function stopDaemon() {
|
|
3634
|
+
try {
|
|
3635
|
+
const state = await types.readDaemonState();
|
|
3636
|
+
if (!state) {
|
|
3637
|
+
types.logger.debug("No daemon state found");
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
types.logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3641
|
+
try {
|
|
3642
|
+
await stopDaemonHttp();
|
|
3643
|
+
await waitForProcessDeath(state.pid, 2e3);
|
|
3644
|
+
types.logger.debug("Daemon stopped gracefully via HTTP");
|
|
3645
|
+
return;
|
|
3646
|
+
} catch (error) {
|
|
3647
|
+
types.logger.debug("HTTP stop failed, will force kill", error);
|
|
3648
|
+
}
|
|
3649
|
+
try {
|
|
3650
|
+
process.kill(state.pid, "SIGKILL");
|
|
3651
|
+
types.logger.debug("Force killed daemon");
|
|
3652
|
+
} catch (error) {
|
|
3653
|
+
types.logger.debug("Daemon already dead");
|
|
3654
|
+
}
|
|
3775
3655
|
} catch (error) {
|
|
3776
|
-
types
|
|
3656
|
+
types.logger.debug("Error stopping daemon", error);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
async function waitForProcessDeath(pid, timeout) {
|
|
3660
|
+
const start = Date.now();
|
|
3661
|
+
while (Date.now() - start < timeout) {
|
|
3662
|
+
try {
|
|
3663
|
+
process.kill(pid, 0);
|
|
3664
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3665
|
+
} catch {
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3777
3668
|
}
|
|
3669
|
+
throw new Error("Process did not die within timeout");
|
|
3778
3670
|
}
|
|
3671
|
+
|
|
3779
3672
|
function findAllHappyProcesses() {
|
|
3780
3673
|
try {
|
|
3781
|
-
const output = node_child_process.execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3782
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3783
3674
|
const allProcesses = [];
|
|
3784
|
-
|
|
3785
|
-
const
|
|
3786
|
-
|
|
3787
|
-
const
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3675
|
+
try {
|
|
3676
|
+
const happyOutput = node_child_process.execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
|
|
3677
|
+
const happyLines = happyOutput.trim().split("\n").filter((line) => line.trim());
|
|
3678
|
+
for (const line of happyLines) {
|
|
3679
|
+
const parts = line.trim().split(/\s+/);
|
|
3680
|
+
if (parts.length < 11) continue;
|
|
3681
|
+
const pid = parseInt(parts[1]);
|
|
3682
|
+
const command = parts.slice(10).join(" ");
|
|
3683
|
+
let type = "unknown";
|
|
3684
|
+
if (pid === process.pid) {
|
|
3685
|
+
type = "current";
|
|
3686
|
+
} else if (command.includes("--version")) {
|
|
3687
|
+
type = "daemon-version-check";
|
|
3688
|
+
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3689
|
+
type = "daemon";
|
|
3690
|
+
} else if (command.includes("--started-by daemon")) {
|
|
3691
|
+
type = "daemon-spawned-session";
|
|
3692
|
+
} else if (command.includes("doctor")) {
|
|
3693
|
+
type = "doctor";
|
|
3694
|
+
} else {
|
|
3695
|
+
type = "user-session";
|
|
3696
|
+
}
|
|
3697
|
+
allProcesses.push({ pid, command, type });
|
|
3800
3698
|
}
|
|
3801
|
-
|
|
3699
|
+
} catch {
|
|
3802
3700
|
}
|
|
3803
3701
|
try {
|
|
3804
|
-
const devOutput = node_child_process.execSync('ps aux | grep -E "
|
|
3702
|
+
const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3805
3703
|
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3806
3704
|
for (const line of devLines) {
|
|
3807
3705
|
const parts = line.trim().split(/\s+/);
|
|
3808
3706
|
if (parts.length < 11) continue;
|
|
3809
3707
|
const pid = parseInt(parts[1]);
|
|
3810
3708
|
const command = parts.slice(10).join(" ");
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
const pwdOutput = node_child_process.execSync(`pwdx ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
3814
|
-
workingDir = pwdOutput.replace(`${pid}:`, "").trim();
|
|
3815
|
-
} catch {
|
|
3709
|
+
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3710
|
+
continue;
|
|
3816
3711
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3712
|
+
let type = "unknown";
|
|
3713
|
+
if (pid === process.pid) {
|
|
3714
|
+
type = "current";
|
|
3715
|
+
} else if (command.includes("--version")) {
|
|
3716
|
+
type = "dev-daemon-version-check";
|
|
3717
|
+
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3718
|
+
type = "dev-daemon";
|
|
3719
|
+
} else if (command.includes("--started-by daemon")) {
|
|
3720
|
+
type = "dev-daemon-spawned";
|
|
3721
|
+
} else if (command.includes("doctor")) {
|
|
3722
|
+
type = "dev-doctor";
|
|
3723
|
+
} else if (command.includes("--yolo")) {
|
|
3724
|
+
type = "dev-session";
|
|
3725
|
+
} else {
|
|
3726
|
+
type = "dev-related";
|
|
3819
3727
|
}
|
|
3728
|
+
allProcesses.push({ pid, command, type });
|
|
3820
3729
|
}
|
|
3821
3730
|
} catch {
|
|
3822
3731
|
}
|
|
@@ -3827,18 +3736,39 @@ function findAllHappyProcesses() {
|
|
|
3827
3736
|
}
|
|
3828
3737
|
function findRunawayHappyProcesses() {
|
|
3829
3738
|
try {
|
|
3830
|
-
const output = node_child_process.execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3831
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3832
3739
|
const processes = [];
|
|
3833
|
-
|
|
3834
|
-
const
|
|
3835
|
-
|
|
3836
|
-
const
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3740
|
+
try {
|
|
3741
|
+
const output = node_child_process.execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
|
|
3742
|
+
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3743
|
+
for (const line of lines) {
|
|
3744
|
+
const parts = line.trim().split(/\s+/);
|
|
3745
|
+
if (parts.length < 11) continue;
|
|
3746
|
+
const pid = parseInt(parts[1]);
|
|
3747
|
+
const command = parts.slice(10).join(" ");
|
|
3748
|
+
if (pid === process.pid) continue;
|
|
3749
|
+
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3750
|
+
processes.push({ pid, command });
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
} catch {
|
|
3754
|
+
}
|
|
3755
|
+
try {
|
|
3756
|
+
const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3757
|
+
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3758
|
+
for (const line of devLines) {
|
|
3759
|
+
const parts = line.trim().split(/\s+/);
|
|
3760
|
+
if (parts.length < 11) continue;
|
|
3761
|
+
const pid = parseInt(parts[1]);
|
|
3762
|
+
const command = parts.slice(10).join(" ");
|
|
3763
|
+
if (pid === process.pid) continue;
|
|
3764
|
+
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3765
|
+
continue;
|
|
3766
|
+
}
|
|
3767
|
+
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3768
|
+
processes.push({ pid, command });
|
|
3769
|
+
}
|
|
3841
3770
|
}
|
|
3771
|
+
} catch {
|
|
3842
3772
|
}
|
|
3843
3773
|
return processes;
|
|
3844
3774
|
} catch (error) {
|
|
@@ -3848,10 +3778,10 @@ function findRunawayHappyProcesses() {
|
|
|
3848
3778
|
async function killRunawayHappyProcesses() {
|
|
3849
3779
|
const runawayProcesses = findRunawayHappyProcesses();
|
|
3850
3780
|
const errors = [];
|
|
3851
|
-
|
|
3852
|
-
for (const { pid, command } of runawayProcesses) {
|
|
3781
|
+
const killPromises = runawayProcesses.map(async ({ pid, command }) => {
|
|
3853
3782
|
try {
|
|
3854
3783
|
process.kill(pid, "SIGTERM");
|
|
3784
|
+
console.log(`Sent SIGTERM to runaway process PID ${pid}: ${command}`);
|
|
3855
3785
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3856
3786
|
try {
|
|
3857
3787
|
process.kill(pid, 0);
|
|
@@ -3859,66 +3789,19 @@ async function killRunawayHappyProcesses() {
|
|
|
3859
3789
|
process.kill(pid, "SIGKILL");
|
|
3860
3790
|
} catch {
|
|
3861
3791
|
}
|
|
3862
|
-
killed
|
|
3863
|
-
|
|
3792
|
+
console.log(`Successfully killed runaway process PID ${pid}`);
|
|
3793
|
+
return { success: true, pid, command };
|
|
3864
3794
|
} catch (error) {
|
|
3865
|
-
|
|
3795
|
+
const errorMessage = error.message;
|
|
3796
|
+
errors.push({ pid, error: errorMessage });
|
|
3797
|
+
console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
|
|
3798
|
+
return { success: false, pid, command };
|
|
3866
3799
|
}
|
|
3867
|
-
}
|
|
3800
|
+
});
|
|
3801
|
+
const results = await Promise.all(killPromises);
|
|
3802
|
+
const killed = results.filter((r) => r.success).length;
|
|
3868
3803
|
return { killed, errors };
|
|
3869
3804
|
}
|
|
3870
|
-
async function stopDaemon() {
|
|
3871
|
-
try {
|
|
3872
|
-
stopCaffeinate();
|
|
3873
|
-
types$1.logger.debug("Stopped sleep prevention");
|
|
3874
|
-
const state = await getDaemonState();
|
|
3875
|
-
if (!state) {
|
|
3876
|
-
types$1.logger.debug("No daemon state found");
|
|
3877
|
-
return;
|
|
3878
|
-
}
|
|
3879
|
-
types$1.logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3880
|
-
try {
|
|
3881
|
-
const { stopDaemonHttp } = await Promise.resolve().then(function () { return controlClient; });
|
|
3882
|
-
await stopDaemonHttp();
|
|
3883
|
-
await waitForProcessDeath(state.pid, 5e3);
|
|
3884
|
-
types$1.logger.debug("Daemon stopped gracefully via HTTP");
|
|
3885
|
-
return;
|
|
3886
|
-
} catch (error) {
|
|
3887
|
-
types$1.logger.debug("HTTP stop failed, will force kill", error);
|
|
3888
|
-
}
|
|
3889
|
-
try {
|
|
3890
|
-
process.kill(state.pid, "SIGKILL");
|
|
3891
|
-
types$1.logger.debug("Force killed daemon");
|
|
3892
|
-
} catch (error) {
|
|
3893
|
-
types$1.logger.debug("Daemon already dead");
|
|
3894
|
-
}
|
|
3895
|
-
} catch (error) {
|
|
3896
|
-
types$1.logger.debug("Error stopping daemon", error);
|
|
3897
|
-
}
|
|
3898
|
-
}
|
|
3899
|
-
async function waitForProcessDeath(pid, timeout) {
|
|
3900
|
-
const start = Date.now();
|
|
3901
|
-
while (Date.now() - start < timeout) {
|
|
3902
|
-
try {
|
|
3903
|
-
process.kill(pid, 0);
|
|
3904
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3905
|
-
} catch {
|
|
3906
|
-
return;
|
|
3907
|
-
}
|
|
3908
|
-
}
|
|
3909
|
-
throw new Error("Process did not die within timeout");
|
|
3910
|
-
}
|
|
3911
|
-
|
|
3912
|
-
var utils = /*#__PURE__*/Object.freeze({
|
|
3913
|
-
__proto__: null,
|
|
3914
|
-
cleanupDaemonState: cleanupDaemonState,
|
|
3915
|
-
findAllHappyProcesses: findAllHappyProcesses,
|
|
3916
|
-
findRunawayHappyProcesses: findRunawayHappyProcesses,
|
|
3917
|
-
getDaemonState: getDaemonState,
|
|
3918
|
-
isDaemonRunning: isDaemonRunning,
|
|
3919
|
-
killRunawayHappyProcesses: killRunawayHappyProcesses,
|
|
3920
|
-
stopDaemon: stopDaemon
|
|
3921
|
-
});
|
|
3922
3805
|
|
|
3923
3806
|
function getEnvironmentInfo() {
|
|
3924
3807
|
return {
|
|
@@ -3931,9 +3814,17 @@ function getEnvironmentInfo() {
|
|
|
3931
3814
|
DEBUG: process.env.DEBUG,
|
|
3932
3815
|
workingDirectory: process.cwd(),
|
|
3933
3816
|
processArgv: process.argv,
|
|
3934
|
-
happyDir: types
|
|
3935
|
-
serverUrl: types
|
|
3936
|
-
logsDir: types
|
|
3817
|
+
happyDir: types.configuration?.happyHomeDir,
|
|
3818
|
+
serverUrl: types.configuration?.serverUrl,
|
|
3819
|
+
logsDir: types.configuration?.logsDir,
|
|
3820
|
+
processPid: process.pid,
|
|
3821
|
+
nodeVersion: process.version,
|
|
3822
|
+
platform: process.platform,
|
|
3823
|
+
arch: process.arch,
|
|
3824
|
+
user: process.env.USER,
|
|
3825
|
+
home: process.env.HOME,
|
|
3826
|
+
shell: process.env.SHELL,
|
|
3827
|
+
terminal: process.env.TERM
|
|
3937
3828
|
};
|
|
3938
3829
|
}
|
|
3939
3830
|
function getLogFiles(logDir) {
|
|
@@ -3945,62 +3836,67 @@ function getLogFiles(logDir) {
|
|
|
3945
3836
|
const path = node_path.join(logDir, file);
|
|
3946
3837
|
const stats = node_fs.statSync(path);
|
|
3947
3838
|
return { file, path, modified: stats.mtime };
|
|
3948
|
-
}).sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
|
3839
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
3949
3840
|
} catch {
|
|
3950
3841
|
return [];
|
|
3951
3842
|
}
|
|
3952
3843
|
}
|
|
3953
|
-
async function runDoctorCommand() {
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
|
|
3957
|
-
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3958
|
-
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3959
|
-
console.log("");
|
|
3960
|
-
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3961
|
-
const projectRoot = projectPath();
|
|
3962
|
-
const wrapperPath = node_path.join(projectRoot, "bin", "happy.mjs");
|
|
3963
|
-
const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
|
|
3964
|
-
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
3965
|
-
console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
|
|
3966
|
-
console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
|
|
3967
|
-
console.log(`Wrapper Exists: ${node_fs.existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3968
|
-
console.log(`CLI Exists: ${node_fs.existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3969
|
-
console.log("");
|
|
3970
|
-
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3971
|
-
console.log(`Happy Home: ${chalk.blue(types$1.configuration.happyHomeDir)}`);
|
|
3972
|
-
console.log(`Server URL: ${chalk.blue(types$1.configuration.serverUrl)}`);
|
|
3973
|
-
console.log(`Logs Dir: ${chalk.blue(types$1.configuration.logsDir)}`);
|
|
3974
|
-
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3975
|
-
const env = getEnvironmentInfo();
|
|
3976
|
-
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3977
|
-
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3978
|
-
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3979
|
-
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3980
|
-
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3981
|
-
try {
|
|
3982
|
-
const settings = await readSettings();
|
|
3983
|
-
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3984
|
-
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3985
|
-
} catch (error) {
|
|
3986
|
-
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3987
|
-
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3844
|
+
async function runDoctorCommand(filter) {
|
|
3845
|
+
if (!filter) {
|
|
3846
|
+
filter = "all";
|
|
3988
3847
|
}
|
|
3989
|
-
console.log(chalk.bold("\n\u{
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3848
|
+
console.log(chalk.bold.cyan("\n\u{1FA7A} Happy CLI Doctor\n"));
|
|
3849
|
+
if (filter === "all") {
|
|
3850
|
+
console.log(chalk.bold("\u{1F4CB} Basic Information"));
|
|
3851
|
+
console.log(`Happy CLI Version: ${chalk.green(types.packageJson.version)}`);
|
|
3852
|
+
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3853
|
+
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3854
|
+
console.log("");
|
|
3855
|
+
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3856
|
+
const projectRoot = projectPath();
|
|
3857
|
+
const wrapperPath = node_path.join(projectRoot, "bin", "happy.mjs");
|
|
3858
|
+
const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
|
|
3859
|
+
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
3860
|
+
console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
|
|
3861
|
+
console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
|
|
3862
|
+
console.log(`Wrapper Exists: ${node_fs.existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3863
|
+
console.log(`CLI Exists: ${node_fs.existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3864
|
+
console.log("");
|
|
3865
|
+
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3866
|
+
console.log(`Happy Home: ${chalk.blue(types.configuration.happyHomeDir)}`);
|
|
3867
|
+
console.log(`Server URL: ${chalk.blue(types.configuration.serverUrl)}`);
|
|
3868
|
+
console.log(`Logs Dir: ${chalk.blue(types.configuration.logsDir)}`);
|
|
3869
|
+
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3870
|
+
const env = getEnvironmentInfo();
|
|
3871
|
+
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3872
|
+
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3873
|
+
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3874
|
+
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3875
|
+
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3876
|
+
try {
|
|
3877
|
+
const settings = await types.readSettings();
|
|
3878
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3879
|
+
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3880
|
+
} catch (error) {
|
|
3881
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3882
|
+
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3883
|
+
}
|
|
3884
|
+
console.log(chalk.bold("\n\u{1F510} Authentication"));
|
|
3885
|
+
try {
|
|
3886
|
+
const credentials = await types.readCredentials();
|
|
3887
|
+
if (credentials) {
|
|
3888
|
+
console.log(chalk.green("\u2713 Authenticated (credentials found)"));
|
|
3889
|
+
} else {
|
|
3890
|
+
console.log(chalk.yellow("\u26A0\uFE0F Not authenticated (no credentials)"));
|
|
3891
|
+
}
|
|
3892
|
+
} catch (error) {
|
|
3893
|
+
console.log(chalk.red("\u274C Error reading credentials"));
|
|
3996
3894
|
}
|
|
3997
|
-
} catch (error) {
|
|
3998
|
-
console.log(chalk.red("\u274C Error reading credentials"));
|
|
3999
3895
|
}
|
|
4000
3896
|
console.log(chalk.bold("\n\u{1F916} Daemon Status"));
|
|
4001
3897
|
try {
|
|
4002
|
-
const isRunning = await
|
|
4003
|
-
const state = await
|
|
3898
|
+
const isRunning = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3899
|
+
const state = await types.readDaemonState();
|
|
4004
3900
|
if (isRunning && state) {
|
|
4005
3901
|
console.log(chalk.green("\u2713 Daemon is running"));
|
|
4006
3902
|
console.log(` PID: ${state.pid}`);
|
|
@@ -4016,7 +3912,7 @@ async function runDoctorCommand() {
|
|
|
4016
3912
|
}
|
|
4017
3913
|
if (state) {
|
|
4018
3914
|
console.log(chalk.bold("\n\u{1F4C4} Daemon State:"));
|
|
4019
|
-
console.log(chalk.blue(`Location: ${types
|
|
3915
|
+
console.log(chalk.blue(`Location: ${types.configuration.daemonStateFile}`));
|
|
4020
3916
|
console.log(chalk.gray(JSON.stringify(state, null, 2)));
|
|
4021
3917
|
}
|
|
4022
3918
|
const allProcesses = findAllHappyProcesses();
|
|
@@ -4031,9 +3927,11 @@ async function runDoctorCommand() {
|
|
|
4031
3927
|
const typeLabels = {
|
|
4032
3928
|
"current": "\u{1F4CD} Current Process",
|
|
4033
3929
|
"daemon": "\u{1F916} Daemon",
|
|
3930
|
+
"daemon-version-check": "\u{1F50D} Daemon Version Check (stuck)",
|
|
4034
3931
|
"daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
|
|
4035
3932
|
"user-session": "\u{1F464} User Sessions",
|
|
4036
3933
|
"dev-daemon": "\u{1F6E0}\uFE0F Dev Daemon",
|
|
3934
|
+
"dev-daemon-version-check": "\u{1F6E0}\uFE0F Dev Daemon Version Check (stuck)",
|
|
4037
3935
|
"dev-session": "\u{1F6E0}\uFE0F Dev Sessions",
|
|
4038
3936
|
"dev-doctor": "\u{1F6E0}\uFE0F Dev Doctor",
|
|
4039
3937
|
"dev-related": "\u{1F6E0}\uFE0F Dev Related",
|
|
@@ -4047,190 +3945,54 @@ ${typeLabels[type] || type}:`));
|
|
|
4047
3945
|
console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
4048
3946
|
});
|
|
4049
3947
|
});
|
|
3948
|
+
} else {
|
|
3949
|
+
console.log(chalk.red("\u274C No happy processes found"));
|
|
4050
3950
|
}
|
|
4051
|
-
|
|
4052
|
-
if (runawayProcesses.length > 0) {
|
|
4053
|
-
console.log(chalk.bold("\n\u{1F6A8} Runaway Happy processes detected"));
|
|
4054
|
-
console.log(chalk.gray("These processes were left running after daemon crashes."));
|
|
4055
|
-
runawayProcesses.forEach(({ pid, command }) => {
|
|
4056
|
-
console.log(` ${chalk.yellow(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
4057
|
-
});
|
|
4058
|
-
console.log(chalk.blue("\nTo clean up: happy daemon kill-runaway"));
|
|
4059
|
-
}
|
|
4060
|
-
if (allProcesses.length > 1) {
|
|
3951
|
+
if (filter === "all" && allProcesses.length > 1) {
|
|
4061
3952
|
console.log(chalk.bold("\n\u{1F4A1} Process Management"));
|
|
4062
|
-
console.log(chalk.gray("To
|
|
3953
|
+
console.log(chalk.gray("To clean up runaway processes: happy doctor clean"));
|
|
4063
3954
|
}
|
|
4064
3955
|
} catch (error) {
|
|
4065
3956
|
console.log(chalk.red("\u274C Error checking daemon status"));
|
|
4066
3957
|
}
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
}
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
console.log(chalk.gray(` ${path}`));
|
|
4084
|
-
});
|
|
4085
|
-
} else {
|
|
4086
|
-
console.log(chalk.yellow("No daemon log files found"));
|
|
4087
|
-
}
|
|
4088
|
-
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
4089
|
-
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
4090
|
-
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
4091
|
-
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4092
|
-
}
|
|
4093
|
-
|
|
4094
|
-
async function daemonPost(path, body) {
|
|
4095
|
-
const state = await getDaemonState();
|
|
4096
|
-
if (!state?.httpPort) {
|
|
4097
|
-
throw new Error("No daemon running");
|
|
4098
|
-
}
|
|
4099
|
-
try {
|
|
4100
|
-
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
4101
|
-
method: "POST",
|
|
4102
|
-
headers: { "Content-Type": "application/json" },
|
|
4103
|
-
body: JSON.stringify(body || {}),
|
|
4104
|
-
signal: AbortSignal.timeout(5e3)
|
|
4105
|
-
});
|
|
4106
|
-
if (!response.ok) {
|
|
4107
|
-
throw new Error(`HTTP ${response.status}`);
|
|
4108
|
-
}
|
|
4109
|
-
return await response.json();
|
|
4110
|
-
} catch (error) {
|
|
4111
|
-
types$1.logger.debug(`[CONTROL CLIENT] Request failed: ${path}`, error);
|
|
4112
|
-
throw error;
|
|
4113
|
-
}
|
|
4114
|
-
}
|
|
4115
|
-
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
4116
|
-
await daemonPost("/session-started", {
|
|
4117
|
-
sessionId,
|
|
4118
|
-
metadata
|
|
4119
|
-
});
|
|
4120
|
-
}
|
|
4121
|
-
async function listDaemonSessions() {
|
|
4122
|
-
const result = await daemonPost("/list");
|
|
4123
|
-
return result.children || [];
|
|
4124
|
-
}
|
|
4125
|
-
async function stopDaemonSession(sessionId) {
|
|
4126
|
-
const result = await daemonPost("/stop-session", { sessionId });
|
|
4127
|
-
return result.success || false;
|
|
4128
|
-
}
|
|
4129
|
-
async function stopDaemonHttp() {
|
|
4130
|
-
await daemonPost("/stop");
|
|
4131
|
-
}
|
|
4132
|
-
|
|
4133
|
-
var controlClient = /*#__PURE__*/Object.freeze({
|
|
4134
|
-
__proto__: null,
|
|
4135
|
-
listDaemonSessions: listDaemonSessions,
|
|
4136
|
-
notifyDaemonSessionStarted: notifyDaemonSessionStarted,
|
|
4137
|
-
stopDaemonHttp: stopDaemonHttp,
|
|
4138
|
-
stopDaemonSession: stopDaemonSession
|
|
4139
|
-
});
|
|
4140
|
-
|
|
4141
|
-
function startDaemonControlServer({
|
|
4142
|
-
getChildren,
|
|
4143
|
-
stopSession,
|
|
4144
|
-
spawnSession,
|
|
4145
|
-
requestShutdown,
|
|
4146
|
-
onHappySessionWebhook
|
|
4147
|
-
}) {
|
|
4148
|
-
return new Promise((resolve) => {
|
|
4149
|
-
const app = fastify({
|
|
4150
|
-
logger: false
|
|
4151
|
-
// We use our own logger
|
|
4152
|
-
});
|
|
4153
|
-
app.setValidatorCompiler(fastifyTypeProviderZod.validatorCompiler);
|
|
4154
|
-
app.setSerializerCompiler(fastifyTypeProviderZod.serializerCompiler);
|
|
4155
|
-
const typed = app.withTypeProvider();
|
|
4156
|
-
typed.post("/session-started", {
|
|
4157
|
-
schema: {
|
|
4158
|
-
body: z.z.object({
|
|
4159
|
-
sessionId: z.z.string(),
|
|
4160
|
-
metadata: z.z.any()
|
|
4161
|
-
// Metadata type from API
|
|
4162
|
-
})
|
|
4163
|
-
}
|
|
4164
|
-
}, async (request, reply) => {
|
|
4165
|
-
const { sessionId, metadata } = request.body;
|
|
4166
|
-
types$1.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4167
|
-
onHappySessionWebhook(sessionId, metadata);
|
|
4168
|
-
return { status: "ok" };
|
|
4169
|
-
});
|
|
4170
|
-
typed.post("/list", async (request, reply) => {
|
|
4171
|
-
const children = getChildren();
|
|
4172
|
-
types$1.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4173
|
-
return { children };
|
|
4174
|
-
});
|
|
4175
|
-
typed.post("/stop-session", {
|
|
4176
|
-
schema: {
|
|
4177
|
-
body: z.z.object({
|
|
4178
|
-
sessionId: z.z.string()
|
|
4179
|
-
})
|
|
4180
|
-
}
|
|
4181
|
-
}, async (request, reply) => {
|
|
4182
|
-
const { sessionId } = request.body;
|
|
4183
|
-
types$1.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4184
|
-
const success = stopSession(sessionId);
|
|
4185
|
-
return { success };
|
|
4186
|
-
});
|
|
4187
|
-
typed.post("/spawn-session", {
|
|
4188
|
-
schema: {
|
|
4189
|
-
body: z.z.object({
|
|
4190
|
-
directory: z.z.string(),
|
|
4191
|
-
sessionId: z.z.string().optional()
|
|
4192
|
-
})
|
|
3958
|
+
if (filter === "all") {
|
|
3959
|
+
console.log(chalk.bold("\n\u{1F4DD} Log Files"));
|
|
3960
|
+
const allLogs = getLogFiles(types.configuration.logsDir);
|
|
3961
|
+
if (allLogs.length > 0) {
|
|
3962
|
+
const daemonLogs = allLogs.filter(({ file }) => file.includes("daemon"));
|
|
3963
|
+
const regularLogs = allLogs.filter(({ file }) => !file.includes("daemon"));
|
|
3964
|
+
if (regularLogs.length > 0) {
|
|
3965
|
+
console.log(chalk.blue("\nRecent Logs:"));
|
|
3966
|
+
const logsToShow = regularLogs.slice(0, 10);
|
|
3967
|
+
logsToShow.forEach(({ file, path, modified }) => {
|
|
3968
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3969
|
+
console.log(chalk.gray(` ${path}`));
|
|
3970
|
+
});
|
|
3971
|
+
if (regularLogs.length > 10) {
|
|
3972
|
+
console.log(chalk.gray(` ... and ${regularLogs.length - 10} more log files`));
|
|
3973
|
+
}
|
|
4193
3974
|
}
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
}
|
|
3975
|
+
if (daemonLogs.length > 0) {
|
|
3976
|
+
console.log(chalk.blue("\nDaemon Logs:"));
|
|
3977
|
+
const daemonLogsToShow = daemonLogs.slice(0, 5);
|
|
3978
|
+
daemonLogsToShow.forEach(({ file, path, modified }) => {
|
|
3979
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3980
|
+
console.log(chalk.gray(` ${path}`));
|
|
3981
|
+
});
|
|
3982
|
+
if (daemonLogs.length > 5) {
|
|
3983
|
+
console.log(chalk.gray(` ... and ${daemonLogs.length - 5} more daemon log files`));
|
|
3984
|
+
}
|
|
4204
3985
|
} else {
|
|
4205
|
-
|
|
4206
|
-
return { error: "Failed to spawn session" };
|
|
3986
|
+
console.log(chalk.yellow("\nNo daemon log files found"));
|
|
4207
3987
|
}
|
|
4208
|
-
}
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
});
|
|
4217
|
-
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4218
|
-
if (err) {
|
|
4219
|
-
types$1.logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4220
|
-
throw err;
|
|
4221
|
-
}
|
|
4222
|
-
const port = parseInt(address.split(":").pop());
|
|
4223
|
-
types$1.logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
4224
|
-
resolve({
|
|
4225
|
-
port,
|
|
4226
|
-
stop: async () => {
|
|
4227
|
-
types$1.logger.debug("[CONTROL SERVER] Stopping server");
|
|
4228
|
-
await app.close();
|
|
4229
|
-
types$1.logger.debug("[CONTROL SERVER] Server stopped");
|
|
4230
|
-
}
|
|
4231
|
-
});
|
|
4232
|
-
});
|
|
4233
|
-
});
|
|
3988
|
+
} else {
|
|
3989
|
+
console.log(chalk.yellow("No log files found"));
|
|
3990
|
+
}
|
|
3991
|
+
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
3992
|
+
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
3993
|
+
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
3994
|
+
}
|
|
3995
|
+
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4234
3996
|
}
|
|
4235
3997
|
|
|
4236
3998
|
function displayQRCode(url) {
|
|
@@ -4246,22 +4008,22 @@ function displayQRCode(url) {
|
|
|
4246
4008
|
}
|
|
4247
4009
|
|
|
4248
4010
|
function generateWebAuthUrl(publicKey) {
|
|
4249
|
-
const publicKeyBase64 = types
|
|
4011
|
+
const publicKeyBase64 = types.encodeBase64(publicKey, "base64url");
|
|
4250
4012
|
return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
|
|
4251
4013
|
}
|
|
4252
4014
|
|
|
4253
4015
|
async function openBrowser(url) {
|
|
4254
4016
|
try {
|
|
4255
4017
|
if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
|
|
4256
|
-
types
|
|
4018
|
+
types.logger.debug("[browser] Headless environment detected, skipping browser open");
|
|
4257
4019
|
return false;
|
|
4258
4020
|
}
|
|
4259
|
-
types
|
|
4021
|
+
types.logger.debug(`[browser] Attempting to open URL: ${url}`);
|
|
4260
4022
|
await open(url);
|
|
4261
|
-
types
|
|
4023
|
+
types.logger.debug("[browser] Browser opened successfully");
|
|
4262
4024
|
return true;
|
|
4263
4025
|
} catch (error) {
|
|
4264
|
-
types
|
|
4026
|
+
types.logger.debug("[browser] Failed to open browser:", error);
|
|
4265
4027
|
return false;
|
|
4266
4028
|
}
|
|
4267
4029
|
}
|
|
@@ -4311,10 +4073,10 @@ async function doAuth() {
|
|
|
4311
4073
|
const secret = new Uint8Array(node_crypto.randomBytes(32));
|
|
4312
4074
|
const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
|
|
4313
4075
|
try {
|
|
4314
|
-
console.log(`[AUTH DEBUG] Sending auth request to: ${types
|
|
4315
|
-
console.log(`[AUTH DEBUG] Public key: ${types
|
|
4316
|
-
await axios.post(`${types
|
|
4317
|
-
publicKey: types
|
|
4076
|
+
console.log(`[AUTH DEBUG] Sending auth request to: ${types.configuration.serverUrl}/v1/auth/request`);
|
|
4077
|
+
console.log(`[AUTH DEBUG] Public key: ${types.encodeBase64(keypair.publicKey).substring(0, 20)}...`);
|
|
4078
|
+
await axios.post(`${types.configuration.serverUrl}/v1/auth/request`, {
|
|
4079
|
+
publicKey: types.encodeBase64(keypair.publicKey)
|
|
4318
4080
|
});
|
|
4319
4081
|
console.log(`[AUTH DEBUG] Auth request sent successfully`);
|
|
4320
4082
|
} catch (error) {
|
|
@@ -4355,7 +4117,7 @@ async function doMobileAuth(keypair) {
|
|
|
4355
4117
|
console.clear();
|
|
4356
4118
|
console.log("\nMobile Authentication\n");
|
|
4357
4119
|
console.log("Scan this QR code with your Happy mobile app:\n");
|
|
4358
|
-
const authUrl = "happy://terminal?" + types
|
|
4120
|
+
const authUrl = "happy://terminal?" + types.encodeBase64Url(keypair.publicKey);
|
|
4359
4121
|
displayQRCode(authUrl);
|
|
4360
4122
|
console.log("\nOr manually enter this URL:");
|
|
4361
4123
|
console.log(authUrl);
|
|
@@ -4392,19 +4154,19 @@ async function waitForAuthentication(keypair) {
|
|
|
4392
4154
|
try {
|
|
4393
4155
|
while (!cancelled) {
|
|
4394
4156
|
try {
|
|
4395
|
-
const response = await axios.post(`${types
|
|
4396
|
-
publicKey: types
|
|
4157
|
+
const response = await axios.post(`${types.configuration.serverUrl}/v1/auth/request`, {
|
|
4158
|
+
publicKey: types.encodeBase64(keypair.publicKey)
|
|
4397
4159
|
});
|
|
4398
4160
|
if (response.data.state === "authorized") {
|
|
4399
4161
|
let token = response.data.token;
|
|
4400
|
-
let r = types
|
|
4162
|
+
let r = types.decodeBase64(response.data.response);
|
|
4401
4163
|
let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
|
|
4402
4164
|
if (decrypted) {
|
|
4403
4165
|
const credentials = {
|
|
4404
4166
|
secret: decrypted,
|
|
4405
4167
|
token
|
|
4406
4168
|
};
|
|
4407
|
-
await writeCredentials(credentials);
|
|
4169
|
+
await types.writeCredentials(credentials);
|
|
4408
4170
|
console.log("\n\n\u2713 Authentication successful\n");
|
|
4409
4171
|
return credentials;
|
|
4410
4172
|
} else {
|
|
@@ -4418,7 +4180,7 @@ async function waitForAuthentication(keypair) {
|
|
|
4418
4180
|
}
|
|
4419
4181
|
process.stdout.write("\rWaiting for authentication" + ".".repeat(dots % 3 + 1) + " ");
|
|
4420
4182
|
dots++;
|
|
4421
|
-
await types
|
|
4183
|
+
await types.delay(1e3);
|
|
4422
4184
|
}
|
|
4423
4185
|
} finally {
|
|
4424
4186
|
process.off("SIGINT", handleInterrupt);
|
|
@@ -4436,22 +4198,22 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
4436
4198
|
return decrypted;
|
|
4437
4199
|
}
|
|
4438
4200
|
async function authAndSetupMachineIfNeeded() {
|
|
4439
|
-
types
|
|
4440
|
-
let credentials = await readCredentials();
|
|
4201
|
+
types.logger.debug("[AUTH] Starting auth and machine setup...");
|
|
4202
|
+
let credentials = await types.readCredentials();
|
|
4441
4203
|
if (!credentials) {
|
|
4442
|
-
types
|
|
4204
|
+
types.logger.debug("[AUTH] No credentials found, starting authentication flow...");
|
|
4443
4205
|
const authResult = await doAuth();
|
|
4444
4206
|
if (!authResult) {
|
|
4445
4207
|
throw new Error("Authentication failed or was cancelled");
|
|
4446
4208
|
}
|
|
4447
4209
|
credentials = authResult;
|
|
4448
4210
|
} else {
|
|
4449
|
-
types
|
|
4211
|
+
types.logger.debug("[AUTH] Using existing credentials");
|
|
4450
4212
|
}
|
|
4451
|
-
const settings = await updateSettings(async (s) => {
|
|
4213
|
+
const settings = await types.updateSettings(async (s) => {
|
|
4452
4214
|
if (!s.machineId) {
|
|
4453
4215
|
const newMachineId = node_crypto.randomUUID();
|
|
4454
|
-
types
|
|
4216
|
+
types.logger.debug(`[AUTH] No machine ID found, generating new one: ${newMachineId}; We will not create machine on startup since we don't have api client intialized`);
|
|
4455
4217
|
return {
|
|
4456
4218
|
...s,
|
|
4457
4219
|
machineId: node_crypto.randomUUID()
|
|
@@ -4459,7 +4221,7 @@ async function authAndSetupMachineIfNeeded() {
|
|
|
4459
4221
|
}
|
|
4460
4222
|
return s;
|
|
4461
4223
|
});
|
|
4462
|
-
types
|
|
4224
|
+
types.logger.debug(`[AUTH] Machine ID: ${settings.machineId}`);
|
|
4463
4225
|
return { credentials, machineId: settings.machineId };
|
|
4464
4226
|
}
|
|
4465
4227
|
|
|
@@ -4473,65 +4235,231 @@ function spawnHappyCLI(args, options = {}) {
|
|
|
4473
4235
|
directory = process.cwd();
|
|
4474
4236
|
}
|
|
4475
4237
|
const fullCommand = `happy ${args.join(" ")}`;
|
|
4476
|
-
types
|
|
4238
|
+
types.logger.debug(`[SPAWN HAPPY CLI] Spawning: ${fullCommand} in ${directory}`);
|
|
4477
4239
|
const nodeArgs = [
|
|
4478
4240
|
"--no-warnings",
|
|
4479
4241
|
"--no-deprecation",
|
|
4480
4242
|
entrypoint,
|
|
4481
4243
|
...args
|
|
4482
4244
|
];
|
|
4245
|
+
if (!node_fs.existsSync(entrypoint)) {
|
|
4246
|
+
const errorMessage = `Entrypoint ${entrypoint} does not exist`;
|
|
4247
|
+
types.logger.debug(`[SPAWN HAPPY CLI] ${errorMessage}`);
|
|
4248
|
+
throw new Error(errorMessage);
|
|
4249
|
+
}
|
|
4483
4250
|
return child_process.spawn("node", nodeArgs, options);
|
|
4484
4251
|
}
|
|
4485
4252
|
|
|
4253
|
+
function startDaemonControlServer({
|
|
4254
|
+
getChildren,
|
|
4255
|
+
stopSession,
|
|
4256
|
+
spawnSession,
|
|
4257
|
+
requestShutdown,
|
|
4258
|
+
onHappySessionWebhook
|
|
4259
|
+
}) {
|
|
4260
|
+
return new Promise((resolve) => {
|
|
4261
|
+
const app = fastify({
|
|
4262
|
+
logger: false
|
|
4263
|
+
// We use our own logger
|
|
4264
|
+
});
|
|
4265
|
+
app.setValidatorCompiler(fastifyTypeProviderZod.validatorCompiler);
|
|
4266
|
+
app.setSerializerCompiler(fastifyTypeProviderZod.serializerCompiler);
|
|
4267
|
+
const typed = app.withTypeProvider();
|
|
4268
|
+
typed.post("/session-started", {
|
|
4269
|
+
schema: {
|
|
4270
|
+
body: z.z.object({
|
|
4271
|
+
sessionId: z.z.string(),
|
|
4272
|
+
metadata: z.z.any()
|
|
4273
|
+
// Metadata type from API
|
|
4274
|
+
})
|
|
4275
|
+
}
|
|
4276
|
+
}, async (request, reply) => {
|
|
4277
|
+
const { sessionId, metadata } = request.body;
|
|
4278
|
+
types.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4279
|
+
onHappySessionWebhook(sessionId, metadata);
|
|
4280
|
+
return { status: "ok" };
|
|
4281
|
+
});
|
|
4282
|
+
typed.post("/list", async (request, reply) => {
|
|
4283
|
+
const children = getChildren();
|
|
4284
|
+
types.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4285
|
+
return {
|
|
4286
|
+
children: children.map((child) => {
|
|
4287
|
+
delete child.childProcess;
|
|
4288
|
+
return child;
|
|
4289
|
+
})
|
|
4290
|
+
};
|
|
4291
|
+
});
|
|
4292
|
+
typed.post("/stop-session", {
|
|
4293
|
+
schema: {
|
|
4294
|
+
body: z.z.object({
|
|
4295
|
+
sessionId: z.z.string()
|
|
4296
|
+
})
|
|
4297
|
+
}
|
|
4298
|
+
}, async (request, reply) => {
|
|
4299
|
+
const { sessionId } = request.body;
|
|
4300
|
+
types.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4301
|
+
const success = stopSession(sessionId);
|
|
4302
|
+
return { success };
|
|
4303
|
+
});
|
|
4304
|
+
typed.post("/spawn-session", {
|
|
4305
|
+
schema: {
|
|
4306
|
+
body: z.z.object({
|
|
4307
|
+
directory: z.z.string(),
|
|
4308
|
+
sessionId: z.z.string().optional()
|
|
4309
|
+
})
|
|
4310
|
+
}
|
|
4311
|
+
}, async (request, reply) => {
|
|
4312
|
+
const { directory, sessionId } = request.body;
|
|
4313
|
+
types.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4314
|
+
const session = await spawnSession(directory, sessionId);
|
|
4315
|
+
if (session) {
|
|
4316
|
+
return {
|
|
4317
|
+
success: true,
|
|
4318
|
+
pid: session.pid,
|
|
4319
|
+
sessionId: session.happySessionId || "pending",
|
|
4320
|
+
message: session.message
|
|
4321
|
+
};
|
|
4322
|
+
} else {
|
|
4323
|
+
reply.code(500);
|
|
4324
|
+
return {
|
|
4325
|
+
success: false,
|
|
4326
|
+
error: "Failed to spawn session. Check the directory path and permissions."
|
|
4327
|
+
};
|
|
4328
|
+
}
|
|
4329
|
+
});
|
|
4330
|
+
typed.post("/stop", async (request, reply) => {
|
|
4331
|
+
types.logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4332
|
+
setTimeout(() => {
|
|
4333
|
+
types.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
4334
|
+
requestShutdown();
|
|
4335
|
+
}, 50);
|
|
4336
|
+
return { status: "stopping" };
|
|
4337
|
+
});
|
|
4338
|
+
typed.post("/dev-simulate-error", {
|
|
4339
|
+
schema: {
|
|
4340
|
+
body: z.z.object({
|
|
4341
|
+
error: z.z.string()
|
|
4342
|
+
})
|
|
4343
|
+
}
|
|
4344
|
+
}, async (request, reply) => {
|
|
4345
|
+
const { error } = request.body;
|
|
4346
|
+
types.logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
|
|
4347
|
+
setTimeout(() => {
|
|
4348
|
+
types.logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
|
|
4349
|
+
throw new Error(error);
|
|
4350
|
+
}, 100);
|
|
4351
|
+
return { status: "error will be thrown" };
|
|
4352
|
+
});
|
|
4353
|
+
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4354
|
+
if (err) {
|
|
4355
|
+
types.logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4356
|
+
throw err;
|
|
4357
|
+
}
|
|
4358
|
+
const port = parseInt(address.split(":").pop());
|
|
4359
|
+
types.logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
4360
|
+
resolve({
|
|
4361
|
+
port,
|
|
4362
|
+
stop: async () => {
|
|
4363
|
+
types.logger.debug("[CONTROL SERVER] Stopping server");
|
|
4364
|
+
await app.close();
|
|
4365
|
+
types.logger.debug("[CONTROL SERVER] Server stopped");
|
|
4366
|
+
}
|
|
4367
|
+
});
|
|
4368
|
+
});
|
|
4369
|
+
});
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4486
4372
|
const initialMachineMetadata = {
|
|
4487
4373
|
host: os$1.hostname(),
|
|
4488
4374
|
platform: os$1.platform(),
|
|
4489
|
-
happyCliVersion: packageJson.version,
|
|
4375
|
+
happyCliVersion: types.packageJson.version,
|
|
4490
4376
|
homeDir: os$1.homedir(),
|
|
4491
|
-
happyHomeDir: types
|
|
4377
|
+
happyHomeDir: types.configuration.happyHomeDir
|
|
4492
4378
|
};
|
|
4493
4379
|
async function startDaemon() {
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4380
|
+
let requestShutdown;
|
|
4381
|
+
let resolvesWhenShutdownRequested = new Promise((resolve2) => {
|
|
4382
|
+
requestShutdown = (source, errorMessage) => {
|
|
4383
|
+
types.logger.debug(`[DAEMON RUN] Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`);
|
|
4384
|
+
setTimeout(async () => {
|
|
4385
|
+
types.logger.debug("[DAEMON RUN] Startup malfunctioned, forcing exit with code 1");
|
|
4386
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4387
|
+
process.exit(1);
|
|
4388
|
+
}, 1e3);
|
|
4389
|
+
resolve2({ source, errorMessage });
|
|
4390
|
+
};
|
|
4391
|
+
});
|
|
4392
|
+
process.on("SIGINT", () => {
|
|
4393
|
+
types.logger.debug("[DAEMON RUN] Received SIGINT");
|
|
4394
|
+
requestShutdown("os-signal");
|
|
4395
|
+
});
|
|
4396
|
+
process.on("SIGTERM", () => {
|
|
4397
|
+
types.logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4398
|
+
requestShutdown("os-signal");
|
|
4399
|
+
});
|
|
4400
|
+
process.on("uncaughtException", (error) => {
|
|
4401
|
+
types.logger.debug("[DAEMON RUN] FATAL: Uncaught exception", error);
|
|
4402
|
+
types.logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
|
|
4403
|
+
requestShutdown("exception", error.message);
|
|
4404
|
+
});
|
|
4405
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
4406
|
+
types.logger.debug("[DAEMON RUN] FATAL: Unhandled promise rejection", reason);
|
|
4407
|
+
types.logger.debug(`[DAEMON RUN] Rejected promise:`, promise);
|
|
4408
|
+
const error = reason instanceof Error ? reason : new Error(`Unhandled promise rejection: ${reason}`);
|
|
4409
|
+
types.logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
|
|
4410
|
+
requestShutdown("exception", error.message);
|
|
4411
|
+
});
|
|
4412
|
+
process.on("exit", (code) => {
|
|
4413
|
+
types.logger.debug(`[DAEMON RUN] Process exiting with code: ${code}`);
|
|
4414
|
+
});
|
|
4415
|
+
process.on("beforeExit", (code) => {
|
|
4416
|
+
types.logger.debug(`[DAEMON RUN] Process about to exit with code: ${code}`);
|
|
4417
|
+
});
|
|
4418
|
+
types.logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4419
|
+
types.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4420
|
+
const runningDaemonVersionMatches = await isDaemonRunningSameVersion();
|
|
4421
|
+
if (!runningDaemonVersionMatches) {
|
|
4422
|
+
types.logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
|
|
4423
|
+
await stopDaemon();
|
|
4424
|
+
} else {
|
|
4425
|
+
types.logger.debug("[DAEMON RUN] Daemon version matches, keeping existing daemon");
|
|
4426
|
+
console.log("Daemon already running with matching version");
|
|
4427
|
+
process.exit(0);
|
|
4507
4428
|
}
|
|
4508
|
-
const
|
|
4509
|
-
if (
|
|
4510
|
-
types
|
|
4429
|
+
const daemonLockHandle = await types.acquireDaemonLock(5, 200);
|
|
4430
|
+
if (!daemonLockHandle) {
|
|
4431
|
+
types.logger.debug("[DAEMON RUN] Daemon lock file already held, another daemon is running");
|
|
4432
|
+
process.exit(0);
|
|
4511
4433
|
}
|
|
4512
4434
|
try {
|
|
4435
|
+
const caffeinateStarted = startCaffeinate();
|
|
4436
|
+
if (caffeinateStarted) {
|
|
4437
|
+
types.logger.debug("[DAEMON RUN] Sleep prevention enabled");
|
|
4438
|
+
}
|
|
4513
4439
|
const { credentials, machineId } = await authAndSetupMachineIfNeeded();
|
|
4514
|
-
types
|
|
4440
|
+
types.logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
4515
4441
|
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
4516
4442
|
const pidToAwaiter = /* @__PURE__ */ new Map();
|
|
4517
4443
|
const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
|
|
4518
4444
|
const onHappySessionWebhook = (sessionId, sessionMetadata) => {
|
|
4445
|
+
types.logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata);
|
|
4519
4446
|
const pid = sessionMetadata.hostPid;
|
|
4520
4447
|
if (!pid) {
|
|
4521
|
-
types
|
|
4448
|
+
types.logger.debug(`[DAEMON RUN] Session webhook missing hostPid for sessionId: ${sessionId}`);
|
|
4522
4449
|
return;
|
|
4523
4450
|
}
|
|
4524
|
-
types
|
|
4451
|
+
types.logger.debug(`[DAEMON RUN] Session webhook: ${sessionId}, PID: ${pid}, started by: ${sessionMetadata.startedBy || "unknown"}`);
|
|
4452
|
+
types.logger.debug(`[DAEMON RUN] Current tracked sessions before webhook: ${Array.from(pidToTrackedSession.keys()).join(", ")}`);
|
|
4525
4453
|
const existingSession = pidToTrackedSession.get(pid);
|
|
4526
4454
|
if (existingSession && existingSession.startedBy === "daemon") {
|
|
4527
4455
|
existingSession.happySessionId = sessionId;
|
|
4528
4456
|
existingSession.happySessionMetadataFromLocalWebhook = sessionMetadata;
|
|
4529
|
-
types
|
|
4457
|
+
types.logger.debug(`[DAEMON RUN] Updated daemon-spawned session ${sessionId} with metadata`);
|
|
4530
4458
|
const awaiter = pidToAwaiter.get(pid);
|
|
4531
4459
|
if (awaiter) {
|
|
4532
4460
|
pidToAwaiter.delete(pid);
|
|
4533
4461
|
awaiter(existingSession);
|
|
4534
|
-
types
|
|
4462
|
+
types.logger.debug(`[DAEMON RUN] Resolved session awaiter for PID ${pid}`);
|
|
4535
4463
|
}
|
|
4536
4464
|
} else if (!existingSession) {
|
|
4537
4465
|
const trackedSession = {
|
|
@@ -4541,12 +4469,41 @@ async function startDaemon() {
|
|
|
4541
4469
|
pid
|
|
4542
4470
|
};
|
|
4543
4471
|
pidToTrackedSession.set(pid, trackedSession);
|
|
4544
|
-
types
|
|
4472
|
+
types.logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
|
|
4545
4473
|
}
|
|
4546
4474
|
};
|
|
4547
4475
|
const spawnSession = async (directory, sessionId) => {
|
|
4476
|
+
let directoryCreated = false;
|
|
4477
|
+
if (directory.startsWith("~")) {
|
|
4478
|
+
directory = path.resolve(os$1.homedir(), directory.replace("~", ""));
|
|
4479
|
+
}
|
|
4480
|
+
try {
|
|
4481
|
+
await fs.access(directory);
|
|
4482
|
+
types.logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
|
|
4483
|
+
} catch (error) {
|
|
4484
|
+
types.logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
|
|
4485
|
+
try {
|
|
4486
|
+
await fs.mkdir(directory, { recursive: true });
|
|
4487
|
+
types.logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
|
|
4488
|
+
directoryCreated = true;
|
|
4489
|
+
} catch (mkdirError) {
|
|
4490
|
+
let errorMessage = `Unable to create directory at '${directory}'. `;
|
|
4491
|
+
if (mkdirError.code === "EACCES") {
|
|
4492
|
+
errorMessage += `Permission denied. You don't have write access to create a folder at this location. Try using a different path or check your permissions.`;
|
|
4493
|
+
} else if (mkdirError.code === "ENOTDIR") {
|
|
4494
|
+
errorMessage += `A file already exists at this path or in the parent path. Cannot create a directory here. Please choose a different location.`;
|
|
4495
|
+
} else if (mkdirError.code === "ENOSPC") {
|
|
4496
|
+
errorMessage += `No space left on device. Your disk is full. Please free up some space and try again.`;
|
|
4497
|
+
} else if (mkdirError.code === "EROFS") {
|
|
4498
|
+
errorMessage += `The file system is read-only. Cannot create directories here. Please choose a writable location.`;
|
|
4499
|
+
} else {
|
|
4500
|
+
errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
|
|
4501
|
+
}
|
|
4502
|
+
types.logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
|
|
4503
|
+
return null;
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4548
4506
|
try {
|
|
4549
|
-
const happyBinPath = path.join(projectPath(), "bin", "happy.mjs");
|
|
4550
4507
|
const args = [
|
|
4551
4508
|
"--happy-starting-mode",
|
|
4552
4509
|
"remote",
|
|
@@ -4561,88 +4518,88 @@ async function startDaemon() {
|
|
|
4561
4518
|
// Capture stdout/stderr for debugging
|
|
4562
4519
|
// env is inherited automatically from parent process
|
|
4563
4520
|
});
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4521
|
+
if (process.env.DEBUG) {
|
|
4522
|
+
happyProcess.stdout?.on("data", (data) => {
|
|
4523
|
+
types.logger.debug(`[DAEMON RUN] Child stdout: ${data.toString()}`);
|
|
4524
|
+
});
|
|
4525
|
+
happyProcess.stderr?.on("data", (data) => {
|
|
4526
|
+
types.logger.debug(`[DAEMON RUN] Child stderr: ${data.toString()}`);
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4570
4529
|
if (!happyProcess.pid) {
|
|
4571
|
-
types
|
|
4530
|
+
types.logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4572
4531
|
return null;
|
|
4573
4532
|
}
|
|
4574
|
-
types
|
|
4533
|
+
types.logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
|
|
4575
4534
|
const trackedSession = {
|
|
4576
4535
|
startedBy: "daemon",
|
|
4577
4536
|
pid: happyProcess.pid,
|
|
4578
|
-
childProcess: happyProcess
|
|
4537
|
+
childProcess: happyProcess,
|
|
4538
|
+
directoryCreated,
|
|
4539
|
+
message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0
|
|
4579
4540
|
};
|
|
4580
4541
|
pidToTrackedSession.set(happyProcess.pid, trackedSession);
|
|
4581
4542
|
happyProcess.on("exit", (code, signal) => {
|
|
4582
|
-
types
|
|
4543
|
+
types.logger.debug(`[DAEMON RUN] Child PID ${happyProcess.pid} exited with code ${code}, signal ${signal}`);
|
|
4583
4544
|
if (happyProcess.pid) {
|
|
4584
4545
|
onChildExited(happyProcess.pid);
|
|
4585
4546
|
}
|
|
4586
4547
|
});
|
|
4587
4548
|
happyProcess.on("error", (error) => {
|
|
4588
|
-
types
|
|
4549
|
+
types.logger.debug(`[DAEMON RUN] Child process error:`, error);
|
|
4589
4550
|
if (happyProcess.pid) {
|
|
4590
4551
|
onChildExited(happyProcess.pid);
|
|
4591
4552
|
}
|
|
4592
4553
|
});
|
|
4593
|
-
types
|
|
4594
|
-
return new Promise((
|
|
4554
|
+
types.logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${happyProcess.pid}`);
|
|
4555
|
+
return new Promise((resolve2, reject) => {
|
|
4595
4556
|
const timeout = setTimeout(() => {
|
|
4596
4557
|
pidToAwaiter.delete(happyProcess.pid);
|
|
4597
|
-
types
|
|
4598
|
-
|
|
4558
|
+
types.logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4559
|
+
resolve2(trackedSession);
|
|
4599
4560
|
}, 1e4);
|
|
4600
4561
|
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4601
4562
|
clearTimeout(timeout);
|
|
4602
|
-
types
|
|
4603
|
-
|
|
4563
|
+
types.logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4564
|
+
resolve2(completedSession);
|
|
4604
4565
|
});
|
|
4605
4566
|
});
|
|
4606
4567
|
} catch (error) {
|
|
4607
|
-
types
|
|
4568
|
+
types.logger.debug("[DAEMON RUN] Failed to spawn session:", error);
|
|
4608
4569
|
return null;
|
|
4609
4570
|
}
|
|
4610
4571
|
};
|
|
4611
4572
|
const stopSession = (sessionId) => {
|
|
4612
|
-
types
|
|
4573
|
+
types.logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
|
|
4613
4574
|
for (const [pid, session] of pidToTrackedSession.entries()) {
|
|
4614
4575
|
if (session.happySessionId === sessionId || sessionId.startsWith("PID-") && pid === parseInt(sessionId.replace("PID-", ""))) {
|
|
4615
4576
|
if (session.startedBy === "daemon" && session.childProcess) {
|
|
4616
4577
|
try {
|
|
4617
4578
|
session.childProcess.kill("SIGTERM");
|
|
4618
|
-
types
|
|
4579
|
+
types.logger.debug(`[DAEMON RUN] Sent SIGTERM to daemon-spawned session ${sessionId}`);
|
|
4619
4580
|
} catch (error) {
|
|
4620
|
-
types
|
|
4581
|
+
types.logger.debug(`[DAEMON RUN] Failed to kill session ${sessionId}:`, error);
|
|
4621
4582
|
}
|
|
4622
4583
|
} else {
|
|
4623
4584
|
try {
|
|
4624
4585
|
process.kill(pid, "SIGTERM");
|
|
4625
|
-
types
|
|
4586
|
+
types.logger.debug(`[DAEMON RUN] Sent SIGTERM to external session PID ${pid}`);
|
|
4626
4587
|
} catch (error) {
|
|
4627
|
-
types
|
|
4588
|
+
types.logger.debug(`[DAEMON RUN] Failed to kill external session PID ${pid}:`, error);
|
|
4628
4589
|
}
|
|
4629
4590
|
}
|
|
4630
4591
|
pidToTrackedSession.delete(pid);
|
|
4631
|
-
types
|
|
4592
|
+
types.logger.debug(`[DAEMON RUN] Removed session ${sessionId} from tracking`);
|
|
4632
4593
|
return true;
|
|
4633
4594
|
}
|
|
4634
4595
|
}
|
|
4635
|
-
types
|
|
4596
|
+
types.logger.debug(`[DAEMON RUN] Session ${sessionId} not found`);
|
|
4636
4597
|
return false;
|
|
4637
4598
|
};
|
|
4638
4599
|
const onChildExited = (pid) => {
|
|
4639
|
-
types
|
|
4600
|
+
types.logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
|
|
4640
4601
|
pidToTrackedSession.delete(pid);
|
|
4641
4602
|
};
|
|
4642
|
-
let requestShutdown;
|
|
4643
|
-
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
4644
|
-
requestShutdown = resolve;
|
|
4645
|
-
});
|
|
4646
4603
|
const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
|
|
4647
4604
|
getChildren: getCurrentChildren,
|
|
4648
4605
|
stopSession,
|
|
@@ -4653,24 +4610,25 @@ async function startDaemon() {
|
|
|
4653
4610
|
const fileState = {
|
|
4654
4611
|
pid: process.pid,
|
|
4655
4612
|
httpPort: controlPort,
|
|
4656
|
-
startTime: (/* @__PURE__ */ new Date()).
|
|
4657
|
-
startedWithCliVersion: packageJson.version
|
|
4613
|
+
startTime: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
4614
|
+
startedWithCliVersion: types.packageJson.version,
|
|
4615
|
+
daemonLogPath: types.logger.logFilePath
|
|
4658
4616
|
};
|
|
4659
|
-
|
|
4660
|
-
types
|
|
4617
|
+
types.writeDaemonState(fileState);
|
|
4618
|
+
types.logger.debug("[DAEMON RUN] Daemon state written");
|
|
4661
4619
|
const initialDaemonState = {
|
|
4662
4620
|
status: "offline",
|
|
4663
4621
|
pid: process.pid,
|
|
4664
4622
|
httpPort: controlPort,
|
|
4665
4623
|
startedAt: Date.now()
|
|
4666
4624
|
};
|
|
4667
|
-
const api = new types
|
|
4625
|
+
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
4668
4626
|
const machine = await api.createMachineOrGetExistingAsIs({
|
|
4669
4627
|
machineId,
|
|
4670
4628
|
metadata: initialMachineMetadata,
|
|
4671
4629
|
daemonState: initialDaemonState
|
|
4672
4630
|
});
|
|
4673
|
-
types
|
|
4631
|
+
types.logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
|
|
4674
4632
|
const apiMachine = api.machineSyncClient(machine);
|
|
4675
4633
|
apiMachine.setRPCHandlers({
|
|
4676
4634
|
spawnSession,
|
|
@@ -4678,63 +4636,96 @@ async function startDaemon() {
|
|
|
4678
4636
|
requestShutdown: () => requestShutdown("happy-app")
|
|
4679
4637
|
});
|
|
4680
4638
|
apiMachine.connect();
|
|
4681
|
-
const
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
status: "shutting-down",
|
|
4687
|
-
shutdownRequestedAt: Date.now(),
|
|
4688
|
-
shutdownSource: source === "happy-app" ? "mobile-app" : source === "happy-cli" ? "cli" : source
|
|
4689
|
-
}));
|
|
4690
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4639
|
+
const heartbeatIntervalMs = parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || "60000");
|
|
4640
|
+
let heartbeatRunning = false;
|
|
4641
|
+
const restartOnStaleVersionAndHeartbeat = setInterval(async () => {
|
|
4642
|
+
if (heartbeatRunning) {
|
|
4643
|
+
return;
|
|
4691
4644
|
}
|
|
4692
|
-
|
|
4693
|
-
|
|
4645
|
+
heartbeatRunning = true;
|
|
4646
|
+
if (process.env.DEBUG) {
|
|
4647
|
+
types.logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
|
|
4694
4648
|
}
|
|
4695
|
-
|
|
4649
|
+
for (const [pid, _] of pidToTrackedSession.entries()) {
|
|
4650
|
+
try {
|
|
4651
|
+
process.kill(pid, 0);
|
|
4652
|
+
} catch (error) {
|
|
4653
|
+
types.logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
|
|
4654
|
+
pidToTrackedSession.delete(pid);
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
const projectVersion = JSON.parse(fs$1.readFileSync(path.join(projectPath(), "package.json"), "utf-8")).version;
|
|
4658
|
+
if (projectVersion !== types.configuration.currentCliVersion) {
|
|
4659
|
+
types.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
|
|
4660
|
+
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
4661
|
+
try {
|
|
4662
|
+
spawnHappyCLI(["daemon", "start"], {
|
|
4663
|
+
detached: true,
|
|
4664
|
+
stdio: "ignore"
|
|
4665
|
+
});
|
|
4666
|
+
} catch (error) {
|
|
4667
|
+
types.logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
|
|
4668
|
+
}
|
|
4669
|
+
types.logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
|
|
4670
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e4));
|
|
4671
|
+
process.exit(0);
|
|
4672
|
+
}
|
|
4673
|
+
const daemonState = await types.readDaemonState();
|
|
4674
|
+
if (daemonState && daemonState.pid !== process.pid) {
|
|
4675
|
+
types.logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
|
|
4676
|
+
requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
|
|
4677
|
+
}
|
|
4678
|
+
try {
|
|
4679
|
+
const updatedState = {
|
|
4680
|
+
pid: process.pid,
|
|
4681
|
+
httpPort: controlPort,
|
|
4682
|
+
startTime: fileState.startTime,
|
|
4683
|
+
startedWithCliVersion: types.packageJson.version,
|
|
4684
|
+
lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
4685
|
+
daemonLogPath: fileState.daemonLogPath
|
|
4686
|
+
};
|
|
4687
|
+
types.writeDaemonState(updatedState);
|
|
4688
|
+
if (process.env.DEBUG) {
|
|
4689
|
+
types.logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
|
|
4690
|
+
}
|
|
4691
|
+
} catch (error) {
|
|
4692
|
+
types.logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
|
|
4693
|
+
}
|
|
4694
|
+
heartbeatRunning = false;
|
|
4695
|
+
}, heartbeatIntervalMs);
|
|
4696
|
+
const cleanupAndShutdown = async (source, errorMessage) => {
|
|
4697
|
+
types.logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
|
|
4698
|
+
if (restartOnStaleVersionAndHeartbeat) {
|
|
4699
|
+
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
4700
|
+
types.logger.debug("[DAEMON RUN] Health check interval cleared");
|
|
4701
|
+
}
|
|
4702
|
+
await apiMachine.updateDaemonState((state) => ({
|
|
4703
|
+
...state,
|
|
4704
|
+
status: "shutting-down",
|
|
4705
|
+
shutdownRequestedAt: Date.now(),
|
|
4706
|
+
shutdownSource: source
|
|
4707
|
+
}));
|
|
4708
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
4709
|
+
apiMachine.shutdown();
|
|
4696
4710
|
await stopControlServer();
|
|
4697
|
-
types$1.logger.debug("[DAEMON RUN] Control server stopped");
|
|
4698
4711
|
await cleanupDaemonState();
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
types
|
|
4712
|
+
await stopCaffeinate();
|
|
4713
|
+
await types.releaseDaemonLock(daemonLockHandle);
|
|
4714
|
+
types.logger.debug("[DAEMON RUN] Cleanup completed, exiting process");
|
|
4702
4715
|
process.exit(0);
|
|
4703
4716
|
};
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
});
|
|
4708
|
-
process.on("SIGTERM", () => {
|
|
4709
|
-
types$1.logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4710
|
-
cleanupAndShutdown("os-signal");
|
|
4711
|
-
});
|
|
4712
|
-
process.on("uncaughtException", (error) => {
|
|
4713
|
-
types$1.logger.debug("[DAEMON RUN] Uncaught exception - cleaning up before crash", error);
|
|
4714
|
-
cleanupAndShutdown("unknown");
|
|
4715
|
-
});
|
|
4716
|
-
process.on("unhandledRejection", (reason) => {
|
|
4717
|
-
types$1.logger.debug("[DAEMON RUN] Unhandled rejection - cleaning up before crash", reason);
|
|
4718
|
-
cleanupAndShutdown("unknown");
|
|
4719
|
-
});
|
|
4720
|
-
process.on("exit", () => {
|
|
4721
|
-
types$1.logger.debug("[DAEMON RUN] Process exit, not killing any children");
|
|
4722
|
-
});
|
|
4723
|
-
types$1.logger.debug("[DAEMON RUN] Daemon started successfully");
|
|
4724
|
-
const shutdownSource = await resolvesWhenShutdownRequested;
|
|
4725
|
-
types$1.logger.debug(`[DAEMON RUN] Shutdown requested (source: ${shutdownSource})`);
|
|
4726
|
-
await cleanupAndShutdown(shutdownSource);
|
|
4717
|
+
types.logger.debug("[DAEMON RUN] Daemon started successfully, waiting for shutdown request");
|
|
4718
|
+
const shutdownRequest = await resolvesWhenShutdownRequested;
|
|
4719
|
+
await cleanupAndShutdown(shutdownRequest.source, shutdownRequest.errorMessage);
|
|
4727
4720
|
} catch (error) {
|
|
4728
|
-
types
|
|
4729
|
-
await cleanupDaemonState();
|
|
4730
|
-
stopCaffeinate();
|
|
4721
|
+
types.logger.debug("[DAEMON RUN][FATAL] Failed somewhere unexpectedly - exiting with code 1", error);
|
|
4731
4722
|
process.exit(1);
|
|
4732
4723
|
}
|
|
4733
4724
|
}
|
|
4734
4725
|
|
|
4735
4726
|
async function startHappyServer(client) {
|
|
4736
4727
|
const handler = async (title) => {
|
|
4737
|
-
types
|
|
4728
|
+
types.logger.debug("[happyMCP] Changing title to:", title);
|
|
4738
4729
|
try {
|
|
4739
4730
|
client.sendClaudeSessionMessage({
|
|
4740
4731
|
type: "summary",
|
|
@@ -4759,7 +4750,7 @@ async function startHappyServer(client) {
|
|
|
4759
4750
|
}
|
|
4760
4751
|
}, async (args) => {
|
|
4761
4752
|
const response = await handler(args.title);
|
|
4762
|
-
types
|
|
4753
|
+
types.logger.debug("[happyMCP] Response:", response);
|
|
4763
4754
|
if (response.success) {
|
|
4764
4755
|
return {
|
|
4765
4756
|
content: [
|
|
@@ -4792,7 +4783,7 @@ async function startHappyServer(client) {
|
|
|
4792
4783
|
try {
|
|
4793
4784
|
await transport.handleRequest(req, res);
|
|
4794
4785
|
} catch (error) {
|
|
4795
|
-
types
|
|
4786
|
+
types.logger.debug("Error handling request:", error);
|
|
4796
4787
|
if (!res.headersSent) {
|
|
4797
4788
|
res.writeHead(500).end();
|
|
4798
4789
|
}
|
|
@@ -4817,74 +4808,79 @@ async function startHappyServer(client) {
|
|
|
4817
4808
|
async function start(credentials, options = {}) {
|
|
4818
4809
|
const workingDirectory = process.cwd();
|
|
4819
4810
|
const sessionTag = node_crypto.randomUUID();
|
|
4820
|
-
types
|
|
4821
|
-
types
|
|
4811
|
+
types.logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
|
|
4812
|
+
types.logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
|
|
4822
4813
|
if (options.startedBy === "daemon" && options.startingMode === "local") {
|
|
4823
|
-
types
|
|
4814
|
+
types.logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
4824
4815
|
options.startingMode = "remote";
|
|
4825
4816
|
}
|
|
4826
|
-
const api = new types
|
|
4817
|
+
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
4827
4818
|
let state = {};
|
|
4828
|
-
const settings = await readSettings();
|
|
4819
|
+
const settings = await types.readSettings();
|
|
4829
4820
|
let machineId = settings?.machineId;
|
|
4830
4821
|
if (!machineId) {
|
|
4831
|
-
console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it
|
|
4832
|
-
|
|
4822
|
+
console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/slopus/happy-cli/issues`);
|
|
4823
|
+
process.exit(1);
|
|
4833
4824
|
}
|
|
4834
|
-
types
|
|
4825
|
+
types.logger.debug(`Using machineId: ${machineId}`);
|
|
4826
|
+
await api.createMachineOrGetExistingAsIs({
|
|
4827
|
+
machineId,
|
|
4828
|
+
metadata: initialMachineMetadata
|
|
4829
|
+
});
|
|
4835
4830
|
let metadata = {
|
|
4836
4831
|
path: workingDirectory,
|
|
4837
4832
|
host: os.hostname(),
|
|
4838
|
-
version: packageJson.version,
|
|
4833
|
+
version: types.packageJson.version,
|
|
4839
4834
|
os: os.platform(),
|
|
4840
4835
|
machineId,
|
|
4841
4836
|
homeDir: os.homedir(),
|
|
4842
|
-
happyHomeDir: types
|
|
4837
|
+
happyHomeDir: types.configuration.happyHomeDir,
|
|
4843
4838
|
startedFromDaemon: options.startedBy === "daemon",
|
|
4844
4839
|
hostPid: process.pid,
|
|
4845
|
-
startedBy: options.startedBy || "terminal"
|
|
4840
|
+
startedBy: options.startedBy || "terminal",
|
|
4841
|
+
// Initialize lifecycle state
|
|
4842
|
+
lifecycleState: "running",
|
|
4843
|
+
lifecycleStateSince: Date.now()
|
|
4846
4844
|
};
|
|
4847
4845
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
4848
|
-
types
|
|
4849
|
-
await api.createMachineOrGetExistingAsIs({
|
|
4850
|
-
machineId,
|
|
4851
|
-
metadata: initialMachineMetadata
|
|
4852
|
-
});
|
|
4846
|
+
types.logger.debug(`Session created: ${response.id}`);
|
|
4853
4847
|
try {
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
types
|
|
4848
|
+
types.logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
|
4849
|
+
const result = await notifyDaemonSessionStarted(response.id, metadata);
|
|
4850
|
+
if (result.error) {
|
|
4851
|
+
types.logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
|
|
4852
|
+
} else {
|
|
4853
|
+
types.logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
4858
4854
|
}
|
|
4859
4855
|
} catch (error) {
|
|
4860
|
-
types
|
|
4856
|
+
types.logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
4861
4857
|
}
|
|
4862
4858
|
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
4863
|
-
types
|
|
4859
|
+
types.logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
4864
4860
|
try {
|
|
4865
4861
|
api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
|
|
4866
4862
|
...currentMetadata,
|
|
4867
4863
|
tools: sdkMetadata.tools,
|
|
4868
4864
|
slashCommands: sdkMetadata.slashCommands
|
|
4869
4865
|
}));
|
|
4870
|
-
types
|
|
4866
|
+
types.logger.debug("[start] Session metadata updated with SDK capabilities");
|
|
4871
4867
|
} catch (error) {
|
|
4872
|
-
types
|
|
4868
|
+
types.logger.debug("[start] Failed to update session metadata:", error);
|
|
4873
4869
|
}
|
|
4874
4870
|
});
|
|
4875
4871
|
const session = api.sessionSyncClient(response);
|
|
4876
4872
|
const happyServer = await startHappyServer(session);
|
|
4877
|
-
types
|
|
4878
|
-
const logPath =
|
|
4879
|
-
types
|
|
4880
|
-
types
|
|
4873
|
+
types.logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
|
|
4874
|
+
const logPath = types.logger.logFilePath;
|
|
4875
|
+
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
4876
|
+
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
4881
4877
|
session.updateAgentState((currentState) => ({
|
|
4882
4878
|
...currentState,
|
|
4883
4879
|
controlledByUser: options.startingMode !== "remote"
|
|
4884
4880
|
}));
|
|
4885
4881
|
const caffeinateStarted = startCaffeinate();
|
|
4886
4882
|
if (caffeinateStarted) {
|
|
4887
|
-
types
|
|
4883
|
+
types.logger.infoDeveloper("Sleep prevention enabled (macOS)");
|
|
4888
4884
|
}
|
|
4889
4885
|
const messageQueue = new MessageQueue2((mode) => hashObject({
|
|
4890
4886
|
isPlan: mode.permissionMode === "plan",
|
|
@@ -4910,64 +4906,64 @@ async function start(credentials, options = {}) {
|
|
|
4910
4906
|
if (validModes.includes(message.meta.permissionMode)) {
|
|
4911
4907
|
messagePermissionMode = message.meta.permissionMode;
|
|
4912
4908
|
currentPermissionMode = messagePermissionMode;
|
|
4913
|
-
types
|
|
4909
|
+
types.logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
4914
4910
|
} else {
|
|
4915
|
-
types
|
|
4911
|
+
types.logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
4916
4912
|
}
|
|
4917
4913
|
} else {
|
|
4918
|
-
types
|
|
4914
|
+
types.logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
|
|
4919
4915
|
}
|
|
4920
4916
|
let messageModel = currentModel;
|
|
4921
4917
|
if (message.meta?.hasOwnProperty("model")) {
|
|
4922
4918
|
messageModel = message.meta.model || void 0;
|
|
4923
4919
|
currentModel = messageModel;
|
|
4924
|
-
types
|
|
4920
|
+
types.logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
|
|
4925
4921
|
} else {
|
|
4926
|
-
types
|
|
4922
|
+
types.logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
|
|
4927
4923
|
}
|
|
4928
4924
|
let messageCustomSystemPrompt = currentCustomSystemPrompt;
|
|
4929
4925
|
if (message.meta?.hasOwnProperty("customSystemPrompt")) {
|
|
4930
4926
|
messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
|
|
4931
4927
|
currentCustomSystemPrompt = messageCustomSystemPrompt;
|
|
4932
|
-
types
|
|
4928
|
+
types.logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
|
|
4933
4929
|
} else {
|
|
4934
|
-
types
|
|
4930
|
+
types.logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
|
|
4935
4931
|
}
|
|
4936
4932
|
let messageFallbackModel = currentFallbackModel;
|
|
4937
4933
|
if (message.meta?.hasOwnProperty("fallbackModel")) {
|
|
4938
4934
|
messageFallbackModel = message.meta.fallbackModel || void 0;
|
|
4939
4935
|
currentFallbackModel = messageFallbackModel;
|
|
4940
|
-
types
|
|
4936
|
+
types.logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
|
|
4941
4937
|
} else {
|
|
4942
|
-
types
|
|
4938
|
+
types.logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
|
|
4943
4939
|
}
|
|
4944
4940
|
let messageAppendSystemPrompt = currentAppendSystemPrompt;
|
|
4945
4941
|
if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
|
|
4946
4942
|
messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
|
|
4947
4943
|
currentAppendSystemPrompt = messageAppendSystemPrompt;
|
|
4948
|
-
types
|
|
4944
|
+
types.logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
|
|
4949
4945
|
} else {
|
|
4950
|
-
types
|
|
4946
|
+
types.logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
|
|
4951
4947
|
}
|
|
4952
4948
|
let messageAllowedTools = currentAllowedTools;
|
|
4953
4949
|
if (message.meta?.hasOwnProperty("allowedTools")) {
|
|
4954
4950
|
messageAllowedTools = message.meta.allowedTools || void 0;
|
|
4955
4951
|
currentAllowedTools = messageAllowedTools;
|
|
4956
|
-
types
|
|
4952
|
+
types.logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
|
|
4957
4953
|
} else {
|
|
4958
|
-
types
|
|
4954
|
+
types.logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
|
|
4959
4955
|
}
|
|
4960
4956
|
let messageDisallowedTools = currentDisallowedTools;
|
|
4961
4957
|
if (message.meta?.hasOwnProperty("disallowedTools")) {
|
|
4962
4958
|
messageDisallowedTools = message.meta.disallowedTools || void 0;
|
|
4963
4959
|
currentDisallowedTools = messageDisallowedTools;
|
|
4964
|
-
types
|
|
4960
|
+
types.logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
|
|
4965
4961
|
} else {
|
|
4966
|
-
types
|
|
4962
|
+
types.logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
|
|
4967
4963
|
}
|
|
4968
4964
|
const specialCommand = parseSpecialCommand(message.content.text);
|
|
4969
4965
|
if (specialCommand.type === "compact") {
|
|
4970
|
-
types
|
|
4966
|
+
types.logger.debug("[start] Detected /compact command");
|
|
4971
4967
|
const enhancedMode2 = {
|
|
4972
4968
|
permissionMode: messagePermissionMode || "default",
|
|
4973
4969
|
model: messageModel,
|
|
@@ -4978,11 +4974,11 @@ async function start(credentials, options = {}) {
|
|
|
4978
4974
|
disallowedTools: messageDisallowedTools
|
|
4979
4975
|
};
|
|
4980
4976
|
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
4981
|
-
types
|
|
4977
|
+
types.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
4982
4978
|
return;
|
|
4983
4979
|
}
|
|
4984
4980
|
if (specialCommand.type === "clear") {
|
|
4985
|
-
types
|
|
4981
|
+
types.logger.debug("[start] Detected /clear command");
|
|
4986
4982
|
const enhancedMode2 = {
|
|
4987
4983
|
permissionMode: messagePermissionMode || "default",
|
|
4988
4984
|
model: messageModel,
|
|
@@ -4993,7 +4989,7 @@ async function start(credentials, options = {}) {
|
|
|
4993
4989
|
disallowedTools: messageDisallowedTools
|
|
4994
4990
|
};
|
|
4995
4991
|
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
4996
|
-
types
|
|
4992
|
+
types.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
4997
4993
|
return;
|
|
4998
4994
|
}
|
|
4999
4995
|
const enhancedMode = {
|
|
@@ -5006,33 +5002,40 @@ async function start(credentials, options = {}) {
|
|
|
5006
5002
|
disallowedTools: messageDisallowedTools
|
|
5007
5003
|
};
|
|
5008
5004
|
messageQueue.push(message.content.text, enhancedMode);
|
|
5009
|
-
types
|
|
5005
|
+
types.logger.debugLargeJson("User message pushed to queue:", message);
|
|
5010
5006
|
});
|
|
5011
5007
|
const cleanup = async () => {
|
|
5012
|
-
types
|
|
5008
|
+
types.logger.debug("[START] Received termination signal, cleaning up...");
|
|
5013
5009
|
try {
|
|
5014
5010
|
if (session) {
|
|
5011
|
+
session.updateMetadata((currentMetadata) => ({
|
|
5012
|
+
...currentMetadata,
|
|
5013
|
+
lifecycleState: "archived",
|
|
5014
|
+
lifecycleStateSince: Date.now(),
|
|
5015
|
+
archivedBy: "cli",
|
|
5016
|
+
archiveReason: "User terminated"
|
|
5017
|
+
}));
|
|
5015
5018
|
session.sendSessionDeath();
|
|
5016
5019
|
await session.flush();
|
|
5017
5020
|
await session.close();
|
|
5018
5021
|
}
|
|
5019
5022
|
stopCaffeinate();
|
|
5020
5023
|
happyServer.stop();
|
|
5021
|
-
types
|
|
5024
|
+
types.logger.debug("[START] Cleanup complete, exiting");
|
|
5022
5025
|
process.exit(0);
|
|
5023
5026
|
} catch (error) {
|
|
5024
|
-
types
|
|
5027
|
+
types.logger.debug("[START] Error during cleanup:", error);
|
|
5025
5028
|
process.exit(1);
|
|
5026
5029
|
}
|
|
5027
5030
|
};
|
|
5028
5031
|
process.on("SIGTERM", cleanup);
|
|
5029
5032
|
process.on("SIGINT", cleanup);
|
|
5030
5033
|
process.on("uncaughtException", (error) => {
|
|
5031
|
-
types
|
|
5034
|
+
types.logger.debug("[START] Uncaught exception:", error);
|
|
5032
5035
|
cleanup();
|
|
5033
5036
|
});
|
|
5034
5037
|
process.on("unhandledRejection", (reason) => {
|
|
5035
|
-
types
|
|
5038
|
+
types.logger.debug("[START] Unhandled rejection:", reason);
|
|
5036
5039
|
cleanup();
|
|
5037
5040
|
});
|
|
5038
5041
|
await loop({
|
|
@@ -5063,14 +5066,14 @@ async function start(credentials, options = {}) {
|
|
|
5063
5066
|
claudeArgs: options.claudeArgs
|
|
5064
5067
|
});
|
|
5065
5068
|
session.sendSessionDeath();
|
|
5066
|
-
types
|
|
5069
|
+
types.logger.debug("Waiting for socket to flush...");
|
|
5067
5070
|
await session.flush();
|
|
5068
|
-
types
|
|
5071
|
+
types.logger.debug("Closing session...");
|
|
5069
5072
|
await session.close();
|
|
5070
5073
|
stopCaffeinate();
|
|
5071
|
-
types
|
|
5074
|
+
types.logger.debug("Stopped sleep prevention");
|
|
5072
5075
|
happyServer.stop();
|
|
5073
|
-
types
|
|
5076
|
+
types.logger.debug("Stopped Happy MCP server");
|
|
5074
5077
|
process.exit(0);
|
|
5075
5078
|
}
|
|
5076
5079
|
|
|
@@ -5078,8 +5081,8 @@ const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
|
5078
5081
|
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
5079
5082
|
async function install$1() {
|
|
5080
5083
|
try {
|
|
5081
|
-
if (fs.existsSync(PLIST_FILE$1)) {
|
|
5082
|
-
types
|
|
5084
|
+
if (fs$1.existsSync(PLIST_FILE$1)) {
|
|
5085
|
+
types.logger.info("Daemon plist already exists. Uninstalling first...");
|
|
5083
5086
|
child_process.execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
5084
5087
|
}
|
|
5085
5088
|
const happyPath = process.argv[0];
|
|
@@ -5122,14 +5125,14 @@ async function install$1() {
|
|
|
5122
5125
|
</dict>
|
|
5123
5126
|
</plist>
|
|
5124
5127
|
`);
|
|
5125
|
-
fs.writeFileSync(PLIST_FILE$1, plistContent);
|
|
5126
|
-
fs.chmodSync(PLIST_FILE$1, 420);
|
|
5127
|
-
types
|
|
5128
|
+
fs$1.writeFileSync(PLIST_FILE$1, plistContent);
|
|
5129
|
+
fs$1.chmodSync(PLIST_FILE$1, 420);
|
|
5130
|
+
types.logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
|
|
5128
5131
|
child_process.execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
5129
|
-
types
|
|
5130
|
-
types
|
|
5132
|
+
types.logger.info("Daemon installed and started successfully");
|
|
5133
|
+
types.logger.info("Check logs at ~/.happy/daemon.log");
|
|
5131
5134
|
} catch (error) {
|
|
5132
|
-
types
|
|
5135
|
+
types.logger.debug("Failed to install daemon:", error);
|
|
5133
5136
|
throw error;
|
|
5134
5137
|
}
|
|
5135
5138
|
}
|
|
@@ -5141,7 +5144,7 @@ async function install() {
|
|
|
5141
5144
|
if (process.getuid && process.getuid() !== 0) {
|
|
5142
5145
|
throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
|
|
5143
5146
|
}
|
|
5144
|
-
types
|
|
5147
|
+
types.logger.info("Installing Happy CLI daemon for macOS...");
|
|
5145
5148
|
await install$1();
|
|
5146
5149
|
}
|
|
5147
5150
|
|
|
@@ -5149,21 +5152,21 @@ const PLIST_LABEL = "com.happy-cli.daemon";
|
|
|
5149
5152
|
const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
|
|
5150
5153
|
async function uninstall$1() {
|
|
5151
5154
|
try {
|
|
5152
|
-
if (!fs.existsSync(PLIST_FILE)) {
|
|
5153
|
-
types
|
|
5155
|
+
if (!fs$1.existsSync(PLIST_FILE)) {
|
|
5156
|
+
types.logger.info("Daemon plist not found. Nothing to uninstall.");
|
|
5154
5157
|
return;
|
|
5155
5158
|
}
|
|
5156
5159
|
try {
|
|
5157
5160
|
child_process.execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
|
|
5158
|
-
types
|
|
5161
|
+
types.logger.info("Daemon stopped successfully");
|
|
5159
5162
|
} catch (error) {
|
|
5160
|
-
types
|
|
5163
|
+
types.logger.info("Failed to unload daemon (it might not be running)");
|
|
5161
5164
|
}
|
|
5162
|
-
fs.unlinkSync(PLIST_FILE);
|
|
5163
|
-
types
|
|
5164
|
-
types
|
|
5165
|
+
fs$1.unlinkSync(PLIST_FILE);
|
|
5166
|
+
types.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
|
|
5167
|
+
types.logger.info("Daemon uninstalled successfully");
|
|
5165
5168
|
} catch (error) {
|
|
5166
|
-
types
|
|
5169
|
+
types.logger.debug("Failed to uninstall daemon:", error);
|
|
5167
5170
|
throw error;
|
|
5168
5171
|
}
|
|
5169
5172
|
}
|
|
@@ -5175,7 +5178,7 @@ async function uninstall() {
|
|
|
5175
5178
|
if (process.getuid && process.getuid() !== 0) {
|
|
5176
5179
|
throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
|
|
5177
5180
|
}
|
|
5178
|
-
types
|
|
5181
|
+
types.logger.info("Uninstalling Happy CLI daemon for macOS...");
|
|
5179
5182
|
await uninstall$1();
|
|
5180
5183
|
}
|
|
5181
5184
|
|
|
@@ -5239,19 +5242,12 @@ ${chalk.bold("Usage:")}
|
|
|
5239
5242
|
happy auth login [--force] Authenticate with Happy
|
|
5240
5243
|
happy auth logout Remove authentication and machine data
|
|
5241
5244
|
happy auth status Show authentication status
|
|
5242
|
-
happy auth
|
|
5245
|
+
happy auth backup Display backup key for mobile/web clients
|
|
5243
5246
|
happy auth help Show this help message
|
|
5244
5247
|
|
|
5245
5248
|
${chalk.bold("Options:")}
|
|
5246
5249
|
--force Clear credentials, machine ID, and stop daemon before re-auth
|
|
5247
5250
|
|
|
5248
|
-
${chalk.bold("Examples:")}
|
|
5249
|
-
happy auth login Authenticate if not already logged in
|
|
5250
|
-
happy auth login --force Force re-authentication (complete reset)
|
|
5251
|
-
happy auth status Check authentication and machine status
|
|
5252
|
-
happy auth show-backup Get backup key to link other devices
|
|
5253
|
-
happy auth logout Remove all authentication data
|
|
5254
|
-
|
|
5255
5251
|
${chalk.bold("Notes:")}
|
|
5256
5252
|
\u2022 Use 'auth login --force' when you need to re-register your machine
|
|
5257
5253
|
\u2022 'auth show-backup' displays the key format expected by mobile/web clients
|
|
@@ -5268,21 +5264,21 @@ async function handleAuthLogin(args) {
|
|
|
5268
5264
|
console.log(chalk.gray(" \u2022 Stop daemon if running"));
|
|
5269
5265
|
console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
|
|
5270
5266
|
try {
|
|
5271
|
-
types
|
|
5267
|
+
types.logger.debug("Stopping daemon for force auth...");
|
|
5272
5268
|
await stopDaemon();
|
|
5273
5269
|
console.log(chalk.gray("\u2713 Stopped daemon"));
|
|
5274
5270
|
} catch (error) {
|
|
5275
|
-
types
|
|
5271
|
+
types.logger.debug("Daemon was not running or failed to stop:", error);
|
|
5276
5272
|
}
|
|
5277
|
-
await clearCredentials();
|
|
5273
|
+
await types.clearCredentials();
|
|
5278
5274
|
console.log(chalk.gray("\u2713 Cleared credentials"));
|
|
5279
|
-
await clearMachineId();
|
|
5275
|
+
await types.clearMachineId();
|
|
5280
5276
|
console.log(chalk.gray("\u2713 Cleared machine ID"));
|
|
5281
5277
|
console.log("");
|
|
5282
5278
|
}
|
|
5283
5279
|
if (!forceAuth) {
|
|
5284
|
-
const existingCreds = await readCredentials();
|
|
5285
|
-
const settings = await readSettings();
|
|
5280
|
+
const existingCreds = await types.readCredentials();
|
|
5281
|
+
const settings = await types.readSettings();
|
|
5286
5282
|
if (existingCreds && settings?.machineId) {
|
|
5287
5283
|
console.log(chalk.green("\u2713 Already authenticated"));
|
|
5288
5284
|
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
@@ -5305,8 +5301,8 @@ async function handleAuthLogin(args) {
|
|
|
5305
5301
|
}
|
|
5306
5302
|
}
|
|
5307
5303
|
async function handleAuthLogout() {
|
|
5308
|
-
const happyDir = types
|
|
5309
|
-
const credentials = await readCredentials();
|
|
5304
|
+
const happyDir = types.configuration.happyHomeDir;
|
|
5305
|
+
const credentials = await types.readCredentials();
|
|
5310
5306
|
if (!credentials) {
|
|
5311
5307
|
console.log(chalk.yellow("Not currently authenticated"));
|
|
5312
5308
|
return;
|
|
@@ -5341,8 +5337,8 @@ async function handleAuthLogout() {
|
|
|
5341
5337
|
}
|
|
5342
5338
|
}
|
|
5343
5339
|
async function handleAuthShowBackup() {
|
|
5344
|
-
const credentials = await readCredentials();
|
|
5345
|
-
const settings = await readSettings();
|
|
5340
|
+
const credentials = await types.readCredentials();
|
|
5341
|
+
const settings = await types.readSettings();
|
|
5346
5342
|
if (!credentials) {
|
|
5347
5343
|
console.log(chalk.yellow("Not authenticated"));
|
|
5348
5344
|
console.log(chalk.gray('Run "happy auth login" to authenticate first'));
|
|
@@ -5366,8 +5362,8 @@ async function handleAuthShowBackup() {
|
|
|
5366
5362
|
console.log(chalk.yellow("\u26A0\uFE0F Keep this key secure - it provides full access to your account"));
|
|
5367
5363
|
}
|
|
5368
5364
|
async function handleAuthStatus() {
|
|
5369
|
-
const credentials = await readCredentials();
|
|
5370
|
-
const settings = await readSettings();
|
|
5365
|
+
const credentials = await types.readCredentials();
|
|
5366
|
+
const settings = await types.readSettings();
|
|
5371
5367
|
console.log(chalk.bold("\nAuthentication Status\n"));
|
|
5372
5368
|
if (!credentials) {
|
|
5373
5369
|
console.log(chalk.red("\u2717 Not authenticated"));
|
|
@@ -5386,10 +5382,9 @@ async function handleAuthStatus() {
|
|
|
5386
5382
|
console.log(chalk.gray(' Run "happy auth login --force" to fix this'));
|
|
5387
5383
|
}
|
|
5388
5384
|
console.log(chalk.gray(`
|
|
5389
|
-
Data directory: ${types
|
|
5385
|
+
Data directory: ${types.configuration.happyHomeDir}`));
|
|
5390
5386
|
try {
|
|
5391
|
-
const
|
|
5392
|
-
const running = await isDaemonRunning();
|
|
5387
|
+
const running = await checkIfDaemonRunningAndCleanupStaleState();
|
|
5393
5388
|
if (running) {
|
|
5394
5389
|
console.log(chalk.green("\u2713 Daemon running"));
|
|
5395
5390
|
} else {
|
|
@@ -5430,9 +5425,19 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5430
5425
|
|
|
5431
5426
|
(async () => {
|
|
5432
5427
|
const args = process.argv.slice(2);
|
|
5433
|
-
|
|
5428
|
+
if (!args.includes("--version")) {
|
|
5429
|
+
types.logger.debug("Starting happy CLI with args: ", process.argv);
|
|
5430
|
+
}
|
|
5434
5431
|
const subcommand = args[0];
|
|
5435
5432
|
if (subcommand === "doctor") {
|
|
5433
|
+
if (args[1] === "clean") {
|
|
5434
|
+
const result = await killRunawayHappyProcesses();
|
|
5435
|
+
console.log(`Cleaned up ${result.killed} runaway processes`);
|
|
5436
|
+
if (result.errors.length > 0) {
|
|
5437
|
+
console.log("Errors:", result.errors);
|
|
5438
|
+
}
|
|
5439
|
+
process.exit(0);
|
|
5440
|
+
}
|
|
5436
5441
|
await runDoctorCommand();
|
|
5437
5442
|
return;
|
|
5438
5443
|
} else if (subcommand === "auth") {
|
|
@@ -5475,16 +5480,10 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5475
5480
|
try {
|
|
5476
5481
|
const sessions = await listDaemonSessions();
|
|
5477
5482
|
if (sessions.length === 0) {
|
|
5478
|
-
console.log("No active sessions");
|
|
5483
|
+
console.log("No active sessions this daemon is aware of (they might have been started by a previous version of the daemon)");
|
|
5479
5484
|
} else {
|
|
5480
5485
|
console.log("Active sessions:");
|
|
5481
|
-
|
|
5482
|
-
pid: s.pid,
|
|
5483
|
-
sessionId: s.happySessionId || `PID-${s.pid}`,
|
|
5484
|
-
startedBy: s.startedBy,
|
|
5485
|
-
directory: s.happySessionMetadataFromLocalWebhook?.directory || "unknown"
|
|
5486
|
-
}));
|
|
5487
|
-
console.log(JSON.stringify(cleanSessions, null, 2));
|
|
5486
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
5488
5487
|
}
|
|
5489
5488
|
} catch (error) {
|
|
5490
5489
|
console.log("No daemon running");
|
|
@@ -5512,7 +5511,7 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5512
5511
|
child.unref();
|
|
5513
5512
|
let started = false;
|
|
5514
5513
|
for (let i = 0; i < 50; i++) {
|
|
5515
|
-
if (await
|
|
5514
|
+
if (await checkIfDaemonRunningAndCleanupStaleState()) {
|
|
5516
5515
|
started = true;
|
|
5517
5516
|
break;
|
|
5518
5517
|
}
|
|
@@ -5532,28 +5531,14 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5532
5531
|
await stopDaemon();
|
|
5533
5532
|
process.exit(0);
|
|
5534
5533
|
} else if (daemonSubcommand === "status") {
|
|
5535
|
-
|
|
5536
|
-
if (!state) {
|
|
5537
|
-
console.log("Daemon is not running");
|
|
5538
|
-
} else {
|
|
5539
|
-
const isRunning = await isDaemonRunning();
|
|
5540
|
-
if (isRunning) {
|
|
5541
|
-
console.log("Daemon is running");
|
|
5542
|
-
console.log(` PID: ${state.pid}`);
|
|
5543
|
-
console.log(` Port: ${state.httpPort}`);
|
|
5544
|
-
console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
|
|
5545
|
-
console.log(` CLI Version: ${state.startedWithCliVersion}`);
|
|
5546
|
-
} else {
|
|
5547
|
-
console.log("Daemon state file exists but daemon is not running (stale)");
|
|
5548
|
-
}
|
|
5549
|
-
}
|
|
5534
|
+
await runDoctorCommand("daemon");
|
|
5550
5535
|
process.exit(0);
|
|
5551
|
-
} else if (daemonSubcommand === "
|
|
5552
|
-
const
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
console.log(
|
|
5536
|
+
} else if (daemonSubcommand === "logs") {
|
|
5537
|
+
const latest = await types.getLatestDaemonLog();
|
|
5538
|
+
if (!latest) {
|
|
5539
|
+
console.log("No daemon logs found");
|
|
5540
|
+
} else {
|
|
5541
|
+
console.log(latest.path);
|
|
5557
5542
|
}
|
|
5558
5543
|
process.exit(0);
|
|
5559
5544
|
} else if (daemonSubcommand === "install") {
|
|
@@ -5577,14 +5562,15 @@ ${chalk.bold("happy daemon")} - Daemon management
|
|
|
5577
5562
|
${chalk.bold("Usage:")}
|
|
5578
5563
|
happy daemon start Start the daemon (detached)
|
|
5579
5564
|
happy daemon stop Stop the daemon (sessions stay alive)
|
|
5580
|
-
happy daemon stop --kill-managed Stop daemon and kill managed sessions
|
|
5581
5565
|
happy daemon status Show daemon status
|
|
5582
5566
|
happy daemon list List active sessions
|
|
5583
|
-
|
|
5584
|
-
|
|
5567
|
+
|
|
5568
|
+
If you want to kill all happy related processes run
|
|
5569
|
+
${chalk.cyan("happy doctor clean")}
|
|
5585
5570
|
|
|
5586
5571
|
${chalk.bold("Note:")} The daemon runs in the background and manages Claude sessions.
|
|
5587
|
-
|
|
5572
|
+
|
|
5573
|
+
${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("happy doctor clean")}
|
|
5588
5574
|
`);
|
|
5589
5575
|
}
|
|
5590
5576
|
return;
|
|
@@ -5592,8 +5578,6 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5592
5578
|
const options = {};
|
|
5593
5579
|
let showHelp = false;
|
|
5594
5580
|
let showVersion = false;
|
|
5595
|
-
let forceAuth = false;
|
|
5596
|
-
let forceAuthNew = false;
|
|
5597
5581
|
const unknownArgs = [];
|
|
5598
5582
|
for (let i = 0; i < args.length; i++) {
|
|
5599
5583
|
const arg = args[i];
|
|
@@ -5603,10 +5587,6 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5603
5587
|
} else if (arg === "-v" || arg === "--version") {
|
|
5604
5588
|
showVersion = true;
|
|
5605
5589
|
unknownArgs.push(arg);
|
|
5606
|
-
} else if (arg === "--auth" || arg === "--login") {
|
|
5607
|
-
forceAuth = true;
|
|
5608
|
-
} else if (arg === "--force-auth") {
|
|
5609
|
-
forceAuthNew = true;
|
|
5610
5590
|
} else if (arg === "--happy-starting-mode") {
|
|
5611
5591
|
options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
|
|
5612
5592
|
} else if (arg === "--yolo") {
|
|
@@ -5628,29 +5608,23 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5628
5608
|
${chalk.bold("happy")} - Claude Code On the Go
|
|
5629
5609
|
|
|
5630
5610
|
${chalk.bold("Usage:")}
|
|
5631
|
-
happy [options]
|
|
5632
|
-
happy auth Manage authentication
|
|
5633
|
-
happy
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
${chalk.bold("Happy Options:")}
|
|
5637
|
-
--help Show this help message
|
|
5638
|
-
--yolo Skip all permissions (--dangerously-skip-permissions)
|
|
5639
|
-
--force-auth Force re-authentication
|
|
5640
|
-
|
|
5641
|
-
${chalk.bold("\u{1F3AF} Happy supports ALL Claude options!")}
|
|
5642
|
-
Use any claude flag exactly as you normally would.
|
|
5611
|
+
happy [options] Start Claude with mobile control
|
|
5612
|
+
happy auth Manage authentication
|
|
5613
|
+
happy daemon Manage background service that allows
|
|
5614
|
+
to spawn new sessions away from your computer
|
|
5615
|
+
happy doctor System diagnostics & troubleshooting
|
|
5643
5616
|
|
|
5644
5617
|
${chalk.bold("Examples:")}
|
|
5645
|
-
happy
|
|
5646
|
-
happy --yolo
|
|
5647
|
-
|
|
5648
|
-
happy
|
|
5649
|
-
happy
|
|
5650
|
-
|
|
5618
|
+
happy Start session
|
|
5619
|
+
happy --yolo Start with bypassing permissions
|
|
5620
|
+
happy sugar for --dangerously-skip-permissions
|
|
5621
|
+
happy auth login --force Authenticate
|
|
5622
|
+
happy doctor Run diagnostics
|
|
5623
|
+
|
|
5624
|
+
${chalk.bold("Happy supports ALL Claude options!")}
|
|
5625
|
+
Use any claude flag with happy as you would with claude. Our favorite:
|
|
5651
5626
|
|
|
5652
|
-
|
|
5653
|
-
${chalk.bold('Use "happy daemon" for background service management.')}
|
|
5627
|
+
happy --resume
|
|
5654
5628
|
|
|
5655
5629
|
${chalk.gray("\u2500".repeat(60))}
|
|
5656
5630
|
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
@@ -5665,34 +5639,12 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5665
5639
|
process.exit(0);
|
|
5666
5640
|
}
|
|
5667
5641
|
if (showVersion) {
|
|
5668
|
-
console.log(packageJson.version);
|
|
5669
|
-
process.exit(0);
|
|
5642
|
+
console.log(`happy version: ${types.packageJson.version}`);
|
|
5670
5643
|
}
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
await stopDaemon();
|
|
5676
|
-
} catch {
|
|
5677
|
-
}
|
|
5678
|
-
await clearCredentials();
|
|
5679
|
-
await clearMachineId();
|
|
5680
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5681
|
-
credentials = result.credentials;
|
|
5682
|
-
} else if (forceAuth) {
|
|
5683
|
-
console.log(chalk.yellow('Note: --auth is deprecated. Use "happy auth login" or --force-auth instead.\n'));
|
|
5684
|
-
const res = await doAuth();
|
|
5685
|
-
if (!res) {
|
|
5686
|
-
process.exit(1);
|
|
5687
|
-
}
|
|
5688
|
-
await writeCredentials(res);
|
|
5689
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5690
|
-
credentials = result.credentials;
|
|
5691
|
-
} else {
|
|
5692
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5693
|
-
credentials = result.credentials;
|
|
5694
|
-
}
|
|
5695
|
-
let settings = await readSettings();
|
|
5644
|
+
const {
|
|
5645
|
+
credentials
|
|
5646
|
+
} = await authAndSetupMachineIfNeeded();
|
|
5647
|
+
let settings = await types.readSettings();
|
|
5696
5648
|
if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
5697
5649
|
const shouldAutoStart = await new Promise((resolve) => {
|
|
5698
5650
|
let hasResolved = false;
|
|
@@ -5708,7 +5660,7 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5708
5660
|
patchConsole: false
|
|
5709
5661
|
});
|
|
5710
5662
|
});
|
|
5711
|
-
settings = await updateSettings((settings2) => ({
|
|
5663
|
+
settings = await types.updateSettings((settings2) => ({
|
|
5712
5664
|
...settings2,
|
|
5713
5665
|
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5714
5666
|
}));
|
|
@@ -5720,15 +5672,18 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5720
5672
|
}
|
|
5721
5673
|
}
|
|
5722
5674
|
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5723
|
-
types
|
|
5724
|
-
if (!await
|
|
5675
|
+
types.logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5676
|
+
if (!await isDaemonRunningSameVersion()) {
|
|
5677
|
+
types.logger.debug("Starting Happy background service...");
|
|
5725
5678
|
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5726
5679
|
detached: true,
|
|
5727
5680
|
stdio: "ignore",
|
|
5728
5681
|
env: process.env
|
|
5729
5682
|
});
|
|
5730
5683
|
daemonProcess.unref();
|
|
5731
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
5684
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5685
|
+
} else {
|
|
5686
|
+
types.logger.debug("Happy background service is running & matches our version");
|
|
5732
5687
|
}
|
|
5733
5688
|
}
|
|
5734
5689
|
try {
|
|
@@ -5783,14 +5738,14 @@ ${chalk.bold("Examples:")}
|
|
|
5783
5738
|
console.log(chalk.gray('Run "happy notify --help" for usage information.'));
|
|
5784
5739
|
process.exit(1);
|
|
5785
5740
|
}
|
|
5786
|
-
let credentials = await readCredentials();
|
|
5741
|
+
let credentials = await types.readCredentials();
|
|
5787
5742
|
if (!credentials) {
|
|
5788
5743
|
console.error(chalk.red('Error: Not authenticated. Please run "happy --auth" first.'));
|
|
5789
5744
|
process.exit(1);
|
|
5790
5745
|
}
|
|
5791
5746
|
console.log(chalk.blue("\u{1F4F1} Sending push notification..."));
|
|
5792
5747
|
try {
|
|
5793
|
-
const api = new types
|
|
5748
|
+
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
5794
5749
|
const notificationTitle = title || "Happy";
|
|
5795
5750
|
api.push().sendToAllDevices(
|
|
5796
5751
|
notificationTitle,
|