happy-coder 0.9.0-5 → 0.9.0
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 +971 -1078
- package/dist/index.mjs +705 -794
- package/dist/lib.cjs +4 -4
- package/dist/lib.d.cts +7 -3
- package/dist/lib.d.mts +7 -3
- package/dist/lib.mjs +4 -4
- package/dist/{types-DJOX-XG-.mjs → types-Cezp_n6O.mjs} +552 -204
- package/dist/{types-a-nJyP-e.cjs → types-CyOnnZ8M.cjs} +448 -69
- package/package.json +6 -6
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-CyOnnZ8M.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;
|
|
@@ -102,7 +83,7 @@ class Session {
|
|
|
102
83
|
*/
|
|
103
84
|
clearSessionId = () => {
|
|
104
85
|
this.sessionId = null;
|
|
105
|
-
types
|
|
86
|
+
types.logger.debug("[Session] Session ID cleared");
|
|
106
87
|
};
|
|
107
88
|
}
|
|
108
89
|
|
|
@@ -117,7 +98,7 @@ function claudeCheckSession(sessionId, path) {
|
|
|
117
98
|
const sessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
118
99
|
const sessionExists = node_fs.existsSync(sessionFile);
|
|
119
100
|
if (!sessionExists) {
|
|
120
|
-
types
|
|
101
|
+
types.logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
|
|
121
102
|
return false;
|
|
122
103
|
}
|
|
123
104
|
const sessionData = node_fs.readFileSync(sessionFile, "utf-8").split("\n");
|
|
@@ -170,7 +151,7 @@ async function claudeLocal(opts) {
|
|
|
170
151
|
const detectedIdsFileSystem = /* @__PURE__ */ new Set();
|
|
171
152
|
watcher.on("change", (event, filename) => {
|
|
172
153
|
if (typeof filename === "string" && filename.toLowerCase().endsWith(".jsonl")) {
|
|
173
|
-
types
|
|
154
|
+
types.logger.debug("change", event, filename);
|
|
174
155
|
const sessionId = filename.replace(".jsonl", "");
|
|
175
156
|
if (detectedIdsFileSystem.has(sessionId)) {
|
|
176
157
|
return;
|
|
@@ -194,7 +175,7 @@ async function claudeLocal(opts) {
|
|
|
194
175
|
const updateThinking = (newThinking) => {
|
|
195
176
|
if (thinking !== newThinking) {
|
|
196
177
|
thinking = newThinking;
|
|
197
|
-
types
|
|
178
|
+
types.logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
|
|
198
179
|
if (opts.onThinkingChange) {
|
|
199
180
|
opts.onThinkingChange(thinking);
|
|
200
181
|
}
|
|
@@ -272,10 +253,10 @@ async function claudeLocal(opts) {
|
|
|
272
253
|
}
|
|
273
254
|
break;
|
|
274
255
|
default:
|
|
275
|
-
types
|
|
256
|
+
types.logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
|
|
276
257
|
}
|
|
277
258
|
} catch (e) {
|
|
278
|
-
types
|
|
259
|
+
types.logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
|
|
279
260
|
}
|
|
280
261
|
});
|
|
281
262
|
rl.on("error", (err) => {
|
|
@@ -379,7 +360,7 @@ class InvalidateSync {
|
|
|
379
360
|
this._pendings = [];
|
|
380
361
|
};
|
|
381
362
|
_doSync = async () => {
|
|
382
|
-
await types
|
|
363
|
+
await types.backoff(async () => {
|
|
383
364
|
if (this._stopped) {
|
|
384
365
|
return;
|
|
385
366
|
}
|
|
@@ -404,21 +385,21 @@ function startFileWatcher(file, onFileChange) {
|
|
|
404
385
|
void (async () => {
|
|
405
386
|
while (true) {
|
|
406
387
|
try {
|
|
407
|
-
types
|
|
408
|
-
const watcher =
|
|
388
|
+
types.logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
|
|
389
|
+
const watcher = fs.watch(file, { persistent: true, signal: abortController.signal });
|
|
409
390
|
for await (const event of watcher) {
|
|
410
391
|
if (abortController.signal.aborted) {
|
|
411
392
|
return;
|
|
412
393
|
}
|
|
413
|
-
types
|
|
394
|
+
types.logger.debug(`[FILE_WATCHER] File changed: ${file}`);
|
|
414
395
|
onFileChange(file);
|
|
415
396
|
}
|
|
416
397
|
} catch (e) {
|
|
417
398
|
if (abortController.signal.aborted) {
|
|
418
399
|
return;
|
|
419
400
|
}
|
|
420
|
-
types
|
|
421
|
-
await types
|
|
401
|
+
types.logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
|
|
402
|
+
await types.delay(1e3);
|
|
422
403
|
}
|
|
423
404
|
}
|
|
424
405
|
})();
|
|
@@ -488,21 +469,21 @@ async function createSessionScanner(opts) {
|
|
|
488
469
|
},
|
|
489
470
|
onNewSession: (sessionId) => {
|
|
490
471
|
if (currentSessionId === sessionId) {
|
|
491
|
-
types
|
|
472
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
|
|
492
473
|
return;
|
|
493
474
|
}
|
|
494
475
|
if (finishedSessions.has(sessionId)) {
|
|
495
|
-
types
|
|
476
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
|
|
496
477
|
return;
|
|
497
478
|
}
|
|
498
479
|
if (pendingSessions.has(sessionId)) {
|
|
499
|
-
types
|
|
480
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
|
|
500
481
|
return;
|
|
501
482
|
}
|
|
502
483
|
if (currentSessionId) {
|
|
503
484
|
pendingSessions.add(currentSessionId);
|
|
504
485
|
}
|
|
505
|
-
types
|
|
486
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
|
|
506
487
|
currentSessionId = sessionId;
|
|
507
488
|
sync.invalidate();
|
|
508
489
|
}
|
|
@@ -523,12 +504,12 @@ function messageKey(message) {
|
|
|
523
504
|
}
|
|
524
505
|
async function readSessionLog(projectDir, sessionId) {
|
|
525
506
|
const expectedSessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
526
|
-
types
|
|
507
|
+
types.logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
|
|
527
508
|
let file;
|
|
528
509
|
try {
|
|
529
|
-
file = await promises
|
|
510
|
+
file = await promises.readFile(expectedSessionFile, "utf-8");
|
|
530
511
|
} catch (error) {
|
|
531
|
-
types
|
|
512
|
+
types.logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
|
|
532
513
|
return [];
|
|
533
514
|
}
|
|
534
515
|
let lines = file.split("\n");
|
|
@@ -539,14 +520,14 @@ async function readSessionLog(projectDir, sessionId) {
|
|
|
539
520
|
continue;
|
|
540
521
|
}
|
|
541
522
|
let message = JSON.parse(l);
|
|
542
|
-
let parsed = types
|
|
523
|
+
let parsed = types.RawJSONLinesSchema.safeParse(message);
|
|
543
524
|
if (!parsed.success) {
|
|
544
|
-
types
|
|
525
|
+
types.logger.debugLargeJson(`[SESSION_SCANNER] Failed to parse message`, message);
|
|
545
526
|
continue;
|
|
546
527
|
}
|
|
547
528
|
messages.push(parsed.data);
|
|
548
529
|
} catch (e) {
|
|
549
|
-
types
|
|
530
|
+
types.logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
550
531
|
continue;
|
|
551
532
|
}
|
|
552
533
|
}
|
|
@@ -574,7 +555,7 @@ async function claudeLocalLauncher(session) {
|
|
|
574
555
|
await exutFuture.promise;
|
|
575
556
|
}
|
|
576
557
|
async function doAbort() {
|
|
577
|
-
types
|
|
558
|
+
types.logger.debug("[local]: doAbort");
|
|
578
559
|
if (!exitReason) {
|
|
579
560
|
exitReason = "switch";
|
|
580
561
|
}
|
|
@@ -582,7 +563,7 @@ async function claudeLocalLauncher(session) {
|
|
|
582
563
|
await abort();
|
|
583
564
|
}
|
|
584
565
|
async function doSwitch() {
|
|
585
|
-
types
|
|
566
|
+
types.logger.debug("[local]: doSwitch");
|
|
586
567
|
if (!exitReason) {
|
|
587
568
|
exitReason = "switch";
|
|
588
569
|
}
|
|
@@ -604,7 +585,7 @@ async function claudeLocalLauncher(session) {
|
|
|
604
585
|
if (exitReason) {
|
|
605
586
|
return exitReason;
|
|
606
587
|
}
|
|
607
|
-
types
|
|
588
|
+
types.logger.debug("[local]: launch");
|
|
608
589
|
try {
|
|
609
590
|
await claudeLocal({
|
|
610
591
|
path: session.path,
|
|
@@ -622,7 +603,7 @@ async function claudeLocalLauncher(session) {
|
|
|
622
603
|
break;
|
|
623
604
|
}
|
|
624
605
|
} catch (e) {
|
|
625
|
-
types
|
|
606
|
+
types.logger.debug("[local]: launch error", e);
|
|
626
607
|
if (!exitReason) {
|
|
627
608
|
session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
628
609
|
continue;
|
|
@@ -630,7 +611,7 @@ async function claudeLocalLauncher(session) {
|
|
|
630
611
|
break;
|
|
631
612
|
}
|
|
632
613
|
}
|
|
633
|
-
types
|
|
614
|
+
types.logger.debug("[local]: launch done");
|
|
634
615
|
}
|
|
635
616
|
} finally {
|
|
636
617
|
exutFuture.resolve(void 0);
|
|
@@ -910,7 +891,7 @@ function getDefaultClaudeCodePath() {
|
|
|
910
891
|
}
|
|
911
892
|
function logDebug(message) {
|
|
912
893
|
if (process.env.DEBUG) {
|
|
913
|
-
types
|
|
894
|
+
types.logger.debug(message);
|
|
914
895
|
console.log(message);
|
|
915
896
|
}
|
|
916
897
|
}
|
|
@@ -989,7 +970,7 @@ class Query {
|
|
|
989
970
|
}
|
|
990
971
|
this.inputStream.enqueue(message);
|
|
991
972
|
} catch (e) {
|
|
992
|
-
types
|
|
973
|
+
types.logger.debug(line);
|
|
993
974
|
}
|
|
994
975
|
}
|
|
995
976
|
}
|
|
@@ -1406,10 +1387,10 @@ async function awaitFileExist(file, timeout = 1e4) {
|
|
|
1406
1387
|
const startTime = Date.now();
|
|
1407
1388
|
while (Date.now() - startTime < timeout) {
|
|
1408
1389
|
try {
|
|
1409
|
-
await
|
|
1390
|
+
await fs.access(file);
|
|
1410
1391
|
return true;
|
|
1411
1392
|
} catch (e) {
|
|
1412
|
-
await types
|
|
1393
|
+
await types.delay(1e3);
|
|
1413
1394
|
}
|
|
1414
1395
|
}
|
|
1415
1396
|
return false;
|
|
@@ -1441,7 +1422,7 @@ async function claudeRemote(opts) {
|
|
|
1441
1422
|
}
|
|
1442
1423
|
let isCompactCommand = false;
|
|
1443
1424
|
if (specialCommand.type === "compact") {
|
|
1444
|
-
types
|
|
1425
|
+
types.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
|
|
1445
1426
|
isCompactCommand = true;
|
|
1446
1427
|
if (opts.onCompletionEvent) {
|
|
1447
1428
|
opts.onCompletionEvent("Compaction started");
|
|
@@ -1470,7 +1451,7 @@ async function claudeRemote(opts) {
|
|
|
1470
1451
|
const updateThinking = (newThinking) => {
|
|
1471
1452
|
if (thinking !== newThinking) {
|
|
1472
1453
|
thinking = newThinking;
|
|
1473
|
-
types
|
|
1454
|
+
types.logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
|
|
1474
1455
|
if (opts.onThinkingChange) {
|
|
1475
1456
|
opts.onThinkingChange(thinking);
|
|
1476
1457
|
}
|
|
@@ -1490,26 +1471,26 @@ async function claudeRemote(opts) {
|
|
|
1490
1471
|
});
|
|
1491
1472
|
updateThinking(true);
|
|
1492
1473
|
try {
|
|
1493
|
-
types
|
|
1474
|
+
types.logger.debug(`[claudeRemote] Starting to iterate over response`);
|
|
1494
1475
|
for await (const message of response) {
|
|
1495
|
-
types
|
|
1476
|
+
types.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
|
|
1496
1477
|
opts.onMessage(message);
|
|
1497
1478
|
if (message.type === "system" && message.subtype === "init") {
|
|
1498
1479
|
updateThinking(true);
|
|
1499
1480
|
const systemInit = message;
|
|
1500
1481
|
if (systemInit.session_id) {
|
|
1501
|
-
types
|
|
1482
|
+
types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
|
|
1502
1483
|
const projectDir = getProjectPath(opts.path);
|
|
1503
1484
|
const found = await awaitFileExist(node_path.join(projectDir, `${systemInit.session_id}.jsonl`));
|
|
1504
|
-
types
|
|
1485
|
+
types.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
|
|
1505
1486
|
opts.onSessionFound(systemInit.session_id);
|
|
1506
1487
|
}
|
|
1507
1488
|
}
|
|
1508
1489
|
if (message.type === "result") {
|
|
1509
1490
|
updateThinking(false);
|
|
1510
|
-
types
|
|
1491
|
+
types.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
|
|
1511
1492
|
if (isCompactCommand) {
|
|
1512
|
-
types
|
|
1493
|
+
types.logger.debug("[claudeRemote] Compaction completed");
|
|
1513
1494
|
if (opts.onCompletionEvent) {
|
|
1514
1495
|
opts.onCompletionEvent("Compaction completed");
|
|
1515
1496
|
}
|
|
@@ -1529,7 +1510,7 @@ async function claudeRemote(opts) {
|
|
|
1529
1510
|
if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
|
|
1530
1511
|
for (let c of msg.message.content) {
|
|
1531
1512
|
if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
|
|
1532
|
-
types
|
|
1513
|
+
types.logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
|
|
1533
1514
|
return;
|
|
1534
1515
|
}
|
|
1535
1516
|
}
|
|
@@ -1538,7 +1519,7 @@ async function claudeRemote(opts) {
|
|
|
1538
1519
|
}
|
|
1539
1520
|
} catch (e) {
|
|
1540
1521
|
if (e instanceof AbortError) {
|
|
1541
|
-
types
|
|
1522
|
+
types.logger.debug(`[claudeRemote] Aborted`);
|
|
1542
1523
|
} else {
|
|
1543
1524
|
throw e;
|
|
1544
1525
|
}
|
|
@@ -1658,9 +1639,9 @@ class PermissionHandler {
|
|
|
1658
1639
|
this.permissionMode = response.mode;
|
|
1659
1640
|
}
|
|
1660
1641
|
if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
|
|
1661
|
-
types
|
|
1642
|
+
types.logger.debug("Plan mode result received", response);
|
|
1662
1643
|
if (response.approved) {
|
|
1663
|
-
types
|
|
1644
|
+
types.logger.debug("Plan approved - injecting PLAN_FAKE_RESTART");
|
|
1664
1645
|
if (response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode)) {
|
|
1665
1646
|
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: response.mode });
|
|
1666
1647
|
} else {
|
|
@@ -1703,7 +1684,7 @@ class PermissionHandler {
|
|
|
1703
1684
|
}
|
|
1704
1685
|
let toolCallId = this.resolveToolCallId(toolName, input);
|
|
1705
1686
|
if (!toolCallId) {
|
|
1706
|
-
await types
|
|
1687
|
+
await types.delay(1e3);
|
|
1707
1688
|
toolCallId = this.resolveToolCallId(toolName, input);
|
|
1708
1689
|
if (!toolCallId) {
|
|
1709
1690
|
throw new Error(`Could not resolve tool call ID for ${toolName}`);
|
|
@@ -1757,7 +1738,7 @@ class PermissionHandler {
|
|
|
1757
1738
|
}
|
|
1758
1739
|
}
|
|
1759
1740
|
}));
|
|
1760
|
-
types
|
|
1741
|
+
types.logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
|
|
1761
1742
|
});
|
|
1762
1743
|
}
|
|
1763
1744
|
/**
|
|
@@ -1879,11 +1860,11 @@ class PermissionHandler {
|
|
|
1879
1860
|
*/
|
|
1880
1861
|
setupClientHandler() {
|
|
1881
1862
|
this.session.client.setHandler("permission", async (message) => {
|
|
1882
|
-
types
|
|
1863
|
+
types.logger.debug(`Permission response: ${JSON.stringify(message)}`);
|
|
1883
1864
|
const id = message.id;
|
|
1884
1865
|
const pending = this.pendingRequests.get(id);
|
|
1885
1866
|
if (!pending) {
|
|
1886
|
-
types
|
|
1867
|
+
types.logger.debug("Permission request not found or already resolved");
|
|
1887
1868
|
return;
|
|
1888
1869
|
}
|
|
1889
1870
|
this.responses.set(id, { ...message, receivedAt: Date.now() });
|
|
@@ -1921,7 +1902,7 @@ class PermissionHandler {
|
|
|
1921
1902
|
}
|
|
1922
1903
|
|
|
1923
1904
|
function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
|
|
1924
|
-
types
|
|
1905
|
+
types.logger.debugLargeJson("[CLAUDE INK] Message from remote mode:", message);
|
|
1925
1906
|
switch (message.type) {
|
|
1926
1907
|
case "system": {
|
|
1927
1908
|
const sysMsg = message;
|
|
@@ -2016,7 +1997,7 @@ function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
|
|
|
2016
1997
|
} else if (resultMsg.subtype === "error_during_execution") {
|
|
2017
1998
|
messageBuffer.addMessage("\u274C Error during execution", "result");
|
|
2018
1999
|
messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns before error`, "status");
|
|
2019
|
-
types
|
|
2000
|
+
types.logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
|
|
2020
2001
|
}
|
|
2021
2002
|
break;
|
|
2022
2003
|
}
|
|
@@ -2263,7 +2244,7 @@ class OutgoingMessageQueue {
|
|
|
2263
2244
|
}
|
|
2264
2245
|
queue = [];
|
|
2265
2246
|
nextId = 1;
|
|
2266
|
-
lock = new types
|
|
2247
|
+
lock = new types.AsyncLock();
|
|
2267
2248
|
processTimer;
|
|
2268
2249
|
delayTimers = /* @__PURE__ */ new Map();
|
|
2269
2250
|
/**
|
|
@@ -2395,9 +2376,9 @@ class OutgoingMessageQueue {
|
|
|
2395
2376
|
}
|
|
2396
2377
|
|
|
2397
2378
|
async function claudeRemoteLauncher(session) {
|
|
2398
|
-
types
|
|
2379
|
+
types.logger.debug("[claudeRemoteLauncher] Starting remote launcher");
|
|
2399
2380
|
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
2400
|
-
types
|
|
2381
|
+
types.logger.debug(`[claudeRemoteLauncher] TTY available: ${hasTTY}`);
|
|
2401
2382
|
let messageBuffer = new MessageBuffer();
|
|
2402
2383
|
let inkInstance = null;
|
|
2403
2384
|
if (hasTTY) {
|
|
@@ -2406,14 +2387,14 @@ async function claudeRemoteLauncher(session) {
|
|
|
2406
2387
|
messageBuffer,
|
|
2407
2388
|
logPath: process.env.DEBUG ? session.logPath : void 0,
|
|
2408
2389
|
onExit: async () => {
|
|
2409
|
-
types
|
|
2390
|
+
types.logger.debug("[remote]: Exiting client via Ctrl-C");
|
|
2410
2391
|
if (!exitReason) {
|
|
2411
2392
|
exitReason = "exit";
|
|
2412
2393
|
}
|
|
2413
2394
|
await abort();
|
|
2414
2395
|
},
|
|
2415
2396
|
onSwitchToLocal: () => {
|
|
2416
|
-
types
|
|
2397
|
+
types.logger.debug("[remote]: Switching to local mode via double space");
|
|
2417
2398
|
doSwitch();
|
|
2418
2399
|
}
|
|
2419
2400
|
}), {
|
|
@@ -2438,11 +2419,11 @@ async function claudeRemoteLauncher(session) {
|
|
|
2438
2419
|
await abortFuture?.promise;
|
|
2439
2420
|
}
|
|
2440
2421
|
async function doAbort() {
|
|
2441
|
-
types
|
|
2422
|
+
types.logger.debug("[remote]: doAbort");
|
|
2442
2423
|
await abort();
|
|
2443
2424
|
}
|
|
2444
2425
|
async function doSwitch() {
|
|
2445
|
-
types
|
|
2426
|
+
types.logger.debug("[remote]: doSwitch");
|
|
2446
2427
|
if (!exitReason) {
|
|
2447
2428
|
exitReason = "switch";
|
|
2448
2429
|
}
|
|
@@ -2472,7 +2453,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2472
2453
|
if (umessage.message.content && Array.isArray(umessage.message.content)) {
|
|
2473
2454
|
for (let c of umessage.message.content) {
|
|
2474
2455
|
if (c.type === "tool_use" && (c.name === "exit_plan_mode" || c.name === "ExitPlanMode")) {
|
|
2475
|
-
types
|
|
2456
|
+
types.logger.debug("[remote]: detected plan mode tool call " + c.id);
|
|
2476
2457
|
planModeToolCalls.add(c.id);
|
|
2477
2458
|
}
|
|
2478
2459
|
}
|
|
@@ -2483,7 +2464,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2483
2464
|
if (umessage.message.content && Array.isArray(umessage.message.content)) {
|
|
2484
2465
|
for (let c of umessage.message.content) {
|
|
2485
2466
|
if (c.type === "tool_use") {
|
|
2486
|
-
types
|
|
2467
|
+
types.logger.debug("[remote]: detected tool use " + c.id + " parent: " + umessage.parent_tool_use_id);
|
|
2487
2468
|
ongoingToolCalls.set(c.id, { parentToolCallId: umessage.parent_tool_use_id ?? null });
|
|
2488
2469
|
}
|
|
2489
2470
|
}
|
|
@@ -2511,8 +2492,8 @@ async function claudeRemoteLauncher(session) {
|
|
|
2511
2492
|
content: umessage.message.content.map((c) => {
|
|
2512
2493
|
if (c.type === "tool_result" && c.tool_use_id && planModeToolCalls.has(c.tool_use_id)) {
|
|
2513
2494
|
if (c.content === PLAN_FAKE_REJECT) {
|
|
2514
|
-
types
|
|
2515
|
-
types
|
|
2495
|
+
types.logger.debug("[remote]: hack plan mode exit");
|
|
2496
|
+
types.logger.debugLargeJson("[remote]: hack plan mode exit", c);
|
|
2516
2497
|
return {
|
|
2517
2498
|
...c,
|
|
2518
2499
|
is_error: false,
|
|
@@ -2597,7 +2578,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2597
2578
|
try {
|
|
2598
2579
|
let pending = null;
|
|
2599
2580
|
while (!exitReason) {
|
|
2600
|
-
types
|
|
2581
|
+
types.logger.debug("[remote]: launch");
|
|
2601
2582
|
messageBuffer.addMessage("\u2550".repeat(40), "status");
|
|
2602
2583
|
messageBuffer.addMessage("Starting new Claude session...", "status");
|
|
2603
2584
|
const controller = new AbortController();
|
|
@@ -2627,7 +2608,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2627
2608
|
let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
|
|
2628
2609
|
if (msg) {
|
|
2629
2610
|
if (modeHash && msg.hash !== modeHash || msg.isolate) {
|
|
2630
|
-
types
|
|
2611
|
+
types.logger.debug("[remote]: mode has changed, pending message");
|
|
2631
2612
|
pending = msg;
|
|
2632
2613
|
return null;
|
|
2633
2614
|
}
|
|
@@ -2650,16 +2631,21 @@ async function claudeRemoteLauncher(session) {
|
|
|
2650
2631
|
claudeArgs: session.claudeArgs,
|
|
2651
2632
|
onMessage,
|
|
2652
2633
|
onCompletionEvent: (message) => {
|
|
2653
|
-
types
|
|
2634
|
+
types.logger.debug(`[remote]: Completion event: ${message}`);
|
|
2654
2635
|
session.client.sendSessionEvent({ type: "message", message });
|
|
2655
2636
|
},
|
|
2656
2637
|
onSessionReset: () => {
|
|
2657
|
-
types
|
|
2638
|
+
types.logger.debug("[remote]: Session reset");
|
|
2658
2639
|
session.clearSessionId();
|
|
2659
2640
|
},
|
|
2660
2641
|
onReady: () => {
|
|
2661
2642
|
if (!pending && session.queue.size() === 0) {
|
|
2662
2643
|
session.client.sendSessionEvent({ type: "ready" });
|
|
2644
|
+
session.api.push().sendToAllDevices(
|
|
2645
|
+
"It's ready!",
|
|
2646
|
+
`Claude is waiting for your command`,
|
|
2647
|
+
{ sessionId: session.client.sessionId }
|
|
2648
|
+
);
|
|
2663
2649
|
}
|
|
2664
2650
|
},
|
|
2665
2651
|
signal: abortController.signal
|
|
@@ -2668,29 +2654,29 @@ async function claudeRemoteLauncher(session) {
|
|
|
2668
2654
|
session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
2669
2655
|
}
|
|
2670
2656
|
} catch (e) {
|
|
2671
|
-
types
|
|
2657
|
+
types.logger.debug("[remote]: launch error", e);
|
|
2672
2658
|
if (!exitReason) {
|
|
2673
2659
|
session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
2674
2660
|
continue;
|
|
2675
2661
|
}
|
|
2676
2662
|
} finally {
|
|
2677
|
-
types
|
|
2663
|
+
types.logger.debug("[remote]: launch finally");
|
|
2678
2664
|
for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
|
|
2679
2665
|
const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
|
|
2680
2666
|
if (converted) {
|
|
2681
|
-
types
|
|
2667
|
+
types.logger.debug("[remote]: terminating tool call " + toolCallId + " parent: " + parentToolCallId);
|
|
2682
2668
|
session.client.sendClaudeSessionMessage(converted);
|
|
2683
2669
|
}
|
|
2684
2670
|
}
|
|
2685
2671
|
ongoingToolCalls.clear();
|
|
2686
|
-
types
|
|
2672
|
+
types.logger.debug("[remote]: flushing message queue");
|
|
2687
2673
|
await messageQueue.flush();
|
|
2688
2674
|
messageQueue.destroy();
|
|
2689
|
-
types
|
|
2675
|
+
types.logger.debug("[remote]: message queue flushed");
|
|
2690
2676
|
abortController = null;
|
|
2691
2677
|
abortFuture?.resolve(void 0);
|
|
2692
2678
|
abortFuture = null;
|
|
2693
|
-
types
|
|
2679
|
+
types.logger.debug("[remote]: launch done");
|
|
2694
2680
|
permissionHandler.reset();
|
|
2695
2681
|
modeHash = null;
|
|
2696
2682
|
mode = null;
|
|
@@ -2714,7 +2700,7 @@ async function claudeRemoteLauncher(session) {
|
|
|
2714
2700
|
}
|
|
2715
2701
|
|
|
2716
2702
|
async function loop(opts) {
|
|
2717
|
-
const logPath =
|
|
2703
|
+
const logPath = types.logger.logFilePath;
|
|
2718
2704
|
let session = new Session({
|
|
2719
2705
|
api: opts.api,
|
|
2720
2706
|
client: opts.session,
|
|
@@ -2733,7 +2719,7 @@ async function loop(opts) {
|
|
|
2733
2719
|
}
|
|
2734
2720
|
let mode = opts.startingMode ?? "local";
|
|
2735
2721
|
while (true) {
|
|
2736
|
-
types
|
|
2722
|
+
types.logger.debug(`[loop] Iteration with mode: ${mode}`);
|
|
2737
2723
|
if (mode === "local") {
|
|
2738
2724
|
let reason = await claudeLocalLauncher(session);
|
|
2739
2725
|
if (reason === "exit") {
|
|
@@ -2759,133 +2745,6 @@ async function loop(opts) {
|
|
|
2759
2745
|
}
|
|
2760
2746
|
}
|
|
2761
2747
|
|
|
2762
|
-
var name = "happy-coder";
|
|
2763
|
-
var version = "0.9.0-5";
|
|
2764
|
-
var description = "Claude Code session sharing CLI";
|
|
2765
|
-
var author = "Kirill Dubovitskiy";
|
|
2766
|
-
var license = "MIT";
|
|
2767
|
-
var type = "module";
|
|
2768
|
-
var homepage = "https://github.com/slopus/happy-cli";
|
|
2769
|
-
var bugs = "https://github.com/slopus/happy-cli/issues";
|
|
2770
|
-
var repository = "slopus/happy-cli";
|
|
2771
|
-
var bin = {
|
|
2772
|
-
happy: "./bin/happy.mjs"
|
|
2773
|
-
};
|
|
2774
|
-
var main = "./dist/index.cjs";
|
|
2775
|
-
var module$1 = "./dist/index.mjs";
|
|
2776
|
-
var types = "./dist/index.d.cts";
|
|
2777
|
-
var exports$1 = {
|
|
2778
|
-
".": {
|
|
2779
|
-
require: {
|
|
2780
|
-
types: "./dist/index.d.cts",
|
|
2781
|
-
"default": "./dist/index.cjs"
|
|
2782
|
-
},
|
|
2783
|
-
"import": {
|
|
2784
|
-
types: "./dist/index.d.mts",
|
|
2785
|
-
"default": "./dist/index.mjs"
|
|
2786
|
-
}
|
|
2787
|
-
},
|
|
2788
|
-
"./lib": {
|
|
2789
|
-
require: {
|
|
2790
|
-
types: "./dist/lib.d.cts",
|
|
2791
|
-
"default": "./dist/lib.cjs"
|
|
2792
|
-
},
|
|
2793
|
-
"import": {
|
|
2794
|
-
types: "./dist/lib.d.mts",
|
|
2795
|
-
"default": "./dist/lib.mjs"
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
};
|
|
2799
|
-
var files = [
|
|
2800
|
-
"dist",
|
|
2801
|
-
"bin",
|
|
2802
|
-
"scripts",
|
|
2803
|
-
"ripgrep",
|
|
2804
|
-
"package.json"
|
|
2805
|
-
];
|
|
2806
|
-
var scripts = {
|
|
2807
|
-
"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",
|
|
2808
|
-
typecheck: "tsc --noEmit",
|
|
2809
|
-
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
|
|
2810
|
-
test: "yarn build && vitest run",
|
|
2811
|
-
"test:watch": "vitest",
|
|
2812
|
-
"test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
2813
|
-
dev: "yarn build && DEBUG=1 npx tsx src/index.ts",
|
|
2814
|
-
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
2815
|
-
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
2816
|
-
prepublishOnly: "yarn build && yarn test",
|
|
2817
|
-
release: "release-it"
|
|
2818
|
-
};
|
|
2819
|
-
var dependencies = {
|
|
2820
|
-
"@anthropic-ai/claude-code": "^1.0.89",
|
|
2821
|
-
"@anthropic-ai/sdk": "^0.56.0",
|
|
2822
|
-
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
2823
|
-
"@stablelib/base64": "^2.0.1",
|
|
2824
|
-
"@types/http-proxy": "^1.17.16",
|
|
2825
|
-
"@types/qrcode-terminal": "^0.12.2",
|
|
2826
|
-
"@types/react": "^19.1.9",
|
|
2827
|
-
axios: "^1.10.0",
|
|
2828
|
-
chalk: "^5.4.1",
|
|
2829
|
-
"expo-server-sdk": "^3.15.0",
|
|
2830
|
-
fastify: "^5.5.0",
|
|
2831
|
-
"fastify-type-provider-zod": "4.0.2",
|
|
2832
|
-
"http-proxy": "^1.18.1",
|
|
2833
|
-
"http-proxy-middleware": "^3.0.5",
|
|
2834
|
-
ink: "^6.1.0",
|
|
2835
|
-
open: "^10.2.0",
|
|
2836
|
-
"qrcode-terminal": "^0.12.0",
|
|
2837
|
-
react: "^19.1.1",
|
|
2838
|
-
"socket.io-client": "^4.8.1",
|
|
2839
|
-
tweetnacl: "^1.0.3",
|
|
2840
|
-
zod: "^3.23.8"
|
|
2841
|
-
};
|
|
2842
|
-
var devDependencies = {
|
|
2843
|
-
"@eslint/compat": "^1",
|
|
2844
|
-
"@types/node": ">=20",
|
|
2845
|
-
"cross-env": "^10.0.0",
|
|
2846
|
-
eslint: "^9",
|
|
2847
|
-
"eslint-config-prettier": "^10",
|
|
2848
|
-
pkgroll: "^2.14.2",
|
|
2849
|
-
"release-it": "^19.0.4",
|
|
2850
|
-
shx: "^0.3.3",
|
|
2851
|
-
"ts-node": "^10",
|
|
2852
|
-
tsx: "^4.20.3",
|
|
2853
|
-
typescript: "^5",
|
|
2854
|
-
vitest: "^3.2.4"
|
|
2855
|
-
};
|
|
2856
|
-
var resolutions = {
|
|
2857
|
-
"whatwg-url": "14.2.0",
|
|
2858
|
-
"parse-path": "7.0.3",
|
|
2859
|
-
"@types/parse-path": "7.0.3"
|
|
2860
|
-
};
|
|
2861
|
-
var publishConfig = {
|
|
2862
|
-
registry: "https://registry.npmjs.org"
|
|
2863
|
-
};
|
|
2864
|
-
var packageManager = "yarn@1.22.22";
|
|
2865
|
-
var packageJson = {
|
|
2866
|
-
name: name,
|
|
2867
|
-
version: version,
|
|
2868
|
-
description: description,
|
|
2869
|
-
author: author,
|
|
2870
|
-
license: license,
|
|
2871
|
-
type: type,
|
|
2872
|
-
homepage: homepage,
|
|
2873
|
-
bugs: bugs,
|
|
2874
|
-
repository: repository,
|
|
2875
|
-
bin: bin,
|
|
2876
|
-
main: main,
|
|
2877
|
-
module: module$1,
|
|
2878
|
-
types: types,
|
|
2879
|
-
exports: exports$1,
|
|
2880
|
-
files: files,
|
|
2881
|
-
scripts: scripts,
|
|
2882
|
-
dependencies: dependencies,
|
|
2883
|
-
devDependencies: devDependencies,
|
|
2884
|
-
resolutions: resolutions,
|
|
2885
|
-
publishConfig: publishConfig,
|
|
2886
|
-
packageManager: packageManager
|
|
2887
|
-
};
|
|
2888
|
-
|
|
2889
2748
|
function run(args, options) {
|
|
2890
2749
|
const RUNNER_PATH = path.resolve(path.join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
2891
2750
|
return new Promise((resolve2, reject) => {
|
|
@@ -2917,7 +2776,7 @@ function run(args, options) {
|
|
|
2917
2776
|
const execAsync = util.promisify(child_process.exec);
|
|
2918
2777
|
function registerHandlers(session) {
|
|
2919
2778
|
session.setHandler("bash", async (data) => {
|
|
2920
|
-
types
|
|
2779
|
+
types.logger.debug("Shell command request:", data.command);
|
|
2921
2780
|
try {
|
|
2922
2781
|
const options = {
|
|
2923
2782
|
cwd: data.cwd,
|
|
@@ -2952,22 +2811,22 @@ function registerHandlers(session) {
|
|
|
2952
2811
|
}
|
|
2953
2812
|
});
|
|
2954
2813
|
session.setHandler("readFile", async (data) => {
|
|
2955
|
-
types
|
|
2814
|
+
types.logger.debug("Read file request:", data.path);
|
|
2956
2815
|
try {
|
|
2957
|
-
const buffer = await
|
|
2816
|
+
const buffer = await fs.readFile(data.path);
|
|
2958
2817
|
const content = buffer.toString("base64");
|
|
2959
2818
|
return { success: true, content };
|
|
2960
2819
|
} catch (error) {
|
|
2961
|
-
types
|
|
2820
|
+
types.logger.debug("Failed to read file:", error);
|
|
2962
2821
|
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
2963
2822
|
}
|
|
2964
2823
|
});
|
|
2965
2824
|
session.setHandler("writeFile", async (data) => {
|
|
2966
|
-
types
|
|
2825
|
+
types.logger.debug("Write file request:", data.path);
|
|
2967
2826
|
try {
|
|
2968
2827
|
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
2969
2828
|
try {
|
|
2970
|
-
const existingBuffer = await
|
|
2829
|
+
const existingBuffer = await fs.readFile(data.path);
|
|
2971
2830
|
const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
|
|
2972
2831
|
if (existingHash !== data.expectedHash) {
|
|
2973
2832
|
return {
|
|
@@ -2987,7 +2846,7 @@ function registerHandlers(session) {
|
|
|
2987
2846
|
}
|
|
2988
2847
|
} else {
|
|
2989
2848
|
try {
|
|
2990
|
-
await
|
|
2849
|
+
await fs.stat(data.path);
|
|
2991
2850
|
return {
|
|
2992
2851
|
success: false,
|
|
2993
2852
|
error: "File already exists but was expected to be new"
|
|
@@ -3000,18 +2859,18 @@ function registerHandlers(session) {
|
|
|
3000
2859
|
}
|
|
3001
2860
|
}
|
|
3002
2861
|
const buffer = Buffer.from(data.content, "base64");
|
|
3003
|
-
await
|
|
2862
|
+
await fs.writeFile(data.path, buffer);
|
|
3004
2863
|
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
3005
2864
|
return { success: true, hash };
|
|
3006
2865
|
} catch (error) {
|
|
3007
|
-
types
|
|
2866
|
+
types.logger.debug("Failed to write file:", error);
|
|
3008
2867
|
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
3009
2868
|
}
|
|
3010
2869
|
});
|
|
3011
2870
|
session.setHandler("listDirectory", async (data) => {
|
|
3012
|
-
types
|
|
2871
|
+
types.logger.debug("List directory request:", data.path);
|
|
3013
2872
|
try {
|
|
3014
|
-
const entries = await
|
|
2873
|
+
const entries = await fs.readdir(data.path, { withFileTypes: true });
|
|
3015
2874
|
const directoryEntries = await Promise.all(
|
|
3016
2875
|
entries.map(async (entry) => {
|
|
3017
2876
|
const fullPath = path.join(data.path, entry.name);
|
|
@@ -3024,11 +2883,11 @@ function registerHandlers(session) {
|
|
|
3024
2883
|
type = "file";
|
|
3025
2884
|
}
|
|
3026
2885
|
try {
|
|
3027
|
-
const stats = await
|
|
2886
|
+
const stats = await fs.stat(fullPath);
|
|
3028
2887
|
size = stats.size;
|
|
3029
2888
|
modified = stats.mtime.getTime();
|
|
3030
2889
|
} catch (error) {
|
|
3031
|
-
types
|
|
2890
|
+
types.logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
3032
2891
|
}
|
|
3033
2892
|
return {
|
|
3034
2893
|
name: entry.name,
|
|
@@ -3045,15 +2904,15 @@ function registerHandlers(session) {
|
|
|
3045
2904
|
});
|
|
3046
2905
|
return { success: true, entries: directoryEntries };
|
|
3047
2906
|
} catch (error) {
|
|
3048
|
-
types
|
|
2907
|
+
types.logger.debug("Failed to list directory:", error);
|
|
3049
2908
|
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
3050
2909
|
}
|
|
3051
2910
|
});
|
|
3052
2911
|
session.setHandler("getDirectoryTree", async (data) => {
|
|
3053
|
-
types
|
|
2912
|
+
types.logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
3054
2913
|
async function buildTree(path$1, name, currentDepth) {
|
|
3055
2914
|
try {
|
|
3056
|
-
const stats = await
|
|
2915
|
+
const stats = await fs.stat(path$1);
|
|
3057
2916
|
const node = {
|
|
3058
2917
|
name,
|
|
3059
2918
|
path: path$1,
|
|
@@ -3062,12 +2921,12 @@ function registerHandlers(session) {
|
|
|
3062
2921
|
modified: stats.mtime.getTime()
|
|
3063
2922
|
};
|
|
3064
2923
|
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
3065
|
-
const entries = await
|
|
2924
|
+
const entries = await fs.readdir(path$1, { withFileTypes: true });
|
|
3066
2925
|
const children = [];
|
|
3067
2926
|
await Promise.all(
|
|
3068
2927
|
entries.map(async (entry) => {
|
|
3069
2928
|
if (entry.isSymbolicLink()) {
|
|
3070
|
-
types
|
|
2929
|
+
types.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
|
|
3071
2930
|
return;
|
|
3072
2931
|
}
|
|
3073
2932
|
const childPath = path.join(path$1, entry.name);
|
|
@@ -3086,7 +2945,7 @@ function registerHandlers(session) {
|
|
|
3086
2945
|
}
|
|
3087
2946
|
return node;
|
|
3088
2947
|
} catch (error) {
|
|
3089
|
-
types
|
|
2948
|
+
types.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
|
|
3090
2949
|
return null;
|
|
3091
2950
|
}
|
|
3092
2951
|
}
|
|
@@ -3101,12 +2960,12 @@ function registerHandlers(session) {
|
|
|
3101
2960
|
}
|
|
3102
2961
|
return { success: true, tree };
|
|
3103
2962
|
} catch (error) {
|
|
3104
|
-
types
|
|
2963
|
+
types.logger.debug("Failed to get directory tree:", error);
|
|
3105
2964
|
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
3106
2965
|
}
|
|
3107
2966
|
});
|
|
3108
2967
|
session.setHandler("ripgrep", async (data) => {
|
|
3109
|
-
types
|
|
2968
|
+
types.logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
3110
2969
|
try {
|
|
3111
2970
|
const result = await run(data.args, { cwd: data.cwd });
|
|
3112
2971
|
return {
|
|
@@ -3116,7 +2975,7 @@ function registerHandlers(session) {
|
|
|
3116
2975
|
stderr: result.stderr.toString()
|
|
3117
2976
|
};
|
|
3118
2977
|
} catch (error) {
|
|
3119
|
-
types
|
|
2978
|
+
types.logger.debug("Failed to run ripgrep:", error);
|
|
3120
2979
|
return {
|
|
3121
2980
|
success: false,
|
|
3122
2981
|
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
@@ -3124,19 +2983,19 @@ function registerHandlers(session) {
|
|
|
3124
2983
|
}
|
|
3125
2984
|
});
|
|
3126
2985
|
session.setHandler("killSession", async () => {
|
|
3127
|
-
types
|
|
2986
|
+
types.logger.debug("Kill session request received");
|
|
3128
2987
|
try {
|
|
3129
2988
|
const response = {
|
|
3130
2989
|
success: true,
|
|
3131
2990
|
message: "Session termination acknowledged, exiting in 100ms"
|
|
3132
2991
|
};
|
|
3133
2992
|
setTimeout(() => {
|
|
3134
|
-
types
|
|
2993
|
+
types.logger.debug("[KILL SESSION] Exiting process as requested");
|
|
3135
2994
|
process.exit(0);
|
|
3136
2995
|
}, 100);
|
|
3137
2996
|
return response;
|
|
3138
2997
|
} catch (error) {
|
|
3139
|
-
types
|
|
2998
|
+
types.logger.debug("Failed to kill session:", error);
|
|
3140
2999
|
return {
|
|
3141
3000
|
success: false,
|
|
3142
3001
|
message: error instanceof Error ? error.message : "Failed to kill session"
|
|
@@ -3145,129 +3004,6 @@ function registerHandlers(session) {
|
|
|
3145
3004
|
});
|
|
3146
3005
|
}
|
|
3147
3006
|
|
|
3148
|
-
const defaultSettings = {
|
|
3149
|
-
onboardingCompleted: false
|
|
3150
|
-
};
|
|
3151
|
-
async function readSettings() {
|
|
3152
|
-
if (!node_fs.existsSync(types$1.configuration.settingsFile)) {
|
|
3153
|
-
return { ...defaultSettings };
|
|
3154
|
-
}
|
|
3155
|
-
try {
|
|
3156
|
-
const content = await promises$1.readFile(types$1.configuration.settingsFile, "utf8");
|
|
3157
|
-
return JSON.parse(content);
|
|
3158
|
-
} catch {
|
|
3159
|
-
return { ...defaultSettings };
|
|
3160
|
-
}
|
|
3161
|
-
}
|
|
3162
|
-
async function updateSettings(updater) {
|
|
3163
|
-
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
3164
|
-
const MAX_LOCK_ATTEMPTS = 50;
|
|
3165
|
-
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
3166
|
-
const lockFile = types$1.configuration.settingsFile + ".lock";
|
|
3167
|
-
const tmpFile = types$1.configuration.settingsFile + ".tmp";
|
|
3168
|
-
let fileHandle;
|
|
3169
|
-
let attempts = 0;
|
|
3170
|
-
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
3171
|
-
try {
|
|
3172
|
-
fileHandle = await promises$1.open(lockFile, node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY);
|
|
3173
|
-
break;
|
|
3174
|
-
} catch (err) {
|
|
3175
|
-
if (err.code === "EEXIST") {
|
|
3176
|
-
attempts++;
|
|
3177
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
3178
|
-
try {
|
|
3179
|
-
const stats = await promises$1.stat(lockFile);
|
|
3180
|
-
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
3181
|
-
await promises$1.unlink(lockFile).catch(() => {
|
|
3182
|
-
});
|
|
3183
|
-
}
|
|
3184
|
-
} catch {
|
|
3185
|
-
}
|
|
3186
|
-
} else {
|
|
3187
|
-
throw err;
|
|
3188
|
-
}
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
if (!fileHandle) {
|
|
3192
|
-
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
3193
|
-
}
|
|
3194
|
-
try {
|
|
3195
|
-
const current = await readSettings() || { ...defaultSettings };
|
|
3196
|
-
const updated = await updater(current);
|
|
3197
|
-
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
3198
|
-
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
3199
|
-
}
|
|
3200
|
-
await promises$1.writeFile(tmpFile, JSON.stringify(updated, null, 2));
|
|
3201
|
-
await promises$1.rename(tmpFile, types$1.configuration.settingsFile);
|
|
3202
|
-
return updated;
|
|
3203
|
-
} finally {
|
|
3204
|
-
await fileHandle.close();
|
|
3205
|
-
await promises$1.unlink(lockFile).catch(() => {
|
|
3206
|
-
});
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
const credentialsSchema = z__namespace.object({
|
|
3210
|
-
secret: z__namespace.string().base64(),
|
|
3211
|
-
token: z__namespace.string()
|
|
3212
|
-
});
|
|
3213
|
-
async function readCredentials() {
|
|
3214
|
-
if (!node_fs.existsSync(types$1.configuration.privateKeyFile)) {
|
|
3215
|
-
return null;
|
|
3216
|
-
}
|
|
3217
|
-
try {
|
|
3218
|
-
const keyBase64 = await promises$1.readFile(types$1.configuration.privateKeyFile, "utf8");
|
|
3219
|
-
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
3220
|
-
return {
|
|
3221
|
-
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
3222
|
-
token: credentials.token
|
|
3223
|
-
};
|
|
3224
|
-
} catch {
|
|
3225
|
-
return null;
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
3228
|
-
async function writeCredentials(credentials) {
|
|
3229
|
-
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
3230
|
-
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
3231
|
-
}
|
|
3232
|
-
await promises$1.writeFile(types$1.configuration.privateKeyFile, JSON.stringify({
|
|
3233
|
-
secret: types$1.encodeBase64(credentials.secret),
|
|
3234
|
-
token: credentials.token
|
|
3235
|
-
}, null, 2));
|
|
3236
|
-
}
|
|
3237
|
-
async function clearCredentials() {
|
|
3238
|
-
if (node_fs.existsSync(types$1.configuration.privateKeyFile)) {
|
|
3239
|
-
await promises$1.unlink(types$1.configuration.privateKeyFile);
|
|
3240
|
-
}
|
|
3241
|
-
}
|
|
3242
|
-
async function clearMachineId() {
|
|
3243
|
-
await updateSettings((settings) => ({
|
|
3244
|
-
...settings,
|
|
3245
|
-
machineId: void 0
|
|
3246
|
-
}));
|
|
3247
|
-
}
|
|
3248
|
-
async function readDaemonState() {
|
|
3249
|
-
try {
|
|
3250
|
-
if (!node_fs.existsSync(types$1.configuration.daemonStateFile)) {
|
|
3251
|
-
return null;
|
|
3252
|
-
}
|
|
3253
|
-
const content = await promises$1.readFile(types$1.configuration.daemonStateFile, "utf-8");
|
|
3254
|
-
return JSON.parse(content);
|
|
3255
|
-
} catch (error) {
|
|
3256
|
-
return null;
|
|
3257
|
-
}
|
|
3258
|
-
}
|
|
3259
|
-
async function writeDaemonState(state) {
|
|
3260
|
-
if (!node_fs.existsSync(types$1.configuration.happyHomeDir)) {
|
|
3261
|
-
await promises$1.mkdir(types$1.configuration.happyHomeDir, { recursive: true });
|
|
3262
|
-
}
|
|
3263
|
-
await promises$1.writeFile(types$1.configuration.daemonStateFile, JSON.stringify(state, null, 2));
|
|
3264
|
-
}
|
|
3265
|
-
async function clearDaemonState() {
|
|
3266
|
-
if (node_fs.existsSync(types$1.configuration.daemonStateFile)) {
|
|
3267
|
-
await promises$1.unlink(types$1.configuration.daemonStateFile);
|
|
3268
|
-
}
|
|
3269
|
-
}
|
|
3270
|
-
|
|
3271
3007
|
class MessageQueue2 {
|
|
3272
3008
|
queue = [];
|
|
3273
3009
|
// Made public for testing
|
|
@@ -3278,7 +3014,7 @@ class MessageQueue2 {
|
|
|
3278
3014
|
constructor(modeHasher, onMessageHandler = null) {
|
|
3279
3015
|
this.modeHasher = modeHasher;
|
|
3280
3016
|
this.onMessageHandler = onMessageHandler;
|
|
3281
|
-
types
|
|
3017
|
+
types.logger.debug(`[MessageQueue2] Initialized`);
|
|
3282
3018
|
}
|
|
3283
3019
|
/**
|
|
3284
3020
|
* Set a handler that will be called when a message arrives
|
|
@@ -3294,7 +3030,7 @@ class MessageQueue2 {
|
|
|
3294
3030
|
throw new Error("Cannot push to closed queue");
|
|
3295
3031
|
}
|
|
3296
3032
|
const modeHash = this.modeHasher(mode);
|
|
3297
|
-
types
|
|
3033
|
+
types.logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
|
|
3298
3034
|
this.queue.push({
|
|
3299
3035
|
message,
|
|
3300
3036
|
mode,
|
|
@@ -3305,12 +3041,12 @@ class MessageQueue2 {
|
|
|
3305
3041
|
this.onMessageHandler(message, mode);
|
|
3306
3042
|
}
|
|
3307
3043
|
if (this.waiter) {
|
|
3308
|
-
types
|
|
3044
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter`);
|
|
3309
3045
|
const waiter = this.waiter;
|
|
3310
3046
|
this.waiter = null;
|
|
3311
3047
|
waiter(true);
|
|
3312
3048
|
}
|
|
3313
|
-
types
|
|
3049
|
+
types.logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
|
|
3314
3050
|
}
|
|
3315
3051
|
/**
|
|
3316
3052
|
* Push a message immediately without batching delay.
|
|
@@ -3321,7 +3057,7 @@ class MessageQueue2 {
|
|
|
3321
3057
|
throw new Error("Cannot push to closed queue");
|
|
3322
3058
|
}
|
|
3323
3059
|
const modeHash = this.modeHasher(mode);
|
|
3324
|
-
types
|
|
3060
|
+
types.logger.debug(`[MessageQueue2] pushImmediate() called with mode hash: ${modeHash}`);
|
|
3325
3061
|
this.queue.push({
|
|
3326
3062
|
message,
|
|
3327
3063
|
mode,
|
|
@@ -3332,12 +3068,12 @@ class MessageQueue2 {
|
|
|
3332
3068
|
this.onMessageHandler(message, mode);
|
|
3333
3069
|
}
|
|
3334
3070
|
if (this.waiter) {
|
|
3335
|
-
types
|
|
3071
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter for immediate message`);
|
|
3336
3072
|
const waiter = this.waiter;
|
|
3337
3073
|
this.waiter = null;
|
|
3338
3074
|
waiter(true);
|
|
3339
3075
|
}
|
|
3340
|
-
types
|
|
3076
|
+
types.logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
|
|
3341
3077
|
}
|
|
3342
3078
|
/**
|
|
3343
3079
|
* Push a message that must be processed in complete isolation.
|
|
@@ -3349,7 +3085,7 @@ class MessageQueue2 {
|
|
|
3349
3085
|
throw new Error("Cannot push to closed queue");
|
|
3350
3086
|
}
|
|
3351
3087
|
const modeHash = this.modeHasher(mode);
|
|
3352
|
-
types
|
|
3088
|
+
types.logger.debug(`[MessageQueue2] pushIsolateAndClear() called with mode hash: ${modeHash} - clearing ${this.queue.length} pending messages`);
|
|
3353
3089
|
this.queue = [];
|
|
3354
3090
|
this.queue.push({
|
|
3355
3091
|
message,
|
|
@@ -3361,12 +3097,12 @@ class MessageQueue2 {
|
|
|
3361
3097
|
this.onMessageHandler(message, mode);
|
|
3362
3098
|
}
|
|
3363
3099
|
if (this.waiter) {
|
|
3364
|
-
types
|
|
3100
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
|
|
3365
3101
|
const waiter = this.waiter;
|
|
3366
3102
|
this.waiter = null;
|
|
3367
3103
|
waiter(true);
|
|
3368
3104
|
}
|
|
3369
|
-
types
|
|
3105
|
+
types.logger.debug(`[MessageQueue2] pushIsolateAndClear() completed. Queue size: ${this.queue.length}`);
|
|
3370
3106
|
}
|
|
3371
3107
|
/**
|
|
3372
3108
|
* Push a message to the beginning of the queue with a mode.
|
|
@@ -3376,7 +3112,7 @@ class MessageQueue2 {
|
|
|
3376
3112
|
throw new Error("Cannot unshift to closed queue");
|
|
3377
3113
|
}
|
|
3378
3114
|
const modeHash = this.modeHasher(mode);
|
|
3379
|
-
types
|
|
3115
|
+
types.logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
|
|
3380
3116
|
this.queue.unshift({
|
|
3381
3117
|
message,
|
|
3382
3118
|
mode,
|
|
@@ -3387,18 +3123,18 @@ class MessageQueue2 {
|
|
|
3387
3123
|
this.onMessageHandler(message, mode);
|
|
3388
3124
|
}
|
|
3389
3125
|
if (this.waiter) {
|
|
3390
|
-
types
|
|
3126
|
+
types.logger.debug(`[MessageQueue2] Notifying waiter`);
|
|
3391
3127
|
const waiter = this.waiter;
|
|
3392
3128
|
this.waiter = null;
|
|
3393
3129
|
waiter(true);
|
|
3394
3130
|
}
|
|
3395
|
-
types
|
|
3131
|
+
types.logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
|
|
3396
3132
|
}
|
|
3397
3133
|
/**
|
|
3398
3134
|
* Reset the queue - clears all messages and resets to empty state
|
|
3399
3135
|
*/
|
|
3400
3136
|
reset() {
|
|
3401
|
-
types
|
|
3137
|
+
types.logger.debug(`[MessageQueue2] reset() called. Clearing ${this.queue.length} messages`);
|
|
3402
3138
|
this.queue = [];
|
|
3403
3139
|
this.closed = false;
|
|
3404
3140
|
this.waiter = null;
|
|
@@ -3407,7 +3143,7 @@ class MessageQueue2 {
|
|
|
3407
3143
|
* Close the queue - no more messages can be pushed
|
|
3408
3144
|
*/
|
|
3409
3145
|
close() {
|
|
3410
|
-
types
|
|
3146
|
+
types.logger.debug(`[MessageQueue2] close() called`);
|
|
3411
3147
|
this.closed = true;
|
|
3412
3148
|
if (this.waiter) {
|
|
3413
3149
|
const waiter = this.waiter;
|
|
@@ -3459,13 +3195,13 @@ class MessageQueue2 {
|
|
|
3459
3195
|
if (firstItem.isolate) {
|
|
3460
3196
|
const item = this.queue.shift();
|
|
3461
3197
|
sameModeMessages.push(item.message);
|
|
3462
|
-
types
|
|
3198
|
+
types.logger.debug(`[MessageQueue2] Collected isolated message with mode hash: ${targetModeHash}`);
|
|
3463
3199
|
} else {
|
|
3464
3200
|
while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash && !this.queue[0].isolate) {
|
|
3465
3201
|
const item = this.queue.shift();
|
|
3466
3202
|
sameModeMessages.push(item.message);
|
|
3467
3203
|
}
|
|
3468
|
-
types
|
|
3204
|
+
types.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
|
|
3469
3205
|
}
|
|
3470
3206
|
const combinedMessage = sameModeMessages.join("\n");
|
|
3471
3207
|
return {
|
|
@@ -3483,7 +3219,7 @@ class MessageQueue2 {
|
|
|
3483
3219
|
let abortHandler = null;
|
|
3484
3220
|
if (abortSignal) {
|
|
3485
3221
|
abortHandler = () => {
|
|
3486
|
-
types
|
|
3222
|
+
types.logger.debug("[MessageQueue2] Wait aborted");
|
|
3487
3223
|
if (this.waiter === waiterFunc) {
|
|
3488
3224
|
this.waiter = null;
|
|
3489
3225
|
}
|
|
@@ -3512,7 +3248,7 @@ class MessageQueue2 {
|
|
|
3512
3248
|
return;
|
|
3513
3249
|
}
|
|
3514
3250
|
this.waiter = waiterFunc;
|
|
3515
|
-
types
|
|
3251
|
+
types.logger.debug("[MessageQueue2] Waiting for messages...");
|
|
3516
3252
|
});
|
|
3517
3253
|
}
|
|
3518
3254
|
}
|
|
@@ -3606,11 +3342,11 @@ function hashObject(obj, options, encoding = "hex") {
|
|
|
3606
3342
|
let caffeinateProcess = null;
|
|
3607
3343
|
function startCaffeinate() {
|
|
3608
3344
|
if (process.platform !== "darwin") {
|
|
3609
|
-
types
|
|
3345
|
+
types.logger.debug("[caffeinate] Not on macOS, skipping caffeinate");
|
|
3610
3346
|
return false;
|
|
3611
3347
|
}
|
|
3612
3348
|
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3613
|
-
types
|
|
3349
|
+
types.logger.debug("[caffeinate] Caffeinate already running");
|
|
3614
3350
|
return true;
|
|
3615
3351
|
}
|
|
3616
3352
|
try {
|
|
@@ -3619,41 +3355,41 @@ function startCaffeinate() {
|
|
|
3619
3355
|
detached: false
|
|
3620
3356
|
});
|
|
3621
3357
|
caffeinateProcess.on("error", (error) => {
|
|
3622
|
-
types
|
|
3358
|
+
types.logger.debug("[caffeinate] Error starting caffeinate:", error);
|
|
3623
3359
|
caffeinateProcess = null;
|
|
3624
3360
|
});
|
|
3625
3361
|
caffeinateProcess.on("exit", (code, signal) => {
|
|
3626
|
-
types
|
|
3362
|
+
types.logger.debug(`[caffeinate] Process exited with code ${code}, signal ${signal}`);
|
|
3627
3363
|
caffeinateProcess = null;
|
|
3628
3364
|
});
|
|
3629
|
-
types
|
|
3365
|
+
types.logger.debug(`[caffeinate] Started with PID ${caffeinateProcess.pid}`);
|
|
3630
3366
|
setupCleanupHandlers();
|
|
3631
3367
|
return true;
|
|
3632
3368
|
} catch (error) {
|
|
3633
|
-
types
|
|
3369
|
+
types.logger.debug("[caffeinate] Failed to start caffeinate:", error);
|
|
3634
3370
|
return false;
|
|
3635
3371
|
}
|
|
3636
3372
|
}
|
|
3637
3373
|
let isStopping = false;
|
|
3638
|
-
function stopCaffeinate() {
|
|
3374
|
+
async function stopCaffeinate() {
|
|
3639
3375
|
if (isStopping) {
|
|
3376
|
+
types.logger.debug("[caffeinate] Already stopping, skipping");
|
|
3640
3377
|
return;
|
|
3641
3378
|
}
|
|
3642
3379
|
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3643
3380
|
isStopping = true;
|
|
3644
|
-
types
|
|
3381
|
+
types.logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
|
|
3645
3382
|
try {
|
|
3646
3383
|
caffeinateProcess.kill("SIGTERM");
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
}, 1e3);
|
|
3384
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3385
|
+
if (caffeinateProcess && !caffeinateProcess.killed) {
|
|
3386
|
+
types.logger.debug("[caffeinate] Force killing caffeinate process");
|
|
3387
|
+
caffeinateProcess.kill("SIGKILL");
|
|
3388
|
+
}
|
|
3389
|
+
caffeinateProcess = null;
|
|
3390
|
+
isStopping = false;
|
|
3655
3391
|
} catch (error) {
|
|
3656
|
-
types
|
|
3392
|
+
types.logger.debug("[caffeinate] Error stopping caffeinate:", error);
|
|
3657
3393
|
isStopping = false;
|
|
3658
3394
|
}
|
|
3659
3395
|
}
|
|
@@ -3673,21 +3409,19 @@ function setupCleanupHandlers() {
|
|
|
3673
3409
|
process.on("SIGUSR1", cleanup);
|
|
3674
3410
|
process.on("SIGUSR2", cleanup);
|
|
3675
3411
|
process.on("uncaughtException", (error) => {
|
|
3676
|
-
types
|
|
3412
|
+
types.logger.debug("[caffeinate] Uncaught exception, cleaning up:", error);
|
|
3677
3413
|
cleanup();
|
|
3678
|
-
process.exit(1);
|
|
3679
3414
|
});
|
|
3680
3415
|
process.on("unhandledRejection", (reason, promise) => {
|
|
3681
|
-
types
|
|
3416
|
+
types.logger.debug("[caffeinate] Unhandled rejection, cleaning up:", reason);
|
|
3682
3417
|
cleanup();
|
|
3683
|
-
process.exit(1);
|
|
3684
3418
|
});
|
|
3685
3419
|
}
|
|
3686
3420
|
|
|
3687
3421
|
async function extractSDKMetadata() {
|
|
3688
3422
|
const abortController = new AbortController();
|
|
3689
3423
|
try {
|
|
3690
|
-
types
|
|
3424
|
+
types.logger.debug("[metadataExtractor] Starting SDK metadata extraction");
|
|
3691
3425
|
const sdkQuery = query({
|
|
3692
3426
|
prompt: "hello",
|
|
3693
3427
|
options: {
|
|
@@ -3703,19 +3437,19 @@ async function extractSDKMetadata() {
|
|
|
3703
3437
|
tools: systemMessage.tools,
|
|
3704
3438
|
slashCommands: systemMessage.slash_commands
|
|
3705
3439
|
};
|
|
3706
|
-
types
|
|
3440
|
+
types.logger.debug("[metadataExtractor] Captured SDK metadata:", metadata);
|
|
3707
3441
|
abortController.abort();
|
|
3708
3442
|
return metadata;
|
|
3709
3443
|
}
|
|
3710
3444
|
}
|
|
3711
|
-
types
|
|
3445
|
+
types.logger.debug("[metadataExtractor] No init message received from SDK");
|
|
3712
3446
|
return {};
|
|
3713
3447
|
} catch (error) {
|
|
3714
3448
|
if (error instanceof Error && error.name === "AbortError") {
|
|
3715
|
-
types
|
|
3449
|
+
types.logger.debug("[metadataExtractor] SDK query aborted after capturing metadata");
|
|
3716
3450
|
return {};
|
|
3717
3451
|
}
|
|
3718
|
-
types
|
|
3452
|
+
types.logger.debug("[metadataExtractor] Error extracting SDK metadata:", error);
|
|
3719
3453
|
return {};
|
|
3720
3454
|
}
|
|
3721
3455
|
}
|
|
@@ -3725,93 +3459,208 @@ function extractSDKMetadataAsync(onComplete) {
|
|
|
3725
3459
|
onComplete(metadata);
|
|
3726
3460
|
}
|
|
3727
3461
|
}).catch((error) => {
|
|
3728
|
-
types
|
|
3462
|
+
types.logger.debug("[metadataExtractor] Async extraction failed:", error);
|
|
3729
3463
|
});
|
|
3730
3464
|
}
|
|
3731
3465
|
|
|
3732
|
-
async function
|
|
3466
|
+
async function daemonPost(path, body) {
|
|
3467
|
+
const state = await types.readDaemonState();
|
|
3468
|
+
if (!state?.httpPort) {
|
|
3469
|
+
const errorMessage = "No daemon running, no state file found";
|
|
3470
|
+
types.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3471
|
+
return {
|
|
3472
|
+
error: errorMessage
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3733
3475
|
try {
|
|
3734
|
-
|
|
3735
|
-
if (!state) {
|
|
3736
|
-
return false;
|
|
3737
|
-
}
|
|
3738
|
-
const isRunning = await isDaemonProcessRunning(state.pid);
|
|
3739
|
-
if (!isRunning) {
|
|
3740
|
-
types$1.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3741
|
-
await cleanupDaemonState();
|
|
3742
|
-
return false;
|
|
3743
|
-
}
|
|
3744
|
-
return true;
|
|
3476
|
+
process.kill(state.pid, 0);
|
|
3745
3477
|
} catch (error) {
|
|
3746
|
-
|
|
3747
|
-
|
|
3478
|
+
const errorMessage = "Daemon is not running, file is stale";
|
|
3479
|
+
types.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3480
|
+
return {
|
|
3481
|
+
error: errorMessage
|
|
3482
|
+
};
|
|
3748
3483
|
}
|
|
3749
|
-
}
|
|
3750
|
-
async function getDaemonState() {
|
|
3751
3484
|
try {
|
|
3752
|
-
|
|
3485
|
+
const timeout = process.env.HAPPY_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.HAPPY_DAEMON_HTTP_TIMEOUT) : 1e4;
|
|
3486
|
+
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
3487
|
+
method: "POST",
|
|
3488
|
+
headers: { "Content-Type": "application/json" },
|
|
3489
|
+
body: JSON.stringify(body || {}),
|
|
3490
|
+
// Mostly increased for stress test
|
|
3491
|
+
signal: AbortSignal.timeout(timeout)
|
|
3492
|
+
});
|
|
3493
|
+
if (!response.ok) {
|
|
3494
|
+
const errorMessage = `Request failed: ${path}, HTTP ${response.status}`;
|
|
3495
|
+
types.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3496
|
+
return {
|
|
3497
|
+
error: errorMessage
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
return await response.json();
|
|
3753
3501
|
} catch (error) {
|
|
3754
|
-
|
|
3755
|
-
|
|
3502
|
+
const errorMessage = `Request failed: ${path}, ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
3503
|
+
types.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
|
|
3504
|
+
return {
|
|
3505
|
+
error: errorMessage
|
|
3506
|
+
};
|
|
3756
3507
|
}
|
|
3757
3508
|
}
|
|
3758
|
-
async function
|
|
3509
|
+
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
3510
|
+
return await daemonPost("/session-started", {
|
|
3511
|
+
sessionId,
|
|
3512
|
+
metadata
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
async function listDaemonSessions() {
|
|
3516
|
+
const result = await daemonPost("/list");
|
|
3517
|
+
return result.children || [];
|
|
3518
|
+
}
|
|
3519
|
+
async function stopDaemonSession(sessionId) {
|
|
3520
|
+
const result = await daemonPost("/stop-session", { sessionId });
|
|
3521
|
+
return result.success || false;
|
|
3522
|
+
}
|
|
3523
|
+
async function stopDaemonHttp() {
|
|
3524
|
+
await daemonPost("/stop");
|
|
3525
|
+
}
|
|
3526
|
+
async function checkIfDaemonRunningAndCleanupStaleState() {
|
|
3527
|
+
const state = await types.readDaemonState();
|
|
3528
|
+
if (!state) {
|
|
3529
|
+
return false;
|
|
3530
|
+
}
|
|
3759
3531
|
try {
|
|
3760
|
-
process.kill(pid, 0);
|
|
3532
|
+
process.kill(state.pid, 0);
|
|
3761
3533
|
return true;
|
|
3762
3534
|
} catch {
|
|
3535
|
+
types.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
|
|
3536
|
+
await cleanupDaemonState();
|
|
3537
|
+
return false;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
async function isDaemonRunningSameVersion() {
|
|
3541
|
+
types.logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
|
|
3542
|
+
const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3543
|
+
if (!runningDaemon) {
|
|
3544
|
+
types.logger.debug("[DAEMON CONTROL] No daemon running, returning false");
|
|
3545
|
+
return false;
|
|
3546
|
+
}
|
|
3547
|
+
const state = await types.readDaemonState();
|
|
3548
|
+
if (!state) {
|
|
3549
|
+
types.logger.debug("[DAEMON CONTROL] No daemon state found, returning false");
|
|
3550
|
+
return false;
|
|
3551
|
+
}
|
|
3552
|
+
try {
|
|
3553
|
+
types.logger.debug(`[DAEMON CONTROL] Current CLI version: ${types.configuration.currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
|
|
3554
|
+
return types.configuration.currentCliVersion === state.startedWithCliVersion;
|
|
3555
|
+
} catch (error) {
|
|
3556
|
+
types.logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
|
|
3763
3557
|
return false;
|
|
3764
3558
|
}
|
|
3765
3559
|
}
|
|
3766
3560
|
async function cleanupDaemonState() {
|
|
3767
3561
|
try {
|
|
3768
|
-
await clearDaemonState();
|
|
3769
|
-
types
|
|
3562
|
+
await types.clearDaemonState();
|
|
3563
|
+
types.logger.debug("[DAEMON RUN] Daemon state file removed");
|
|
3564
|
+
} catch (error) {
|
|
3565
|
+
types.logger.debug("[DAEMON RUN] Error cleaning up daemon metadata", error);
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
async function stopDaemon() {
|
|
3569
|
+
try {
|
|
3570
|
+
const state = await types.readDaemonState();
|
|
3571
|
+
if (!state) {
|
|
3572
|
+
types.logger.debug("No daemon state found");
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
types.logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3576
|
+
try {
|
|
3577
|
+
await stopDaemonHttp();
|
|
3578
|
+
await waitForProcessDeath(state.pid, 2e3);
|
|
3579
|
+
types.logger.debug("Daemon stopped gracefully via HTTP");
|
|
3580
|
+
return;
|
|
3581
|
+
} catch (error) {
|
|
3582
|
+
types.logger.debug("HTTP stop failed, will force kill", error);
|
|
3583
|
+
}
|
|
3584
|
+
try {
|
|
3585
|
+
process.kill(state.pid, "SIGKILL");
|
|
3586
|
+
types.logger.debug("Force killed daemon");
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
types.logger.debug("Daemon already dead");
|
|
3589
|
+
}
|
|
3770
3590
|
} catch (error) {
|
|
3771
|
-
types
|
|
3591
|
+
types.logger.debug("Error stopping daemon", error);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
async function waitForProcessDeath(pid, timeout) {
|
|
3595
|
+
const start = Date.now();
|
|
3596
|
+
while (Date.now() - start < timeout) {
|
|
3597
|
+
try {
|
|
3598
|
+
process.kill(pid, 0);
|
|
3599
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3600
|
+
} catch {
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3772
3603
|
}
|
|
3604
|
+
throw new Error("Process did not die within timeout");
|
|
3773
3605
|
}
|
|
3606
|
+
|
|
3774
3607
|
function findAllHappyProcesses() {
|
|
3775
3608
|
try {
|
|
3776
|
-
const output = node_child_process.execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3777
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3778
3609
|
const allProcesses = [];
|
|
3779
|
-
|
|
3780
|
-
const
|
|
3781
|
-
|
|
3782
|
-
const
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3610
|
+
try {
|
|
3611
|
+
const happyOutput = node_child_process.execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
|
|
3612
|
+
const happyLines = happyOutput.trim().split("\n").filter((line) => line.trim());
|
|
3613
|
+
for (const line of happyLines) {
|
|
3614
|
+
const parts = line.trim().split(/\s+/);
|
|
3615
|
+
if (parts.length < 11) continue;
|
|
3616
|
+
const pid = parseInt(parts[1]);
|
|
3617
|
+
const command = parts.slice(10).join(" ");
|
|
3618
|
+
let type = "unknown";
|
|
3619
|
+
if (pid === process.pid) {
|
|
3620
|
+
type = "current";
|
|
3621
|
+
} else if (command.includes("--version")) {
|
|
3622
|
+
type = "daemon-version-check";
|
|
3623
|
+
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3624
|
+
type = "daemon";
|
|
3625
|
+
} else if (command.includes("--started-by daemon")) {
|
|
3626
|
+
type = "daemon-spawned-session";
|
|
3627
|
+
} else if (command.includes("doctor")) {
|
|
3628
|
+
type = "doctor";
|
|
3629
|
+
} else {
|
|
3630
|
+
type = "user-session";
|
|
3631
|
+
}
|
|
3632
|
+
allProcesses.push({ pid, command, type });
|
|
3795
3633
|
}
|
|
3796
|
-
|
|
3634
|
+
} catch {
|
|
3797
3635
|
}
|
|
3798
3636
|
try {
|
|
3799
|
-
const devOutput = node_child_process.execSync('ps aux | grep -E "
|
|
3637
|
+
const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3800
3638
|
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3801
3639
|
for (const line of devLines) {
|
|
3802
3640
|
const parts = line.trim().split(/\s+/);
|
|
3803
3641
|
if (parts.length < 11) continue;
|
|
3804
3642
|
const pid = parseInt(parts[1]);
|
|
3805
3643
|
const command = parts.slice(10).join(" ");
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
const pwdOutput = node_child_process.execSync(`pwdx ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
3809
|
-
workingDir = pwdOutput.replace(`${pid}:`, "").trim();
|
|
3810
|
-
} catch {
|
|
3644
|
+
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3645
|
+
continue;
|
|
3811
3646
|
}
|
|
3812
|
-
|
|
3813
|
-
|
|
3647
|
+
let type = "unknown";
|
|
3648
|
+
if (pid === process.pid) {
|
|
3649
|
+
type = "current";
|
|
3650
|
+
} else if (command.includes("--version")) {
|
|
3651
|
+
type = "dev-daemon-version-check";
|
|
3652
|
+
} else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
|
|
3653
|
+
type = "dev-daemon";
|
|
3654
|
+
} else if (command.includes("--started-by daemon")) {
|
|
3655
|
+
type = "dev-daemon-spawned";
|
|
3656
|
+
} else if (command.includes("doctor")) {
|
|
3657
|
+
type = "dev-doctor";
|
|
3658
|
+
} else if (command.includes("--yolo")) {
|
|
3659
|
+
type = "dev-session";
|
|
3660
|
+
} else {
|
|
3661
|
+
type = "dev-related";
|
|
3814
3662
|
}
|
|
3663
|
+
allProcesses.push({ pid, command, type });
|
|
3815
3664
|
}
|
|
3816
3665
|
} catch {
|
|
3817
3666
|
}
|
|
@@ -3822,18 +3671,39 @@ function findAllHappyProcesses() {
|
|
|
3822
3671
|
}
|
|
3823
3672
|
function findRunawayHappyProcesses() {
|
|
3824
3673
|
try {
|
|
3825
|
-
const output = node_child_process.execSync('ps aux | grep "happy.mjs" | grep -v grep', { encoding: "utf8" });
|
|
3826
|
-
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3827
3674
|
const processes = [];
|
|
3828
|
-
|
|
3829
|
-
const
|
|
3830
|
-
|
|
3831
|
-
const
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3675
|
+
try {
|
|
3676
|
+
const output = node_child_process.execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
|
|
3677
|
+
const lines = output.trim().split("\n").filter((line) => line.trim());
|
|
3678
|
+
for (const line of lines) {
|
|
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
|
+
if (pid === process.pid) continue;
|
|
3684
|
+
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3685
|
+
processes.push({ pid, command });
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
} catch {
|
|
3689
|
+
}
|
|
3690
|
+
try {
|
|
3691
|
+
const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
|
|
3692
|
+
const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
|
|
3693
|
+
for (const line of devLines) {
|
|
3694
|
+
const parts = line.trim().split(/\s+/);
|
|
3695
|
+
if (parts.length < 11) continue;
|
|
3696
|
+
const pid = parseInt(parts[1]);
|
|
3697
|
+
const command = parts.slice(10).join(" ");
|
|
3698
|
+
if (pid === process.pid) continue;
|
|
3699
|
+
if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
|
|
3700
|
+
continue;
|
|
3701
|
+
}
|
|
3702
|
+
if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
|
|
3703
|
+
processes.push({ pid, command });
|
|
3704
|
+
}
|
|
3836
3705
|
}
|
|
3706
|
+
} catch {
|
|
3837
3707
|
}
|
|
3838
3708
|
return processes;
|
|
3839
3709
|
} catch (error) {
|
|
@@ -3843,10 +3713,10 @@ function findRunawayHappyProcesses() {
|
|
|
3843
3713
|
async function killRunawayHappyProcesses() {
|
|
3844
3714
|
const runawayProcesses = findRunawayHappyProcesses();
|
|
3845
3715
|
const errors = [];
|
|
3846
|
-
|
|
3847
|
-
for (const { pid, command } of runawayProcesses) {
|
|
3716
|
+
const killPromises = runawayProcesses.map(async ({ pid, command }) => {
|
|
3848
3717
|
try {
|
|
3849
3718
|
process.kill(pid, "SIGTERM");
|
|
3719
|
+
console.log(`Sent SIGTERM to runaway process PID ${pid}: ${command}`);
|
|
3850
3720
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3851
3721
|
try {
|
|
3852
3722
|
process.kill(pid, 0);
|
|
@@ -3854,66 +3724,19 @@ async function killRunawayHappyProcesses() {
|
|
|
3854
3724
|
process.kill(pid, "SIGKILL");
|
|
3855
3725
|
} catch {
|
|
3856
3726
|
}
|
|
3857
|
-
killed
|
|
3858
|
-
|
|
3727
|
+
console.log(`Successfully killed runaway process PID ${pid}`);
|
|
3728
|
+
return { success: true, pid, command };
|
|
3859
3729
|
} catch (error) {
|
|
3860
|
-
|
|
3730
|
+
const errorMessage = error.message;
|
|
3731
|
+
errors.push({ pid, error: errorMessage });
|
|
3732
|
+
console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
|
|
3733
|
+
return { success: false, pid, command };
|
|
3861
3734
|
}
|
|
3862
|
-
}
|
|
3735
|
+
});
|
|
3736
|
+
const results = await Promise.all(killPromises);
|
|
3737
|
+
const killed = results.filter((r) => r.success).length;
|
|
3863
3738
|
return { killed, errors };
|
|
3864
3739
|
}
|
|
3865
|
-
async function stopDaemon() {
|
|
3866
|
-
try {
|
|
3867
|
-
stopCaffeinate();
|
|
3868
|
-
types$1.logger.debug("Stopped sleep prevention");
|
|
3869
|
-
const state = await getDaemonState();
|
|
3870
|
-
if (!state) {
|
|
3871
|
-
types$1.logger.debug("No daemon state found");
|
|
3872
|
-
return;
|
|
3873
|
-
}
|
|
3874
|
-
types$1.logger.debug(`Stopping daemon with PID ${state.pid}`);
|
|
3875
|
-
try {
|
|
3876
|
-
const { stopDaemonHttp } = await Promise.resolve().then(function () { return controlClient; });
|
|
3877
|
-
await stopDaemonHttp();
|
|
3878
|
-
await waitForProcessDeath(state.pid, 5e3);
|
|
3879
|
-
types$1.logger.debug("Daemon stopped gracefully via HTTP");
|
|
3880
|
-
return;
|
|
3881
|
-
} catch (error) {
|
|
3882
|
-
types$1.logger.debug("HTTP stop failed, will force kill", error);
|
|
3883
|
-
}
|
|
3884
|
-
try {
|
|
3885
|
-
process.kill(state.pid, "SIGKILL");
|
|
3886
|
-
types$1.logger.debug("Force killed daemon");
|
|
3887
|
-
} catch (error) {
|
|
3888
|
-
types$1.logger.debug("Daemon already dead");
|
|
3889
|
-
}
|
|
3890
|
-
} catch (error) {
|
|
3891
|
-
types$1.logger.debug("Error stopping daemon", error);
|
|
3892
|
-
}
|
|
3893
|
-
}
|
|
3894
|
-
async function waitForProcessDeath(pid, timeout) {
|
|
3895
|
-
const start = Date.now();
|
|
3896
|
-
while (Date.now() - start < timeout) {
|
|
3897
|
-
try {
|
|
3898
|
-
process.kill(pid, 0);
|
|
3899
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3900
|
-
} catch {
|
|
3901
|
-
return;
|
|
3902
|
-
}
|
|
3903
|
-
}
|
|
3904
|
-
throw new Error("Process did not die within timeout");
|
|
3905
|
-
}
|
|
3906
|
-
|
|
3907
|
-
var utils = /*#__PURE__*/Object.freeze({
|
|
3908
|
-
__proto__: null,
|
|
3909
|
-
cleanupDaemonState: cleanupDaemonState,
|
|
3910
|
-
findAllHappyProcesses: findAllHappyProcesses,
|
|
3911
|
-
findRunawayHappyProcesses: findRunawayHappyProcesses,
|
|
3912
|
-
getDaemonState: getDaemonState,
|
|
3913
|
-
isDaemonRunning: isDaemonRunning,
|
|
3914
|
-
killRunawayHappyProcesses: killRunawayHappyProcesses,
|
|
3915
|
-
stopDaemon: stopDaemon
|
|
3916
|
-
});
|
|
3917
3740
|
|
|
3918
3741
|
function getEnvironmentInfo() {
|
|
3919
3742
|
return {
|
|
@@ -3926,9 +3749,17 @@ function getEnvironmentInfo() {
|
|
|
3926
3749
|
DEBUG: process.env.DEBUG,
|
|
3927
3750
|
workingDirectory: process.cwd(),
|
|
3928
3751
|
processArgv: process.argv,
|
|
3929
|
-
happyDir: types
|
|
3930
|
-
serverUrl: types
|
|
3931
|
-
logsDir: types
|
|
3752
|
+
happyDir: types.configuration?.happyHomeDir,
|
|
3753
|
+
serverUrl: types.configuration?.serverUrl,
|
|
3754
|
+
logsDir: types.configuration?.logsDir,
|
|
3755
|
+
processPid: process.pid,
|
|
3756
|
+
nodeVersion: process.version,
|
|
3757
|
+
platform: process.platform,
|
|
3758
|
+
arch: process.arch,
|
|
3759
|
+
user: process.env.USER,
|
|
3760
|
+
home: process.env.HOME,
|
|
3761
|
+
shell: process.env.SHELL,
|
|
3762
|
+
terminal: process.env.TERM
|
|
3932
3763
|
};
|
|
3933
3764
|
}
|
|
3934
3765
|
function getLogFiles(logDir) {
|
|
@@ -3940,62 +3771,67 @@ function getLogFiles(logDir) {
|
|
|
3940
3771
|
const path = node_path.join(logDir, file);
|
|
3941
3772
|
const stats = node_fs.statSync(path);
|
|
3942
3773
|
return { file, path, modified: stats.mtime };
|
|
3943
|
-
}).sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
|
3774
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
3944
3775
|
} catch {
|
|
3945
3776
|
return [];
|
|
3946
3777
|
}
|
|
3947
3778
|
}
|
|
3948
|
-
async function runDoctorCommand() {
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
console.log(`Happy CLI Version: ${chalk.green(packageJson.version)}`);
|
|
3952
|
-
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3953
|
-
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3954
|
-
console.log("");
|
|
3955
|
-
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3956
|
-
const projectRoot = projectPath();
|
|
3957
|
-
const wrapperPath = node_path.join(projectRoot, "bin", "happy.mjs");
|
|
3958
|
-
const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
|
|
3959
|
-
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
3960
|
-
console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
|
|
3961
|
-
console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
|
|
3962
|
-
console.log(`Wrapper Exists: ${node_fs.existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3963
|
-
console.log(`CLI Exists: ${node_fs.existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3964
|
-
console.log("");
|
|
3965
|
-
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3966
|
-
console.log(`Happy Home: ${chalk.blue(types$1.configuration.happyHomeDir)}`);
|
|
3967
|
-
console.log(`Server URL: ${chalk.blue(types$1.configuration.serverUrl)}`);
|
|
3968
|
-
console.log(`Logs Dir: ${chalk.blue(types$1.configuration.logsDir)}`);
|
|
3969
|
-
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3970
|
-
const env = getEnvironmentInfo();
|
|
3971
|
-
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3972
|
-
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3973
|
-
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3974
|
-
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3975
|
-
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3976
|
-
try {
|
|
3977
|
-
const settings = await readSettings();
|
|
3978
|
-
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3979
|
-
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3980
|
-
} catch (error) {
|
|
3981
|
-
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3982
|
-
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3779
|
+
async function runDoctorCommand(filter) {
|
|
3780
|
+
if (!filter) {
|
|
3781
|
+
filter = "all";
|
|
3983
3782
|
}
|
|
3984
|
-
console.log(chalk.bold("\n\u{
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3783
|
+
console.log(chalk.bold.cyan("\n\u{1FA7A} Happy CLI Doctor\n"));
|
|
3784
|
+
if (filter === "all") {
|
|
3785
|
+
console.log(chalk.bold("\u{1F4CB} Basic Information"));
|
|
3786
|
+
console.log(`Happy CLI Version: ${chalk.green(types.packageJson.version)}`);
|
|
3787
|
+
console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
|
|
3788
|
+
console.log(`Node.js Version: ${chalk.green(process.version)}`);
|
|
3789
|
+
console.log("");
|
|
3790
|
+
console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
|
|
3791
|
+
const projectRoot = projectPath();
|
|
3792
|
+
const wrapperPath = node_path.join(projectRoot, "bin", "happy.mjs");
|
|
3793
|
+
const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
|
|
3794
|
+
console.log(`Project Root: ${chalk.blue(projectRoot)}`);
|
|
3795
|
+
console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
|
|
3796
|
+
console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
|
|
3797
|
+
console.log(`Wrapper Exists: ${node_fs.existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3798
|
+
console.log(`CLI Exists: ${node_fs.existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
|
|
3799
|
+
console.log("");
|
|
3800
|
+
console.log(chalk.bold("\u2699\uFE0F Configuration"));
|
|
3801
|
+
console.log(`Happy Home: ${chalk.blue(types.configuration.happyHomeDir)}`);
|
|
3802
|
+
console.log(`Server URL: ${chalk.blue(types.configuration.serverUrl)}`);
|
|
3803
|
+
console.log(`Logs Dir: ${chalk.blue(types.configuration.logsDir)}`);
|
|
3804
|
+
console.log(chalk.bold("\n\u{1F30D} Environment Variables"));
|
|
3805
|
+
const env = getEnvironmentInfo();
|
|
3806
|
+
console.log(`HAPPY_HOME_DIR: ${env.HAPPY_HOME_DIR ? chalk.green(env.HAPPY_HOME_DIR) : chalk.gray("not set")}`);
|
|
3807
|
+
console.log(`HAPPY_SERVER_URL: ${env.HAPPY_SERVER_URL ? chalk.green(env.HAPPY_SERVER_URL) : chalk.gray("not set")}`);
|
|
3808
|
+
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow("ENABLED") : chalk.gray("not set")}`);
|
|
3809
|
+
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray("not set")}`);
|
|
3810
|
+
console.log(`NODE_ENV: ${env.NODE_ENV ? chalk.green(env.NODE_ENV) : chalk.gray("not set")}`);
|
|
3811
|
+
try {
|
|
3812
|
+
const settings = await types.readSettings();
|
|
3813
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings (settings.json):"));
|
|
3814
|
+
console.log(chalk.gray(JSON.stringify(settings, null, 2)));
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
console.log(chalk.bold("\n\u{1F4C4} Settings:"));
|
|
3817
|
+
console.log(chalk.red("\u274C Failed to read settings"));
|
|
3818
|
+
}
|
|
3819
|
+
console.log(chalk.bold("\n\u{1F510} Authentication"));
|
|
3820
|
+
try {
|
|
3821
|
+
const credentials = await types.readCredentials();
|
|
3822
|
+
if (credentials) {
|
|
3823
|
+
console.log(chalk.green("\u2713 Authenticated (credentials found)"));
|
|
3824
|
+
} else {
|
|
3825
|
+
console.log(chalk.yellow("\u26A0\uFE0F Not authenticated (no credentials)"));
|
|
3826
|
+
}
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
console.log(chalk.red("\u274C Error reading credentials"));
|
|
3991
3829
|
}
|
|
3992
|
-
} catch (error) {
|
|
3993
|
-
console.log(chalk.red("\u274C Error reading credentials"));
|
|
3994
3830
|
}
|
|
3995
3831
|
console.log(chalk.bold("\n\u{1F916} Daemon Status"));
|
|
3996
3832
|
try {
|
|
3997
|
-
const isRunning = await
|
|
3998
|
-
const state = await
|
|
3833
|
+
const isRunning = await checkIfDaemonRunningAndCleanupStaleState();
|
|
3834
|
+
const state = await types.readDaemonState();
|
|
3999
3835
|
if (isRunning && state) {
|
|
4000
3836
|
console.log(chalk.green("\u2713 Daemon is running"));
|
|
4001
3837
|
console.log(` PID: ${state.pid}`);
|
|
@@ -4011,7 +3847,7 @@ async function runDoctorCommand() {
|
|
|
4011
3847
|
}
|
|
4012
3848
|
if (state) {
|
|
4013
3849
|
console.log(chalk.bold("\n\u{1F4C4} Daemon State:"));
|
|
4014
|
-
console.log(chalk.blue(`Location: ${types
|
|
3850
|
+
console.log(chalk.blue(`Location: ${types.configuration.daemonStateFile}`));
|
|
4015
3851
|
console.log(chalk.gray(JSON.stringify(state, null, 2)));
|
|
4016
3852
|
}
|
|
4017
3853
|
const allProcesses = findAllHappyProcesses();
|
|
@@ -4026,9 +3862,11 @@ async function runDoctorCommand() {
|
|
|
4026
3862
|
const typeLabels = {
|
|
4027
3863
|
"current": "\u{1F4CD} Current Process",
|
|
4028
3864
|
"daemon": "\u{1F916} Daemon",
|
|
3865
|
+
"daemon-version-check": "\u{1F50D} Daemon Version Check (stuck)",
|
|
4029
3866
|
"daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
|
|
4030
3867
|
"user-session": "\u{1F464} User Sessions",
|
|
4031
3868
|
"dev-daemon": "\u{1F6E0}\uFE0F Dev Daemon",
|
|
3869
|
+
"dev-daemon-version-check": "\u{1F6E0}\uFE0F Dev Daemon Version Check (stuck)",
|
|
4032
3870
|
"dev-session": "\u{1F6E0}\uFE0F Dev Sessions",
|
|
4033
3871
|
"dev-doctor": "\u{1F6E0}\uFE0F Dev Doctor",
|
|
4034
3872
|
"dev-related": "\u{1F6E0}\uFE0F Dev Related",
|
|
@@ -4042,190 +3880,54 @@ ${typeLabels[type] || type}:`));
|
|
|
4042
3880
|
console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
4043
3881
|
});
|
|
4044
3882
|
});
|
|
3883
|
+
} else {
|
|
3884
|
+
console.log(chalk.red("\u274C No happy processes found"));
|
|
4045
3885
|
}
|
|
4046
|
-
|
|
4047
|
-
if (runawayProcesses.length > 0) {
|
|
4048
|
-
console.log(chalk.bold("\n\u{1F6A8} Runaway Happy processes detected"));
|
|
4049
|
-
console.log(chalk.gray("These processes were left running after daemon crashes."));
|
|
4050
|
-
runawayProcesses.forEach(({ pid, command }) => {
|
|
4051
|
-
console.log(` ${chalk.yellow(`PID ${pid}`)}: ${chalk.gray(command)}`);
|
|
4052
|
-
});
|
|
4053
|
-
console.log(chalk.blue("\nTo clean up: happy daemon kill-runaway"));
|
|
4054
|
-
}
|
|
4055
|
-
if (allProcesses.length > 1) {
|
|
3886
|
+
if (filter === "all" && allProcesses.length > 1) {
|
|
4056
3887
|
console.log(chalk.bold("\n\u{1F4A1} Process Management"));
|
|
4057
|
-
console.log(chalk.gray("To
|
|
3888
|
+
console.log(chalk.gray("To clean up runaway processes: happy doctor clean"));
|
|
4058
3889
|
}
|
|
4059
3890
|
} catch (error) {
|
|
4060
3891
|
console.log(chalk.red("\u274C Error checking daemon status"));
|
|
4061
3892
|
}
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
}
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
console.log(chalk.gray(` ${path}`));
|
|
4079
|
-
});
|
|
4080
|
-
} else {
|
|
4081
|
-
console.log(chalk.yellow("No daemon log files found"));
|
|
4082
|
-
}
|
|
4083
|
-
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
4084
|
-
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
4085
|
-
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
4086
|
-
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4087
|
-
}
|
|
4088
|
-
|
|
4089
|
-
async function daemonPost(path, body) {
|
|
4090
|
-
const state = await getDaemonState();
|
|
4091
|
-
if (!state?.httpPort) {
|
|
4092
|
-
throw new Error("No daemon running");
|
|
4093
|
-
}
|
|
4094
|
-
try {
|
|
4095
|
-
const response = await fetch(`http://127.0.0.1:${state.httpPort}${path}`, {
|
|
4096
|
-
method: "POST",
|
|
4097
|
-
headers: { "Content-Type": "application/json" },
|
|
4098
|
-
body: JSON.stringify(body || {}),
|
|
4099
|
-
signal: AbortSignal.timeout(5e3)
|
|
4100
|
-
});
|
|
4101
|
-
if (!response.ok) {
|
|
4102
|
-
throw new Error(`HTTP ${response.status}`);
|
|
4103
|
-
}
|
|
4104
|
-
return await response.json();
|
|
4105
|
-
} catch (error) {
|
|
4106
|
-
types$1.logger.debug(`[CONTROL CLIENT] Request failed: ${path}`, error);
|
|
4107
|
-
throw error;
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
async function notifyDaemonSessionStarted(sessionId, metadata) {
|
|
4111
|
-
await daemonPost("/session-started", {
|
|
4112
|
-
sessionId,
|
|
4113
|
-
metadata
|
|
4114
|
-
});
|
|
4115
|
-
}
|
|
4116
|
-
async function listDaemonSessions() {
|
|
4117
|
-
const result = await daemonPost("/list");
|
|
4118
|
-
return result.children || [];
|
|
4119
|
-
}
|
|
4120
|
-
async function stopDaemonSession(sessionId) {
|
|
4121
|
-
const result = await daemonPost("/stop-session", { sessionId });
|
|
4122
|
-
return result.success || false;
|
|
4123
|
-
}
|
|
4124
|
-
async function stopDaemonHttp() {
|
|
4125
|
-
await daemonPost("/stop");
|
|
4126
|
-
}
|
|
4127
|
-
|
|
4128
|
-
var controlClient = /*#__PURE__*/Object.freeze({
|
|
4129
|
-
__proto__: null,
|
|
4130
|
-
listDaemonSessions: listDaemonSessions,
|
|
4131
|
-
notifyDaemonSessionStarted: notifyDaemonSessionStarted,
|
|
4132
|
-
stopDaemonHttp: stopDaemonHttp,
|
|
4133
|
-
stopDaemonSession: stopDaemonSession
|
|
4134
|
-
});
|
|
4135
|
-
|
|
4136
|
-
function startDaemonControlServer({
|
|
4137
|
-
getChildren,
|
|
4138
|
-
stopSession,
|
|
4139
|
-
spawnSession,
|
|
4140
|
-
requestShutdown,
|
|
4141
|
-
onHappySessionWebhook
|
|
4142
|
-
}) {
|
|
4143
|
-
return new Promise((resolve) => {
|
|
4144
|
-
const app = fastify({
|
|
4145
|
-
logger: false
|
|
4146
|
-
// We use our own logger
|
|
4147
|
-
});
|
|
4148
|
-
app.setValidatorCompiler(fastifyTypeProviderZod.validatorCompiler);
|
|
4149
|
-
app.setSerializerCompiler(fastifyTypeProviderZod.serializerCompiler);
|
|
4150
|
-
const typed = app.withTypeProvider();
|
|
4151
|
-
typed.post("/session-started", {
|
|
4152
|
-
schema: {
|
|
4153
|
-
body: z.z.object({
|
|
4154
|
-
sessionId: z.z.string(),
|
|
4155
|
-
metadata: z.z.any()
|
|
4156
|
-
// Metadata type from API
|
|
4157
|
-
})
|
|
4158
|
-
}
|
|
4159
|
-
}, async (request, reply) => {
|
|
4160
|
-
const { sessionId, metadata } = request.body;
|
|
4161
|
-
types$1.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4162
|
-
onHappySessionWebhook(sessionId, metadata);
|
|
4163
|
-
return { status: "ok" };
|
|
4164
|
-
});
|
|
4165
|
-
typed.post("/list", async (request, reply) => {
|
|
4166
|
-
const children = getChildren();
|
|
4167
|
-
types$1.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4168
|
-
return { children };
|
|
4169
|
-
});
|
|
4170
|
-
typed.post("/stop-session", {
|
|
4171
|
-
schema: {
|
|
4172
|
-
body: z.z.object({
|
|
4173
|
-
sessionId: z.z.string()
|
|
4174
|
-
})
|
|
4175
|
-
}
|
|
4176
|
-
}, async (request, reply) => {
|
|
4177
|
-
const { sessionId } = request.body;
|
|
4178
|
-
types$1.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4179
|
-
const success = stopSession(sessionId);
|
|
4180
|
-
return { success };
|
|
4181
|
-
});
|
|
4182
|
-
typed.post("/spawn-session", {
|
|
4183
|
-
schema: {
|
|
4184
|
-
body: z.z.object({
|
|
4185
|
-
directory: z.z.string(),
|
|
4186
|
-
sessionId: z.z.string().optional()
|
|
4187
|
-
})
|
|
3893
|
+
if (filter === "all") {
|
|
3894
|
+
console.log(chalk.bold("\n\u{1F4DD} Log Files"));
|
|
3895
|
+
const allLogs = getLogFiles(types.configuration.logsDir);
|
|
3896
|
+
if (allLogs.length > 0) {
|
|
3897
|
+
const daemonLogs = allLogs.filter(({ file }) => file.includes("daemon"));
|
|
3898
|
+
const regularLogs = allLogs.filter(({ file }) => !file.includes("daemon"));
|
|
3899
|
+
if (regularLogs.length > 0) {
|
|
3900
|
+
console.log(chalk.blue("\nRecent Logs:"));
|
|
3901
|
+
const logsToShow = regularLogs.slice(0, 10);
|
|
3902
|
+
logsToShow.forEach(({ file, path, modified }) => {
|
|
3903
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3904
|
+
console.log(chalk.gray(` ${path}`));
|
|
3905
|
+
});
|
|
3906
|
+
if (regularLogs.length > 10) {
|
|
3907
|
+
console.log(chalk.gray(` ... and ${regularLogs.length - 10} more log files`));
|
|
3908
|
+
}
|
|
4188
3909
|
}
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
}
|
|
3910
|
+
if (daemonLogs.length > 0) {
|
|
3911
|
+
console.log(chalk.blue("\nDaemon Logs:"));
|
|
3912
|
+
const daemonLogsToShow = daemonLogs.slice(0, 5);
|
|
3913
|
+
daemonLogsToShow.forEach(({ file, path, modified }) => {
|
|
3914
|
+
console.log(` ${chalk.green(file)} - ${modified.toLocaleString()}`);
|
|
3915
|
+
console.log(chalk.gray(` ${path}`));
|
|
3916
|
+
});
|
|
3917
|
+
if (daemonLogs.length > 5) {
|
|
3918
|
+
console.log(chalk.gray(` ... and ${daemonLogs.length - 5} more daemon log files`));
|
|
3919
|
+
}
|
|
4199
3920
|
} else {
|
|
4200
|
-
|
|
4201
|
-
return { error: "Failed to spawn session" };
|
|
4202
|
-
}
|
|
4203
|
-
});
|
|
4204
|
-
typed.post("/stop", async (request, reply) => {
|
|
4205
|
-
types$1.logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4206
|
-
setTimeout(() => {
|
|
4207
|
-
types$1.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
4208
|
-
requestShutdown();
|
|
4209
|
-
}, 50);
|
|
4210
|
-
return { status: "stopping" };
|
|
4211
|
-
});
|
|
4212
|
-
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4213
|
-
if (err) {
|
|
4214
|
-
types$1.logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4215
|
-
throw err;
|
|
3921
|
+
console.log(chalk.yellow("\nNo daemon log files found"));
|
|
4216
3922
|
}
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
}
|
|
4226
|
-
});
|
|
4227
|
-
});
|
|
4228
|
-
});
|
|
3923
|
+
} else {
|
|
3924
|
+
console.log(chalk.yellow("No log files found"));
|
|
3925
|
+
}
|
|
3926
|
+
console.log(chalk.bold("\n\u{1F41B} Support & Bug Reports"));
|
|
3927
|
+
console.log(`Report issues: ${chalk.blue("https://github.com/slopus/happy-cli/issues")}`);
|
|
3928
|
+
console.log(`Documentation: ${chalk.blue("https://happy.engineering/")}`);
|
|
3929
|
+
}
|
|
3930
|
+
console.log(chalk.green("\n\u2705 Doctor diagnosis complete!\n"));
|
|
4229
3931
|
}
|
|
4230
3932
|
|
|
4231
3933
|
function displayQRCode(url) {
|
|
@@ -4241,22 +3943,22 @@ function displayQRCode(url) {
|
|
|
4241
3943
|
}
|
|
4242
3944
|
|
|
4243
3945
|
function generateWebAuthUrl(publicKey) {
|
|
4244
|
-
const publicKeyBase64 = types
|
|
3946
|
+
const publicKeyBase64 = types.encodeBase64(publicKey, "base64url");
|
|
4245
3947
|
return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
|
|
4246
3948
|
}
|
|
4247
3949
|
|
|
4248
3950
|
async function openBrowser(url) {
|
|
4249
3951
|
try {
|
|
4250
3952
|
if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
|
|
4251
|
-
types
|
|
3953
|
+
types.logger.debug("[browser] Headless environment detected, skipping browser open");
|
|
4252
3954
|
return false;
|
|
4253
3955
|
}
|
|
4254
|
-
types
|
|
3956
|
+
types.logger.debug(`[browser] Attempting to open URL: ${url}`);
|
|
4255
3957
|
await open(url);
|
|
4256
|
-
types
|
|
3958
|
+
types.logger.debug("[browser] Browser opened successfully");
|
|
4257
3959
|
return true;
|
|
4258
3960
|
} catch (error) {
|
|
4259
|
-
types
|
|
3961
|
+
types.logger.debug("[browser] Failed to open browser:", error);
|
|
4260
3962
|
return false;
|
|
4261
3963
|
}
|
|
4262
3964
|
}
|
|
@@ -4306,10 +4008,10 @@ async function doAuth() {
|
|
|
4306
4008
|
const secret = new Uint8Array(node_crypto.randomBytes(32));
|
|
4307
4009
|
const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
|
|
4308
4010
|
try {
|
|
4309
|
-
console.log(`[AUTH DEBUG] Sending auth request to: ${types
|
|
4310
|
-
console.log(`[AUTH DEBUG] Public key: ${types
|
|
4311
|
-
await axios.post(`${types
|
|
4312
|
-
publicKey: types
|
|
4011
|
+
console.log(`[AUTH DEBUG] Sending auth request to: ${types.configuration.serverUrl}/v1/auth/request`);
|
|
4012
|
+
console.log(`[AUTH DEBUG] Public key: ${types.encodeBase64(keypair.publicKey).substring(0, 20)}...`);
|
|
4013
|
+
await axios.post(`${types.configuration.serverUrl}/v1/auth/request`, {
|
|
4014
|
+
publicKey: types.encodeBase64(keypair.publicKey)
|
|
4313
4015
|
});
|
|
4314
4016
|
console.log(`[AUTH DEBUG] Auth request sent successfully`);
|
|
4315
4017
|
} catch (error) {
|
|
@@ -4350,7 +4052,7 @@ async function doMobileAuth(keypair) {
|
|
|
4350
4052
|
console.clear();
|
|
4351
4053
|
console.log("\nMobile Authentication\n");
|
|
4352
4054
|
console.log("Scan this QR code with your Happy mobile app:\n");
|
|
4353
|
-
const authUrl = "happy://terminal?" + types
|
|
4055
|
+
const authUrl = "happy://terminal?" + types.encodeBase64Url(keypair.publicKey);
|
|
4354
4056
|
displayQRCode(authUrl);
|
|
4355
4057
|
console.log("\nOr manually enter this URL:");
|
|
4356
4058
|
console.log(authUrl);
|
|
@@ -4387,19 +4089,19 @@ async function waitForAuthentication(keypair) {
|
|
|
4387
4089
|
try {
|
|
4388
4090
|
while (!cancelled) {
|
|
4389
4091
|
try {
|
|
4390
|
-
const response = await axios.post(`${types
|
|
4391
|
-
publicKey: types
|
|
4092
|
+
const response = await axios.post(`${types.configuration.serverUrl}/v1/auth/request`, {
|
|
4093
|
+
publicKey: types.encodeBase64(keypair.publicKey)
|
|
4392
4094
|
});
|
|
4393
4095
|
if (response.data.state === "authorized") {
|
|
4394
4096
|
let token = response.data.token;
|
|
4395
|
-
let r = types
|
|
4097
|
+
let r = types.decodeBase64(response.data.response);
|
|
4396
4098
|
let decrypted = decryptWithEphemeralKey(r, keypair.secretKey);
|
|
4397
4099
|
if (decrypted) {
|
|
4398
4100
|
const credentials = {
|
|
4399
4101
|
secret: decrypted,
|
|
4400
4102
|
token
|
|
4401
4103
|
};
|
|
4402
|
-
await writeCredentials(credentials);
|
|
4104
|
+
await types.writeCredentials(credentials);
|
|
4403
4105
|
console.log("\n\n\u2713 Authentication successful\n");
|
|
4404
4106
|
return credentials;
|
|
4405
4107
|
} else {
|
|
@@ -4413,7 +4115,7 @@ async function waitForAuthentication(keypair) {
|
|
|
4413
4115
|
}
|
|
4414
4116
|
process.stdout.write("\rWaiting for authentication" + ".".repeat(dots % 3 + 1) + " ");
|
|
4415
4117
|
dots++;
|
|
4416
|
-
await types
|
|
4118
|
+
await types.delay(1e3);
|
|
4417
4119
|
}
|
|
4418
4120
|
} finally {
|
|
4419
4121
|
process.off("SIGINT", handleInterrupt);
|
|
@@ -4431,22 +4133,22 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
4431
4133
|
return decrypted;
|
|
4432
4134
|
}
|
|
4433
4135
|
async function authAndSetupMachineIfNeeded() {
|
|
4434
|
-
types
|
|
4435
|
-
let credentials = await readCredentials();
|
|
4136
|
+
types.logger.debug("[AUTH] Starting auth and machine setup...");
|
|
4137
|
+
let credentials = await types.readCredentials();
|
|
4436
4138
|
if (!credentials) {
|
|
4437
|
-
types
|
|
4139
|
+
types.logger.debug("[AUTH] No credentials found, starting authentication flow...");
|
|
4438
4140
|
const authResult = await doAuth();
|
|
4439
4141
|
if (!authResult) {
|
|
4440
4142
|
throw new Error("Authentication failed or was cancelled");
|
|
4441
4143
|
}
|
|
4442
4144
|
credentials = authResult;
|
|
4443
4145
|
} else {
|
|
4444
|
-
types
|
|
4146
|
+
types.logger.debug("[AUTH] Using existing credentials");
|
|
4445
4147
|
}
|
|
4446
|
-
const settings = await updateSettings(async (s) => {
|
|
4148
|
+
const settings = await types.updateSettings(async (s) => {
|
|
4447
4149
|
if (!s.machineId) {
|
|
4448
4150
|
const newMachineId = node_crypto.randomUUID();
|
|
4449
|
-
types
|
|
4151
|
+
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`);
|
|
4450
4152
|
return {
|
|
4451
4153
|
...s,
|
|
4452
4154
|
machineId: node_crypto.randomUUID()
|
|
@@ -4454,7 +4156,7 @@ async function authAndSetupMachineIfNeeded() {
|
|
|
4454
4156
|
}
|
|
4455
4157
|
return s;
|
|
4456
4158
|
});
|
|
4457
|
-
types
|
|
4159
|
+
types.logger.debug(`[AUTH] Machine ID: ${settings.machineId}`);
|
|
4458
4160
|
return { credentials, machineId: settings.machineId };
|
|
4459
4161
|
}
|
|
4460
4162
|
|
|
@@ -4468,65 +4170,231 @@ function spawnHappyCLI(args, options = {}) {
|
|
|
4468
4170
|
directory = process.cwd();
|
|
4469
4171
|
}
|
|
4470
4172
|
const fullCommand = `happy ${args.join(" ")}`;
|
|
4471
|
-
types
|
|
4173
|
+
types.logger.debug(`[SPAWN HAPPY CLI] Spawning: ${fullCommand} in ${directory}`);
|
|
4472
4174
|
const nodeArgs = [
|
|
4473
4175
|
"--no-warnings",
|
|
4474
4176
|
"--no-deprecation",
|
|
4475
4177
|
entrypoint,
|
|
4476
4178
|
...args
|
|
4477
4179
|
];
|
|
4180
|
+
if (!node_fs.existsSync(entrypoint)) {
|
|
4181
|
+
const errorMessage = `Entrypoint ${entrypoint} does not exist`;
|
|
4182
|
+
types.logger.debug(`[SPAWN HAPPY CLI] ${errorMessage}`);
|
|
4183
|
+
throw new Error(errorMessage);
|
|
4184
|
+
}
|
|
4478
4185
|
return child_process.spawn("node", nodeArgs, options);
|
|
4479
4186
|
}
|
|
4480
4187
|
|
|
4188
|
+
function startDaemonControlServer({
|
|
4189
|
+
getChildren,
|
|
4190
|
+
stopSession,
|
|
4191
|
+
spawnSession,
|
|
4192
|
+
requestShutdown,
|
|
4193
|
+
onHappySessionWebhook
|
|
4194
|
+
}) {
|
|
4195
|
+
return new Promise((resolve) => {
|
|
4196
|
+
const app = fastify({
|
|
4197
|
+
logger: false
|
|
4198
|
+
// We use our own logger
|
|
4199
|
+
});
|
|
4200
|
+
app.setValidatorCompiler(fastifyTypeProviderZod.validatorCompiler);
|
|
4201
|
+
app.setSerializerCompiler(fastifyTypeProviderZod.serializerCompiler);
|
|
4202
|
+
const typed = app.withTypeProvider();
|
|
4203
|
+
typed.post("/session-started", {
|
|
4204
|
+
schema: {
|
|
4205
|
+
body: z.z.object({
|
|
4206
|
+
sessionId: z.z.string(),
|
|
4207
|
+
metadata: z.z.any()
|
|
4208
|
+
// Metadata type from API
|
|
4209
|
+
})
|
|
4210
|
+
}
|
|
4211
|
+
}, async (request, reply) => {
|
|
4212
|
+
const { sessionId, metadata } = request.body;
|
|
4213
|
+
types.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
4214
|
+
onHappySessionWebhook(sessionId, metadata);
|
|
4215
|
+
return { status: "ok" };
|
|
4216
|
+
});
|
|
4217
|
+
typed.post("/list", async (request, reply) => {
|
|
4218
|
+
const children = getChildren();
|
|
4219
|
+
types.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
4220
|
+
return {
|
|
4221
|
+
children: children.map((child) => {
|
|
4222
|
+
delete child.childProcess;
|
|
4223
|
+
return child;
|
|
4224
|
+
})
|
|
4225
|
+
};
|
|
4226
|
+
});
|
|
4227
|
+
typed.post("/stop-session", {
|
|
4228
|
+
schema: {
|
|
4229
|
+
body: z.z.object({
|
|
4230
|
+
sessionId: z.z.string()
|
|
4231
|
+
})
|
|
4232
|
+
}
|
|
4233
|
+
}, async (request, reply) => {
|
|
4234
|
+
const { sessionId } = request.body;
|
|
4235
|
+
types.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
4236
|
+
const success = stopSession(sessionId);
|
|
4237
|
+
return { success };
|
|
4238
|
+
});
|
|
4239
|
+
typed.post("/spawn-session", {
|
|
4240
|
+
schema: {
|
|
4241
|
+
body: z.z.object({
|
|
4242
|
+
directory: z.z.string(),
|
|
4243
|
+
sessionId: z.z.string().optional()
|
|
4244
|
+
})
|
|
4245
|
+
}
|
|
4246
|
+
}, async (request, reply) => {
|
|
4247
|
+
const { directory, sessionId } = request.body;
|
|
4248
|
+
types.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
|
|
4249
|
+
const session = await spawnSession(directory, sessionId);
|
|
4250
|
+
if (session) {
|
|
4251
|
+
return {
|
|
4252
|
+
success: true,
|
|
4253
|
+
pid: session.pid,
|
|
4254
|
+
sessionId: session.happySessionId || "pending",
|
|
4255
|
+
message: session.message
|
|
4256
|
+
};
|
|
4257
|
+
} else {
|
|
4258
|
+
reply.code(500);
|
|
4259
|
+
return {
|
|
4260
|
+
success: false,
|
|
4261
|
+
error: "Failed to spawn session. Check the directory path and permissions."
|
|
4262
|
+
};
|
|
4263
|
+
}
|
|
4264
|
+
});
|
|
4265
|
+
typed.post("/stop", async (request, reply) => {
|
|
4266
|
+
types.logger.debug("[CONTROL SERVER] Stop daemon request received");
|
|
4267
|
+
setTimeout(() => {
|
|
4268
|
+
types.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
|
|
4269
|
+
requestShutdown();
|
|
4270
|
+
}, 50);
|
|
4271
|
+
return { status: "stopping" };
|
|
4272
|
+
});
|
|
4273
|
+
typed.post("/dev-simulate-error", {
|
|
4274
|
+
schema: {
|
|
4275
|
+
body: z.z.object({
|
|
4276
|
+
error: z.z.string()
|
|
4277
|
+
})
|
|
4278
|
+
}
|
|
4279
|
+
}, async (request, reply) => {
|
|
4280
|
+
const { error } = request.body;
|
|
4281
|
+
types.logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
|
|
4282
|
+
setTimeout(() => {
|
|
4283
|
+
types.logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
|
|
4284
|
+
throw new Error(error);
|
|
4285
|
+
}, 100);
|
|
4286
|
+
return { status: "error will be thrown" };
|
|
4287
|
+
});
|
|
4288
|
+
app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
|
|
4289
|
+
if (err) {
|
|
4290
|
+
types.logger.debug("[CONTROL SERVER] Failed to start:", err);
|
|
4291
|
+
throw err;
|
|
4292
|
+
}
|
|
4293
|
+
const port = parseInt(address.split(":").pop());
|
|
4294
|
+
types.logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
4295
|
+
resolve({
|
|
4296
|
+
port,
|
|
4297
|
+
stop: async () => {
|
|
4298
|
+
types.logger.debug("[CONTROL SERVER] Stopping server");
|
|
4299
|
+
await app.close();
|
|
4300
|
+
types.logger.debug("[CONTROL SERVER] Server stopped");
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
});
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4481
4307
|
const initialMachineMetadata = {
|
|
4482
4308
|
host: os$1.hostname(),
|
|
4483
4309
|
platform: os$1.platform(),
|
|
4484
|
-
happyCliVersion: packageJson.version,
|
|
4310
|
+
happyCliVersion: types.packageJson.version,
|
|
4485
4311
|
homeDir: os$1.homedir(),
|
|
4486
|
-
happyHomeDir: types
|
|
4312
|
+
happyHomeDir: types.configuration.happyHomeDir
|
|
4487
4313
|
};
|
|
4488
4314
|
async function startDaemon() {
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4315
|
+
let requestShutdown;
|
|
4316
|
+
let resolvesWhenShutdownRequested = new Promise((resolve2) => {
|
|
4317
|
+
requestShutdown = (source, errorMessage) => {
|
|
4318
|
+
types.logger.debug(`[DAEMON RUN] Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`);
|
|
4319
|
+
setTimeout(async () => {
|
|
4320
|
+
types.logger.debug("[DAEMON RUN] Startup malfunctioned, forcing exit with code 1");
|
|
4321
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4322
|
+
process.exit(1);
|
|
4323
|
+
}, 1e3);
|
|
4324
|
+
resolve2({ source, errorMessage });
|
|
4325
|
+
};
|
|
4326
|
+
});
|
|
4327
|
+
process.on("SIGINT", () => {
|
|
4328
|
+
types.logger.debug("[DAEMON RUN] Received SIGINT");
|
|
4329
|
+
requestShutdown("os-signal");
|
|
4330
|
+
});
|
|
4331
|
+
process.on("SIGTERM", () => {
|
|
4332
|
+
types.logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4333
|
+
requestShutdown("os-signal");
|
|
4334
|
+
});
|
|
4335
|
+
process.on("uncaughtException", (error) => {
|
|
4336
|
+
types.logger.debug("[DAEMON RUN] FATAL: Uncaught exception", error);
|
|
4337
|
+
types.logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
|
|
4338
|
+
requestShutdown("exception", error.message);
|
|
4339
|
+
});
|
|
4340
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
4341
|
+
types.logger.debug("[DAEMON RUN] FATAL: Unhandled promise rejection", reason);
|
|
4342
|
+
types.logger.debug(`[DAEMON RUN] Rejected promise:`, promise);
|
|
4343
|
+
const error = reason instanceof Error ? reason : new Error(`Unhandled promise rejection: ${reason}`);
|
|
4344
|
+
types.logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
|
|
4345
|
+
requestShutdown("exception", error.message);
|
|
4346
|
+
});
|
|
4347
|
+
process.on("exit", (code) => {
|
|
4348
|
+
types.logger.debug(`[DAEMON RUN] Process exiting with code: ${code}`);
|
|
4349
|
+
});
|
|
4350
|
+
process.on("beforeExit", (code) => {
|
|
4351
|
+
types.logger.debug(`[DAEMON RUN] Process about to exit with code: ${code}`);
|
|
4352
|
+
});
|
|
4353
|
+
types.logger.debug("[DAEMON RUN] Starting daemon process...");
|
|
4354
|
+
types.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
|
|
4355
|
+
const runningDaemonVersionMatches = await isDaemonRunningSameVersion();
|
|
4356
|
+
if (!runningDaemonVersionMatches) {
|
|
4357
|
+
types.logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
|
|
4358
|
+
await stopDaemon();
|
|
4359
|
+
} else {
|
|
4360
|
+
types.logger.debug("[DAEMON RUN] Daemon version matches, keeping existing daemon");
|
|
4361
|
+
console.log("Daemon already running with matching version");
|
|
4362
|
+
process.exit(0);
|
|
4502
4363
|
}
|
|
4503
|
-
const
|
|
4504
|
-
if (
|
|
4505
|
-
types
|
|
4364
|
+
const daemonLockHandle = await types.acquireDaemonLock(5, 200);
|
|
4365
|
+
if (!daemonLockHandle) {
|
|
4366
|
+
types.logger.debug("[DAEMON RUN] Daemon lock file already held, another daemon is running");
|
|
4367
|
+
process.exit(0);
|
|
4506
4368
|
}
|
|
4507
4369
|
try {
|
|
4370
|
+
const caffeinateStarted = startCaffeinate();
|
|
4371
|
+
if (caffeinateStarted) {
|
|
4372
|
+
types.logger.debug("[DAEMON RUN] Sleep prevention enabled");
|
|
4373
|
+
}
|
|
4508
4374
|
const { credentials, machineId } = await authAndSetupMachineIfNeeded();
|
|
4509
|
-
types
|
|
4375
|
+
types.logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
4510
4376
|
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
4511
4377
|
const pidToAwaiter = /* @__PURE__ */ new Map();
|
|
4512
4378
|
const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
|
|
4513
4379
|
const onHappySessionWebhook = (sessionId, sessionMetadata) => {
|
|
4380
|
+
types.logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata);
|
|
4514
4381
|
const pid = sessionMetadata.hostPid;
|
|
4515
4382
|
if (!pid) {
|
|
4516
|
-
types
|
|
4383
|
+
types.logger.debug(`[DAEMON RUN] Session webhook missing hostPid for sessionId: ${sessionId}`);
|
|
4517
4384
|
return;
|
|
4518
4385
|
}
|
|
4519
|
-
types
|
|
4386
|
+
types.logger.debug(`[DAEMON RUN] Session webhook: ${sessionId}, PID: ${pid}, started by: ${sessionMetadata.startedBy || "unknown"}`);
|
|
4387
|
+
types.logger.debug(`[DAEMON RUN] Current tracked sessions before webhook: ${Array.from(pidToTrackedSession.keys()).join(", ")}`);
|
|
4520
4388
|
const existingSession = pidToTrackedSession.get(pid);
|
|
4521
4389
|
if (existingSession && existingSession.startedBy === "daemon") {
|
|
4522
4390
|
existingSession.happySessionId = sessionId;
|
|
4523
4391
|
existingSession.happySessionMetadataFromLocalWebhook = sessionMetadata;
|
|
4524
|
-
types
|
|
4392
|
+
types.logger.debug(`[DAEMON RUN] Updated daemon-spawned session ${sessionId} with metadata`);
|
|
4525
4393
|
const awaiter = pidToAwaiter.get(pid);
|
|
4526
4394
|
if (awaiter) {
|
|
4527
4395
|
pidToAwaiter.delete(pid);
|
|
4528
4396
|
awaiter(existingSession);
|
|
4529
|
-
types
|
|
4397
|
+
types.logger.debug(`[DAEMON RUN] Resolved session awaiter for PID ${pid}`);
|
|
4530
4398
|
}
|
|
4531
4399
|
} else if (!existingSession) {
|
|
4532
4400
|
const trackedSession = {
|
|
@@ -4536,12 +4404,41 @@ async function startDaemon() {
|
|
|
4536
4404
|
pid
|
|
4537
4405
|
};
|
|
4538
4406
|
pidToTrackedSession.set(pid, trackedSession);
|
|
4539
|
-
types
|
|
4407
|
+
types.logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
|
|
4540
4408
|
}
|
|
4541
4409
|
};
|
|
4542
4410
|
const spawnSession = async (directory, sessionId) => {
|
|
4411
|
+
let directoryCreated = false;
|
|
4412
|
+
if (directory.startsWith("~")) {
|
|
4413
|
+
directory = path.resolve(os$1.homedir(), directory.replace("~", ""));
|
|
4414
|
+
}
|
|
4415
|
+
try {
|
|
4416
|
+
await fs.access(directory);
|
|
4417
|
+
types.logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
|
|
4418
|
+
} catch (error) {
|
|
4419
|
+
types.logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
|
|
4420
|
+
try {
|
|
4421
|
+
await fs.mkdir(directory, { recursive: true });
|
|
4422
|
+
types.logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
|
|
4423
|
+
directoryCreated = true;
|
|
4424
|
+
} catch (mkdirError) {
|
|
4425
|
+
let errorMessage = `Unable to create directory at '${directory}'. `;
|
|
4426
|
+
if (mkdirError.code === "EACCES") {
|
|
4427
|
+
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.`;
|
|
4428
|
+
} else if (mkdirError.code === "ENOTDIR") {
|
|
4429
|
+
errorMessage += `A file already exists at this path or in the parent path. Cannot create a directory here. Please choose a different location.`;
|
|
4430
|
+
} else if (mkdirError.code === "ENOSPC") {
|
|
4431
|
+
errorMessage += `No space left on device. Your disk is full. Please free up some space and try again.`;
|
|
4432
|
+
} else if (mkdirError.code === "EROFS") {
|
|
4433
|
+
errorMessage += `The file system is read-only. Cannot create directories here. Please choose a writable location.`;
|
|
4434
|
+
} else {
|
|
4435
|
+
errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
|
|
4436
|
+
}
|
|
4437
|
+
types.logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
|
|
4438
|
+
return null;
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4543
4441
|
try {
|
|
4544
|
-
const happyBinPath = path.join(projectPath(), "bin", "happy.mjs");
|
|
4545
4442
|
const args = [
|
|
4546
4443
|
"--happy-starting-mode",
|
|
4547
4444
|
"remote",
|
|
@@ -4556,88 +4453,88 @@ async function startDaemon() {
|
|
|
4556
4453
|
// Capture stdout/stderr for debugging
|
|
4557
4454
|
// env is inherited automatically from parent process
|
|
4558
4455
|
});
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4456
|
+
if (process.env.DEBUG) {
|
|
4457
|
+
happyProcess.stdout?.on("data", (data) => {
|
|
4458
|
+
types.logger.debug(`[DAEMON RUN] Child stdout: ${data.toString()}`);
|
|
4459
|
+
});
|
|
4460
|
+
happyProcess.stderr?.on("data", (data) => {
|
|
4461
|
+
types.logger.debug(`[DAEMON RUN] Child stderr: ${data.toString()}`);
|
|
4462
|
+
});
|
|
4463
|
+
}
|
|
4565
4464
|
if (!happyProcess.pid) {
|
|
4566
|
-
types
|
|
4465
|
+
types.logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
|
|
4567
4466
|
return null;
|
|
4568
4467
|
}
|
|
4569
|
-
types
|
|
4468
|
+
types.logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
|
|
4570
4469
|
const trackedSession = {
|
|
4571
4470
|
startedBy: "daemon",
|
|
4572
4471
|
pid: happyProcess.pid,
|
|
4573
|
-
childProcess: happyProcess
|
|
4472
|
+
childProcess: happyProcess,
|
|
4473
|
+
directoryCreated,
|
|
4474
|
+
message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0
|
|
4574
4475
|
};
|
|
4575
4476
|
pidToTrackedSession.set(happyProcess.pid, trackedSession);
|
|
4576
4477
|
happyProcess.on("exit", (code, signal) => {
|
|
4577
|
-
types
|
|
4478
|
+
types.logger.debug(`[DAEMON RUN] Child PID ${happyProcess.pid} exited with code ${code}, signal ${signal}`);
|
|
4578
4479
|
if (happyProcess.pid) {
|
|
4579
4480
|
onChildExited(happyProcess.pid);
|
|
4580
4481
|
}
|
|
4581
4482
|
});
|
|
4582
4483
|
happyProcess.on("error", (error) => {
|
|
4583
|
-
types
|
|
4484
|
+
types.logger.debug(`[DAEMON RUN] Child process error:`, error);
|
|
4584
4485
|
if (happyProcess.pid) {
|
|
4585
4486
|
onChildExited(happyProcess.pid);
|
|
4586
4487
|
}
|
|
4587
4488
|
});
|
|
4588
|
-
types
|
|
4589
|
-
return new Promise((
|
|
4489
|
+
types.logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${happyProcess.pid}`);
|
|
4490
|
+
return new Promise((resolve2, reject) => {
|
|
4590
4491
|
const timeout = setTimeout(() => {
|
|
4591
4492
|
pidToAwaiter.delete(happyProcess.pid);
|
|
4592
|
-
types
|
|
4593
|
-
|
|
4493
|
+
types.logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
|
|
4494
|
+
resolve2(trackedSession);
|
|
4594
4495
|
}, 1e4);
|
|
4595
4496
|
pidToAwaiter.set(happyProcess.pid, (completedSession) => {
|
|
4596
4497
|
clearTimeout(timeout);
|
|
4597
|
-
types
|
|
4598
|
-
|
|
4498
|
+
types.logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
|
|
4499
|
+
resolve2(completedSession);
|
|
4599
4500
|
});
|
|
4600
4501
|
});
|
|
4601
4502
|
} catch (error) {
|
|
4602
|
-
types
|
|
4503
|
+
types.logger.debug("[DAEMON RUN] Failed to spawn session:", error);
|
|
4603
4504
|
return null;
|
|
4604
4505
|
}
|
|
4605
4506
|
};
|
|
4606
4507
|
const stopSession = (sessionId) => {
|
|
4607
|
-
types
|
|
4508
|
+
types.logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
|
|
4608
4509
|
for (const [pid, session] of pidToTrackedSession.entries()) {
|
|
4609
4510
|
if (session.happySessionId === sessionId || sessionId.startsWith("PID-") && pid === parseInt(sessionId.replace("PID-", ""))) {
|
|
4610
4511
|
if (session.startedBy === "daemon" && session.childProcess) {
|
|
4611
4512
|
try {
|
|
4612
4513
|
session.childProcess.kill("SIGTERM");
|
|
4613
|
-
types
|
|
4514
|
+
types.logger.debug(`[DAEMON RUN] Sent SIGTERM to daemon-spawned session ${sessionId}`);
|
|
4614
4515
|
} catch (error) {
|
|
4615
|
-
types
|
|
4516
|
+
types.logger.debug(`[DAEMON RUN] Failed to kill session ${sessionId}:`, error);
|
|
4616
4517
|
}
|
|
4617
4518
|
} else {
|
|
4618
4519
|
try {
|
|
4619
4520
|
process.kill(pid, "SIGTERM");
|
|
4620
|
-
types
|
|
4521
|
+
types.logger.debug(`[DAEMON RUN] Sent SIGTERM to external session PID ${pid}`);
|
|
4621
4522
|
} catch (error) {
|
|
4622
|
-
types
|
|
4523
|
+
types.logger.debug(`[DAEMON RUN] Failed to kill external session PID ${pid}:`, error);
|
|
4623
4524
|
}
|
|
4624
4525
|
}
|
|
4625
4526
|
pidToTrackedSession.delete(pid);
|
|
4626
|
-
types
|
|
4527
|
+
types.logger.debug(`[DAEMON RUN] Removed session ${sessionId} from tracking`);
|
|
4627
4528
|
return true;
|
|
4628
4529
|
}
|
|
4629
4530
|
}
|
|
4630
|
-
types
|
|
4531
|
+
types.logger.debug(`[DAEMON RUN] Session ${sessionId} not found`);
|
|
4631
4532
|
return false;
|
|
4632
4533
|
};
|
|
4633
4534
|
const onChildExited = (pid) => {
|
|
4634
|
-
types
|
|
4535
|
+
types.logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
|
|
4635
4536
|
pidToTrackedSession.delete(pid);
|
|
4636
4537
|
};
|
|
4637
|
-
let requestShutdown;
|
|
4638
|
-
let resolvesWhenShutdownRequested = new Promise((resolve) => {
|
|
4639
|
-
requestShutdown = resolve;
|
|
4640
|
-
});
|
|
4641
4538
|
const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
|
|
4642
4539
|
getChildren: getCurrentChildren,
|
|
4643
4540
|
stopSession,
|
|
@@ -4648,24 +4545,25 @@ async function startDaemon() {
|
|
|
4648
4545
|
const fileState = {
|
|
4649
4546
|
pid: process.pid,
|
|
4650
4547
|
httpPort: controlPort,
|
|
4651
|
-
startTime: (/* @__PURE__ */ new Date()).
|
|
4652
|
-
startedWithCliVersion: packageJson.version
|
|
4548
|
+
startTime: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
4549
|
+
startedWithCliVersion: types.packageJson.version,
|
|
4550
|
+
daemonLogPath: types.logger.logFilePath
|
|
4653
4551
|
};
|
|
4654
|
-
|
|
4655
|
-
types
|
|
4552
|
+
types.writeDaemonState(fileState);
|
|
4553
|
+
types.logger.debug("[DAEMON RUN] Daemon state written");
|
|
4656
4554
|
const initialDaemonState = {
|
|
4657
4555
|
status: "offline",
|
|
4658
4556
|
pid: process.pid,
|
|
4659
4557
|
httpPort: controlPort,
|
|
4660
4558
|
startedAt: Date.now()
|
|
4661
4559
|
};
|
|
4662
|
-
const api = new types
|
|
4560
|
+
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
4663
4561
|
const machine = await api.createMachineOrGetExistingAsIs({
|
|
4664
4562
|
machineId,
|
|
4665
4563
|
metadata: initialMachineMetadata,
|
|
4666
4564
|
daemonState: initialDaemonState
|
|
4667
4565
|
});
|
|
4668
|
-
types
|
|
4566
|
+
types.logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
|
|
4669
4567
|
const apiMachine = api.machineSyncClient(machine);
|
|
4670
4568
|
apiMachine.setRPCHandlers({
|
|
4671
4569
|
spawnSession,
|
|
@@ -4673,63 +4571,96 @@ async function startDaemon() {
|
|
|
4673
4571
|
requestShutdown: () => requestShutdown("happy-app")
|
|
4674
4572
|
});
|
|
4675
4573
|
apiMachine.connect();
|
|
4676
|
-
const
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
})
|
|
4685
|
-
|
|
4574
|
+
const heartbeatIntervalMs = parseInt(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL || "60000");
|
|
4575
|
+
let heartbeatRunning = false;
|
|
4576
|
+
const restartOnStaleVersionAndHeartbeat = setInterval(async () => {
|
|
4577
|
+
if (heartbeatRunning) {
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
heartbeatRunning = true;
|
|
4581
|
+
if (process.env.DEBUG) {
|
|
4582
|
+
types.logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
|
|
4583
|
+
}
|
|
4584
|
+
for (const [pid, _] of pidToTrackedSession.entries()) {
|
|
4585
|
+
try {
|
|
4586
|
+
process.kill(pid, 0);
|
|
4587
|
+
} catch (error) {
|
|
4588
|
+
types.logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
|
|
4589
|
+
pidToTrackedSession.delete(pid);
|
|
4590
|
+
}
|
|
4686
4591
|
}
|
|
4687
|
-
|
|
4688
|
-
|
|
4592
|
+
const projectVersion = JSON.parse(fs$1.readFileSync(path.join(projectPath(), "package.json"), "utf-8")).version;
|
|
4593
|
+
if (projectVersion !== types.configuration.currentCliVersion) {
|
|
4594
|
+
types.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
|
|
4595
|
+
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
4596
|
+
try {
|
|
4597
|
+
spawnHappyCLI(["daemon", "start"], {
|
|
4598
|
+
detached: true,
|
|
4599
|
+
stdio: "ignore"
|
|
4600
|
+
});
|
|
4601
|
+
} catch (error) {
|
|
4602
|
+
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);
|
|
4603
|
+
}
|
|
4604
|
+
types.logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
|
|
4605
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e4));
|
|
4606
|
+
process.exit(0);
|
|
4689
4607
|
}
|
|
4690
|
-
|
|
4608
|
+
const daemonState = await types.readDaemonState();
|
|
4609
|
+
if (daemonState && daemonState.pid !== process.pid) {
|
|
4610
|
+
types.logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
|
|
4611
|
+
requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
|
|
4612
|
+
}
|
|
4613
|
+
try {
|
|
4614
|
+
const updatedState = {
|
|
4615
|
+
pid: process.pid,
|
|
4616
|
+
httpPort: controlPort,
|
|
4617
|
+
startTime: fileState.startTime,
|
|
4618
|
+
startedWithCliVersion: types.packageJson.version,
|
|
4619
|
+
lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
4620
|
+
daemonLogPath: fileState.daemonLogPath
|
|
4621
|
+
};
|
|
4622
|
+
types.writeDaemonState(updatedState);
|
|
4623
|
+
if (process.env.DEBUG) {
|
|
4624
|
+
types.logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
|
|
4625
|
+
}
|
|
4626
|
+
} catch (error) {
|
|
4627
|
+
types.logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
|
|
4628
|
+
}
|
|
4629
|
+
heartbeatRunning = false;
|
|
4630
|
+
}, heartbeatIntervalMs);
|
|
4631
|
+
const cleanupAndShutdown = async (source, errorMessage) => {
|
|
4632
|
+
types.logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
|
|
4633
|
+
if (restartOnStaleVersionAndHeartbeat) {
|
|
4634
|
+
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
4635
|
+
types.logger.debug("[DAEMON RUN] Health check interval cleared");
|
|
4636
|
+
}
|
|
4637
|
+
await apiMachine.updateDaemonState((state) => ({
|
|
4638
|
+
...state,
|
|
4639
|
+
status: "shutting-down",
|
|
4640
|
+
shutdownRequestedAt: Date.now(),
|
|
4641
|
+
shutdownSource: source
|
|
4642
|
+
}));
|
|
4643
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
4644
|
+
apiMachine.shutdown();
|
|
4691
4645
|
await stopControlServer();
|
|
4692
|
-
types$1.logger.debug("[DAEMON RUN] Control server stopped");
|
|
4693
4646
|
await cleanupDaemonState();
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
types
|
|
4647
|
+
await stopCaffeinate();
|
|
4648
|
+
await types.releaseDaemonLock(daemonLockHandle);
|
|
4649
|
+
types.logger.debug("[DAEMON RUN] Cleanup completed, exiting process");
|
|
4697
4650
|
process.exit(0);
|
|
4698
4651
|
};
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
});
|
|
4703
|
-
process.on("SIGTERM", () => {
|
|
4704
|
-
types$1.logger.debug("[DAEMON RUN] Received SIGTERM");
|
|
4705
|
-
cleanupAndShutdown("os-signal");
|
|
4706
|
-
});
|
|
4707
|
-
process.on("uncaughtException", (error) => {
|
|
4708
|
-
types$1.logger.debug("[DAEMON RUN] Uncaught exception - cleaning up before crash", error);
|
|
4709
|
-
cleanupAndShutdown("unknown");
|
|
4710
|
-
});
|
|
4711
|
-
process.on("unhandledRejection", (reason) => {
|
|
4712
|
-
types$1.logger.debug("[DAEMON RUN] Unhandled rejection - cleaning up before crash", reason);
|
|
4713
|
-
cleanupAndShutdown("unknown");
|
|
4714
|
-
});
|
|
4715
|
-
process.on("exit", () => {
|
|
4716
|
-
types$1.logger.debug("[DAEMON RUN] Process exit, not killing any children");
|
|
4717
|
-
});
|
|
4718
|
-
types$1.logger.debug("[DAEMON RUN] Daemon started successfully");
|
|
4719
|
-
const shutdownSource = await resolvesWhenShutdownRequested;
|
|
4720
|
-
types$1.logger.debug(`[DAEMON RUN] Shutdown requested (source: ${shutdownSource})`);
|
|
4721
|
-
await cleanupAndShutdown(shutdownSource);
|
|
4652
|
+
types.logger.debug("[DAEMON RUN] Daemon started successfully, waiting for shutdown request");
|
|
4653
|
+
const shutdownRequest = await resolvesWhenShutdownRequested;
|
|
4654
|
+
await cleanupAndShutdown(shutdownRequest.source, shutdownRequest.errorMessage);
|
|
4722
4655
|
} catch (error) {
|
|
4723
|
-
types
|
|
4724
|
-
await cleanupDaemonState();
|
|
4725
|
-
stopCaffeinate();
|
|
4656
|
+
types.logger.debug("[DAEMON RUN][FATAL] Failed somewhere unexpectedly - exiting with code 1", error);
|
|
4726
4657
|
process.exit(1);
|
|
4727
4658
|
}
|
|
4728
4659
|
}
|
|
4729
4660
|
|
|
4730
4661
|
async function startHappyServer(client) {
|
|
4731
4662
|
const handler = async (title) => {
|
|
4732
|
-
types
|
|
4663
|
+
types.logger.debug("[happyMCP] Changing title to:", title);
|
|
4733
4664
|
try {
|
|
4734
4665
|
client.sendClaudeSessionMessage({
|
|
4735
4666
|
type: "summary",
|
|
@@ -4754,7 +4685,7 @@ async function startHappyServer(client) {
|
|
|
4754
4685
|
}
|
|
4755
4686
|
}, async (args) => {
|
|
4756
4687
|
const response = await handler(args.title);
|
|
4757
|
-
types
|
|
4688
|
+
types.logger.debug("[happyMCP] Response:", response);
|
|
4758
4689
|
if (response.success) {
|
|
4759
4690
|
return {
|
|
4760
4691
|
content: [
|
|
@@ -4787,7 +4718,7 @@ async function startHappyServer(client) {
|
|
|
4787
4718
|
try {
|
|
4788
4719
|
await transport.handleRequest(req, res);
|
|
4789
4720
|
} catch (error) {
|
|
4790
|
-
types
|
|
4721
|
+
types.logger.debug("Error handling request:", error);
|
|
4791
4722
|
if (!res.headersSent) {
|
|
4792
4723
|
res.writeHead(500).end();
|
|
4793
4724
|
}
|
|
@@ -4812,74 +4743,76 @@ async function startHappyServer(client) {
|
|
|
4812
4743
|
async function start(credentials, options = {}) {
|
|
4813
4744
|
const workingDirectory = process.cwd();
|
|
4814
4745
|
const sessionTag = node_crypto.randomUUID();
|
|
4815
|
-
types
|
|
4816
|
-
types
|
|
4746
|
+
types.logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
|
|
4747
|
+
types.logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
|
|
4817
4748
|
if (options.startedBy === "daemon" && options.startingMode === "local") {
|
|
4818
|
-
types
|
|
4749
|
+
types.logger.debug("Daemon spawn requested with local mode - forcing remote mode");
|
|
4819
4750
|
options.startingMode = "remote";
|
|
4820
4751
|
}
|
|
4821
|
-
const api = new types
|
|
4752
|
+
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
4822
4753
|
let state = {};
|
|
4823
|
-
const settings = await readSettings();
|
|
4754
|
+
const settings = await types.readSettings();
|
|
4824
4755
|
let machineId = settings?.machineId;
|
|
4825
4756
|
if (!machineId) {
|
|
4826
|
-
console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it
|
|
4827
|
-
|
|
4757
|
+
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`);
|
|
4758
|
+
process.exit(1);
|
|
4828
4759
|
}
|
|
4829
|
-
types
|
|
4760
|
+
types.logger.debug(`Using machineId: ${machineId}`);
|
|
4761
|
+
await api.createMachineOrGetExistingAsIs({
|
|
4762
|
+
machineId,
|
|
4763
|
+
metadata: initialMachineMetadata
|
|
4764
|
+
});
|
|
4830
4765
|
let metadata = {
|
|
4831
4766
|
path: workingDirectory,
|
|
4832
4767
|
host: os.hostname(),
|
|
4833
|
-
version: packageJson.version,
|
|
4768
|
+
version: types.packageJson.version,
|
|
4834
4769
|
os: os.platform(),
|
|
4835
4770
|
machineId,
|
|
4836
4771
|
homeDir: os.homedir(),
|
|
4837
|
-
happyHomeDir: types
|
|
4772
|
+
happyHomeDir: types.configuration.happyHomeDir,
|
|
4838
4773
|
startedFromDaemon: options.startedBy === "daemon",
|
|
4839
4774
|
hostPid: process.pid,
|
|
4840
4775
|
startedBy: options.startedBy || "terminal"
|
|
4841
4776
|
};
|
|
4842
4777
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
4843
|
-
types
|
|
4844
|
-
await api.createMachineOrGetExistingAsIs({
|
|
4845
|
-
machineId,
|
|
4846
|
-
metadata: initialMachineMetadata
|
|
4847
|
-
});
|
|
4778
|
+
types.logger.debug(`Session created: ${response.id}`);
|
|
4848
4779
|
try {
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
types
|
|
4780
|
+
types.logger.debug(`[START] Reporting session ${response.id} to daemon`);
|
|
4781
|
+
const result = await notifyDaemonSessionStarted(response.id, metadata);
|
|
4782
|
+
if (result.error) {
|
|
4783
|
+
types.logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
|
|
4784
|
+
} else {
|
|
4785
|
+
types.logger.debug(`[START] Reported session ${response.id} to daemon`);
|
|
4853
4786
|
}
|
|
4854
4787
|
} catch (error) {
|
|
4855
|
-
types
|
|
4788
|
+
types.logger.debug("[START] Failed to report to daemon (may not be running):", error);
|
|
4856
4789
|
}
|
|
4857
4790
|
extractSDKMetadataAsync(async (sdkMetadata) => {
|
|
4858
|
-
types
|
|
4791
|
+
types.logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
|
|
4859
4792
|
try {
|
|
4860
4793
|
api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
|
|
4861
4794
|
...currentMetadata,
|
|
4862
4795
|
tools: sdkMetadata.tools,
|
|
4863
4796
|
slashCommands: sdkMetadata.slashCommands
|
|
4864
4797
|
}));
|
|
4865
|
-
types
|
|
4798
|
+
types.logger.debug("[start] Session metadata updated with SDK capabilities");
|
|
4866
4799
|
} catch (error) {
|
|
4867
|
-
types
|
|
4800
|
+
types.logger.debug("[start] Failed to update session metadata:", error);
|
|
4868
4801
|
}
|
|
4869
4802
|
});
|
|
4870
4803
|
const session = api.sessionSyncClient(response);
|
|
4871
4804
|
const happyServer = await startHappyServer(session);
|
|
4872
|
-
types
|
|
4873
|
-
const logPath =
|
|
4874
|
-
types
|
|
4875
|
-
types
|
|
4805
|
+
types.logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
|
|
4806
|
+
const logPath = types.logger.logFilePath;
|
|
4807
|
+
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
4808
|
+
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
4876
4809
|
session.updateAgentState((currentState) => ({
|
|
4877
4810
|
...currentState,
|
|
4878
4811
|
controlledByUser: options.startingMode !== "remote"
|
|
4879
4812
|
}));
|
|
4880
4813
|
const caffeinateStarted = startCaffeinate();
|
|
4881
4814
|
if (caffeinateStarted) {
|
|
4882
|
-
types
|
|
4815
|
+
types.logger.infoDeveloper("Sleep prevention enabled (macOS)");
|
|
4883
4816
|
}
|
|
4884
4817
|
const messageQueue = new MessageQueue2((mode) => hashObject({
|
|
4885
4818
|
isPlan: mode.permissionMode === "plan",
|
|
@@ -4905,64 +4838,64 @@ async function start(credentials, options = {}) {
|
|
|
4905
4838
|
if (validModes.includes(message.meta.permissionMode)) {
|
|
4906
4839
|
messagePermissionMode = message.meta.permissionMode;
|
|
4907
4840
|
currentPermissionMode = messagePermissionMode;
|
|
4908
|
-
types
|
|
4841
|
+
types.logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
|
|
4909
4842
|
} else {
|
|
4910
|
-
types
|
|
4843
|
+
types.logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
|
|
4911
4844
|
}
|
|
4912
4845
|
} else {
|
|
4913
|
-
types
|
|
4846
|
+
types.logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
|
|
4914
4847
|
}
|
|
4915
4848
|
let messageModel = currentModel;
|
|
4916
4849
|
if (message.meta?.hasOwnProperty("model")) {
|
|
4917
4850
|
messageModel = message.meta.model || void 0;
|
|
4918
4851
|
currentModel = messageModel;
|
|
4919
|
-
types
|
|
4852
|
+
types.logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
|
|
4920
4853
|
} else {
|
|
4921
|
-
types
|
|
4854
|
+
types.logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
|
|
4922
4855
|
}
|
|
4923
4856
|
let messageCustomSystemPrompt = currentCustomSystemPrompt;
|
|
4924
4857
|
if (message.meta?.hasOwnProperty("customSystemPrompt")) {
|
|
4925
4858
|
messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
|
|
4926
4859
|
currentCustomSystemPrompt = messageCustomSystemPrompt;
|
|
4927
|
-
types
|
|
4860
|
+
types.logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
|
|
4928
4861
|
} else {
|
|
4929
|
-
types
|
|
4862
|
+
types.logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
|
|
4930
4863
|
}
|
|
4931
4864
|
let messageFallbackModel = currentFallbackModel;
|
|
4932
4865
|
if (message.meta?.hasOwnProperty("fallbackModel")) {
|
|
4933
4866
|
messageFallbackModel = message.meta.fallbackModel || void 0;
|
|
4934
4867
|
currentFallbackModel = messageFallbackModel;
|
|
4935
|
-
types
|
|
4868
|
+
types.logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
|
|
4936
4869
|
} else {
|
|
4937
|
-
types
|
|
4870
|
+
types.logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
|
|
4938
4871
|
}
|
|
4939
4872
|
let messageAppendSystemPrompt = currentAppendSystemPrompt;
|
|
4940
4873
|
if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
|
|
4941
4874
|
messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
|
|
4942
4875
|
currentAppendSystemPrompt = messageAppendSystemPrompt;
|
|
4943
|
-
types
|
|
4876
|
+
types.logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
|
|
4944
4877
|
} else {
|
|
4945
|
-
types
|
|
4878
|
+
types.logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
|
|
4946
4879
|
}
|
|
4947
4880
|
let messageAllowedTools = currentAllowedTools;
|
|
4948
4881
|
if (message.meta?.hasOwnProperty("allowedTools")) {
|
|
4949
4882
|
messageAllowedTools = message.meta.allowedTools || void 0;
|
|
4950
4883
|
currentAllowedTools = messageAllowedTools;
|
|
4951
|
-
types
|
|
4884
|
+
types.logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
|
|
4952
4885
|
} else {
|
|
4953
|
-
types
|
|
4886
|
+
types.logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
|
|
4954
4887
|
}
|
|
4955
4888
|
let messageDisallowedTools = currentDisallowedTools;
|
|
4956
4889
|
if (message.meta?.hasOwnProperty("disallowedTools")) {
|
|
4957
4890
|
messageDisallowedTools = message.meta.disallowedTools || void 0;
|
|
4958
4891
|
currentDisallowedTools = messageDisallowedTools;
|
|
4959
|
-
types
|
|
4892
|
+
types.logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
|
|
4960
4893
|
} else {
|
|
4961
|
-
types
|
|
4894
|
+
types.logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
|
|
4962
4895
|
}
|
|
4963
4896
|
const specialCommand = parseSpecialCommand(message.content.text);
|
|
4964
4897
|
if (specialCommand.type === "compact") {
|
|
4965
|
-
types
|
|
4898
|
+
types.logger.debug("[start] Detected /compact command");
|
|
4966
4899
|
const enhancedMode2 = {
|
|
4967
4900
|
permissionMode: messagePermissionMode || "default",
|
|
4968
4901
|
model: messageModel,
|
|
@@ -4973,11 +4906,11 @@ async function start(credentials, options = {}) {
|
|
|
4973
4906
|
disallowedTools: messageDisallowedTools
|
|
4974
4907
|
};
|
|
4975
4908
|
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
4976
|
-
types
|
|
4909
|
+
types.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
4977
4910
|
return;
|
|
4978
4911
|
}
|
|
4979
4912
|
if (specialCommand.type === "clear") {
|
|
4980
|
-
types
|
|
4913
|
+
types.logger.debug("[start] Detected /clear command");
|
|
4981
4914
|
const enhancedMode2 = {
|
|
4982
4915
|
permissionMode: messagePermissionMode || "default",
|
|
4983
4916
|
model: messageModel,
|
|
@@ -4988,7 +4921,7 @@ async function start(credentials, options = {}) {
|
|
|
4988
4921
|
disallowedTools: messageDisallowedTools
|
|
4989
4922
|
};
|
|
4990
4923
|
messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
|
|
4991
|
-
types
|
|
4924
|
+
types.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
|
|
4992
4925
|
return;
|
|
4993
4926
|
}
|
|
4994
4927
|
const enhancedMode = {
|
|
@@ -5001,10 +4934,10 @@ async function start(credentials, options = {}) {
|
|
|
5001
4934
|
disallowedTools: messageDisallowedTools
|
|
5002
4935
|
};
|
|
5003
4936
|
messageQueue.push(message.content.text, enhancedMode);
|
|
5004
|
-
types
|
|
4937
|
+
types.logger.debugLargeJson("User message pushed to queue:", message);
|
|
5005
4938
|
});
|
|
5006
4939
|
const cleanup = async () => {
|
|
5007
|
-
types
|
|
4940
|
+
types.logger.debug("[START] Received termination signal, cleaning up...");
|
|
5008
4941
|
try {
|
|
5009
4942
|
if (session) {
|
|
5010
4943
|
session.sendSessionDeath();
|
|
@@ -5013,21 +4946,21 @@ async function start(credentials, options = {}) {
|
|
|
5013
4946
|
}
|
|
5014
4947
|
stopCaffeinate();
|
|
5015
4948
|
happyServer.stop();
|
|
5016
|
-
types
|
|
4949
|
+
types.logger.debug("[START] Cleanup complete, exiting");
|
|
5017
4950
|
process.exit(0);
|
|
5018
4951
|
} catch (error) {
|
|
5019
|
-
types
|
|
4952
|
+
types.logger.debug("[START] Error during cleanup:", error);
|
|
5020
4953
|
process.exit(1);
|
|
5021
4954
|
}
|
|
5022
4955
|
};
|
|
5023
4956
|
process.on("SIGTERM", cleanup);
|
|
5024
4957
|
process.on("SIGINT", cleanup);
|
|
5025
4958
|
process.on("uncaughtException", (error) => {
|
|
5026
|
-
types
|
|
4959
|
+
types.logger.debug("[START] Uncaught exception:", error);
|
|
5027
4960
|
cleanup();
|
|
5028
4961
|
});
|
|
5029
4962
|
process.on("unhandledRejection", (reason) => {
|
|
5030
|
-
types
|
|
4963
|
+
types.logger.debug("[START] Unhandled rejection:", reason);
|
|
5031
4964
|
cleanup();
|
|
5032
4965
|
});
|
|
5033
4966
|
await loop({
|
|
@@ -5058,14 +4991,14 @@ async function start(credentials, options = {}) {
|
|
|
5058
4991
|
claudeArgs: options.claudeArgs
|
|
5059
4992
|
});
|
|
5060
4993
|
session.sendSessionDeath();
|
|
5061
|
-
types
|
|
4994
|
+
types.logger.debug("Waiting for socket to flush...");
|
|
5062
4995
|
await session.flush();
|
|
5063
|
-
types
|
|
4996
|
+
types.logger.debug("Closing session...");
|
|
5064
4997
|
await session.close();
|
|
5065
4998
|
stopCaffeinate();
|
|
5066
|
-
types
|
|
4999
|
+
types.logger.debug("Stopped sleep prevention");
|
|
5067
5000
|
happyServer.stop();
|
|
5068
|
-
types
|
|
5001
|
+
types.logger.debug("Stopped Happy MCP server");
|
|
5069
5002
|
process.exit(0);
|
|
5070
5003
|
}
|
|
5071
5004
|
|
|
@@ -5073,8 +5006,8 @@ const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
|
5073
5006
|
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
5074
5007
|
async function install$1() {
|
|
5075
5008
|
try {
|
|
5076
|
-
if (fs.existsSync(PLIST_FILE$1)) {
|
|
5077
|
-
types
|
|
5009
|
+
if (fs$1.existsSync(PLIST_FILE$1)) {
|
|
5010
|
+
types.logger.info("Daemon plist already exists. Uninstalling first...");
|
|
5078
5011
|
child_process.execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
5079
5012
|
}
|
|
5080
5013
|
const happyPath = process.argv[0];
|
|
@@ -5117,14 +5050,14 @@ async function install$1() {
|
|
|
5117
5050
|
</dict>
|
|
5118
5051
|
</plist>
|
|
5119
5052
|
`);
|
|
5120
|
-
fs.writeFileSync(PLIST_FILE$1, plistContent);
|
|
5121
|
-
fs.chmodSync(PLIST_FILE$1, 420);
|
|
5122
|
-
types
|
|
5053
|
+
fs$1.writeFileSync(PLIST_FILE$1, plistContent);
|
|
5054
|
+
fs$1.chmodSync(PLIST_FILE$1, 420);
|
|
5055
|
+
types.logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
|
|
5123
5056
|
child_process.execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
5124
|
-
types
|
|
5125
|
-
types
|
|
5057
|
+
types.logger.info("Daemon installed and started successfully");
|
|
5058
|
+
types.logger.info("Check logs at ~/.happy/daemon.log");
|
|
5126
5059
|
} catch (error) {
|
|
5127
|
-
types
|
|
5060
|
+
types.logger.debug("Failed to install daemon:", error);
|
|
5128
5061
|
throw error;
|
|
5129
5062
|
}
|
|
5130
5063
|
}
|
|
@@ -5136,7 +5069,7 @@ async function install() {
|
|
|
5136
5069
|
if (process.getuid && process.getuid() !== 0) {
|
|
5137
5070
|
throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
|
|
5138
5071
|
}
|
|
5139
|
-
types
|
|
5072
|
+
types.logger.info("Installing Happy CLI daemon for macOS...");
|
|
5140
5073
|
await install$1();
|
|
5141
5074
|
}
|
|
5142
5075
|
|
|
@@ -5144,21 +5077,21 @@ const PLIST_LABEL = "com.happy-cli.daemon";
|
|
|
5144
5077
|
const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
|
|
5145
5078
|
async function uninstall$1() {
|
|
5146
5079
|
try {
|
|
5147
|
-
if (!fs.existsSync(PLIST_FILE)) {
|
|
5148
|
-
types
|
|
5080
|
+
if (!fs$1.existsSync(PLIST_FILE)) {
|
|
5081
|
+
types.logger.info("Daemon plist not found. Nothing to uninstall.");
|
|
5149
5082
|
return;
|
|
5150
5083
|
}
|
|
5151
5084
|
try {
|
|
5152
5085
|
child_process.execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
|
|
5153
|
-
types
|
|
5086
|
+
types.logger.info("Daemon stopped successfully");
|
|
5154
5087
|
} catch (error) {
|
|
5155
|
-
types
|
|
5088
|
+
types.logger.info("Failed to unload daemon (it might not be running)");
|
|
5156
5089
|
}
|
|
5157
|
-
fs.unlinkSync(PLIST_FILE);
|
|
5158
|
-
types
|
|
5159
|
-
types
|
|
5090
|
+
fs$1.unlinkSync(PLIST_FILE);
|
|
5091
|
+
types.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
|
|
5092
|
+
types.logger.info("Daemon uninstalled successfully");
|
|
5160
5093
|
} catch (error) {
|
|
5161
|
-
types
|
|
5094
|
+
types.logger.debug("Failed to uninstall daemon:", error);
|
|
5162
5095
|
throw error;
|
|
5163
5096
|
}
|
|
5164
5097
|
}
|
|
@@ -5170,7 +5103,7 @@ async function uninstall() {
|
|
|
5170
5103
|
if (process.getuid && process.getuid() !== 0) {
|
|
5171
5104
|
throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
|
|
5172
5105
|
}
|
|
5173
|
-
types
|
|
5106
|
+
types.logger.info("Uninstalling Happy CLI daemon for macOS...");
|
|
5174
5107
|
await uninstall$1();
|
|
5175
5108
|
}
|
|
5176
5109
|
|
|
@@ -5263,21 +5196,21 @@ async function handleAuthLogin(args) {
|
|
|
5263
5196
|
console.log(chalk.gray(" \u2022 Stop daemon if running"));
|
|
5264
5197
|
console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
|
|
5265
5198
|
try {
|
|
5266
|
-
types
|
|
5199
|
+
types.logger.debug("Stopping daemon for force auth...");
|
|
5267
5200
|
await stopDaemon();
|
|
5268
5201
|
console.log(chalk.gray("\u2713 Stopped daemon"));
|
|
5269
5202
|
} catch (error) {
|
|
5270
|
-
types
|
|
5203
|
+
types.logger.debug("Daemon was not running or failed to stop:", error);
|
|
5271
5204
|
}
|
|
5272
|
-
await clearCredentials();
|
|
5205
|
+
await types.clearCredentials();
|
|
5273
5206
|
console.log(chalk.gray("\u2713 Cleared credentials"));
|
|
5274
|
-
await clearMachineId();
|
|
5207
|
+
await types.clearMachineId();
|
|
5275
5208
|
console.log(chalk.gray("\u2713 Cleared machine ID"));
|
|
5276
5209
|
console.log("");
|
|
5277
5210
|
}
|
|
5278
5211
|
if (!forceAuth) {
|
|
5279
|
-
const existingCreds = await readCredentials();
|
|
5280
|
-
const settings = await readSettings();
|
|
5212
|
+
const existingCreds = await types.readCredentials();
|
|
5213
|
+
const settings = await types.readSettings();
|
|
5281
5214
|
if (existingCreds && settings?.machineId) {
|
|
5282
5215
|
console.log(chalk.green("\u2713 Already authenticated"));
|
|
5283
5216
|
console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
|
|
@@ -5300,8 +5233,8 @@ async function handleAuthLogin(args) {
|
|
|
5300
5233
|
}
|
|
5301
5234
|
}
|
|
5302
5235
|
async function handleAuthLogout() {
|
|
5303
|
-
const happyDir = types
|
|
5304
|
-
const credentials = await readCredentials();
|
|
5236
|
+
const happyDir = types.configuration.happyHomeDir;
|
|
5237
|
+
const credentials = await types.readCredentials();
|
|
5305
5238
|
if (!credentials) {
|
|
5306
5239
|
console.log(chalk.yellow("Not currently authenticated"));
|
|
5307
5240
|
return;
|
|
@@ -5336,8 +5269,8 @@ async function handleAuthLogout() {
|
|
|
5336
5269
|
}
|
|
5337
5270
|
}
|
|
5338
5271
|
async function handleAuthShowBackup() {
|
|
5339
|
-
const credentials = await readCredentials();
|
|
5340
|
-
const settings = await readSettings();
|
|
5272
|
+
const credentials = await types.readCredentials();
|
|
5273
|
+
const settings = await types.readSettings();
|
|
5341
5274
|
if (!credentials) {
|
|
5342
5275
|
console.log(chalk.yellow("Not authenticated"));
|
|
5343
5276
|
console.log(chalk.gray('Run "happy auth login" to authenticate first'));
|
|
@@ -5361,8 +5294,8 @@ async function handleAuthShowBackup() {
|
|
|
5361
5294
|
console.log(chalk.yellow("\u26A0\uFE0F Keep this key secure - it provides full access to your account"));
|
|
5362
5295
|
}
|
|
5363
5296
|
async function handleAuthStatus() {
|
|
5364
|
-
const credentials = await readCredentials();
|
|
5365
|
-
const settings = await readSettings();
|
|
5297
|
+
const credentials = await types.readCredentials();
|
|
5298
|
+
const settings = await types.readSettings();
|
|
5366
5299
|
console.log(chalk.bold("\nAuthentication Status\n"));
|
|
5367
5300
|
if (!credentials) {
|
|
5368
5301
|
console.log(chalk.red("\u2717 Not authenticated"));
|
|
@@ -5381,10 +5314,9 @@ async function handleAuthStatus() {
|
|
|
5381
5314
|
console.log(chalk.gray(' Run "happy auth login --force" to fix this'));
|
|
5382
5315
|
}
|
|
5383
5316
|
console.log(chalk.gray(`
|
|
5384
|
-
Data directory: ${types
|
|
5317
|
+
Data directory: ${types.configuration.happyHomeDir}`));
|
|
5385
5318
|
try {
|
|
5386
|
-
const
|
|
5387
|
-
const running = await isDaemonRunning();
|
|
5319
|
+
const running = await checkIfDaemonRunningAndCleanupStaleState();
|
|
5388
5320
|
if (running) {
|
|
5389
5321
|
console.log(chalk.green("\u2713 Daemon running"));
|
|
5390
5322
|
} else {
|
|
@@ -5425,9 +5357,19 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5425
5357
|
|
|
5426
5358
|
(async () => {
|
|
5427
5359
|
const args = process.argv.slice(2);
|
|
5428
|
-
|
|
5360
|
+
if (!args.includes("--version")) {
|
|
5361
|
+
types.logger.debug("Starting happy CLI with args: ", process.argv);
|
|
5362
|
+
}
|
|
5429
5363
|
const subcommand = args[0];
|
|
5430
5364
|
if (subcommand === "doctor") {
|
|
5365
|
+
if (args[1] === "clean") {
|
|
5366
|
+
const result = await killRunawayHappyProcesses();
|
|
5367
|
+
console.log(`Cleaned up ${result.killed} runaway processes`);
|
|
5368
|
+
if (result.errors.length > 0) {
|
|
5369
|
+
console.log("Errors:", result.errors);
|
|
5370
|
+
}
|
|
5371
|
+
process.exit(0);
|
|
5372
|
+
}
|
|
5431
5373
|
await runDoctorCommand();
|
|
5432
5374
|
return;
|
|
5433
5375
|
} else if (subcommand === "auth") {
|
|
@@ -5470,16 +5412,10 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5470
5412
|
try {
|
|
5471
5413
|
const sessions = await listDaemonSessions();
|
|
5472
5414
|
if (sessions.length === 0) {
|
|
5473
|
-
console.log("No active sessions");
|
|
5415
|
+
console.log("No active sessions this daemon is aware of (they might have been started by a previous version of the daemon)");
|
|
5474
5416
|
} else {
|
|
5475
5417
|
console.log("Active sessions:");
|
|
5476
|
-
|
|
5477
|
-
pid: s.pid,
|
|
5478
|
-
sessionId: s.happySessionId || `PID-${s.pid}`,
|
|
5479
|
-
startedBy: s.startedBy,
|
|
5480
|
-
directory: s.happySessionMetadataFromLocalWebhook?.directory || "unknown"
|
|
5481
|
-
}));
|
|
5482
|
-
console.log(JSON.stringify(cleanSessions, null, 2));
|
|
5418
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
5483
5419
|
}
|
|
5484
5420
|
} catch (error) {
|
|
5485
5421
|
console.log("No daemon running");
|
|
@@ -5507,7 +5443,7 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5507
5443
|
child.unref();
|
|
5508
5444
|
let started = false;
|
|
5509
5445
|
for (let i = 0; i < 50; i++) {
|
|
5510
|
-
if (await
|
|
5446
|
+
if (await checkIfDaemonRunningAndCleanupStaleState()) {
|
|
5511
5447
|
started = true;
|
|
5512
5448
|
break;
|
|
5513
5449
|
}
|
|
@@ -5527,28 +5463,14 @@ const DaemonPrompt = ({ onSelect }) => {
|
|
|
5527
5463
|
await stopDaemon();
|
|
5528
5464
|
process.exit(0);
|
|
5529
5465
|
} else if (daemonSubcommand === "status") {
|
|
5530
|
-
|
|
5531
|
-
if (!state) {
|
|
5532
|
-
console.log("Daemon is not running");
|
|
5533
|
-
} else {
|
|
5534
|
-
const isRunning = await isDaemonRunning();
|
|
5535
|
-
if (isRunning) {
|
|
5536
|
-
console.log("Daemon is running");
|
|
5537
|
-
console.log(` PID: ${state.pid}`);
|
|
5538
|
-
console.log(` Port: ${state.httpPort}`);
|
|
5539
|
-
console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
|
|
5540
|
-
console.log(` CLI Version: ${state.startedWithCliVersion}`);
|
|
5541
|
-
} else {
|
|
5542
|
-
console.log("Daemon state file exists but daemon is not running (stale)");
|
|
5543
|
-
}
|
|
5544
|
-
}
|
|
5466
|
+
await runDoctorCommand("daemon");
|
|
5545
5467
|
process.exit(0);
|
|
5546
|
-
} else if (daemonSubcommand === "
|
|
5547
|
-
const
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
console.log(
|
|
5468
|
+
} else if (daemonSubcommand === "logs") {
|
|
5469
|
+
const latest = await types.getLatestDaemonLog();
|
|
5470
|
+
if (!latest) {
|
|
5471
|
+
console.log("No daemon logs found");
|
|
5472
|
+
} else {
|
|
5473
|
+
console.log(latest.path);
|
|
5552
5474
|
}
|
|
5553
5475
|
process.exit(0);
|
|
5554
5476
|
} else if (daemonSubcommand === "install") {
|
|
@@ -5572,14 +5494,15 @@ ${chalk.bold("happy daemon")} - Daemon management
|
|
|
5572
5494
|
${chalk.bold("Usage:")}
|
|
5573
5495
|
happy daemon start Start the daemon (detached)
|
|
5574
5496
|
happy daemon stop Stop the daemon (sessions stay alive)
|
|
5575
|
-
happy daemon stop --kill-managed Stop daemon and kill managed sessions
|
|
5576
5497
|
happy daemon status Show daemon status
|
|
5577
5498
|
happy daemon list List active sessions
|
|
5578
|
-
|
|
5579
|
-
|
|
5499
|
+
|
|
5500
|
+
If you want to kill all happy related processes run
|
|
5501
|
+
${chalk.cyan("happy doctor clean")}
|
|
5580
5502
|
|
|
5581
5503
|
${chalk.bold("Note:")} The daemon runs in the background and manages Claude sessions.
|
|
5582
|
-
|
|
5504
|
+
|
|
5505
|
+
${chalk.bold("To clean up runaway processes:")} Use ${chalk.cyan("happy doctor clean")}
|
|
5583
5506
|
`);
|
|
5584
5507
|
}
|
|
5585
5508
|
return;
|
|
@@ -5587,8 +5510,6 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5587
5510
|
const options = {};
|
|
5588
5511
|
let showHelp = false;
|
|
5589
5512
|
let showVersion = false;
|
|
5590
|
-
let forceAuth = false;
|
|
5591
|
-
let forceAuthNew = false;
|
|
5592
5513
|
const unknownArgs = [];
|
|
5593
5514
|
for (let i = 0; i < args.length; i++) {
|
|
5594
5515
|
const arg = args[i];
|
|
@@ -5598,11 +5519,7 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5598
5519
|
} else if (arg === "-v" || arg === "--version") {
|
|
5599
5520
|
showVersion = true;
|
|
5600
5521
|
unknownArgs.push(arg);
|
|
5601
|
-
} else if (arg === "--auth" || arg === "--login") {
|
|
5602
|
-
forceAuth = true;
|
|
5603
|
-
} else if (arg === "--force-auth") {
|
|
5604
|
-
forceAuthNew = true;
|
|
5605
|
-
} else if (arg === "--happy-starting-mode") {
|
|
5522
|
+
} else if (arg === "--auth" || arg === "--login") ; else if (arg === "--force-auth") ; else if (arg === "--happy-starting-mode") {
|
|
5606
5523
|
options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
|
|
5607
5524
|
} else if (arg === "--yolo") {
|
|
5608
5525
|
unknownArgs.push("--dangerously-skip-permissions");
|
|
@@ -5623,29 +5540,23 @@ Sessions spawned by the daemon will continue running after daemon stops unless -
|
|
|
5623
5540
|
${chalk.bold("happy")} - Claude Code On the Go
|
|
5624
5541
|
|
|
5625
5542
|
${chalk.bold("Usage:")}
|
|
5626
|
-
happy [options]
|
|
5627
|
-
happy auth Manage authentication
|
|
5628
|
-
happy
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
${chalk.bold("Happy Options:")}
|
|
5632
|
-
--help Show this help message
|
|
5633
|
-
--yolo Skip all permissions (--dangerously-skip-permissions)
|
|
5634
|
-
--force-auth Force re-authentication
|
|
5635
|
-
|
|
5636
|
-
${chalk.bold("\u{1F3AF} Happy supports ALL Claude options!")}
|
|
5637
|
-
Use any claude flag exactly as you normally would.
|
|
5543
|
+
happy [options] Start Claude with mobile control
|
|
5544
|
+
happy auth Manage authentication
|
|
5545
|
+
happy daemon Manage background service that allows
|
|
5546
|
+
to spawn new sessions away from your computer
|
|
5547
|
+
happy doctor System diagnostics & troubleshooting
|
|
5638
5548
|
|
|
5639
5549
|
${chalk.bold("Examples:")}
|
|
5640
|
-
happy
|
|
5641
|
-
happy --yolo
|
|
5642
|
-
happy --verbose
|
|
5643
|
-
happy
|
|
5644
|
-
happy
|
|
5645
|
-
happy notify -p "Done!" Send notification
|
|
5550
|
+
happy Start session
|
|
5551
|
+
happy --yolo Bypass permissions
|
|
5552
|
+
happy --verbose Enable verbose mode
|
|
5553
|
+
happy auth login --force Authenticate
|
|
5554
|
+
happy doctor Run diagnostics
|
|
5646
5555
|
|
|
5647
5556
|
${chalk.bold("Happy is a wrapper around Claude Code that enables remote control via mobile app.")}
|
|
5648
|
-
|
|
5557
|
+
|
|
5558
|
+
${chalk.bold("Happy supports ALL Claude options!")}
|
|
5559
|
+
Use any claude flag exactly as you normally would.
|
|
5649
5560
|
|
|
5650
5561
|
${chalk.gray("\u2500".repeat(60))}
|
|
5651
5562
|
${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
@@ -5660,34 +5571,13 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5660
5571
|
process.exit(0);
|
|
5661
5572
|
}
|
|
5662
5573
|
if (showVersion) {
|
|
5663
|
-
console.log(packageJson.version);
|
|
5574
|
+
console.log(types.packageJson.version);
|
|
5664
5575
|
process.exit(0);
|
|
5665
5576
|
}
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
await stopDaemon();
|
|
5671
|
-
} catch {
|
|
5672
|
-
}
|
|
5673
|
-
await clearCredentials();
|
|
5674
|
-
await clearMachineId();
|
|
5675
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5676
|
-
credentials = result.credentials;
|
|
5677
|
-
} else if (forceAuth) {
|
|
5678
|
-
console.log(chalk.yellow('Note: --auth is deprecated. Use "happy auth login" or --force-auth instead.\n'));
|
|
5679
|
-
const res = await doAuth();
|
|
5680
|
-
if (!res) {
|
|
5681
|
-
process.exit(1);
|
|
5682
|
-
}
|
|
5683
|
-
await writeCredentials(res);
|
|
5684
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5685
|
-
credentials = result.credentials;
|
|
5686
|
-
} else {
|
|
5687
|
-
const result = await authAndSetupMachineIfNeeded();
|
|
5688
|
-
credentials = result.credentials;
|
|
5689
|
-
}
|
|
5690
|
-
let settings = await readSettings();
|
|
5577
|
+
const {
|
|
5578
|
+
credentials
|
|
5579
|
+
} = await authAndSetupMachineIfNeeded();
|
|
5580
|
+
let settings = await types.readSettings();
|
|
5691
5581
|
if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
|
|
5692
5582
|
const shouldAutoStart = await new Promise((resolve) => {
|
|
5693
5583
|
let hasResolved = false;
|
|
@@ -5703,7 +5593,7 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5703
5593
|
patchConsole: false
|
|
5704
5594
|
});
|
|
5705
5595
|
});
|
|
5706
|
-
settings = await updateSettings((settings2) => ({
|
|
5596
|
+
settings = await types.updateSettings((settings2) => ({
|
|
5707
5597
|
...settings2,
|
|
5708
5598
|
daemonAutoStartWhenRunningHappy: shouldAutoStart
|
|
5709
5599
|
}));
|
|
@@ -5715,15 +5605,18 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
|
|
|
5715
5605
|
}
|
|
5716
5606
|
}
|
|
5717
5607
|
if (settings && settings.daemonAutoStartWhenRunningHappy) {
|
|
5718
|
-
types
|
|
5719
|
-
if (!await
|
|
5608
|
+
types.logger.debug("Ensuring Happy background service is running & matches our version...");
|
|
5609
|
+
if (!await isDaemonRunningSameVersion()) {
|
|
5610
|
+
types.logger.debug("Starting Happy background service...");
|
|
5720
5611
|
const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
|
|
5721
5612
|
detached: true,
|
|
5722
5613
|
stdio: "ignore",
|
|
5723
5614
|
env: process.env
|
|
5724
5615
|
});
|
|
5725
5616
|
daemonProcess.unref();
|
|
5726
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
5617
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
5618
|
+
} else {
|
|
5619
|
+
types.logger.debug("Happy background service is running & matches our version");
|
|
5727
5620
|
}
|
|
5728
5621
|
}
|
|
5729
5622
|
try {
|
|
@@ -5778,14 +5671,14 @@ ${chalk.bold("Examples:")}
|
|
|
5778
5671
|
console.log(chalk.gray('Run "happy notify --help" for usage information.'));
|
|
5779
5672
|
process.exit(1);
|
|
5780
5673
|
}
|
|
5781
|
-
let credentials = await readCredentials();
|
|
5674
|
+
let credentials = await types.readCredentials();
|
|
5782
5675
|
if (!credentials) {
|
|
5783
5676
|
console.error(chalk.red('Error: Not authenticated. Please run "happy --auth" first.'));
|
|
5784
5677
|
process.exit(1);
|
|
5785
5678
|
}
|
|
5786
5679
|
console.log(chalk.blue("\u{1F4F1} Sending push notification..."));
|
|
5787
5680
|
try {
|
|
5788
|
-
const api = new types
|
|
5681
|
+
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
5789
5682
|
const notificationTitle = title || "Happy";
|
|
5790
5683
|
api.push().sendToAllDevices(
|
|
5791
5684
|
notificationTitle,
|