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.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", () => {
@@ -221,15 +246,11 @@ async function claudeRemote(opts) {
221
246
  logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
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;
581
+ let watchers = /* @__PURE__ */ new Map();
527
582
  let processedMessages = /* @__PURE__ */ new Set();
528
583
  let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
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) {
@@ -567,6 +627,7 @@ function createSessionScanner(opts) {
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) {
@@ -658,7 +716,7 @@ function sortKeys(value) {
658
716
  }
659
717
 
660
718
  async function loop(opts) {
661
- let mode = opts.startingMode ?? "interactive";
719
+ let mode = opts.startingMode ?? "local";
662
720
  let currentMessageQueue = new MessageQueue();
663
721
  let sessionId = null;
664
722
  let onMessage = null;
@@ -682,23 +740,38 @@ async function loop(opts) {
682
740
  };
683
741
  while (true) {
684
742
  if (currentMessageQueue.size() > 0) {
685
- mode = "remote";
743
+ if (mode !== "remote") {
744
+ mode = "remote";
745
+ if (opts.onModeChange) {
746
+ opts.onModeChange(mode);
747
+ }
748
+ }
686
749
  continue;
687
750
  }
688
- if (mode === "interactive") {
751
+ if (mode === "local") {
689
752
  let abortedOutside = false;
690
753
  const interactiveAbortController = new AbortController();
691
754
  opts.session.setHandler("switch", () => {
692
755
  if (!interactiveAbortController.signal.aborted) {
693
756
  abortedOutside = true;
694
- mode = "remote";
757
+ if (mode !== "remote") {
758
+ mode = "remote";
759
+ if (opts.onModeChange) {
760
+ opts.onModeChange(mode);
761
+ }
762
+ }
695
763
  interactiveAbortController.abort();
696
764
  }
697
765
  });
698
766
  onMessage = () => {
699
767
  if (!interactiveAbortController.signal.aborted) {
700
768
  abortedOutside = true;
701
- mode = "remote";
769
+ if (mode !== "remote") {
770
+ mode = "remote";
771
+ if (opts.onModeChange) {
772
+ opts.onModeChange(mode);
773
+ }
774
+ }
702
775
  interactiveAbortController.abort();
703
776
  }
704
777
  onMessage = null;
@@ -707,13 +780,15 @@ async function loop(opts) {
707
780
  path: opts.path,
708
781
  sessionId,
709
782
  onSessionFound,
710
- abort: interactiveAbortController.signal
783
+ abort: interactiveAbortController.signal,
784
+ claudeEnvVars: opts.claudeEnvVars,
785
+ claudeArgs: opts.claudeArgs
711
786
  });
712
787
  onMessage = null;
713
788
  if (!abortedOutside) {
714
789
  return;
715
790
  }
716
- if (mode !== "interactive") {
791
+ if (mode !== "local") {
717
792
  console.log("Switching to remote mode...");
718
793
  }
719
794
  }
@@ -727,7 +802,12 @@ async function loop(opts) {
727
802
  });
728
803
  const abortHandler = () => {
729
804
  if (!remoteAbortController.signal.aborted) {
730
- mode = "interactive";
805
+ if (mode !== "local") {
806
+ mode = "local";
807
+ if (opts.onModeChange) {
808
+ opts.onModeChange(mode);
809
+ }
810
+ }
731
811
  remoteAbortController.abort();
732
812
  }
733
813
  if (process.stdin.isTTY) {
@@ -751,7 +831,9 @@ async function loop(opts) {
751
831
  onSessionFound,
752
832
  messages: currentMessageQueue,
753
833
  onAssistantResult: opts.onAssistantResult,
754
- interruptController: opts.interruptController
834
+ interruptController: opts.interruptController,
835
+ claudeEnvVars: opts.claudeEnvVars,
836
+ claudeArgs: opts.claudeArgs
755
837
  });
756
838
  } finally {
757
839
  process.stdin.off("data", abortHandler);
@@ -868,159 +950,41 @@ class InterruptController {
868
950
  }
869
951
  }
870
952
 
871
- var version = "0.1.10";
953
+ var version = "0.1.11";
872
954
  var packageJson = {
873
955
  version: version};
874
956
 
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
- }
957
+ const __dirname = dirname$1(fileURLToPath$1(import.meta.url));
958
+ const RUNNER_PATH = join$1(__dirname, "..", "..", "scripts", "ripgrep_launcher.cjs");
959
+ function run(args, options) {
960
+ return new Promise((resolve, reject) => {
961
+ const child = spawn$1("node", [RUNNER_PATH, JSON.stringify(args)], {
962
+ stdio: ["pipe", "pipe", "pipe"],
963
+ cwd: options?.cwd
917
964
  });
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();
965
+ let stdout = "";
966
+ let stderr = "";
967
+ child.stdout.on("data", (data) => {
968
+ stdout += data.toString();
961
969
  });
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);
970
+ child.stderr.on("data", (data) => {
971
+ stderr += data.toString();
981
972
  });
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();
973
+ child.on("close", (code) => {
974
+ resolve({
975
+ exitCode: code || 0,
976
+ stdout,
977
+ stderr
978
+ });
991
979
  });
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
- }
980
+ child.on("error", (err) => {
981
+ reject(err);
1002
982
  });
1003
983
  });
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
984
  }
1021
985
 
1022
986
  const execAsync = promisify(exec);
1023
- function registerHandlers(session, interruptController, permissionCallbacks) {
987
+ function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
1024
988
  session.setHandler("abort", async () => {
1025
989
  logger.info("Abort request - interrupting Claude");
1026
990
  await interruptController.interrupt();
@@ -1240,6 +1204,121 @@ function registerHandlers(session, interruptController, permissionCallbacks) {
1240
1204
  return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
1241
1205
  }
1242
1206
  });
1207
+ session.setHandler("ripgrep", async (data) => {
1208
+ logger.info("Ripgrep request with args:", data.args, "cwd:", data.cwd);
1209
+ try {
1210
+ const result = await run(data.args, { cwd: data.cwd });
1211
+ return {
1212
+ success: true,
1213
+ exitCode: result.exitCode,
1214
+ stdout: result.stdout,
1215
+ stderr: result.stderr
1216
+ };
1217
+ } catch (error) {
1218
+ logger.debug("Failed to run ripgrep:", error);
1219
+ return {
1220
+ success: false,
1221
+ error: error instanceof Error ? error.message : "Failed to run ripgrep"
1222
+ };
1223
+ }
1224
+ });
1225
+ }
1226
+
1227
+ async function startHTTPDirectProxy(options) {
1228
+ const proxy = httpProxy.createProxyServer({
1229
+ target: options.target,
1230
+ changeOrigin: true,
1231
+ secure: false
1232
+ });
1233
+ proxy.on("error", (err, req, res) => {
1234
+ logger.debug(`[HTTPProxy] Proxy error: ${err.message} for ${req.method} ${req.url}`);
1235
+ if (res instanceof ServerResponse && !res.headersSent) {
1236
+ res.writeHead(500, { "Content-Type": "text/plain" });
1237
+ res.end("Proxy error");
1238
+ }
1239
+ });
1240
+ proxy.on("proxyReq", (proxyReq, req, res) => {
1241
+ if (options.onRequest) {
1242
+ options.onRequest(req, proxyReq);
1243
+ }
1244
+ });
1245
+ proxy.on("proxyRes", (proxyRes, req, res) => {
1246
+ if (options.onResponse) {
1247
+ options.onResponse(req, proxyRes);
1248
+ }
1249
+ });
1250
+ const server = createServer((req, res) => {
1251
+ proxy.web(req, res);
1252
+ });
1253
+ const url = await new Promise((resolve, reject) => {
1254
+ server.listen(0, "127.0.0.1", () => {
1255
+ const addr = server.address();
1256
+ if (addr && typeof addr === "object") {
1257
+ const proxyUrl = `http://127.0.0.1:${addr.port}`;
1258
+ logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
1259
+ resolve(proxyUrl);
1260
+ } else {
1261
+ reject(new Error("Failed to get server address"));
1262
+ }
1263
+ });
1264
+ });
1265
+ return url;
1266
+ }
1267
+
1268
+ async function startClaudeActivityTracker(onThinking) {
1269
+ let requestCounter = 0;
1270
+ const activeRequests = /* @__PURE__ */ new Set();
1271
+ let stopThinkingTimeout = null;
1272
+ let isThinking = false;
1273
+ const proxyUrl = await startHTTPDirectProxy({
1274
+ target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1275
+ onRequest: (req, proxyReq) => {
1276
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1277
+ const requestId = ++requestCounter;
1278
+ activeRequests.add(requestId);
1279
+ req._requestId = requestId;
1280
+ if (stopThinkingTimeout) {
1281
+ clearTimeout(stopThinkingTimeout);
1282
+ stopThinkingTimeout = null;
1283
+ }
1284
+ if (!isThinking) {
1285
+ logger.debug(`[ClaudeActivityTracker] Thinking started`);
1286
+ isThinking = true;
1287
+ onThinking(true);
1288
+ }
1289
+ }
1290
+ },
1291
+ onResponse: (req, proxyRes) => {
1292
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1293
+ const requestId = req._requestId;
1294
+ proxyRes.on("end", () => {
1295
+ activeRequests.delete(requestId);
1296
+ if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1297
+ stopThinkingTimeout = setTimeout(() => {
1298
+ if (isThinking) {
1299
+ isThinking = false;
1300
+ logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
1301
+ onThinking(false);
1302
+ }
1303
+ }, 500);
1304
+ }
1305
+ });
1306
+ proxyRes.on("error", () => {
1307
+ activeRequests.delete(requestId);
1308
+ if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1309
+ stopThinkingTimeout = setTimeout(() => {
1310
+ if (isThinking) {
1311
+ isThinking = false;
1312
+ logger.debug(`[ClaudeActivityTracker] Thinking stopped`);
1313
+ onThinking(false);
1314
+ }
1315
+ }, 500);
1316
+ }
1317
+ });
1318
+ }
1319
+ }
1320
+ });
1321
+ return proxyUrl;
1243
1322
  }
