happy-coder 0.1.10 → 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 +420 -258
- package/dist/index.mjs +410 -248
- package/dist/install-B0DnBGS_.mjs +29 -0
- package/dist/install-B2r_gX72.cjs +109 -0
- package/dist/install-C809w0Cj.cjs +31 -0
- package/dist/install-DEPy62QN.mjs +97 -0
- package/dist/install-GZIzyuIE.cjs +99 -0
- package/dist/install-HKe7dyS4.mjs +107 -0
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +8 -3
- package/dist/lib.d.mts +8 -3
- package/dist/lib.mjs +1 -1
- package/dist/run-BmEaINbl.cjs +250 -0
- package/dist/run-DMbKhYfb.mjs +247 -0
- package/dist/run-FBXkmmN7.mjs +32 -0
- package/dist/run-q2To6b-c.cjs +34 -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-DnQGY77F.mjs → types-D39L8JSd.mjs} +55 -23
- package/dist/types-DYBiuNUQ.cjs +883 -0
- package/dist/types-Df5dlWLV.mjs +871 -0
- package/dist/types-fXgEaaqP.mjs +861 -0
- package/dist/{types-B2JzqUiU.cjs → types-hotUTaWz.cjs} +53 -21
- package/dist/types-mykDX2xe.cjs +872 -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-C42CoSCI.cjs +53 -0
- package/dist/uninstall-CLkTtlMv.mjs +51 -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,27 +1,27 @@
|
|
|
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 node_https = require('node:https');
|
|
19
|
-
var net = require('node:net');
|
|
20
19
|
var child_process = require('child_process');
|
|
21
20
|
var util = require('util');
|
|
22
|
-
var promises$1 = require('fs/promises');
|
|
23
21
|
var crypto = require('crypto');
|
|
24
22
|
var path = require('path');
|
|
23
|
+
var url = require('url');
|
|
24
|
+
var httpProxy = require('http-proxy');
|
|
25
25
|
var tweetnacl = require('tweetnacl');
|
|
26
26
|
var axios = require('axios');
|
|
27
27
|
var qrcode = require('qrcode-terminal');
|
|
@@ -171,9 +171,13 @@ function printDivider() {
|
|
|
171
171
|
console.log(chalk.gray("\u2550".repeat(60)));
|
|
172
172
|
}
|
|
173
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
|
+
|
|
174
179
|
function claudeCheckSession(sessionId, path) {
|
|
175
|
-
const
|
|
176
|
-
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
180
|
+
const projectDir = getProjectPath(path);
|
|
177
181
|
const sessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
178
182
|
const sessionExists = node_fs.existsSync(sessionFile);
|
|
179
183
|
if (!sessionExists) {
|
|
@@ -191,11 +195,29 @@ function claudeCheckSession(sessionId, path) {
|
|
|
191
195
|
return hasGoodMessage;
|
|
192
196
|
}
|
|
193
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
|
+
|
|
194
211
|
async function claudeRemote(opts) {
|
|
195
212
|
let startFrom = opts.sessionId;
|
|
196
213
|
if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
|
|
197
214
|
startFrom = null;
|
|
198
215
|
}
|
|
216
|
+
if (opts.claudeEnvVars) {
|
|
217
|
+
Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
|
|
218
|
+
process.env[key] = value;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
199
221
|
const abortController = new AbortController();
|
|
200
222
|
const sdkOptions = {
|
|
201
223
|
cwd: opts.path,
|
|
@@ -205,6 +227,9 @@ async function claudeRemote(opts) {
|
|
|
205
227
|
executable: "node",
|
|
206
228
|
abortController
|
|
207
229
|
};
|
|
230
|
+
if (opts.claudeArgs && opts.claudeArgs.length > 0) {
|
|
231
|
+
sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
|
|
232
|
+
}
|
|
208
233
|
let aborted = false;
|
|
209
234
|
let response;
|
|
210
235
|
opts.abort.addEventListener("abort", () => {
|
|
@@ -242,15 +267,11 @@ async function claudeRemote(opts) {
|
|
|
242
267
|
types.logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
|
|
243
268
|
formatClaudeMessage(message, opts.onAssistantResult);
|
|
244
269
|
if (message.type === "system" && message.subtype === "init") {
|
|
245
|
-
|
|
246
|
-
const projectDir =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
opts.onSessionFound(message.session_id);
|
|
251
|
-
watcher.close();
|
|
252
|
-
}
|
|
253
|
-
});
|
|
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);
|
|
254
275
|
}
|
|
255
276
|
}
|
|
256
277
|
types.logger.debug(`[claudeRemote] Finished iterating over response`);
|
|
@@ -272,10 +293,9 @@ async function claudeRemote(opts) {
|
|
|
272
293
|
types.logger.debug(`[claudeRemote] Function completed`);
|
|
273
294
|
}
|
|
274
295
|
|
|
275
|
-
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))));
|
|
276
297
|
async function claudeLocal(opts) {
|
|
277
|
-
const
|
|
278
|
-
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
298
|
+
const projectDir = getProjectPath(opts.path);
|
|
279
299
|
node_fs.mkdirSync(projectDir, { recursive: true });
|
|
280
300
|
const watcher = node_fs.watch(projectDir);
|
|
281
301
|
let resolvedSessionId = null;
|
|
@@ -309,11 +329,19 @@ async function claudeLocal(opts) {
|
|
|
309
329
|
if (startFrom) {
|
|
310
330
|
args.push("--resume", startFrom);
|
|
311
331
|
}
|
|
312
|
-
|
|
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
|
+
};
|
|
313
340
|
const child = node_child_process.spawn("node", [claudeCliPath, ...args], {
|
|
314
341
|
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
315
342
|
signal: opts.abort,
|
|
316
|
-
cwd: opts.path
|
|
343
|
+
cwd: opts.path,
|
|
344
|
+
env
|
|
317
345
|
});
|
|
318
346
|
if (child.stdio[3]) {
|
|
319
347
|
const rl = node_readline.createInterface({
|
|
@@ -538,16 +566,44 @@ class InvalidateSync {
|
|
|
538
566
|
};
|
|
539
567
|
}
|
|
540
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
|
+
|
|
541
597
|
function createSessionScanner(opts) {
|
|
542
|
-
const
|
|
543
|
-
const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
|
|
598
|
+
const projectDir = getProjectPath(opts.workingDirectory);
|
|
544
599
|
let finishedSessions = /* @__PURE__ */ new Set();
|
|
545
600
|
let pendingSessions = /* @__PURE__ */ new Set();
|
|
546
601
|
let currentSessionId = null;
|
|
547
|
-
let
|
|
602
|
+
let watchers = /* @__PURE__ */ new Map();
|
|
548
603
|
let processedMessages = /* @__PURE__ */ new Set();
|
|
549
604
|
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
550
605
|
const sync = new InvalidateSync(async () => {
|
|
606
|
+
types.logger.debug(`[SESSION_SCANNER] Syncing...`);
|
|
551
607
|
let sessions = [];
|
|
552
608
|
for (let p of pendingSessions) {
|
|
553
609
|
sessions.push(p);
|
|
@@ -559,13 +615,17 @@ function createSessionScanner(opts) {
|
|
|
559
615
|
const expectedSessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
|
|
560
616
|
let file;
|
|
561
617
|
try {
|
|
562
|
-
file = await promises.readFile(expectedSessionFile, "utf-8");
|
|
618
|
+
file = await promises$1.readFile(expectedSessionFile, "utf-8");
|
|
563
619
|
} catch (error) {
|
|
620
|
+
types.logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
|
|
564
621
|
return;
|
|
565
622
|
}
|
|
566
623
|
let lines = file.split("\n");
|
|
567
624
|
for (let l of lines) {
|
|
568
625
|
try {
|
|
626
|
+
if (l.trim() === "") {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
569
629
|
let message = JSON.parse(l);
|
|
570
630
|
let parsed = types.RawJSONLinesSchema.safeParse(message);
|
|
571
631
|
if (!parsed.success) {
|
|
@@ -588,6 +648,7 @@ function createSessionScanner(opts) {
|
|
|
588
648
|
}
|
|
589
649
|
opts.onMessage(message);
|
|
590
650
|
} catch (e) {
|
|
651
|
+
types.logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
|
|
591
652
|
continue;
|
|
592
653
|
}
|
|
593
654
|
}
|
|
@@ -601,40 +662,37 @@ function createSessionScanner(opts) {
|
|
|
601
662
|
finishedSessions.add(p);
|
|
602
663
|
}
|
|
603
664
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
try {
|
|
610
|
-
for await (const change of promises.watch(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
|
|
611
|
-
await processSessionFile(currentSessionId);
|
|
612
|
-
}
|
|
613
|
-
} catch (error) {
|
|
614
|
-
if (error.name !== "AbortError") {
|
|
615
|
-
types.logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
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
|
+
}));
|
|
618
670
|
}
|
|
619
|
-
}
|
|
671
|
+
}
|
|
620
672
|
});
|
|
673
|
+
sync.invalidate();
|
|
621
674
|
const intervalId = setInterval(() => {
|
|
622
675
|
sync.invalidate();
|
|
623
676
|
}, 3e3);
|
|
624
677
|
return {
|
|
625
|
-
refresh: () => sync.invalidate(),
|
|
626
678
|
cleanup: () => {
|
|
627
679
|
clearInterval(intervalId);
|
|
628
|
-
|
|
680
|
+
for (let w of watchers.values()) {
|
|
681
|
+
w();
|
|
682
|
+
}
|
|
683
|
+
watchers.clear();
|
|
629
684
|
},
|
|
630
685
|
onNewSession: (sessionId) => {
|
|
631
686
|
if (currentSessionId === sessionId) {
|
|
687
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
|
|
632
688
|
return;
|
|
633
689
|
}
|
|
634
690
|
if (finishedSessions.has(sessionId)) {
|
|
691
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
|
|
635
692
|
return;
|
|
636
693
|
}
|
|
637
694
|
if (pendingSessions.has(sessionId)) {
|
|
695
|
+
types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
|
|
638
696
|
return;
|
|
639
697
|
}
|
|
640
698
|
if (currentSessionId) {
|
|
@@ -679,7 +737,7 @@ function sortKeys(value) {
|
|
|
679
737
|
}
|
|
680
738
|
|
|
681
739
|
async function loop(opts) {
|
|
682
|
-
let mode = opts.startingMode ?? "
|
|
740
|
+
let mode = opts.startingMode ?? "local";
|
|
683
741
|
let currentMessageQueue = new MessageQueue();
|
|
684
742
|
let sessionId = null;
|
|
685
743
|
let onMessage = null;
|
|
@@ -703,23 +761,38 @@ async function loop(opts) {
|
|
|
703
761
|
};
|
|
704
762
|
while (true) {
|
|
705
763
|
if (currentMessageQueue.size() > 0) {
|
|
706
|
-
mode
|
|
764
|
+
if (mode !== "remote") {
|
|
765
|
+
mode = "remote";
|
|
766
|
+
if (opts.onModeChange) {
|
|
767
|
+
opts.onModeChange(mode);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
707
770
|
continue;
|
|
708
771
|
}
|
|
709
|
-
if (mode === "
|
|
772
|
+
if (mode === "local") {
|
|
710
773
|
let abortedOutside = false;
|
|
711
774
|
const interactiveAbortController = new AbortController();
|
|
712
775
|
opts.session.setHandler("switch", () => {
|
|
713
776
|
if (!interactiveAbortController.signal.aborted) {
|
|
714
777
|
abortedOutside = true;
|
|
715
|
-
mode
|
|
778
|
+
if (mode !== "remote") {
|
|
779
|
+
mode = "remote";
|
|
780
|
+
if (opts.onModeChange) {
|
|
781
|
+
opts.onModeChange(mode);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
716
784
|
interactiveAbortController.abort();
|
|
717
785
|
}
|
|
718
786
|
});
|
|
719
787
|
onMessage = () => {
|
|
720
788
|
if (!interactiveAbortController.signal.aborted) {
|
|
721
789
|
abortedOutside = true;
|
|
722
|
-
mode
|
|
790
|
+
if (mode !== "remote") {
|
|
791
|
+
mode = "remote";
|
|
792
|
+
if (opts.onModeChange) {
|
|
793
|
+
opts.onModeChange(mode);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
723
796
|
interactiveAbortController.abort();
|
|
724
797
|
}
|
|
725
798
|
onMessage = null;
|
|
@@ -728,13 +801,15 @@ async function loop(opts) {
|
|
|
728
801
|
path: opts.path,
|
|
729
802
|
sessionId,
|
|
730
803
|
onSessionFound,
|
|
731
|
-
abort: interactiveAbortController.signal
|
|
804
|
+
abort: interactiveAbortController.signal,
|
|
805
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
806
|
+
claudeArgs: opts.claudeArgs
|
|
732
807
|
});
|
|
733
808
|
onMessage = null;
|
|
734
809
|
if (!abortedOutside) {
|
|
735
810
|
return;
|
|
736
811
|
}
|
|
737
|
-
if (mode !== "
|
|
812
|
+
if (mode !== "local") {
|
|
738
813
|
console.log("Switching to remote mode...");
|
|
739
814
|
}
|
|
740
815
|
}
|
|
@@ -748,7 +823,12 @@ async function loop(opts) {
|
|
|
748
823
|
});
|
|
749
824
|
const abortHandler = () => {
|
|
750
825
|
if (!remoteAbortController.signal.aborted) {
|
|
751
|
-
mode
|
|
826
|
+
if (mode !== "local") {
|
|
827
|
+
mode = "local";
|
|
828
|
+
if (opts.onModeChange) {
|
|
829
|
+
opts.onModeChange(mode);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
752
832
|
remoteAbortController.abort();
|
|
753
833
|
}
|
|
754
834
|
if (process.stdin.isTTY) {
|
|
@@ -772,7 +852,9 @@ async function loop(opts) {
|
|
|
772
852
|
onSessionFound,
|
|
773
853
|
messages: currentMessageQueue,
|
|
774
854
|
onAssistantResult: opts.onAssistantResult,
|
|
775
|
-
interruptController: opts.interruptController
|
|
855
|
+
interruptController: opts.interruptController,
|
|
856
|
+
claudeEnvVars: opts.claudeEnvVars,
|
|
857
|
+
claudeArgs: opts.claudeArgs
|
|
776
858
|
});
|
|
777
859
|
} finally {
|
|
778
860
|
process.stdin.off("data", abortHandler);
|
|
@@ -889,159 +971,41 @@ class InterruptController {
|
|
|
889
971
|
}
|
|
890
972
|
}
|
|
891
973
|
|
|
892
|
-
var version = "0.1.
|
|
974
|
+
var version = "0.1.11";
|
|
893
975
|
var packageJson = {
|
|
894
976
|
version: version};
|
|
895
977
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const timeout = requestTimeouts.get(requestId);
|
|
904
|
-
if (timeout) {
|
|
905
|
-
clearTimeout(timeout);
|
|
906
|
-
requestTimeouts.delete(requestId);
|
|
907
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
|
|
908
|
-
claudeDidSomeWork();
|
|
909
|
-
}
|
|
910
|
-
};
|
|
911
|
-
const claudeDidSomeWork = () => {
|
|
912
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
913
|
-
if (requestTimeouts.size === 0) {
|
|
914
|
-
idleTimer = setTimeout(() => {
|
|
915
|
-
types.logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
|
|
916
|
-
onClaudeActivity("idle");
|
|
917
|
-
}, maxTimeBeforeIdle);
|
|
918
|
-
}
|
|
919
|
-
};
|
|
920
|
-
const server = node_http.createServer((req, res) => {
|
|
921
|
-
const requestId = ++requestCounter;
|
|
922
|
-
const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
|
|
923
|
-
if (isAnthropicRequest) {
|
|
924
|
-
const timeout = setTimeout(() => {
|
|
925
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
|
|
926
|
-
cleanupRequest(requestId, "timeout");
|
|
927
|
-
}, requestTimeout);
|
|
928
|
-
requestTimeouts.set(requestId, timeout);
|
|
929
|
-
onClaudeActivity("working");
|
|
930
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
|
|
931
|
-
}
|
|
932
|
-
const chunks = [];
|
|
933
|
-
req.on("data", (chunk) => {
|
|
934
|
-
chunks.push(chunk);
|
|
935
|
-
if (isAnthropicRequest) {
|
|
936
|
-
claudeDidSomeWork();
|
|
937
|
-
}
|
|
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
|
|
938
985
|
});
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
targetUrl = new node_url.URL(req.url || "/", "https://api.anthropic.com");
|
|
944
|
-
} else {
|
|
945
|
-
const protocol = req.headers["x-forwarded-proto"] || "https";
|
|
946
|
-
const host = req.headers.host || "localhost";
|
|
947
|
-
targetUrl = new node_url.URL(req.url || "/", `${protocol}://${host}`);
|
|
948
|
-
}
|
|
949
|
-
const options = {
|
|
950
|
-
hostname: targetUrl.hostname,
|
|
951
|
-
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
952
|
-
path: targetUrl.pathname + targetUrl.search,
|
|
953
|
-
method: req.method,
|
|
954
|
-
headers: {
|
|
955
|
-
...req.headers,
|
|
956
|
-
host: targetUrl.hostname
|
|
957
|
-
}
|
|
958
|
-
};
|
|
959
|
-
const requestMethod = targetUrl.protocol === "https:" ? node_https.request : node_http.request;
|
|
960
|
-
const proxyReq = requestMethod(options, (proxyRes) => {
|
|
961
|
-
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
962
|
-
proxyRes.pipe(res);
|
|
963
|
-
proxyRes.on("end", () => {
|
|
964
|
-
if (isAnthropicRequest) {
|
|
965
|
-
cleanupRequest(requestId, "completed");
|
|
966
|
-
}
|
|
967
|
-
});
|
|
968
|
-
});
|
|
969
|
-
proxyReq.on("error", (error) => {
|
|
970
|
-
if (isAnthropicRequest) {
|
|
971
|
-
cleanupRequest(requestId, `error: ${error.message}`);
|
|
972
|
-
} else {
|
|
973
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
|
|
974
|
-
}
|
|
975
|
-
res.writeHead(502);
|
|
976
|
-
res.end("Bad Gateway");
|
|
977
|
-
});
|
|
978
|
-
if (body.length > 0) {
|
|
979
|
-
proxyReq.write(body);
|
|
980
|
-
}
|
|
981
|
-
proxyReq.end();
|
|
986
|
+
let stdout = "";
|
|
987
|
+
let stderr = "";
|
|
988
|
+
child.stdout.on("data", (data) => {
|
|
989
|
+
stdout += data.toString();
|
|
982
990
|
});
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const requestId = ++requestCounter;
|
|
986
|
-
const [hostname, port] = req.url?.split(":") || ["", "443"];
|
|
987
|
-
const isAnthropicRequest = hostname === "api.anthropic.com";
|
|
988
|
-
if (isAnthropicRequest) {
|
|
989
|
-
const timeout = setTimeout(() => {
|
|
990
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
|
|
991
|
-
cleanupRequest(requestId, "timeout");
|
|
992
|
-
}, requestTimeout);
|
|
993
|
-
requestTimeouts.set(requestId, timeout);
|
|
994
|
-
onClaudeActivity("working");
|
|
995
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
|
|
996
|
-
}
|
|
997
|
-
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
|
|
998
|
-
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
999
|
-
serverSocket.write(head);
|
|
1000
|
-
serverSocket.pipe(clientSocket);
|
|
1001
|
-
clientSocket.pipe(serverSocket);
|
|
991
|
+
child.stderr.on("data", (data) => {
|
|
992
|
+
stderr += data.toString();
|
|
1002
993
|
});
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
|
|
1010
|
-
clientSocket.end();
|
|
1011
|
-
cleanup();
|
|
994
|
+
child.on("close", (code) => {
|
|
995
|
+
resolve({
|
|
996
|
+
exitCode: code || 0,
|
|
997
|
+
stdout,
|
|
998
|
+
stderr
|
|
999
|
+
});
|
|
1012
1000
|
});
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
serverSocket.on("end", cleanup);
|
|
1016
|
-
});
|
|
1017
|
-
const url = await new Promise((resolve) => {
|
|
1018
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1019
|
-
const addr = server.address();
|
|
1020
|
-
if (addr && typeof addr === "object") {
|
|
1021
|
-
resolve(`http://127.0.0.1:${addr.port}`);
|
|
1022
|
-
}
|
|
1001
|
+
child.on("error", (err) => {
|
|
1002
|
+
reject(err);
|
|
1023
1003
|
});
|
|
1024
1004
|
});
|
|
1025
|
-
types.logger.debug(`[AnthropicProxy] Started at ${url}`);
|
|
1026
|
-
return {
|
|
1027
|
-
url,
|
|
1028
|
-
cleanup: () => {
|
|
1029
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
1030
|
-
for (const [requestId, timeout] of requestTimeouts) {
|
|
1031
|
-
clearTimeout(timeout);
|
|
1032
|
-
types.logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
|
|
1033
|
-
}
|
|
1034
|
-
requestTimeouts.clear();
|
|
1035
|
-
if (requestTimeouts.size > 0) {
|
|
1036
|
-
types.logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
|
|
1037
|
-
}
|
|
1038
|
-
server.close();
|
|
1039
|
-
}
|
|
1040
|
-
};
|
|
1041
1005
|
}
|
|
1042
1006
|
|
|
1043
1007
|
const execAsync = util.promisify(child_process.exec);
|
|
1044
|
-
function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
1008
|
+
function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
|
|
1045
1009
|
session.setHandler("abort", async () => {
|
|
1046
1010
|
types.logger.info("Abort request - interrupting Claude");
|
|
1047
1011
|
await interruptController.interrupt();
|
|
@@ -1110,7 +1074,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1110
1074
|
session.setHandler("readFile", async (data) => {
|
|
1111
1075
|
types.logger.info("Read file request:", data.path);
|
|
1112
1076
|
try {
|
|
1113
|
-
const buffer = await promises
|
|
1077
|
+
const buffer = await promises.readFile(data.path);
|
|
1114
1078
|
const content = buffer.toString("base64");
|
|
1115
1079
|
return { success: true, content };
|
|
1116
1080
|
} catch (error) {
|
|
@@ -1123,7 +1087,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1123
1087
|
try {
|
|
1124
1088
|
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
1125
1089
|
try {
|
|
1126
|
-
const existingBuffer = await promises
|
|
1090
|
+
const existingBuffer = await promises.readFile(data.path);
|
|
1127
1091
|
const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
|
|
1128
1092
|
if (existingHash !== data.expectedHash) {
|
|
1129
1093
|
return {
|
|
@@ -1143,7 +1107,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1143
1107
|
}
|
|
1144
1108
|
} else {
|
|
1145
1109
|
try {
|
|
1146
|
-
await promises
|
|
1110
|
+
await promises.stat(data.path);
|
|
1147
1111
|
return {
|
|
1148
1112
|
success: false,
|
|
1149
1113
|
error: "File already exists but was expected to be new"
|
|
@@ -1156,7 +1120,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1156
1120
|
}
|
|
1157
1121
|
}
|
|
1158
1122
|
const buffer = Buffer.from(data.content, "base64");
|
|
1159
|
-
await promises
|
|
1123
|
+
await promises.writeFile(data.path, buffer);
|
|
1160
1124
|
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
1161
1125
|
return { success: true, hash };
|
|
1162
1126
|
} catch (error) {
|
|
@@ -1167,7 +1131,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1167
1131
|
session.setHandler("listDirectory", async (data) => {
|
|
1168
1132
|
types.logger.info("List directory request:", data.path);
|
|
1169
1133
|
try {
|
|
1170
|
-
const entries = await promises
|
|
1134
|
+
const entries = await promises.readdir(data.path, { withFileTypes: true });
|
|
1171
1135
|
const directoryEntries = await Promise.all(
|
|
1172
1136
|
entries.map(async (entry) => {
|
|
1173
1137
|
const fullPath = path.join(data.path, entry.name);
|
|
@@ -1180,7 +1144,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1180
1144
|
type = "file";
|
|
1181
1145
|
}
|
|
1182
1146
|
try {
|
|
1183
|
-
const stats = await promises
|
|
1147
|
+
const stats = await promises.stat(fullPath);
|
|
1184
1148
|
size = stats.size;
|
|
1185
1149
|
modified = stats.mtime.getTime();
|
|
1186
1150
|
} catch (error) {
|
|
@@ -1209,7 +1173,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1209
1173
|
types.logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1210
1174
|
async function buildTree(path$1, name, currentDepth) {
|
|
1211
1175
|
try {
|
|
1212
|
-
const stats = await promises
|
|
1176
|
+
const stats = await promises.stat(path$1);
|
|
1213
1177
|
const node = {
|
|
1214
1178
|
name,
|
|
1215
1179
|
path: path$1,
|
|
@@ -1218,7 +1182,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1218
1182
|
modified: stats.mtime.getTime()
|
|
1219
1183
|
};
|
|
1220
1184
|
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
1221
|
-
const entries = await promises
|
|
1185
|
+
const entries = await promises.readdir(path$1, { withFileTypes: true });
|
|
1222
1186
|
const children = [];
|
|
1223
1187
|
await Promise.all(
|
|
1224
1188
|
entries.map(async (entry) => {
|
|
@@ -1261,6 +1225,121 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
|
1261
1225
|
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1262
1226
|
}
|
|
1263
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
|
+
}
|
|
1260
|
+
});
|
|
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) => {
|
|
1275
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1276
|
+
const addr = server.address();
|
|
1277
|
+
if (addr && typeof addr === "object") {
|
|
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"));
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
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
|
+
}
|
|
1310
|
+
}
|
|
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
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
return proxyUrl;
|
|
1264
1343
|
}
|
|
1265
1344
|
|
|
1266
1345
|
async function start(credentials, options = {}) {
|
|
@@ -1274,22 +1353,15 @@ async function start(credentials, options = {}) {
|
|
|
1274
1353
|
const session = api.session(response);
|
|
1275
1354
|
const pushClient = api.push();
|
|
1276
1355
|
let thinking = false;
|
|
1356
|
+
let mode = "local";
|
|
1277
1357
|
let pingInterval = setInterval(() => {
|
|
1278
|
-
session.keepAlive(thinking);
|
|
1358
|
+
session.keepAlive(thinking, mode);
|
|
1279
1359
|
}, 2e3);
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
types.logger.debug(`[PING] Thinking state changed: ${thinking}`);
|
|
1286
|
-
session.keepAlive(thinking);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
);
|
|
1290
|
-
process.env.HTTP_PROXY = antropicActivityProxy.url;
|
|
1291
|
-
process.env.HTTPS_PROXY = antropicActivityProxy.url;
|
|
1292
|
-
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;
|
|
1293
1365
|
const logPath = await types.logger.logFilePathPromise;
|
|
1294
1366
|
types.logger.infoDeveloper(`Session: ${response.id}`);
|
|
1295
1367
|
types.logger.infoDeveloper(`Logs: ${logPath}`);
|
|
@@ -1301,10 +1373,10 @@ async function start(credentials, options = {}) {
|
|
|
1301
1373
|
requests.set(id, resolve);
|
|
1302
1374
|
});
|
|
1303
1375
|
let timeout = setTimeout(async () => {
|
|
1304
|
-
types.logger.
|
|
1376
|
+
types.logger.debug("Permission timeout - attempting to interrupt Claude");
|
|
1305
1377
|
const interrupted = await interruptController.interrupt();
|
|
1306
1378
|
if (interrupted) {
|
|
1307
|
-
types.logger.
|
|
1379
|
+
types.logger.debug("Claude interrupted successfully");
|
|
1308
1380
|
}
|
|
1309
1381
|
requests.delete(id);
|
|
1310
1382
|
session.updateAgentState((currentState) => {
|
|
@@ -1316,7 +1388,7 @@ async function start(credentials, options = {}) {
|
|
|
1316
1388
|
};
|
|
1317
1389
|
});
|
|
1318
1390
|
}, 1e3 * 60 * 4.5);
|
|
1319
|
-
types.logger.
|
|
1391
|
+
types.logger.debug("Permission request" + id + " " + JSON.stringify(request));
|
|
1320
1392
|
try {
|
|
1321
1393
|
await pushClient.sendToAllDevices(
|
|
1322
1394
|
"Permission Request",
|
|
@@ -1328,7 +1400,7 @@ async function start(credentials, options = {}) {
|
|
|
1328
1400
|
type: "permission_request"
|
|
1329
1401
|
}
|
|
1330
1402
|
);
|
|
1331
|
-
types.logger.
|
|
1403
|
+
types.logger.debug("Push notification sent for permission request");
|
|
1332
1404
|
} catch (error) {
|
|
1333
1405
|
types.logger.debug("Failed to send push notification:", error);
|
|
1334
1406
|
}
|
|
@@ -1370,6 +1442,15 @@ async function start(credentials, options = {}) {
|
|
|
1370
1442
|
model: options.model,
|
|
1371
1443
|
permissionMode: options.permissionMode,
|
|
1372
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
|
+
},
|
|
1373
1454
|
mcpServers: {
|
|
1374
1455
|
"permission": {
|
|
1375
1456
|
type: "http",
|
|
@@ -1379,13 +1460,11 @@ async function start(credentials, options = {}) {
|
|
|
1379
1460
|
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1380
1461
|
session,
|
|
1381
1462
|
onAssistantResult,
|
|
1382
|
-
interruptController
|
|
1463
|
+
interruptController,
|
|
1464
|
+
claudeEnvVars: options.claudeEnvVars,
|
|
1465
|
+
claudeArgs: options.claudeArgs
|
|
1383
1466
|
});
|
|
1384
1467
|
clearInterval(pingInterval);
|
|
1385
|
-
if (antropicActivityProxy) {
|
|
1386
|
-
types.logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
|
|
1387
|
-
antropicActivityProxy.cleanup();
|
|
1388
|
-
}
|
|
1389
1468
|
process.exit(0);
|
|
1390
1469
|
}
|
|
1391
1470
|
|
|
@@ -1397,7 +1476,7 @@ async function readSettings() {
|
|
|
1397
1476
|
return { ...defaultSettings };
|
|
1398
1477
|
}
|
|
1399
1478
|
try {
|
|
1400
|
-
const content = await promises.readFile(types.configuration.settingsFile, "utf8");
|
|
1479
|
+
const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
|
|
1401
1480
|
return JSON.parse(content);
|
|
1402
1481
|
} catch {
|
|
1403
1482
|
return { ...defaultSettings };
|
|
@@ -1405,9 +1484,9 @@ async function readSettings() {
|
|
|
1405
1484
|
}
|
|
1406
1485
|
async function writeSettings(settings) {
|
|
1407
1486
|
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1408
|
-
await promises.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1487
|
+
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1409
1488
|
}
|
|
1410
|
-
await promises.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1489
|
+
await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1411
1490
|
}
|
|
1412
1491
|
const credentialsSchema = z__namespace.object({
|
|
1413
1492
|
secret: z__namespace.string().base64(),
|
|
@@ -1418,7 +1497,7 @@ async function readCredentials() {
|
|
|
1418
1497
|
return null;
|
|
1419
1498
|
}
|
|
1420
1499
|
try {
|
|
1421
|
-
const keyBase64 = await promises.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1500
|
+
const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
|
|
1422
1501
|
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
1423
1502
|
return {
|
|
1424
1503
|
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
@@ -1430,9 +1509,9 @@ async function readCredentials() {
|
|
|
1430
1509
|
}
|
|
1431
1510
|
async function writeCredentials(credentials) {
|
|
1432
1511
|
if (!node_fs.existsSync(types.configuration.happyDir)) {
|
|
1433
|
-
await promises.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1512
|
+
await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
|
|
1434
1513
|
}
|
|
1435
|
-
await promises.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1514
|
+
await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
|
|
1436
1515
|
secret: types.encodeBase64(credentials.secret),
|
|
1437
1516
|
token: credentials.token
|
|
1438
1517
|
}, null, 2));
|
|
@@ -1635,17 +1714,25 @@ class ApiDaemonSession extends node_events.EventEmitter {
|
|
|
1635
1714
|
}
|
|
1636
1715
|
}
|
|
1637
1716
|
|
|
1638
|
-
const DAEMON_PID_FILE = path.join(os$1.homedir(), ".happy", "daemon-pid");
|
|
1639
1717
|
async function startDaemon() {
|
|
1640
|
-
|
|
1718
|
+
console.log("[DAEMON] Starting daemon process...");
|
|
1719
|
+
if (await isDaemonRunning()) {
|
|
1641
1720
|
console.log("Happy daemon is already running");
|
|
1642
1721
|
process.exit(0);
|
|
1643
1722
|
}
|
|
1644
|
-
|
|
1723
|
+
console.log("[DAEMON] Writing PID file with PID:", process.pid);
|
|
1645
1724
|
writePidFile();
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
process.on("
|
|
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
|
+
});
|
|
1649
1736
|
try {
|
|
1650
1737
|
const settings = await readSettings() || { onboardingCompleted: false };
|
|
1651
1738
|
if (!settings.machineId) {
|
|
@@ -1696,22 +1783,37 @@ async function startDaemon() {
|
|
|
1696
1783
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1697
1784
|
}
|
|
1698
1785
|
}
|
|
1699
|
-
function isDaemonRunning() {
|
|
1786
|
+
async function isDaemonRunning() {
|
|
1700
1787
|
try {
|
|
1701
|
-
if
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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");
|
|
1713
1812
|
}
|
|
1714
|
-
|
|
1813
|
+
return false;
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
console.log("[isDaemonRunning] Error:", error);
|
|
1816
|
+
types.logger.debug("[DAEMON] Error checking daemon status", error);
|
|
1715
1817
|
return false;
|
|
1716
1818
|
}
|
|
1717
1819
|
}
|
|
@@ -1720,19 +1822,54 @@ function writePidFile() {
|
|
|
1720
1822
|
if (!fs.existsSync(happyDir)) {
|
|
1721
1823
|
fs.mkdirSync(happyDir, { recursive: true });
|
|
1722
1824
|
}
|
|
1723
|
-
|
|
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
|
+
}
|
|
1724
1834
|
}
|
|
1725
|
-
function stopDaemon() {
|
|
1835
|
+
async function stopDaemon() {
|
|
1726
1836
|
try {
|
|
1727
|
-
if (fs.existsSync(
|
|
1728
|
-
types.
|
|
1729
|
-
|
|
1730
|
-
|
|
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);
|
|
1731
1852
|
}
|
|
1732
1853
|
} catch (error) {
|
|
1733
|
-
types.logger.debug("[DAEMON] Error
|
|
1854
|
+
types.logger.debug("[DAEMON] Error stopping daemon", error);
|
|
1734
1855
|
}
|
|
1735
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
|
+
}
|
|
1736
1873
|
|
|
1737
1874
|
function trimIdent(text) {
|
|
1738
1875
|
const lines = text.split("\n");
|
|
@@ -1934,7 +2071,18 @@ Currently only supported on macOS.
|
|
|
1934
2071
|
} else if (arg === "--local") {
|
|
1935
2072
|
i++;
|
|
1936
2073
|
} else if (arg === "--happy-starting-mode") {
|
|
1937
|
-
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];
|
|
1938
2086
|
} else {
|
|
1939
2087
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1940
2088
|
process.exit(1);
|
|
@@ -1947,6 +2095,7 @@ ${chalk.bold("happy")} - Claude Code session sharing
|
|
|
1947
2095
|
${chalk.bold("Usage:")}
|
|
1948
2096
|
happy [options]
|
|
1949
2097
|
happy logout Logs out of your account and removes data directory
|
|
2098
|
+
happy daemon Manage the background daemon (macOS only)
|
|
1950
2099
|
|
|
1951
2100
|
${chalk.bold("Options:")}
|
|
1952
2101
|
-h, --help Show this help message
|
|
@@ -1954,6 +2103,14 @@ ${chalk.bold("Options:")}
|
|
|
1954
2103
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1955
2104
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1956
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
|
|
1957
2114
|
|
|
1958
2115
|
[Advanced]
|
|
1959
2116
|
--local < global | local >
|
|
@@ -1967,6 +2124,10 @@ ${chalk.bold("Examples:")}
|
|
|
1967
2124
|
happy -m opus Use Claude Opus model
|
|
1968
2125
|
happy -p plan Use plan permission mode
|
|
1969
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
|
|
1970
2131
|
happy logout Logs out of your account and removes data directory
|
|
1971
2132
|
`);
|
|
1972
2133
|
process.exit(0);
|
|
@@ -1983,6 +2144,7 @@ ${chalk.bold("Examples:")}
|
|
|
1983
2144
|
}
|
|
1984
2145
|
credentials = res;
|
|
1985
2146
|
}
|
|
2147
|
+
await readSettings() || { };
|
|
1986
2148
|
try {
|
|
1987
2149
|
await start(credentials, options);
|
|
1988
2150
|
} catch (error) {
|