happy-coder 0.9.1 → 0.10.0-1

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.
package/dist/index.cjs CHANGED
@@ -3,36 +3,39 @@
3
3
  var chalk = require('chalk');
4
4
  var os = require('node:os');
5
5
  var node_crypto = require('node:crypto');
6
- var types = require('./types-DNUk09Np.cjs');
6
+ var types = require('./types-D9P2bndj.cjs');
7
7
  var node_child_process = require('node:child_process');
8
8
  var node_path = require('node:path');
9
9
  var node_readline = require('node:readline');
10
- var node_url = require('node:url');
11
10
  var node_fs = require('node:fs');
12
- var path = require('path');
13
- var url = require('url');
14
11
  var promises = require('node:fs/promises');
15
12
  var fs = require('fs/promises');
16
13
  var ink = require('ink');
17
14
  var React = require('react');
15
+ var node_url = require('node:url');
18
16
  var axios = require('axios');
19
17
  require('node:events');
20
18
  require('socket.io-client');
21
19
  var tweetnacl = require('tweetnacl');
22
20
  require('expo-server-sdk');
23
- var child_process = require('child_process');
24
- var util = require('util');
25
21
  var crypto = require('crypto');
22
+ var child_process = require('child_process');
23
+ var fs$1 = require('fs');
24
+ var path = require('path');
25
+ var psList = require('ps-list');
26
+ var spawn = require('cross-spawn');
26
27
  var os$1 = require('os');
27
28
  var qrcode = require('qrcode-terminal');
28
29
  var open = require('open');
29
30
  var fastify = require('fastify');
30
31
  var z = require('zod');
31
32
  var fastifyTypeProviderZod = require('fastify-type-provider-zod');
32
- var fs$1 = require('fs');
33
33
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
34
34
  var node_http = require('node:http');
35
35
  var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
36
+ var http = require('http');
37
+ var util = require('util');
38
+ require('url');
36
39
 
