happy-coder 0.1.9 → 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 (40) 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 +997 -260
  5. package/dist/index.mjs +996 -259
  6. package/dist/install-B0DnBGS_.mjs +29 -0
  7. package/dist/install-C809w0Cj.cjs +31 -0
  8. package/dist/install-DEPy62QN.mjs +97 -0
  9. package/dist/install-GZIzyuIE.cjs +99 -0
  10. package/dist/lib.cjs +1 -1
  11. package/dist/lib.d.cts +265 -460
  12. package/dist/lib.d.mts +265 -460
  13. package/dist/lib.mjs +1 -1
  14. package/dist/run-BmEaINbl.cjs +250 -0
  15. package/dist/run-DMbKhYfb.mjs +247 -0
  16. package/dist/types-BRICSarm.mjs +870 -0
  17. package/dist/types-BTQRfIr3.cjs +892 -0
  18. package/dist/types-CEvzGLMI.cjs +882 -0
  19. package/dist/types-D39L8JSd.mjs +850 -0
  20. package/dist/types-DYBiuNUQ.cjs +883 -0
  21. package/dist/types-Df5dlWLV.mjs +871 -0
  22. package/dist/types-hotUTaWz.cjs +863 -0
  23. package/dist/types-tLWMaptR.mjs +879 -0
  24. package/dist/uninstall-BGgl5V8F.mjs +29 -0
  25. package/dist/uninstall-BWHglipH.mjs +40 -0
  26. package/dist/uninstall-CdHMb6wi.cjs +31 -0
  27. package/dist/uninstall-FXyyAuGU.cjs +42 -0
  28. package/package.json +8 -2
  29. package/ripgrep/COPYING +3 -0
  30. package/ripgrep/arm64-darwin/rg +0 -0
  31. package/ripgrep/arm64-darwin/ripgrep.node +0 -0
  32. package/ripgrep/arm64-linux/rg +0 -0
  33. package/ripgrep/arm64-linux/ripgrep.node +0 -0
  34. package/ripgrep/x64-darwin/rg +0 -0
  35. package/ripgrep/x64-darwin/ripgrep.node +0 -0
  36. package/ripgrep/x64-linux/rg +0 -0
  37. package/ripgrep/x64-linux/ripgrep.node +0 -0
  38. package/ripgrep/x64-win32/rg.exe +0 -0
  39. package/ripgrep/x64-win32/ripgrep.node +0 -0
  40. package/scripts/ripgrep_launcher.cjs +57 -0
package/dist/index.cjs CHANGED
@@ -1,28 +1,34 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var types = require('./types-mykDX2xe.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');
19
+ var child_process = require('child_process');
20
+ var util = require('util');
21
+ var crypto = require('crypto');
22
+ var path = require('path');
23
+ var url = require('url');
24
+ var httpProxy = require('http-proxy');
20
25
  var tweetnacl = require('tweetnacl');
21
26
  var axios = require('axios');
22
27
  var qrcode = require('qrcode-terminal');
23
- require('fs');
24
- require('node:events');
25
- require('socket.io-client');
28
+ var node_events = require('node:events');
29
+ var socket_ioClient = require('socket.io-client');
30
+ var os$1 = require('os');
31
+ var fs = require('fs');
26
32
  require('expo-server-sdk');
27
33
 
28
34
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -165,9 +171,13 @@ function printDivider() {
165
171
  console.log(chalk.gray("\u2550".repeat(60)));
166
172
  }
