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.
Files changed (48) hide show
  1. package/README.md +2 -0
  2. package/dist/index-B2GqfEZV.cjs +1564 -0
  3. package/dist/index-QItBXhux.mjs +1540 -0
  4. package/dist/index.cjs +420 -258
  5. package/dist/index.mjs +410 -248
  6. package/dist/install-B0DnBGS_.mjs +29 -0
  7. package/dist/install-B2r_gX72.cjs +109 -0
  8. package/dist/install-C809w0Cj.cjs +31 -0
  9. package/dist/install-DEPy62QN.mjs +97 -0
  10. package/dist/install-GZIzyuIE.cjs +99 -0
  11. package/dist/install-HKe7dyS4.mjs +107 -0
  12. package/dist/lib.cjs +1 -1
  13. package/dist/lib.d.cts +8 -3
  14. package/dist/lib.d.mts +8 -3
  15. package/dist/lib.mjs +1 -1
  16. package/dist/run-BmEaINbl.cjs +250 -0
  17. package/dist/run-DMbKhYfb.mjs +247 -0
  18. package/dist/run-FBXkmmN7.mjs +32 -0
  19. package/dist/run-q2To6b-c.cjs +34 -0
  20. package/dist/types-BRICSarm.mjs +870 -0
  21. package/dist/types-BTQRfIr3.cjs +892 -0
  22. package/dist/types-CEvzGLMI.cjs +882 -0
  23. package/dist/{types-DnQGY77F.mjs → types-D39L8JSd.mjs} +55 -23
  24. package/dist/types-DYBiuNUQ.cjs +883 -0
  25. package/dist/types-Df5dlWLV.mjs +871 -0
  26. package/dist/types-fXgEaaqP.mjs +861 -0
  27. package/dist/{types-B2JzqUiU.cjs → types-hotUTaWz.cjs} +53 -21
  28. package/dist/types-mykDX2xe.cjs +872 -0
  29. package/dist/types-tLWMaptR.mjs +879 -0
  30. package/dist/uninstall-BGgl5V8F.mjs +29 -0
  31. package/dist/uninstall-BWHglipH.mjs +40 -0
  32. package/dist/uninstall-C42CoSCI.cjs +53 -0
  33. package/dist/uninstall-CLkTtlMv.mjs +51 -0
  34. package/dist/uninstall-CdHMb6wi.cjs +31 -0
  35. package/dist/uninstall-FXyyAuGU.cjs +42 -0
  36. package/package.json +8 -2
  37. package/ripgrep/COPYING +3 -0
  38. package/ripgrep/arm64-darwin/rg +0 -0
  39. package/ripgrep/arm64-darwin/ripgrep.node +0 -0
  40. package/ripgrep/arm64-linux/rg +0 -0
  41. package/ripgrep/arm64-linux/ripgrep.node +0 -0
  42. package/ripgrep/x64-darwin/rg +0 -0
  43. package/ripgrep/x64-darwin/ripgrep.node +0 -0
  44. package/ripgrep/x64-linux/rg +0 -0
  45. package/ripgrep/x64-linux/ripgrep.node +0 -0
  46. package/ripgrep/x64-win32/rg.exe +0 -0
  47. package/ripgrep/x64-win32/ripgrep.node +0 -0
  48. 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", () => {
@@ -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
- 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;
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
- 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) {
@@ -679,7 +737,7 @@ function sortKeys(value) {
679
737
  }
680
738
 
681
739
  async function loop(opts) {
682
- let mode = opts.startingMode ?? "interactive";
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 = "remote";
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 === "interactive") {
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 = "remote";
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 = "remote";
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 !== "interactive") {
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 = "interactive";
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.10";
974
+ var version = "0.1.11";
893
975
  var packageJson = {
894
976
  version: version};
895
977
 
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
- }
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
- 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();
986
+ let stdout = "";
987
+ let stderr = "";
988
+ child.stdout.on("data", (data) => {
989
+ stdout += data.toString();
982
990
  });
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);
991
+ child.stderr.on("data", (data) => {
992
+ stderr += data.toString();
1002
993
  });
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();
994
+ child.on("close", (code) => {
995
+ resolve({
996
+ exitCode: code || 0,
997
+ stdout,
998
+ stderr
999
+ });
1012
1000
  });
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
- }
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$1.readFile(data.path);
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$1.readFile(data.path);
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$1.stat(data.path);
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$1.writeFile(data.path, buffer);
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$1.readdir(data.path, { withFileTypes: true });
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$1.stat(fullPath);
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$1.stat(path$1);
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$1.readdir(path$1, { withFileTypes: true });
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 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}`);
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.info("Permission timeout - attempting to interrupt Claude");
1376
+ types.logger.debug("Permission timeout - attempting to interrupt Claude");
1305
1377
  const interrupted = await interruptController.interrupt();
1306
1378
  if (interrupted) {
1307
- types.logger.info("Claude interrupted successfully");
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.info("Permission request" + id + " " + JSON.stringify(request));
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.info("Push notification sent for permission request");
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
- if (isDaemonRunning()) {
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
- types.logger.info("Happy CLI daemon started successfully");
1723
+ console.log("[DAEMON] Writing PID file with PID:", process.pid);
1645
1724
  writePidFile();
1646
- process.on("SIGINT", stopDaemon);
1647
- process.on("SIGTERM", stopDaemon);
1648
- process.on("exit", stopDaemon);
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 (!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;
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
- } catch {
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
- fs.writeFileSync(DAEMON_PID_FILE, process.pid.toString());
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(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);
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 cleaning up PID file", 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(["interactive", "remote"]).parse(args[++i]);
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) {