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.mjs CHANGED
@@ -1,32 +1,32 @@
1
1
  import chalk from 'chalk';
2
- import { l as logger, d as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, e as encodeBase64, f as encodeBase64Url, g as decodeBase64, h as delay, j as encrypt, k as decrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DnQGY77F.mjs';
2
+ import { l as logger, d as delay, e as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, f as encodeBase64, g as encodeBase64Url, h as decodeBase64, j as encrypt, k as decrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-D39L8JSd.mjs';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
4
  import { query, AbortError } from '@anthropic-ai/claude-code';
5
5
  import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
6
- import os, { homedir } from 'node:os';
7
6
  import { resolve, join, dirname } from 'node:path';
7
+ import os, { homedir } from 'node:os';
8
+ import { access, watch as watch$1, readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
8
9
  import { spawn } from 'node:child_process';
9
10
  import { createInterface } from 'node:readline';
10
- import { fileURLToPath, URL as URL$1 } from 'node:url';
11
- import { watch as watch$1, readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
12
13
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
- import { createServer, request as request$1 } from 'node:http';
14
+ import { createServer, ServerResponse } from 'node:http';
14
15
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
15
16
  import * as z from 'zod';
16
17
  import { z as z$1 } from 'zod';
17
- import { request } from 'node:https';
18
- import net from 'node:net';
19
- import { exec, spawn as spawn$1, execSync } from 'child_process';
18
+ import { spawn as spawn$1, exec, execSync } from 'child_process';
20
19
  import { promisify } from 'util';
21
- import { readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
22
20
  import crypto, { createHash } from 'crypto';
23
- import { join as join$1 } from 'path';
21
+ import { dirname as dirname$1, join as join$1 } from 'path';
22
+ import { fileURLToPath as fileURLToPath$1 } from 'url';
23
+ import httpProxy from 'http-proxy';
24
24
  import tweetnacl from 'tweetnacl';
25
25
  import axios from 'axios';
26
26
  import qrcode from 'qrcode-terminal';
27
27
  import { EventEmitter } from 'node:events';
28
28
  import { io } from 'socket.io-client';
29
- import { homedir as homedir$1, hostname } from 'os';
29
+ import { hostname, homedir as homedir$1 } from 'os';
30
30
  import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
31
31
  import 'expo-server-sdk';
32
32
 
@@ -150,9 +150,13 @@ function printDivider() {
150
150
  console.log(chalk.gray("\u2550".repeat(60)));
151
151
  }
152
152
 
153
+ function getProjectPath(workingDirectory) {
154
+ const projectId = resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
155
+ return join(homedir(), ".claude", "projects", projectId);
156
+ }
157
+
153
158
  function claudeCheckSession(sessionId, path) {
154
- const projectName = resolve(path).replace(/\//g, "-");
155
- const projectDir = join(homedir(), ".claude", "projects", projectName);
159
+ const projectDir = getProjectPath(path);
156
160
  const sessionFile = join(projectDir, `${sessionId}.jsonl`);
157
161
  const sessionExists = existsSync(sessionFile);
158
162
  if (!sessionExists) {
@@ -170,11 +174,29 @@ function claudeCheckSession(sessionId, path) {
170
174
  return hasGoodMessage;
171
175
  }
172
176
 
177
+ async function awaitFileExist(file, timeout = 1e4) {
178
+ const startTime = Date.now();
179
+ while (Date.now() - startTime < timeout) {
180
+ try {
181
+ await access(file);
182
+ return true;
183
+ } catch (e) {
184
+ await delay(1e3);
185
+ }
186
+ }
187
+ return false;
188
+ }
189
+
173
190
  async function claudeRemote(opts) {
174
191
  let startFrom = opts.sessionId;
175
192
  if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
176
193
  startFrom = null;
177
194
  }
195
+ if (opts.claudeEnvVars) {
196
+ Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
197
+ process.env[key] = value;
198
+ });
199
+ }
178
200
  const abortController = new AbortController();
179
201
  const sdkOptions = {
180
202
  cwd: opts.path,
@@ -184,6 +206,9 @@ async function claudeRemote(opts) {
184
206
  executable: "node",
185
207
  abortController
186
208
  };
209
+ if (opts.claudeArgs && opts.claudeArgs.length > 0) {
210
+ sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
211
+ }
187
212
  let aborted = false;
188
213
  let response;
189
214
  opts.abort.addEventListener("abort", () => {
@@ -218,18 +243,14 @@ async function claudeRemote(opts) {
218
243
  try {
219
244
  logger.debug(`[claudeRemote] Starting to iterate over response`);
220
245
  for await (const message of response) {
221
- logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
246
+ logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
222
247
  formatClaudeMessage(message, opts.onAssistantResult);
223
248
  if (message.type === "system" && message.subtype === "init") {
224
- const projectName = resolve(opts.path).replace(/\//g, "-");
225
- const projectDir = join(homedir(), ".claude", "projects", projectName);
226
- mkdirSync(projectDir, { recursive: true });
227
- const watcher = watch(projectDir).on("change", (_, filename) => {
228
- if (filename === `${message.session_id}.jsonl`) {
229
- opts.onSessionFound(message.session_id);
230
- watcher.close();
231
- }
232
- });
249
+ logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
250
+ const projectDir = getProjectPath(opts.path);
251
+ const found = await awaitFileExist(join(projectDir, `${message.session_id}.jsonl`));
252
+ logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
253
+ opts.onSessionFound(message.session_id);
233
254
  }
234
255
  }
235
256
  logger.debug(`[claudeRemote] Finished iterating over response`);
@@ -251,10 +272,9 @@ async function claudeRemote(opts) {
251
272
  logger.debug(`[claudeRemote] Function completed`);
252
273
  }
253
274
 
254
- const __dirname = dirname(fileURLToPath(import.meta.url));
275
+ const __dirname$1 = dirname(fileURLToPath(import.meta.url));
255
276
  async function claudeLocal(opts) {
256
- const projectName = resolve(opts.path).replace(/\//g, "-");
257
- const projectDir = join(homedir(), ".claude", "projects", projectName);
277
+ const projectDir = getProjectPath(opts.path);
258
278
  mkdirSync(projectDir, { recursive: true });
259
279
  const watcher = watch(projectDir);
260
280
  let resolvedSessionId = null;
@@ -288,11 +308,19 @@ async function claudeLocal(opts) {
288
308
  if (startFrom) {
289
309
  args.push("--resume", startFrom);
290
310
  }
291
- const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || resolve(join(__dirname, "..", "scripts", "claudeInteractiveLaunch.cjs"));
311
+ if (opts.claudeArgs) {
312
+ args.push(...opts.claudeArgs);
313
+ }
314
+ const claudeCliPath = process.env.HAPPY_CLAUDE_CLI_PATH || resolve(join(__dirname$1, "..", "scripts", "claudeInteractiveLaunch.cjs"));
315
+ const env = {
316
+ ...process.env,
317
+ ...opts.claudeEnvVars
318
+ };
292
319
  const child = spawn("node", [claudeCliPath, ...args], {
293
320
  stdio: ["inherit", "inherit", "inherit", "pipe"],
294
321
  signal: opts.abort,
295
- cwd: opts.path
322
+ cwd: opts.path,
323
+ env
296
324
  });
297
325
  if (child.stdio[3]) {
298
326
  const rl = createInterface({
@@ -517,16 +545,44 @@ class InvalidateSync {
517
545
  };
518
546
  }
519
547
 
548
+ function startFileWatcher(file, onFileChange) {
549
+ const abortController = new AbortController();
550
+ void (async () => {
551
+ while (true) {
552
+ try {
553
+ logger.debug(`[FILE_WATCHER] Starting watcher for ${file}`);
554
+ const watcher = watch$1(file, { persistent: true, signal: abortController.signal });
555
+ for await (const event of watcher) {
556
+ if (abortController.signal.aborted) {
557
+ return;
558
+ }
559
+ logger.debug(`[FILE_WATCHER] File changed: ${file}`);
560
+ onFileChange(file);
561
+ }
562
+ } catch (e) {
563
+ if (abortController.signal.aborted) {
564
+ return;
565
+ }
566
+ logger.debug(`[FILE_WATCHER] Watch error: ${e.message}, restarting watcher in a second`);
567
+ await delay(1e3);
568
+ }
569
+ }
570
+ })();
571
+ return () => {
572
+ abortController.abort();
573
+ };
574
+ }
575
+
520
576
  function createSessionScanner(opts) {
521
- const projectName = resolve(opts.workingDirectory).replace(/\//g, "-");
522
- const projectDir = join(homedir(), ".claude", "projects", projectName);
577
+ const projectDir = getProjectPath(opts.workingDirectory);
523
578
  let finishedSessions = /* @__PURE__ */ new Set();
524
579
  let pendingSessions = /* @__PURE__ */ new Set();
525
580
  let currentSessionId = null;
526
- let currentSessionWatcherAbortController = null;
527
- let processedMessages = /* @__PURE__ */ new Set();
528
- let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
581
+ let watchers = /* @__PURE__ */ new Map();
582
+ let processedMessageKeys = /* @__PURE__ */ new Set();
583
+ let unmatchedServerMessageContents = /* @__PURE__ */ new Set();
529
584
  const sync = new InvalidateSync(async () => {
585
+ logger.debug(`[SESSION_SCANNER] Syncing...`);
530
586
  let sessions = [];
531
587
  for (let p of pendingSessions) {
532
588
  sessions.push(p);
@@ -540,11 +596,15 @@ function createSessionScanner(opts) {
540
596
  try {
541
597
  file = await readFile(expectedSessionFile, "utf-8");
542
598
  } catch (error) {
599
+ logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
543
600
  return;
544
601
  }
545
602
  let lines = file.split("\n");
546
603
  for (let l of lines) {
547
604
  try {
605
+ if (l.trim() === "") {
606
+ continue;
607
+ }
548
608
  let message = JSON.parse(l);
549
609
  let parsed = RawJSONLinesSchema.safeParse(message);
550
610
  if (!parsed.success) {
@@ -552,21 +612,22 @@ function createSessionScanner(opts) {
552
612
  continue;
553
613
  }
554
614
  let key = getMessageKey(parsed.data);
555
- if (processedMessages.has(key)) {
615
+ if (processedMessageKeys.has(key)) {
556
616
  continue;
557
617
  }
558
- processedMessages.add(key);
618
+ processedMessageKeys.add(key);
559
619
  logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
560
620
  logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
561
621
  if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
562
- const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
563
- if (currentCounter && currentCounter > 0) {
564
- seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
622
+ if (unmatchedServerMessageContents.has(parsed.data.message.content)) {
623
+ logger.debug(`[SESSION_SCANNER] Matched server message echo: ${parsed.data.uuid}`);
624
+ unmatchedServerMessageContents.delete(parsed.data.message.content);
565
625
  continue;
566
626
  }
567
627
  }
568
628
  opts.onMessage(message);
569
629
  } catch (e) {
630
+ logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
570
631
  continue;
571
632
  }
572
633
  }
@@ -580,40 +641,37 @@ function createSessionScanner(opts) {
580
641
  finishedSessions.add(p);
581
642
  }
582
643
  }
583
- currentSessionWatcherAbortController?.abort();
584
- currentSessionWatcherAbortController = new AbortController();
585
- void (async () => {
586
- if (currentSessionId) {
587
- const sessionFile = join(projectDir, `${currentSessionId}.jsonl`);
588
- try {
589
- for await (const change of watch$1(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
590
- await processSessionFile(currentSessionId);
591
- }
592
- } catch (error) {
593
- if (error.name !== "AbortError") {
594
- logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
595
- }
596
- }
644
+ for (let p of sessions) {
645
+ if (!watchers.has(p)) {
646
+ watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
647
+ sync.invalidate();
648
+ }));
597
649
  }
598
- })();
650
+ }
599
651
  });
652
+ sync.invalidate();
600
653
  const intervalId = setInterval(() => {
601
654
  sync.invalidate();
602
655
  }, 3e3);
603
656
  return {
604
- refresh: () => sync.invalidate(),
605
657
  cleanup: () => {
606
658
  clearInterval(intervalId);
607
- currentSessionWatcherAbortController?.abort();
659
+ for (let w of watchers.values()) {
660
+ w();
661
+ }
662
+ watchers.clear();
608
663
  },
609
664
  onNewSession: (sessionId) => {
610
665
  if (currentSessionId === sessionId) {
666
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
611
667
  return;
612
668
  }
613
669
  if (finishedSessions.has(sessionId)) {
670
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
614
671
  return;
615
672
  }
616
673
  if (pendingSessions.has(sessionId)) {
674
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
617
675
  return;
618
676
  }
619
677
  if (currentSessionId) {
@@ -624,13 +682,18 @@ function createSessionScanner(opts) {
624
682
  sync.invalidate();
625
683
  },
626
684
  onRemoteUserMessageForDeduplication: (messageContent) => {
627
- seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
685
+ logger.debug(`[SESSION_SCANNER] Adding unmatched server message content: ${messageContent.substring(0, 50)}...`);
686
+ unmatchedServerMessageContents.add(messageContent);
628
687
  }
629
688
  };
630
689
  }
631
690
  function getMessageKey(message) {
632
691
  if (message.type === "user") {
633
- return `user:${message.uuid}`;
692
+ if (Array.isArray(message.message.content) && message.message.content.length > 0 && typeof message.message.content[0] === "object" && "text" in message.message.content[0]) {
693
+ return `user-message-content:${stableStringify(message.message.content[0].text)}`;
694
+ } else {
695
+ return `user-message-content:${stableStringify(message.message.content)}`;
696
+ }
634
697
  } else if (message.type === "assistant") {
635
698
  const { usage, ...messageWithoutUsage } = message.message;
636
699
  return stableStringify(messageWithoutUsage);
@@ -642,6 +705,9 @@ function getMessageKey(message) {
642
705
  return `unknown:<error, this should be unreachable>`;
643
706
  }
644
707
  function stableStringify(obj) {
708
+ if (!obj) {
709
+ return "null";
710
+ }
645
711
  return JSON.stringify(sortKeys(obj), null, 2);
646
712
  }
647
713
  function sortKeys(value) {
@@ -658,7 +724,7 @@ function sortKeys(value) {
658
724
  }
659
725
 
660
726
  async function loop(opts) {
661
- let mode = opts.startingMode ?? "interactive";
727
+ let mode = opts.startingMode ?? "local";
662
728
  let currentMessageQueue = new MessageQueue();
663
729
  let sessionId = null;
664
730
  let onMessage = null;
@@ -682,38 +748,74 @@ async function loop(opts) {
682
748
  };
683
749
  while (true) {
684
750
  if (currentMessageQueue.size() > 0) {
685
- mode = "remote";
751
+ if (mode !== "remote") {
752
+ mode = "remote";
753
+ if (opts.onModeChange) {
754
+ opts.onModeChange(mode);
755
+ }
756
+ }
686
757
  continue;
687
758
  }
688
- if (mode === "interactive") {
759
+ if (mode === "local") {
689
760
  let abortedOutside = false;
690
761
  const interactiveAbortController = new AbortController();
691
762
  opts.session.setHandler("switch", () => {
692
763
  if (!interactiveAbortController.signal.aborted) {
693
764
  abortedOutside = true;
694
- mode = "remote";
765
+ if (mode !== "remote") {
766
+ mode = "remote";
767
+ if (opts.onModeChange) {
768
+ opts.onModeChange(mode);
769
+ }
770
+ }
695
771
  interactiveAbortController.abort();
696
772
  }
697
773
  });
774
+ opts.session.setHandler("abort", () => {
775
+ if (onMessage) {
776
+ onMessage();
777
+ }
778
+ });
698
779
  onMessage = () => {
699
780
  if (!interactiveAbortController.signal.aborted) {
700
781
  abortedOutside = true;
701
- mode = "remote";
782
+ if (mode !== "remote") {
783
+ mode = "remote";
784
+ if (opts.onModeChange) {
785
+ opts.onModeChange(mode);
786
+ }
787
+ }
788
+ opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
702
789
  interactiveAbortController.abort();
703
790
  }
704
791
  onMessage = null;
705
792
  };
706
- await claudeLocal({
707
- path: opts.path,
708
- sessionId,
709
- onSessionFound,
710
- abort: interactiveAbortController.signal
711
- });
793
+ try {
794
+ if (opts.onProcessStart) {
795
+ opts.onProcessStart("local");
796
+ }
797
+ await claudeLocal({
798
+ path: opts.path,
799
+ sessionId,
800
+ onSessionFound,
801
+ abort: interactiveAbortController.signal,
802
+ claudeEnvVars: opts.claudeEnvVars,
803
+ claudeArgs: opts.claudeArgs
804
+ });
805
+ } catch (e) {
806
+ if (!interactiveAbortController.signal.aborted) {
807
+ opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
808
+ }
809
+ } finally {
810
+ if (opts.onProcessStop) {
811
+ opts.onProcessStop("local");
812
+ }
813
+ }
712
814
  onMessage = null;
713
815
  if (!abortedOutside) {
714
816
  return;
715
817
  }
716
- if (mode !== "interactive") {
818
+ if (mode !== "local") {
717
819
  console.log("Switching to remote mode...");
718
820
  }
719
821
  }
@@ -727,7 +829,13 @@ async function loop(opts) {
727
829
  });
728
830
  const abortHandler = () => {
729
831
  if (!remoteAbortController.signal.aborted) {
730
- mode = "interactive";
832
+ if (mode !== "local") {
833
+ mode = "local";
834
+ if (opts.onModeChange) {
835
+ opts.onModeChange(mode);
836
+ }
837
+ }
838
+ opts.session.sendSessionEvent({ type: "message", message: "Inference aborted" });
731
839
  remoteAbortController.abort();
732
840
  }
733
841
  if (process.stdin.isTTY) {
@@ -742,6 +850,9 @@ async function loop(opts) {
742
850
  process.stdin.on("data", abortHandler);
743
851
  try {
744
852
  logger.debug(`Starting claudeRemote with messages: ${currentMessageQueue.size()}`);
853
+ if (opts.onProcessStart) {
854
+ opts.onProcessStart("remote");
855
+ }
745
856
  await claudeRemote({
746
857
  abort: remoteAbortController.signal,
747
858
  sessionId,
@@ -751,9 +862,18 @@ async function loop(opts) {
751
862
  onSessionFound,
752
863
  messages: currentMessageQueue,
753
864
  onAssistantResult: opts.onAssistantResult,
754
- interruptController: opts.interruptController
865
+ interruptController: opts.interruptController,
866
+ claudeEnvVars: opts.claudeEnvVars,
867
+ claudeArgs: opts.claudeArgs
755
868
  });
869
+ } catch (e) {
870
+ if (!remoteAbortController.signal.aborted) {
871
+ opts.session.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
872
+ }
756
873
  } finally {
874
+ if (opts.onProcessStop) {
875
+ opts.onProcessStop("remote");
876
+ }
757
877
  process.stdin.off("data", abortHandler);
758
878
  if (process.stdin.isTTY) {
759
879
  process.stdin.setRawMode(false);
@@ -868,159 +988,41 @@ class InterruptController {
868
988
  }
869
989
  }
870
990
 
871
- var version = "0.1.10";
991
+ var version = "0.1.12";
872
992
  var packageJson = {
873
993
  version: version};
874
994
 
875
- async function startAnthropicActivityProxy(onClaudeActivity) {
876
- const requestTimeouts = /* @__PURE__ */ new Map();
877
- let requestCounter = 0;
878
- let idleTimer = null;
879
- const maxTimeBeforeIdle = 50;
880
- const requestTimeout = 5 * 60 * 1e3;
881
- const cleanupRequest = (requestId, reason) => {
882
- const timeout = requestTimeouts.get(requestId);
883
- if (timeout) {
884
- clearTimeout(timeout);
885
- requestTimeouts.delete(requestId);
886
- logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
887
- claudeDidSomeWork();
888
- }
889
- };
890
- const claudeDidSomeWork = () => {
891
- if (idleTimer) clearTimeout(idleTimer);
892
- if (requestTimeouts.size === 0) {
893
- idleTimer = setTimeout(() => {
894
- logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
895
- onClaudeActivity("idle");
896
- }, maxTimeBeforeIdle);
897
- }
898
- };
899
- const server = createServer((req, res) => {
900
- const requestId = ++requestCounter;
901
- const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
902
- if (isAnthropicRequest) {
903
- const timeout = setTimeout(() => {
904
- logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
905
- cleanupRequest(requestId, "timeout");
906
- }, requestTimeout);
907
- requestTimeouts.set(requestId, timeout);
908
- onClaudeActivity("working");
909
- logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
910
- }
911
- const chunks = [];
912
- req.on("data", (chunk) => {
913
- chunks.push(chunk);
914
- if (isAnthropicRequest) {
915
- claudeDidSomeWork();
916
- }
995
+ const __dirname = dirname$1(fileURLToPath$1(import.meta.url));
996
+ const RUNNER_PATH = join$1(__dirname, "..", "..", "scripts", "ripgrep_launcher.cjs");
997
+ function run(args, options) {
998
+ return new Promise((resolve, reject) => {
999
+ const child = spawn$1("node", [RUNNER_PATH, JSON.stringify(args)], {
1000
+ stdio: ["pipe", "pipe", "pipe"],
1001
+ cwd: options?.cwd
917
1002
  });
918
- req.on("end", () => {
919
- const body = Buffer.concat(chunks);
920
- let targetUrl;
921
- if (isAnthropicRequest) {
922
- targetUrl = new URL$1(req.url || "/", "https://api.anthropic.com");
923
- } else {
924
- const protocol = req.headers["x-forwarded-proto"] || "https";
925
- const host = req.headers.host || "localhost";
926
- targetUrl = new URL$1(req.url || "/", `${protocol}://${host}`);
927
- }
928
- const options = {
929
- hostname: targetUrl.hostname,
930
- port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
931
- path: targetUrl.pathname + targetUrl.search,
932
- method: req.method,
933
- headers: {
934
- ...req.headers,
935
- host: targetUrl.hostname
936
- }
937
- };
938
- const requestMethod = targetUrl.protocol === "https:" ? request : request$1;
939
- const proxyReq = requestMethod(options, (proxyRes) => {
940
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
941
- proxyRes.pipe(res);
942
- proxyRes.on("end", () => {
943
- if (isAnthropicRequest) {
944
- cleanupRequest(requestId, "completed");
945
- }
946
- });
947
- });
948
- proxyReq.on("error", (error) => {
949
- if (isAnthropicRequest) {
950
- cleanupRequest(requestId, `error: ${error.message}`);
951
- } else {
952
- logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
953
- }
954
- res.writeHead(502);
955
- res.end("Bad Gateway");
956
- });
957
- if (body.length > 0) {
958
- proxyReq.write(body);
959
- }
960
- proxyReq.end();
1003
+ let stdout = "";
1004
+ let stderr = "";
1005
+ child.stdout.on("data", (data) => {
1006
+ stdout += data.toString();
961
1007
  });
962
- });
963
- server.on("connect", (req, clientSocket, head) => {
964
- const requestId = ++requestCounter;
965
- const [hostname, port] = req.url?.split(":") || ["", "443"];
966
- const isAnthropicRequest = hostname === "api.anthropic.com";
967
- if (isAnthropicRequest) {
968
- const timeout = setTimeout(() => {
969
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
970
- cleanupRequest(requestId, "timeout");
971
- }, requestTimeout);
972
- requestTimeouts.set(requestId, timeout);
973
- onClaudeActivity("working");
974
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
975
- }
976
- const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
977
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
978
- serverSocket.write(head);
979
- serverSocket.pipe(clientSocket);
980
- clientSocket.pipe(serverSocket);
1008
+ child.stderr.on("data", (data) => {
1009
+ stderr += data.toString();
981
1010
  });
982
- const cleanup = () => {
983
- if (isAnthropicRequest) {
984
- cleanupRequest(requestId, "CONNECT closed");
985
- }
986
- };
987
- serverSocket.on("error", (err) => {
988
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
989
- clientSocket.end();
990
- cleanup();
1011
+ child.on("close", (code) => {
1012
+ resolve({
1013
+ exitCode: code || 0,
1014
+ stdout,
1015
+ stderr
1016
+ });
991
1017
  });
992
- clientSocket.on("error", cleanup);
993
- clientSocket.on("end", cleanup);
994
- serverSocket.on("end", cleanup);
995
- });
996
- const url = await new Promise((resolve) => {
997
- server.listen(0, "127.0.0.1", () => {
998
- const addr = server.address();
999
- if (addr && typeof addr === "object") {
1000
- resolve(`http://127.0.0.1:${addr.port}`);
1001
- }
1018
+ child.on("error", (err) => {
1019
+ reject(err);
1002
1020
  });
1003
1021
  });
1004
- logger.debug(`[AnthropicProxy] Started at ${url}`);
1005
- return {
1006
- url,
1007
- cleanup: () => {
1008
- if (idleTimer) clearTimeout(idleTimer);
1009
- for (const [requestId, timeout] of requestTimeouts) {
1010
- clearTimeout(timeout);
1011
- logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
1012
- }
1013
- requestTimeouts.clear();
1014
- if (requestTimeouts.size > 0) {
1015
- logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
1016
- }
1017
- server.close();
1018
- }
1019
- };
1020
1022
  }
1021
1023
 
1022
1024
  const execAsync = promisify(exec);
1023
- function registerHandlers(session, interruptController, permissionCallbacks) {
1025
+ function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
1024
1026
  session.setHandler("abort", async () => {
1025
1027
  logger.info("Abort request - interrupting Claude");
1026
1028
  await interruptController.interrupt();
@@ -1042,11 +1044,22 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1042
1044
  return;
1043
1045
  }
1044
1046
  session.updateAgentState((currentState) => {
1047
+ const request = currentState.requests?.[id];
1048
+ if (!request) return currentState;
1045
1049
  let r = { ...currentState.requests };
1046
1050
  delete r[id];
1047
1051
  return {
1048
1052
  ...currentState,
1049
- requests: r
1053
+ requests: r,
1054
+ completedRequests: {
1055
+ ...currentState.completedRequests,
1056
+ [id]: {
1057
+ ...request,
1058
+ completedAt: Date.now(),
1059
+ status: message.approved ? "approved" : "denied",
1060
+ reason: message.reason
1061
+ }
1062
+ }
1050
1063
  };
1051
1064
  });
1052
1065
  });
@@ -1240,6 +1253,158 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1240
1253
  return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
1241
1254
  }
1242
1255
  });
1256
+ session.setHandler("ripgrep", async (data) => {
1257
+ logger.info("Ripgrep request with args:", data.args, "cwd:", data.cwd);
1258
+ try {
1259
+ const result = await run(data.args, { cwd: data.cwd });
1260
+ return {
1261
+ success: true,
1262
+ exitCode: result.exitCode,
1263
+ stdout: result.stdout,
1264
+ stderr: result.stderr
1265
+ };
1266
+ } catch (error) {
1267
+ logger.debug("Failed to run ripgrep:", error);
1268
+ return {
1269
+ success: false,
1270
+ error: error instanceof Error ? error.message : "Failed to run ripgrep"
1271
+ };
1272
+ }
1273
+ });
1274
+ }
1275
+
1276
+ async function startHTTPDirectProxy(options) {
1277
+ const proxy = httpProxy.createProxyServer({
1278
+ target: options.target,
1279
+ changeOrigin: true,
1280
+ secure: false
1281
+ });
1282
+ proxy.on("error", (err, req, res) => {
1283
+ logger.debug(`[HTTPProxy] Proxy error: ${err.message} for ${req.method} ${req.url}`);
1284
+ if (res instanceof ServerResponse && !res.headersSent) {
1285
+ res.writeHead(500, { "Content-Type": "text/plain" });
1286
+ res.end("Proxy error");
1287
+ }
1288
+ });
1289
+ proxy.on("proxyReq", (proxyReq, req, res) => {
1290
+ if (options.onRequest) {
1291
+ options.onRequest(req, proxyReq);
1292
+ }
1293
+ });
1294
+ proxy.on("proxyRes", (proxyRes, req, res) => {
1295
+ if (options.onResponse) {
1296
+ options.onResponse(req, proxyRes);
1297
+ }
1298
+ });
1299
+ const server = createServer((req, res) => {
1300
+ proxy.web(req, res);
1301
+ });
1302
+ const url = await new Promise((resolve, reject) => {
1303
+ server.listen(0, "127.0.0.1", () => {
1304
+ const addr = server.address();
1305
+ if (addr && typeof addr === "object") {
1306
+ const proxyUrl = `http://127.0.0.1:${addr.port}`;
1307
+ logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
1308
+ resolve(proxyUrl);
1309
+ } else {
1310
+ reject(new Error("Failed to get server address"));
1311
+ }
1312
+ });
1313
+ });
1314
+ return url;
1315
+ }
1316
+
1317
+ async function startClaudeActivityTracker(onThinking) {
1318
+ logger.debug(`[ClaudeActivityTracker] Starting activity tracker`);
1319
+ let requestCounter = 0;
1320
+ const activeRequests = /* @__PURE__ */ new Map();
1321
+ let stopThinkingTimeout = null;
1322
+ let isThinking = false;
1323
+ const REQUEST_TIMEOUT = 5 * 60 * 1e3;
1324
+ const checkAndStopThinking = () => {
1325
+ if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1326
+ stopThinkingTimeout = setTimeout(() => {
1327
+ if (isThinking && activeRequests.size === 0) {
1328
+ isThinking = false;
1329
+ onThinking(false);
1330
+ }
1331
+ stopThinkingTimeout = null;
1332
+ }, 500);
1333
+ }
1334
+ };
1335
+ const proxyUrl = await startHTTPDirectProxy({
1336
+ target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1337
+ onRequest: (req, proxyReq) => {
1338
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1339
+ const requestId = ++requestCounter;
1340
+ req._requestId = requestId;
1341
+ if (stopThinkingTimeout) {
1342
+ clearTimeout(stopThinkingTimeout);
1343
+ stopThinkingTimeout = null;
1344
+ }
1345
+ const timeout = setTimeout(() => {
1346
+ activeRequests.delete(requestId);
1347
+ checkAndStopThinking();
1348
+ }, REQUEST_TIMEOUT);
1349
+ activeRequests.set(requestId, timeout);
1350
+ if (!isThinking) {
1351
+ isThinking = true;
1352
+ onThinking(true);
1353
+ }
1354
+ }
1355
+ },
1356
+ onResponse: (req, proxyRes) => {
1357
+ proxyRes.headers["connection"] = "close";
1358
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1359
+ const requestId = req._requestId;
1360
+ const timeout = activeRequests.get(requestId);
1361
+ if (timeout) {
1362
+ clearTimeout(timeout);
1363
+ }
1364
+ let cleaned = false;
1365
+ const cleanupRequest = () => {
1366
+ if (!cleaned) {
1367
+ cleaned = true;
1368
+ activeRequests.delete(requestId);
1369
+ checkAndStopThinking();
1370
+ }
1371
+ };
1372
+ proxyRes.on("end", () => {
1373
+ cleanupRequest();
1374
+ });
1375
+ proxyRes.on("error", (err) => {
1376
+ cleanupRequest();
1377
+ });
1378
+ proxyRes.on("aborted", () => {
1379
+ cleanupRequest();
1380
+ });
1381
+ proxyRes.on("close", () => {
1382
+ cleanupRequest();
1383
+ });
1384
+ req.on("close", () => {
1385
+ cleanupRequest();
1386
+ });
1387
+ }
1388
+ }
1389
+ });
1390
+ const reset = () => {
1391
+ for (const [requestId, timeout] of activeRequests) {
1392
+ clearTimeout(timeout);
1393
+ }
1394
+ activeRequests.clear();
1395
+ if (stopThinkingTimeout) {
1396
+ clearTimeout(stopThinkingTimeout);
1397
+ stopThinkingTimeout = null;
1398
+ }
1399
+ if (isThinking) {
1400
+ isThinking = false;
1401
+ onThinking(false);
1402
+ }
1403
+ };
1404
+ return {
1405
+ proxyUrl,
1406
+ reset
1407
+ };
1243
1408
  }
1244
1409
 
1245
1410
  async function start(credentials, options = {}) {
@@ -1253,22 +1418,15 @@ async function start(credentials, options = {}) {
1253
1418
  const session = api.session(response);
1254
1419
  const pushClient = api.push();
1255
1420
  let thinking = false;
1421
+ let mode = "local";
1256
1422
  let pingInterval = setInterval(() => {
1257
- session.keepAlive(thinking);
1423
+ session.keepAlive(thinking, mode);
1258
1424
  }, 2e3);
1259
- const antropicActivityProxy = await startAnthropicActivityProxy(
1260
- (activity) => {
1261
- const newThinking = activity === "working";
1262
- if (newThinking !== thinking) {
1263
- thinking = newThinking;
1264
- logger.debug(`[PING] Thinking state changed: ${thinking}`);
1265
- session.keepAlive(thinking);
1266
- }
1267
- }
1268
- );
1269
- process.env.HTTP_PROXY = antropicActivityProxy.url;
1270
- process.env.HTTPS_PROXY = antropicActivityProxy.url;
1271
- logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
1425
+ const activityTracker = await startClaudeActivityTracker((newThinking) => {
1426
+ thinking = newThinking;
1427
+ session.keepAlive(thinking, mode);
1428
+ });
1429
+ process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
1272
1430
  const logPath = await logger.logFilePathPromise;
1273
1431
  logger.infoDeveloper(`Session: ${response.id}`);
1274
1432
  logger.infoDeveloper(`Logs: ${logPath}`);
@@ -1280,22 +1438,33 @@ async function start(credentials, options = {}) {
1280
1438
  requests.set(id, resolve);
1281
1439
  });
1282
1440
  let timeout = setTimeout(async () => {
1283
- logger.info("Permission timeout - attempting to interrupt Claude");
1441
+ logger.debug("Permission timeout - attempting to interrupt Claude");
1284
1442
  const interrupted = await interruptController.interrupt();
1285
1443
  if (interrupted) {
1286
- logger.info("Claude interrupted successfully");
1444
+ logger.debug("Claude interrupted successfully");
1287
1445
  }
1288
1446
  requests.delete(id);
1289
1447
  session.updateAgentState((currentState) => {
1448
+ const request2 = currentState.requests?.[id];
1449
+ if (!request2) return currentState;
1290
1450
  let r = { ...currentState.requests };
1291
1451
  delete r[id];
1292
1452
  return {
1293
1453
  ...currentState,
1294
- requests: r
1454
+ requests: r,
1455
+ completedRequests: {
1456
+ ...currentState.completedRequests,
1457
+ [id]: {
1458
+ ...request2,
1459
+ completedAt: Date.now(),
1460
+ status: "canceled",
1461
+ reason: "Timeout"
1462
+ }
1463
+ }
1295
1464
  };
1296
1465
  });
1297
1466
  }, 1e3 * 60 * 4.5);
1298
- logger.info("Permission request" + id + " " + JSON.stringify(request));
1467
+ logger.debug("Permission request" + id + " " + JSON.stringify(request));
1299
1468
  try {
1300
1469
  await pushClient.sendToAllDevices(
1301
1470
  "Permission Request",
@@ -1307,7 +1476,7 @@ async function start(credentials, options = {}) {
1307
1476
  type: "permission_request"
1308
1477
  }
1309
1478
  );
1310
- logger.info("Push notification sent for permission request");
1479
+ logger.debug("Push notification sent for permission request");
1311
1480
  } catch (error) {
1312
1481
  logger.debug("Failed to send push notification:", error);
1313
1482
  }
@@ -1317,7 +1486,8 @@ async function start(credentials, options = {}) {
1317
1486
  ...currentState.requests,
1318
1487
  [id]: {
1319
1488
  tool: request.name,
1320
- arguments: request.arguments
1489
+ arguments: request.arguments,
1490
+ createdAt: Date.now()
1321
1491
  }
1322
1492
  }
1323
1493
  }));
@@ -1349,6 +1519,63 @@ async function start(credentials, options = {}) {
1349
1519
  model: options.model,
1350
1520
  permissionMode: options.permissionMode,
1351
1521
  startingMode: options.startingMode,
1522
+ onModeChange: (newMode) => {
1523
+ mode = newMode;
1524
+ session.sendSessionEvent({ type: "switch", mode: newMode });
1525
+ session.keepAlive(thinking, mode);
1526
+ if (newMode === "local") {
1527
+ logger.debug("Switching to local mode - clearing pending permission requests");
1528
+ for (const [id, resolve] of requests) {
1529
+ logger.debug(`Rejecting pending permission request: ${id}`);
1530
+ resolve({ approved: false, reason: "Session switched to local mode" });
1531
+ }
1532
+ requests.clear();
1533
+ session.updateAgentState((currentState) => {
1534
+ const pendingRequests = currentState.requests || {};
1535
+ const completedRequests = { ...currentState.completedRequests };
1536
+ for (const [id, request] of Object.entries(pendingRequests)) {
1537
+ completedRequests[id] = {
1538
+ ...request,
1539
+ completedAt: Date.now(),
1540
+ status: "canceled",
1541
+ reason: "Session switched to local mode"
1542
+ };
1543
+ }
1544
+ return {
1545
+ ...currentState,
1546
+ controlledByUser: true,
1547
+ requests: {},
1548
+ // Clear all pending requests
1549
+ completedRequests
1550
+ };
1551
+ });
1552
+ } else {
1553
+ session.updateAgentState((currentState) => ({
1554
+ ...currentState,
1555
+ controlledByUser: false
1556
+ }));
1557
+ }
1558
+ },
1559
+ onProcessStart: (processMode) => {
1560
+ logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
1561
+ activityTracker.reset();
1562
+ logger.debug("Starting process - clearing any stale permission requests");
1563
+ for (const [id, resolve] of requests) {
1564
+ logger.debug(`Rejecting stale permission request: ${id}`);
1565
+ resolve({ approved: false, reason: "Process restarted" });
1566
+ }
1567
+ requests.clear();
1568
+ },
1569
+ onProcessStop: (processMode) => {
1570
+ logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
1571
+ activityTracker.reset();
1572
+ logger.debug("Stopping process - clearing any stale permission requests");
1573
+ for (const [id, resolve] of requests) {
1574
+ logger.debug(`Rejecting stale permission request: ${id}`);
1575
+ resolve({ approved: false, reason: "Process restarted" });
1576
+ }
1577
+ requests.clear();
1578
+ },
1352
1579
  mcpServers: {
1353
1580
  "permission": {
1354
1581
  type: "http",
@@ -1358,13 +1585,11 @@ async function start(credentials, options = {}) {
1358
1585
  permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
1359
1586
  session,
1360
1587
  onAssistantResult,
1361
- interruptController
1588
+ interruptController,
1589
+ claudeEnvVars: options.claudeEnvVars,
1590
+ claudeArgs: options.claudeArgs
1362
1591
  });
1363
1592
  clearInterval(pingInterval);
1364
- if (antropicActivityProxy) {
1365
- logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
1366
- antropicActivityProxy.cleanup();
1367
- }
1368
1593
  process.exit(0);
1369
1594
  }
1370
1595
 
@@ -1614,17 +1839,25 @@ class ApiDaemonSession extends EventEmitter {
1614
1839
  }
1615
1840
  }
1616
1841
 
1617
- const DAEMON_PID_FILE = join$1(homedir$1(), ".happy", "daemon-pid");
1618
1842
  async function startDaemon() {
1619
- if (isDaemonRunning()) {
1843
+ console.log("[DAEMON] Starting daemon process...");
1844
+ if (await isDaemonRunning()) {
1620
1845
  console.log("Happy daemon is already running");
1621
1846
  process.exit(0);
1622
1847
  }
1623
- logger.info("Happy CLI daemon started successfully");
1848
+ console.log("[DAEMON] Writing PID file with PID:", process.pid);
1624
1849
  writePidFile();
1625
- process.on("SIGINT", stopDaemon);
1626
- process.on("SIGTERM", stopDaemon);
1627
- process.on("exit", stopDaemon);
1850
+ console.log("[DAEMON] PID file written successfully");
1851
+ logger.info("Happy CLI daemon started successfully");
1852
+ process.on("SIGINT", () => {
1853
+ stopDaemon().catch(console.error);
1854
+ });
1855
+ process.on("SIGTERM", () => {
1856
+ stopDaemon().catch(console.error);
1857
+ });
1858
+ process.on("exit", () => {
1859
+ stopDaemon().catch(console.error);
1860
+ });
1628
1861
  try {
1629
1862
  const settings = await readSettings() || { onboardingCompleted: false };
1630
1863
  if (!settings.machineId) {
@@ -1675,22 +1908,37 @@ async function startDaemon() {
1675
1908
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1676
1909
  }
1677
1910
  }
1678
- function isDaemonRunning() {
1911
+ async function isDaemonRunning() {
1679
1912
  try {
1680
- if (!existsSync$1(DAEMON_PID_FILE)) {
1681
- console.log("No PID file found");
1682
- return false;
1683
- }
1684
- const pid = parseInt(readFileSync$1(DAEMON_PID_FILE, "utf-8"));
1685
- try {
1686
- process.kill(pid, 0);
1687
- return true;
1688
- } catch (error) {
1689
- console.log("Process not running", error);
1690
- unlinkSync(DAEMON_PID_FILE);
1691
- return false;
1913
+ console.log("[isDaemonRunning] Checking if daemon is running...");
1914
+ if (existsSync$1(configuration.daemonPidFile)) {
1915
+ console.log("[isDaemonRunning] PID file exists");
1916
+ const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
1917
+ console.log("[isDaemonRunning] PID from file:", pid);
1918
+ try {
1919
+ process.kill(pid, 0);
1920
+ console.log("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
1921
+ const isHappyDaemon = await isProcessHappyDaemon(pid);
1922
+ console.log("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
1923
+ if (isHappyDaemon) {
1924
+ return true;
1925
+ } else {
1926
+ console.log("[isDaemonRunning] PID is not a happy daemon, cleaning up");
1927
+ logger.debug(`[DAEMON] PID ${pid} is not a happy daemon, cleaning up`);
1928
+ unlinkSync(configuration.daemonPidFile);
1929
+ }
1930
+ } catch (error) {
1931
+ console.log("[isDaemonRunning] Process not running, cleaning up stale PID file");
1932
+ logger.debug("[DAEMON] Process not running, cleaning up stale PID file");
1933
+ unlinkSync(configuration.daemonPidFile);
1934
+ }
1935
+ } else {
1936
+ console.log("[isDaemonRunning] No PID file found");
1692
1937
  }
1693
- } catch {
1938
+ return false;
1939
+ } catch (error) {
1940
+ console.log("[isDaemonRunning] Error:", error);
1941
+ logger.debug("[DAEMON] Error checking daemon status", error);
1694
1942
  return false;
1695
1943
  }
1696
1944
  }
@@ -1699,19 +1947,54 @@ function writePidFile() {
1699
1947
  if (!existsSync$1(happyDir)) {
1700
1948
  mkdirSync$1(happyDir, { recursive: true });
1701
1949
  }
1702
- writeFileSync(DAEMON_PID_FILE, process.pid.toString());
1950
+ try {
1951
+ writeFileSync(configuration.daemonPidFile, process.pid.toString(), { flag: "wx" });
1952
+ } catch (error) {
1953
+ if (error.code === "EEXIST") {
1954
+ logger.debug("[DAEMON] PID file already exists, another daemon may be starting");
1955
+ throw new Error("Daemon PID file already exists");
1956
+ }
1957
+ throw error;
1958
+ }
1703
1959
  }
1704
- function stopDaemon() {
1960
+ async function stopDaemon() {
1705
1961
  try {
1706
- if (existsSync$1(DAEMON_PID_FILE)) {
1707
- logger.debug("[DAEMON] Stopping daemon");
1708
- process.kill(parseInt(readFileSync$1(DAEMON_PID_FILE, "utf-8")), "SIGTERM");
1709
- unlinkSync(DAEMON_PID_FILE);
1962
+ if (existsSync$1(configuration.daemonPidFile)) {
1963
+ const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
1964
+ logger.debug(`[DAEMON] Stopping daemon with PID ${pid}`);
1965
+ try {
1966
+ process.kill(pid, "SIGTERM");
1967
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1968
+ try {
1969
+ process.kill(pid, 0);
1970
+ process.kill(pid, "SIGKILL");
1971
+ } catch {
1972
+ }
1973
+ } catch (error) {
1974
+ logger.debug("[DAEMON] Process already dead or inaccessible", error);
1975
+ }
1976
+ unlinkSync(configuration.daemonPidFile);
1710
1977
  }
1711
1978
  } catch (error) {
1712
- logger.debug("[DAEMON] Error cleaning up PID file", error);
1979
+ logger.debug("[DAEMON] Error stopping daemon", error);
1713
1980
  }
1714
1981
  }
1982
+ async function isProcessHappyDaemon(pid) {
1983
+ return new Promise((resolve) => {
1984
+ const ps = spawn$1("ps", ["-p", pid.toString(), "-o", "command="]);
1985
+ let output = "";
1986
+ ps.stdout.on("data", (data) => {
1987
+ output += data.toString();
1988
+ });
1989
+ ps.on("close", () => {
1990
+ const isHappyDaemon = output.includes("daemon start") && (output.includes("happy") || output.includes("src/index"));
1991
+ resolve(isHappyDaemon);
1992
+ });
1993
+ ps.on("error", () => {
1994
+ resolve(false);
1995
+ });
1996
+ });
1997
+ }
1715
1998
 
1716
1999
  function trimIdent(text) {
1717
2000
  const lines = text.split("\n");
@@ -1910,10 +2193,19 @@ Currently only supported on macOS.
1910
2193
  options.model = args[++i];
1911
2194
  } else if (arg === "-p" || arg === "--permission-mode") {
1912
2195
  options.permissionMode = z$1.enum(["auto", "default", "plan"]).parse(args[++i]);
1913
- } else if (arg === "--local") {
1914
- i++;
1915
- } else if (arg === "--happy-starting-mode") {
1916
- options.startingMode = z$1.enum(["interactive", "remote"]).parse(args[++i]);
2196
+ } else if (arg === "--local") ; else if (arg === "--happy-starting-mode") {
2197
+ options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
2198
+ } else if (arg === "--claude-env") {
2199
+ const envVar = args[++i];
2200
+ const [key, value] = envVar.split("=", 2);
2201
+ if (!key || value === void 0) {
2202
+ console.error(chalk.red(`Invalid environment variable format: ${envVar}. Use KEY=VALUE`));
2203
+ process.exit(1);
2204
+ }
2205
+ options.claudeEnvVars = { ...options.claudeEnvVars, [key]: value };
2206
+ } else if (arg === "--claude-arg") {
2207
+ const claudeArg = args[++i];
2208
+ options.claudeArgs = [...options.claudeArgs || [], claudeArg];
1917
2209
  } else {
1918
2210
  console.error(chalk.red(`Unknown argument: ${arg}`));
1919
2211
  process.exit(1);
@@ -1926,6 +2218,7 @@ ${chalk.bold("happy")} - Claude Code session sharing
1926
2218
  ${chalk.bold("Usage:")}
1927
2219
  happy [options]
1928
2220
  happy logout Logs out of your account and removes data directory
2221
+ happy daemon Manage the background daemon (macOS only)
1929
2222
 
1930
2223
  ${chalk.bold("Options:")}
1931
2224
  -h, --help Show this help message
@@ -1933,6 +2226,14 @@ ${chalk.bold("Options:")}
1933
2226
  -m, --model <model> Claude model to use (default: sonnet)
1934
2227
  -p, --permission-mode Permission mode: auto, default, or plan
1935
2228
  --auth, --login Force re-authentication
2229
+ --claude-env KEY=VALUE Set environment variable for Claude Code
2230
+ --claude-arg ARG Pass additional argument to Claude CLI
2231
+
2232
+ [Daemon Management]
2233
+ --happy-daemon-start Start the daemon in background
2234
+ --happy-daemon-stop Stop the daemon
2235
+ --happy-daemon-install Install daemon to run on startup
2236
+ --happy-daemon-uninstall Uninstall daemon from startup
1936
2237
 
1937
2238
  [Advanced]
1938
2239
  --local < global | local >
@@ -1946,6 +2247,10 @@ ${chalk.bold("Examples:")}
1946
2247
  happy -m opus Use Claude Opus model
1947
2248
  happy -p plan Use plan permission mode
1948
2249
  happy --auth Force re-authentication before starting session
2250
+ happy --claude-env KEY=VALUE
2251
+ Set environment variable for Claude Code
2252
+ happy --claude-arg --option
2253
+ Pass argument to Claude CLI
1949
2254
  happy logout Logs out of your account and removes data directory
1950
2255
  `);
1951
2256
  process.exit(0);
@@ -1962,6 +2267,7 @@ ${chalk.bold("Examples:")}
1962
2267
  }
1963
2268
  credentials = res;
1964
2269
  }
2270
+ await readSettings() || { };
1965
2271
  try {
1966
2272
  await start(credentials, options);
1967
2273
  } catch (error) {