happy-coder 0.1.10 → 0.1.12

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.
Files changed (50) hide show
  1. package/README.md +2 -0
  2. package/bin/happy +1 -0
  3. package/bin/happy.cmd +1 -0
  4. package/dist/index-B2GqfEZV.cjs +1564 -0
  5. package/dist/index-QItBXhux.mjs +1540 -0
  6. package/dist/index.cjs +585 -279
  7. package/dist/index.mjs +575 -269
  8. package/dist/install-B0DnBGS_.mjs +29 -0
  9. package/dist/install-B2r_gX72.cjs +109 -0
  10. package/dist/install-C809w0Cj.cjs +31 -0
  11. package/dist/install-DEPy62QN.mjs +97 -0
  12. package/dist/install-GZIzyuIE.cjs +99 -0
  13. package/dist/install-HKe7dyS4.mjs +107 -0
  14. package/dist/lib.cjs +1 -1
  15. package/dist/lib.d.cts +22 -3
  16. package/dist/lib.d.mts +22 -3
  17. package/dist/lib.mjs +1 -1
  18. package/dist/run-BmEaINbl.cjs +250 -0
  19. package/dist/run-DMbKhYfb.mjs +247 -0
  20. package/dist/run-FBXkmmN7.mjs +32 -0
  21. package/dist/run-q2To6b-c.cjs +34 -0
  22. package/dist/types-BRICSarm.mjs +870 -0
  23. package/dist/types-BTQRfIr3.cjs +892 -0
  24. package/dist/types-CEvzGLMI.cjs +882 -0
  25. package/dist/{types-DnQGY77F.mjs → types-D39L8JSd.mjs} +55 -23
  26. package/dist/types-DYBiuNUQ.cjs +883 -0
  27. package/dist/types-Df5dlWLV.mjs +871 -0
  28. package/dist/types-fXgEaaqP.mjs +861 -0
  29. package/dist/{types-B2JzqUiU.cjs → types-hotUTaWz.cjs} +53 -21
  30. package/dist/types-mykDX2xe.cjs +872 -0
  31. package/dist/types-tLWMaptR.mjs +879 -0
  32. package/dist/uninstall-BGgl5V8F.mjs +29 -0
  33. package/dist/uninstall-BWHglipH.mjs +40 -0
  34. package/dist/uninstall-C42CoSCI.cjs +53 -0
  35. package/dist/uninstall-CLkTtlMv.mjs +51 -0
  36. package/dist/uninstall-CdHMb6wi.cjs +31 -0
  37. package/dist/uninstall-FXyyAuGU.cjs +42 -0
  38. package/package.json +9 -3
  39. package/ripgrep/COPYING +3 -0
  40. package/ripgrep/arm64-darwin/rg +0 -0
  41. package/ripgrep/arm64-darwin/ripgrep.node +0 -0
  42. package/ripgrep/arm64-linux/rg +0 -0
  43. package/ripgrep/arm64-linux/ripgrep.node +0 -0
  44. package/ripgrep/x64-darwin/rg +0 -0
  45. package/ripgrep/x64-darwin/ripgrep.node +0 -0
  46. package/ripgrep/x64-linux/rg +0 -0
  47. package/ripgrep/x64-linux/ripgrep.node +0 -0
  48. package/ripgrep/x64-win32/rg.exe +0 -0
  49. package/ripgrep/x64-win32/ripgrep.node +0 -0
  50. 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-B2JzqUiU.cjs');
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 projectName = node_path.resolve(path).replace(/\//g, "-");
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", () => {
@@ -239,18 +264,14 @@ async function claudeRemote(opts) {
239
264
  try {
240
265
  types.logger.debug(`[claudeRemote] Starting to iterate over response`);
241
266
  for await (const message of response) {
242
- types.logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
267
+ types.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
243
268
  formatClaudeMessage(message, opts.onAssistantResult);
244
269
  if (message.type === "system" && message.subtype === "init") {
245
- const projectName = node_path.resolve(opts.path).replace(/\//g, "-");
246
- const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
247
- node_fs.mkdirSync(projectDir, { recursive: true });
248
- const watcher = node_fs.watch(projectDir).on("change", (_, filename) => {
249
- if (filename === `${message.session_id}.jsonl`) {
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$1 = 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))));
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 projectName = node_path.resolve(opts.path).replace(/\//g, "-");
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
- const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || node_path.resolve(node_path.join(__dirname$1, "..", "scripts", "claudeInteractiveLaunch.cjs"));
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 projectName = node_path.resolve(opts.workingDirectory).replace(/\//g, "-");
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 currentSessionWatcherAbortController = null;
548
- let processedMessages = /* @__PURE__ */ new Set();
549
- let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
602
+ let watchers = /* @__PURE__ */ new Map();
603
+ let processedMessageKeys = /* @__PURE__ */ new Set();
604
+ let unmatchedServerMessageContents = /* @__PURE__ */ new Set();
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) {
@@ -573,21 +633,22 @@ function createSessionScanner(opts) {
573
633
  continue;
574
634
  }
575
635
  let key = getMessageKey(parsed.data);
576
- if (processedMessages.has(key)) {
636
+ if (processedMessageKeys.has(key)) {
577
637
  continue;
578
638
  }
579
- processedMessages.add(key);
639
+ processedMessageKeys.add(key);
580
640
  types.logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
581
641
  types.logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
582
642
  if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
583
- const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
584
- if (currentCounter && currentCounter > 0) {
585
- seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
643
+ if (unmatchedServerMessageContents.has(parsed.data.message.content)) {
644
+ types.logger.debug(`[SESSION_SCANNER] Matched server message echo: ${parsed.data.uuid}`);
645
+ unmatchedServerMessageContents.delete(parsed.data.message.content);
586
646
  continue;
587
647
  }
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
- currentSessionWatcherAbortController?.abort();
605
- currentSessionWatcherAbortController = new AbortController();
606
- void (async () => {
607
- if (currentSessionId) {
608
- const sessionFile = node_path.join(projectDir, `${currentSessionId}.jsonl`);
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
- currentSessionWatcherAbortController?.abort();
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) {
@@ -645,13 +703,18 @@ function createSessionScanner(opts) {
645
703
  sync.invalidate();
646
704
  },
647
705
  onRemoteUserMessageForDeduplication: (messageContent) => {
648
- seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
706
+ types.logger.debug(`[SESSION_SCANNER] Adding unmatched server message content: ${messageContent.substring(0, 50)}...`);
707
+ unmatchedServerMessageContents.add(messageContent);
649
708
  }
650
709
  };
651
710
  }
652
711
  function getMessageKey(message) {
653
712
  if (message.type === "user") {
654
- return `user:${message.uuid}`;
713
+ if (Array.isArray(message.message.content) && message.message.content.length > 0 && typeof message.message.content[0] === "object" && "text" in message.message.content[0]) {
714
+ return `user-message-content:${stableStringify(message.message.content[0].text)}`;
715
+ } else {
716
+ return `user-message-content:${stableStringify(message.message.content)}`;
717
+ }
655
718
  } else if (message.type === "assistant") {
656
719
  const { usage, ...messageWithoutUsage } = message.message;
657
720
  return stableStringify(messageWithoutUsage);
@@ -663,6 +726,9 @@ function getMessageKey(message) {
663
726
  return `unknown:<error, this should be unreachable>`;
664
727
  }
665
728
  function stableStringify(obj) {
729
+ if (!obj) {
730
+ return "null";
731
+ }
666
732
  return JSON.stringify(sortKeys(obj), null, 2);
667
733
  }
668
734
  function sortKeys(value) {
@@ -679,7 +745,7 @@ function sortKeys(value) {
679
745
  }
680
746
 
681
747
  async function loop(opts) {
682
- let mode = opts.startingMode ?? "interactive";
748
+ let mode = opts.startingMode ?? "local";
683
749
  let currentMessageQueue = new MessageQueue();
684
750
  let sessionId = null;
685
751
  let onMessage = null;
@@ -703,38 +769,74 @@ async function loop(opts) {
703
769
  };
704
770
  while (true) {
705
771
  if (currentMessageQueue.size() > 0) {
706
- mode = "remote";
772
+ if (mode !== "remote") {
773
+ mode = "remote";
774
+ if (opts.onModeChange) {
775
+ opts.onModeChange(mode);
776
+ }
777
+ }
707
778
  continue;
708
779
  }
709
- if (mode === "interactive") {
780
+ if (mode === "local") {
710
781
  let abortedOutside = false;
711
782
  const interactiveAbortController = new AbortController();
712
783
  opts.session.setHandler("switch", () => {
713
784
  if (!interactiveAbortController.signal.aborted) {
714
785
  abortedOutside = true;
715
- mode = "remote";
786
+ if (mode !== "remote") {
787
+ mode = "remote";
788
+ if (opts.onModeChange) {
789
+ opts.onModeChange(mode);
790
+ }
791
+ }
716
792
  interactiveAbortController.abort();
717
793
  }
718
794
  });
795
+ opts.session.setHandler("abort", () => {
796
+ if (onMessage) {
797
+ onMessage();
798
+ }
799
+ });
719
800
  onMessage = () => {
720
801
  if (!interactiveAbortController.signal.aborted) {
721
802
  abortedOutside = true;
722
- mode = "remote";
803
+ if (mode !== "remote") {
804
+ mode = "remote";
805
+ if (opts.onModeChange) {
806
+ opts.onModeChange(mode);
807
+ }
808
+ }
809
+ opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
723
810
  interactiveAbortController.abort();
724
811
  }
725
812
  onMessage = null;
726
813
  };
727
- await claudeLocal({
728
- path: opts.path,
729
- sessionId,
730
- onSessionFound,
731
- abort: interactiveAbortController.signal
732
- });
814
+ try {
815
+ if (opts.onProcessStart) {
816
+ opts.onProcessStart("local");
817
+ }
818
+ await claudeLocal({
819
+ path: opts.path,
820
+ sessionId,
821
+ onSessionFound,
822
+ abort: interactiveAbortController.signal,
823
+ claudeEnvVars: opts.claudeEnvVars,
824
+ claudeArgs: opts.claudeArgs
825
+ });
826
+ } catch (e) {
827
+ if (!interactiveAbortController.signal.aborted) {
828
+ opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
829
+ }
830
+ } finally {
831
+ if (opts.onProcessStop) {
832
+ opts.onProcessStop("local");
833
+ }
834
+ }
733
835
  onMessage = null;
734
836
  if (!abortedOutside) {
735
837
  return;
736
838
  }
737
- if (mode !== "interactive") {
839
+ if (mode !== "local") {
738
840
  console.log("Switching to remote mode...");
739
841
  }
740
842
  }
@@ -748,7 +850,13 @@ async function loop(opts) {
748
850
  });
749
851
  const abortHandler = () => {
750
852
  if (!remoteAbortController.signal.aborted) {
751
- mode = "interactive";
853
+ if (mode !== "local") {
854
+ mode = "local";
855
+ if (opts.onModeChange) {
856
+ opts.onModeChange(mode);
857
+ }
858
+ }
859
+ opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
752
860
  remoteAbortController.abort();
753
861
  }
754
862
  if (process.stdin.isTTY) {
@@ -763,6 +871,9 @@ async function loop(opts) {
763
871
  process.stdin.on("data", abortHandler);
764
872
  try {
765
873
  types.logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
874
+ if (opts.onProcessStart) {
875
+ opts.onProcessStart("remote");
876
+ }
766
877
  await claudeRemote({
767
878
  abort: remoteAbortController.signal,
768
879
  sessionId,
@@ -772,9 +883,18 @@ async function loop(opts) {
772
883
  onSessionFound,
773
884
  messages: currentMessageQueue,
774
885
  onAssistantResult: opts.onAssistantResult,
775
- interruptController: opts.interruptController
886
+ interruptController: opts.interruptController,
887
+ claudeEnvVars: opts.claudeEnvVars,
888
+ claudeArgs: opts.claudeArgs
776
889
  });
890
+ } catch (e) {
891
+ if (!remoteAbortController.signal.aborted) {
892
+ opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
893
+ }
777
894
  } finally {
895
+ if (opts.onProcessStop) {
896
+ opts.onProcessStop("remote");
897
+ }
778
898
  process.stdin.off("data", abortHandler);
779
899
  if (process.stdin.isTTY) {
780
900
  process.stdin.setRawMode(false);
@@ -889,159 +1009,41 @@ class InterruptController {
889
1009
  }
890
1010
  }
891
1011
 
892
- var version = "0.1.10";
1012
+ var version = "0.1.12";
893
1013
  var packageJson = {
894
1014
  version: version};
895
1015
 
896
- async function startAnthropicActivityProxy(onClaudeActivity) {
897
- const requestTimeouts = /* @__PURE__ */ new Map();
898
- let requestCounter = 0;
899
- let idleTimer = null;
900
- const maxTimeBeforeIdle = 50;
901
- const requestTimeout = 5 * 60 * 1e3;
902
- const cleanupRequest = (requestId, reason) => {
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
- }
1016
+ 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))));
1017
+ const RUNNER_PATH = path.join(__dirname$1, "..", "..", "scripts", "ripgrep_launcher.cjs");
1018
+ function run(args, options) {
1019
+ return new Promise((resolve, reject) => {
1020
+ const child = child_process.spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
1021
+ stdio: ["pipe", "pipe", "pipe"],
1022
+ cwd: options?.cwd
938
1023
  });
939
- req.on("end", () => {
940
- const body = Buffer.concat(chunks);
941
- let targetUrl;
942
- if (isAnthropicRequest) {
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();
1024
+ let stdout = "";
1025
+ let stderr = "";
1026
+ child.stdout.on("data", (data) => {
1027
+ stdout += data.toString();
982
1028
  });
983
- });
984
- server.on("connect", (req, clientSocket, head) => {
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);
1029
+ child.stderr.on("data", (data) => {
1030
+ stderr += data.toString();
1002
1031
  });
1003
- const cleanup = () => {
1004
- if (isAnthropicRequest) {
1005
- cleanupRequest(requestId, "CONNECT closed");
1006
- }
1007
- };
1008
- serverSocket.on("error", (err) => {
1009
- types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
1010
- clientSocket.end();
1011
- cleanup();
1032
+ child.on("close", (code) => {
1033
+ resolve({
1034
+ exitCode: code || 0,
1035
+ stdout,
1036
+ stderr
1037
+ });
1012
1038
  });
1013
- clientSocket.on("error", cleanup);
1014
- clientSocket.on("end", cleanup);
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
- }
1039
+ child.on("error", (err) => {
1040
+ reject(err);
1023
1041
  });
1024
1042
  });
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
1043
  }
1042
1044
 
1043
1045
  const execAsync = util.promisify(child_process.exec);
1044
- function registerHandlers(session, interruptController, permissionCallbacks) {
1046
+ function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
1045
1047
  session.setHandler("abort", async () => {
1046
1048
  types.logger.info("Abort request - interrupting Claude");
1047
1049
  await interruptController.interrupt();
@@ -1063,11 +1065,22 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1063
1065
  return;
1064
1066
  }
1065
1067
  session.updateAgentState((currentState) => {
1068
+ const request = currentState.requests?.[id];
1069
+ if (!request) return currentState;
1066
1070
  let r = { ...currentState.requests };
1067
1071
  delete r[id];
1068
1072
  return {
1069
1073
  ...currentState,
1070
- requests: r
1074
+ requests: r,
1075
+ completedRequests: {
1076
+ ...currentState.completedRequests,
1077
+ [id]: {
1078
+ ...request,
1079
+ completedAt: Date.now(),
1080
+ status: message.approved ? "approved" : "denied",
1081
+ reason: message.reason
1082
+ }
1083
+ }
1071
1084
  };
1072
1085
  });
1073
1086
  });
@@ -1110,7 +1123,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1110
1123
  session.setHandler("readFile", async (data) => {
1111
1124
  types.logger.info("Read file request:", data.path);
1112
1125
  try {
1113
- const buffer = await promises$1.readFile(data.path);
1126
+ const buffer = await promises.readFile(data.path);
1114
1127
  const content = buffer.toString("base64");
1115
1128
  return { success: true, content };
1116
1129
  } catch (error) {
@@ -1123,7 +1136,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1123
1136
  try {
1124
1137
  if (data.expectedHash !== null && data.expectedHash !== void 0) {
1125
1138
  try {
1126
- const existingBuffer = await promises$1.readFile(data.path);
1139
+ const existingBuffer = await promises.readFile(data.path);
1127
1140
  const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
1128
1141
  if (existingHash !== data.expectedHash) {
1129
1142
  return {
@@ -1143,7 +1156,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1143
1156
  }
1144
1157
  } else {
1145
1158
  try {
1146
- await promises$1.stat(data.path);
1159
+ await promises.stat(data.path);
1147
1160
  return {
1148
1161
  success: false,
1149
1162
  error: "File already exists but was expected to be new"
@@ -1156,7 +1169,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1156
1169
  }
1157
1170
  }
1158
1171
  const buffer = Buffer.from(data.content, "base64");
1159
- await promises$1.writeFile(data.path, buffer);
1172
+ await promises.writeFile(data.path, buffer);
1160
1173
  const hash = crypto.createHash("sha256").update(buffer).digest("hex");
1161
1174
  return { success: true, hash };
1162
1175
  } catch (error) {
@@ -1167,7 +1180,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1167
1180
  session.setHandler("listDirectory", async (data) => {
1168
1181
  types.logger.info("List directory request:", data.path);
1169
1182
  try {
1170
- const entries = await promises$1.readdir(data.path, { withFileTypes: true });
1183
+ const entries = await promises.readdir(data.path, { withFileTypes: true });
1171
1184
  const directoryEntries = await Promise.all(
1172
1185
  entries.map(async (entry) => {
1173
1186
  const fullPath = path.join(data.path, entry.name);
@@ -1180,7 +1193,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1180
1193
  type = "file";
1181
1194
  }
1182
1195
  try {
1183
- const stats = await promises$1.stat(fullPath);
1196
+ const stats = await promises.stat(fullPath);
1184
1197
  size = stats.size;
1185
1198
  modified = stats.mtime.getTime();
1186
1199
  } catch (error) {
@@ -1209,7 +1222,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1209
1222
  types.logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1210
1223
  async function buildTree(path$1, name, currentDepth) {
1211
1224
  try {
1212
- const stats = await promises$1.stat(path$1);
1225
+ const stats = await promises.stat(path$1);
1213
1226
  const node = {
1214
1227
  name,
1215
1228
  path: path$1,
@@ -1218,7 +1231,7 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1218
1231
  modified: stats.mtime.getTime()
1219
1232
  };
1220
1233
  if (stats.isDirectory() && currentDepth < data.maxDepth) {
1221
- const entries = await promises$1.readdir(path$1, { withFileTypes: true });
1234
+ const entries = await promises.readdir(path$1, { withFileTypes: true });
1222
1235
  const children = [];
1223
1236
  await Promise.all(
1224
1237
  entries.map(async (entry) => {
@@ -1261,6 +1274,158 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1261
1274
  return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
1262
1275
  }
1263
1276
  });
1277
+ session.setHandler("ripgrep", async (data) => {
1278
+ types.logger.info("Ripgrep request with args:", data.args, "cwd:", data.cwd);
1279
+ try {
1280
+ const result = await run(data.args, { cwd: data.cwd });
1281
+ return {
1282
+ success: true,
1283
+ exitCode: result.exitCode,
1284
+ stdout: result.stdout,
1285
+ stderr: result.stderr
1286
+ };
1287
+ } catch (error) {
1288
+ types.logger.debug("Failed to run ripgrep:", error);
1289
+ return {
1290
+ success: false,
1291
+ error: error instanceof Error ? error.message : "Failed to run ripgrep"
1292
+ };
1293
+ }
1294
+ });
1295
+ }
1296
+
1297
+ async function startHTTPDirectProxy(options) {
1298
+ const proxy = httpProxy.createProxyServer({
1299
+ target: options.target,
1300
+ changeOrigin: true,
1301
+ secure: false
1302
+ });
1303
+ proxy.on("error", (err, req, res) => {
1304
+ types.logger.debug(`[HTTPProxy] Proxy error: ${err.message} for ${req.method} ${req.url}`);
1305
+ if (res instanceof node_http.ServerResponse && !res.headersSent) {
1306
+ res.writeHead(500, { "Content-Type": "text/plain" });
1307
+ res.end("Proxy error");
1308
+ }
1309
+ });
1310
+ proxy.on("proxyReq", (proxyReq, req, res) => {
1311
+ if (options.onRequest) {
1312
+ options.onRequest(req, proxyReq);
1313
+ }
1314
+ });
1315
+ proxy.on("proxyRes", (proxyRes, req, res) => {
1316
+ if (options.onResponse) {
1317
+ options.onResponse(req, proxyRes);
1318
+ }
1319
+ });
1320
+ const server = node_http.createServer((req, res) => {
1321
+ proxy.web(req, res);
1322
+ });
1323
+ const url = await new Promise((resolve, reject) => {
1324
+ server.listen(0, "127.0.0.1", () => {
1325
+ const addr = server.address();
1326
+ if (addr && typeof addr === "object") {
1327
+ const proxyUrl = `http://127.0.0.1:${addr.port}`;
1328
+ types.logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
1329
+ resolve(proxyUrl);
1330
+ } else {
1331
+ reject(new Error("Failed to get server address"));
1332
+ }
1333
+ });
1334
+ });
1335
+ return url;
1336
+ }
1337
+
1338
+ async function startClaudeActivityTracker(onThinking) {
1339
+ types.logger.debug(`[ClaudeActivityTracker] Starting activity tracker`);
1340
+ let requestCounter = 0;
1341
+ const activeRequests = /* @__PURE__ */ new Map();
1342
+ let stopThinkingTimeout = null;
1343
+ let isThinking = false;
1344
+ const REQUEST_TIMEOUT = 5 * 60 * 1e3;
1345
+ const checkAndStopThinking = () => {
1346
+ if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1347
+ stopThinkingTimeout = setTimeout(() => {
1348
+ if (isThinking && activeRequests.size === 0) {
1349
+ isThinking = false;
1350
+ onThinking(false);
1351
+ }
1352
+ stopThinkingTimeout = null;
1353
+ }, 500);
1354
+ }
1355
+ };
1356
+ const proxyUrl = await startHTTPDirectProxy({
1357
+ target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1358
+ onRequest: (req, proxyReq) => {
1359
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1360
+ const requestId = ++requestCounter;
1361
+ req._requestId = requestId;
1362
+ if (stopThinkingTimeout) {
1363
+ clearTimeout(stopThinkingTimeout);
1364
+ stopThinkingTimeout = null;
1365
+ }
1366
+ const timeout = setTimeout(() => {
1367
+ activeRequests.delete(requestId);
1368
+ checkAndStopThinking();
1369
+ }, REQUEST_TIMEOUT);
1370
+ activeRequests.set(requestId, timeout);
1371
+ if (!isThinking) {
1372
+ isThinking = true;
1373
+ onThinking(true);
1374
+ }
1375
+ }
1376
+ },
1377
+ onResponse: (req, proxyRes) => {
1378
+ proxyRes.headers["connection"] = "close";
1379
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1380
+ const requestId = req._requestId;
1381
+ const timeout = activeRequests.get(requestId);
1382
+ if (timeout) {
1383
+ clearTimeout(timeout);
1384
+ }
1385
+ let cleaned = false;
1386
+ const cleanupRequest = () => {
1387
+ if (!cleaned) {
1388
+ cleaned = true;
1389
+ activeRequests.delete(requestId);
1390
+ checkAndStopThinking();
1391
+ }
1392
+ };
1393
+ proxyRes.on("end", () => {
1394
+ cleanupRequest();
1395
+ });
1396
+ proxyRes.on("error", (err) => {
1397
+ cleanupRequest();
1398
+ });
1399
+ proxyRes.on("aborted", () => {
1400
+ cleanupRequest();
1401
+ });
1402
+ proxyRes.on("close", () => {
1403
+ cleanupRequest();
1404
+ });
1405
+ req.on("close", () => {
1406
+ cleanupRequest();
1407
+ });
1408
+ }
1409
+ }
1410
+ });
1411
+ const reset = () => {
1412
+ for (const [requestId, timeout] of activeRequests) {
1413
+ clearTimeout(timeout);
1414
+ }
1415
+ activeRequests.clear();
1416
+ if (stopThinkingTimeout) {
1417
+ clearTimeout(stopThinkingTimeout);
1418
+ stopThinkingTimeout = null;
1419
+ }
1420
+ if (isThinking) {
1421
+ isThinking = false;
1422
+ onThinking(false);
1423
+ }
1424
+ };
1425
+ return {
1426
+ proxyUrl,
1427
+ reset
1428
+ };
1264
1429
  }
1265
1430
 
1266
1431
  async function start(credentials, options = {}) {
@@ -1274,22 +1439,15 @@ async function start(credentials, options = {}) {
1274
1439
  const session = api.session(response);
1275
1440
  const pushClient = api.push();
1276
1441
  let thinking = false;
1442
+ let mode = "local";
1277
1443
  let pingInterval = setInterval(() => {
1278
- session.keepAlive(thinking);
1444
+ session.keepAlive(thinking, mode);
1279
1445
  }, 2e3);
1280
- const antropicActivityProxy = await startAnthropicActivityProxy(
1281
- (activity) => {
1282
- const newThinking = activity === "working";
1283
- if (newThinking !== thinking) {
1284
- thinking = newThinking;
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}`);
1446
+ const activityTracker = await startClaudeActivityTracker((newThinking) => {
1447
+ thinking = newThinking;
1448
+ session.keepAlive(thinking, mode);
1449
+ });
1450
+ process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
1293
1451
  const logPath = await types.logger.logFilePathPromise;
1294
1452
  types.logger.infoDeveloper(`Session: ${response.id}`);
1295
1453
  types.logger.infoDeveloper(`Logs: ${logPath}`);
@@ -1301,22 +1459,33 @@ async function start(credentials, options = {}) {
1301
1459
  requests.set(id, resolve);
1302
1460
  });
1303
1461
  let timeout = setTimeout(async () => {
1304
- types.logger.info("Permission timeout - attempting to interrupt Claude");
1462
+ types.logger.debug("Permission timeout - attempting to interrupt Claude");
1305
1463
  const interrupted = await interruptController.interrupt();
1306
1464
  if (interrupted) {
1307
- types.logger.info("Claude interrupted successfully");
1465
+ types.logger.debug("Claude interrupted successfully");
1308
1466
  }
1309
1467
  requests.delete(id);
1310
1468
  session.updateAgentState((currentState) => {
1469
+ const request2 = currentState.requests?.[id];
1470
+ if (!request2) return currentState;
1311
1471
  let r = { ...currentState.requests };
1312
1472
  delete r[id];
1313
1473
  return {
1314
1474
  ...currentState,
1315
- requests: r
1475
+ requests: r,
1476
+ completedRequests: {
1477
+ ...currentState.completedRequests,
1478
+ [id]: {
1479
+ ...request2,
1480
+ completedAt: Date.now(),
1481
+ status: "canceled",
1482
+ reason: "Timeout"
1483
+ }
1484
+ }
1316
1485
  };
1317
1486
  });
1318
1487
  }, 1e3 * 60 * 4.5);
1319
- types.logger.info("Permission request" + id + " " + JSON.stringify(request));
1488
+ types.logger.debug("Permission request" + id + " " + JSON.stringify(request));
1320
1489
  try {
1321
1490
  await pushClient.sendToAllDevices(
1322
1491
  "Permission Request",
@@ -1328,7 +1497,7 @@ async function start(credentials, options = {}) {
1328
1497
  type: "permission_request"
1329
1498
  }
1330
1499
  );
1331
- types.logger.info("Push notification sent for permission request");
1500
+ types.logger.debug("Push notification sent for permission request");
1332
1501
  } catch (error) {
1333
1502
  types.logger.debug("Failed to send push notification:", error);
1334
1503
  }
@@ -1338,7 +1507,8 @@ async function start(credentials, options = {}) {
1338
1507
  ...currentState.requests,
1339
1508
  [id]: {
1340
1509
  tool: request.name,
1341
- arguments: request.arguments
1510
+ arguments: request.arguments,
1511
+ createdAt: Date.now()
1342
1512
  }
1343
1513
  }
1344
1514
  }));
@@ -1370,6 +1540,63 @@ async function start(credentials, options = {}) {
1370
1540
  model: options.model,
1371
1541
  permissionMode: options.permissionMode,
1372
1542
  startingMode: options.startingMode,
1543
+ onModeChange: (newMode) => {
1544
+ mode = newMode;
1545
+ session.sendSessionEvent({ type: "switch", mode: newMode });
1546
+ session.keepAlive(thinking, mode);
1547
+ if (newMode === "local") {
1548
+ types.logger.debug("Switching to local mode - clearing pending permission requests");
1549
+ for (const [id, resolve] of requests) {
1550
+ types.logger.debug(`Rejecting pending permission request: ${id}`);
1551
+ resolve({ approved: false, reason: "Session switched to local mode" });
1552
+ }
1553
+ requests.clear();
1554
+ session.updateAgentState((currentState) => {
1555
+ const pendingRequests = currentState.requests || {};
1556
+ const completedRequests = { ...currentState.completedRequests };
1557
+ for (const [id, request] of Object.entries(pendingRequests)) {
1558
+ completedRequests[id] = {
1559
+ ...request,
1560
+ completedAt: Date.now(),
1561
+ status: "canceled",
1562
+ reason: "Session switched to local mode"
1563
+ };
1564
+ }
1565
+ return {
1566
+ ...currentState,
1567
+ controlledByUser: true,
1568
+ requests: {},
1569
+ // Clear all pending requests
1570
+ completedRequests
1571
+ };
1572
+ });
1573
+ } else {
1574
+ session.updateAgentState((currentState) => ({
1575
+ ...currentState,
1576
+ controlledByUser: false
1577
+ }));
1578
+ }
1579
+ },
1580
+ onProcessStart: (processMode) => {
1581
+ types.logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
1582
+ activityTracker.reset();
1583
+ types.logger.debug("Starting process - clearing any stale permission requests");
1584
+ for (const [id, resolve] of requests) {
1585
+ types.logger.debug(`Rejecting stale permission request: ${id}`);
1586
+ resolve({ approved: false, reason: "Process restarted" });
1587
+ }
1588
+ requests.clear();
1589
+ },
1590
+ onProcessStop: (processMode) => {
1591
+ types.logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
1592
+ activityTracker.reset();
1593
+ types.logger.debug("Stopping process - clearing any stale permission requests");
1594
+ for (const [id, resolve] of requests) {
1595
+ types.logger.debug(`Rejecting stale permission request: ${id}`);
1596
+ resolve({ approved: false, reason: "Process restarted" });
1597
+ }
1598
+ requests.clear();
1599
+ },
1373
1600
  mcpServers: {
1374
1601
  "permission": {
1375
1602
  type: "http",
@@ -1379,13 +1606,11 @@ async function start(credentials, options = {}) {
1379
1606
  permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
1380
1607
  session,
1381
1608
  onAssistantResult,
1382
- interruptController
1609
+ interruptController,
1610
+ claudeEnvVars: options.claudeEnvVars,
1611
+ claudeArgs: options.claudeArgs
1383
1612
  });
1384
1613
  clearInterval(pingInterval);
1385
- if (antropicActivityProxy) {
1386
- types.logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
1387
- antropicActivityProxy.cleanup();
1388
- }
1389
1614
  process.exit(0);
1390
1615
  }
1391
1616
 
@@ -1397,7 +1622,7 @@ async function readSettings() {
1397
1622
  return { ...defaultSettings };
1398
1623
  }
1399
1624
  try {
1400
- const content = await promises.readFile(types.configuration.settingsFile, "utf8");
1625
+ const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
1401
1626
  return JSON.parse(content);
1402
1627
  } catch {
1403
1628
  return { ...defaultSettings };
@@ -1405,9 +1630,9 @@ async function readSettings() {
1405
1630
  }
1406
1631
  async function writeSettings(settings) {
1407
1632
  if (!node_fs.existsSync(types.configuration.happyDir)) {
1408
- await promises.mkdir(types.configuration.happyDir, { recursive: true });
1633
+ await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
1409
1634
  }
1410
- await promises.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
1635
+ await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
1411
1636
  }
1412
1637
  const credentialsSchema = z__namespace.object({
1413
1638
  secret: z__namespace.string().base64(),
@@ -1418,7 +1643,7 @@ async function readCredentials() {
1418
1643
  return null;
1419
1644
  }
1420
1645
  try {
1421
- const keyBase64 = await promises.readFile(types.configuration.privateKeyFile, "utf8");
1646
+ const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
1422
1647
  const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
1423
1648
  return {
1424
1649
  secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
@@ -1430,9 +1655,9 @@ async function readCredentials() {
1430
1655
  }
1431
1656
  async function writeCredentials(credentials) {
1432
1657
  if (!node_fs.existsSync(types.configuration.happyDir)) {
1433
- await promises.mkdir(types.configuration.happyDir, { recursive: true });
1658
+ await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
1434
1659
  }
1435
- await promises.writeFile(types.configuration.privateKeyFile, JSON.stringify({
1660
+ await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
1436
1661
  secret: types.encodeBase64(credentials.secret),
1437
1662
  token: credentials.token
1438
1663
  }, null, 2));
@@ -1635,17 +1860,25 @@ class ApiDaemonSession extends node_events.EventEmitter {
1635
1860
  }
1636
1861
  }
1637
1862
 
1638
- const DAEMON_PID_FILE = path.join(os$1.homedir(), ".happy", "daemon-pid");
1639
1863
  async function startDaemon() {
1640
- if (isDaemonRunning()) {
1864
+ console.log("[DAEMON] Starting daemon process...");
1865
+ if (await isDaemonRunning()) {
1641
1866
  console.log("Happy daemon is already running");
1642
1867
  process.exit(0);
1643
1868
  }
1644
- types.logger.info("Happy CLI daemon started successfully");
1869
+ console.log("[DAEMON] Writing PID file with PID:", process.pid);
1645
1870
  writePidFile();
1646
- process.on("SIGINT", stopDaemon);
1647
- process.on("SIGTERM", stopDaemon);
1648
- process.on("exit", stopDaemon);
1871
+ console.log("[DAEMON] PID file written successfully");
1872
+ types.logger.info("Happy CLI daemon started successfully");
1873
+ process.on("SIGINT", () => {
1874
+ stopDaemon().catch(console.error);
1875
+ });
1876
+ process.on("SIGTERM", () => {
1877
+ stopDaemon().catch(console.error);
1878
+ });
1879
+ process.on("exit", () => {
1880
+ stopDaemon().catch(console.error);
1881
+ });
1649
1882
  try {
1650
1883
  const settings = await readSettings() || { onboardingCompleted: false };
1651
1884
  if (!settings.machineId) {
@@ -1696,22 +1929,37 @@ async function startDaemon() {
1696
1929
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1697
1930
  }
1698
1931
  }
1699
- function isDaemonRunning() {
1932
+ async function isDaemonRunning() {
1700
1933
  try {
1701
- if (!fs.existsSync(DAEMON_PID_FILE)) {
1702
- console.log("No PID file found");
1703
- return false;
1704
- }
1705
- const pid = parseInt(fs.readFileSync(DAEMON_PID_FILE, "utf-8"));
1706
- try {
1707
- process.kill(pid, 0);
1708
- return true;
1709
- } catch (error) {
1710
- console.log("Process not running", error);
1711
- fs.unlinkSync(DAEMON_PID_FILE);
1712
- return false;
1934
+ console.log("[isDaemonRunning] Checking if daemon is running...");
1935
+ if (fs.existsSync(types.configuration.daemonPidFile)) {
1936
+ console.log("[isDaemonRunning] PID file exists");
1937
+ const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
1938
+ console.log("[isDaemonRunning] PID from file:", pid);
1939
+ try {
1940
+ process.kill(pid, 0);
1941
+ console.log("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
1942
+ const isHappyDaemon = await isProcessHappyDaemon(pid);
1943
+ console.log("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
1944
+ if (isHappyDaemon) {
1945
+ return true;
1946
+ } else {
1947
+ console.log("[isDaemonRunning] PID is not a happy daemon, cleaning up");
1948
+ types.logger.debug(`[DAEMON] PID ${pid} is not a happy daemon, cleaning up`);
1949
+ fs.unlinkSync(types.configuration.daemonPidFile);
1950
+ }
1951
+ } catch (error) {
1952
+ console.log("[isDaemonRunning] Process not running, cleaning up stale PID file");
1953
+ types.logger.debug("[DAEMON] Process not running, cleaning up stale PID file");
1954
+ fs.unlinkSync(types.configuration.daemonPidFile);
1955
+ }
1956
+ } else {
1957
+ console.log("[isDaemonRunning] No PID file found");
1713
1958
  }
1714
- } catch {
1959
+ return false;
1960
+ } catch (error) {
1961
+ console.log("[isDaemonRunning] Error:", error);
1962
+ types.logger.debug("[DAEMON] Error checking daemon status", error);
1715
1963
  return false;
1716
1964
  }
1717
1965
  }
@@ -1720,19 +1968,54 @@ function writePidFile() {
1720
1968
  if (!fs.existsSync(happyDir)) {
1721
1969
  fs.mkdirSync(happyDir, { recursive: true });
1722
1970
  }
1723
- fs.writeFileSync(DAEMON_PID_FILE, process.pid.toString());
1971
+ try {
1972
+ fs.writeFileSync(types.configuration.daemonPidFile, process.pid.toString(), { flag: "wx" });
1973
+ } catch (error) {
1974
+ if (error.code === "EEXIST") {
1975
+ types.logger.debug("[DAEMON] PID file already exists, another daemon may be starting");
1976
+ throw new Error("Daemon PID file already exists");
1977
+ }
1978
+ throw error;
1979
+ }
1724
1980
  }
1725
- function stopDaemon() {
1981
+ async function stopDaemon() {
1726
1982
  try {
1727
- if (fs.existsSync(DAEMON_PID_FILE)) {
1728
- types.logger.debug("[DAEMON] Stopping daemon");
1729
- process.kill(parseInt(fs.readFileSync(DAEMON_PID_FILE, "utf-8")), "SIGTERM");
1730
- fs.unlinkSync(DAEMON_PID_FILE);
1983
+ if (fs.existsSync(types.configuration.daemonPidFile)) {
1984
+ const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
1985
+ types.logger.debug(`[DAEMON] Stopping daemon with PID ${pid}`);
1986
+ try {
1987
+ process.kill(pid, "SIGTERM");
1988
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1989
+ try {
1990
+ process.kill(pid, 0);
1991
+ process.kill(pid, "SIGKILL");
1992
+ } catch {
1993
+ }
1994
+ } catch (error) {
1995
+ types.logger.debug("[DAEMON] Process already dead or inaccessible", error);
1996
+ }
1997
+ fs.unlinkSync(types.configuration.daemonPidFile);
1731
1998
  }
1732
1999
  } catch (error) {
1733
- types.logger.debug("[DAEMON] Error cleaning up PID file", error);
2000
+ types.logger.debug("[DAEMON] Error stopping daemon", error);
1734
2001
  }
1735
2002
  }
2003
+ async function isProcessHappyDaemon(pid) {
2004
+ return new Promise((resolve) => {
2005
+ const ps = child_process.spawn("ps", ["-p", pid.toString(), "-o", "command="]);
2006
+ let output = "";
2007
+ ps.stdout.on("data", (data) => {
2008
+ output += data.toString();
2009
+ });
2010
+ ps.on("close", () => {
2011
+ const isHappyDaemon = output.includes("daemon start") && (output.includes("happy") || output.includes("src/index"));
2012
+ resolve(isHappyDaemon);
2013
+ });
2014
+ ps.on("error", () => {
2015
+ resolve(false);
2016
+ });
2017
+ });
2018
+ }
1736
2019
 
1737
2020
  function trimIdent(text) {
1738
2021
  const lines = text.split("\n");
@@ -1931,10 +2214,19 @@ Currently only supported on macOS.
1931
2214
  options.model = args[++i];
1932
2215
  } else if (arg === "-p" || arg === "--permission-mode") {
1933
2216
  options.permissionMode = z.z.enum(["auto", "default", "plan"]).parse(args[++i]);
1934
- } else if (arg === "--local") {
1935
- i++;
1936
- } else if (arg === "--happy-starting-mode") {
1937
- options.startingMode = z.z.enum(["interactive", "remote"]).parse(args[++i]);
2217
+ } else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
2218
+ options.startingMode = z.z.enum(["local", "remote"]).parse(args[++i]);
2219
+ } else if (arg === "--claude-env") {
2220
+ const envVar = args[++i];
2221
+ const [key, value] = envVar.split("=", 2);
2222
+ if (!key || value === void 0) {
2223
+ console.error(chalk.red(`Invalid environment variable format: ${envVar}. Use KEY=VALUE`));
2224
+ process.exit(1);
2225
+ }
2226
+ options.claudeEnvVars = { ...options.claudeEnvVars, [key]: value };
2227
+ } else if (arg === "--claude-arg") {
2228
+ const claudeArg = args[++i];
2229
+ options.claudeArgs = [...options.claudeArgs || [], claudeArg];
1938
2230
  } else {
1939
2231
  console.error(chalk.red(`Unknown argument: ${arg}`));
1940
2232
  process.exit(1);
@@ -1947,6 +2239,7 @@ ${chalk.bold("happy")} - Claude Code session sharing
1947
2239
  ${chalk.bold("Usage:")}
1948
2240
  happy [options]
1949
2241
  happy logout Logs out of your account and removes data directory
2242
+ happy daemon Manage the background daemon (macOS only)
1950
2243
 
1951
2244
  ${chalk.bold("Options:")}
1952
2245
  -h, --help Show this help message
@@ -1954,6 +2247,14 @@ ${chalk.bold("Options:")}
1954
2247
  -m, --model <model> Claude model to use (default: sonnet)
1955
2248
  -p, --permission-mode Permission mode: auto, default, or plan
1956
2249
  --auth, --login Force re-authentication
2250
+ --claude-env KEY=VALUE Set environment variable for Claude Code
2251
+ --claude-arg ARG Pass additional argument to Claude CLI
2252
+
2253
+ [Daemon Management]
2254
+ --happy-daemon-start Start the daemon in background
2255
+ --happy-daemon-stop Stop the daemon
2256
+ --happy-daemon-install Install daemon to run on startup
2257
+ --happy-daemon-uninstall Uninstall daemon from startup
1957
2258
 
1958
2259
  [Advanced]
1959
2260
  --local < global | local >
@@ -1967,6 +2268,10 @@ ${chalk.bold("Examples:")}
1967
2268
  happy -m opus Use Claude Opus model
1968
2269
  happy -p plan Use plan permission mode
1969
2270
  happy --auth Force re-authentication before starting session
2271
+ happy --claude-env KEY=VALUE
2272
+ Set environment variable for Claude Code
2273
+ happy --claude-arg --option
2274
+ Pass argument to Claude CLI
1970
2275
  happy logout Logs out of your account and removes data directory
1971
2276
  `);
1972
2277
  process.exit(0);
@@ -1983,6 +2288,7 @@ ${chalk.bold("Examples:")}
1983
2288
  }
1984
2289
  credentials = res;
1985
2290
  }
2291
+ await readSettings() || { };
1986
2292
  try {
1987
2293
  await start(credentials, options);
1988
2294
  } catch (error) {