1244
1323
 
1245
1324
  async function start(credentials, options = {}) {
@@ -1253,22 +1332,15 @@ async function start(credentials, options = {}) {
1253
1332
  const session = api.session(response);
1254
1333
  const pushClient = api.push();
1255
1334
  let thinking = false;
1335
+ let mode = "local";
1256
1336
  let pingInterval = setInterval(() => {
1257
- session.keepAlive(thinking);
1337
+ session.keepAlive(thinking, mode);
1258
1338
  }, 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}`);
1339
+ const proxyUrl = await startClaudeActivityTracker((newThinking) => {
1340
+ thinking = newThinking;
1341
+ session.keepAlive(thinking, mode);
1342
+ });
1343
+ process.env.ANTHROPIC_BASE_URL = proxyUrl;
1272
1344
  const logPath = await logger.logFilePathPromise;
1273
1345
  logger.infoDeveloper(`Session: ${response.id}`);
1274
1346
  logger.infoDeveloper(`Logs: ${logPath}`);
@@ -1280,10 +1352,10 @@ async function start(credentials, options = {}) {
1280
1352
  requests.set(id, resolve);
1281
1353
  });
1282
1354
  let timeout = setTimeout(async () => {
1283
- logger.info("Permission timeout - attempting to interrupt Claude");
1355
+ logger.debug("Permission timeout - attempting to interrupt Claude");
1284
1356
  const interrupted = await interruptController.interrupt();
1285
1357
  if (interrupted) {
1286
- logger.info("Claude interrupted successfully");
1358
+ logger.debug("Claude interrupted successfully");
1287
1359
  }
1288
1360
  requests.delete(id);
1289
1361
  session.updateAgentState((currentState) => {
@@ -1295,7 +1367,7 @@ async function start(credentials, options = {}) {
1295
1367
  };
1296
1368
  });
1297
1369
  }, 1e3 * 60 * 4.5);
1298
- logger.info("Permission request" + id + " " + JSON.stringify(request));
1370
+ logger.debug("Permission request" + id + " " + JSON.stringify(request));
1299
1371
  try {
1300
1372
  await pushClient.sendToAllDevices(
1301
1373
  "Permission Request",
@@ -1307,7 +1379,7 @@ async function start(credentials, options = {}) {
1307
1379
  type: "permission_request"
1308
1380
  }
1309
1381
  );
1310
- logger.info("Push notification sent for permission request");
1382
+ logger.debug("Push notification sent for permission request");
1311
1383
  } catch (error) {
1312
1384
  logger.debug("Failed to send push notification:", error);
1313
1385
  }
@@ -1349,6 +1421,15 @@ async function start(credentials, options = {}) {
1349
1421
  model: options.model,
1350
1422
  permissionMode: options.permissionMode,
1351
1423
  startingMode: options.startingMode,
1424
+ onModeChange: (newMode) => {
1425
+ mode = newMode;
1426
+ session.sendSessionEvent({ type: "switch", mode: newMode });
1427
+ session.keepAlive(thinking, mode);
1428
+ session.updateAgentState((currentState) => ({
1429
+ ...currentState,
1430
+ controlledByUser: newMode === "local" ? true : false
1431
+ }));
1432
+ },
1352
1433
  mcpServers: {
1353
1434
  "permission": {
1354
1435
  type: "http",
@@ -1358,13 +1439,11 @@ async function start(credentials, options = {}) {
1358
1439
  permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
1359
1440
  session,
1360
1441
  onAssistantResult,
1361
- interruptController
1442
+ interruptController,
1443
+ claudeEnvVars: options.claudeEnvVars,
1444
+ claudeArgs: options.claudeArgs
1362
1445
  });
1363
1446
  clearInterval(pingInterval);
1364
- if (antropicActivityProxy) {
1365
- logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
1366
- antropicActivityProxy.cleanup();
1367
- }
1368
1447
  process.exit(0);
1369
1448
  }
1370
1449
 
@@ -1614,17 +1693,25 @@ class ApiDaemonSession extends EventEmitter {
1614
1693
  }
1615
1694
  }
1616
1695
 
1617
- const DAEMON_PID_FILE = join$1(homedir$1(), ".happy", "daemon-pid");
1618
1696
  async function startDaemon() {
1619
- if (isDaemonRunning()) {
1697
+ console.log("[DAEMON] Starting daemon process...");
1698
+ if (await isDaemonRunning()) {
1620
1699
  console.log("Happy daemon is already running");
1621
1700
  process.exit(0);
1622
1701
  }
1623
- logger.info("Happy CLI daemon started successfully");
1702
+ console.log("[DAEMON] Writing PID file with PID:", process.pid);
1624
1703
  writePidFile();
1625
- process.on("SIGINT", stopDaemon);
1626
- process.on("SIGTERM", stopDaemon);
1627
- process.on("exit", stopDaemon);
1704
+ console.log("[DAEMON] PID file written successfully");
1705
+ logger.info("Happy CLI daemon started successfully");
1706
+ process.on("SIGINT", () => {
1707
+ stopDaemon().catch(console.error);
1708
+ });
1709
+ process.on("SIGTERM", () => {
1710
+ stopDaemon().catch(console.error);
1711
+ });
1712
+ process.on("exit", () => {
1713
+ stopDaemon().catch(console.error);
1714
+ });
1628
1715
  try {
1629
1716
  const settings = await readSettings() || { onboardingCompleted: false };
1630
1717
  if (!settings.machineId) {
@@ -1675,22 +1762,37 @@ async function startDaemon() {
1675
1762
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1676
1763
  }
1677
1764
  }
1678
- function isDaemonRunning() {
1765
+ async function isDaemonRunning() {
1679
1766
  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;
1767
+ console.log("[isDaemonRunning] Checking if daemon is running...");
1768
+ if (existsSync$1(configuration.daemonPidFile)) {
1769
+ console.log("[isDaemonRunning] PID file exists");
1770
+ const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
1771
+ console.log("[isDaemonRunning] PID from file:", pid);
1772
+ try {
1773
+ process.kill(pid, 0);
1774
+ console.log("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
1775
+ const isHappyDaemon = await isProcessHappyDaemon(pid);
1776
+ console.log("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
1777
+ if (isHappyDaemon) {
1778
+ return true;
1779
+ } else {
1780
+ console.log("[isDaemonRunning] PID is not a happy daemon, cleaning up");
1781
+ logger.debug(`[DAEMON] PID ${pid} is not a happy daemon, cleaning up`);
1782
+ unlinkSync(configuration.daemonPidFile);
1783
+ }
1784
+ } catch (error) {
1785
+ console.log("[isDaemonRunning] Process not running, cleaning up stale PID file");
1786
+ logger.debug("[DAEMON] Process not running, cleaning up stale PID file");
1787
+ unlinkSync(configuration.daemonPidFile);
1788
+ }
1789
+ } else {
1790
+ console.log("[isDaemonRunning] No PID file found");
1692
1791
  }
1693
- } catch {
1792
+ return false;
1793
+ } catch (error) {
1794
+ console.log("[isDaemonRunning] Error:", error);
1795
+ logger.debug("[DAEMON] Error checking daemon status", error);
1694
1796
  return false;
1695
1797
  }
1696
1798
  }
@@ -1699,19 +1801,54 @@ function writePidFile() {
1699
1801
  if (!existsSync$1(happyDir)) {
1700
1802
  mkdirSync$1(happyDir, { recursive: true });
1701
1803
  }
1702
- writeFileSync(DAEMON_PID_FILE, process.pid.toString());
1804
+ try {
1805
+ writeFileSync(configuration.daemonPidFile, process.pid.toString(), { flag: "wx" });
1806
+ } catch (error) {
1807
+ if (error.code === "EEXIST") {
1808
+ logger.debug("[DAEMON] PID file already exists, another daemon may be starting");
1809
+ throw new Error("Daemon PID file already exists");
1810
+ }
1811
+ throw error;
1812
+ }
1703
1813
  }
1704
- function stopDaemon() {
1814
+ async function stopDaemon() {
1705
1815
  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);
1816
+ if (existsSync$1(configuration.daemonPidFile)) {
1817
+ const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
1818
+ logger.debug(`[DAEMON] Stopping daemon with PID ${pid}`);
1819
+ try {
1820
+ process.kill(pid, "SIGTERM");
1821
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1822
+ try {
1823
+ process.kill(pid, 0);
1824
+ process.kill(pid, "SIGKILL");
1825
+ } catch {
1826
+ }
1827
+ } catch (error) {
1828
+ logger.debug("[DAEMON] Process already dead or inaccessible", error);
1829
+ }
1830
+ unlinkSync(configuration.daemonPidFile);
1710
1831
  }
1711
1832
  } catch (error) {
1712
- logger.debug("[DAEMON] Error cleaning up PID file", error);
1833
+ logger.debug("[DAEMON] Error stopping daemon", error);
1713
1834
  }
1714
1835
  }
1836
+ async function isProcessHappyDaemon(pid) {
1837
+ return new Promise((resolve) => {
1838
+ const ps = spawn$1("ps", ["-p", pid.toString(), "-o", "command="]);
1839
+ let output = "";
1840
+ ps.stdout.on("data", (data) => {
1841
+ output += data.toString();
1842
+ });
1843
+ ps.on("close", () => {
1844
+ const isHappyDaemon = output.includes("daemon start") && (output.includes("happy") || output.includes("src/index"));
1845
+ resolve(isHappyDaemon);
1846
+ });
1847
+ ps.on("error", () => {
1848
+ resolve(false);
1849
+ });
1850
+ });
1851
+ }
1715
1852
 
1716
1853
  function trimIdent(text) {
1717
1854
  const lines = text.split("\n");
@@ -1913,7 +2050,18 @@ Currently only supported on macOS.
1913
2050
  } else if (arg === "--local") {
1914
2051
  i++;
1915
2052
  } else if (arg === "--happy-starting-mode") {
1916
- options.startingMode = z$1.enum(["interactive", "remote"]).parse(args[++i]);
2053
+ options.startingMode = z$1.enum(["local", "remote"]).parse(args[++i]);
2054
+ } else if (arg === "--claude-env") {
2055
+ const envVar = args[++i];
2056
+ const [key, value] = envVar.split("=", 2);
2057
+ if (!key || value === void 0) {
2058
+ console.error(chalk.red(`Invalid environment variable format: ${envVar}. Use KEY=VALUE`));
2059
+ process.exit(1);
2060
+ }
2061
+ options.claudeEnvVars = { ...options.claudeEnvVars, [key]: value };
2062
+ } else if (arg === "--claude-arg") {
2063
+ const claudeArg = args[++i];
2064
+ options.claudeArgs = [...options.claudeArgs || [], claudeArg];
1917
2065
  } else {
1918
2066
  console.error(chalk.red(`Unknown argument: ${arg}`));
1919
2067
  process.exit(1);
@@ -1926,6 +2074,7 @@ ${chalk.bold("happy")} - Claude Code session sharing
1926
2074
  ${chalk.bold("Usage:")}
1927
2075
  happy [options]
1928
2076
  happy logout Logs out of your account and removes data directory
2077
+ happy daemon Manage the background daemon (macOS only)
1929
2078
 
1930
2079
  ${chalk.bold("Options:")}
1931
2080
  -h, --help Show this help message
@@ -1933,6 +2082,14 @@ ${chalk.bold("Options:")}
1933
2082
  -m, --model <model> Claude model to use (default: sonnet)
1934
2083
  -p, --permission-mode Permission mode: auto, default, or plan
1935
2084
  --auth, --login Force re-authentication
2085
+ --claude-env KEY=VALUE Set environment variable for Claude Code
2086
+ --claude-arg ARG Pass additional argument to Claude CLI
2087
+
2088
+ [Daemon Management]
2089
+ --happy-daemon-start Start the daemon in background
2090
+ --happy-daemon-stop Stop the daemon
2091
+ --happy-daemon-install Install daemon to run on startup
2092
+ --happy-daemon-uninstall Uninstall daemon from startup
1936
2093
 
1937
2094
  [Advanced]
1938
2095
  --local < global | local >
@@ -1946,6 +2103,10 @@ ${chalk.bold("Examples:")}
1946
2103
  happy -m opus Use Claude Opus model
1947
2104
  happy -p plan Use plan permission mode
1948
2105
  happy --auth Force re-authentication before starting session
2106
+ happy --claude-env KEY=VALUE
2107
+ Set environment variable for Claude Code
2108
+ happy --claude-arg --option
2109
+ Pass argument to Claude CLI
1949
2110
  happy logout Logs out of your account and removes data directory
1950
2111
  `);
1951
2112
  process.exit(0);
@@ -1962,6 +2123,7 @@ ${chalk.bold("Examples:")}
1962
2123
  }
1963
2124
  credentials = res;
1964
2125
  }
2126
+ await readSettings() || { };
1965
2127
  try {
1966
2128
  await start(credentials, options);
1967
2129
  } catch (error) {