37
40
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
38
41
  class Session {
@@ -145,12 +148,6 @@ function claudeCheckSession(sessionId, path) {
145
148
  return hasGoodMessage;
146
149
  }
147
150
 
148
- const __dirname$2 = 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))));
149
- function projectPath() {
150
- const path$1 = path.resolve(__dirname$2, "..");
151
- return path$1;
152
- }
153
-
154
151
  function trimIdent(text) {
155
152
  const lines = text.split("\n");
156
153
  while (lines.length > 0 && lines[0].trim() === "") {
@@ -184,7 +181,7 @@ const systemPrompt = trimIdent(`
184
181
  Co-Authored-By: Happy <yesreply@happy.engineering>
185
182
  `);
186
183
 
187
- 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))));
184
+ const claudeCliPath = node_path.resolve(node_path.join(types.projectPath(), "scripts", "claude_local_launcher.cjs"));
188
185
  async function claudeLocal(opts) {
189
186
  const projectDir = getProjectPath(opts.path);
190
187
  node_fs.mkdirSync(projectDir, { recursive: true });
@@ -241,7 +238,6 @@ async function claudeLocal(opts) {
241
238
  if (opts.claudeArgs) {
242
239
  args.push(...opts.claudeArgs);
243
240
  }
244
- const claudeCliPath = node_path.resolve(node_path.join(projectPath(), "scripts", "claude_local_launcher.cjs"));
245
241
  if (!claudeCliPath || !node_fs.existsSync(claudeCliPath)) {
246
242
  throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
247
243
  }
@@ -612,8 +608,8 @@ async function claudeLocalLauncher(session) {
612
608
  }
613
609
  await abort();
614
610
  }
615
- session.client.setHandler("abort", doAbort);
616
- session.client.setHandler("switch", doSwitch);
611
+ session.client.rpcHandlerManager.registerHandler("abort", doAbort);
612
+ session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
617
613
  session.queue.setOnMessage((message, mode) => {
618
614
  doSwitch();
619
615
  });
@@ -659,9 +655,9 @@ async function claudeLocalLauncher(session) {
659
655
  }
660
656
  } finally {
661
657
  exutFuture.resolve(void 0);
662
- session.client.setHandler("abort", async () => {
658
+ session.client.rpcHandlerManager.registerHandler("abort", async () => {
663
659
  });
664
- session.client.setHandler("switch", async () => {
660
+ session.client.rpcHandlerManager.registerHandler("switch", async () => {
665
661
  });
666
662
  session.queue.setOnMessage(null);
667
663
  await scanner.cleanup();
@@ -1508,7 +1504,7 @@ async function claudeRemote(opts) {
1508
1504
  executable: "node",
1509
1505
  abort: opts.signal,
1510
1506
  pathToClaudeCodeExecutable: (() => {
1511
- return node_path.resolve(node_path.join(projectPath(), "scripts", "claude_remote_launcher.cjs"));
1507
+ return node_path.resolve(node_path.join(types.projectPath(), "scripts", "claude_remote_launcher.cjs"));
1512
1508
  })()
1513
1509
  };
1514
1510
  let thinking = false;
@@ -1923,7 +1919,7 @@ class PermissionHandler {
1923
1919
  * Sets up the client handler for permission responses
1924
1920
  */
1925
1921
  setupClientHandler() {
1926
- this.session.client.setHandler("permission", async (message) => {
1922
+ this.session.client.rpcHandlerManager.registerHandler("permission", async (message) => {
1927
1923
  types.logger.debug(`Permission response: ${JSON.stringify(message)}`);
1928
1924
  const id = message.id;
1929
1925
  const pending = this.pendingRequests.get(id);
@@ -2493,8 +2489,8 @@ async function claudeRemoteLauncher(session) {
2493
2489
  }
2494
2490
  await abort();
2495
2491
  }
2496
- session.client.setHandler("abort", doAbort);
2497
- session.client.setHandler("switch", doSwitch);
2492
+ session.client.rpcHandlerManager.registerHandler("abort", doAbort);
2493
+ session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
2498
2494
  const permissionHandler = new PermissionHandler(session);
2499
2495
  const messageQueue = new OutgoingMessageQueue(
2500
2496
  (logMessage) => session.client.sendClaudeSessionMessage(logMessage)
@@ -2810,265 +2806,6 @@ async function loop(opts) {
2810
2806
  }
2811
2807
  }
2812
2808
 
2813
- function run(args, options) {
2814
- const RUNNER_PATH = path.resolve(path.join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
2815
- return new Promise((resolve2, reject) => {
2816
- const child = child_process.spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
2817
- stdio: ["pipe", "pipe", "pipe"],
2818
- cwd: options?.cwd
2819
- });
2820
- let stdout = "";
2821
- let stderr = "";
2822
- child.stdout.on("data", (data) => {
2823
- stdout += data.toString();
2824
- });
2825
- child.stderr.on("data", (data) => {
2826
- stderr += data.toString();
2827
- });
2828
- child.on("close", (code) => {
2829
- resolve2({
2830
- exitCode: code || 0,
2831
- stdout,
2832
- stderr
2833
- });
2834
- });
2835
- child.on("error", (err) => {
2836
- reject(err);
2837
- });
2838
- });
2839
- }
2840
-
2841
- const execAsync = util.promisify(child_process.exec);
2842
- function registerHandlers(session) {
2843
- session.setHandler("bash", async (data) => {
2844
- types.logger.debug("Shell command request:", data.command);
2845
- try {
2846
- const options = {
2847
- cwd: data.cwd,
2848
- timeout: data.timeout || 3e4
2849
- // Default 30 seconds timeout
2850
- };
2851
- const { stdout, stderr } = await execAsync(data.command, options);
2852
- return {
2853
- success: true,
2854
- stdout: stdout ? stdout.toString() : "",
2855
- stderr: stderr ? stderr.toString() : "",
2856
- exitCode: 0
2857
- };
2858
- } catch (error) {
2859
- const execError = error;
2860
- if (execError.code === "ETIMEDOUT" || execError.killed) {
2861
- return {
2862
- success: false,
2863
- stdout: execError.stdout || "",
2864
- stderr: execError.stderr || "",
2865
- exitCode: typeof execError.code === "number" ? execError.code : -1,
2866
- error: "Command timed out"
2867
- };
2868
- }
2869
- return {
2870
- success: false,
2871
- stdout: execError.stdout ? execError.stdout.toString() : "",
2872
- stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
2873
- exitCode: typeof execError.code === "number" ? execError.code : 1,
2874
- error: execError.message || "Command failed"
2875
- };
2876
- }
2877
- });
2878
- session.setHandler("readFile", async (data) => {
2879
- types.logger.debug("Read file request:", data.path);
2880
- try {
2881
- const buffer = await fs.readFile(data.path);
2882
- const content = buffer.toString("base64");
2883
- return { success: true, content };
2884
- } catch (error) {
2885
- types.logger.debug("Failed to read file:", error);
2886
- return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
2887
- }
2888
- });
2889
- session.setHandler("writeFile", async (data) => {
2890
- types.logger.debug("Write file request:", data.path);
2891
- try {
2892
- if (data.expectedHash !== null && data.expectedHash !== void 0) {
2893
- try {
2894
- const existingBuffer = await fs.readFile(data.path);
2895
- const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
2896
- if (existingHash !== data.expectedHash) {
2897
- return {
2898
- success: false,
2899
- error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
2900
- };
2901
- }
2902
- } catch (error) {
2903
- const nodeError = error;
2904
- if (nodeError.code !== "ENOENT") {
2905
- throw error;
2906
- }
2907
- return {
2908
- success: false,
2909
- error: "File does not exist but hash was provided"
2910
- };
2911
- }
2912
- } else {
2913
- try {
2914
- await fs.stat(data.path);
2915
- return {
2916
- success: false,
2917
- error: "File already exists but was expected to be new"
2918
- };
2919
- } catch (error) {
2920
- const nodeError = error;
2921
- if (nodeError.code !== "ENOENT") {
2922
- throw error;
2923
- }
2924
- }
2925
- }
2926
- const buffer = Buffer.from(data.content, "base64");
2927
- await fs.writeFile(data.path, buffer);
2928
- const hash = crypto.createHash("sha256").update(buffer).digest("hex");
2929
- return { success: true, hash };
2930
- } catch (error) {
2931
- types.logger.debug("Failed to write file:", error);
2932
- return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
2933
- }
2934
- });
2935
- session.setHandler("listDirectory", async (data) => {
2936
- types.logger.debug("List directory request:", data.path);
2937
- try {
2938
- const entries = await fs.readdir(data.path, { withFileTypes: true });
2939
- const directoryEntries = await Promise.all(
2940
- entries.map(async (entry) => {
2941
- const fullPath = path.join(data.path, entry.name);
2942
- let type = "other";
2943
- let size;
2944
- let modified;
2945
- if (entry.isDirectory()) {
2946
- type = "directory";
2947
- } else if (entry.isFile()) {
2948
- type = "file";
2949
- }
2950
- try {
2951
- const stats = await fs.stat(fullPath);
2952
- size = stats.size;
2953
- modified = stats.mtime.getTime();
2954
- } catch (error) {
2955
- types.logger.debug(`Failed to stat ${fullPath}:`, error);
2956
- }
2957
- return {
2958
- name: entry.name,
2959
- type,
2960
- size,
2961
- modified
2962
- };
2963
- })
2964
- );
2965
- directoryEntries.sort((a, b) => {
2966
- if (a.type === "directory" && b.type !== "directory") return -1;
2967
- if (a.type !== "directory" && b.type === "directory") return 1;
2968
- return a.name.localeCompare(b.name);
2969
- });
2970
- return { success: true, entries: directoryEntries };
2971
- } catch (error) {
2972
- types.logger.debug("Failed to list directory:", error);
2973
- return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
2974
- }
2975
- });
2976
- session.setHandler("getDirectoryTree", async (data) => {
2977
- types.logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
2978
- async function buildTree(path$1, name, currentDepth) {
2979
- try {
2980
- const stats = await fs.stat(path$1);
2981
- const node = {
2982
- name,
2983
- path: path$1,
2984
- type: stats.isDirectory() ? "directory" : "file",
2985
- size: stats.size,
2986
- modified: stats.mtime.getTime()
2987
- };
2988
- if (stats.isDirectory() && currentDepth < data.maxDepth) {
2989
- const entries = await fs.readdir(path$1, { withFileTypes: true });
2990
- const children = [];
2991
- await Promise.all(
2992
- entries.map(async (entry) => {
2993
- if (entry.isSymbolicLink()) {
2994
- types.logger.debug(`Skipping symlink: ${path.join(path$1, entry.name)}`);
2995
- return;
2996
- }
2997
- const childPath = path.join(path$1, entry.name);
2998
- const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
2999
- if (childNode) {
3000
- children.push(childNode);
3001
- }
3002
- })
3003
- );
3004
- children.sort((a, b) => {
3005
- if (a.type === "directory" && b.type !== "directory") return -1;
3006
- if (a.type !== "directory" && b.type === "directory") return 1;
3007
- return a.name.localeCompare(b.name);
3008
- });
3009
- node.children = children;
3010
- }
3011
- return node;
3012
- } catch (error) {
3013
- types.logger.debug(`Failed to process ${path$1}:`, error instanceof Error ? error.message : String(error));
3014
- return null;
3015
- }
3016
- }
3017
- try {
3018
- if (data.maxDepth < 0) {
3019
- return { success: false, error: "maxDepth must be non-negative" };
3020
- }
3021
- const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
3022
- const tree = await buildTree(data.path, baseName, 0);
3023
- if (!tree) {
3024
- return { success: false, error: "Failed to access the specified path" };
3025
- }
3026
- return { success: true, tree };
3027
- } catch (error) {
3028
- types.logger.debug("Failed to get directory tree:", error);
3029
- return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
3030
- }
3031
- });
3032
- session.setHandler("ripgrep", async (data) => {
3033
- types.logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
3034
- try {
3035
- const result = await run(data.args, { cwd: data.cwd });
3036
- return {
3037
- success: true,
3038
- exitCode: result.exitCode,
3039
- stdout: result.stdout.toString(),
3040
- stderr: result.stderr.toString()
3041
- };
3042
- } catch (error) {
3043
- types.logger.debug("Failed to run ripgrep:", error);
3044
- return {
3045
- success: false,
3046
- error: error instanceof Error ? error.message : "Failed to run ripgrep"
3047
- };
3048
- }
3049
- });
3050
- session.setHandler("killSession", async () => {
3051
- types.logger.debug("Kill session request received");
3052
- try {
3053
- const response = {
3054
- success: true,
3055
- message: "Session termination acknowledged, exiting in 100ms"
3056
- };
3057
- setTimeout(() => {
3058
- types.logger.debug("[KILL SESSION] Exiting process as requested");
3059
- process.exit(0);
3060
- }, 100);
3061
- return response;
3062
- } catch (error) {
3063
- types.logger.debug("Failed to kill session:", error);
3064
- return {
3065
- success: false,
3066
- message: error instanceof Error ? error.message : "Failed to kill session"
3067
- };
3068
- }
3069
- });
3070
- }
3071
-
3072
2809
  class MessageQueue2 {
3073
2810
  queue = [];
3074
2811
  // Made public for testing
@@ -3602,7 +3339,7 @@ async function checkIfDaemonRunningAndCleanupStaleState() {
3602
3339
  return false;
3603
3340
  }
3604
3341
  }
3605
- async function isDaemonRunningSameVersion() {
3342
+ async function isDaemonRunningCurrentlyInstalledHappyVersion() {
3606
3343
  types.logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
3607
3344
  const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
3608
3345
  if (!runningDaemon) {
@@ -3615,8 +3352,11 @@ async function isDaemonRunningSameVersion() {
3615
3352
  return false;
3616
3353
  }
3617
3354
  try {
3618
- types.logger.debug(`[DAEMON CONTROL] Current CLI version: ${types.configuration.currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
3619
- return types.configuration.currentCliVersion === state.startedWithCliVersion;
3355
+ const packageJsonPath = path.join(types.projectPath(), "package.json");
3356
+ const packageJson = JSON.parse(fs$1.readFileSync(packageJsonPath, "utf-8"));
3357
+ const currentCliVersion = packageJson.version;
3358
+ types.logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
3359
+ return currentCliVersion === state.startedWithCliVersion;
3620
3360
  } catch (error) {
3621
3361
  types.logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
3622
3362
  return false;
@@ -3669,137 +3409,73 @@ async function waitForProcessDeath(pid, timeout) {
3669
3409
  throw new Error("Process did not die within timeout");
3670
3410
  }
3671
3411
 
3672
- function findAllHappyProcesses() {
3412
+ async function findAllHappyProcesses() {
3673
3413
  try {
3414
+ const processes = await psList();
3674
3415
  const allProcesses = [];
3675
- try {
3676
- const happyOutput = node_child_process.execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
3677
- const happyLines = happyOutput.trim().split("\n").filter((line) => line.trim());
3678
- for (const line of happyLines) {
3679
- const parts = line.trim().split(/\s+/);
3680
- if (parts.length < 11) continue;
3681
- const pid = parseInt(parts[1]);
3682
- const command = parts.slice(10).join(" ");
3683
- let type = "unknown";
3684
- if (pid === process.pid) {
3685
- type = "current";
3686
- } else if (command.includes("--version")) {
3687
- type = "daemon-version-check";
3688
- } else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
3689
- type = "daemon";
3690
- } else if (command.includes("--started-by daemon")) {
3691
- type = "daemon-spawned-session";
3692
- } else if (command.includes("doctor")) {
3693
- type = "doctor";
3694
- } else {
3695
- type = "user-session";
3696
- }
3697
- allProcesses.push({ pid, command, type });
3698
- }
3699
- } catch {
3700
- }
3701
- try {
3702
- const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
3703
- const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
3704
- for (const line of devLines) {
3705
- const parts = line.trim().split(/\s+/);
3706
- if (parts.length < 11) continue;
3707
- const pid = parseInt(parts[1]);
3708
- const command = parts.slice(10).join(" ");
3709
- if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
3710
- continue;
3711
- }
3712
- let type = "unknown";
3713
- if (pid === process.pid) {
3714
- type = "current";
3715
- } else if (command.includes("--version")) {
3716
- type = "dev-daemon-version-check";
3717
- } else if (command.includes("daemon start-sync") || command.includes("daemon start")) {
3718
- type = "dev-daemon";
3719
- } else if (command.includes("--started-by daemon")) {
3720
- type = "dev-daemon-spawned";
3721
- } else if (command.includes("doctor")) {
3722
- type = "dev-doctor";
3723
- } else if (command.includes("--yolo")) {
3724
- type = "dev-session";
3725
- } else {
3726
- type = "dev-related";
3727
- }
3728
- allProcesses.push({ pid, command, type });
3416
+ for (const proc of processes) {
3417
+ const cmd = proc.cmd || "";
3418
+ const name = proc.name || "";
3419
+ const isHappy = name.includes("happy") || name === "node" && (cmd.includes("happy-cli") || cmd.includes("dist/index.mjs")) || cmd.includes("happy.mjs") || cmd.includes("happy-coder") || cmd.includes("tsx") && cmd.includes("src/index.ts") && cmd.includes("happy-cli");
3420
+ if (!isHappy) continue;
3421
+ let type = "unknown";
3422
+ if (proc.pid === process.pid) {
3423
+ type = "current";
3424
+ } else if (cmd.includes("--version")) {
3425
+ type = cmd.includes("tsx") ? "dev-daemon-version-check" : "daemon-version-check";
3426
+ } else if (cmd.includes("daemon start-sync") || cmd.includes("daemon start")) {
3427
+ type = cmd.includes("tsx") ? "dev-daemon" : "daemon";
3428
+ } else if (cmd.includes("--started-by daemon")) {
3429
+ type = cmd.includes("tsx") ? "dev-daemon-spawned" : "daemon-spawned-session";
3430
+ } else if (cmd.includes("doctor")) {
3431
+ type = cmd.includes("tsx") ? "dev-doctor" : "doctor";
3432
+ } else if (cmd.includes("--yolo")) {
3433
+ type = "dev-session";
3434
+ } else {
3435
+ type = cmd.includes("tsx") ? "dev-related" : "user-session";
3729
3436
  }
3730
- } catch {
3437
+ allProcesses.push({ pid: proc.pid, command: cmd || name, type });
3731
3438
  }
3732
3439
  return allProcesses;
3733
3440
  } catch (error) {
3734
3441
  return [];
3735
3442
  }
3736
3443
  }
3737
- function findRunawayHappyProcesses() {
3738
- try {
3739
- const processes = [];
3740
- try {
3741
- const output = node_child_process.execSync('ps aux | grep -E "(happy\\.mjs|happy-coder|happy-cli.*dist/index\\.mjs)" | grep -v grep', { encoding: "utf8" });
3742
- const lines = output.trim().split("\n").filter((line) => line.trim());
3743
- for (const line of lines) {
3744
- const parts = line.trim().split(/\s+/);
3745
- if (parts.length < 11) continue;
3746
- const pid = parseInt(parts[1]);
3747
- const command = parts.slice(10).join(" ");
3748
- if (pid === process.pid) continue;
3749
- if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
3750
- processes.push({ pid, command });
3751
- }
3752
- }
3753
- } catch {
3754
- }
3755
- try {
3756
- const devOutput = node_child_process.execSync('ps aux | grep -E "tsx.*src/index\\.ts" | grep -v grep', { encoding: "utf8" });
3757
- const devLines = devOutput.trim().split("\n").filter((line) => line.trim());
3758
- for (const line of devLines) {
3759
- const parts = line.trim().split(/\s+/);
3760
- if (parts.length < 11) continue;
3761
- const pid = parseInt(parts[1]);
3762
- const command = parts.slice(10).join(" ");
3763
- if (pid === process.pid) continue;
3764
- if (!command.includes("happy-cli/node_modules/tsx") && !command.includes("/bin/tsx src/index.ts")) {
3765
- continue;
3766
- }
3767
- if (command.includes("--started-by daemon") || command.includes("daemon start-sync") || command.includes("daemon start") || command.includes("--version")) {
3768
- processes.push({ pid, command });
3769
- }
3770
- }
3771
- } catch {
3772
- }
3773
- return processes;
3774
- } catch (error) {
3775
- return [];
3776
- }
3444
+ async function findRunawayHappyProcesses() {
3445
+ const allProcesses = await findAllHappyProcesses();
3446
+ return allProcesses.filter(
3447
+ (p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "dev-daemon" || p.type === "daemon-spawned-session" || p.type === "dev-daemon-spawned" || p.type === "daemon-version-check" || p.type === "dev-daemon-version-check")
3448
+ ).map((p) => ({ pid: p.pid, command: p.command }));
3777
3449
  }
3778
3450
  async function killRunawayHappyProcesses() {
3779
- const runawayProcesses = findRunawayHappyProcesses();
3451
+ const runawayProcesses = await findRunawayHappyProcesses();
3780
3452
  const errors = [];
3781
- const killPromises = runawayProcesses.map(async ({ pid, command }) => {
3453
+ let killed = 0;
3454
+ for (const { pid, command } of runawayProcesses) {
3782
3455
  try {
3783
- process.kill(pid, "SIGTERM");
3784
- console.log(`Sent SIGTERM to runaway process PID ${pid}: ${command}`);
3785
- await new Promise((resolve) => setTimeout(resolve, 1e3));
3786
- try {
3787
- process.kill(pid, 0);
3788
- console.log(`Process PID ${pid} ignored SIGTERM, using SIGKILL`);
3789
- process.kill(pid, "SIGKILL");
3790
- } catch {
3456
+ console.log(`Killing runaway process PID ${pid}: ${command}`);
3457
+ if (process.platform === "win32") {
3458
+ const result = spawn.sync("taskkill", ["/F", "/PID", pid.toString()], { stdio: "pipe" });
3459
+ if (result.error) throw result.error;
3460
+ if (result.status !== 0) throw new Error(`taskkill exited with code ${result.status}`);
3461
+ } else {
3462
+ process.kill(pid, "SIGTERM");
3463
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
3464
+ const processes = await psList();
3465
+ const stillAlive = processes.find((p) => p.pid === pid);
3466
+ if (stillAlive) {
3467
+ console.log(`Process PID ${pid} ignored SIGTERM, using SIGKILL`);
3468
+ process.kill(pid, "SIGKILL");
3469
+ }
3791
3470
  }
3792
3471
  console.log(`Successfully killed runaway process PID ${pid}`);
3793
- return { success: true, pid, command };
3472
+ killed++;
3794
3473
  } catch (error) {
3795
3474
  const errorMessage = error.message;
3796
3475
  errors.push({ pid, error: errorMessage });
3797
3476
  console.log(`Failed to kill process PID ${pid}: ${errorMessage}`);
3798
- return { success: false, pid, command };
3799
3477
  }
3800
- });
3801
- const results = await Promise.all(killPromises);
3802
- const killed = results.filter((r) => r.success).length;
3478
+ }
3803
3479
  return { killed, errors };
3804
3480
  }
3805
3481
 
@@ -3853,7 +3529,7 @@ async function runDoctorCommand(filter) {
3853
3529
  console.log(`Node.js Version: ${chalk.green(process.version)}`);
3854
3530
  console.log("");
3855
3531
  console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
3856
- const projectRoot = projectPath();
3532
+ const projectRoot = types.projectPath();
3857
3533
  const wrapperPath = node_path.join(projectRoot, "bin", "happy.mjs");
3858
3534
  const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
3859
3535
  console.log(`Project Root: ${chalk.blue(projectRoot)}`);
@@ -3915,7 +3591,7 @@ async function runDoctorCommand(filter) {
3915
3591
  console.log(chalk.blue(`Location: ${types.configuration.daemonStateFile}`));
3916
3592
  console.log(chalk.gray(JSON.stringify(state, null, 2)));
3917
3593
  }
3918
- const allProcesses = findAllHappyProcesses();
3594
+ const allProcesses = await findAllHappyProcesses();
3919
3595
  if (allProcesses.length > 0) {
3920
3596
  console.log(chalk.bold("\n\u{1F50D} All Happy CLI Processes"));
3921
3597
  const grouped = allProcesses.reduce((groups, process2) => {
@@ -4226,7 +3902,7 @@ async function authAndSetupMachineIfNeeded() {
4226
3902
  }
4227
3903
 
4228
3904
  function spawnHappyCLI(args, options = {}) {
4229
- const projectRoot = projectPath();
3905
+ const projectRoot = types.projectPath();
4230
3906
  const entrypoint = node_path.join(projectRoot, "dist", "index.mjs");
4231
3907
  let directory;
4232
3908
  if ("cwd" in options) {
@@ -4271,31 +3947,54 @@ function startDaemonControlServer({
4271
3947
  sessionId: z.z.string(),
4272
3948
  metadata: z.z.any()
4273
3949
  // Metadata type from API
4274
- })
3950
+ }),
3951
+ response: {
3952
+ 200: z.z.object({
3953
+ status: z.z.literal("ok")
3954
+ })
3955
+ }
4275
3956
  }
4276
- }, async (request, reply) => {
3957
+ }, async (request) => {
4277
3958
  const { sessionId, metadata } = request.body;
4278
3959
  types.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
4279
3960
  onHappySessionWebhook(sessionId, metadata);
4280
3961
  return { status: "ok" };
4281
3962
  });
4282
- typed.post("/list", async (request, reply) => {
3963
+ typed.post("/list", {
3964
+ schema: {
3965
+ response: {
3966
+ 200: z.z.object({
3967
+ children: z.z.array(z.z.object({
3968
+ startedBy: z.z.string(),
3969
+ happySessionId: z.z.string(),
3970
+ pid: z.z.number()
3971
+ }))
3972
+ })
3973
+ }
3974
+ }
3975
+ }, async () => {
4283
3976
  const children = getChildren();
4284
3977
  types.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
4285
3978
  return {
4286
- children: children.map((child) => {
4287
- delete child.childProcess;
4288
- return child;
4289
- })
3979
+ children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
3980
+ startedBy: child.startedBy,
3981
+ happySessionId: child.happySessionId,
3982
+ pid: child.pid
3983
+ }))
4290
3984
  };
4291
3985
  });
4292
3986
  typed.post("/stop-session", {
4293
3987
  schema: {
4294
3988
  body: z.z.object({
4295
3989
  sessionId: z.z.string()
4296
- })
3990
+ }),
3991
+ response: {
3992
+ 200: z.z.object({
3993
+ success: z.z.boolean()
3994
+ })
3995
+ }
4297
3996
  }
4298
- }, async (request, reply) => {
3997
+ }, async (request) => {
4299
3998
  const { sessionId } = request.body;
4300
3999
  types.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
4301
4000
  const success = stopSession(sessionId);
@@ -4306,28 +4005,68 @@ function startDaemonControlServer({
4306
4005
  body: z.z.object({
4307
4006
  directory: z.z.string(),
4308
4007
  sessionId: z.z.string().optional()
4309
- })
4008
+ }),
4009
+ response: {
4010
+ 200: z.z.object({
4011
+ success: z.z.boolean(),
4012
+ sessionId: z.z.string().optional(),
4013
+ approvedNewDirectoryCreation: z.z.boolean().optional()
4014
+ }),
4015
+ 409: z.z.object({
4016
+ success: z.z.boolean(),
4017
+ requiresUserApproval: z.z.boolean().optional(),
4018
+ actionRequired: z.z.string().optional(),
4019
+ directory: z.z.string().optional()
4020
+ }),
4021
+ 500: z.z.object({
4022
+ success: z.z.boolean(),
4023
+ error: z.z.string().optional()
4024
+ })
4025
+ }
4310
4026
  }
4311
4027
  }, async (request, reply) => {
4312
4028
  const { directory, sessionId } = request.body;
4313
4029
  types.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
4314
- const session = await spawnSession(directory, sessionId);
4315
- if (session) {
4316
- return {
4317
- success: true,
4318
- pid: session.pid,
4319
- sessionId: session.happySessionId || "pending",
4320
- message: session.message
4321
- };
4322
- } else {
4323
- reply.code(500);
4324
- return {
4325
- success: false,
4326
- error: "Failed to spawn session. Check the directory path and permissions."
4327
- };
4030
+ const result = await spawnSession({ directory, sessionId });
4031
+ switch (result.type) {
4032
+ case "success":
4033
+ if (!result.sessionId) {
4034
+ reply.code(500);
4035
+ return {
4036
+ success: false,
4037
+ error: "Failed to spawn session: no session ID returned"
4038
+ };
4039
+ }
4040
+ return {
4041
+ success: true,
4042
+ sessionId: result.sessionId,
4043
+ approvedNewDirectoryCreation: true
4044
+ };
4045
+ case "requestToApproveDirectoryCreation":
4046
+ reply.code(409);
4047
+ return {
4048
+ success: false,
4049
+ requiresUserApproval: true,
4050
+ actionRequired: "CREATE_DIRECTORY",
4051
+ directory: result.directory
4052
+ };
4053
+ case "error":
4054
+ reply.code(500);
4055
+ return {
4056
+ success: false,
4057
+ error: result.errorMessage
4058
+ };
4328
4059
  }
4329
4060
  });
4330
- typed.post("/stop", async (request, reply) => {
4061
+ typed.post("/stop", {
4062
+ schema: {
4063
+ response: {
4064
+ 200: z.z.object({
4065
+ status: z.z.string()
4066
+ })
4067
+ }
4068
+ }
4069
+ }, async () => {
4331
4070
  types.logger.debug("[CONTROL SERVER] Stop daemon request received");
4332
4071
  setTimeout(() => {
4333
4072
  types.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
@@ -4335,21 +4074,6 @@ function startDaemonControlServer({
4335
4074
  }, 50);
4336
4075
  return { status: "stopping" };
4337
4076
  });
4338
- typed.post("/dev-simulate-error", {
4339
- schema: {
4340
- body: z.z.object({
4341
- error: z.z.string()
4342
- })
4343
- }
4344
- }, async (request, reply) => {
4345
- const { error } = request.body;
4346
- types.logger.debug(`[CONTROL SERVER] Dev: Simulating error: ${error}`);
4347
- setTimeout(() => {
4348
- types.logger.debug(`[CONTROL SERVER] Dev: Throwing simulated error now`);
4349
- throw new Error(error);
4350
- }, 100);
4351
- return { status: "error will be thrown" };
4352
- });
4353
4077
  app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
4354
4078
  if (err) {
4355
4079
  types.logger.debug("[CONTROL SERVER] Failed to start:", err);
@@ -4417,7 +4141,7 @@ async function startDaemon() {
4417
4141
  });
4418
4142
  types.logger.debug("[DAEMON RUN] Starting daemon process...");
4419
4143
  types.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
4420
- const runningDaemonVersionMatches = await isDaemonRunningSameVersion();
4144
+ const runningDaemonVersionMatches = await isDaemonRunningCurrentlyInstalledHappyVersion();
4421
4145
  if (!runningDaemonVersionMatches) {
4422
4146
  types.logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
4423
4147
  await stopDaemon();
@@ -4472,16 +4196,22 @@ async function startDaemon() {
4472
4196
  types.logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
4473
4197
  }
4474
4198
  };
4475
- const spawnSession = async (directory, sessionId) => {
4199
+ const spawnSession = async (options) => {
4200
+ types.logger.debugLargeJson("[DAEMON RUN] Spawning session", options);
4201
+ const { directory, sessionId, machineId: machineId2, approvedNewDirectoryCreation = true } = options;
4476
4202
  let directoryCreated = false;
4477
- if (directory.startsWith("~")) {
4478
- directory = path.resolve(os$1.homedir(), directory.replace("~", ""));
4479
- }
4480
4203
  try {
4481
4204
  await fs.access(directory);
4482
4205
  types.logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
4483
4206
  } catch (error) {
4484
4207
  types.logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
4208
+ if (!approvedNewDirectoryCreation) {
4209
+ types.logger.debug(`[DAEMON RUN] Directory creation not approved for: ${directory}`);
4210
+ return {
4211
+ type: "requestToApproveDirectoryCreation",
4212
+ directory
4213
+ };
4214
+ }
4485
4215
  try {
4486
4216
  await fs.mkdir(directory, { recursive: true });
4487
4217
  types.logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
@@ -4500,7 +4230,10 @@ async function startDaemon() {
4500
4230
  errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
4501
4231
  }
4502
4232
  types.logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
4503
- return null;
4233
+ return {
4234
+ type: "error",
4235
+ errorMessage
4236
+ };
4504
4237
  }
4505
4238
  }
4506
4239
  try {
@@ -4528,7 +4261,10 @@ async function startDaemon() {
4528
4261
  }
4529
4262
  if (!happyProcess.pid) {
4530
4263
  types.logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
4531
- return null;
4264
+ return {
4265
+ type: "error",
4266
+ errorMessage: "Failed to spawn Happy process - no PID returned"
4267
+ };
4532
4268
  }
4533
4269
  types.logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
4534
4270
  const trackedSession = {
@@ -4556,17 +4292,27 @@ async function startDaemon() {
4556
4292
  const timeout = setTimeout(() => {
4557
4293
  pidToAwaiter.delete(happyProcess.pid);
4558
4294
  types.logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
4559
- resolve2(trackedSession);
4295
+ resolve2({
4296
+ type: "error",
4297
+ errorMessage: `Session webhook timeout for PID ${happyProcess.pid}`
4298
+ });
4560
4299
  }, 1e4);
4561
4300
  pidToAwaiter.set(happyProcess.pid, (completedSession) => {
4562
4301
  clearTimeout(timeout);
4563
4302
  types.logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
4564
- resolve2(completedSession);
4303
+ resolve2({
4304
+ type: "success",
4305
+ sessionId: completedSession.happySessionId
4306
+ });
4565
4307
  });
4566
4308
  });
4567
4309
  } catch (error) {
4310
+ const errorMessage = error instanceof Error ? error.message : String(error);
4568
4311
  types.logger.debug("[DAEMON RUN] Failed to spawn session:", error);
4569
- return null;
4312
+ return {
4313
+ type: "error",
4314
+ errorMessage: `Failed to spawn session: ${errorMessage}`
4315
+ };
4570
4316
  }
4571
4317
  };
4572
4318
  const stopSession = (sessionId) => {
@@ -4623,7 +4369,7 @@ async function startDaemon() {
4623
4369
  startedAt: Date.now()
4624
4370
  };
4625
4371
  const api = new types.ApiClient(credentials.token, credentials.secret);
4626
- const machine = await api.createMachineOrGetExistingAsIs({
4372
+ const machine = await api.getOrCreateMachine({
4627
4373
  machineId,
4628
4374
  metadata: initialMachineMetadata,
4629
4375
  daemonState: initialDaemonState
@@ -4654,7 +4400,7 @@ async function startDaemon() {
4654
4400
  pidToTrackedSession.delete(pid);
4655
4401
  }
4656
4402
  }
4657
- const projectVersion = JSON.parse(fs$1.readFileSync(path.join(projectPath(), "package.json"), "utf-8")).version;
4403
+ const projectVersion = JSON.parse(fs$1.readFileSync(path.join(types.projectPath(), "package.json"), "utf-8")).version;
4658
4404
  if (projectVersion !== types.configuration.currentCliVersion) {
4659
4405
  types.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
4660
4406
  clearInterval(restartOnStaleVersionAndHeartbeat);
@@ -4805,6 +4551,17 @@ async function startHappyServer(client) {
4805
4551
  };
4806
4552
  }
4807
4553
 
4554
+ function registerKillSessionHandler(rpcHandlerManager, killThisHappy) {
4555
+ rpcHandlerManager.registerHandler("killSession", async () => {
4556
+ types.logger.debug("Kill session request received");
4557
+ void killThisHappy();
4558
+ return {
4559
+ success: true,
4560
+ message: "Killing happy-cli process"
4561
+ };
4562
+ });
4563
+ }
4564
+
4808
4565
  async function start(credentials, options = {}) {
4809
4566
  const workingDirectory = process.cwd();
4810
4567
  const sessionTag = node_crypto.randomUUID();
@@ -4823,7 +4580,7 @@ async function start(credentials, options = {}) {
4823
4580
  process.exit(1);
4824
4581
  }
4825
4582
  types.logger.debug(`Using machineId: ${machineId}`);
4826
- await api.createMachineOrGetExistingAsIs({
4583
+ await api.getOrCreateMachine({
4827
4584
  machineId,
4828
4585
  metadata: initialMachineMetadata
4829
4586
  });
@@ -4891,7 +4648,6 @@ async function start(credentials, options = {}) {
4891
4648
  allowedTools: mode.allowedTools,
4892
4649
  disallowedTools: mode.disallowedTools
4893
4650
  }));
4894
- registerHandlers(session);
4895
4651
  let currentPermissionMode = options.permissionMode;
4896
4652
  let currentModel = options.model;
4897
4653
  let currentFallbackModel = void 0;
@@ -5038,6 +4794,7 @@ async function start(credentials, options = {}) {
5038
4794
  types.logger.debug("[START] Unhandled rejection:", reason);
5039
4795
  cleanup();
5040
4796
  });
4797
+ registerKillSessionHandler(session.rpcHandlerManager, cleanup);
5041
4798
  await loop({
5042
4799
  path: workingDirectory,
5043
4800
  model: options.model,
@@ -5053,7 +4810,7 @@ async function start(credentials, options = {}) {
5053
4810
  controlledByUser: newMode === "local"
5054
4811
  }));
5055
4812
  },
5056
- onSessionReady: (sessionInstance) => {
4813
+ onSessionReady: (_sessionInstance) => {
5057
4814
  },
5058
4815
  mcpServers: {
5059
4816
  "happy": {
@@ -5395,33 +5152,561 @@ async function handleAuthStatus() {
5395
5152
  }
5396
5153
  }
5397
5154
 
5398
- const DaemonPrompt = ({ onSelect }) => {
5399
- const [selectedIndex, setSelectedIndex] = React.useState(0);
5400
- const options = [
5401
- { value: true, label: "Yes (recommended)", key: "Y" },
5402
- { value: false, label: "No", key: "N" }
5403
- ];
5404
- ink.useInput((input, key) => {
5405
- const upperInput = input.toUpperCase();
5406
- if (key.upArrow || key.leftArrow) {
5407
- setSelectedIndex(0);
5408
- } else if (key.downArrow || key.rightArrow) {
5409
- setSelectedIndex(1);
5410
- } else if (key.return) {
5411
- onSelect(options[selectedIndex].value);
5412
- } else if (upperInput === "Y") {
5413
- onSelect(true);
5414
- } else if (upperInput === "N") {
5415
- onSelect(false);
5416
- } else if (key.escape || key.ctrl && input === "c") {
5417
- onSelect(false);
5155
+ const CLIENT_ID$2 = "app_EMoamEEZ73f0CkXaXp7hrann";
5156
+ const AUTH_BASE_URL = "https://auth.openai.com";
5157
+ const DEFAULT_PORT$2 = 1455;
5158
+ function generatePKCE$2() {
5159
+ const verifier = crypto.randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
5160
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
5161
+ return { verifier, challenge };
5162
+ }
5163
+ function generateState$2() {
5164
+ return crypto.randomBytes(16).toString("hex");
5165
+ }
5166
+ function parseJWT(token) {
5167
+ const parts = token.split(".");
5168
+ if (parts.length !== 3) {
5169
+ throw new Error("Invalid JWT format");
5170
+ }
5171
+ const payload = Buffer.from(parts[1], "base64url").toString();
5172
+ return JSON.parse(payload);
5173
+ }
5174
+ async function findAvailablePort$2() {
5175
+ return new Promise((resolve) => {
5176
+ const server = http.createServer();
5177
+ server.listen(0, "127.0.0.1", () => {
5178
+ const port = server.address().port;
5179
+ server.close(() => resolve(port));
5180
+ });
5181
+ });
5182
+ }
5183
+ async function isPortAvailable$2(port) {
5184
+ return new Promise((resolve) => {
5185
+ const testServer = http.createServer();
5186
+ testServer.once("error", () => {
5187
+ testServer.close();
5188
+ resolve(false);
5189
+ });
5190
+ testServer.listen(port, "127.0.0.1", () => {
5191
+ testServer.close(() => resolve(true));
5192
+ });
5193
+ });
5194
+ }
5195
+ async function exchangeCodeForTokens$2(code, verifier, port) {
5196
+ const response = await fetch(`${AUTH_BASE_URL}/oauth/token`, {
5197
+ method: "POST",
5198
+ headers: {
5199
+ "Content-Type": "application/x-www-form-urlencoded"
5200
+ },
5201
+ body: new URLSearchParams({
5202
+ grant_type: "authorization_code",
5203
+ client_id: CLIENT_ID$2,
5204
+ code,
5205
+ code_verifier: verifier,
5206
+ redirect_uri: `http://localhost:${port}/auth/callback`
5207
+ })
5208
+ });
5209
+ if (!response.ok) {
5210
+ const error = await response.text();
5211
+ throw new Error(`Token exchange failed: ${error}`);
5212
+ }
5213
+ const data = await response.json();
5214
+ const idTokenPayload = parseJWT(data.id_token);
5215
+ let accountId = idTokenPayload.chatgpt_account_id;
5216
+ if (!accountId) {
5217
+ const authClaim = idTokenPayload["https://api.openai.com/auth"];
5218
+ if (authClaim && typeof authClaim === "object") {
5219
+ accountId = authClaim.chatgpt_account_id || authClaim.account_id;
5418
5220
  }
5221
+ }
5222
+ return {
5223
+ id_token: data.id_token,
5224
+ access_token: data.access_token || data.id_token,
5225
+ refresh_token: data.refresh_token,
5226
+ account_id: accountId
5227
+ };
5228
+ }
5229
+ async function startCallbackServer$2(state, verifier, port) {
5230
+ return new Promise((resolve, reject) => {
5231
+ const server = http.createServer(async (req, res) => {
5232
+ const url = new URL(req.url, `http://localhost:${port}`);
5233
+ if (url.pathname === "/auth/callback") {
5234
+ const code = url.searchParams.get("code");
5235
+ const receivedState = url.searchParams.get("state");
5236
+ if (receivedState !== state) {
5237
+ res.writeHead(400);
5238
+ res.end("Invalid state parameter");
5239
+ server.close();
5240
+ reject(new Error("Invalid state parameter"));
5241
+ return;
5242
+ }
5243
+ if (!code) {
5244
+ res.writeHead(400);
5245
+ res.end("No authorization code received");
5246
+ server.close();
5247
+ reject(new Error("No authorization code received"));
5248
+ return;
5249
+ }
5250
+ try {
5251
+ const tokens = await exchangeCodeForTokens$2(code, verifier, port);
5252
+ res.writeHead(200, { "Content-Type": "text/html" });
5253
+ res.end(`
5254
+ <html>
5255
+ <body style="font-family: sans-serif; padding: 20px;">
5256
+ <h2>\u2705 Authentication Successful!</h2>
5257
+ <p>You can close this window and return to your terminal.</p>
5258
+ <script>setTimeout(() => window.close(), 3000);<\/script>
5259
+ </body>
5260
+ </html>
5261
+ `);
5262
+ server.close();
5263
+ resolve(tokens);
5264
+ } catch (error) {
5265
+ res.writeHead(500);
5266
+ res.end("Token exchange failed");
5267
+ server.close();
5268
+ reject(error);
5269
+ }
5270
+ }
5271
+ });
5272
+ server.listen(port, "127.0.0.1", () => {
5273
+ });
5274
+ setTimeout(() => {
5275
+ server.close();
5276
+ reject(new Error("Authentication timeout"));
5277
+ }, 5 * 60 * 1e3);
5419
5278
  });
5420
- return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(ink.Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { bold: true, color: "cyan" }, "\u{1F680} Happy Daemon Setup")), /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, null, "\u{1F4F1} Happy can run a background service that allows you to:"), /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan" }, " \u2022 Spawn new conversations from your phone"), /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan" }, " \u2022 Continue closed conversations remotely"), /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan" }, " \u2022 Work with Claude while your computer has internet")), /* @__PURE__ */ React.createElement(ink.Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, null, "Would you like Happy to start this service automatically?")), /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, options.map((option, index) => {
5421
- const isSelected = selectedIndex === index;
5422
- return /* @__PURE__ */ React.createElement(ink.Box, { key: option.key }, /* @__PURE__ */ React.createElement(ink.Text, { color: isSelected ? "green" : "gray" }, isSelected ? "\u203A " : " ", "[", option.key, "] ", option.label));
5423
- })), /* @__PURE__ */ React.createElement(ink.Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { dimColor: true }, "Press Y/N or use arrows + Enter to select")));
5424
- };
5279
+ }
5280
+ async function authenticateCodex() {
5281
+ const { verifier, challenge } = generatePKCE$2();
5282
+ const state = generateState$2();
5283
+ let port = DEFAULT_PORT$2;
5284
+ const portAvailable = await isPortAvailable$2(port);
5285
+ if (!portAvailable) {
5286
+ port = await findAvailablePort$2();
5287
+ }
5288
+ const serverPromise = startCallbackServer$2(state, verifier, port);
5289
+ await new Promise((resolve) => setTimeout(resolve, 100));
5290
+ const redirect_uri = `http://localhost:${port}/auth/callback`;
5291
+ const params = [
5292
+ ["response_type", "code"],
5293
+ ["client_id", CLIENT_ID$2],
5294
+ ["redirect_uri", redirect_uri],
5295
+ ["scope", "openid profile email offline_access"],
5296
+ ["code_challenge", challenge],
5297
+ ["code_challenge_method", "S256"],
5298
+ ["id_token_add_organizations", "true"],
5299
+ ["codex_cli_simplified_flow", "true"],
5300
+ ["state", state]
5301
+ ];
5302
+ const queryString = params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
5303
+ const authUrl = `${AUTH_BASE_URL}/oauth/authorize?${queryString}`;
5304
+ console.log("\u{1F4CB} Opening browser for authentication...");
5305
+ console.log(`If browser doesn't open, visit:
5306
+ ${authUrl}
5307
+ `);
5308
+ await openBrowser(authUrl);
5309
+ const tokens = await serverPromise;
5310
+ console.log("\u{1F389} Authentication successful!");
5311
+ return tokens;
5312
+ }
5313
+
5314
+ const CLIENT_ID$1 = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
5315
+ const CLAUDE_AI_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
5316
+ const TOKEN_URL$1 = "https://console.anthropic.com/v1/oauth/token";
5317
+ const DEFAULT_PORT$1 = 54545;
5318
+ const SCOPE = "user:inference";
5319
+ function generatePKCE$1() {
5320
+ const verifier = crypto.randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
5321
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
5322
+ return { verifier, challenge };
5323
+ }
5324
+ function generateState$1() {
5325
+ return crypto.randomBytes(32).toString("base64url");
5326
+ }
5327
+ async function findAvailablePort$1() {
5328
+ return new Promise((resolve) => {
5329
+ const server = http.createServer();
5330
+ server.listen(0, "127.0.0.1", () => {
5331
+ const port = server.address().port;
5332
+ server.close(() => resolve(port));
5333
+ });
5334
+ });
5335
+ }
5336
+ async function isPortAvailable$1(port) {
5337
+ return new Promise((resolve) => {
5338
+ const testServer = http.createServer();
5339
+ testServer.once("error", () => {
5340
+ testServer.close();
5341
+ resolve(false);
5342
+ });
5343
+ testServer.listen(port, "127.0.0.1", () => {
5344
+ testServer.close(() => resolve(true));
5345
+ });
5346
+ });
5347
+ }
5348
+ async function exchangeCodeForTokens$1(code, verifier, port, state) {
5349
+ const tokenResponse = await fetch(TOKEN_URL$1, {
5350
+ method: "POST",
5351
+ headers: {
5352
+ "Content-Type": "application/json"
5353
+ },
5354
+ body: JSON.stringify({
5355
+ grant_type: "authorization_code",
5356
+ code,
5357
+ redirect_uri: `http://localhost:${port}/callback`,
5358
+ client_id: CLIENT_ID$1,
5359
+ code_verifier: verifier,
5360
+ state
5361
+ })
5362
+ });
5363
+ if (!tokenResponse.ok) {
5364
+ throw new Error(`Token exchange failed: ${tokenResponse.statusText}`);
5365
+ }
5366
+ const tokenData = await tokenResponse.json();
5367
+ return {
5368
+ raw: tokenData,
5369
+ token: tokenData.access_token,
5370
+ expires: Date.now() + tokenData.expires_in * 1e3
5371
+ };
5372
+ }
5373
+ async function startCallbackServer$1(state, verifier, port) {
5374
+ return new Promise((resolve, reject) => {
5375
+ const server = http.createServer(async (req, res) => {
5376
+ const url = new URL(req.url, `http://localhost:${port}`);
5377
+ if (url.pathname === "/callback") {
5378
+ const code = url.searchParams.get("code");
5379
+ const receivedState = url.searchParams.get("state");
5380
+ if (receivedState !== state) {
5381
+ res.writeHead(400);
5382
+ res.end("Invalid state parameter");
5383
+ server.close();
5384
+ reject(new Error("Invalid state parameter"));
5385
+ return;
5386
+ }
5387
+ if (!code) {
5388
+ res.writeHead(400);
5389
+ res.end("No authorization code received");
5390
+ server.close();
5391
+ reject(new Error("No authorization code received"));
5392
+ return;
5393
+ }
5394
+ try {
5395
+ const tokens = await exchangeCodeForTokens$1(code, verifier, port, state);
5396
+ res.writeHead(302, {
5397
+ "Location": "https://console.anthropic.com/oauth/code/success?app=claude-code"
5398
+ });
5399
+ res.end();
5400
+ server.close();
5401
+ resolve(tokens);
5402
+ } catch (error) {
5403
+ res.writeHead(500);
5404
+ res.end("Token exchange failed");
5405
+ server.close();
5406
+ reject(error);
5407
+ }
5408
+ }
5409
+ });
5410
+ server.listen(port, "127.0.0.1", () => {
5411
+ });
5412
+ setTimeout(() => {
5413
+ server.close();
5414
+ reject(new Error("Authentication timeout"));
5415
+ }, 5 * 60 * 1e3);
5416
+ });
5417
+ }
5418
+ async function authenticateClaude() {
5419
+ console.log("\u{1F680} Starting Anthropic Claude authentication...");
5420
+ const { verifier, challenge } = generatePKCE$1();
5421
+ const state = generateState$1();
5422
+ let port = DEFAULT_PORT$1;
5423
+ const portAvailable = await isPortAvailable$1(port);
5424
+ if (!portAvailable) {
5425
+ console.log(`Port ${port} is in use, finding an available port...`);
5426
+ port = await findAvailablePort$1();
5427
+ }
5428
+ console.log(`\u{1F4E1} Using callback port: ${port}`);
5429
+ const serverPromise = startCallbackServer$1(state, verifier, port);
5430
+ await new Promise((resolve) => setTimeout(resolve, 100));
5431
+ const redirect_uri = `http://localhost:${port}/callback`;
5432
+ const params = new URLSearchParams({
5433
+ code: "true",
5434
+ // This tells Claude.ai to show the code AND redirect
5435
+ client_id: CLIENT_ID$1,
5436
+ response_type: "code",
5437
+ redirect_uri,
5438
+ scope: SCOPE,
5439
+ code_challenge: challenge,
5440
+ code_challenge_method: "S256",
5441
+ state
5442
+ });
5443
+ const authUrl = `${CLAUDE_AI_AUTHORIZE_URL}?${params}`;
5444
+ console.log("\u{1F4CB} Opening browser for authentication...");
5445
+ console.log("If browser doesn't open, visit this URL:");
5446
+ console.log();
5447
+ console.log(`${authUrl}`);
5448
+ console.log();
5449
+ await openBrowser(authUrl);
5450
+ try {
5451
+ const tokens = await serverPromise;
5452
+ console.log("\u{1F389} Authentication successful!");
5453
+ console.log("\u2705 OAuth tokens received");
5454
+ return tokens;
5455
+ } catch (error) {
5456
+ console.error("\n\u274C Failed to authenticate with Anthropic");
5457
+ throw error;
5458
+ }
5459
+ }
5460
+
5461
+ const execAsync = util.promisify(child_process.exec);
5462
+ const CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
5463
+ const CLIENT_SECRET = "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";
5464
+ const AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
5465
+ const TOKEN_URL = "https://oauth2.googleapis.com/token";
5466
+ const DEFAULT_PORT = 54545;
5467
+ const SCOPES = [
5468
+ "https://www.googleapis.com/auth/cloud-platform",
5469
+ "https://www.googleapis.com/auth/userinfo.email",
5470
+ "https://www.googleapis.com/auth/userinfo.profile"
5471
+ ].join(" ");
5472
+ function generatePKCE() {
5473
+ const verifier = crypto.randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "");
5474
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
5475
+ return { verifier, challenge };
5476
+ }
5477
+ function generateState() {
5478
+ return crypto.randomBytes(32).toString("hex");
5479
+ }
5480
+ async function findAvailablePort() {
5481
+ return new Promise((resolve) => {
5482
+ const server = http.createServer();
5483
+ server.listen(0, "127.0.0.1", () => {
5484
+ const port = server.address().port;
5485
+ server.close(() => resolve(port));
5486
+ });
5487
+ });
5488
+ }
5489
+ async function isPortAvailable(port) {
5490
+ return new Promise((resolve) => {
5491
+ const testServer = http.createServer();
5492
+ testServer.once("error", () => {
5493
+ testServer.close();
5494
+ resolve(false);
5495
+ });
5496
+ testServer.listen(port, "127.0.0.1", () => {
5497
+ testServer.close(() => resolve(true));
5498
+ });
5499
+ });
5500
+ }
5501
+ async function exchangeCodeForTokens(code, verifier, port) {
5502
+ const response = await fetch(TOKEN_URL, {
5503
+ method: "POST",
5504
+ headers: {
5505
+ "Content-Type": "application/x-www-form-urlencoded"
5506
+ },
5507
+ body: new URLSearchParams({
5508
+ grant_type: "authorization_code",
5509
+ client_id: CLIENT_ID,
5510
+ client_secret: CLIENT_SECRET,
5511
+ code,
5512
+ code_verifier: verifier,
5513
+ redirect_uri: `http://localhost:${port}/oauth2callback`
5514
+ })
5515
+ });
5516
+ if (!response.ok) {
5517
+ const error = await response.text();
5518
+ throw new Error(`Token exchange failed: ${error}`);
5519
+ }
5520
+ const data = await response.json();
5521
+ return data;
5522
+ }
5523
+ async function startCallbackServer(state, verifier, port) {
5524
+ return new Promise((resolve, reject) => {
5525
+ const server = http.createServer(async (req, res) => {
5526
+ const url = new URL(req.url, `http://localhost:${port}`);
5527
+ if (url.pathname === "/oauth2callback") {
5528
+ const code = url.searchParams.get("code");
5529
+ const receivedState = url.searchParams.get("state");
5530
+ const error = url.searchParams.get("error");
5531
+ if (error) {
5532
+ res.writeHead(302, {
5533
+ "Location": "https://developers.google.com/gemini-code-assist/auth_failure_gemini"
5534
+ });
5535
+ res.end();
5536
+ server.close();
5537
+ reject(new Error(`Authentication error: ${error}`));
5538
+ return;
5539
+ }
5540
+ if (receivedState !== state) {
5541
+ res.writeHead(400);
5542
+ res.end("State mismatch. Possible CSRF attack");
5543
+ server.close();
5544
+ reject(new Error("Invalid state parameter"));
5545
+ return;
5546
+ }
5547
+ if (!code) {
5548
+ res.writeHead(400);
5549
+ res.end("No authorization code received");
5550
+ server.close();
5551
+ reject(new Error("No authorization code received"));
5552
+ return;
5553
+ }
5554
+ try {
5555
+ const tokens = await exchangeCodeForTokens(code, verifier, port);
5556
+ res.writeHead(302, {
5557
+ "Location": "https://developers.google.com/gemini-code-assist/auth_success_gemini"
5558
+ });
5559
+ res.end();
5560
+ server.close();
5561
+ resolve(tokens);
5562
+ } catch (error2) {
5563
+ res.writeHead(500);
5564
+ res.end("Token exchange failed");
5565
+ server.close();
5566
+ reject(error2);
5567
+ }
5568
+ }
5569
+ });
5570
+ server.listen(port, "127.0.0.1", () => {
5571
+ });
5572
+ setTimeout(() => {
5573
+ server.close();
5574
+ reject(new Error("Authentication timeout"));
5575
+ }, 5 * 60 * 1e3);
5576
+ });
5577
+ }
5578
+ async function authenticateGemini() {
5579
+ console.log("\u{1F680} Starting Google Gemini authentication...");
5580
+ const { verifier, challenge } = generatePKCE();
5581
+ const state = generateState();
5582
+ let port = DEFAULT_PORT;
5583
+ const portAvailable = await isPortAvailable(port);
5584
+ if (!portAvailable) {
5585
+ console.log(`Port ${port} is in use, finding an available port...`);
5586
+ port = await findAvailablePort();
5587
+ }
5588
+ console.log(`\u{1F4E1} Using callback port: ${port}`);
5589
+ const serverPromise = startCallbackServer(state, verifier, port);
5590
+ await new Promise((resolve) => setTimeout(resolve, 100));
5591
+ const redirect_uri = `http://localhost:${port}/oauth2callback`;
5592
+ const params = new URLSearchParams({
5593
+ client_id: CLIENT_ID,
5594
+ response_type: "code",
5595
+ redirect_uri,
5596
+ scope: SCOPES,
5597
+ access_type: "offline",
5598
+ // To get refresh token
5599
+ code_challenge: challenge,
5600
+ code_challenge_method: "S256",
5601
+ state,
5602
+ prompt: "consent"
5603
+ // Force consent to get refresh token
5604
+ });
5605
+ const authUrl = `${AUTHORIZE_URL}?${params}`;
5606
+ console.log("\n\u{1F4CB} Opening browser for authentication...");
5607
+ console.log("If browser doesn't open, visit this URL:");
5608
+ console.log(`
5609
+ ${authUrl}
5610
+ `);
5611
+ const platform = process.platform;
5612
+ const openCommand = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
5613
+ try {
5614
+ await execAsync(`${openCommand} "${authUrl}"`);
5615
+ } catch {
5616
+ console.log("\u26A0\uFE0F Could not open browser automatically");
5617
+ }
5618
+ try {
5619
+ const tokens = await serverPromise;
5620
+ console.log("\n\u{1F389} Authentication successful!");
5621
+ console.log("\u2705 OAuth tokens received");
5622
+ return tokens;
5623
+ } catch (error) {
5624
+ console.error("\n\u274C Failed to authenticate with Google");
5625
+ throw error;
5626
+ }
5627
+ }
5628
+
5629
+ async function handleConnectCommand(args) {
5630
+ const subcommand = args[0];
5631
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
5632
+ showConnectHelp();
5633
+ return;
5634
+ }
5635
+ switch (subcommand.toLowerCase()) {
5636
+ case "codex":
5637
+ await handleConnectVendor("codex", "OpenAI");
5638
+ break;
5639
+ case "claude":
5640
+ await handleConnectVendor("claude", "Anthropic");
5641
+ break;
5642
+ case "gemini":
5643
+ await handleConnectVendor("gemini", "Gemini");
5644
+ break;
5645
+ default:
5646
+ console.error(chalk.red(`Unknown connect target: ${subcommand}`));
5647
+ showConnectHelp();
5648
+ process.exit(1);
5649
+ }
5650
+ }
5651
+ function showConnectHelp() {
5652
+ console.log(`
5653
+ ${chalk.bold("happy connect")} - Connect AI vendor API keys to Happy cloud
5654
+
5655
+ ${chalk.bold("Usage:")}
5656
+ happy connect codex Store your Codex API key in Happy cloud
5657
+ happy connect anthropic Store your Anthropic API key in Happy cloud
5658
+ happy connect gemini Store your Gemini API key in Happy cloud
5659
+ happy connect help Show this help message
5660
+
5661
+ ${chalk.bold("Description:")}
5662
+ The connect command allows you to securely store your AI vendor API keys
5663
+ in Happy cloud. This enables you to use these services through Happy
5664
+ without exposing your API keys locally.
5665
+
5666
+ ${chalk.bold("Examples:")}
5667
+ happy connect codex
5668
+ happy connect anthropic
5669
+ happy connect gemini
5670
+
5671
+ ${chalk.bold("Notes:")}
5672
+ \u2022 You must be authenticated with Happy first (run 'happy auth login')
5673
+ \u2022 API keys are encrypted and stored securely in Happy cloud
5674
+ \u2022 You can manage your stored keys at app.happy.engineering
5675
+ `);
5676
+ }
5677
+ async function handleConnectVendor(vendor, displayName) {
5678
+ console.log(chalk.bold(`
5679
+ \u{1F50C} Connecting ${displayName} to Happy cloud
5680
+ `));
5681
+ const credentials = await types.readCredentials();
5682
+ if (!credentials) {
5683
+ console.log(chalk.yellow("\u26A0\uFE0F Not authenticated with Happy"));
5684
+ console.log(chalk.gray(' Please run "happy auth login" first'));
5685
+ process.exit(1);
5686
+ }
5687
+ const api = new types.ApiClient(credentials.token, credentials.secret);
5688
+ if (vendor === "codex") {
5689
+ console.log("\u{1F680} Registering Codex token with server");
5690
+ const codexAuthTokens = await authenticateCodex();
5691
+ await api.registerVendorToken("openai", { oauth: codexAuthTokens });
5692
+ console.log("\u2705 Codex token registered with server");
5693
+ process.exit(0);
5694
+ } else if (vendor === "claude") {
5695
+ console.log("\u{1F680} Registering Anthropic token with server");
5696
+ const anthropicAuthTokens = await authenticateClaude();
5697
+ await api.registerVendorToken("anthropic", { oauth: anthropicAuthTokens });
5698
+ console.log("\u2705 Anthropic token registered with server");
5699
+ process.exit(0);
5700
+ } else if (vendor === "gemini") {
5701
+ console.log("\u{1F680} Registering Gemini token with server");
5702
+ const geminiAuthTokens = await authenticateGemini();
5703
+ await api.registerVendorToken("gemini", { oauth: geminiAuthTokens });
5704
+ console.log("\u2705 Gemini token registered with server");
5705
+ process.exit(0);
5706
+ } else {
5707
+ throw new Error(`Unsupported vendor: ${vendor}`);
5708
+ }
5709
+ }
5425
5710
 