167
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
+
168
179
  function claudeCheckSession(sessionId, path) {
169
- const projectName = node_path.resolve(path).replace(/\//g, "-");
170
- const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
180
+ const projectDir = getProjectPath(path);
171
181
  const sessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
172
182
  const sessionExists = node_fs.existsSync(sessionFile);
173
183
  if (!sessionExists) {
@@ -185,11 +195,29 @@ function claudeCheckSession(sessionId, path) {
185
195
  return hasGoodMessage;
186
196
  }
187
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
+
188
211
  async function claudeRemote(opts) {
189
212
  let startFrom = opts.sessionId;
190
213
  if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
191
214
  startFrom = null;
192
215
  }
216
+ if (opts.claudeEnvVars) {
217
+ Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
218
+ process.env[key] = value;
219
+ });
220
+ }
193
221
  const abortController = new AbortController();
194
222
  const sdkOptions = {
195
223
  cwd: opts.path,
@@ -199,6 +227,9 @@ async function claudeRemote(opts) {
199
227
  executable: "node",
200
228
  abortController
201
229
  };
230
+ if (opts.claudeArgs && opts.claudeArgs.length > 0) {
231
+ sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
232
+ }
202
233
  let aborted = false;
203
234
  let response;
204
235
  opts.abort.addEventListener("abort", () => {
@@ -236,15 +267,11 @@ async function claudeRemote(opts) {
236
267
  types.logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
237
268
  formatClaudeMessage(message, opts.onAssistantResult);
238
269
  if (message.type === "system" && message.subtype === "init") {
239
- const projectName = node_path.resolve(opts.path).replace(/\//g, "-");
240
- const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
241
- node_fs.mkdirSync(projectDir, { recursive: true });
242
- const watcher = node_fs.watch(projectDir).on("change", (_, filename) => {
243
- if (filename === `${message.session_id}.jsonl`) {
244
- opts.onSessionFound(message.session_id);
245
- watcher.close();
246
- }
247
- });
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);
248
275
  }
249
276
  }
250
277
  types.logger.debug(`[claudeRemote] Finished iterating over response`);
@@ -266,10 +293,9 @@ async function claudeRemote(opts) {
266
293
  types.logger.debug(`[claudeRemote] Function completed`);
267
294
  }
268
295
 
269
- 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))));
270
297
  async function claudeLocal(opts) {
271
- const projectName = node_path.resolve(opts.path).replace(/\//g, "-");
272
- const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
298
+ const projectDir = getProjectPath(opts.path);
273
299
  node_fs.mkdirSync(projectDir, { recursive: true });
274
300
  const watcher = node_fs.watch(projectDir);
275
301
  let resolvedSessionId = null;
@@ -303,11 +329,19 @@ async function claudeLocal(opts) {
303
329
  if (startFrom) {
304
330
  args.push("--resume", startFrom);
305
331
  }
306
- 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
+ };
307
340
  const child = node_child_process.spawn("node", [claudeCliPath, ...args], {
308
341
  stdio: ["inherit", "inherit", "inherit", "pipe"],
309
342
  signal: opts.abort,
310
- cwd: opts.path
343
+ cwd: opts.path,
344
+ env
311
345
  });
312
346
  if (child.stdio[3]) {
313
347
  const rl = node_readline.createInterface({
@@ -532,16 +566,44 @@ class InvalidateSync {
532
566
  };
533
567
  }
534
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
+
535
597
  function createSessionScanner(opts) {
536
- const projectName = node_path.resolve(opts.workingDirectory).replace(/\//g, "-");
537
- const projectDir = node_path.join(os.homedir(), ".claude", "projects", projectName);
598
+ const projectDir = getProjectPath(opts.workingDirectory);
538
599
  let finishedSessions = /* @__PURE__ */ new Set();
539
600
  let pendingSessions = /* @__PURE__ */ new Set();
540
601
  let currentSessionId = null;
541
- let currentSessionWatcherAbortController = null;
602
+ let watchers = /* @__PURE__ */ new Map();
542
603
  let processedMessages = /* @__PURE__ */ new Set();
543
604
  let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
544
605
  const sync = new InvalidateSync(async () => {
606
+ types.logger.debug(`[SESSION_SCANNER] Syncing...`);
545
607
  let sessions = [];
546
608
  for (let p of pendingSessions) {
547
609
  sessions.push(p);
@@ -553,13 +615,17 @@ function createSessionScanner(opts) {
553
615
  const expectedSessionFile = node_path.join(projectDir, `${sessionId}.jsonl`);
554
616
  let file;
555
617
  try {
556
- file = await promises.readFile(expectedSessionFile, "utf-8");
618
+ file = await promises$1.readFile(expectedSessionFile, "utf-8");
557
619
  } catch (error) {
620
+ types.logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
558
621
  return;
559
622
  }
560
623
  let lines = file.split("\n");
561
624
  for (let l of lines) {
562
625
  try {
626
+ if (l.trim() === "") {
627
+ continue;
628
+ }
563
629
  let message = JSON.parse(l);
564
630
  let parsed = types.RawJSONLinesSchema.safeParse(message);
565
631
  if (!parsed.success) {
@@ -573,15 +639,16 @@ function createSessionScanner(opts) {
573
639
  processedMessages.add(key);
574
640
  types.logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
575
641
  types.logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
576
- if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
642
+ if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
577
643
  const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
578
644
  if (currentCounter && currentCounter > 0) {
579
645
  seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
580
646
  continue;
581
647
  }
582
648
  }
583
- opts.onMessage(parsed.data);
649
+ opts.onMessage(message);
584
650
  } catch (e) {
651
+ types.logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
585
652
  continue;
586
653
  }
587
654
  }
@@ -595,40 +662,37 @@ function createSessionScanner(opts) {
595
662
  finishedSessions.add(p);
596
663
  }
597
664
  }
598
- currentSessionWatcherAbortController?.abort();
599
- currentSessionWatcherAbortController = new AbortController();
600
- void (async () => {
601
- if (currentSessionId) {
602
- const sessionFile = node_path.join(projectDir, `${currentSessionId}.jsonl`);
603
- try {
604
- for await (const change of promises.watch(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
605
- await processSessionFile(currentSessionId);
606
- }
607
- } catch (error) {
608
- if (error.name !== "AbortError") {
609
- types.logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
610
- }
611
- }
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
+ }));
612
670
  }
613
- })();
671
+ }
614
672
  });
673
+ sync.invalidate();
615
674
  const intervalId = setInterval(() => {
616
675
  sync.invalidate();
617
676
  }, 3e3);
618
677
  return {
619
- refresh: () => sync.invalidate(),
620
678
  cleanup: () => {
621
679
  clearInterval(intervalId);
622
- currentSessionWatcherAbortController?.abort();
680
+ for (let w of watchers.values()) {
681
+ w();
682
+ }
683
+ watchers.clear();
623
684
  },
624
685
  onNewSession: (sessionId) => {
625
686
  if (currentSessionId === sessionId) {
687
+ types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
626
688
  return;
627
689
  }
628
690
  if (finishedSessions.has(sessionId)) {
691
+ types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
629
692
  return;
630
693
  }
631
694
  if (pendingSessions.has(sessionId)) {
695
+ types.logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
632
696
  return;
633
697
  }
634
698
  if (currentSessionId) {
@@ -673,7 +737,7 @@ function sortKeys(value) {
673
737
  }
674
738
 
675
739
  async function loop(opts) {
676
- let mode = opts.startingMode ?? "interactive";
740
+ let mode = opts.startingMode ?? "local";
677
741
  let currentMessageQueue = new MessageQueue();
678
742
  let sessionId = null;
679
743
  let onMessage = null;
@@ -697,23 +761,38 @@ async function loop(opts) {
697
761
  };
698
762
  while (true) {
699
763
  if (currentMessageQueue.size() > 0) {
700
- mode = "remote";
764
+ if (mode !== "remote") {
765
+ mode = "remote";
766
+ if (opts.onModeChange) {
767
+ opts.onModeChange(mode);
768
+ }
769
+ }
701
770
  continue;
702
771
  }
703
- if (mode === "interactive") {
772
+ if (mode === "local") {
704
773
  let abortedOutside = false;
705
774
  const interactiveAbortController = new AbortController();
706
775
  opts.session.setHandler("switch", () => {
707
776
  if (!interactiveAbortController.signal.aborted) {
708
777
  abortedOutside = true;
709
- mode = "remote";
778
+ if (mode !== "remote") {
779
+ mode = "remote";
780
+ if (opts.onModeChange) {
781
+ opts.onModeChange(mode);
782
+ }
783
+ }
710
784
  interactiveAbortController.abort();
711
785
  }
712
786
  });
713
787
  onMessage = () => {
714
788
  if (!interactiveAbortController.signal.aborted) {
715
789
  abortedOutside = true;
716
- mode = "remote";
790
+ if (mode !== "remote") {
791
+ mode = "remote";
792
+ if (opts.onModeChange) {
793
+ opts.onModeChange(mode);
794
+ }
795
+ }
717
796
  interactiveAbortController.abort();
718
797
  }
719
798
  onMessage = null;
@@ -722,13 +801,15 @@ async function loop(opts) {
722
801
  path: opts.path,
723
802
  sessionId,
724
803
  onSessionFound,
725
- abort: interactiveAbortController.signal
804
+ abort: interactiveAbortController.signal,
805
+ claudeEnvVars: opts.claudeEnvVars,
806
+ claudeArgs: opts.claudeArgs
726
807
  });
727
808
  onMessage = null;
728
809
  if (!abortedOutside) {
729
810
  return;
730
811
  }
731
- if (mode !== "interactive") {
812
+ if (mode !== "local") {
732
813
  console.log("Switching to remote mode...");
733
814
  }
734
815
  }
@@ -742,7 +823,12 @@ async function loop(opts) {
742
823
  });
743
824
  const abortHandler = () => {
744
825
  if (!remoteAbortController.signal.aborted) {
745
- mode = "interactive";
826
+ if (mode !== "local") {
827
+ mode = "local";
828
+ if (opts.onModeChange) {
829
+ opts.onModeChange(mode);
830
+ }
831
+ }
746
832
  remoteAbortController.abort();
747
833
  }
748
834
  if (process.stdin.isTTY) {
@@ -766,7 +852,9 @@ async function loop(opts) {
766
852
  onSessionFound,
767
853
  messages: currentMessageQueue,
768
854
  onAssistantResult: opts.onAssistantResult,
769
- interruptController: opts.interruptController
855
+ interruptController: opts.interruptController,
856
+ claudeEnvVars: opts.claudeEnvVars,
857
+ claudeArgs: opts.claudeArgs
770
858
  });
771
859
  } finally {
772
860
  process.stdin.off("data", abortHandler);
@@ -883,155 +971,375 @@ class InterruptController {
883
971
  }
884
972
  }
885
973
 
886
- var version = "0.1.9";
974
+ var version = "0.1.11";
887
975
  var packageJson = {
888
976
  version: version};
889
977
 
890
- async function startAnthropicActivityProxy(onClaudeActivity) {
891
- const requestTimeouts = /* @__PURE__ */ new Map();
892
- let requestCounter = 0;
893
- let idleTimer = null;
894
- const maxTimeBeforeIdle = 50;
895
- const requestTimeout = 5 * 60 * 1e3;
896
- const cleanupRequest = (requestId, reason) => {
897
- const timeout = requestTimeouts.get(requestId);
898
- if (timeout) {
899
- clearTimeout(timeout);
900
- requestTimeouts.delete(requestId);
901
- types.logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
902
- claudeDidSomeWork();
903
- }
904
- };
905
- const claudeDidSomeWork = () => {
906
- if (idleTimer) clearTimeout(idleTimer);
907
- if (requestTimeouts.size === 0) {
908
- idleTimer = setTimeout(() => {
909
- types.logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
910
- onClaudeActivity("idle");
911
- }, maxTimeBeforeIdle);
912
- }
913
- };
914
- const server = node_http.createServer((req, res) => {
915
- const requestId = ++requestCounter;
916
- const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
917
- if (isAnthropicRequest) {
918
- const timeout = setTimeout(() => {
919
- types.logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
920
- cleanupRequest(requestId, "timeout");
921
- }, requestTimeout);
922
- requestTimeouts.set(requestId, timeout);
923
- onClaudeActivity("working");
924
- types.logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
925
- }
926
- const chunks = [];
927
- req.on("data", (chunk) => {
928
- chunks.push(chunk);
929
- if (isAnthropicRequest) {
930
- claudeDidSomeWork();
931
- }
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
985
+ });
986
+ let stdout = "";
987
+ let stderr = "";
988
+ child.stdout.on("data", (data) => {
989
+ stdout += data.toString();
990
+ });
991
+ child.stderr.on("data", (data) => {
992
+ stderr += data.toString();
932
993
  });
933
- req.on("end", () => {
934
- const body = Buffer.concat(chunks);
935
- let targetUrl;
936
- if (isAnthropicRequest) {
937
- targetUrl = new node_url.URL(req.url || "/", "https://api.anthropic.com");
994
+ child.on("close", (code) => {
995
+ resolve({
996
+ exitCode: code || 0,
997
+ stdout,
998
+ stderr
999
+ });
1000
+ });
1001
+ child.on("error", (err) => {
1002
+ reject(err);
1003
+ });
1004
+ });
1005
+ }
1006
+
1007
+ const execAsync = util.promisify(child_process.exec);
1008
+ function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
1009
+ session.setHandler("abort", async () => {
1010
+ types.logger.info("Abort request - interrupting Claude");
1011
+ await interruptController.interrupt();
1012
+ });
1013
+ if (permissionCallbacks) {
1014
+ session.setHandler("permission", async (message) => {
1015
+ types.logger.info("Permission response" + JSON.stringify(message));
1016
+ const id = message.id;
1017
+ const resolve = permissionCallbacks.requests.get(id);
1018
+ if (resolve) {
1019
+ if (!message.approved) {
1020
+ types.logger.debug("Permission denied, interrupting Claude");
1021
+ await interruptController.interrupt();
1022
+ }
1023
+ resolve({ approved: message.approved, reason: message.reason });
1024
+ permissionCallbacks.requests.delete(id);
938
1025
  } else {
939
- const protocol = req.headers["x-forwarded-proto"] || "https";
940
- const host = req.headers.host || "localhost";
941
- targetUrl = new node_url.URL(req.url || "/", `${protocol}://${host}`);
1026
+ types.logger.info("Permission request stale, likely timed out");
1027
+ return;
942
1028
  }
1029
+ session.updateAgentState((currentState) => {
1030
+ let r = { ...currentState.requests };
1031
+ delete r[id];
1032
+ return {
1033
+ ...currentState,
1034
+ requests: r
1035
+ };
1036
+ });
1037
+ });
1038
+ }
1039
+ session.setHandler("bash", async (data) => {
1040
+ types.logger.info("Shell command request:", data.command);
1041
+ try {
943
1042
  const options = {
944
- hostname: targetUrl.hostname,
945
- port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
946
- path: targetUrl.pathname + targetUrl.search,
947
- method: req.method,
948
- headers: {
949
- ...req.headers,
950
- host: targetUrl.hostname
951
- }
1043
+ cwd: data.cwd,
1044
+ timeout: data.timeout || 3e4
1045
+ // Default 30 seconds timeout
952
1046
  };
953
- const requestMethod = targetUrl.protocol === "https:" ? node_https.request : node_http.request;
954
- const proxyReq = requestMethod(options, (proxyRes) => {
955
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
956
- proxyRes.pipe(res);
957
- proxyRes.on("end", () => {
958
- if (isAnthropicRequest) {
959
- cleanupRequest(requestId, "completed");
1047
+ const { stdout, stderr } = await execAsync(data.command, options);
1048
+ return {
1049
+ success: true,
1050
+ stdout: stdout || "",
1051
+ stderr: stderr || "",
1052
+ exitCode: 0
1053
+ };
1054
+ } catch (error) {
1055
+ const execError = error;
1056
+ if (execError.code === "ETIMEDOUT" || execError.killed) {
1057
+ return {
1058
+ success: false,
1059
+ stdout: execError.stdout || "",
1060
+ stderr: execError.stderr || "",
1061
+ exitCode: typeof execError.code === "number" ? execError.code : -1,
1062
+ error: "Command timed out"
1063
+ };
1064
+ }
1065
+ return {
1066
+ success: false,
1067
+ stdout: execError.stdout || "",
1068
+ stderr: execError.stderr || execError.message || "Command failed",
1069
+ exitCode: typeof execError.code === "number" ? execError.code : 1,
1070
+ error: execError.message || "Command failed"
1071
+ };
1072
+ }
1073
+ });
1074
+ session.setHandler("readFile", async (data) => {
1075
+ types.logger.info("Read file request:", data.path);
1076
+ try {
1077
+ const buffer = await promises.readFile(data.path);
1078
+ const content = buffer.toString("base64");
1079
+ return { success: true, content };
1080
+ } catch (error) {
1081
+ types.logger.debug("Failed to read file:", error);
1082
+ return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
1083
+ }
1084
+ });
1085
+ session.setHandler("writeFile", async (data) => {
1086
+ types.logger.info("Write file request:", data.path);
1087
+ try {
1088
+ if (data.expectedHash !== null && data.expectedHash !== void 0) {
1089
+ try {
1090
+ const existingBuffer = await promises.readFile(data.path);
1091
+ const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
1092
+ if (existingHash !== data.expectedHash) {
1093
+ return {
1094
+ success: false,
1095
+ error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
1096
+ };
1097
+ }
1098
+ } catch (error) {
1099
+ const nodeError = error;
1100
+ if (nodeError.code !== "ENOENT") {
1101
+ throw error;
1102
+ }
1103
+ return {
1104
+ success: false,
1105
+ error: "File does not exist but hash was provided"
1106
+ };
1107
+ }
1108
+ } else {
1109
+ try {
1110
+ await promises.stat(data.path);
1111
+ return {
1112
+ success: false,
1113
+ error: "File already exists but was expected to be new"
1114
+ };
1115
+ } catch (error) {
1116
+ const nodeError = error;
1117
+ if (nodeError.code !== "ENOENT") {
1118
+ throw error;
960
1119
  }
961
- });
962
- });
963
- proxyReq.on("error", (error) => {
964
- if (isAnthropicRequest) {
965
- cleanupRequest(requestId, `error: ${error.message}`);
966
- } else {
967
- types.logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
968
1120
  }
969
- res.writeHead(502);
970
- res.end("Bad Gateway");
971
- });
972
- if (body.length > 0) {
973
- proxyReq.write(body);
974
1121
  }
975
- proxyReq.end();
976
- });
1122
+ const buffer = Buffer.from(data.content, "base64");
1123
+ await promises.writeFile(data.path, buffer);
1124
+ const hash = crypto.createHash("sha256").update(buffer).digest("hex");
1125
+ return { success: true, hash };
1126
+ } catch (error) {
1127
+ types.logger.debug("Failed to write file:", error);
1128
+ return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
1129
+ }
977
1130
  });
978
- server.on("connect", (req, clientSocket, head) => {
979
- const requestId = ++requestCounter;
980
- const [hostname, port] = req.url?.split(":") || ["", "443"];
981
- const isAnthropicRequest = hostname === "api.anthropic.com";
982
- if (isAnthropicRequest) {
983
- const timeout = setTimeout(() => {
984
- types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
985
- cleanupRequest(requestId, "timeout");
986
- }, requestTimeout);
987
- requestTimeouts.set(requestId, timeout);
988
- onClaudeActivity("working");
989
- types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
990
- }
991
- const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
992
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
993
- serverSocket.write(head);
994
- serverSocket.pipe(clientSocket);
995
- clientSocket.pipe(serverSocket);
996
- });
997
- const cleanup = () => {
998
- if (isAnthropicRequest) {
999
- cleanupRequest(requestId, "CONNECT closed");
1131
+ session.setHandler("listDirectory", async (data) => {
1132
+ types.logger.info("List directory request:", data.path);
1133
+ try {
1134
+ const entries = await promises.readdir(data.path, { withFileTypes: true });
1135
+ const directoryEntries = await Promise.all(
1136
+ entries.map(async (entry) => {
1137
+ const fullPath = path.join(data.path, entry.name);
1138
+ let type = "other";
1139
+ let size;
1140
+ let modified;
1141
+ if (entry.isDirectory()) {
1142
+ type = "directory";
1143
+ } else if (entry.isFile()) {
1144
+ type = "file";
1145
+ }
1146
+ try {
1147
+ const stats = await promises.stat(fullPath);
1148
+ size = stats.size;
1149
+ modified = stats.mtime.getTime();
1150
+ } catch (error) {
1151
+ types.logger.debug(`Failed to stat ${fullPath}:`, error);
1152
+ }
1153
+ return {
1154
+ name: entry.name,
1155
+ type,
1156
+ size,
1157
+ modified
1158
+ };
1159
+ })
1160
+ );
1161
+ directoryEntries.sort((a, b) => {
1162
+ if (a.type === "directory" && b.type !== "directory") return -1;
1163
+ if (a.type !== "directory" && b.type === "directory") return 1;
1164
+ return a.name.localeCompare(b.name);
1165
+ });
1166
+ return { success: true, entries: directoryEntries };
1167
+ } catch (error) {
1168
+ types.logger.debug("Failed to list directory:", error);
1169
+ return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
1170
+ }
1171
+ });
1172
+ session.setHandler("getDirectoryTree", async (data) => {
1173
+ types.logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1174
+ async function buildTree(path$1, name, currentDepth) {
1175
+ try {
1176
+ const stats = await promises.stat(path$1);
1177
+ const node = {
1178
+ name,
1179
+ path: path$1,
1180
+ type: stats.isDirectory() ? "directory" : "file",
1181
+ size: stats.size,
1182
+ modified: stats.mtime.getTime()
1183
+ };
1184
+ if (stats.isDirectory() && currentDepth < data.maxDepth) {
1185
+ const entries = await promises.readdir(path$1, { withFileTypes: true });
1186
+ const children = [];
1187
+ await Promise.all(
1188
+ entries.map(async (entry) => {
1189
+ if (entry.isSymbolicLink()) {
1190
+ types.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
1191
+ return;
1192
+ }
1193
+ const childPath = path.join(path$1, entry.name);
1194
+ const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
1195
+ if (childNode) {
1196
+ children.push(childNode);
1197
+ }
1198
+ })
1199
+ );
1200
+ children.sort((a, b) => {
1201
+ if (a.type === "directory" && b.type !== "directory") return -1;
1202
+ if (a.type !== "directory" && b.type === "directory") return 1;
1203
+ return a.name.localeCompare(b.name);
1204
+ });
1205
+ node.children = children;
1206
+ }
1207
+ return node;
1208
+ } catch (error) {
1209
+ types.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
1210
+ return null;
1000
1211
  }
1001
- };
1002
- serverSocket.on("error", (err) => {
1003
- types.logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
1004
- clientSocket.end();
1005
- cleanup();
1006
- });
1007
- clientSocket.on("error", cleanup);
1008
- clientSocket.on("end", cleanup);
1009
- serverSocket.on("end", cleanup);
1212
+ }
1213
+ try {
1214
+ if (data.maxDepth < 0) {
1215
+ return { success: false, error: "maxDepth must be non-negative" };
1216
+ }
1217
+ const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
1218
+ const tree = await buildTree(data.path, baseName, 0);
1219
+ if (!tree) {
1220
+ return { success: false, error: "Failed to access the specified path" };
1221
+ }
1222
+ return { success: true, tree };
1223
+ } catch (error) {
1224
+ types.logger.debug("Failed to get directory tree:", error);
1225
+ return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
1226
+ }
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
+ }
1010
1260
  });
1011
- const url = await new Promise((resolve) => {
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) => {
1012
1275
  server.listen(0, "127.0.0.1", () => {
1013
1276
  const addr = server.address();
1014
1277
  if (addr && typeof addr === "object") {
1015
- resolve(`http://127.0.0.1:${addr.port}`);
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"));
1016
1283
  }
1017
1284
  });
1018
1285
  });
1019
- types.logger.debug(`[AnthropicProxy] Started at ${url}`);
1020
- return {
1021
- url,
1022
- cleanup: () => {
1023
- if (idleTimer) clearTimeout(idleTimer);
1024
- for (const [requestId, timeout] of requestTimeouts) {
1025
- clearTimeout(timeout);
1026
- types.logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
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
+ }
1027
1310
  }
1028
- requestTimeouts.clear();
1029
- if (requestTimeouts.size > 0) {
1030
- types.logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
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
+ });
1031
1339
  }
1032
- server.close();
1033
1340
  }
1034
- };
1341
+ });
1342
+ return proxyUrl;
1035
1343
  }
1036
1344
 
1037
1345
  async function start(credentials, options = {}) {
@@ -1039,30 +1347,23 @@ async function start(credentials, options = {}) {
1039
1347
  const sessionTag = node_crypto.randomUUID();
1040
1348
  const api = new types.ApiClient(credentials.token, credentials.secret);
1041
1349
  let state = {};
1042
- let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
1350
+ let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
1043
1351
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1044
1352
  types.logger.debug(`Session created: ${response.id}`);
1045
1353
  const session = api.session(response);
1046
1354
  const pushClient = api.push();
1047
1355
  let thinking = false;
1356
+ let mode = "local";
1048
1357
  let pingInterval = setInterval(() => {
1049
- session.keepAlive(thinking);
1358
+ session.keepAlive(thinking, mode);
1050
1359
  }, 2e3);
1051
- const antropicActivityProxy = await startAnthropicActivityProxy(
1052
- (activity) => {
1053
- const newThinking = activity === "working";
1054
- if (newThinking !== thinking) {
1055
- thinking = newThinking;
1056
- types.logger.debug(`[PING] Thinking state changed: ${thinking}`);
1057
- session.keepAlive(thinking);
1058
- }
1059
- }
1060
- );
1061
- process.env.HTTP_PROXY = antropicActivityProxy.url;
1062
- process.env.HTTPS_PROXY = antropicActivityProxy.url;
1063
- 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;
1064
1365
  const logPath = await types.logger.logFilePathPromise;
1065
- types.logger.info(`Session: ${response.id}`);
1366
+ types.logger.infoDeveloper(`Session: ${response.id}`);
1066
1367
  types.logger.infoDeveloper(`Logs: ${logPath}`);
1067
1368
  const interruptController = new InterruptController();
1068
1369
  let requests = /* @__PURE__ */ new Map();
@@ -1072,10 +1373,10 @@ async function start(credentials, options = {}) {
1072
1373
  requests.set(id, resolve);
1073
1374
  });
1074
1375
  let timeout = setTimeout(async () => {
1075
- types.logger.info("Permission timeout - attempting to interrupt Claude");
1376
+ types.logger.debug("Permission timeout - attempting to interrupt Claude");
1076
1377
  const interrupted = await interruptController.interrupt();
1077
1378
  if (interrupted) {
1078
- types.logger.info("Claude interrupted successfully");
1379
+ types.logger.debug("Claude interrupted successfully");
1079
1380
  }
1080
1381
  requests.delete(id);
1081
1382
  session.updateAgentState((currentState) => {
@@ -1087,7 +1388,7 @@ async function start(credentials, options = {}) {
1087
1388
  };
1088
1389
  });
1089
1390
  }, 1e3 * 60 * 4.5);
1090
- types.logger.info("Permission request" + id + " " + JSON.stringify(request));
1391
+ types.logger.debug("Permission request" + id + " " + JSON.stringify(request));
1091
1392
  try {
1092
1393
  await pushClient.sendToAllDevices(
1093
1394
  "Permission Request",
@@ -1099,7 +1400,7 @@ async function start(credentials, options = {}) {
1099
1400
  type: "permission_request"
1100
1401
  }
1101
1402
  );
1102
- types.logger.info("Push notification sent for permission request");
1403
+ types.logger.debug("Push notification sent for permission request");
1103
1404
  } catch (error) {
1104
1405
  types.logger.debug("Failed to send push notification:", error);
1105
1406
  }
@@ -1116,29 +1417,7 @@ async function start(credentials, options = {}) {
1116
1417
  promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1117
1418
  return promise;
1118
1419
  });
1119
- session.setHandler("permission", (message) => {
1120
- types.logger.info("Permission response" + JSON.stringify(message));
1121
- const id = message.id;
1122
- const resolve = requests.get(id);
1123
- if (resolve) {
1124
- resolve({ approved: message.approved, reason: message.reason });
1125
- } else {
1126
- types.logger.info("Permission request stale, likely timed out");
1127
- return;
1128
- }
1129
- session.updateAgentState((currentState) => {
1130
- let r = { ...currentState.requests };
1131
- delete r[id];
1132
- return {
1133
- ...currentState,
1134
- requests: r
1135
- };
1136
- });
1137
- });
1138
- session.setHandler("abort", async () => {
1139
- types.logger.info("Abort request - interrupting Claude");
1140
- await interruptController.interrupt();
1141
- });
1420
+ registerHandlers(session, interruptController, { requests });
1142
1421
  const onAssistantResult = async (result) => {
1143
1422
  try {
1144
1423
  const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
@@ -1163,6 +1442,15 @@ async function start(credentials, options = {}) {
1163
1442
  model: options.model,
1164
1443
  permissionMode: options.permissionMode,
1165
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
+ },
1166
1454
  mcpServers: {
1167
1455
  "permission": {
1168
1456
  type: "http",
@@ -1172,16 +1460,34 @@ async function start(credentials, options = {}) {
1172
1460
  permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
1173
1461
  session,
1174
1462
  onAssistantResult,
1175
- interruptController
1463
+ interruptController,
1464
+ claudeEnvVars: options.claudeEnvVars,
1465
+ claudeArgs: options.claudeArgs
1176
1466
  });
1177
1467
  clearInterval(pingInterval);
1178
- if (antropicActivityProxy) {
1179
- types.logger.info("[AnthropicProxy] Shutting down activity monitoring proxy");
1180
- antropicActivityProxy.cleanup();
1181
- }
1182
1468
  process.exit(0);
1183
1469
  }
1184
1470
 
1471
+ const defaultSettings = {
1472
+ onboardingCompleted: false
1473
+ };
1474
+ async function readSettings() {
1475
+ if (!node_fs.existsSync(types.configuration.settingsFile)) {
1476
+ return { ...defaultSettings };
1477
+ }
1478
+ try {
1479
+ const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
1480
+ return JSON.parse(content);
1481
+ } catch {
1482
+ return { ...defaultSettings };
1483
+ }
1484
+ }
1485
+ async function writeSettings(settings) {
1486
+ if (!node_fs.existsSync(types.configuration.happyDir)) {
1487
+ await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
1488
+ }
1489
+ await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
1490
+ }
1185
1491
  const credentialsSchema = z__namespace.object({
1186
1492
  secret: z__namespace.string().base64(),
1187
1493
  token: z__namespace.string()
@@ -1191,7 +1497,7 @@ async function readCredentials() {
1191
1497
  return null;
1192
1498
  }
1193
1499
  try {
1194
- const keyBase64 = await promises.readFile(types.configuration.privateKeyFile, "utf8");
1500
+ const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
1195
1501
  const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
1196
1502
  return {
1197
1503
  secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
@@ -1203,9 +1509,9 @@ async function readCredentials() {
1203
1509
  }
1204
1510
  async function writeCredentials(credentials) {
1205
1511
  if (!node_fs.existsSync(types.configuration.happyDir)) {
1206
- await promises.mkdir(types.configuration.happyDir, { recursive: true });
1512
+ await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
1207
1513
  }
1208
- await promises.writeFile(types.configuration.privateKeyFile, JSON.stringify({
1514
+ await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
1209
1515
  secret: types.encodeBase64(credentials.secret),
1210
1516
  token: credentials.token
1211
1517
  }, null, 2));
@@ -1283,6 +1589,413 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1283
1589
  return decrypted;
1284
1590
  }
1285
1591
 
1592
+ class ApiDaemonSession extends node_events.EventEmitter {
1593
+ socket;
1594
+ machineIdentity;
1595
+ keepAliveInterval = null;
1596
+ token;
1597
+ secret;
1598
+ constructor(token, secret, machineIdentity) {
1599
+ super();
1600
+ this.token = token;
1601
+ this.secret = secret;
1602
+ this.machineIdentity = machineIdentity;
1603
+ const socket = socket_ioClient.io(types.configuration.serverUrl, {
1604
+ auth: {
1605
+ token: this.token,
1606
+ clientType: "machine-scoped",
1607
+ machineId: this.machineIdentity.machineId
1608
+ },
1609
+ path: "/v1/user-machine-daemon",
1610
+ reconnection: true,
1611
+ reconnectionAttempts: Infinity,
1612
+ reconnectionDelay: 1e3,
1613
+ reconnectionDelayMax: 5e3,
1614
+ transports: ["websocket"],
1615
+ withCredentials: true,
1616
+ autoConnect: false
1617
+ });
1618
+ socket.on("connect", () => {
1619
+ types.logger.debug("[DAEMON] Connected to server");
1620
+ this.emit("connected");
1621
+ socket.emit("machine-connect", {
1622
+ token: this.token,
1623
+ machineIdentity: types.encodeBase64(types.encrypt(this.machineIdentity, this.secret))
1624
+ });
1625
+ this.startKeepAlive();
1626
+ });
1627
+ socket.on("disconnect", () => {
1628
+ types.logger.debug("[DAEMON] Disconnected from server");
1629
+ this.emit("disconnected");
1630
+ this.stopKeepAlive();
1631
+ });
1632
+ socket.on("spawn-session", async (encryptedData, callback) => {
1633
+ let requestData;
1634
+ try {
1635
+ requestData = types.decrypt(types.decodeBase64(encryptedData), this.secret);
1636
+ types.logger.debug("[DAEMON] Received spawn-session request", requestData);
1637
+ const args = [
1638
+ "--directory",
1639
+ requestData.directory,
1640
+ "--happy-starting-mode",
1641
+ requestData.startingMode
1642
+ ];
1643
+ if (requestData.metadata) {
1644
+ args.push("--metadata", requestData.metadata);
1645
+ }
1646
+ if (requestData.startingMode === "interactive" && process.platform === "darwin") {
1647
+ const script = `
1648
+ tell application "Terminal"
1649
+ activate
1650
+ do script "cd ${requestData.directory} && happy ${args.join(" ")}"
1651
+ end tell
1652
+ `;
1653
+ child_process.spawn("osascript", ["-e", script], { detached: true });
1654
+ } else {
1655
+ const child = child_process.spawn("happy", args, {
1656
+ detached: true,
1657
+ stdio: "ignore",
1658
+ cwd: requestData.directory
1659
+ });
1660
+ child.unref();
1661
+ }
1662
+ const result = { success: true };
1663
+ socket.emit("session-spawn-result", {
1664
+ requestId: requestData.requestId,
1665
+ result: types.encodeBase64(types.encrypt(result, this.secret))
1666
+ });
1667
+ callback(types.encodeBase64(types.encrypt({ success: true }, this.secret)));
1668
+ } catch (error) {
1669
+ types.logger.debug("[DAEMON] Failed to spawn session", error);
1670
+ const errorResult = {
1671
+ success: false,
1672
+ error: error instanceof Error ? error.message : "Unknown error"
1673
+ };
1674
+ socket.emit("session-spawn-result", {
1675
+ requestId: requestData?.requestId || "",
1676
+ result: types.encodeBase64(types.encrypt(errorResult, this.secret))
1677
+ });
1678
+ callback(types.encodeBase64(types.encrypt(errorResult, this.secret)));
1679
+ }
1680
+ });
1681
+ socket.on("daemon-command", (data) => {
1682
+ switch (data.command) {
1683
+ case "shutdown":
1684
+ this.shutdown();
1685
+ break;
1686
+ case "status":
1687
+ this.emit("status-request");
1688
+ break;
1689
+ }
1690
+ });
1691
+ this.socket = socket;
1692
+ }
1693
+ startKeepAlive() {
1694
+ this.stopKeepAlive();
1695
+ this.keepAliveInterval = setInterval(() => {
1696
+ this.socket.volatile.emit("machine-alive", {
1697
+ time: Date.now()
1698
+ });
1699
+ }, 2e4);
1700
+ }
1701
+ stopKeepAlive() {
1702
+ if (this.keepAliveInterval) {
1703
+ clearInterval(this.keepAliveInterval);
1704
+ this.keepAliveInterval = null;
1705
+ }
1706
+ }
1707
+ connect() {
1708
+ this.socket.connect();
1709
+ }
1710
+ shutdown() {
1711
+ this.stopKeepAlive();
1712
+ this.socket.close();
1713
+ this.emit("shutdown");
1714
+ }
1715
+ }
1716
+
1717
+ async function startDaemon() {
1718
+ console.log("[DAEMON] Starting daemon process...");
1719
+ if (await isDaemonRunning()) {
1720
+ console.log("Happy daemon is already running");
1721
+ process.exit(0);
1722
+ }
1723
+ console.log("[DAEMON] Writing PID file with PID:", process.pid);
1724
+ writePidFile();
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
+ });
1736
+ try {
1737
+ const settings = await readSettings() || { onboardingCompleted: false };
1738
+ if (!settings.machineId) {
1739
+ settings.machineId = crypto.randomUUID();
1740
+ settings.machineHost = os$1.hostname();
1741
+ await writeSettings(settings);
1742
+ }
1743
+ const machineIdentity = {
1744
+ machineId: settings.machineId,
1745
+ machineHost: settings.machineHost || os$1.hostname(),
1746
+ platform: process.platform,
1747
+ version: process.env.npm_package_version || "unknown"
1748
+ };
1749
+ let credentials = await readCredentials();
1750
+ if (!credentials) {
1751
+ types.logger.debug("[DAEMON] No credentials found, running auth");
1752
+ await doAuth();
1753
+ credentials = await readCredentials();
1754
+ if (!credentials) {
1755
+ throw new Error("Failed to authenticate");
1756
+ }
1757
+ }
1758
+ const { token, secret } = credentials;
1759
+ const daemon = new ApiDaemonSession(token, secret, machineIdentity);
1760
+ daemon.on("connected", () => {
1761
+ types.logger.debug("[DAEMON] Successfully connected to server");
1762
+ });
1763
+ daemon.on("disconnected", () => {
1764
+ types.logger.debug("[DAEMON] Disconnected from server");
1765
+ });
1766
+ daemon.on("shutdown", () => {
1767
+ types.logger.debug("[DAEMON] Shutdown requested");
1768
+ stopDaemon();
1769
+ process.exit(0);
1770
+ });
1771
+ daemon.connect();
1772
+ setInterval(() => {
1773
+ }, 1e3);
1774
+ } catch (error) {
1775
+ types.logger.debug("[DAEMON] Failed to start daemon", error);
1776
+ stopDaemon();
1777
+ process.exit(1);
1778
+ }
1779
+ process.on("SIGINT", () => process.exit(0));
1780
+ process.on("SIGTERM", () => process.exit(0));
1781
+ process.on("exit", () => process.exit(0));
1782
+ while (true) {
1783
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1784
+ }
1785
+ }
1786
+ async function isDaemonRunning() {
1787
+ try {
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");
1812
+ }
1813
+ return false;
1814
+ } catch (error) {
1815
+ console.log("[isDaemonRunning] Error:", error);
1816
+ types.logger.debug("[DAEMON] Error checking daemon status", error);
1817
+ return false;
1818
+ }
1819
+ }
1820
+ function writePidFile() {
1821
+ const happyDir = path.join(os$1.homedir(), ".happy");
1822
+ if (!fs.existsSync(happyDir)) {
1823
+ fs.mkdirSync(happyDir, { recursive: true });
1824
+ }
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
+ }
1834
+ }
1835
+ async function stopDaemon() {
1836
+ try {
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);
1852
+ }
1853
+ } catch (error) {
1854
+ types.logger.debug("[DAEMON] Error stopping daemon", error);
1855
+ }
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
+ }
1873
+
1874
+ function trimIdent(text) {
1875
+ const lines = text.split("\n");
1876
+ while (lines.length > 0 && lines[0].trim() === "") {
1877
+ lines.shift();
1878
+ }
1879
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1880
+ lines.pop();
1881
+ }
1882
+ const minSpaces = lines.reduce((min, line) => {
1883
+ if (line.trim() === "") {
1884
+ return min;
1885
+ }
1886
+ const leadingSpaces = line.match(/^\s*/)[0].length;
1887
+ return Math.min(min, leadingSpaces);
1888
+ }, Infinity);
1889
+ const trimmedLines = lines.map((line) => line.slice(minSpaces));
1890
+ return trimmedLines.join("\n");
1891
+ }
1892
+
1893
+ const PLIST_LABEL$1 = "com.happy-cli.daemon";
1894
+ const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
1895
+ const USER_HOME = process.env.HOME || process.env.USERPROFILE;
1896
+ async function install$1() {
1897
+ try {
1898
+ if (fs.existsSync(PLIST_FILE$1)) {
1899
+ types.logger.info("Daemon plist already exists. Uninstalling first...");
1900
+ child_process.execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
1901
+ }
1902
+ const happyPath = process.argv[0];
1903
+ const scriptPath = process.argv[1];
1904
+ const plistContent = trimIdent(`
1905
+ <?xml version="1.0" encoding="UTF-8"?>
1906
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1907
+ <plist version="1.0">
1908
+ <dict>
1909
+ <key>Label</key>
1910
+ <string>${PLIST_LABEL$1}</string>
1911
+
1912
+ <key>ProgramArguments</key>
1913
+ <array>
1914
+ <string>${happyPath}</string>
1915
+ <string>${scriptPath}</string>
1916
+ <string>happy-daemon</string>
1917
+ </array>
1918
+
1919
+ <key>EnvironmentVariables</key>
1920
+ <dict>
1921
+ <key>HAPPY_DAEMON_MODE</key>
1922
+ <string>true</string>
1923
+ </dict>
1924
+
1925
+ <key>RunAtLoad</key>
1926
+ <true/>
1927
+
1928
+ <key>KeepAlive</key>
1929
+ <true/>
1930
+
1931
+ <key>StandardErrorPath</key>
1932
+ <string>${USER_HOME}/.happy/daemon.err</string>
1933
+
1934
+ <key>StandardOutPath</key>
1935
+ <string>${USER_HOME}/.happy/daemon.log</string>
1936
+
1937
+ <key>WorkingDirectory</key>
1938
+ <string>/tmp</string>
1939
+ </dict>
1940
+ </plist>
1941
+ `);
1942
+ fs.writeFileSync(PLIST_FILE$1, plistContent);
1943
+ fs.chmodSync(PLIST_FILE$1, 420);
1944
+ types.logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
1945
+ child_process.execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
1946
+ types.logger.info("Daemon installed and started successfully");
1947
+ types.logger.info("Check logs at ~/.happy/daemon.log");
1948
+ } catch (error) {
1949
+ types.logger.debug("Failed to install daemon:", error);
1950
+ throw error;
1951
+ }
1952
+ }
1953
+
1954
+ async function install() {
1955
+ if (process.platform !== "darwin") {
1956
+ throw new Error("Daemon installation is currently only supported on macOS");
1957
+ }
1958
+ if (process.getuid && process.getuid() !== 0) {
1959
+ throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
1960
+ }
1961
+ types.logger.info("Installing Happy CLI daemon for macOS...");
1962
+ await install$1();
1963
+ }
1964
+
1965
+ const PLIST_LABEL = "com.happy-cli.daemon";
1966
+ const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
1967
+ async function uninstall$1() {
1968
+ try {
1969
+ if (!fs.existsSync(PLIST_FILE)) {
1970
+ types.logger.info("Daemon plist not found. Nothing to uninstall.");
1971
+ return;
1972
+ }
1973
+ try {
1974
+ child_process.execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
1975
+ types.logger.info("Daemon stopped successfully");
1976
+ } catch (error) {
1977
+ types.logger.info("Failed to unload daemon (it might not be running)");
1978
+ }
1979
+ fs.unlinkSync(PLIST_FILE);
1980
+ types.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
1981
+ types.logger.info("Daemon uninstalled successfully");
1982
+ } catch (error) {
1983
+ types.logger.debug("Failed to uninstall daemon:", error);
1984
+ throw error;
1985
+ }
1986
+ }
1987
+
1988
+ async function uninstall() {
1989
+ if (process.platform !== "darwin") {
1990
+ throw new Error("Daemon uninstallation is currently only supported on macOS");
1991
+ }
1992
+ if (process.getuid && process.getuid() !== 0) {
1993
+ throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
1994
+ }
1995
+ types.logger.info("Uninstalling Happy CLI daemon for macOS...");
1996
+ await uninstall$1();
1997
+ }
1998
+
1286
1999
  (async () => {
1287
2000
  const args = process.argv.slice(2);
1288
2001
  let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
@@ -1302,39 +2015,40 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1302
2015
  }
1303
2016
  return;
1304
2017
  } else if (subcommand === "daemon") {
1305
- if (process.env.HAPPY_DAEMON_MODE) {
1306
- const { run } = await Promise.resolve().then(function () { return require('./run-q2To6b-c.cjs'); });
1307
- await run();
2018
+ const daemonSubcommand = args[1];
2019
+ if (daemonSubcommand === "start") {
2020
+ await startDaemon();
2021
+ process.exit(0);
2022
+ } else if (daemonSubcommand === "stop") {
2023
+ await stopDaemon();
2024
+ process.exit(0);
2025
+ } else if (daemonSubcommand === "install") {
2026
+ try {
2027
+ await install();
2028
+ } catch (error) {
2029
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
2030
+ process.exit(1);
2031
+ }
2032
+ } else if (daemonSubcommand === "uninstall") {
2033
+ try {
2034
+ await uninstall();
2035
+ } catch (error) {
2036
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
2037
+ process.exit(1);
2038
+ }
1308
2039
  } else {
1309
- const daemonSubcommand = args[1];
1310
- if (daemonSubcommand === "install") {
1311
- const { install } = await Promise.resolve().then(function () { return require('./install-B2r_gX72.cjs'); });
1312
- try {
1313
- await install();
1314
- } catch (error) {
1315
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1316
- process.exit(1);
1317
- }
1318
- } else if (daemonSubcommand === "uninstall") {
1319
- const { uninstall } = await Promise.resolve().then(function () { return require('./uninstall-C42CoSCI.cjs'); });
1320
- try {
1321
- await uninstall();
1322
- } catch (error) {
1323
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1324
- process.exit(1);
1325
- }
1326
- } else {
1327
- console.log(`
2040
+ console.log(`
1328
2041
  ${chalk.bold("happy daemon")} - Daemon management
1329
2042
 
1330
2043
  ${chalk.bold("Usage:")}
2044
+ happy daemon start Start the daemon
2045
+ happy daemon stop Stop the daemon
1331
2046
  sudo happy daemon install Install the daemon (requires sudo)
1332
2047
  sudo happy daemon uninstall Uninstall the daemon (requires sudo)
1333
2048
 
1334
2049
  ${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
1335
2050
  Currently only supported on macOS.
1336
2051
  `);
1337
- }
1338
2052
  }
1339
2053
  return;
1340
2054
  } else {
@@ -1357,7 +2071,18 @@ Currently only supported on macOS.
1357
2071
  } else if (arg === "--local") {
1358
2072
  i++;
1359
2073
  } else if (arg === "--happy-starting-mode") {
1360
- 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];
1361
2086
  } else {
1362
2087
  console.error(chalk.red(`Unknown argument: ${arg}`));
1363
2088
  process.exit(1);
@@ -1378,20 +2103,31 @@ ${chalk.bold("Options:")}
1378
2103
  -m, --model <model> Claude model to use (default: sonnet)
1379
2104
  -p, --permission-mode Permission mode: auto, default, or plan
1380
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
1381
2114
 
1382
2115
  [Advanced]
1383
2116
  --local < global | local >
1384
2117
  Will use .happy folder in the current directory for storing your private key and debug logs.
1385
2118
  You will require re-login each time you run this in a new directory.
1386
-
1387
- --happy-starting-mode <mode> Start in specified mode (interactive or remote)
1388
- Default: interactive
2119
+ --happy-starting-mode <interactive|remote>
2120
+ Set the starting mode for new sessions (default: remote)
1389
2121
 
1390
2122
  ${chalk.bold("Examples:")}
1391
2123
  happy Start a session with default settings
1392
2124
  happy -m opus Use Claude Opus model
1393
2125
  happy -p plan Use plan permission mode
1394
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
1395
2131
  happy logout Logs out of your account and removes data directory
1396
2132
  `);
1397
2133
  process.exit(0);
@@ -1408,6 +2144,7 @@ ${chalk.bold("Examples:")}
1408
2144
  }
1409
2145
  credentials = res;
1410
2146
  }
2147
+ await readSettings() || { };
1411
2148
  try {
1412
2149
  await start(credentials, options);
1413
2150
  } catch (error) {