happy-coder 0.1.9 → 0.1.11
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/README.md +2 -0
- package/dist/index-B2GqfEZV.cjs +1564 -0
- package/dist/index-QItBXhux.mjs +1540 -0
- package/dist/index.cjs +997 -260
- package/dist/index.mjs +996 -259
- package/dist/install-B0DnBGS_.mjs +29 -0
- package/dist/install-C809w0Cj.cjs +31 -0
- package/dist/install-DEPy62QN.mjs +97 -0
- package/dist/install-GZIzyuIE.cjs +99 -0
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +265 -460
- package/dist/lib.d.mts +265 -460
- package/dist/lib.mjs +1 -1
- package/dist/run-BmEaINbl.cjs +250 -0
- package/dist/run-DMbKhYfb.mjs +247 -0
- package/dist/types-BRICSarm.mjs +870 -0
- package/dist/types-BTQRfIr3.cjs +892 -0
- package/dist/types-CEvzGLMI.cjs +882 -0
- package/dist/types-D39L8JSd.mjs +850 -0
- package/dist/types-DYBiuNUQ.cjs +883 -0
- package/dist/types-Df5dlWLV.mjs +871 -0
- package/dist/types-hotUTaWz.cjs +863 -0
- package/dist/types-tLWMaptR.mjs +879 -0
- package/dist/uninstall-BGgl5V8F.mjs +29 -0
- package/dist/uninstall-BWHglipH.mjs +40 -0
- package/dist/uninstall-CdHMb6wi.cjs +31 -0
- package/dist/uninstall-FXyyAuGU.cjs +42 -0
- package/package.json +8 -2
- package/ripgrep/COPYING +3 -0
- package/ripgrep/arm64-darwin/rg +0 -0
- package/ripgrep/arm64-darwin/ripgrep.node +0 -0
- package/ripgrep/arm64-linux/rg +0 -0
- package/ripgrep/arm64-linux/ripgrep.node +0 -0
- package/ripgrep/x64-darwin/rg +0 -0
- package/ripgrep/x64-darwin/ripgrep.node +0 -0
- package/ripgrep/x64-linux/rg +0 -0
- package/ripgrep/x64-linux/ripgrep.node +0 -0
- package/ripgrep/x64-win32/rg.exe +0 -0
- package/ripgrep/x64-win32/ripgrep.node +0 -0
- package/scripts/ripgrep_launcher.cjs +57 -0
package/dist/index.cjs
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var types = require('./types-
|
|
4
|
+
var types = require('./types-hotUTaWz.cjs');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
6
|
var claudeCode = require('@anthropic-ai/claude-code');
|
|
7
7
|
var node_fs = require('node:fs');
|
|
8
|
-
var os = require('node:os');
|
|
9
8
|
var node_path = require('node:path');
|
|
9
|
+
var os = require('node:os');
|
|
10
|
+
var promises = require('fs/promises');
|
|
10
11
|
var node_child_process = require('node:child_process');
|
|
11
12
|
var node_readline = require('node:readline');
|
|
12
13
|
var node_url = require('node:url');
|
|
13
|
-
var promises = require('node:fs/promises');
|
|
14
|
+
var promises$1 = require('node:fs/promises');
|
|
14
15
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
15
16
|
var node_http = require('node:http');
|
|
16
17
|
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
17
18
|
var z = require('zod');
|
|
18
|
-
var
|
|
19
|
-
var
|
|
19
|
+
var child_process = require('child_process');
|
|
20
|
+
var util = require('util');
|
|
21
|
+
var crypto = require('crypto');
|
|
22
|
+
var path = require('path');
|
|
23
|
+
var url = require('url');
|
|
24
|
+
var httpProxy = require('http-proxy');
|
|
20
25
|
var tweetnacl = require('tweetnacl');
|
|
21
26
|
var axios = require('axios');
|
|
22
27
|
var qrcode = require('qrcode-terminal');
|
|
23
|
-
require('
|
|
24
|
-
require('
|
|
25
|
-
require('
|
|
28
|
+
var node_events = require('node:events');
|
|
29
|
+
var socket_ioClient = require('socket.io-client');
|
|
30
|
+
var os$1 = require('os');
|
|
31
|
+
var fs = require('fs');
|
|
26
32
|
require('expo-server-sdk');
|
|
27
33
|
|
|
28
34
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
@@ -165,9 +171,13 @@ function printDivider() {
|
|
|
165
171
|
console.log(chalk.gray("\u2550".repeat(60)));
|
|
166
172
|
}
|
|
167
173
|
|
|
174
|
+
function getProjectPath(workingDirectory) {
|
|
175
|
+
const projectId = node_path.resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
|
|
176
|
+
return node_path.join(os.homedir(), ".claude", "projects", projectId);
|
|
177
|
+
}
|
|
178
|
+
|
|
168
179
|
function claudeCheckSession(sessionId, path) {
|
|
169
|
-
const
|
|
170
|
-
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
180
|
+
const projectDir = getProjectPath(path);
|
|
171
181
|
const sessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
172
182
|
const sessionExists = node_fs.existsSync(sessionFile);
|
|
173
183
|
if (!sessionExists) {
|
|
@@ -185,11 +195,29 @@ function claudeCheckSession(sessionId, path) {
|
|
|
185
195
|
return hasGoodMessage;
|
|
186
196
|
}
|
|
187
197
|
|
|
198
|
+
async function awaitFileExist(file, timeout = 1e4) {
|
|
199
|
+
const startTime = Date.now();
|
|
200
|
+
while (Date.now() - startTime < timeout) {
|
|
201
|
+
try {
|
|
202
|
+
await promises.access(file);
|
|
203
|
+
return true;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
await types.delay(1e3);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
188
211
|
async function claudeRemote(opts) {
|
|
189
212
|
let startFrom = opts.sessionId;
|
|
190
213
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
191
214
|
startFrom = null;
|
|
192
215
|
}
|
|
216
|
+
if (opts.claudeEnvVars) {
|
|
217
|
+
Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
|
|
218
|
+
process.env[key] = value;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
193
221
|
const abortController = new AbortController();
|
|
194
222
|
const sdkOptions = {
|
|
195
223
|
cwd: opts.path,
|
|
@@ -199,6 +227,9 @@ async function claudeRemote(opts) {
|
|
|
199
227
|
executable: "node",
|
|
200
228
|
abortController
|
|
201
229
|
};
|
|
230
|
+
if (opts.claudeArgs && opts.claudeArgs.length > 0) {
|
|
231
|
+
sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
|
|
232
|
+
}
|
|
202
233
|
let aborted = false;
|
|
203
234
|
let response;
|
|
204
235
|
opts.abort.addEventListener("abort", () => {
|
|
@@ -236,15 +267,11 @@ async function claudeRemote(opts) {
|
|
|
236
267
|
types.logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
|
|
237
268
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
238
269
|
if (message.type === "system" && message.subtype === "init") {
|
|
239
|
-
|
|
240
|
-
const projectDir =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
opts.onSessionFound(message.session_id);
|
|
245
|
-
watcher.close();
|
|
246
|
-
}
|
|
247
|
-
});
|
|
270
|
+
types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
|
|
271
|
+
const projectDir = getProjectPath(opts.path);
|
|
272
|
+
const found = await awaitFileExist(node_path.join(projectDir, `${message.session_id}.jsonl`));
|
|
273
|
+
types.logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
|
|
274
|
+
opts.onSessionFound(message.session_id);
|
|
248
275
|
}
|
|
249
276
|
}
|
|
250
277
|
types.logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
@@ -266,10 +293,9 @@ async function claudeRemote(opts) {
|
|
|
266
293
|
types.logger.debug(`[claudeRemote] Function completed`);
|
|
267
294
|
}
|
|
268
295
|
|
|
269
|
-
const __dirname$
|
|
296
|
+
const __dirname$2 = node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
270
297
|
async function claudeLocal(opts) {
|
|
271
|
-
const
|
|
272
|
-
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
298
|
+
const projectDir = getProjectPath(opts.path);
|
|
273
299
|
node_fs.mkdirSync(projectDir, { recursive: true });
|
|
274
300
|
const watcher = node_fs.watch(projectDir);
|
|
275
301
|
let resolvedSessionId = null;
|
|
@@ -303,11 +329,19 @@ async function claudeLocal(opts) {
|
|
|
303
329
|
if (startFrom) {
|
|
304
330
|
args.push("--resume", startFrom);
|
|
305
331
|
}
|
|
306
|
-
|
|
332
|
+
if (opts.claudeArgs) {
|
|
333
|
+
args.push(...opts.claudeArgs);
|
|
334
|
+
}
|
|
335
|
+
const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || node_path.resolve(node_path.join(__dirname$2, "..", "scripts", "claudeInteractiveLaunch.cjs"));
|
|
336
|
+
const env = {
|
|
337
|
+
...process.env,
|
|
338
|
+
...opts.claudeEnvVars
|
|
339
|
+
};
|
|
307
340
|
const child = node_child_process.spawn("node", [claudeCliPath, ...args], {
|
|
308
341
|
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
309
342
|
signal: opts.abort,
|
|
310
|
-
cwd: opts.path
|
|
343
|
+
cwd: opts.path,
|
|
344
|
+
env
|
|
311
345
|
});
|
|
312
346
|
if (child.stdio[3]) {
|
|
313
347
|
const rl = node_readline.createInterface({
|
|
@@ -532,16 +566,44 @@ class InvalidateSync {
|
|
|
532
566
|
};
|
|
533
567
|
}
|
|
534
568
|
|
|
569
|
+
function startFileWatcher(file, onFileChange) {
|
|
570
|
+
const abortController = new AbortController();
|
|
571
|
+
void (async () => {
|
|
572
|
+
while (true) {
|
|
573
|
+
try {
|
|
574
|
+
types.logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
|
|
575
|
+
const watcher = promises.watch(file, { persistent: true, signal: abortController.signal });
|
|
576
|
+
for await (const event of watcher) {
|
|
577
|
+
if (abortController.signal.aborted) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
types.logger.debug(`[FILE_WATCHER] File changed: ${file}`);
|
|
581
|
+
onFileChange(file);
|
|
582
|
+
}
|
|
583
|
+
} catch (e) {
|
|
584
|
+
if (abortController.signal.aborted) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
types.logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
|
|
588
|
+
await types.delay(1e3);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
})();
|
|
592
|
+
return () => {
|
|
593
|
+
abortController.abort();
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
535
597
|
function createSessionScanner(opts) {
|
|
536
|
-
const
|
|
537
|
-
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
598
|
+
const projectDir = getProjectPath(opts.workingDirectory);
|
|
538
599
|
let finishedSessions = /* @__PURE__ */ new Set();
|
|
539
600
|
let pendingSessions = /* @__PURE__ */ new Set();
|
|
540
601
|
let currentSessionId = null;
|
|
541
|
-
let
|
|
602
|
+
let watchers = /* @__PURE__ */ new Map();
|
|
542
603
|
let processedMessages = /* @__PURE__ */ new Set();
|
|
543
604
|
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
544
605
|
const sync = new InvalidateSync(async () => {
|
|
606
|
+
types.logger.debug(`[SESSION_SCANNER] Syncing...`);
|
|
545
607
|
let sessions = [];
|
|
546
608
|
for (let p of pendingSessions) {
|
|
547
609
|
sessions.push(p);
|
|
@@ -553,13 +615,17 @@ function createSessionScanner(opts) {
|
|
|
553
615
|
const expectedSessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
554
616
|
let file;
|
|
555
617
|
try {
|
|
556
|
-
file = await promises.readFile(expectedSessionFile, "utf-8");
|
|
618
|
+
file = await promises$1.readFile(expectedSessionFile, "utf-8");
|
|
557
619
|
} catch (error) {
|
|
620
|
+
types.logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
|
|
558
621
|
return;
|
|
559
622
|
}
|
|
560
623
|
let lines = file.split("\n");
|
|
561
624
|
for (let l of lines) {
|
|
562
625
|
try {
|
|
626
|
+
if (l.trim() === "") {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
563
629
|
let message = JSON.parse(l);
|
|
564
630
|
let parsed = types.RawJSONLinesSchema.safeParse(message);
|
|
565
631
|
if (!parsed.success) {
|
|
@@ -573,15 +639,16 @@ function createSessionScanner(opts) {
|
|
|
573
639
|
processedMessages.add(key);
|
|
574
640
|
types.logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
575
641
|
types.logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
576
|
-
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
|
|
642
|
+
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
|
|
577
643
|
const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
|
|
578
644
|
if (currentCounter && currentCounter > 0) {
|
|
579
645
|
seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
|
|
580
646
|
continue;
|
|
581
647
|
}
|
|
582
648
|
}
|
|
583
|
-
opts.onMessage(
|
|
649
|
+
opts.onMessage(message);
|
|
584
650
|
} catch (e) {
|
|
651
|
+
types.logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
585
652
|
continue;
|
|
586
653
|
}
|
|
587
654
|
}
|
|
@@ -595,40 +662,37 @@ function createSessionScanner(opts) {
|
|
|
595
662
|
finishedSessions.add(p);
|
|
596
663
|
}
|
|
597
664
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
for await (const change of promises.watch(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
|
|
605
|
-
await processSessionFile(currentSessionId);
|
|
606
|
-
}
|
|
607
|
-
} catch (error) {
|
|
608
|
-
if (error.name !== "AbortError") {
|
|
609
|
-
types.logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
665
|
+
for (let p of sessions) {
|
|
666
|
+
if (!watchers.has(p)) {
|
|
667
|
+
watchers.set(p, startFileWatcher(node_path.join(projectDir, `${p}.jsonl`), () => {
|
|
668
|
+
sync.invalidate();
|
|
669
|
+
}));
|
|
612
670
|
}
|
|
613
|
-
}
|
|
671
|
+
}
|
|
614
672
|
});
|
|
673
|
+
sync.invalidate();
|
|
615
674
|
const intervalId = setInterval(() => {
|
|
616
675
|
sync.invalidate();
|
|
617
676
|
}, 3e3);
|
|
618
677
|
return {
|
|
619
|
-
refresh: () => sync.invalidate(),
|
|
620
678
|
cleanup: () => {
|
|
621
679
|
clearInterval(intervalId);
|
|
622
|
-
|
|
680
|
+
for (let w of watchers.values()) {
|
|
681
|
+
w();
|
|
682
|
+
}
|
|
683
|
+
watchers.clear();
|
|
623
684
|
},
|
|
624
685
|
onNewSession: (sessionId) => {
|
|
625
686
|
if (currentSessionId === sessionId) {
|
|
687
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
|
|
626
688
|
return;
|
|
627
689
|
}
|
|
628
690
|
if (finishedSessions.has(sessionId)) {
|
|
691
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
|
|
629
692
|
return;
|
|
630
693
|
}
|
|
631
694
|
if (pendingSessions.has(sessionId)) {
|
|
695
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
|
|
632
696
|
return;
|
|
633
697
|
}
|
|
634
698
|
if (currentSessionId) {
|
|
@@ -673,7 +737,7 @@ function sortKeys(value) {
|
|
|
673
737
|
}
|
|
674
738
|
|
|
675
739
|
async function loop(opts) {
|
|
676
|
-
let mode = opts.startingMode ?? "
|
|
740
|
+
let mode = opts.startingMode ?? "local";
|
|
677
741
|
let currentMessageQueue = new MessageQueue();
|
|
678
742
|
let sessionId = null;
|
|
679
743
|
let onMessage = null;
|
|
@@ -697,23 +761,38 @@ async function loop(opts) {
|
|
|
697
761
|
};
|
|
698
762
|
while (true) {
|
|
699
763
|
if (currentMessageQueue.size() > 0) {
|
|
700
|
-
mode
|
|
764
|
+
if (mode !== "remote") {
|
|
765
|
+
mode = "remote";
|
|
766
|
+
if (opts.onModeChange) {
|
|
767
|
+
opts.onModeChange(mode);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
701
770
|
continue;
|
|
702
771
|
}
|
|
703
|
-
if (mode === "
|
|
772
|
+
if (mode === "local") {
|
|
704
773
|
let abortedOutside = false;
|
|
705
774
|
const interactiveAbortController = new AbortController();
|
|
706
775
|
opts.session.setHandler("switch", () => {
|
|
707
776
|
if (!interactiveAbortController.signal.aborted) {
|
|
708
777
|
abortedOutside = true;
|
|
709
|
-
mode
|
|
778
|
+
if (mode !== "remote") {
|
|
779
|
+
mode = "remote";
|
|
780
|
+
if (opts.onModeChange) {
|
|
781
|
+
opts.onModeChange(mode);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
710
784
|
interactiveAbortController.abort();
|
|
711
785
|
}
|
|
712
786
|
});
|
|
713
787
|
onMessage = () => {
|
|
714
788
|
if (!interactiveAbortController.signal.aborted) {
|
|
715
789
|
abortedOutside = true;
|
|
716
|
-
mode
|
|
790
|
+
if (mode !== "remote") {
|
|
791
|
+
mode = "remote";
|
|
792
|
+
if (opts.onModeChange) {
|
|
793
|
+
opts.onModeChange(mode);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
717
796
|
interactiveAbortController.abort();
|
|
718
797
|
}
|
|
719
798
|
onMessage = null;
|
|
@@ -722,13 +801,15 @@ async function loop(opts) {
|
|
|
722
801
|
path: opts.path,
|
|
723
802
|
sessionId,
|
|
724
803
|
onSessionFound,
|
|
725
|
-
abort: interactiveAbortController.signal
|
|
804
|
+
abort: interactiveAbortController.signal,
|
|
805
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
806
|
+
claudeArgs: opts.claudeArgs
|
|
726
807
|
});
|
|
727
808
|
onMessage = null;
|
|
728
809
|
if (!abortedOutside) {
|
|
729
810
|
return;
|
|
730
811
|
}
|
|
731
|
-
if (mode !== "
|
|
812
|
+
if (mode !== "local") {
|
|
732
813
|
console.log("Switching to remote mode...");
|
|
733
814
|
}
|
|
734
815
|
}
|
|
@@ -742,7 +823,12 @@ async function loop(opts) {
|
|
|
742
823
|
});
|
|
743
824
|
const abortHandler = () => {
|
|
744
825
|
if (!remoteAbortController.signal.aborted) {
|
|
745
|
-
mode
|
|
826
|
+
if (mode !== "local") {
|
|
827
|
+
mode = "local";
|
|
828
|
+
if (opts.onModeChange) {
|
|
829
|
+
opts.onModeChange(mode);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
746
832
|
remoteAbortController.abort();
|
|
747
833
|
}
|
|
748
834
|
if (process.stdin.isTTY) {
|
|
@@ -766,7 +852,9 @@ async function loop(opts) {
|
|
|
766
852
|
onSessionFound,
|
|
767
853
|
messages: currentMessageQueue,
|
|
768
854
|
onAssistantResult: opts.onAssistantResult,
|
|
769
|
-
interruptController: opts.interruptController
|
|
855
|
+
interruptController: opts.interruptController,
|
|
856
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
857
|
+
claudeArgs: opts.claudeArgs
|
|
770
858
|
});
|
|
771
859
|
} finally {
|
|
772
860
|
process.stdin.off("data", abortHandler);
|
|
@@ -883,155 +971,375 @@ class InterruptController {
|
|
|
883
971
|
}
|
|
884
972
|
}
|
|
885
973
|
|
|
886
|
-
var version = "0.1.
|
|
974
|
+
var version = "0.1.11";
|
|
887
975
|
var packageJson = {
|
|
888
976
|
version: version};
|
|
889
977
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const claudeDidSomeWork = () => {
|
|
906
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
907
|
-
if (requestTimeouts.size === 0) {
|
|
908
|
-
idleTimer = setTimeout(() => {
|
|
909
|
-
types.logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
|
|
910
|
-
onClaudeActivity("idle");
|
|
911
|
-
}, maxTimeBeforeIdle);
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
const server = node_http.createServer((req, res) => {
|
|
915
|
-
const requestId = ++requestCounter;
|
|
916
|
-
const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
|
|
917
|
-
if (isAnthropicRequest) {
|
|
918
|
-
const timeout = setTimeout(() => {
|
|
919
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
|
|
920
|
-
cleanupRequest(requestId, "timeout");
|
|
921
|
-
}, requestTimeout);
|
|
922
|
-
requestTimeouts.set(requestId, timeout);
|
|
923
|
-
onClaudeActivity("working");
|
|
924
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
|
|
925
|
-
}
|
|
926
|
-
const chunks = [];
|
|
927
|
-
req.on("data", (chunk) => {
|
|
928
|
-
chunks.push(chunk);
|
|
929
|
-
if (isAnthropicRequest) {
|
|
930
|
-
claudeDidSomeWork();
|
|
931
|
-
}
|
|
978
|
+
const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
979
|
+
const RUNNER_PATH = path.join(__dirname$1, "..", "..", "scripts", "ripgrep_launcher.cjs");
|
|
980
|
+
function run(args, options) {
|
|
981
|
+
return new Promise((resolve, reject) => {
|
|
982
|
+
const child = child_process.spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
|
|
983
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
984
|
+
cwd: options?.cwd
|
|
985
|
+
});
|
|
986
|
+
let stdout = "";
|
|
987
|
+
let stderr = "";
|
|
988
|
+
child.stdout.on("data", (data) => {
|
|
989
|
+
stdout += data.toString();
|
|
990
|
+
});
|
|
991
|
+
child.stderr.on("data", (data) => {
|
|
992
|
+
stderr += data.toString();
|
|
932
993
|
});
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
994
|
+
child.on("close", (code) => {
|
|
995
|
+
resolve({
|
|
996
|
+
exitCode: code || 0,
|
|
997
|
+
stdout,
|
|
998
|
+
stderr
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
child.on("error", (err) => {
|
|
1002
|
+
reject(err);
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const execAsync = util.promisify(child_process.exec);
|
|
1008
|
+
function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
|
|
1009
|
+
session.setHandler("abort", async () => {
|
|
1010
|
+
types.logger.info("Abort request - interrupting Claude");
|
|
1011
|
+
await interruptController.interrupt();
|
|
1012
|
+
});
|
|
1013
|
+
if (permissionCallbacks) {
|
|
1014
|
+
session.setHandler("permission", async (message) => {
|
|
1015
|
+
types.logger.info("Permission response" + JSON.stringify(message));
|
|
1016
|
+
const id = message.id;
|
|
1017
|
+
const resolve = permissionCallbacks.requests.get(id);
|
|
1018
|
+
if (resolve) {
|
|
1019
|
+
if (!message.approved) {
|
|
1020
|
+
types.logger.debug("Permission denied, interrupting Claude");
|
|
1021
|
+
await interruptController.interrupt();
|
|
1022
|
+
}
|
|
1023
|
+
resolve({ approved: message.approved, reason: message.reason });
|
|
1024
|
+
permissionCallbacks.requests.delete(id);
|
|
938
1025
|
} else {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
targetUrl = new node_url.URL(req.url || "/", `${protocol}://${host}`);
|
|
1026
|
+
types.logger.info("Permission request stale, likely timed out");
|
|
1027
|
+
return;
|
|
942
1028
|
}
|
|
1029
|
+
session.updateAgentState((currentState) => {
|
|
1030
|
+
let r = { ...currentState.requests };
|
|
1031
|
+
delete r[id];
|
|
1032
|
+
return {
|
|
1033
|
+
...currentState,
|
|
1034
|
+
requests: r
|
|
1035
|
+
};
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
session.setHandler("bash", async (data) => {
|
|
1040
|
+
types.logger.info("Shell command request:", data.command);
|
|
1041
|
+
try {
|
|
943
1042
|
const options = {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
method: req.method,
|
|
948
|
-
headers: {
|
|
949
|
-
...req.headers,
|
|
950
|
-
host: targetUrl.hostname
|
|
951
|
-
}
|
|
1043
|
+
cwd: data.cwd,
|
|
1044
|
+
timeout: data.timeout || 3e4
|
|
1045
|
+
// Default 30 seconds timeout
|
|
952
1046
|
};
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1047
|
+
const { stdout, stderr } = await execAsync(data.command, options);
|
|
1048
|
+
return {
|
|
1049
|
+
success: true,
|
|
1050
|
+
stdout: stdout || "",
|
|
1051
|
+
stderr: stderr || "",
|
|
1052
|
+
exitCode: 0
|
|
1053
|
+
};
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
const execError = error;
|
|
1056
|
+
if (execError.code === "ETIMEDOUT" || execError.killed) {
|
|
1057
|
+
return {
|
|
1058
|
+
success: false,
|
|
1059
|
+
stdout: execError.stdout || "",
|
|
1060
|
+
stderr: execError.stderr || "",
|
|
1061
|
+
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
1062
|
+
error: "Command timed out"
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
success: false,
|
|
1067
|
+
stdout: execError.stdout || "",
|
|
1068
|
+
stderr: execError.stderr || execError.message || "Command failed",
|
|
1069
|
+
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
1070
|
+
error: execError.message || "Command failed"
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
session.setHandler("readFile", async (data) => {
|
|
1075
|
+
types.logger.info("Read file request:", data.path);
|
|
1076
|
+
try {
|
|
1077
|
+
const buffer = await promises.readFile(data.path);
|
|
1078
|
+
const content = buffer.toString("base64");
|
|
1079
|
+
return { success: true, content };
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
types.logger.debug("Failed to read file:", error);
|
|
1082
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
session.setHandler("writeFile", async (data) => {
|
|
1086
|
+
types.logger.info("Write file request:", data.path);
|
|
1087
|
+
try {
|
|
1088
|
+
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
1089
|
+
try {
|
|
1090
|
+
const existingBuffer = await promises.readFile(data.path);
|
|
1091
|
+
const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
|
|
1092
|
+
if (existingHash !== data.expectedHash) {
|
|
1093
|
+
return {
|
|
1094
|
+
success: false,
|
|
1095
|
+
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
const nodeError = error;
|
|
1100
|
+
if (nodeError.code !== "ENOENT") {
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
success: false,
|
|
1105
|
+
error: "File does not exist but hash was provided"
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
} else {
|
|
1109
|
+
try {
|
|
1110
|
+
await promises.stat(data.path);
|
|
1111
|
+
return {
|
|
1112
|
+
success: false,
|
|
1113
|
+
error: "File already exists but was expected to be new"
|
|
1114
|
+
};
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
const nodeError = error;
|
|
1117
|
+
if (nodeError.code !== "ENOENT") {
|
|
1118
|
+
throw error;
|
|
960
1119
|
}
|
|
961
|
-
});
|
|
962
|
-
});
|
|
963
|
-
proxyReq.on("error", (error) => {
|
|
964
|
-
if (isAnthropicRequest) {
|
|
965
|
-
cleanupRequest(requestId, `error: ${error.message}`);
|
|
966
|
-
} else {
|
|
967
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
|
|
968
1120
|
}
|
|
969
|
-
res.writeHead(502);
|
|
970
|
-
res.end("Bad Gateway");
|
|
971
|
-
});
|
|
972
|
-
if (body.length > 0) {
|
|
973
|
-
proxyReq.write(body);
|
|
974
1121
|
}
|
|
975
|
-
|
|
976
|
-
|
|
1122
|
+
const buffer = Buffer.from(data.content, "base64");
|
|
1123
|
+
await promises.writeFile(data.path, buffer);
|
|
1124
|
+
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
1125
|
+
return { success: true, hash };
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
types.logger.debug("Failed to write file:", error);
|
|
1128
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
1129
|
+
}
|
|
977
1130
|
});
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1131
|
+
session.setHandler("listDirectory", async (data) => {
|
|
1132
|
+
types.logger.info("List directory request:", data.path);
|
|
1133
|
+
try {
|
|
1134
|
+
const entries = await promises.readdir(data.path, { withFileTypes: true });
|
|
1135
|
+
const directoryEntries = await Promise.all(
|
|
1136
|
+
entries.map(async (entry) => {
|
|
1137
|
+
const fullPath = path.join(data.path, entry.name);
|
|
1138
|
+
let type = "other";
|
|
1139
|
+
let size;
|
|
1140
|
+
let modified;
|
|
1141
|
+
if (entry.isDirectory()) {
|
|
1142
|
+
type = "directory";
|
|
1143
|
+
} else if (entry.isFile()) {
|
|
1144
|
+
type = "file";
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
const stats = await promises.stat(fullPath);
|
|
1148
|
+
size = stats.size;
|
|
1149
|
+
modified = stats.mtime.getTime();
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
types.logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
name: entry.name,
|
|
1155
|
+
type,
|
|
1156
|
+
size,
|
|
1157
|
+
modified
|
|
1158
|
+
};
|
|
1159
|
+
})
|
|
1160
|
+
);
|
|
1161
|
+
directoryEntries.sort((a, b) => {
|
|
1162
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1163
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1164
|
+
return a.name.localeCompare(b.name);
|
|
1165
|
+
});
|
|
1166
|
+
return { success: true, entries: directoryEntries };
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
types.logger.debug("Failed to list directory:", error);
|
|
1169
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
session.setHandler("getDirectoryTree", async (data) => {
|
|
1173
|
+
types.logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1174
|
+
async function buildTree(path$1, name, currentDepth) {
|
|
1175
|
+
try {
|
|
1176
|
+
const stats = await promises.stat(path$1);
|
|
1177
|
+
const node = {
|
|
1178
|
+
name,
|
|
1179
|
+
path: path$1,
|
|
1180
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
1181
|
+
size: stats.size,
|
|
1182
|
+
modified: stats.mtime.getTime()
|
|
1183
|
+
};
|
|
1184
|
+
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
1185
|
+
const entries = await promises.readdir(path$1, { withFileTypes: true });
|
|
1186
|
+
const children = [];
|
|
1187
|
+
await Promise.all(
|
|
1188
|
+
entries.map(async (entry) => {
|
|
1189
|
+
if (entry.isSymbolicLink()) {
|
|
1190
|
+
types.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const childPath = path.join(path$1, entry.name);
|
|
1194
|
+
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
1195
|
+
if (childNode) {
|
|
1196
|
+
children.push(childNode);
|
|
1197
|
+
}
|
|
1198
|
+
})
|
|
1199
|
+
);
|
|
1200
|
+
children.sort((a, b) => {
|
|
1201
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1202
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1203
|
+
return a.name.localeCompare(b.name);
|
|
1204
|
+
});
|
|
1205
|
+
node.children = children;
|
|
1206
|
+
}
|
|
1207
|
+
return node;
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
types.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
|
|
1210
|
+
return null;
|
|
1000
1211
|
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
if (data.maxDepth < 0) {
|
|
1215
|
+
return { success: false, error: "maxDepth must be non-negative" };
|
|
1216
|
+
}
|
|
1217
|
+
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
|
|
1218
|
+
const tree = await buildTree(data.path, baseName, 0);
|
|
1219
|
+
if (!tree) {
|
|
1220
|
+
return { success: false, error: "Failed to access the specified path" };
|
|
1221
|
+
}
|
|
1222
|
+
return { success: true, tree };
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
types.logger.debug("Failed to get directory tree:", error);
|
|
1225
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
session.setHandler("ripgrep", async (data) => {
|
|
1229
|
+
types.logger.info("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
1230
|
+
try {
|
|
1231
|
+
const result = await run(data.args, { cwd: data.cwd });
|
|
1232
|
+
return {
|
|
1233
|
+
success: true,
|
|
1234
|
+
exitCode: result.exitCode,
|
|
1235
|
+
stdout: result.stdout,
|
|
1236
|
+
stderr: result.stderr
|
|
1237
|
+
};
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
types.logger.debug("Failed to run ripgrep:", error);
|
|
1240
|
+
return {
|
|
1241
|
+
success: false,
|
|
1242
|
+
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async function startHTTPDirectProxy(options) {
|
|
1249
|
+
const proxy = httpProxy.createProxyServer({
|
|
1250
|
+
target: options.target,
|
|
1251
|
+
changeOrigin: true,
|
|
1252
|
+
secure: false
|
|
1253
|
+
});
|
|
1254
|
+
proxy.on("error", (err, req, res) => {
|
|
1255
|
+
types.logger.debug(`[HTTPProxy] Proxy error: ${err.message} for ${req.method} ${req.url}`);
|
|
1256
|
+
if (res instanceof node_http.ServerResponse && !res.headersSent) {
|
|
1257
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1258
|
+
res.end("Proxy error");
|
|
1259
|
+
}
|
|
1010
1260
|
});
|
|
1011
|
-
|
|
1261
|
+
proxy.on("proxyReq", (proxyReq, req, res) => {
|
|
1262
|
+
if (options.onRequest) {
|
|
1263
|
+
options.onRequest(req, proxyReq);
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1267
|
+
if (options.onResponse) {
|
|
1268
|
+
options.onResponse(req, proxyRes);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
const server = node_http.createServer((req, res) => {
|
|
1272
|
+
proxy.web(req, res);
|
|
1273
|
+
});
|
|
1274
|
+
const url = await new Promise((resolve, reject) => {
|
|
1012
1275
|
server.listen(0, "127.0.0.1", () => {
|
|
1013
1276
|
const addr = server.address();
|
|
1014
1277
|
if (addr && typeof addr === "object") {
|
|
1015
|
-
|
|
1278
|
+
const proxyUrl = `http://127.0.0.1:${addr.port}`;
|
|
1279
|
+
types.logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
|
|
1280
|
+
resolve(proxyUrl);
|
|
1281
|
+
} else {
|
|
1282
|
+
reject(new Error("Failed to get server address"));
|
|
1016
1283
|
}
|
|
1017
1284
|
});
|
|
1018
1285
|
});
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1286
|
+
return url;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function startClaudeActivityTracker(onThinking) {
|
|
1290
|
+
let requestCounter = 0;
|
|
1291
|
+
const activeRequests = /* @__PURE__ */ new Set();
|
|
1292
|
+
let stopThinkingTimeout = null;
|
|
1293
|
+
let isThinking = false;
|
|
1294
|
+
const proxyUrl = await startHTTPDirectProxy({
|
|
1295
|
+
target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
|
|
1296
|
+
onRequest: (req, proxyReq) => {
|
|
1297
|
+
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1298
|
+
const requestId = ++requestCounter;
|
|
1299
|
+
activeRequests.add(requestId);
|
|
1300
|
+
req._requestId = requestId;
|
|
1301
|
+
if (stopThinkingTimeout) {
|
|
1302
|
+
clearTimeout(stopThinkingTimeout);
|
|
1303
|
+
stopThinkingTimeout = null;
|
|
1304
|
+
}
|
|
1305
|
+
if (!isThinking) {
|
|
1306
|
+
types.logger.debug(`[ClaudeActivityTracker] Thinking started`);
|
|
1307
|
+
isThinking = true;
|
|
1308
|
+
onThinking(true);
|
|
1309
|
+
}
|
|
1027
1310
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1311
|
+
},
|
|
1312
|
+
onResponse: (req, proxyRes) => {
|
|
1313
|
+
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
1314
|
+
const requestId = req._requestId;
|
|
1315
|
+
proxyRes.on("end", () => {
|
|
1316
|
+
activeRequests.delete(requestId);
|
|
1317
|
+
if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
|
|
1318
|
+
stopThinkingTimeout = setTimeout(() => {
|
|
1319
|
+
if (isThinking) {
|
|
1320
|
+
isThinking = false;
|
|
1321
|
+
types.logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
|
|
1322
|
+
onThinking(false);
|
|
1323
|
+
}
|
|
1324
|
+
}, 500);
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
proxyRes.on("error", () => {
|
|
1328
|
+
activeRequests.delete(requestId);
|
|
1329
|
+
if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
|
|
1330
|
+
stopThinkingTimeout = setTimeout(() => {
|
|
1331
|
+
if (isThinking) {
|
|
1332
|
+
isThinking = false;
|
|
1333
|
+
types.logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
|
|
1334
|
+
onThinking(false);
|
|
1335
|
+
}
|
|
1336
|
+
}, 500);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1031
1339
|
}
|
|
1032
|
-
server.close();
|
|
1033
1340
|
}
|
|
1034
|
-
};
|
|
1341
|
+
});
|
|
1342
|
+
return proxyUrl;
|
|
1035
1343
|
}
|
|
1036
1344
|
|
|
1037
1345
|
async function start(credentials, options = {}) {
|
|
@@ -1039,30 +1347,23 @@ async function start(credentials, options = {}) {
|
|
|
1039
1347
|
const sessionTag = node_crypto.randomUUID();
|
|
1040
1348
|
const api = new types.ApiClient(credentials.token, credentials.secret);
|
|
1041
1349
|
let state = {};
|
|
1042
|
-
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
|
|
1350
|
+
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
|
|
1043
1351
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1044
1352
|
types.logger.debug(`Session created: ${response.id}`);
|
|
1045
1353
|
const session = api.session(response);
|
|
1046
1354
|
const pushClient = api.push();
|
|
1047
1355
|
let thinking = false;
|
|
1356
|
+
let mode = "local";
|
|
1048
1357
|
let pingInterval = setInterval(() => {
|
|
1049
|
-
session.keepAlive(thinking);
|
|
1358
|
+
session.keepAlive(thinking, mode);
|
|
1050
1359
|
}, 2e3);
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
types.logger.debug(`[PING] Thinking state changed: ${thinking}`);
|
|
1057
|
-
session.keepAlive(thinking);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
);
|
|
1061
|
-
process.env.HTTP_PROXY = antropicActivityProxy.url;
|
|
1062
|
-
process.env.HTTPS_PROXY = antropicActivityProxy.url;
|
|
1063
|
-
types.logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
|
|
1360
|
+
const proxyUrl = await startClaudeActivityTracker((newThinking) => {
|
|
1361
|
+
thinking = newThinking;
|
|
1362
|
+
session.keepAlive(thinking, mode);
|
|
1363
|
+
});
|
|
1364
|
+
process.env.ANTHROPIC_BASE_URL = proxyUrl;
|
|
1064
1365
|
const logPath = await types.logger.logFilePathPromise;
|
|
1065
|
-
types.logger.
|
|
1366
|
+
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
1066
1367
|
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1067
1368
|
const interruptController = new InterruptController();
|
|
1068
1369
|
let requests = /* @__PURE__ */ new Map();
|
|
@@ -1072,10 +1373,10 @@ async function start(credentials, options = {}) {
|
|
|
1072
1373
|
requests.set(id, resolve);
|
|
1073
1374
|
});
|
|
1074
1375
|
let timeout = setTimeout(async () => {
|
|
1075
|
-
types.logger.
|
|
1376
|
+
types.logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
1076
1377
|
const interrupted = await interruptController.interrupt();
|
|
1077
1378
|
if (interrupted) {
|
|
1078
|
-
types.logger.
|
|
1379
|
+
types.logger.debug("Claude interrupted successfully");
|
|
1079
1380
|
}
|
|
1080
1381
|
requests.delete(id);
|
|
1081
1382
|
session.updateAgentState((currentState) => {
|
|
@@ -1087,7 +1388,7 @@ async function start(credentials, options = {}) {
|
|
|
1087
1388
|
};
|
|
1088
1389
|
});
|
|
1089
1390
|
}, 1e3 * 60 * 4.5);
|
|
1090
|
-
types.logger.
|
|
1391
|
+
types.logger.debug("Permission request" + id + " " + JSON.stringify(request));
|
|
1091
1392
|
try {
|
|
1092
1393
|
await pushClient.sendToAllDevices(
|
|
1093
1394
|
"Permission Request",
|
|
@@ -1099,7 +1400,7 @@ async function start(credentials, options = {}) {
|
|
|
1099
1400
|
type: "permission_request"
|
|
1100
1401
|
}
|
|
1101
1402
|
);
|
|
1102
|
-
types.logger.
|
|
1403
|
+
types.logger.debug("Push notification sent for permission request");
|
|
1103
1404
|
} catch (error) {
|
|
1104
1405
|
types.logger.debug("Failed to send push notification:", error);
|
|
1105
1406
|
}
|
|
@@ -1116,29 +1417,7 @@ async function start(credentials, options = {}) {
|
|
|
1116
1417
|
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1117
1418
|
return promise;
|
|
1118
1419
|
});
|
|
1119
|
-
session
|
|
1120
|
-
types.logger.info("Permission response" + JSON.stringify(message));
|
|
1121
|
-
const id = message.id;
|
|
1122
|
-
const resolve = requests.get(id);
|
|
1123
|
-
if (resolve) {
|
|
1124
|
-
resolve({ approved: message.approved, reason: message.reason });
|
|
1125
|
-
} else {
|
|
1126
|
-
types.logger.info("Permission request stale, likely timed out");
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
session.updateAgentState((currentState) => {
|
|
1130
|
-
let r = { ...currentState.requests };
|
|
1131
|
-
delete r[id];
|
|
1132
|
-
return {
|
|
1133
|
-
...currentState,
|
|
1134
|
-
requests: r
|
|
1135
|
-
};
|
|
1136
|
-
});
|
|
1137
|
-
});
|
|
1138
|
-
session.setHandler("abort", async () => {
|
|
1139
|
-
types.logger.info("Abort request - interrupting Claude");
|
|
1140
|
-
await interruptController.interrupt();
|
|
1141
|
-
});
|
|
1420
|
+
registerHandlers(session, interruptController, { requests });
|
|
1142
1421
|
const onAssistantResult = async (result) => {
|
|
1143
1422
|
try {
|
|
1144
1423
|
const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
|
|
@@ -1163,6 +1442,15 @@ async function start(credentials, options = {}) {
|
|
|
1163
1442
|
model: options.model,
|
|
1164
1443
|
permissionMode: options.permissionMode,
|
|
1165
1444
|
startingMode: options.startingMode,
|
|
1445
|
+
onModeChange: (newMode) => {
|
|
1446
|
+
mode = newMode;
|
|
1447
|
+
session.sendSessionEvent({ type: "switch", mode: newMode });
|
|
1448
|
+
session.keepAlive(thinking, mode);
|
|
1449
|
+
session.updateAgentState((currentState) => ({
|
|
1450
|
+
...currentState,
|
|
1451
|
+
controlledByUser: newMode === "local" ? true : false
|
|
1452
|
+
}));
|
|
1453
|
+
},
|
|
1166
1454
|
mcpServers: {
|
|
1167
1455
|
"permission": {
|
|
1168
1456
|
type: "http",
|
|
@@ -1172,16 +1460,34 @@ async function start(credentials, options = {}) {
|
|
|
1172
1460
|
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1173
1461
|
session,
|
|
1174
1462
|
onAssistantResult,
|
|
1175
|
-
interruptController
|
|
1463
|
+
interruptController,
|
|
1464
|
+
claudeEnvVars: options.claudeEnvVars,
|
|
1465
|
+
claudeArgs: options.claudeArgs
|
|
1176
1466
|
});
|
|
1177
1467
|
clearInterval(pingInterval);
|
|
1178
|
-
if (antropicActivityProxy) {
|
|
1179
|
-
types.logger.info("[AnthropicProxy] Shutting down activity monitoring proxy");
|
|
1180
|
-
antropicActivityProxy.cleanup();
|
|
1181
|
-
}
|
|
1182
1468
|
process.exit(0);
|
|
1183
1469
|
}
|
|
1184
1470
|
|
|
1471
|
+
const defaultSettings = {
|
|
1472
|
+
onboardingCompleted: false
|
|
1473
|
+
};
|
|
1474
|
+
async function readSettings() {
|
|
1475
|
+
if (!node_fs.existsSync(types.configuration.settingsFile)) {
|
|
1476
|
+
return { ...defaultSettings };
|
|
1477
|
+
}
|
|
1478
|
+
try {
|
|
1479
|
+
const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
|
|
1480
|
+
return JSON.parse(content);
|
|
1481
|
+
} catch {
|
|
1482
|
+
return { ...defaultSettings };
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async function writeSettings(settings) {
|
|
1486
|
+
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1487
|
+
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1488
|
+
}
|
|
1489
|
+
await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1490
|
+
}
|
|
1185
1491
|
const credentialsSchema = z__namespace.object({
|
|
1186
1492
|
secret: z__namespace.string().base64(),
|
|
1187
1493
|
token: z__namespace.string()
|
|
@@ -1191,7 +1497,7 @@ async function readCredentials() {
|
|
|
1191
1497
|
return null;
|
|
1192
1498
|
}
|
|
1193
1499
|
try {
|
|
1194
|
-
const keyBase64 = await promises.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1500
|
+
const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1195
1501
|
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1196
1502
|
return {
|
|
1197
1503
|
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
@@ -1203,9 +1509,9 @@ async function readCredentials() {
|
|
|
1203
1509
|
}
|
|
1204
1510
|
async function writeCredentials(credentials) {
|
|
1205
1511
|
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1206
|
-
await promises.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1512
|
+
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1207
1513
|
}
|
|
1208
|
-
await promises.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1514
|
+
await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1209
1515
|
secret: types.encodeBase64(credentials.secret),
|
|
1210
1516
|
token: credentials.token
|
|
1211
1517
|
}, null, 2));
|
|
@@ -1283,6 +1589,413 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1283
1589
|
return decrypted;
|
|
1284
1590
|
}
|
|
1285
1591
|
|
|
1592
|
+
class ApiDaemonSession extends node_events.EventEmitter {
|
|
1593
|
+
socket;
|
|
1594
|
+
machineIdentity;
|
|
1595
|
+
keepAliveInterval = null;
|
|
1596
|
+
token;
|
|
1597
|
+
secret;
|
|
1598
|
+
constructor(token, secret, machineIdentity) {
|
|
1599
|
+
super();
|
|
1600
|
+
this.token = token;
|
|
1601
|
+
this.secret = secret;
|
|
1602
|
+
this.machineIdentity = machineIdentity;
|
|
1603
|
+
const socket = socket_ioClient.io(types.configuration.serverUrl, {
|
|
1604
|
+
auth: {
|
|
1605
|
+
token: this.token,
|
|
1606
|
+
clientType: "machine-scoped",
|
|
1607
|
+
machineId: this.machineIdentity.machineId
|
|
1608
|
+
},
|
|
1609
|
+
path: "/v1/user-machine-daemon",
|
|
1610
|
+
reconnection: true,
|
|
1611
|
+
reconnectionAttempts: Infinity,
|
|
1612
|
+
reconnectionDelay: 1e3,
|
|
1613
|
+
reconnectionDelayMax: 5e3,
|
|
1614
|
+
transports: ["websocket"],
|
|
1615
|
+
withCredentials: true,
|
|
1616
|
+
autoConnect: false
|
|
1617
|
+
});
|
|
1618
|
+
socket.on("connect", () => {
|
|
1619
|
+
types.logger.debug("[DAEMON] Connected to server");
|
|
1620
|
+
this.emit("connected");
|
|
1621
|
+
socket.emit("machine-connect", {
|
|
1622
|
+
token: this.token,
|
|
1623
|
+
machineIdentity: types.encodeBase64(types.encrypt(this.machineIdentity, this.secret))
|
|
1624
|
+
});
|
|
1625
|
+
this.startKeepAlive();
|
|
1626
|
+
});
|
|
1627
|
+
socket.on("disconnect", () => {
|
|
1628
|
+
types.logger.debug("[DAEMON] Disconnected from server");
|
|
1629
|
+
this.emit("disconnected");
|
|
1630
|
+
this.stopKeepAlive();
|
|
1631
|
+
});
|
|
1632
|
+
socket.on("spawn-session", async (encryptedData, callback) => {
|
|
1633
|
+
let requestData;
|
|
1634
|
+
try {
|
|
1635
|
+
requestData = types.decrypt(types.decodeBase64(encryptedData), this.secret);
|
|
1636
|
+
types.logger.debug("[DAEMON] Received spawn-session request", requestData);
|
|
1637
|
+
const args = [
|
|
1638
|
+
"--directory",
|
|
1639
|
+
requestData.directory,
|
|
1640
|
+
"--happy-starting-mode",
|
|
1641
|
+
requestData.startingMode
|
|
1642
|
+
];
|
|
1643
|
+
if (requestData.metadata) {
|
|
1644
|
+
args.push("--metadata", requestData.metadata);
|
|
1645
|
+
}
|
|
1646
|
+
if (requestData.startingMode === "interactive" && process.platform === "darwin") {
|
|
1647
|
+
const script = `
|
|
1648
|
+
tell application "Terminal"
|
|
1649
|
+
activate
|
|
1650
|
+
do script "cd ${requestData.directory} && happy ${args.join(" ")}"
|
|
1651
|
+
end tell
|
|
1652
|
+
`;
|
|
1653
|
+
child_process.spawn("osascript", ["-e", script], { detached: true });
|
|
1654
|
+
} else {
|
|
1655
|
+
const child = child_process.spawn("happy", args, {
|
|
1656
|
+
detached: true,
|
|
1657
|
+
stdio: "ignore",
|
|
1658
|
+
cwd: requestData.directory
|
|
1659
|
+
});
|
|
1660
|
+
child.unref();
|
|
1661
|
+
}
|
|
1662
|
+
const result = { success: true };
|
|
1663
|
+
socket.emit("session-spawn-result", {
|
|
1664
|
+
requestId: requestData.requestId,
|
|
1665
|
+
result: types.encodeBase64(types.encrypt(result, this.secret))
|
|
1666
|
+
});
|
|
1667
|
+
callback(types.encodeBase64(types.encrypt({ success: true }, this.secret)));
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
types.logger.debug("[DAEMON] Failed to spawn session", error);
|
|
1670
|
+
const errorResult = {
|
|
1671
|
+
success: false,
|
|
1672
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1673
|
+
};
|
|
1674
|
+
socket.emit("session-spawn-result", {
|
|
1675
|
+
requestId: requestData?.requestId || "",
|
|
1676
|
+
result: types.encodeBase64(types.encrypt(errorResult, this.secret))
|
|
1677
|
+
});
|
|
1678
|
+
callback(types.encodeBase64(types.encrypt(errorResult, this.secret)));
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
socket.on("daemon-command", (data) => {
|
|
1682
|
+
switch (data.command) {
|
|
1683
|
+
case "shutdown":
|
|
1684
|
+
this.shutdown();
|
|
1685
|
+
break;
|
|
1686
|
+
case "status":
|
|
1687
|
+
this.emit("status-request");
|
|
1688
|
+
break;
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
this.socket = socket;
|
|
1692
|
+
}
|
|
1693
|
+
startKeepAlive() {
|
|
1694
|
+
this.stopKeepAlive();
|
|
1695
|
+
this.keepAliveInterval = setInterval(() => {
|
|
1696
|
+
this.socket.volatile.emit("machine-alive", {
|
|
1697
|
+
time: Date.now()
|
|
1698
|
+
});
|
|
1699
|
+
}, 2e4);
|
|
1700
|
+
}
|
|
1701
|
+
stopKeepAlive() {
|
|
1702
|
+
if (this.keepAliveInterval) {
|
|
1703
|
+
clearInterval(this.keepAliveInterval);
|
|
1704
|
+
this.keepAliveInterval = null;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
connect() {
|
|
1708
|
+
this.socket.connect();
|
|
1709
|
+
}
|
|
1710
|
+
shutdown() {
|
|
1711
|
+
this.stopKeepAlive();
|
|
1712
|
+
this.socket.close();
|
|
1713
|
+
this.emit("shutdown");
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
async function startDaemon() {
|
|
1718
|
+
console.log("[DAEMON] Starting daemon process...");
|
|
1719
|
+
if (await isDaemonRunning()) {
|
|
1720
|
+
console.log("Happy daemon is already running");
|
|
1721
|
+
process.exit(0);
|
|
1722
|
+
}
|
|
1723
|
+
console.log("[DAEMON] Writing PID file with PID:", process.pid);
|
|
1724
|
+
writePidFile();
|
|
1725
|
+
console.log("[DAEMON] PID file written successfully");
|
|
1726
|
+
types.logger.info("Happy CLI daemon started successfully");
|
|
1727
|
+
process.on("SIGINT", () => {
|
|
1728
|
+
stopDaemon().catch(console.error);
|
|
1729
|
+
});
|
|
1730
|
+
process.on("SIGTERM", () => {
|
|
1731
|
+
stopDaemon().catch(console.error);
|
|
1732
|
+
});
|
|
1733
|
+
process.on("exit", () => {
|
|
1734
|
+
stopDaemon().catch(console.error);
|
|
1735
|
+
});
|
|
1736
|
+
try {
|
|
1737
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
1738
|
+
if (!settings.machineId) {
|
|
1739
|
+
settings.machineId = crypto.randomUUID();
|
|
1740
|
+
settings.machineHost = os$1.hostname();
|
|
1741
|
+
await writeSettings(settings);
|
|
1742
|
+
}
|
|
1743
|
+
const machineIdentity = {
|
|
1744
|
+
machineId: settings.machineId,
|
|
1745
|
+
machineHost: settings.machineHost || os$1.hostname(),
|
|
1746
|
+
platform: process.platform,
|
|
1747
|
+
version: process.env.npm_package_version || "unknown"
|
|
1748
|
+
};
|
|
1749
|
+
let credentials = await readCredentials();
|
|
1750
|
+
if (!credentials) {
|
|
1751
|
+
types.logger.debug("[DAEMON] No credentials found, running auth");
|
|
1752
|
+
await doAuth();
|
|
1753
|
+
credentials = await readCredentials();
|
|
1754
|
+
if (!credentials) {
|
|
1755
|
+
throw new Error("Failed to authenticate");
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
const { token, secret } = credentials;
|
|
1759
|
+
const daemon = new ApiDaemonSession(token, secret, machineIdentity);
|
|
1760
|
+
daemon.on("connected", () => {
|
|
1761
|
+
types.logger.debug("[DAEMON] Successfully connected to server");
|
|
1762
|
+
});
|
|
1763
|
+
daemon.on("disconnected", () => {
|
|
1764
|
+
types.logger.debug("[DAEMON] Disconnected from server");
|
|
1765
|
+
});
|
|
1766
|
+
daemon.on("shutdown", () => {
|
|
1767
|
+
types.logger.debug("[DAEMON] Shutdown requested");
|
|
1768
|
+
stopDaemon();
|
|
1769
|
+
process.exit(0);
|
|
1770
|
+
});
|
|
1771
|
+
daemon.connect();
|
|
1772
|
+
setInterval(() => {
|
|
1773
|
+
}, 1e3);
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
types.logger.debug("[DAEMON] Failed to start daemon", error);
|
|
1776
|
+
stopDaemon();
|
|
1777
|
+
process.exit(1);
|
|
1778
|
+
}
|
|
1779
|
+
process.on("SIGINT", () => process.exit(0));
|
|
1780
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
1781
|
+
process.on("exit", () => process.exit(0));
|
|
1782
|
+
while (true) {
|
|
1783
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async function isDaemonRunning() {
|
|
1787
|
+
try {
|
|
1788
|
+
console.log("[isDaemonRunning] Checking if daemon is running...");
|
|
1789
|
+
if (fs.existsSync(types.configuration.daemonPidFile)) {
|
|
1790
|
+
console.log("[isDaemonRunning] PID file exists");
|
|
1791
|
+
const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
|
|
1792
|
+
console.log("[isDaemonRunning] PID from file:", pid);
|
|
1793
|
+
try {
|
|
1794
|
+
process.kill(pid, 0);
|
|
1795
|
+
console.log("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
|
|
1796
|
+
const isHappyDaemon = await isProcessHappyDaemon(pid);
|
|
1797
|
+
console.log("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
|
|
1798
|
+
if (isHappyDaemon) {
|
|
1799
|
+
return true;
|
|
1800
|
+
} else {
|
|
1801
|
+
console.log("[isDaemonRunning] PID is not a happy daemon, cleaning up");
|
|
1802
|
+
types.logger.debug(`[DAEMON] PID ${pid} is not a happy daemon, cleaning up`);
|
|
1803
|
+
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1804
|
+
}
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
console.log("[isDaemonRunning] Process not running, cleaning up stale PID file");
|
|
1807
|
+
types.logger.debug("[DAEMON] Process not running, cleaning up stale PID file");
|
|
1808
|
+
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1809
|
+
}
|
|
1810
|
+
} else {
|
|
1811
|
+
console.log("[isDaemonRunning] No PID file found");
|
|
1812
|
+
}
|
|
1813
|
+
return false;
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
console.log("[isDaemonRunning] Error:", error);
|
|
1816
|
+
types.logger.debug("[DAEMON] Error checking daemon status", error);
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function writePidFile() {
|
|
1821
|
+
const happyDir = path.join(os$1.homedir(), ".happy");
|
|
1822
|
+
if (!fs.existsSync(happyDir)) {
|
|
1823
|
+
fs.mkdirSync(happyDir, { recursive: true });
|
|
1824
|
+
}
|
|
1825
|
+
try {
|
|
1826
|
+
fs.writeFileSync(types.configuration.daemonPidFile, process.pid.toString(), { flag: "wx" });
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
if (error.code === "EEXIST") {
|
|
1829
|
+
types.logger.debug("[DAEMON] PID file already exists, another daemon may be starting");
|
|
1830
|
+
throw new Error("Daemon PID file already exists");
|
|
1831
|
+
}
|
|
1832
|
+
throw error;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
async function stopDaemon() {
|
|
1836
|
+
try {
|
|
1837
|
+
if (fs.existsSync(types.configuration.daemonPidFile)) {
|
|
1838
|
+
const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
|
|
1839
|
+
types.logger.debug(`[DAEMON] Stopping daemon with PID ${pid}`);
|
|
1840
|
+
try {
|
|
1841
|
+
process.kill(pid, "SIGTERM");
|
|
1842
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1843
|
+
try {
|
|
1844
|
+
process.kill(pid, 0);
|
|
1845
|
+
process.kill(pid, "SIGKILL");
|
|
1846
|
+
} catch {
|
|
1847
|
+
}
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
types.logger.debug("[DAEMON] Process already dead or inaccessible", error);
|
|
1850
|
+
}
|
|
1851
|
+
fs.unlinkSync(types.configuration.daemonPidFile);
|
|
1852
|
+
}
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
types.logger.debug("[DAEMON] Error stopping daemon", error);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
async function isProcessHappyDaemon(pid) {
|
|
1858
|
+
return new Promise((resolve) => {
|
|
1859
|
+
const ps = child_process.spawn("ps", ["-p", pid.toString(), "-o", "command="]);
|
|
1860
|
+
let output = "";
|
|
1861
|
+
ps.stdout.on("data", (data) => {
|
|
1862
|
+
output += data.toString();
|
|
1863
|
+
});
|
|
1864
|
+
ps.on("close", () => {
|
|
1865
|
+
const isHappyDaemon = output.includes("daemon start") && (output.includes("happy") || output.includes("src/index"));
|
|
1866
|
+
resolve(isHappyDaemon);
|
|
1867
|
+
});
|
|
1868
|
+
ps.on("error", () => {
|
|
1869
|
+
resolve(false);
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
function trimIdent(text) {
|
|
1875
|
+
const lines = text.split("\n");
|
|
1876
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
1877
|
+
lines.shift();
|
|
1878
|
+
}
|
|
1879
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
1880
|
+
lines.pop();
|
|
1881
|
+
}
|
|
1882
|
+
const minSpaces = lines.reduce((min, line) => {
|
|
1883
|
+
if (line.trim() === "") {
|
|
1884
|
+
return min;
|
|
1885
|
+
}
|
|
1886
|
+
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
1887
|
+
return Math.min(min, leadingSpaces);
|
|
1888
|
+
}, Infinity);
|
|
1889
|
+
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
1890
|
+
return trimmedLines.join("\n");
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
1894
|
+
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
1895
|
+
const USER_HOME = process.env.HOME || process.env.USERPROFILE;
|
|
1896
|
+
async function install$1() {
|
|
1897
|
+
try {
|
|
1898
|
+
if (fs.existsSync(PLIST_FILE$1)) {
|
|
1899
|
+
types.logger.info("Daemon plist already exists. Uninstalling first...");
|
|
1900
|
+
child_process.execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1901
|
+
}
|
|
1902
|
+
const happyPath = process.argv[0];
|
|
1903
|
+
const scriptPath = process.argv[1];
|
|
1904
|
+
const plistContent = trimIdent(`
|
|
1905
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
1906
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1907
|
+
<plist version="1.0">
|
|
1908
|
+
<dict>
|
|
1909
|
+
<key>Label</key>
|
|
1910
|
+
<string>${PLIST_LABEL$1}</string>
|
|
1911
|
+
|
|
1912
|
+
<key>ProgramArguments</key>
|
|
1913
|
+
<array>
|
|
1914
|
+
<string>${happyPath}</string>
|
|
1915
|
+
<string>${scriptPath}</string>
|
|
1916
|
+
<string>happy-daemon</string>
|
|
1917
|
+
</array>
|
|
1918
|
+
|
|
1919
|
+
<key>EnvironmentVariables</key>
|
|
1920
|
+
<dict>
|
|
1921
|
+
<key>HAPPY_DAEMON_MODE</key>
|
|
1922
|
+
<string>true</string>
|
|
1923
|
+
</dict>
|
|
1924
|
+
|
|
1925
|
+
<key>RunAtLoad</key>
|
|
1926
|
+
<true/>
|
|
1927
|
+
|
|
1928
|
+
<key>KeepAlive</key>
|
|
1929
|
+
<true/>
|
|
1930
|
+
|
|
1931
|
+
<key>StandardErrorPath</key>
|
|
1932
|
+
<string>${USER_HOME}/.happy/daemon.err</string>
|
|
1933
|
+
|
|
1934
|
+
<key>StandardOutPath</key>
|
|
1935
|
+
<string>${USER_HOME}/.happy/daemon.log</string>
|
|
1936
|
+
|
|
1937
|
+
<key>WorkingDirectory</key>
|
|
1938
|
+
<string>/tmp</string>
|
|
1939
|
+
</dict>
|
|
1940
|
+
</plist>
|
|
1941
|
+
`);
|
|
1942
|
+
fs.writeFileSync(PLIST_FILE$1, plistContent);
|
|
1943
|
+
fs.chmodSync(PLIST_FILE$1, 420);
|
|
1944
|
+
types.logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
|
|
1945
|
+
child_process.execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1946
|
+
types.logger.info("Daemon installed and started successfully");
|
|
1947
|
+
types.logger.info("Check logs at ~/.happy/daemon.log");
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
types.logger.debug("Failed to install daemon:", error);
|
|
1950
|
+
throw error;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
async function install() {
|
|
1955
|
+
if (process.platform !== "darwin") {
|
|
1956
|
+
throw new Error("Daemon installation is currently only supported on macOS");
|
|
1957
|
+
}
|
|
1958
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1959
|
+
throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
|
|
1960
|
+
}
|
|
1961
|
+
types.logger.info("Installing Happy CLI daemon for macOS...");
|
|
1962
|
+
await install$1();
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
const PLIST_LABEL = "com.happy-cli.daemon";
|
|
1966
|
+
const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
|
|
1967
|
+
async function uninstall$1() {
|
|
1968
|
+
try {
|
|
1969
|
+
if (!fs.existsSync(PLIST_FILE)) {
|
|
1970
|
+
types.logger.info("Daemon plist not found. Nothing to uninstall.");
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
try {
|
|
1974
|
+
child_process.execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
|
|
1975
|
+
types.logger.info("Daemon stopped successfully");
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
types.logger.info("Failed to unload daemon (it might not be running)");
|
|
1978
|
+
}
|
|
1979
|
+
fs.unlinkSync(PLIST_FILE);
|
|
1980
|
+
types.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
|
|
1981
|
+
types.logger.info("Daemon uninstalled successfully");
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
types.logger.debug("Failed to uninstall daemon:", error);
|
|
1984
|
+
throw error;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
async function uninstall() {
|
|
1989
|
+
if (process.platform !== "darwin") {
|
|
1990
|
+
throw new Error("Daemon uninstallation is currently only supported on macOS");
|
|
1991
|
+
}
|
|
1992
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1993
|
+
throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
|
|
1994
|
+
}
|
|
1995
|
+
types.logger.info("Uninstalling Happy CLI daemon for macOS...");
|
|
1996
|
+
await uninstall$1();
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1286
1999
|
(async () => {
|
|
1287
2000
|
const args = process.argv.slice(2);
|
|
1288
2001
|
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
@@ -1302,39 +2015,40 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1302
2015
|
}
|
|
1303
2016
|
return;
|
|
1304
2017
|
} else if (subcommand === "daemon") {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
await
|
|
2018
|
+
const daemonSubcommand = args[1];
|
|
2019
|
+
if (daemonSubcommand === "start") {
|
|
2020
|
+
await startDaemon();
|
|
2021
|
+
process.exit(0);
|
|
2022
|
+
} else if (daemonSubcommand === "stop") {
|
|
2023
|
+
await stopDaemon();
|
|
2024
|
+
process.exit(0);
|
|
2025
|
+
} else if (daemonSubcommand === "install") {
|
|
2026
|
+
try {
|
|
2027
|
+
await install();
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
2030
|
+
process.exit(1);
|
|
2031
|
+
}
|
|
2032
|
+
} else if (daemonSubcommand === "uninstall") {
|
|
2033
|
+
try {
|
|
2034
|
+
await uninstall();
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
2037
|
+
process.exit(1);
|
|
2038
|
+
}
|
|
1308
2039
|
} else {
|
|
1309
|
-
|
|
1310
|
-
if (daemonSubcommand === "install") {
|
|
1311
|
-
const { install } = await Promise.resolve().then(function () { return require('./install-B2r_gX72.cjs'); });
|
|
1312
|
-
try {
|
|
1313
|
-
await install();
|
|
1314
|
-
} catch (error) {
|
|
1315
|
-
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1316
|
-
process.exit(1);
|
|
1317
|
-
}
|
|
1318
|
-
} else if (daemonSubcommand === "uninstall") {
|
|
1319
|
-
const { uninstall } = await Promise.resolve().then(function () { return require('./uninstall-C42CoSCI.cjs'); });
|
|
1320
|
-
try {
|
|
1321
|
-
await uninstall();
|
|
1322
|
-
} catch (error) {
|
|
1323
|
-
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1324
|
-
process.exit(1);
|
|
1325
|
-
}
|
|
1326
|
-
} else {
|
|
1327
|
-
console.log(`
|
|
2040
|
+
console.log(`
|
|
1328
2041
|
${chalk.bold("happy daemon")} - Daemon management
|
|
1329
2042
|
|
|
1330
2043
|
${chalk.bold("Usage:")}
|
|
2044
|
+
happy daemon start Start the daemon
|
|
2045
|
+
happy daemon stop Stop the daemon
|
|
1331
2046
|
sudo happy daemon install Install the daemon (requires sudo)
|
|
1332
2047
|
sudo happy daemon uninstall Uninstall the daemon (requires sudo)
|
|
1333
2048
|
|
|
1334
2049
|
${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
|
|
1335
2050
|
Currently only supported on macOS.
|
|
1336
2051
|
`);
|
|
1337
|
-
}
|
|
1338
2052
|
}
|
|
1339
2053
|
return;
|
|
1340
2054
|
} else {
|
|
@@ -1357,7 +2071,18 @@ Currently only supported on macOS.
|
|
|
1357
2071
|
} else if (arg === "--local") {
|
|
1358
2072
|
i++;
|
|
1359
2073
|
} else if (arg === "--happy-starting-mode") {
|
|
1360
|
-
options.startingMode = z.z.enum(["
|
|
2074
|
+
options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
|
|
2075
|
+
} else if (arg === "--claude-env") {
|
|
2076
|
+
const envVar = args[++i];
|
|
2077
|
+
const [key, value] = envVar.split("=", 2);
|
|
2078
|
+
if (!key || value === void 0) {
|
|
2079
|
+
console.error(chalk.red(`Invalid environment variable format: ${envVar}. Use KEY=VALUE`));
|
|
2080
|
+
process.exit(1);
|
|
2081
|
+
}
|
|
2082
|
+
options.claudeEnvVars = { ...options.claudeEnvVars, [key]: value };
|
|
2083
|
+
} else if (arg === "--claude-arg") {
|
|
2084
|
+
const claudeArg = args[++i];
|
|
2085
|
+
options.claudeArgs = [...options.claudeArgs || [], claudeArg];
|
|
1361
2086
|
} else {
|
|
1362
2087
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1363
2088
|
process.exit(1);
|
|
@@ -1378,20 +2103,31 @@ ${chalk.bold("Options:")}
|
|
|
1378
2103
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1379
2104
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1380
2105
|
--auth, --login Force re-authentication
|
|
2106
|
+
--claude-env KEY=VALUE Set environment variable for Claude Code
|
|
2107
|
+
--claude-arg ARG Pass additional argument to Claude CLI
|
|
2108
|
+
|
|
2109
|
+
[Daemon Management]
|
|
2110
|
+
--happy-daemon-start Start the daemon in background
|
|
2111
|
+
--happy-daemon-stop Stop the daemon
|
|
2112
|
+
--happy-daemon-install Install daemon to run on startup
|
|
2113
|
+
--happy-daemon-uninstall Uninstall daemon from startup
|
|
1381
2114
|
|
|
1382
2115
|
[Advanced]
|
|
1383
2116
|
--local < global | local >
|
|
1384
2117
|
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
1385
2118
|
You will require re-login each time you run this in a new directory.
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
Default: interactive
|
|
2119
|
+
--happy-starting-mode <interactive|remote>
|
|
2120
|
+
Set the starting mode for new sessions (default: remote)
|
|
1389
2121
|
|
|
1390
2122
|
${chalk.bold("Examples:")}
|
|
1391
2123
|
happy Start a session with default settings
|
|
1392
2124
|
happy -m opus Use Claude Opus model
|
|
1393
2125
|
happy -p plan Use plan permission mode
|
|
1394
2126
|
happy --auth Force re-authentication before starting session
|
|
2127
|
+
happy --claude-env KEY=VALUE
|
|
2128
|
+
Set environment variable for Claude Code
|
|
2129
|
+
happy --claude-arg --option
|
|
2130
|
+
Pass argument to Claude CLI
|
|
1395
2131
|
happy logout Logs out of your account and removes data directory
|
|
1396
2132
|
`);
|
|
1397
2133
|
process.exit(0);
|
|
@@ -1408,6 +2144,7 @@ ${chalk.bold("Examples:")}
|
|
|
1408
2144
|
}
|
|
1409
2145
|
credentials = res;
|
|
1410
2146
|
}
|
|
2147
|
+
await readSettings() || { };
|
|
1411
2148
|
try {
|
|
1412
2149
|
await start(credentials, options);
|
|
1413
2150
|
} catch (error) {
|