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