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.mjs CHANGED
@@ -1,27 +1,33 @@
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, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-fXgEaaqP.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 } 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';
18
+ import { spawn as spawn$1, exec, execSync } from 'child_process';
19
+ import { promisify } from 'util';
20
+ import crypto, { createHash } from 'crypto';
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';
19
24
  import tweetnacl from 'tweetnacl';
20
25
  import axios from 'axios';
21
26
  import qrcode from 'qrcode-terminal';
22
- import 'fs';
23
- import 'node:events';
24
- import 'socket.io-client';
27
+ import { EventEmitter } from 'node:events';
28
+ import { io } from 'socket.io-client';
29
+ import { hostname, homedir as homedir$1 } from 'os';
30
+ import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
25
31
  import 'expo-server-sdk';
26
32
 
27
33
  function formatClaudeMessage(message, onAssistantResult) {
@@ -144,9 +150,13 @@ function printDivider() {
144
150
  console.log(chalk.gray("\u2550".repeat(60)));
145
151
  }
146
152
 
153
+ function getProjectPath(workingDirectory) {
154
+ const projectId = resolve(workingDirectory).replace(/[\\\/\.:]/g, "-");
155
+ return join(homedir(), ".claude", "projects", projectId);
156
+ }
157
+
147
158
  function claudeCheckSession(sessionId, path) {
148
- const projectName = resolve(path).replace(/\//g, "-");
149
- const projectDir = join(homedir(), ".claude", "projects", projectName);
159
+ const projectDir = getProjectPath(path);
150
160
  const sessionFile = join(projectDir, `${sessionId}.jsonl`);
151
161
  const sessionExists = existsSync(sessionFile);
152
162
  if (!sessionExists) {
@@ -164,11 +174,29 @@ function claudeCheckSession(sessionId, path) {
164
174
  return hasGoodMessage;
165
175
  }
166
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
+
167
190
  async function claudeRemote(opts) {
168
191
  let startFrom = opts.sessionId;
169
192
  if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
170
193
  startFrom = null;
171
194
  }
195
+ if (opts.claudeEnvVars) {
196
+ Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
197
+ process.env[key] = value;
198
+ });
199
+ }
172
200
  const abortController = new AbortController();
173
201
  const sdkOptions = {
174
202
  cwd: opts.path,
@@ -178,6 +206,9 @@ async function claudeRemote(opts) {
178
206
  executable: "node",
179
207
  abortController
180
208
  };
209
+ if (opts.claudeArgs && opts.claudeArgs.length > 0) {
210
+ sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
211
+ }
181
212
  let aborted = false;
182
213
  let response;
183
214
  opts.abort.addEventListener("abort", () => {
@@ -215,15 +246,11 @@ async function claudeRemote(opts) {
215
246
  logger.debug(`[claudeRemote] Received message from SDK: ${message.type}`);
216
247
  formatClaudeMessage(message, opts.onAssistantResult);
217
248
  if (message.type === "system" && message.subtype === "init") {
218
- const projectName = resolve(opts.path).replace(/\//g, "-");
219
- const projectDir = join(homedir(), ".claude", "projects", projectName);
220
- mkdirSync(projectDir, { recursive: true });
221
- const watcher = watch(projectDir).on("change", (_, filename) => {
222
- if (filename === `${message.session_id}.jsonl`) {
223
- opts.onSessionFound(message.session_id);
224
- watcher.close();
225
- }
226
- });
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);
227
254
  }
228
255
  }
229
256
  logger.debug(`[claudeRemote] Finished iterating over response`);
@@ -245,10 +272,9 @@ async function claudeRemote(opts) {
245
272
  logger.debug(`[claudeRemote] Function completed`);
246
273
  }
247
274
 
248
- const __dirname = dirname(fileURLToPath(import.meta.url));
275
+ const __dirname$1 = dirname(fileURLToPath(import.meta.url));
249
276
  async function claudeLocal(opts) {
250
- const projectName = resolve(opts.path).replace(/\//g, "-");
251
- const projectDir = join(homedir(), ".claude", "projects", projectName);
277
+ const projectDir = getProjectPath(opts.path);
252
278
  mkdirSync(projectDir, { recursive: true });
253
279
  const watcher = watch(projectDir);
254
280
  let resolvedSessionId = null;
@@ -282,11 +308,19 @@ async function claudeLocal(opts) {
282
308
  if (startFrom) {
283
309
  args.push("--resume", startFrom);
284
310
  }
285
- 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
+ };
286
319
  const child = spawn("node", [claudeCliPath, ...args], {
287
320
  stdio: ["inherit", "inherit", "inherit", "pipe"],
288
321
  signal: opts.abort,
289
- cwd: opts.path
322
+ cwd: opts.path,
323
+ env
290
324
  });
291
325
  if (child.stdio[3]) {
292
326
  const rl = createInterface({
@@ -511,16 +545,44 @@ class InvalidateSync {
511
545
  };
512
546
  }
513
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
+
514
576
  function createSessionScanner(opts) {
515
- const projectName = resolve(opts.workingDirectory).replace(/\//g, "-");
516
- const projectDir = join(homedir(), ".claude", "projects", projectName);
577
+ const projectDir = getProjectPath(opts.workingDirectory);
517
578
  let finishedSessions = /* @__PURE__ */ new Set();
518
579
  let pendingSessions = /* @__PURE__ */ new Set();
519
580
  let currentSessionId = null;
520
- let currentSessionWatcherAbortController = null;
581
+ let watchers = /* @__PURE__ */ new Map();
521
582
  let processedMessages = /* @__PURE__ */ new Set();
522
583
  let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
523
584
  const sync = new InvalidateSync(async () => {
585
+ logger.debug(`[SESSION_SCANNER] Syncing...`);
524
586
  let sessions = [];
525
587
  for (let p of pendingSessions) {
526
588
  sessions.push(p);
@@ -534,11 +596,15 @@ function createSessionScanner(opts) {
534
596
  try {
535
597
  file = await readFile(expectedSessionFile, "utf-8");
536
598
  } catch (error) {
599
+ logger.debug(`[SESSION_SCANNER] Session file not found: ${expectedSessionFile}`);
537
600
  return;
538
601
  }
539
602
  let lines = file.split("\n");
540
603
  for (let l of lines) {
541
604
  try {
605
+ if (l.trim() === "") {
606
+ continue;
607
+ }
542
608
  let message = JSON.parse(l);
543
609
  let parsed = RawJSONLinesSchema.safeParse(message);
544
610
  if (!parsed.success) {
@@ -552,15 +618,16 @@ function createSessionScanner(opts) {
552
618
  processedMessages.add(key);
553
619
  logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
554
620
  logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
555
- if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
621
+ if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
556
622
  const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
557
623
  if (currentCounter && currentCounter > 0) {
558
624
  seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
559
625
  continue;
560
626
  }
561
627
  }
562
- opts.onMessage(parsed.data);
628
+ opts.onMessage(message);
563
629
  } catch (e) {
630
+ logger.debug(`[SESSION_SCANNER] Error processing message: ${e}`);
564
631
  continue;
565
632
  }
566
633
  }
@@ -574,40 +641,37 @@ function createSessionScanner(opts) {
574
641
  finishedSessions.add(p);
575
642
  }
576
643
  }
577
- currentSessionWatcherAbortController?.abort();
578
- currentSessionWatcherAbortController = new AbortController();
579
- void (async () => {
580
- if (currentSessionId) {
581
- const sessionFile = join(projectDir, `${currentSessionId}.jsonl`);
582
- try {
583
- for await (const change of watch$1(sessionFile, { persistent: true, signal: currentSessionWatcherAbortController.signal })) {
584
- await processSessionFile(currentSessionId);
585
- }
586
- } catch (error) {
587
- if (error.name !== "AbortError") {
588
- logger.debug(`[SESSION_SCANNER] Watch error: ${error.message}`);
589
- }
590
- }
644
+ for (let p of sessions) {
645
+ if (!watchers.has(p)) {
646
+ watchers.set(p, startFileWatcher(join(projectDir, `${p}.jsonl`), () => {
647
+ sync.invalidate();
648
+ }));
591
649
  }
592
- })();
650
+ }
593
651
  });
652
+ sync.invalidate();
594
653
  const intervalId = setInterval(() => {
595
654
  sync.invalidate();
596
655
  }, 3e3);
597
656
  return {
598
- refresh: () => sync.invalidate(),
599
657
  cleanup: () => {
600
658
  clearInterval(intervalId);
601
- currentSessionWatcherAbortController?.abort();
659
+ for (let w of watchers.values()) {
660
+ w();
661
+ }
662
+ watchers.clear();
602
663
  },
603
664
  onNewSession: (sessionId) => {
604
665
  if (currentSessionId === sessionId) {
666
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
605
667
  return;
606
668
  }
607
669
  if (finishedSessions.has(sessionId)) {
670
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already finished, skipping`);
608
671
  return;
609
672
  }
610
673
  if (pendingSessions.has(sessionId)) {
674
+ logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
611
675
  return;
612
676
  }
613
677
  if (currentSessionId) {
@@ -652,7 +716,7 @@ function sortKeys(value) {
652
716
  }
653
717
 
654
718
  async function loop(opts) {
655
- let mode = opts.startingMode ?? "interactive";
719
+ let mode = opts.startingMode ?? "local";
656
720
  let currentMessageQueue = new MessageQueue();
657
721
  let sessionId = null;
658
722
  let onMessage = null;
@@ -676,23 +740,38 @@ async function loop(opts) {
676
740
  };
677
741
  while (true) {
678
742
  if (currentMessageQueue.size() > 0) {
679
- mode = "remote";
743
+ if (mode !== "remote") {
744
+ mode = "remote";
745
+ if (opts.onModeChange) {
746
+ opts.onModeChange(mode);
747
+ }
748
+ }
680
749
  continue;
681
750
  }
682
- if (mode === "interactive") {
751
+ if (mode === "local") {
683
752
  let abortedOutside = false;
684
753
  const interactiveAbortController = new AbortController();
685
754
  opts.session.setHandler("switch", () => {
686
755
  if (!interactiveAbortController.signal.aborted) {
687
756
  abortedOutside = true;
688
- mode = "remote";
757
+ if (mode !== "remote") {
758
+ mode = "remote";
759
+ if (opts.onModeChange) {
760
+ opts.onModeChange(mode);
761
+ }
762
+ }
689
763
  interactiveAbortController.abort();
690
764
  }
691
765
  });
692
766
  onMessage = () => {
693
767
  if (!interactiveAbortController.signal.aborted) {
694
768
  abortedOutside = true;
695
- mode = "remote";
769
+ if (mode !== "remote") {
770
+ mode = "remote";
771
+ if (opts.onModeChange) {
772
+ opts.onModeChange(mode);
773
+ }
774
+ }
696
775
  interactiveAbortController.abort();
697
776
  }
698
777
  onMessage = null;
@@ -701,13 +780,15 @@ async function loop(opts) {
701
780
  path: opts.path,
702
781
  sessionId,
703
782
  onSessionFound,
704
- abort: interactiveAbortController.signal
783
+ abort: interactiveAbortController.signal,
784
+ claudeEnvVars: opts.claudeEnvVars,
785
+ claudeArgs: opts.claudeArgs
705
786
  });
706
787
  onMessage = null;
707
788
  if (!abortedOutside) {
708
789
  return;
709
790
  }
710
- if (mode !== "interactive") {
791
+ if (mode !== "local") {
711
792
  console.log("Switching to remote mode...");
712
793
  }
713
794
  }
@@ -721,7 +802,12 @@ async function loop(opts) {
721
802
  });
722
803
  const abortHandler = () => {
723
804
  if (!remoteAbortController.signal.aborted) {
724
- mode = "interactive";
805
+ if (mode !== "local") {
806
+ mode = "local";
807
+ if (opts.onModeChange) {
808
+ opts.onModeChange(mode);
809
+ }
810
+ }
725
811
  remoteAbortController.abort();
726
812
  }
727
813
  if (process.stdin.isTTY) {
@@ -745,7 +831,9 @@ async function loop(opts) {
745
831
  onSessionFound,
746
832
  messages: currentMessageQueue,
747
833
  onAssistantResult: opts.onAssistantResult,
748
- interruptController: opts.interruptController
834
+ interruptController: opts.interruptController,
835
+ claudeEnvVars: opts.claudeEnvVars,
836
+ claudeArgs: opts.claudeArgs
749
837
  });
750
838
  } finally {
751
839
  process.stdin.off("data", abortHandler);
@@ -862,155 +950,375 @@ class InterruptController {
862
950
  }
863
951
  }
864
952
 
865
- var version = "0.1.9";
953
+ var version = "0.1.11";
866
954
  var packageJson = {
867
955
  version: version};
868
956
 
869
- async function startAnthropicActivityProxy(onClaudeActivity) {
870
- const requestTimeouts = /* @__PURE__ */ new Map();
871
- let requestCounter = 0;
872
- let idleTimer = null;
873
- const maxTimeBeforeIdle = 50;
874
- const requestTimeout = 5 * 60 * 1e3;
875
- const cleanupRequest = (requestId, reason) => {
876
- const timeout = requestTimeouts.get(requestId);
877
- if (timeout) {
878
- clearTimeout(timeout);
879
- requestTimeouts.delete(requestId);
880
- logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
881
- claudeDidSomeWork();
882
- }
883
- };
884
- const claudeDidSomeWork = () => {
885
- if (idleTimer) clearTimeout(idleTimer);
886
- if (requestTimeouts.size === 0) {
887
- idleTimer = setTimeout(() => {
888
- logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
889
- onClaudeActivity("idle");
890
- }, maxTimeBeforeIdle);
891
- }
892
- };
893
- const server = createServer((req, res) => {
894
- const requestId = ++requestCounter;
895
- const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
896
- if (isAnthropicRequest) {
897
- const timeout = setTimeout(() => {
898
- logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
899
- cleanupRequest(requestId, "timeout");
900
- }, requestTimeout);
901
- requestTimeouts.set(requestId, timeout);
902
- onClaudeActivity("working");
903
- logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
904
- }
905
- const chunks = [];
906
- req.on("data", (chunk) => {
907
- chunks.push(chunk);
908
- if (isAnthropicRequest) {
909
- claudeDidSomeWork();
910
- }
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
964
+ });
965
+ let stdout = "";
966
+ let stderr = "";
967
+ child.stdout.on("data", (data) => {
968
+ stdout += data.toString();
969
+ });
970
+ child.stderr.on("data", (data) => {
971
+ stderr += data.toString();
972
+ });
973
+ child.on("close", (code) => {
974
+ resolve({
975
+ exitCode: code || 0,
976
+ stdout,
977
+ stderr
978
+ });
979
+ });
980
+ child.on("error", (err) => {
981
+ reject(err);
911
982
  });
912
- req.on("end", () => {
913
- const body = Buffer.concat(chunks);
914
- let targetUrl;
915
- if (isAnthropicRequest) {
916
- targetUrl = new URL$1(req.url || "/", "https://api.anthropic.com");
983
+ });
984
+ }
985
+
986
+ const execAsync = promisify(exec);
987
+ function registerHandlers(session, interruptController, permissionCallbacks, onSwitchRemoteRequested) {
988
+ session.setHandler("abort", async () => {
989
+ logger.info("Abort request - interrupting Claude");
990
+ await interruptController.interrupt();
991
+ });
992
+ if (permissionCallbacks) {
993
+ session.setHandler("permission", async (message) => {
994
+ logger.info("Permission response" + JSON.stringify(message));
995
+ const id = message.id;
996
+ const resolve = permissionCallbacks.requests.get(id);
997
+ if (resolve) {
998
+ if (!message.approved) {
999
+ logger.debug("Permission denied, interrupting Claude");
1000
+ await interruptController.interrupt();
1001
+ }
1002
+ resolve({ approved: message.approved, reason: message.reason });
1003
+ permissionCallbacks.requests.delete(id);
917
1004
  } else {
918
- const protocol = req.headers["x-forwarded-proto"] || "https";
919
- const host = req.headers.host || "localhost";
920
- targetUrl = new URL$1(req.url || "/", `${protocol}://${host}`);
1005
+ logger.info("Permission request stale, likely timed out");
1006
+ return;
921
1007
  }
1008
+ session.updateAgentState((currentState) => {
1009
+ let r = { ...currentState.requests };
1010
+ delete r[id];
1011
+ return {
1012
+ ...currentState,
1013
+ requests: r
1014
+ };
1015
+ });
1016
+ });
1017
+ }
1018
+ session.setHandler("bash", async (data) => {
1019
+ logger.info("Shell command request:", data.command);
1020
+ try {
922
1021
  const options = {
923
- hostname: targetUrl.hostname,
924
- port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
925
- path: targetUrl.pathname + targetUrl.search,
926
- method: req.method,
927
- headers: {
928
- ...req.headers,
929
- host: targetUrl.hostname
930
- }
1022
+ cwd: data.cwd,
1023
+ timeout: data.timeout || 3e4
1024
+ // Default 30 seconds timeout
931
1025
  };
932
- const requestMethod = targetUrl.protocol === "https:" ? request : request$1;
933
- const proxyReq = requestMethod(options, (proxyRes) => {
934
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
935
- proxyRes.pipe(res);
936
- proxyRes.on("end", () => {
937
- if (isAnthropicRequest) {
938
- cleanupRequest(requestId, "completed");
1026
+ const { stdout, stderr } = await execAsync(data.command, options);
1027
+ return {
1028
+ success: true,
1029
+ stdout: stdout || "",
1030
+ stderr: stderr || "",
1031
+ exitCode: 0
1032
+ };
1033
+ } catch (error) {
1034
+ const execError = error;
1035
+ if (execError.code === "ETIMEDOUT" || execError.killed) {
1036
+ return {
1037
+ success: false,
1038
+ stdout: execError.stdout || "",
1039
+ stderr: execError.stderr || "",
1040
+ exitCode: typeof execError.code === "number" ? execError.code : -1,
1041
+ error: "Command timed out"
1042
+ };
1043
+ }
1044
+ return {
1045
+ success: false,
1046
+ stdout: execError.stdout || "",
1047
+ stderr: execError.stderr || execError.message || "Command failed",
1048
+ exitCode: typeof execError.code === "number" ? execError.code : 1,
1049
+ error: execError.message || "Command failed"
1050
+ };
1051
+ }
1052
+ });
1053
+ session.setHandler("readFile", async (data) => {
1054
+ logger.info("Read file request:", data.path);
1055
+ try {
1056
+ const buffer = await readFile$1(data.path);
1057
+ const content = buffer.toString("base64");
1058
+ return { success: true, content };
1059
+ } catch (error) {
1060
+ logger.debug("Failed to read file:", error);
1061
+ return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
1062
+ }
1063
+ });
1064
+ session.setHandler("writeFile", async (data) => {
1065
+ logger.info("Write file request:", data.path);
1066
+ try {
1067
+ if (data.expectedHash !== null && data.expectedHash !== void 0) {
1068
+ try {
1069
+ const existingBuffer = await readFile$1(data.path);
1070
+ const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
1071
+ if (existingHash !== data.expectedHash) {
1072
+ return {
1073
+ success: false,
1074
+ error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
1075
+ };
1076
+ }
1077
+ } catch (error) {
1078
+ const nodeError = error;
1079
+ if (nodeError.code !== "ENOENT") {
1080
+ throw error;
1081
+ }
1082
+ return {
1083
+ success: false,
1084
+ error: "File does not exist but hash was provided"
1085
+ };
1086
+ }
1087
+ } else {
1088
+ try {
1089
+ await stat(data.path);
1090
+ return {
1091
+ success: false,
1092
+ error: "File already exists but was expected to be new"
1093
+ };
1094
+ } catch (error) {
1095
+ const nodeError = error;
1096
+ if (nodeError.code !== "ENOENT") {
1097
+ throw error;
939
1098
  }
940
- });
941
- });
942
- proxyReq.on("error", (error) => {
943
- if (isAnthropicRequest) {
944
- cleanupRequest(requestId, `error: ${error.message}`);
945
- } else {
946
- logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
947
1099
  }
948
- res.writeHead(502);
949
- res.end("Bad Gateway");
950
- });
951
- if (body.length > 0) {
952
- proxyReq.write(body);
953
1100
  }
954
- proxyReq.end();
955
- });
1101
+ const buffer = Buffer.from(data.content, "base64");
1102
+ await writeFile(data.path, buffer);
1103
+ const hash = createHash("sha256").update(buffer).digest("hex");
1104
+ return { success: true, hash };
1105
+ } catch (error) {
1106
+ logger.debug("Failed to write file:", error);
1107
+ return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
1108
+ }
956
1109
  });
957
- server.on("connect", (req, clientSocket, head) => {
958
- const requestId = ++requestCounter;
959
- const [hostname, port] = req.url?.split(":") || ["", "443"];
960
- const isAnthropicRequest = hostname === "api.anthropic.com";
961
- if (isAnthropicRequest) {
962
- const timeout = setTimeout(() => {
963
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
964
- cleanupRequest(requestId, "timeout");
965
- }, requestTimeout);
966
- requestTimeouts.set(requestId, timeout);
967
- onClaudeActivity("working");
968
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
969
- }
970
- const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
971
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
972
- serverSocket.write(head);
973
- serverSocket.pipe(clientSocket);
974
- clientSocket.pipe(serverSocket);
975
- });
976
- const cleanup = () => {
977
- if (isAnthropicRequest) {
978
- cleanupRequest(requestId, "CONNECT closed");
1110
+ session.setHandler("listDirectory", async (data) => {
1111
+ logger.info("List directory request:", data.path);
1112
+ try {
1113
+ const entries = await readdir(data.path, { withFileTypes: true });
1114
+ const directoryEntries = await Promise.all(
1115
+ entries.map(async (entry) => {
1116
+ const fullPath = join$1(data.path, entry.name);
1117
+ let type = "other";
1118
+ let size;
1119
+ let modified;
1120
+ if (entry.isDirectory()) {
1121
+ type = "directory";
1122
+ } else if (entry.isFile()) {
1123
+ type = "file";
1124
+ }
1125
+ try {
1126
+ const stats = await stat(fullPath);
1127
+ size = stats.size;
1128
+ modified = stats.mtime.getTime();
1129
+ } catch (error) {
1130
+ logger.debug(`Failed to stat ${fullPath}:`, error);
1131
+ }
1132
+ return {
1133
+ name: entry.name,
1134
+ type,
1135
+ size,
1136
+ modified
1137
+ };
1138
+ })
1139
+ );
1140
+ directoryEntries.sort((a, b) => {
1141
+ if (a.type === "directory" && b.type !== "directory") return -1;
1142
+ if (a.type !== "directory" && b.type === "directory") return 1;
1143
+ return a.name.localeCompare(b.name);
1144
+ });
1145
+ return { success: true, entries: directoryEntries };
1146
+ } catch (error) {
1147
+ logger.debug("Failed to list directory:", error);
1148
+ return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
1149
+ }
1150
+ });
1151
+ session.setHandler("getDirectoryTree", async (data) => {
1152
+ logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1153
+ async function buildTree(path, name, currentDepth) {
1154
+ try {
1155
+ const stats = await stat(path);
1156
+ const node = {
1157
+ name,
1158
+ path,
1159
+ type: stats.isDirectory() ? "directory" : "file",
1160
+ size: stats.size,
1161
+ modified: stats.mtime.getTime()
1162
+ };
1163
+ if (stats.isDirectory() && currentDepth < data.maxDepth) {
1164
+ const entries = await readdir(path, { withFileTypes: true });
1165
+ const children = [];
1166
+ await Promise.all(
1167
+ entries.map(async (entry) => {
1168
+ if (entry.isSymbolicLink()) {
1169
+ logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
1170
+ return;
1171
+ }
1172
+ const childPath = join$1(path, entry.name);
1173
+ const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
1174
+ if (childNode) {
1175
+ children.push(childNode);
1176
+ }
1177
+ })
1178
+ );
1179
+ children.sort((a, b) => {
1180
+ if (a.type === "directory" && b.type !== "directory") return -1;
1181
+ if (a.type !== "directory" && b.type === "directory") return 1;
1182
+ return a.name.localeCompare(b.name);
1183
+ });
1184
+ node.children = children;
1185
+ }
1186
+ return node;
1187
+ } catch (error) {
1188
+ logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
1189
+ return null;
979
1190
  }
980
- };
981
- serverSocket.on("error", (err) => {
982
- logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
983
- clientSocket.end();
984
- cleanup();
985
- });
986
- clientSocket.on("error", cleanup);
987
- clientSocket.on("end", cleanup);
988
- serverSocket.on("end", cleanup);
1191
+ }
1192
+ try {
1193
+ if (data.maxDepth < 0) {
1194
+ return { success: false, error: "maxDepth must be non-negative" };
1195
+ }
1196
+ const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
1197
+ const tree = await buildTree(data.path, baseName, 0);
1198
+ if (!tree) {
1199
+ return { success: false, error: "Failed to access the specified path" };
1200
+ }
1201
+ return { success: true, tree };
1202
+ } catch (error) {
1203
+ logger.debug("Failed to get directory tree:", error);
1204
+ return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
1205
+ }
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
+ }
989
1224
  });
990
- const url = await new Promise((resolve) => {
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) => {
991
1254
  server.listen(0, "127.0.0.1", () => {
992
1255
  const addr = server.address();
993
1256
  if (addr && typeof addr === "object") {
994
- resolve(`http://127.0.0.1:${addr.port}`);
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"));
995
1262
  }
996
1263
  });
997
1264
  });
998
- logger.debug(`[AnthropicProxy] Started at ${url}`);
999
- return {
1000
- url,
1001
- cleanup: () => {
1002
- if (idleTimer) clearTimeout(idleTimer);
1003
- for (const [requestId, timeout] of requestTimeouts) {
1004
- clearTimeout(timeout);
1005
- logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
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
+ }
1006
1289
  }
1007
- requestTimeouts.clear();
1008
- if (requestTimeouts.size > 0) {
1009
- logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
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
+ });
1010
1318
  }
1011
- server.close();
1012
1319
  }
1013
- };
1320
+ });
1321
+ return proxyUrl;
1014
1322
  }
1015
1323
 
1016
1324
  async function start(credentials, options = {}) {
@@ -1018,30 +1326,23 @@ async function start(credentials, options = {}) {
1018
1326
  const sessionTag = randomUUID();
1019
1327
  const api = new ApiClient(credentials.token, credentials.secret);
1020
1328
  let state = {};
1021
- let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
1329
+ let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
1022
1330
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1023
1331
  logger.debug(`Session created: ${response.id}`);
1024
1332
  const session = api.session(response);
1025
1333
  const pushClient = api.push();
1026
1334
  let thinking = false;
1335
+ let mode = "local";
1027
1336
  let pingInterval = setInterval(() => {
1028
- session.keepAlive(thinking);
1337
+ session.keepAlive(thinking, mode);
1029
1338
  }, 2e3);
1030
- const antropicActivityProxy = await startAnthropicActivityProxy(
1031
- (activity) => {
1032
- const newThinking = activity === "working";
1033
- if (newThinking !== thinking) {
1034
- thinking = newThinking;
1035
- logger.debug(`[PING] Thinking state changed: ${thinking}`);
1036
- session.keepAlive(thinking);
1037
- }
1038
- }
1039
- );
1040
- process.env.HTTP_PROXY = antropicActivityProxy.url;
1041
- process.env.HTTPS_PROXY = antropicActivityProxy.url;
1042
- 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;
1043
1344
  const logPath = await logger.logFilePathPromise;
1044
- logger.info(`Session: ${response.id}`);
1345
+ logger.infoDeveloper(`Session: ${response.id}`);
1045
1346
  logger.infoDeveloper(`Logs: ${logPath}`);
1046
1347
  const interruptController = new InterruptController();
1047
1348
  let requests = /* @__PURE__ */ new Map();
@@ -1051,10 +1352,10 @@ async function start(credentials, options = {}) {
1051
1352
  requests.set(id, resolve);
1052
1353
  });
1053
1354
  let timeout = setTimeout(async () => {
1054
- logger.info("Permission timeout - attempting to interrupt Claude");
1355
+ logger.debug("Permission timeout - attempting to interrupt Claude");
1055
1356
  const interrupted = await interruptController.interrupt();
1056
1357
  if (interrupted) {
1057
- logger.info("Claude interrupted successfully");
1358
+ logger.debug("Claude interrupted successfully");
1058
1359
  }
1059
1360
  requests.delete(id);
1060
1361
  session.updateAgentState((currentState) => {
@@ -1066,7 +1367,7 @@ async function start(credentials, options = {}) {
1066
1367
  };
1067
1368
  });
1068
1369
  }, 1e3 * 60 * 4.5);
1069
- logger.info("Permission request" + id + " " + JSON.stringify(request));
1370
+ logger.debug("Permission request" + id + " " + JSON.stringify(request));
1070
1371
  try {
1071
1372
  await pushClient.sendToAllDevices(
1072
1373
  "Permission Request",
@@ -1078,7 +1379,7 @@ async function start(credentials, options = {}) {
1078
1379
  type: "permission_request"
1079
1380
  }
1080
1381
  );
1081
- logger.info("Push notification sent for permission request");
1382
+ logger.debug("Push notification sent for permission request");
1082
1383
  } catch (error) {
1083
1384
  logger.debug("Failed to send push notification:", error);
1084
1385
  }
@@ -1095,29 +1396,7 @@ async function start(credentials, options = {}) {
1095
1396
  promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1096
1397
  return promise;
1097
1398
  });
1098
- session.setHandler("permission", (message) => {
1099
- logger.info("Permission response" + JSON.stringify(message));
1100
- const id = message.id;
1101
- const resolve = requests.get(id);
1102
- if (resolve) {
1103
- resolve({ approved: message.approved, reason: message.reason });
1104
- } else {
1105
- logger.info("Permission request stale, likely timed out");
1106
- return;
1107
- }
1108
- session.updateAgentState((currentState) => {
1109
- let r = { ...currentState.requests };
1110
- delete r[id];
1111
- return {
1112
- ...currentState,
1113
- requests: r
1114
- };
1115
- });
1116
- });
1117
- session.setHandler("abort", async () => {
1118
- logger.info("Abort request - interrupting Claude");
1119
- await interruptController.interrupt();
1120
- });
1399
+ registerHandlers(session, interruptController, { requests });
1121
1400
  const onAssistantResult = async (result) => {
1122
1401
  try {
1123
1402
  const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
@@ -1142,6 +1421,15 @@ async function start(credentials, options = {}) {
1142
1421
  model: options.model,
1143
1422
  permissionMode: options.permissionMode,
1144
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
+ },
1145
1433
  mcpServers: {
1146
1434
  "permission": {
1147
1435
  type: "http",
@@ -1151,16 +1439,34 @@ async function start(credentials, options = {}) {
1151
1439
  permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
1152
1440
  session,
1153
1441
  onAssistantResult,
1154
- interruptController
1442
+ interruptController,
1443
+ claudeEnvVars: options.claudeEnvVars,
1444
+ claudeArgs: options.claudeArgs
1155
1445
  });
1156
1446
  clearInterval(pingInterval);
1157
- if (antropicActivityProxy) {
1158
- logger.info("[AnthropicProxy] Shutting down activity monitoring proxy");
1159
- antropicActivityProxy.cleanup();
1160
- }
1161
1447
  process.exit(0);
1162
1448
  }
1163
1449
 
1450
+ const defaultSettings = {
1451
+ onboardingCompleted: false
1452
+ };
1453
+ async function readSettings() {
1454
+ if (!existsSync(configuration.settingsFile)) {
1455
+ return { ...defaultSettings };
1456
+ }
1457
+ try {
1458
+ const content = await readFile(configuration.settingsFile, "utf8");
1459
+ return JSON.parse(content);
1460
+ } catch {
1461
+ return { ...defaultSettings };
1462
+ }
1463
+ }
1464
+ async function writeSettings(settings) {
1465
+ if (!existsSync(configuration.happyDir)) {
1466
+ await mkdir(configuration.happyDir, { recursive: true });
1467
+ }
1468
+ await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
1469
+ }
1164
1470
  const credentialsSchema = z.object({
1165
1471
  secret: z.string().base64(),
1166
1472
  token: z.string()
@@ -1184,7 +1490,7 @@ async function writeCredentials(credentials) {
1184
1490
  if (!existsSync(configuration.happyDir)) {
1185
1491
  await mkdir(configuration.happyDir, { recursive: true });
1186
1492
  }
1187
- await writeFile(configuration.privateKeyFile, JSON.stringify({
1493
+ await writeFile$1(configuration.privateKeyFile, JSON.stringify({
1188
1494
  secret: encodeBase64(credentials.secret),
1189
1495
  token: credentials.token
1190
1496
  }, null, 2));
@@ -1262,6 +1568,413 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1262
1568
  return decrypted;
1263
1569
  }
1264
1570
 
1571
+ class ApiDaemonSession extends EventEmitter {
1572
+ socket;
1573
+ machineIdentity;
1574
+ keepAliveInterval = null;
1575
+ token;
1576
+ secret;
1577
+ constructor(token, secret, machineIdentity) {
1578
+ super();
1579
+ this.token = token;
1580
+ this.secret = secret;
1581
+ this.machineIdentity = machineIdentity;
1582
+ const socket = io(configuration.serverUrl, {
1583
+ auth: {
1584
+ token: this.token,
1585
+ clientType: "machine-scoped",
1586
+ machineId: this.machineIdentity.machineId
1587
+ },
1588
+ path: "/v1/user-machine-daemon",
1589
+ reconnection: true,
1590
+ reconnectionAttempts: Infinity,
1591
+ reconnectionDelay: 1e3,
1592
+ reconnectionDelayMax: 5e3,
1593
+ transports: ["websocket"],
1594
+ withCredentials: true,
1595
+ autoConnect: false
1596
+ });
1597
+ socket.on("connect", () => {
1598
+ logger.debug("[DAEMON] Connected to server");
1599
+ this.emit("connected");
1600
+ socket.emit("machine-connect", {
1601
+ token: this.token,
1602
+ machineIdentity: encodeBase64(encrypt(this.machineIdentity, this.secret))
1603
+ });
1604
+ this.startKeepAlive();
1605
+ });
1606
+ socket.on("disconnect", () => {
1607
+ logger.debug("[DAEMON] Disconnected from server");
1608
+ this.emit("disconnected");
1609
+ this.stopKeepAlive();
1610
+ });
1611
+ socket.on("spawn-session", async (encryptedData, callback) => {
1612
+ let requestData;
1613
+ try {
1614
+ requestData = decrypt(decodeBase64(encryptedData), this.secret);
1615
+ logger.debug("[DAEMON] Received spawn-session request", requestData);
1616
+ const args = [
1617
+ "--directory",
1618
+ requestData.directory,
1619
+ "--happy-starting-mode",
1620
+ requestData.startingMode
1621
+ ];
1622
+ if (requestData.metadata) {
1623
+ args.push("--metadata", requestData.metadata);
1624
+ }
1625
+ if (requestData.startingMode === "interactive" && process.platform === "darwin") {
1626
+ const script = `
1627
+ tell application "Terminal"
1628
+ activate
1629
+ do script "cd ${requestData.directory} && happy ${args.join(" ")}"
1630
+ end tell
1631
+ `;
1632
+ spawn$1("osascript", ["-e", script], { detached: true });
1633
+ } else {
1634
+ const child = spawn$1("happy", args, {
1635
+ detached: true,
1636
+ stdio: "ignore",
1637
+ cwd: requestData.directory
1638
+ });
1639
+ child.unref();
1640
+ }
1641
+ const result = { success: true };
1642
+ socket.emit("session-spawn-result", {
1643
+ requestId: requestData.requestId,
1644
+ result: encodeBase64(encrypt(result, this.secret))
1645
+ });
1646
+ callback(encodeBase64(encrypt({ success: true }, this.secret)));
1647
+ } catch (error) {
1648
+ logger.debug("[DAEMON] Failed to spawn session", error);
1649
+ const errorResult = {
1650
+ success: false,
1651
+ error: error instanceof Error ? error.message : "Unknown error"
1652
+ };
1653
+ socket.emit("session-spawn-result", {
1654
+ requestId: requestData?.requestId || "",
1655
+ result: encodeBase64(encrypt(errorResult, this.secret))
1656
+ });
1657
+ callback(encodeBase64(encrypt(errorResult, this.secret)));
1658
+ }
1659
+ });
1660
+ socket.on("daemon-command", (data) => {
1661
+ switch (data.command) {
1662
+ case "shutdown":
1663
+ this.shutdown();
1664
+ break;
1665
+ case "status":
1666
+ this.emit("status-request");
1667
+ break;
1668
+ }
1669
+ });
1670
+ this.socket = socket;
1671
+ }
1672
+ startKeepAlive() {
1673
+ this.stopKeepAlive();
1674
+ this.keepAliveInterval = setInterval(() => {
1675
+ this.socket.volatile.emit("machine-alive", {
1676
+ time: Date.now()
1677
+ });
1678
+ }, 2e4);
1679
+ }
1680
+ stopKeepAlive() {
1681
+ if (this.keepAliveInterval) {
1682
+ clearInterval(this.keepAliveInterval);
1683
+ this.keepAliveInterval = null;
1684
+ }
1685
+ }
1686
+ connect() {
1687
+ this.socket.connect();
1688
+ }
1689
+ shutdown() {
1690
+ this.stopKeepAlive();
1691
+ this.socket.close();
1692
+ this.emit("shutdown");
1693
+ }
1694
+ }
1695
+
1696
+ async function startDaemon() {
1697
+ console.log("[DAEMON] Starting daemon process...");
1698
+ if (await isDaemonRunning()) {
1699
+ console.log("Happy daemon is already running");
1700
+ process.exit(0);
1701
+ }
1702
+ console.log("[DAEMON] Writing PID file with PID:", process.pid);
1703
+ writePidFile();
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
+ });
1715
+ try {
1716
+ const settings = await readSettings() || { onboardingCompleted: false };
1717
+ if (!settings.machineId) {
1718
+ settings.machineId = crypto.randomUUID();
1719
+ settings.machineHost = hostname();
1720
+ await writeSettings(settings);
1721
+ }
1722
+ const machineIdentity = {
1723
+ machineId: settings.machineId,
1724
+ machineHost: settings.machineHost || hostname(),
1725
+ platform: process.platform,
1726
+ version: process.env.npm_package_version || "unknown"
1727
+ };
1728
+ let credentials = await readCredentials();
1729
+ if (!credentials) {
1730
+ logger.debug("[DAEMON] No credentials found, running auth");
1731
+ await doAuth();
1732
+ credentials = await readCredentials();
1733
+ if (!credentials) {
1734
+ throw new Error("Failed to authenticate");
1735
+ }
1736
+ }
1737
+ const { token, secret } = credentials;
1738
+ const daemon = new ApiDaemonSession(token, secret, machineIdentity);
1739
+ daemon.on("connected", () => {
1740
+ logger.debug("[DAEMON] Successfully connected to server");
1741
+ });
1742
+ daemon.on("disconnected", () => {
1743
+ logger.debug("[DAEMON] Disconnected from server");
1744
+ });
1745
+ daemon.on("shutdown", () => {
1746
+ logger.debug("[DAEMON] Shutdown requested");
1747
+ stopDaemon();
1748
+ process.exit(0);
1749
+ });
1750
+ daemon.connect();
1751
+ setInterval(() => {
1752
+ }, 1e3);
1753
+ } catch (error) {
1754
+ logger.debug("[DAEMON] Failed to start daemon", error);
1755
+ stopDaemon();
1756
+ process.exit(1);
1757
+ }
1758
+ process.on("SIGINT", () => process.exit(0));
1759
+ process.on("SIGTERM", () => process.exit(0));
1760
+ process.on("exit", () => process.exit(0));
1761
+ while (true) {
1762
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1763
+ }
1764
+ }
1765
+ async function isDaemonRunning() {
1766
+ try {
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");
1791
+ }
1792
+ return false;
1793
+ } catch (error) {
1794
+ console.log("[isDaemonRunning] Error:", error);
1795
+ logger.debug("[DAEMON] Error checking daemon status", error);
1796
+ return false;
1797
+ }
1798
+ }
1799
+ function writePidFile() {
1800
+ const happyDir = join$1(homedir$1(), ".happy");
1801
+ if (!existsSync$1(happyDir)) {
1802
+ mkdirSync$1(happyDir, { recursive: true });
1803
+ }
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
+ }
1813
+ }
1814
+ async function stopDaemon() {
1815
+ try {
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);
1831
+ }
1832
+ } catch (error) {
1833
+ logger.debug("[DAEMON] Error stopping daemon", error);
1834
+ }
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
+ }
1852
+
1853
+ function trimIdent(text) {
1854
+ const lines = text.split("\n");
1855
+ while (lines.length > 0 && lines[0].trim() === "") {
1856
+ lines.shift();
1857
+ }
1858
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1859
+ lines.pop();
1860
+ }
1861
+ const minSpaces = lines.reduce((min, line) => {
1862
+ if (line.trim() === "") {
1863
+ return min;
1864
+ }
1865
+ const leadingSpaces = line.match(/^\s*/)[0].length;
1866
+ return Math.min(min, leadingSpaces);
1867
+ }, Infinity);
1868
+ const trimmedLines = lines.map((line) => line.slice(minSpaces));
1869
+ return trimmedLines.join("\n");
1870
+ }
1871
+
1872
+ const PLIST_LABEL$1 = "com.happy-cli.daemon";
1873
+ const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
1874
+ const USER_HOME = process.env.HOME || process.env.USERPROFILE;
1875
+ async function install$1() {
1876
+ try {
1877
+ if (existsSync$1(PLIST_FILE$1)) {
1878
+ logger.info("Daemon plist already exists. Uninstalling first...");
1879
+ execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
1880
+ }
1881
+ const happyPath = process.argv[0];
1882
+ const scriptPath = process.argv[1];
1883
+ const plistContent = trimIdent(`
1884
+ <?xml version="1.0" encoding="UTF-8"?>
1885
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1886
+ <plist version="1.0">
1887
+ <dict>
1888
+ <key>Label</key>
1889
+ <string>${PLIST_LABEL$1}</string>
1890
+
1891
+ <key>ProgramArguments</key>
1892
+ <array>
1893
+ <string>${happyPath}</string>
1894
+ <string>${scriptPath}</string>
1895
+ <string>happy-daemon</string>
1896
+ </array>
1897
+
1898
+ <key>EnvironmentVariables</key>
1899
+ <dict>
1900
+ <key>HAPPY_DAEMON_MODE</key>
1901
+ <string>true</string>
1902
+ </dict>
1903
+
1904
+ <key>RunAtLoad</key>
1905
+ <true/>
1906
+
1907
+ <key>KeepAlive</key>
1908
+ <true/>
1909
+
1910
+ <key>StandardErrorPath</key>
1911
+ <string>${USER_HOME}/.happy/daemon.err</string>
1912
+
1913
+ <key>StandardOutPath</key>
1914
+ <string>${USER_HOME}/.happy/daemon.log</string>
1915
+
1916
+ <key>WorkingDirectory</key>
1917
+ <string>/tmp</string>
1918
+ </dict>
1919
+ </plist>
1920
+ `);
1921
+ writeFileSync(PLIST_FILE$1, plistContent);
1922
+ chmodSync(PLIST_FILE$1, 420);
1923
+ logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
1924
+ execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
1925
+ logger.info("Daemon installed and started successfully");
1926
+ logger.info("Check logs at ~/.happy/daemon.log");
1927
+ } catch (error) {
1928
+ logger.debug("Failed to install daemon:", error);
1929
+ throw error;
1930
+ }
1931
+ }
1932
+
1933
+ async function install() {
1934
+ if (process.platform !== "darwin") {
1935
+ throw new Error("Daemon installation is currently only supported on macOS");
1936
+ }
1937
+ if (process.getuid && process.getuid() !== 0) {
1938
+ throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
1939
+ }
1940
+ logger.info("Installing Happy CLI daemon for macOS...");
1941
+ await install$1();
1942
+ }
1943
+
1944
+ const PLIST_LABEL = "com.happy-cli.daemon";
1945
+ const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
1946
+ async function uninstall$1() {
1947
+ try {
1948
+ if (!existsSync$1(PLIST_FILE)) {
1949
+ logger.info("Daemon plist not found. Nothing to uninstall.");
1950
+ return;
1951
+ }
1952
+ try {
1953
+ execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
1954
+ logger.info("Daemon stopped successfully");
1955
+ } catch (error) {
1956
+ logger.info("Failed to unload daemon (it might not be running)");
1957
+ }
1958
+ unlinkSync(PLIST_FILE);
1959
+ logger.info(`Removed daemon plist from ${PLIST_FILE}`);
1960
+ logger.info("Daemon uninstalled successfully");
1961
+ } catch (error) {
1962
+ logger.debug("Failed to uninstall daemon:", error);
1963
+ throw error;
1964
+ }
1965
+ }
1966
+
1967
+ async function uninstall() {
1968
+ if (process.platform !== "darwin") {
1969
+ throw new Error("Daemon uninstallation is currently only supported on macOS");
1970
+ }
1971
+ if (process.getuid && process.getuid() !== 0) {
1972
+ throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
1973
+ }
1974
+ logger.info("Uninstalling Happy CLI daemon for macOS...");
1975
+ await uninstall$1();
1976
+ }
1977
+
1265
1978
  (async () => {
1266
1979
  const args = process.argv.slice(2);
1267
1980
  let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
@@ -1281,39 +1994,40 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1281
1994
  }
1282
1995
  return;
1283
1996
  } else if (subcommand === "daemon") {
1284
- if (process.env.HAPPY_DAEMON_MODE) {
1285
- const { run } = await import('./run-FBXkmmN7.mjs');
1286
- await run();
1997
+ const daemonSubcommand = args[1];
1998
+ if (daemonSubcommand === "start") {
1999
+ await startDaemon();
2000
+ process.exit(0);
2001
+ } else if (daemonSubcommand === "stop") {
2002
+ await stopDaemon();
2003
+ process.exit(0);
2004
+ } else if (daemonSubcommand === "install") {
2005
+ try {
2006
+ await install();
2007
+ } catch (error) {
2008
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
2009
+ process.exit(1);
2010
+ }
2011
+ } else if (daemonSubcommand === "uninstall") {
2012
+ try {
2013
+ await uninstall();
2014
+ } catch (error) {
2015
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
2016
+ process.exit(1);
2017
+ }
1287
2018
  } else {
1288
- const daemonSubcommand = args[1];
1289
- if (daemonSubcommand === "install") {
1290
- const { install } = await import('./install-HKe7dyS4.mjs');
1291
- try {
1292
- await install();
1293
- } catch (error) {
1294
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1295
- process.exit(1);
1296
- }
1297
- } else if (daemonSubcommand === "uninstall") {
1298
- const { uninstall } = await import('./uninstall-CLkTtlMv.mjs');
1299
- try {
1300
- await uninstall();
1301
- } catch (error) {
1302
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1303
- process.exit(1);
1304
- }
1305
- } else {
1306
- console.log(`
2019
+ console.log(`
1307
2020
  ${chalk.bold("happy daemon")} - Daemon management
1308
2021
 
1309
2022
  ${chalk.bold("Usage:")}
2023
+ happy daemon start Start the daemon
2024
+ happy daemon stop Stop the daemon
1310
2025
  sudo happy daemon install Install the daemon (requires sudo)
1311
2026
  sudo happy daemon uninstall Uninstall the daemon (requires sudo)
1312
2027
 
1313
2028
  ${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
1314
2029
  Currently only supported on macOS.
1315
2030
  `);
1316
- }
1317
2031
  }
1318
2032
  return;
1319
2033
  } else {
@@ -1336,7 +2050,18 @@ Currently only supported on macOS.
1336
2050
  } else if (arg === "--local") {
1337
2051
  i++;
1338
2052
  } else if (arg === "--happy-starting-mode") {
1339
- 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];
1340
2065
  } else {
1341
2066
  console.error(chalk.red(`Unknown argument: ${arg}`));
1342
2067
  process.exit(1);
@@ -1357,20 +2082,31 @@ ${chalk.bold("Options:")}
1357
2082
  -m, --model <model> Claude model to use (default: sonnet)
1358
2083
  -p, --permission-mode Permission mode: auto, default, or plan
1359
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
1360
2093
 
1361
2094
  [Advanced]
1362
2095
  --local < global | local >
1363
2096
  Will use .happy folder in the current directory for storing your private key and debug logs.
1364
2097
  You will require re-login each time you run this in a new directory.
1365
-
1366
- --happy-starting-mode <mode> Start in specified mode (interactive or remote)
1367
- Default: interactive
2098
+ --happy-starting-mode <interactive|remote>
2099
+ Set the starting mode for new sessions (default: remote)
1368
2100
 
1369
2101
  ${chalk.bold("Examples:")}
1370
2102
  happy Start a session with default settings
1371
2103
  happy -m opus Use Claude Opus model
1372
2104
  happy -p plan Use plan permission mode
1373
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
1374
2110
  happy logout Logs out of your account and removes data directory
1375
2111
  `);
1376
2112
  process.exit(0);
@@ -1387,6 +2123,7 @@ ${chalk.bold("Examples:")}
1387
2123
  }
1388
2124
  credentials = res;
1389
2125
  }
2126
+ await readSettings() || { };
1390
2127
  try {
1391
2128
  await start(credentials, options);
1392
2129
  } catch (error) {