happy-coder 0.7.1-beta.3 → 0.7.2

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,12 +1,12 @@
1
1
  import chalk from 'chalk';
2
- import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, c as configuration, e as encodeBase64, A as ApiClient, f as encodeBase64Url, g as decodeBase64 } from './types-BZC9-exR.mjs';
2
+ import { l as logger, b as backoff, d as delay, R as RawJSONLinesSchema, c as configuration, e as encodeBase64, f as encodeBase64Url, g as decodeBase64, A as ApiClient } from './types-CJU8o8xd.mjs';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
4
  import { spawn, execSync } from 'node:child_process';
5
5
  import { resolve, join, dirname as dirname$1 } from 'node:path';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { fileURLToPath as fileURLToPath$1 } from 'node:url';
8
8
  import { existsSync, readFileSync, mkdirSync, watch, constants, readdirSync, statSync, rmSync } from 'node:fs';
9
- import os, { homedir } from 'node:os';
9
+ import os$1, { homedir } from 'node:os';
10
10
  import { dirname, resolve as resolve$1, join as join$1 } from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { readFile, unlink, mkdir, writeFile as writeFile$1, open, stat as stat$1, rename } from 'node:fs/promises';
@@ -26,11 +26,11 @@ import { z as z$1 } from 'zod';
26
26
  import { spawn as spawn$1, exec, execSync as execSync$1 } from 'child_process';
27
27
  import { promisify } from 'util';
28
28
  import { createHash } from 'crypto';
29
- import qrcode from 'qrcode-terminal';
30
- import open$1 from 'open';
31
29
  import fastify from 'fastify';
32
30
  import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
33
- import os$1 from 'os';
31
+ import os from 'os';
32
+ import qrcode from 'qrcode-terminal';
33
+ import open$1 from 'open';
34
34
  import { existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync } from 'fs';
35
35
 