5426
5711
  (async () => {
5427
5712
  const args = process.argv.slice(2);
@@ -5451,6 +5736,17 @@ const DaemonPrompt = ({ onSelect }) => {
5451
5736
  process.exit(1);
5452
5737
  }
5453
5738
  return;
5739
+ } else if (subcommand === "connect") {
5740
+ try {
5741
+ await handleConnectCommand(args.slice(1));
5742
+ } catch (error) {
5743
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
5744
+ if (process.env.DEBUG) {
5745
+ console.error(error);
5746
+ }
5747
+ process.exit(1);
5748
+ }
5749
+ return;
5454
5750
  } else if (subcommand === "logout") {
5455
5751
  console.log(chalk.yellow('Note: "happy logout" is deprecated. Use "happy auth logout" instead.\n'));
5456
5752
  try {
@@ -5629,9 +5925,8 @@ ${chalk.bold("Happy supports ALL Claude options!")}
5629
5925
  ${chalk.gray("\u2500".repeat(60))}
5630
5926
  ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
5631
5927
  `);
5632
- const { execSync } = await import('child_process');
5633
5928
  try {
5634
- const claudeHelp = execSync("claude --help", { encoding: "utf8" });
5929
+ const claudeHelp = node_child_process.execFileSync(process.execPath, [claudeCliPath, "--help"], { encoding: "utf8" });
5635
5930
  console.log(claudeHelp);
5636
5931
  } catch (e) {
5637
5932
  console.log(chalk.yellow("Could not retrieve claude help. Make sure claude is installed."));
@@ -5644,47 +5939,16 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
5644
5939
  const {
5645
5940
  credentials
5646
5941
  } = await authAndSetupMachineIfNeeded();
5647
- let settings = await types.readSettings();
5648
- if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
5649
- const shouldAutoStart = await new Promise((resolve) => {
5650
- let hasResolved = false;
5651
- const onSelect = (autoStart) => {
5652
- if (!hasResolved) {
5653
- hasResolved = true;
5654
- app.unmount();
5655
- resolve(autoStart);
5656
- }
5657
- };
5658
- const app = ink.render(React.createElement(DaemonPrompt, { onSelect }), {
5659
- exitOnCtrlC: false,
5660
- patchConsole: false
5661
- });
5942
+ types.logger.debug("Ensuring Happy background service is running & matches our version...");
5943
+ if (!await isDaemonRunningCurrentlyInstalledHappyVersion()) {
5944
+ types.logger.debug("Starting Happy background service...");
5945
+ const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
5946
+ detached: true,
5947
+ stdio: "ignore",
5948
+ env: process.env
5662
5949
  });
5663
- settings = await types.updateSettings((settings2) => ({
5664
- ...settings2,
5665
- daemonAutoStartWhenRunningHappy: shouldAutoStart
5666
- }));
5667
- if (shouldAutoStart) {
5668
- console.log(chalk.green("\n\u2713 Happy will start the background service automatically"));
5669
- console.log(chalk.gray(" The service will run whenever you use the happy command"));
5670
- } else {
5671
- console.log(chalk.yellow("\n You can enable this later by running: happy daemon install"));
5672
- }
5673
- }
5674
- if (settings && settings.daemonAutoStartWhenRunningHappy) {
5675
- types.logger.debug("Ensuring Happy background service is running & matches our version...");
5676
- if (!await isDaemonRunningSameVersion()) {
5677
- types.logger.debug("Starting Happy background service...");
5678
- const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
5679
- detached: true,
5680
- stdio: "ignore",
5681
- env: process.env
5682
- });
5683
- daemonProcess.unref();
5684
- await new Promise((resolve) => setTimeout(resolve, 100));
5685
- } else {
5686
- types.logger.debug("Happy background service is running & matches our version");
5687
- }
5950
+ daemonProcess.unref();
5951
+ await new Promise((resolve) => setTimeout(resolve, 200));
5688
5952
  }
5689
5953
  try {
5690
5954
  await start(credentials, options);