36
36
  class Session {
@@ -2343,7 +2343,7 @@ async function loop(opts) {
2343
2343
  }
2344
2344
 
2345
2345
  var name = "happy-coder";
2346
- var version = "0.7.1-beta.3";
2346
+ var version = "0.7.2";
2347
2347
  var description = "Claude Code session sharing CLI";
2348
2348
  var author = "Kirill Dubovitskiy";
2349
2349
  var license = "MIT";
@@ -2428,7 +2428,7 @@ var dependencies = {
2428
2428
  };
2429
2429
  var devDependencies = {
2430
2430
  "@eslint/compat": "^1",
2431
- "@types/node": ">=18",
2431
+ "@types/node": ">=20",
2432
2432
  "cross-env": "^10.0.0",
2433
2433
  eslint: "^9",
2434
2434
  "eslint-config-prettier": "^10",
@@ -2439,9 +2439,10 @@ var devDependencies = {
2439
2439
  typescript: "^5",
2440
2440
  vitest: "^3.2.4"
2441
2441
  };
2442
- var overrides = {
2442
+ var resolutions = {
2443
2443
  "whatwg-url": "14.2.0"
2444
2444
  };
2445
+ var packageManager = "yarn@1.22.22";
2445
2446
  var packageJson = {
2446
2447
  name: name,
2447
2448
  version: version,
@@ -2461,7 +2462,8 @@ var packageJson = {
2461
2462
  scripts: scripts,
2462
2463
  dependencies: dependencies,
2463
2464
  devDependencies: devDependencies,
2464
- overrides: overrides
2465
+ resolutions: resolutions,
2466
+ packageManager: packageManager
2465
2467
  };
2466
2468
 
2467
2469
  function run(args, options) {
@@ -3525,6 +3527,16 @@ async function runDoctorCommand() {
3525
3527
  console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
3526
3528
  console.log(`Node.js Version: ${chalk.green(process.version)}`);
3527
3529
  console.log("");
3530
+ console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
3531
+ const projectRoot = projectPath();
3532
+ const wrapperPath = join(projectRoot, "bin", "happy.mjs");
3533
+ const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
3534
+ console.log(`Project Root: ${chalk.blue(projectRoot)}`);
3535
+ console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
3536
+ console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
3537
+ console.log(`Wrapper Exists: ${existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
3538
+ console.log(`CLI Exists: ${existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
3539
+ console.log("");
3528
3540
  console.log(chalk.bold("\u2699\uFE0F Configuration"));
3529
3541
  console.log(`Happy Home: ${chalk.blue(configuration.happyHomeDir)}`);
3530
3542
  console.log(`Server URL: ${chalk.blue(configuration.serverUrl)}`);
@@ -3696,313 +3708,175 @@ var controlClient = /*#__PURE__*/Object.freeze({
3696
3708
  stopDaemonSession: stopDaemonSession
3697
3709
  });
3698
3710
 
3699
- async function start(credentials, options = {}) {
3700
- const workingDirectory = process.cwd();
3701
- const sessionTag = randomUUID();
3702
- logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
3703
- logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
3704
- if (options.startedBy === "daemon" && options.startingMode === "local") {
3705
- logger.debug("Daemon spawn requested with local mode - forcing remote mode");
3706
- options.startingMode = "remote";
3707
- }
3708
- const api = new ApiClient(credentials.token, credentials.secret);
3709
- let state = {};
3710
- const settings = await readSettings();
3711
- const machineId = settings?.machineId || "unknown";
3712
- logger.debug(`Using machineId: ${machineId}`);
3713
- let metadata = {
3714
- path: workingDirectory,
3715
- host: os.hostname(),
3716
- version: packageJson.version,
3717
- os: os.platform(),
3718
- machineId,
3719
- homeDir: os.homedir(),
3720
- happyHomeDir: configuration.happyHomeDir,
3721
- startedFromDaemon: options.startedBy === "daemon",
3722
- hostPid: process.pid,
3723
- startedBy: options.startedBy || "terminal"
3724
- };
3725
- const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
3726
- logger.debug(`Session created: ${response.id}`);
3711
+ function startDaemonControlServer({
3712
+ getChildren,
3713
+ stopSession,
3714
+ spawnSession,
3715
+ requestShutdown,
3716
+ onHappySessionWebhook
3717
+ }) {
3718
+ return new Promise((resolve) => {
3719
+ const app = fastify({
3720
+ logger: false
3721
+ // We use our own logger
3722
+ });
3723
+ app.setValidatorCompiler(validatorCompiler);
3724
+ app.setSerializerCompiler(serializerCompiler);
3725
+ const typed = app.withTypeProvider();
3726
+ typed.post("/session-started", {
3727
+ schema: {
3728
+ body: z$1.object({
3729
+ sessionId: z$1.string(),
3730
+ metadata: z$1.any()
3731
+ // Metadata type from API
3732
+ })
3733
+ }
3734
+ }, async (request, reply) => {
3735
+ const { sessionId, metadata } = request.body;
3736
+ logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
3737
+ onHappySessionWebhook(sessionId, metadata);
3738
+ return { status: "ok" };
3739
+ });
3740
+ typed.post("/list", async (request, reply) => {
3741
+ const children = getChildren();
3742
+ logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
3743
+ return { children };
3744
+ });
3745
+ typed.post("/stop-session", {
3746
+ schema: {
3747
+ body: z$1.object({
3748
+ sessionId: z$1.string()
3749
+ })
3750
+ }
3751
+ }, async (request, reply) => {
3752
+ const { sessionId } = request.body;
3753
+ logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
3754
+ const success = stopSession(sessionId);
3755
+ return { success };
3756
+ });
3757
+ typed.post("/spawn-session", {
3758
+ schema: {
3759
+ body: z$1.object({
3760
+ directory: z$1.string(),
3761
+ sessionId: z$1.string().optional()
3762
+ })
3763
+ }
3764
+ }, async (request, reply) => {
3765
+ const { directory, sessionId } = request.body;
3766
+ logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
3767
+ const session = await spawnSession(directory, sessionId);
3768
+ if (session) {
3769
+ return {
3770
+ success: true,
3771
+ pid: session.pid,
3772
+ sessionId: session.happySessionId || "pending"
3773
+ };
3774
+ } else {
3775
+ reply.code(500);
3776
+ return { error: "Failed to spawn session" };
3777
+ }
3778
+ });
3779
+ typed.post("/stop", async (request, reply) => {
3780
+ logger.debug("[CONTROL SERVER] Stop daemon request received");
3781
+ setTimeout(() => {
3782
+ logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
3783
+ requestShutdown();
3784
+ }, 50);
3785
+ return { status: "stopping" };
3786
+ });
3787
+ app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
3788
+ if (err) {
3789
+ logger.debug("[CONTROL SERVER] Failed to start:", err);
3790
+ throw err;
3791
+ }
3792
+ const port = parseInt(address.split(":").pop());
3793
+ logger.debug(`[CONTROL SERVER] Started on port ${port}`);
3794
+ resolve({
3795
+ port,
3796
+ stop: async () => {
3797
+ logger.debug("[CONTROL SERVER] Stopping server");
3798
+ await app.close();
3799
+ logger.debug("[CONTROL SERVER] Server stopped");
3800
+ }
3801
+ });
3802
+ });
3803
+ });
3804
+ }
3805
+
3806
+ function displayQRCode(url) {
3807
+ console.log("=".repeat(80));
3808
+ console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
3809
+ console.log("=".repeat(80));
3810
+ qrcode.generate(url, { small: true }, (qr) => {
3811
+ for (let l of qr.split("\n")) {
3812
+ console.log(" ".repeat(10) + l);
3813
+ }
3814
+ });
3815
+ console.log("=".repeat(80));
3816
+ }
3817
+
3818
+ function generateWebAuthUrl(publicKey) {
3819
+ const publicKeyBase64 = encodeBase64(publicKey, "base64url");
3820
+ return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
3821
+ }
3822
+
3823
+ async function openBrowser(url) {
3727
3824
  try {
3728
- const daemonState = await getDaemonState();
3729
- if (daemonState?.httpPort) {
3730
- await notifyDaemonSessionStarted(response.id, metadata);
3731
- logger.debug(`[START] Reported session ${response.id} to daemon`);
3825
+ if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
3826
+ logger.debug("[browser] Headless environment detected, skipping browser open");
3827
+ return false;
3732
3828
  }
3829
+ logger.debug(`[browser] Attempting to open URL: ${url}`);
3830
+ await open$1(url);
3831
+ logger.debug("[browser] Browser opened successfully");
3832
+ return true;
3733
3833
  } catch (error) {
3734
- logger.debug("[START] Failed to report to daemon (may not be running):", error);
3834
+ logger.debug("[browser] Failed to open browser:", error);
3835
+ return false;
3735
3836
  }
3736
- extractSDKMetadataAsync(async (sdkMetadata) => {
3737
- logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
3738
- try {
3739
- api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
3740
- ...currentMetadata,
3741
- tools: sdkMetadata.tools,
3742
- slashCommands: sdkMetadata.slashCommands
3743
- }));
3744
- logger.debug("[start] Session metadata updated with SDK capabilities");
3745
- } catch (error) {
3746
- logger.debug("[start] Failed to update session metadata:", error);
3837
+ }
3838
+
3839
+ const AuthSelector = ({ onSelect, onCancel }) => {
3840
+ const [selectedIndex, setSelectedIndex] = useState(0);
3841
+ const options = [
3842
+ {
3843
+ method: "mobile",
3844
+ label: "Mobile App"
3845
+ },
3846
+ {
3847
+ method: "web",
3848
+ label: "Web Browser"
3849
+ }
3850
+ ];
3851
+ useInput((input, key) => {
3852
+ if (key.upArrow) {
3853
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
3854
+ } else if (key.downArrow) {
3855
+ setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
3856
+ } else if (key.return) {
3857
+ onSelect(options[selectedIndex].method);
3858
+ } else if (key.escape || key.ctrl && input === "c") {
3859
+ onCancel();
3860
+ } else if (input === "1") {
3861
+ setSelectedIndex(0);
3862
+ onSelect("mobile");
3863
+ } else if (input === "2") {
3864
+ setSelectedIndex(1);
3865
+ onSelect("web");
3747
3866
  }
3748
3867
  });
3749
- const session = api.sessionSyncClient(response);
3750
- const logPath = await logger.logFilePathPromise;
3751
- logger.infoDeveloper(`Session: ${response.id}`);
3752
- logger.infoDeveloper(`Logs: ${logPath}`);
3753
- session.updateAgentState((currentState) => ({
3754
- ...currentState,
3755
- controlledByUser: options.startingMode !== "remote"
3756
- }));
3757
- const caffeinateStarted = startCaffeinate();
3758
- if (caffeinateStarted) {
3759
- logger.infoDeveloper("Sleep prevention enabled (macOS)");
3760
- }
3761
- const messageQueue = new MessageQueue2((mode) => hashObject(mode));
3762
- registerHandlers(session);
3763
- let currentPermissionMode = options.permissionMode;
3764
- let currentModel = options.model;
3765
- let currentFallbackModel = void 0;
3766
- let currentCustomSystemPrompt = void 0;
3767
- let currentAppendSystemPrompt = void 0;
3768
- let currentAllowedTools = void 0;
3769
- let currentDisallowedTools = void 0;
3770
- session.onUserMessage((message) => {
3771
- let messagePermissionMode = currentPermissionMode;
3772
- if (message.meta?.permissionMode) {
3773
- const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
3774
- if (validModes.includes(message.meta.permissionMode)) {
3775
- messagePermissionMode = message.meta.permissionMode;
3776
- currentPermissionMode = messagePermissionMode;
3777
- logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
3778
- } else {
3779
- logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
3780
- }
3781
- } else {
3782
- logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
3783
- }
3784
- let messageModel = currentModel;
3785
- if (message.meta?.hasOwnProperty("model")) {
3786
- messageModel = message.meta.model || void 0;
3787
- currentModel = messageModel;
3788
- logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
3789
- } else {
3790
- logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
3791
- }
3792
- let messageCustomSystemPrompt = currentCustomSystemPrompt;
3793
- if (message.meta?.hasOwnProperty("customSystemPrompt")) {
3794
- messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
3795
- currentCustomSystemPrompt = messageCustomSystemPrompt;
3796
- logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
3797
- } else {
3798
- logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
3799
- }
3800
- let messageFallbackModel = currentFallbackModel;
3801
- if (message.meta?.hasOwnProperty("fallbackModel")) {
3802
- messageFallbackModel = message.meta.fallbackModel || void 0;
3803
- currentFallbackModel = messageFallbackModel;
3804
- logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
3805
- } else {
3806
- logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
3807
- }
3808
- let messageAppendSystemPrompt = currentAppendSystemPrompt;
3809
- if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
3810
- messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
3811
- currentAppendSystemPrompt = messageAppendSystemPrompt;
3812
- logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
3813
- } else {
3814
- logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
3815
- }
3816
- let messageAllowedTools = currentAllowedTools;
3817
- if (message.meta?.hasOwnProperty("allowedTools")) {
3818
- messageAllowedTools = message.meta.allowedTools || void 0;
3819
- currentAllowedTools = messageAllowedTools;
3820
- logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
3821
- } else {
3822
- logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
3823
- }
3824
- let messageDisallowedTools = currentDisallowedTools;
3825
- if (message.meta?.hasOwnProperty("disallowedTools")) {
3826
- messageDisallowedTools = message.meta.disallowedTools || void 0;
3827
- currentDisallowedTools = messageDisallowedTools;
3828
- logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
3829
- } else {
3830
- logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
3831
- }
3832
- const specialCommand = parseSpecialCommand(message.content.text);
3833
- if (specialCommand.type === "compact") {
3834
- logger.debug("[start] Detected /compact command");
3835
- const enhancedMode2 = {
3836
- permissionMode: messagePermissionMode || "default",
3837
- model: messageModel,
3838
- fallbackModel: messageFallbackModel,
3839
- customSystemPrompt: messageCustomSystemPrompt,
3840
- appendSystemPrompt: messageAppendSystemPrompt,
3841
- allowedTools: messageAllowedTools,
3842
- disallowedTools: messageDisallowedTools
3843
- };
3844
- messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3845
- logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3846
- return;
3847
- }
3848
- if (specialCommand.type === "clear") {
3849
- logger.debug("[start] Detected /clear command");
3850
- const enhancedMode2 = {
3851
- permissionMode: messagePermissionMode || "default",
3852
- model: messageModel,
3853
- fallbackModel: messageFallbackModel,
3854
- customSystemPrompt: messageCustomSystemPrompt,
3855
- appendSystemPrompt: messageAppendSystemPrompt,
3856
- allowedTools: messageAllowedTools,
3857
- disallowedTools: messageDisallowedTools
3858
- };
3859
- messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3860
- logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3861
- return;
3862
- }
3863
- const enhancedMode = {
3864
- permissionMode: messagePermissionMode || "default",
3865
- model: messageModel,
3866
- fallbackModel: messageFallbackModel,
3867
- customSystemPrompt: messageCustomSystemPrompt,
3868
- appendSystemPrompt: messageAppendSystemPrompt,
3869
- allowedTools: messageAllowedTools,
3870
- disallowedTools: messageDisallowedTools
3871
- };
3872
- messageQueue.push(message.content.text, enhancedMode);
3873
- logger.debugLargeJson("User message pushed to queue:", message);
3874
- });
3875
- const cleanup = async () => {
3876
- logger.debug("[START] Received termination signal, cleaning up...");
3877
- try {
3878
- if (session) {
3879
- session.sendSessionDeath();
3880
- await session.flush();
3881
- await session.close();
3882
- }
3883
- stopCaffeinate();
3884
- logger.debug("[START] Cleanup complete, exiting");
3885
- process.exit(0);
3886
- } catch (error) {
3887
- logger.debug("[START] Error during cleanup:", error);
3888
- process.exit(1);
3889
- }
3890
- };
3891
- process.on("SIGTERM", cleanup);
3892
- process.on("SIGINT", cleanup);
3893
- process.on("uncaughtException", (error) => {
3894
- logger.debug("[START] Uncaught exception:", error);
3895
- cleanup();
3896
- });
3897
- process.on("unhandledRejection", (reason) => {
3898
- logger.debug("[START] Unhandled rejection:", reason);
3899
- cleanup();
3900
- });
3901
- await loop({
3902
- path: workingDirectory,
3903
- model: options.model,
3904
- permissionMode: options.permissionMode,
3905
- startingMode: options.startingMode,
3906
- messageQueue,
3907
- api,
3908
- onModeChange: (newMode) => {
3909
- session.sendSessionEvent({ type: "switch", mode: newMode });
3910
- session.updateAgentState((currentState) => ({
3911
- ...currentState,
3912
- controlledByUser: newMode === "local"
3913
- }));
3914
- },
3915
- onSessionReady: (sessionInstance) => {
3916
- },
3917
- mcpServers: {},
3918
- session,
3919
- claudeEnvVars: options.claudeEnvVars,
3920
- claudeArgs: options.claudeArgs
3921
- });
3922
- session.sendSessionDeath();
3923
- logger.debug("Waiting for socket to flush...");
3924
- await session.flush();
3925
- logger.debug("Closing session...");
3926
- await session.close();
3927
- stopCaffeinate();
3928
- logger.debug("Stopped sleep prevention");
3929
- process.exit(0);
3930
- }
3931
-
3932
- function displayQRCode(url) {
3933
- console.log("=".repeat(80));
3934
- console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
3935
- console.log("=".repeat(80));
3936
- qrcode.generate(url, { small: true }, (qr) => {
3937
- for (let l of qr.split("\n")) {
3938
- console.log(" ".repeat(10) + l);
3939
- }
3940
- });
3941
- console.log("=".repeat(80));
3942
- }
3943
-
3944
- function generateWebAuthUrl(publicKey) {
3945
- const publicKeyBase64 = encodeBase64(publicKey, "base64url");
3946
- return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
3947
- }
3948
-
3949
- async function openBrowser(url) {
3950
- try {
3951
- if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
3952
- logger.debug("[browser] Headless environment detected, skipping browser open");
3953
- return false;
3954
- }
3955
- logger.debug(`[browser] Attempting to open URL: ${url}`);
3956
- await open$1(url);
3957
- logger.debug("[browser] Browser opened successfully");
3958
- return true;
3959
- } catch (error) {
3960
- logger.debug("[browser] Failed to open browser:", error);
3961
- return false;
3962
- }
3963
- }
3964
-
3965
- const AuthSelector = ({ onSelect, onCancel }) => {
3966
- const [selectedIndex, setSelectedIndex] = useState(0);
3967
- const options = [
3968
- {
3969
- method: "mobile",
3970
- label: "Mobile App"
3971
- },
3972
- {
3973
- method: "web",
3974
- label: "Web Browser"
3975
- }
3976
- ];
3977
- useInput((input, key) => {
3978
- if (key.upArrow) {
3979
- setSelectedIndex((prev) => Math.max(0, prev - 1));
3980
- } else if (key.downArrow) {
3981
- setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
3982
- } else if (key.return) {
3983
- onSelect(options[selectedIndex].method);
3984
- } else if (key.escape || key.ctrl && input === "c") {
3985
- onCancel();
3986
- } else if (input === "1") {
3987
- setSelectedIndex(0);
3988
- onSelect("mobile");
3989
- } else if (input === "2") {
3990
- setSelectedIndex(1);
3991
- onSelect("web");
3992
- }
3993
- });
3994
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "How would you like to authenticate?")), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, options.map((option, index) => {
3995
- const isSelected = selectedIndex === index;
3996
- return /* @__PURE__ */ React.createElement(Box, { key: option.method, marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "cyan" : "gray" }, isSelected ? "\u203A " : " ", index + 1, ". ", option.label));
3997
- })), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Use arrows or 1-2 to select, Enter to confirm")));
3998
- };
3999
-
4000
- async function doAuth() {
4001
- console.clear();
4002
- const authMethod = await selectAuthenticationMethod();
4003
- if (!authMethod) {
4004
- console.log("\nAuthentication cancelled.\n");
4005
- return null;
3868
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "How would you like to authenticate?")), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, options.map((option, index) => {
3869
+ const isSelected = selectedIndex === index;
3870
+ return /* @__PURE__ */ React.createElement(Box, { key: option.method, marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "cyan" : "gray" }, isSelected ? "\u203A " : " ", index + 1, ". ", option.label));
3871
+ })), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Use arrows or 1-2 to select, Enter to confirm")));
3872
+ };
3873
+
3874
+ async function doAuth() {
3875
+ console.clear();
3876
+ const authMethod = await selectAuthenticationMethod();
3877
+ if (!authMethod) {
3878
+ console.log("\nAuthentication cancelled.\n");
3879
+ return null;
4006
3880
  }
4007
3881
  const secret = new Uint8Array(randomBytes(32));
4008
3882
  const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
@@ -4146,6 +4020,8 @@ async function authAndSetupMachineIfNeeded() {
4146
4020
  }
4147
4021
  const settings = await updateSettings(async (s) => {
4148
4022
  if (!s.machineId) {
4023
+ const newMachineId = randomUUID();
4024
+ logger.debug(`[AUTH] No machine ID found, generating new one: ${newMachineId}; We will not create machine on startup since we don't have api client intialized`);
4149
4025
  return {
4150
4026
  ...s,
4151
4027
  machineId: randomUUID()
@@ -4157,101 +4033,33 @@ async function authAndSetupMachineIfNeeded() {
4157
4033
  return { credentials, machineId: settings.machineId };
4158
4034
  }
4159
4035
 
4160
- function startDaemonControlServer({
4161
- getChildren,
4162
- stopSession,
4163
- spawnSession,
4164
- requestShutdown,
4165
- onHappySessionWebhook
4166
- }) {
4167
- return new Promise((resolve) => {
4168
- const app = fastify({
4169
- logger: false
4170
- // We use our own logger
4171
- });
4172
- app.setValidatorCompiler(validatorCompiler);
4173
- app.setSerializerCompiler(serializerCompiler);
4174
- const typed = app.withTypeProvider();
4175
- typed.post("/session-started", {
4176
- schema: {
4177
- body: z$1.object({
4178
- sessionId: z$1.string(),
4179
- metadata: z$1.any()
4180
- // Metadata type from API
4181
- })
4182
- }
4183
- }, async (request, reply) => {
4184
- const { sessionId, metadata } = request.body;
4185
- logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
4186
- onHappySessionWebhook(sessionId, metadata);
4187
- return { status: "ok" };
4188
- });
4189
- typed.post("/list", async (request, reply) => {
4190
- const children = getChildren();
4191
- logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
4192
- return { children };
4193
- });
4194
- typed.post("/stop-session", {
4195
- schema: {
4196
- body: z$1.object({
4197
- sessionId: z$1.string()
4198
- })
4199
- }
4200
- }, async (request, reply) => {
4201
- const { sessionId } = request.body;
4202
- logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
4203
- const success = stopSession(sessionId);
4204
- return { success };
4205
- });
4206
- typed.post("/spawn-session", {
4207
- schema: {
4208
- body: z$1.object({
4209
- directory: z$1.string(),
4210
- sessionId: z$1.string().optional()
4211
- })
4212
- }
4213
- }, async (request, reply) => {
4214
- const { directory, sessionId } = request.body;
4215
- logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
4216
- const session = await spawnSession(directory, sessionId);
4217
- if (session) {
4218
- return {
4219
- success: true,
4220
- pid: session.pid,
4221
- sessionId: session.happySessionId || "pending"
4222
- };
4223
- } else {
4224
- reply.code(500);
4225
- return { error: "Failed to spawn session" };
4226
- }
4227
- });
4228
- typed.post("/stop", async (request, reply) => {
4229
- logger.debug("[CONTROL SERVER] Stop daemon request received");
4230
- setTimeout(() => {
4231
- logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
4232
- requestShutdown();
4233
- }, 50);
4234
- return { status: "stopping" };
4235
- });
4236
- app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
4237
- if (err) {
4238
- logger.debug("[CONTROL SERVER] Failed to start:", err);
4239
- throw err;
4240
- }
4241
- const port = parseInt(address.split(":").pop());
4242
- logger.debug(`[CONTROL SERVER] Started on port ${port}`);
4243
- resolve({
4244
- port,
4245
- stop: async () => {
4246
- logger.debug("[CONTROL SERVER] Stopping server");
4247
- await app.close();
4248
- logger.debug("[CONTROL SERVER] Server stopped");
4249
- }
4250
- });
4251
- });
4252
- });
4253
- }
4254
-
4036
+ function spawnHappyCLI(args, options = {}) {
4037
+ const projectRoot = projectPath();
4038
+ const entrypoint = join(projectRoot, "dist", "index.mjs");
4039
+ let directory;
4040
+ if ("cwd" in options) {
4041
+ directory = options.cwd;
4042
+ } else {
4043
+ directory = process.cwd();
4044
+ }
4045
+ const fullCommand = `happy ${args.join(" ")}`;
4046
+ logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
4047
+ const nodeArgs = [
4048
+ "--no-warnings",
4049
+ "--no-deprecation",
4050
+ entrypoint,
4051
+ ...args
4052
+ ];
4053
+ return spawn$1("node", nodeArgs, options);
4054
+ }
4055
+
4056
+ const initialMachineMetadata = {
4057
+ host: os.hostname(),
4058
+ platform: os.platform(),
4059
+ happyCliVersion: packageJson.version,
4060
+ homeDir: os.homedir(),
4061
+ happyHomeDir: configuration.happyHomeDir
4062
+ };
4255
4063
  async function startDaemon() {
4256
4064
  logger.debug("[DAEMON RUN] Starting daemon process...");
4257
4065
  logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
@@ -4260,6 +4068,7 @@ async function startDaemon() {
4260
4068
  try {
4261
4069
  process.kill(runningDaemon.pid, 0);
4262
4070
  logger.debug("[DAEMON RUN] Daemon already running");
4071
+ console.log(`Daemon already running (PID: ${runningDaemon.pid})`);
4263
4072
  process.exit(0);
4264
4073
  } catch {
4265
4074
  logger.debug("[DAEMON RUN] Stale state found, cleaning up");
@@ -4314,9 +4123,7 @@ async function startDaemon() {
4314
4123
  "--started-by",
4315
4124
  "daemon"
4316
4125
  ];
4317
- const fullCommand = `${happyBinPath} ${args.join(" ")}`;
4318
- logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
4319
- const happyProcess = spawn$1(happyBinPath, args, {
4126
+ const happyProcess = spawnHappyCLI(args, {
4320
4127
  cwd: directory,
4321
4128
  detached: true,
4322
4129
  // Sessions stay alive when daemon stops
@@ -4421,13 +4228,6 @@ async function startDaemon() {
4421
4228
  };
4422
4229
  await writeDaemonState(fileState);
4423
4230
  logger.debug("[DAEMON RUN] Daemon state written");
4424
- const initialMetadata = {
4425
- host: os$1.hostname(),
4426
- platform: os$1.platform(),
4427
- happyCliVersion: packageJson.version,
4428
- homeDir: os$1.homedir(),
4429
- happyHomeDir: configuration.happyHomeDir
4430
- };
4431
4231
  const initialDaemonState = {
4432
4232
  status: "offline",
4433
4233
  pid: process.pid,
@@ -4435,9 +4235,9 @@ async function startDaemon() {
4435
4235
  startedAt: Date.now()
4436
4236
  };
4437
4237
  const api = new ApiClient(credentials.token, credentials.secret);
4438
- const machine = await api.createOrReturnExistingAsIs({
4238
+ const machine = await api.createMachineOrGetExistingAsIs({
4439
4239
  machineId,
4440
- metadata: initialMetadata,
4240
+ metadata: initialMachineMetadata,
4441
4241
  daemonState: initialDaemonState
4442
4242
  });
4443
4243
  logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
@@ -4502,6 +4302,247 @@ async function startDaemon() {
4502
4302
  }
4503
4303
  }
4504
4304
 
4305
+ async function start(credentials, options = {}) {
4306
+ const workingDirectory = process.cwd();
4307
+ const sessionTag = randomUUID();
4308
+ logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
4309
+ logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
4310
+ if (options.startedBy === "daemon" && options.startingMode === "local") {
4311
+ logger.debug("Daemon spawn requested with local mode - forcing remote mode");
4312
+ options.startingMode = "remote";
4313
+ }
4314
+ const api = new ApiClient(credentials.token, credentials.secret);
4315
+ let state = {};
4316
+ const settings = await readSettings();
4317
+ let machineId = settings?.machineId;
4318
+ if (!machineId) {
4319
+ console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it, using 'unknown' id instead`);
4320
+ machineId = "unknown";
4321
+ }
4322
+ logger.debug(`Using machineId: ${machineId}`);
4323
+ let metadata = {
4324
+ path: workingDirectory,
4325
+ host: os$1.hostname(),
4326
+ version: packageJson.version,
4327
+ os: os$1.platform(),
4328
+ machineId,
4329
+ homeDir: os$1.homedir(),
4330
+ happyHomeDir: configuration.happyHomeDir,
4331
+ startedFromDaemon: options.startedBy === "daemon",
4332
+ hostPid: process.pid,
4333
+ startedBy: options.startedBy || "terminal"
4334
+ };
4335
+ const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
4336
+ logger.debug(`Session created: ${response.id}`);
4337
+ await api.createMachineOrGetExistingAsIs({
4338
+ machineId,
4339
+ metadata: initialMachineMetadata
4340
+ });
4341
+ try {
4342
+ const daemonState = await getDaemonState();
4343
+ if (daemonState?.httpPort) {
4344
+ await notifyDaemonSessionStarted(response.id, metadata);
4345
+ logger.debug(`[START] Reported session ${response.id} to daemon`);
4346
+ }
4347
+ } catch (error) {
4348
+ logger.debug("[START] Failed to report to daemon (may not be running):", error);
4349
+ }
4350
+ extractSDKMetadataAsync(async (sdkMetadata) => {
4351
+ logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
4352
+ try {
4353
+ api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
4354
+ ...currentMetadata,
4355
+ tools: sdkMetadata.tools,
4356
+ slashCommands: sdkMetadata.slashCommands
4357
+ }));
4358
+ logger.debug("[start] Session metadata updated with SDK capabilities");
4359
+ } catch (error) {
4360
+ logger.debug("[start] Failed to update session metadata:", error);
4361
+ }
4362
+ });
4363
+ const session = api.sessionSyncClient(response);
4364
+ const logPath = await logger.logFilePathPromise;
4365
+ logger.infoDeveloper(`Session: ${response.id}`);
4366
+ logger.infoDeveloper(`Logs: ${logPath}`);
4367
+ session.updateAgentState((currentState) => ({
4368
+ ...currentState,
4369
+ controlledByUser: options.startingMode !== "remote"
4370
+ }));
4371
+ const caffeinateStarted = startCaffeinate();
4372
+ if (caffeinateStarted) {
4373
+ logger.infoDeveloper("Sleep prevention enabled (macOS)");
4374
+ }
4375
+ const messageQueue = new MessageQueue2((mode) => hashObject(mode));
4376
+ registerHandlers(session);
4377
+ let currentPermissionMode = options.permissionMode;
4378
+ let currentModel = options.model;
4379
+ let currentFallbackModel = void 0;
4380
+ let currentCustomSystemPrompt = void 0;
4381
+ let currentAppendSystemPrompt = void 0;
4382
+ let currentAllowedTools = void 0;
4383
+ let currentDisallowedTools = void 0;
4384
+ session.onUserMessage((message) => {
4385
+ let messagePermissionMode = currentPermissionMode;
4386
+ if (message.meta?.permissionMode) {
4387
+ const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
4388
+ if (validModes.includes(message.meta.permissionMode)) {
4389
+ messagePermissionMode = message.meta.permissionMode;
4390
+ currentPermissionMode = messagePermissionMode;
4391
+ logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
4392
+ } else {
4393
+ logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
4394
+ }
4395
+ } else {
4396
+ logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
4397
+ }
4398
+ let messageModel = currentModel;
4399
+ if (message.meta?.hasOwnProperty("model")) {
4400
+ messageModel = message.meta.model || void 0;
4401
+ currentModel = messageModel;
4402
+ logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
4403
+ } else {
4404
+ logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
4405
+ }
4406
+ let messageCustomSystemPrompt = currentCustomSystemPrompt;
4407
+ if (message.meta?.hasOwnProperty("customSystemPrompt")) {
4408
+ messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
4409
+ currentCustomSystemPrompt = messageCustomSystemPrompt;
4410
+ logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
4411
+ } else {
4412
+ logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
4413
+ }
4414
+ let messageFallbackModel = currentFallbackModel;
4415
+ if (message.meta?.hasOwnProperty("fallbackModel")) {
4416
+ messageFallbackModel = message.meta.fallbackModel || void 0;
4417
+ currentFallbackModel = messageFallbackModel;
4418
+ logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
4419
+ } else {
4420
+ logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
4421
+ }
4422
+ let messageAppendSystemPrompt = currentAppendSystemPrompt;
4423
+ if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
4424
+ messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
4425
+ currentAppendSystemPrompt = messageAppendSystemPrompt;
4426
+ logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
4427
+ } else {
4428
+ logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
4429
+ }
4430
+ let messageAllowedTools = currentAllowedTools;
4431
+ if (message.meta?.hasOwnProperty("allowedTools")) {
4432
+ messageAllowedTools = message.meta.allowedTools || void 0;
4433
+ currentAllowedTools = messageAllowedTools;
4434
+ logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
4435
+ } else {
4436
+ logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
4437
+ }
4438
+ let messageDisallowedTools = currentDisallowedTools;
4439
+ if (message.meta?.hasOwnProperty("disallowedTools")) {
4440
+ messageDisallowedTools = message.meta.disallowedTools || void 0;
4441
+ currentDisallowedTools = messageDisallowedTools;
4442
+ logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
4443
+ } else {
4444
+ logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
4445
+ }
4446
+ const specialCommand = parseSpecialCommand(message.content.text);
4447
+ if (specialCommand.type === "compact") {
4448
+ logger.debug("[start] Detected /compact command");
4449
+ const enhancedMode2 = {
4450
+ permissionMode: messagePermissionMode || "default",
4451
+ model: messageModel,
4452
+ fallbackModel: messageFallbackModel,
4453
+ customSystemPrompt: messageCustomSystemPrompt,
4454
+ appendSystemPrompt: messageAppendSystemPrompt,
4455
+ allowedTools: messageAllowedTools,
4456
+ disallowedTools: messageDisallowedTools
4457
+ };
4458
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
4459
+ logger.debugLargeJson("[start] /compact command pushed to queue:", message);
4460
+ return;
4461
+ }
4462
+ if (specialCommand.type === "clear") {
4463
+ logger.debug("[start] Detected /clear command");
4464
+ const enhancedMode2 = {
4465
+ permissionMode: messagePermissionMode || "default",
4466
+ model: messageModel,
4467
+ fallbackModel: messageFallbackModel,
4468
+ customSystemPrompt: messageCustomSystemPrompt,
4469
+ appendSystemPrompt: messageAppendSystemPrompt,
4470
+ allowedTools: messageAllowedTools,
4471
+ disallowedTools: messageDisallowedTools
4472
+ };
4473
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
4474
+ logger.debugLargeJson("[start] /compact command pushed to queue:", message);
4475
+ return;
4476
+ }
4477
+ const enhancedMode = {
4478
+ permissionMode: messagePermissionMode || "default",
4479
+ model: messageModel,
4480
+ fallbackModel: messageFallbackModel,
4481
+ customSystemPrompt: messageCustomSystemPrompt,
4482
+ appendSystemPrompt: messageAppendSystemPrompt,
4483
+ allowedTools: messageAllowedTools,
4484
+ disallowedTools: messageDisallowedTools
4485
+ };
4486
+ messageQueue.push(message.content.text, enhancedMode);
4487
+ logger.debugLargeJson("User message pushed to queue:", message);
4488
+ });
4489
+ const cleanup = async () => {
4490
+ logger.debug("[START] Received termination signal, cleaning up...");
4491
+ try {
4492
+ if (session) {
4493
+ session.sendSessionDeath();
4494
+ await session.flush();
4495
+ await session.close();
4496
+ }
4497
+ stopCaffeinate();
4498
+ logger.debug("[START] Cleanup complete, exiting");
4499
+ process.exit(0);
4500
+ } catch (error) {
4501
+ logger.debug("[START] Error during cleanup:", error);
4502
+ process.exit(1);
4503
+ }
4504
+ };
4505
+ process.on("SIGTERM", cleanup);
4506
+ process.on("SIGINT", cleanup);
4507
+ process.on("uncaughtException", (error) => {
4508
+ logger.debug("[START] Uncaught exception:", error);
4509
+ cleanup();
4510
+ });
4511
+ process.on("unhandledRejection", (reason) => {
4512
+ logger.debug("[START] Unhandled rejection:", reason);
4513
+ cleanup();
4514
+ });
4515
+ await loop({
4516
+ path: workingDirectory,
4517
+ model: options.model,
4518
+ permissionMode: options.permissionMode,
4519
+ startingMode: options.startingMode,
4520
+ messageQueue,
4521
+ api,
4522
+ onModeChange: (newMode) => {
4523
+ session.sendSessionEvent({ type: "switch", mode: newMode });
4524
+ session.updateAgentState((currentState) => ({
4525
+ ...currentState,
4526
+ controlledByUser: newMode === "local"
4527
+ }));
4528
+ },
4529
+ onSessionReady: (sessionInstance) => {
4530
+ },
4531
+ mcpServers: {},
4532
+ session,
4533
+ claudeEnvVars: options.claudeEnvVars,
4534
+ claudeArgs: options.claudeArgs
4535
+ });
4536
+ session.sendSessionDeath();
4537
+ logger.debug("Waiting for socket to flush...");
4538
+ await session.flush();
4539
+ logger.debug("Closing session...");
4540
+ await session.close();
4541
+ stopCaffeinate();
4542
+ logger.debug("Stopped sleep prevention");
4543
+ process.exit(0);
4544
+ }
4545
+
4505
4546
  function trimIdent(text) {
4506
4547
  const lines = text.split("\n");
4507
4548
  while (lines.length > 0 && lines[0].trim() === "") {
@@ -4559,10 +4600,10 @@ async function install$1() {
4559
4600
  <true/>
4560
4601
 
4561
4602
  <key>StandardErrorPath</key>
4562
- <string>${os$1.homedir()}/.happy/daemon.err</string>
4603
+ <string>${os.homedir()}/.happy/daemon.err</string>
4563
4604
 
4564
4605
  <key>StandardOutPath</key>
4565
- <string>${os$1.homedir()}/.happy/daemon.log</string>
4606
+ <string>${os.homedir()}/.happy/daemon.log</string>
4566
4607
 
4567
4608
  <key>WorkingDirectory</key>
4568
4609
  <string>/tmp</string>
@@ -4733,7 +4774,7 @@ async function handleAuthLogin(args) {
4733
4774
  if (existingCreds && settings?.machineId) {
4734
4775
  console.log(chalk.green("\u2713 Already authenticated"));
4735
4776
  console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
4736
- console.log(chalk.gray(` Host: ${os.hostname()}`));
4777
+ console.log(chalk.gray(` Host: ${os$1.hostname()}`));
4737
4778
  console.log(chalk.gray(` Use 'happy auth login --force' to re-authenticate`));
4738
4779
  return;
4739
4780
  } else if (existingCreds && !settings?.machineId) {
@@ -4802,7 +4843,7 @@ async function handleAuthShowBackup() {
4802
4843
  console.log("");
4803
4844
  console.log(chalk.cyan("Machine Information:"));
4804
4845
  console.log(` Machine ID: ${settings?.machineId || "not set"}`);
4805
- console.log(` Host: ${os.hostname()}`);
4846
+ console.log(` Host: ${os$1.hostname()}`);
4806
4847
  console.log("");
4807
4848
  console.log(chalk.bold("How to use this backup key:"));
4808
4849
  console.log(chalk.gray("\u2022 In Happy mobile app: Go to restore/link device and enter this key"));
@@ -4827,7 +4868,7 @@ async function handleAuthStatus() {
4827
4868
  if (settings?.machineId) {
4828
4869
  console.log(chalk.green("\u2713 Machine registered"));
4829
4870
  console.log(chalk.gray(` Machine ID: ${settings.machineId}`));
4830
- console.log(chalk.gray(` Host: ${os.hostname()}`));
4871
+ console.log(chalk.gray(` Host: ${os$1.hostname()}`));
4831
4872
  } else {
4832
4873
  console.log(chalk.yellow("\u26A0\uFE0F Machine not registered"));
4833
4874
  console.log(chalk.gray(' Run "happy auth login --force" to fix this'));
@@ -4847,6 +4888,34 @@ async function handleAuthStatus() {
4847
4888
  }
4848
4889
  }
4849
4890
 
4891
+ const DaemonPrompt = ({ onSelect }) => {
4892
+ const [selectedIndex, setSelectedIndex] = useState(0);
4893
+ const options = [
4894
+ { value: true, label: "Yes (recommended)", key: "Y" },
4895
+ { value: false, label: "No", key: "N" }
4896
+ ];
4897
+ useInput((input, key) => {
4898
+ const upperInput = input.toUpperCase();
4899
+ if (key.upArrow || key.leftArrow) {
4900
+ setSelectedIndex(0);
4901
+ } else if (key.downArrow || key.rightArrow) {
4902
+ setSelectedIndex(1);
4903
+ } else if (key.return) {
4904
+ onSelect(options[selectedIndex].value);
4905
+ } else if (upperInput === "Y") {
4906
+ onSelect(true);
4907
+ } else if (upperInput === "N") {
4908
+ onSelect(false);
4909
+ } else if (key.escape || key.ctrl && input === "c") {
4910
+ onSelect(false);
4911
+ }
4912
+ });
4913
+ 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) => {
4914
+ const isSelected = selectedIndex === index;
4915
+ return /* @__PURE__ */ React.createElement(Box, { key: option.key }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "green" : "gray" }, isSelected ? "\u203A " : " ", "[", option.key, "] ", option.label));
4916
+ })), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press Y/N or use arrows + Enter to select")));
4917
+ };
4918
+
4850
4919
  (async () => {
4851
4920
  const args = process.argv.slice(2);
4852
4921
  logger.debug("Starting happy CLI with args: ", process.argv);
@@ -4923,8 +4992,7 @@ async function handleAuthStatus() {
4923
4992
  }
4924
4993
  return;
4925
4994
  } else if (daemonSubcommand === "start") {
4926
- const happyBinPath = join(projectPath(), "bin", "happy.mjs");
4927
- const child = spawn$1(happyBinPath, ["daemon", "start-sync"], {
4995
+ const child = spawnHappyCLI(["daemon", "start-sync"], {
4928
4996
  detached: true,
4929
4997
  stdio: "ignore",
4930
4998
  env: process.env
@@ -5114,36 +5182,35 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
5114
5182
  }
5115
5183
  let settings = await readSettings();
5116
5184
  if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
5117
- console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
5118
- const rl = createInterface({
5119
- input: process.stdin,
5120
- output: process.stdout
5121
- });
5122
- console.log(chalk.cyan("\n\u{1F4F1} Happy can run a background service that allows you to:"));
5123
- console.log(chalk.cyan(" \u2022 Spawn new conversations from your phone"));
5124
- console.log(chalk.cyan(" \u2022 Continue closed conversations remotely"));
5125
- console.log(chalk.cyan(" \u2022 Work with Claude while your computer has internet\n"));
5126
- const answer = await new Promise((resolve) => {
5127
- rl.question(chalk.green("Would you like Happy to start this service automatically? (recommended) [Y/n]: "), resolve);
5185
+ const shouldAutoStart = await new Promise((resolve) => {
5186
+ let hasResolved = false;
5187
+ const onSelect = (autoStart) => {
5188
+ if (!hasResolved) {
5189
+ hasResolved = true;
5190
+ app.unmount();
5191
+ resolve(autoStart);
5192
+ }
5193
+ };
5194
+ const app = render(React.createElement(DaemonPrompt, { onSelect }), {
5195
+ exitOnCtrlC: false,
5196
+ patchConsole: false
5197
+ });
5128
5198
  });
5129
- rl.close();
5130
- const shouldAutoStart = answer.toLowerCase() !== "n";
5131
5199
  settings = await updateSettings((settings2) => ({
5132
5200
  ...settings2,
5133
5201
  daemonAutoStartWhenRunningHappy: shouldAutoStart
5134
5202
  }));
5135
5203
  if (shouldAutoStart) {
5136
- console.log(chalk.green("\u2713 Happy will start the background service automatically"));
5204
+ console.log(chalk.green("\n\u2713 Happy will start the background service automatically"));
5137
5205
  console.log(chalk.gray(" The service will run whenever you use the happy command"));
5138
5206
  } else {
5139
- console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
5207
+ console.log(chalk.yellow("\n You can enable this later by running: happy daemon install"));
5140
5208
  }
5141
5209
  }
5142
5210
  if (settings && settings.daemonAutoStartWhenRunningHappy) {
5143
5211
  logger.debug("Starting Happy background service...");
5144
5212
  if (!await isDaemonRunning()) {
5145
- const happyBinPath = join(projectPath(), "bin", "happy.mjs");
5146
- const daemonProcess = spawn$1(happyBinPath, ["daemon", "start-sync"], {
5213
+ const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
5147
5214
  detached: true,
5148
5215
  stdio: "ignore",
5149
5216
  env: process.env