happy-coder 0.7.1-beta.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var types$1 = require('./types-CzvFvJwf.cjs');
4
+ var types$1 = require('./types-B4GgojGc.cjs');
5
5
  var node_crypto = require('node:crypto');
6
6
  var node_child_process = require('node:child_process');
7
7
  var node_path = require('node:path');
@@ -27,11 +27,11 @@ var z = require('zod');
27
27
  var child_process = require('child_process');
28
28
  var util = require('util');
29
29
  var crypto = require('crypto');
30
- var qrcode = require('qrcode-terminal');
31
- var open = require('open');
32
30
  var fastify = require('fastify');
33
31
  var fastifyTypeProviderZod = require('fastify-type-provider-zod');
34
32
  var os$1 = require('os');
33
+ var qrcode = require('qrcode-terminal');
34
+ var open = require('open');
35
35
  var fs = require('fs');
36
36
 
37
37
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -2364,7 +2364,7 @@ async function loop(opts) {
2364
2364
  }
2365
2365
 
2366
2366
  var name = "happy-coder";
2367
- var version = "0.7.1-beta.3";
2367
+ var version = "0.7.1";
2368
2368
  var description = "Claude Code session sharing CLI";
2369
2369
  var author = "Kirill Dubovitskiy";
2370
2370
  var license = "MIT";
@@ -2449,7 +2449,7 @@ var dependencies = {
2449
2449
  };
2450
2450
  var devDependencies = {
2451
2451
  "@eslint/compat": "^1",
2452
- "@types/node": ">=18",
2452
+ "@types/node": ">=20",
2453
2453
  "cross-env": "^10.0.0",
2454
2454
  eslint: "^9",
2455
2455
  "eslint-config-prettier": "^10",
@@ -2460,9 +2460,10 @@ var devDependencies = {
2460
2460
  typescript: "^5",
2461
2461
  vitest: "^3.2.4"
2462
2462
  };
2463
- var overrides = {
2463
+ var resolutions = {
2464
2464
  "whatwg-url": "14.2.0"
2465
2465
  };
2466
+ var packageManager = "yarn@1.22.22";
2466
2467
  var packageJson = {
2467
2468
  name: name,
2468
2469
  version: version,
@@ -2482,7 +2483,8 @@ var packageJson = {
2482
2483
  scripts: scripts,
2483
2484
  dependencies: dependencies,
2484
2485
  devDependencies: devDependencies,
2485
- overrides: overrides
2486
+ resolutions: resolutions,
2487
+ packageManager: packageManager
2486
2488
  };
2487
2489
 
2488
2490
  function run(args, options) {
@@ -3546,6 +3548,16 @@ async function runDoctorCommand() {
3546
3548
  console.log(`Platform: ${chalk.green(process.platform)} ${process.arch}`);
3547
3549
  console.log(`Node.js Version: ${chalk.green(process.version)}`);
3548
3550
  console.log("");
3551
+ console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
3552
+ const projectRoot = projectPath();
3553
+ const wrapperPath = node_path.join(projectRoot, "bin", "happy.mjs");
3554
+ const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
3555
+ console.log(`Project Root: ${chalk.blue(projectRoot)}`);
3556
+ console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
3557
+ console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
3558
+ console.log(`Wrapper Exists: ${node_fs.existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
3559
+ console.log(`CLI Exists: ${node_fs.existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
3560
+ console.log("");
3549
3561
  console.log(chalk.bold("\u2699\uFE0F Configuration"));
3550
3562
  console.log(`Happy Home: ${chalk.blue(types$1.configuration.happyHomeDir)}`);
3551
3563
  console.log(`Server URL: ${chalk.blue(types$1.configuration.serverUrl)}`);
@@ -3717,313 +3729,175 @@ var controlClient = /*#__PURE__*/Object.freeze({
3717
3729
  stopDaemonSession: stopDaemonSession
3718
3730
  });
3719
3731
 
3720
- async function start(credentials, options = {}) {
3721
- const workingDirectory = process.cwd();
3722
- const sessionTag = node_crypto.randomUUID();
3723
- types$1.logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
3724
- types$1.logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
3725
- if (options.startedBy === "daemon" && options.startingMode === "local") {
3726
- types$1.logger.debug("Daemon spawn requested with local mode - forcing remote mode");
3727
- options.startingMode = "remote";
3728
- }
3729
- const api = new types$1.ApiClient(credentials.token, credentials.secret);
3730
- let state = {};
3731
- const settings = await readSettings();
3732
- const machineId = settings?.machineId || "unknown";
3733
- types$1.logger.debug(`Using machineId: ${machineId}`);
3734
- let metadata = {
3735
- path: workingDirectory,
3736
- host: os.hostname(),
3737
- version: packageJson.version,
3738
- os: os.platform(),
3739
- machineId,
3740
- homeDir: os.homedir(),
3741
- happyHomeDir: types$1.configuration.happyHomeDir,
3742
- startedFromDaemon: options.startedBy === "daemon",
3743
- hostPid: process.pid,
3744
- startedBy: options.startedBy || "terminal"
3745
- };
3746
- const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
3747
- types$1.logger.debug(`Session created: ${response.id}`);
3732
+ function startDaemonControlServer({
3733
+ getChildren,
3734
+ stopSession,
3735
+ spawnSession,
3736
+ requestShutdown,
3737
+ onHappySessionWebhook
3738
+ }) {
3739
+ return new Promise((resolve) => {
3740
+ const app = fastify({
3741
+ logger: false
3742
+ // We use our own logger
3743
+ });
3744
+ app.setValidatorCompiler(fastifyTypeProviderZod.validatorCompiler);
3745
+ app.setSerializerCompiler(fastifyTypeProviderZod.serializerCompiler);
3746
+ const typed = app.withTypeProvider();
3747
+ typed.post("/session-started", {
3748
+ schema: {
3749
+ body: z.z.object({
3750
+ sessionId: z.z.string(),
3751
+ metadata: z.z.any()
3752
+ // Metadata type from API
3753
+ })
3754
+ }
3755
+ }, async (request, reply) => {
3756
+ const { sessionId, metadata } = request.body;
3757
+ types$1.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
3758
+ onHappySessionWebhook(sessionId, metadata);
3759
+ return { status: "ok" };
3760
+ });
3761
+ typed.post("/list", async (request, reply) => {
3762
+ const children = getChildren();
3763
+ types$1.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
3764
+ return { children };
3765
+ });
3766
+ typed.post("/stop-session", {
3767
+ schema: {
3768
+ body: z.z.object({
3769
+ sessionId: z.z.string()
3770
+ })
3771
+ }
3772
+ }, async (request, reply) => {
3773
+ const { sessionId } = request.body;
3774
+ types$1.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
3775
+ const success = stopSession(sessionId);
3776
+ return { success };
3777
+ });
3778
+ typed.post("/spawn-session", {
3779
+ schema: {
3780
+ body: z.z.object({
3781
+ directory: z.z.string(),
3782
+ sessionId: z.z.string().optional()
3783
+ })
3784
+ }
3785
+ }, async (request, reply) => {
3786
+ const { directory, sessionId } = request.body;
3787
+ types$1.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
3788
+ const session = await spawnSession(directory, sessionId);
3789
+ if (session) {
3790
+ return {
3791
+ success: true,
3792
+ pid: session.pid,
3793
+ sessionId: session.happySessionId || "pending"
3794
+ };
3795
+ } else {
3796
+ reply.code(500);
3797
+ return { error: "Failed to spawn session" };
3798
+ }
3799
+ });
3800
+ typed.post("/stop", async (request, reply) => {
3801
+ types$1.logger.debug("[CONTROL SERVER] Stop daemon request received");
3802
+ setTimeout(() => {
3803
+ types$1.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
3804
+ requestShutdown();
3805
+ }, 50);
3806
+ return { status: "stopping" };
3807
+ });
3808
+ app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
3809
+ if (err) {
3810
+ types$1.logger.debug("[CONTROL SERVER] Failed to start:", err);
3811
+ throw err;
3812
+ }
3813
+ const port = parseInt(address.split(":").pop());
3814
+ types$1.logger.debug(`[CONTROL SERVER] Started on port ${port}`);
3815
+ resolve({
3816
+ port,
3817
+ stop: async () => {
3818
+ types$1.logger.debug("[CONTROL SERVER] Stopping server");
3819
+ await app.close();
3820
+ types$1.logger.debug("[CONTROL SERVER] Server stopped");
3821
+ }
3822
+ });
3823
+ });
3824
+ });
3825
+ }
3826
+
3827
+ function displayQRCode(url) {
3828
+ console.log("=".repeat(80));
3829
+ console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
3830
+ console.log("=".repeat(80));
3831
+ qrcode.generate(url, { small: true }, (qr) => {
3832
+ for (let l of qr.split("\n")) {
3833
+ console.log(" ".repeat(10) + l);
3834
+ }
3835
+ });
3836
+ console.log("=".repeat(80));
3837
+ }
3838
+
3839
+ function generateWebAuthUrl(publicKey) {
3840
+ const publicKeyBase64 = types$1.encodeBase64(publicKey, "base64url");
3841
+ return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
3842
+ }
3843
+
3844
+ async function openBrowser(url) {
3748
3845
  try {
3749
- const daemonState = await getDaemonState();
3750
- if (daemonState?.httpPort) {
3751
- await notifyDaemonSessionStarted(response.id, metadata);
3752
- types$1.logger.debug(`[START] Reported session ${response.id} to daemon`);
3846
+ if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
3847
+ types$1.logger.debug("[browser] Headless environment detected, skipping browser open");
3848
+ return false;
3753
3849
  }
3850
+ types$1.logger.debug(`[browser] Attempting to open URL: ${url}`);
3851
+ await open(url);
3852
+ types$1.logger.debug("[browser] Browser opened successfully");
3853
+ return true;
3754
3854
  } catch (error) {
3755
- types$1.logger.debug("[START] Failed to report to daemon (may not be running):", error);
3855
+ types$1.logger.debug("[browser] Failed to open browser:", error);
3856
+ return false;
3756
3857
  }
3757
- extractSDKMetadataAsync(async (sdkMetadata) => {
3758
- types$1.logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
3759
- try {
3760
- api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
3761
- ...currentMetadata,
3762
- tools: sdkMetadata.tools,
3763
- slashCommands: sdkMetadata.slashCommands
3764
- }));
3765
- types$1.logger.debug("[start] Session metadata updated with SDK capabilities");
3766
- } catch (error) {
3767
- types$1.logger.debug("[start] Failed to update session metadata:", error);
3858
+ }
3859
+
3860
+ const AuthSelector = ({ onSelect, onCancel }) => {
3861
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
3862
+ const options = [
3863
+ {
3864
+ method: "mobile",
3865
+ label: "Mobile App"
3866
+ },
3867
+ {
3868
+ method: "web",
3869
+ label: "Web Browser"
3870
+ }
3871
+ ];
3872
+ ink.useInput((input, key) => {
3873
+ if (key.upArrow) {
3874
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
3875
+ } else if (key.downArrow) {
3876
+ setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
3877
+ } else if (key.return) {
3878
+ onSelect(options[selectedIndex].method);
3879
+ } else if (key.escape || key.ctrl && input === "c") {
3880
+ onCancel();
3881
+ } else if (input === "1") {
3882
+ setSelectedIndex(0);
3883
+ onSelect("mobile");
3884
+ } else if (input === "2") {
3885
+ setSelectedIndex(1);
3886
+ onSelect("web");
3768
3887
  }
3769
3888
  });
3770
- const session = api.sessionSyncClient(response);
3771
- const logPath = await types$1.logger.logFilePathPromise;
3772
- types$1.logger.infoDeveloper(`Session: ${response.id}`);
3773
- types$1.logger.infoDeveloper(`Logs: ${logPath}`);
3774
- session.updateAgentState((currentState) => ({
3775
- ...currentState,
3776
- controlledByUser: options.startingMode !== "remote"
3777
- }));
3778
- const caffeinateStarted = startCaffeinate();
3779
- if (caffeinateStarted) {
3780
- types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
3781
- }
3782
- const messageQueue = new MessageQueue2((mode) => hashObject(mode));
3783
- registerHandlers(session);
3784
- let currentPermissionMode = options.permissionMode;
3785
- let currentModel = options.model;
3786
- let currentFallbackModel = void 0;
3787
- let currentCustomSystemPrompt = void 0;
3788
- let currentAppendSystemPrompt = void 0;
3789
- let currentAllowedTools = void 0;
3790
- let currentDisallowedTools = void 0;
3791
- session.onUserMessage((message) => {
3792
- let messagePermissionMode = currentPermissionMode;
3793
- if (message.meta?.permissionMode) {
3794
- const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
3795
- if (validModes.includes(message.meta.permissionMode)) {
3796
- messagePermissionMode = message.meta.permissionMode;
3797
- currentPermissionMode = messagePermissionMode;
3798
- types$1.logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
3799
- } else {
3800
- types$1.logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
3801
- }
3802
- } else {
3803
- types$1.logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
3804
- }
3805
- let messageModel = currentModel;
3806
- if (message.meta?.hasOwnProperty("model")) {
3807
- messageModel = message.meta.model || void 0;
3808
- currentModel = messageModel;
3809
- types$1.logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
3810
- } else {
3811
- types$1.logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
3812
- }
3813
- let messageCustomSystemPrompt = currentCustomSystemPrompt;
3814
- if (message.meta?.hasOwnProperty("customSystemPrompt")) {
3815
- messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
3816
- currentCustomSystemPrompt = messageCustomSystemPrompt;
3817
- types$1.logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
3818
- } else {
3819
- types$1.logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
3820
- }
3821
- let messageFallbackModel = currentFallbackModel;
3822
- if (message.meta?.hasOwnProperty("fallbackModel")) {
3823
- messageFallbackModel = message.meta.fallbackModel || void 0;
3824
- currentFallbackModel = messageFallbackModel;
3825
- types$1.logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
3826
- } else {
3827
- types$1.logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
3828
- }
3829
- let messageAppendSystemPrompt = currentAppendSystemPrompt;
3830
- if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
3831
- messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
3832
- currentAppendSystemPrompt = messageAppendSystemPrompt;
3833
- types$1.logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
3834
- } else {
3835
- types$1.logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
3836
- }
3837
- let messageAllowedTools = currentAllowedTools;
3838
- if (message.meta?.hasOwnProperty("allowedTools")) {
3839
- messageAllowedTools = message.meta.allowedTools || void 0;
3840
- currentAllowedTools = messageAllowedTools;
3841
- types$1.logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
3842
- } else {
3843
- types$1.logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
3844
- }
3845
- let messageDisallowedTools = currentDisallowedTools;
3846
- if (message.meta?.hasOwnProperty("disallowedTools")) {
3847
- messageDisallowedTools = message.meta.disallowedTools || void 0;
3848
- currentDisallowedTools = messageDisallowedTools;
3849
- types$1.logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
3850
- } else {
3851
- types$1.logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
3852
- }
3853
- const specialCommand = parseSpecialCommand(message.content.text);
3854
- if (specialCommand.type === "compact") {
3855
- types$1.logger.debug("[start] Detected /compact command");
3856
- const enhancedMode2 = {
3857
- permissionMode: messagePermissionMode || "default",
3858
- model: messageModel,
3859
- fallbackModel: messageFallbackModel,
3860
- customSystemPrompt: messageCustomSystemPrompt,
3861
- appendSystemPrompt: messageAppendSystemPrompt,
3862
- allowedTools: messageAllowedTools,
3863
- disallowedTools: messageDisallowedTools
3864
- };
3865
- messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3866
- types$1.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3867
- return;
3868
- }
3869
- if (specialCommand.type === "clear") {
3870
- types$1.logger.debug("[start] Detected /clear command");
3871
- const enhancedMode2 = {
3872
- permissionMode: messagePermissionMode || "default",
3873
- model: messageModel,
3874
- fallbackModel: messageFallbackModel,
3875
- customSystemPrompt: messageCustomSystemPrompt,
3876
- appendSystemPrompt: messageAppendSystemPrompt,
3877
- allowedTools: messageAllowedTools,
3878
- disallowedTools: messageDisallowedTools
3879
- };
3880
- messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3881
- types$1.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3882
- return;
3883
- }
3884
- const enhancedMode = {
3885
- permissionMode: messagePermissionMode || "default",
3886
- model: messageModel,
3887
- fallbackModel: messageFallbackModel,
3888
- customSystemPrompt: messageCustomSystemPrompt,
3889
- appendSystemPrompt: messageAppendSystemPrompt,
3890
- allowedTools: messageAllowedTools,
3891
- disallowedTools: messageDisallowedTools
3892
- };
3893
- messageQueue.push(message.content.text, enhancedMode);
3894
- types$1.logger.debugLargeJson("User message pushed to queue:", message);
3895
- });
3896
- const cleanup = async () => {
3897
- types$1.logger.debug("[START] Received termination signal, cleaning up...");
3898
- try {
3899
- if (session) {
3900
- session.sendSessionDeath();
3901
- await session.flush();
3902
- await session.close();
3903
- }
3904
- stopCaffeinate();
3905
- types$1.logger.debug("[START] Cleanup complete, exiting");
3906
- process.exit(0);
3907
- } catch (error) {
3908
- types$1.logger.debug("[START] Error during cleanup:", error);
3909
- process.exit(1);
3910
- }
3911
- };
3912
- process.on("SIGTERM", cleanup);
3913
- process.on("SIGINT", cleanup);
3914
- process.on("uncaughtException", (error) => {
3915
- types$1.logger.debug("[START] Uncaught exception:", error);
3916
- cleanup();
3917
- });
3918
- process.on("unhandledRejection", (reason) => {
3919
- types$1.logger.debug("[START] Unhandled rejection:", reason);
3920
- cleanup();
3921
- });
3922
- await loop({
3923
- path: workingDirectory,
3924
- model: options.model,
3925
- permissionMode: options.permissionMode,
3926
- startingMode: options.startingMode,
3927
- messageQueue,
3928
- api,
3929
- onModeChange: (newMode) => {
3930
- session.sendSessionEvent({ type: "switch", mode: newMode });
3931
- session.updateAgentState((currentState) => ({
3932
- ...currentState,
3933
- controlledByUser: newMode === "local"
3934
- }));
3935
- },
3936
- onSessionReady: (sessionInstance) => {
3937
- },
3938
- mcpServers: {},
3939
- session,
3940
- claudeEnvVars: options.claudeEnvVars,
3941
- claudeArgs: options.claudeArgs
3942
- });
3943
- session.sendSessionDeath();
3944
- types$1.logger.debug("Waiting for socket to flush...");
3945
- await session.flush();
3946
- types$1.logger.debug("Closing session...");
3947
- await session.close();
3948
- stopCaffeinate();
3949
- types$1.logger.debug("Stopped sleep prevention");
3950
- process.exit(0);
3951
- }
3952
-
3953
- function displayQRCode(url) {
3954
- console.log("=".repeat(80));
3955
- console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
3956
- console.log("=".repeat(80));
3957
- qrcode.generate(url, { small: true }, (qr) => {
3958
- for (let l of qr.split("\n")) {
3959
- console.log(" ".repeat(10) + l);
3960
- }
3961
- });
3962
- console.log("=".repeat(80));
3963
- }
3964
-
3965
- function generateWebAuthUrl(publicKey) {
3966
- const publicKeyBase64 = types$1.encodeBase64(publicKey, "base64url");
3967
- return `https://app.happy.engineering/terminal/connect#key=${publicKeyBase64}`;
3968
- }
3969
-
3970
- async function openBrowser(url) {
3971
- try {
3972
- if (!process.stdout.isTTY || process.env.CI || process.env.HEADLESS) {
3973
- types$1.logger.debug("[browser] Headless environment detected, skipping browser open");
3974
- return false;
3975
- }
3976
- types$1.logger.debug(`[browser] Attempting to open URL: ${url}`);
3977
- await open(url);
3978
- types$1.logger.debug("[browser] Browser opened successfully");
3979
- return true;
3980
- } catch (error) {
3981
- types$1.logger.debug("[browser] Failed to open browser:", error);
3982
- return false;
3983
- }
3984
- }
3985
-
3986
- const AuthSelector = ({ onSelect, onCancel }) => {
3987
- const [selectedIndex, setSelectedIndex] = React.useState(0);
3988
- const options = [
3989
- {
3990
- method: "mobile",
3991
- label: "Mobile App"
3992
- },
3993
- {
3994
- method: "web",
3995
- label: "Web Browser"
3996
- }
3997
- ];
3998
- ink.useInput((input, key) => {
3999
- if (key.upArrow) {
4000
- setSelectedIndex((prev) => Math.max(0, prev - 1));
4001
- } else if (key.downArrow) {
4002
- setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
4003
- } else if (key.return) {
4004
- onSelect(options[selectedIndex].method);
4005
- } else if (key.escape || key.ctrl && input === "c") {
4006
- onCancel();
4007
- } else if (input === "1") {
4008
- setSelectedIndex(0);
4009
- onSelect("mobile");
4010
- } else if (input === "2") {
4011
- setSelectedIndex(1);
4012
- onSelect("web");
4013
- }
4014
- });
4015
- return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React.createElement(ink.Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, null, "How would you like to authenticate?")), /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, options.map((option, index) => {
4016
- const isSelected = selectedIndex === index;
4017
- return /* @__PURE__ */ React.createElement(ink.Box, { key: option.method, marginY: 0 }, /* @__PURE__ */ React.createElement(ink.Text, { color: isSelected ? "cyan" : "gray" }, isSelected ? "\u203A " : " ", index + 1, ". ", option.label));
4018
- })), /* @__PURE__ */ React.createElement(ink.Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { dimColor: true }, "Use arrows or 1-2 to select, Enter to confirm")));
4019
- };
4020
-
4021
- async function doAuth() {
4022
- console.clear();
4023
- const authMethod = await selectAuthenticationMethod();
4024
- if (!authMethod) {
4025
- console.log("\nAuthentication cancelled.\n");
4026
- return null;
3889
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", paddingY: 1 }, /* @__PURE__ */ React.createElement(ink.Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, null, "How would you like to authenticate?")), /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, options.map((option, index) => {
3890
+ const isSelected = selectedIndex === index;
3891
+ return /* @__PURE__ */ React.createElement(ink.Box, { key: option.method, marginY: 0 }, /* @__PURE__ */ React.createElement(ink.Text, { color: isSelected ? "cyan" : "gray" }, isSelected ? "\u203A " : " ", index + 1, ". ", option.label));
3892
+ })), /* @__PURE__ */ React.createElement(ink.Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { dimColor: true }, "Use arrows or 1-2 to select, Enter to confirm")));
3893
+ };
3894
+
3895
+ async function doAuth() {
3896
+ console.clear();
3897
+ const authMethod = await selectAuthenticationMethod();
3898
+ if (!authMethod) {
3899
+ console.log("\nAuthentication cancelled.\n");
3900
+ return null;
4027
3901
  }
4028
3902
  const secret = new Uint8Array(node_crypto.randomBytes(32));
4029
3903
  const keypair = tweetnacl.box.keyPair.fromSecretKey(secret);
@@ -4167,6 +4041,8 @@ async function authAndSetupMachineIfNeeded() {
4167
4041
  }
4168
4042
  const settings = await updateSettings(async (s) => {
4169
4043
  if (!s.machineId) {
4044
+ const newMachineId = node_crypto.randomUUID();
4045
+ console.log(`[AUTH] No machine ID found, generating new one: ${newMachineId}; We will not create machine on startup since we don't have api client intialized`);
4170
4046
  return {
4171
4047
  ...s,
4172
4048
  machineId: node_crypto.randomUUID()
@@ -4178,101 +4054,33 @@ async function authAndSetupMachineIfNeeded() {
4178
4054
  return { credentials, machineId: settings.machineId };
4179
4055
  }
4180
4056
 
4181
- function startDaemonControlServer({
4182
- getChildren,
4183
- stopSession,
4184
- spawnSession,
4185
- requestShutdown,
4186
- onHappySessionWebhook
4187
- }) {
4188
- return new Promise((resolve) => {
4189
- const app = fastify({
4190
- logger: false
4191
- // We use our own logger
4192
- });
4193
- app.setValidatorCompiler(fastifyTypeProviderZod.validatorCompiler);
4194
- app.setSerializerCompiler(fastifyTypeProviderZod.serializerCompiler);
4195
- const typed = app.withTypeProvider();
4196
- typed.post("/session-started", {
4197
- schema: {
4198
- body: z.z.object({
4199
- sessionId: z.z.string(),
4200
- metadata: z.z.any()
4201
- // Metadata type from API
4202
- })
4203
- }
4204
- }, async (request, reply) => {
4205
- const { sessionId, metadata } = request.body;
4206
- types$1.logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
4207
- onHappySessionWebhook(sessionId, metadata);
4208
- return { status: "ok" };
4209
- });
4210
- typed.post("/list", async (request, reply) => {
4211
- const children = getChildren();
4212
- types$1.logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
4213
- return { children };
4214
- });
4215
- typed.post("/stop-session", {
4216
- schema: {
4217
- body: z.z.object({
4218
- sessionId: z.z.string()
4219
- })
4220
- }
4221
- }, async (request, reply) => {
4222
- const { sessionId } = request.body;
4223
- types$1.logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
4224
- const success = stopSession(sessionId);
4225
- return { success };
4226
- });
4227
- typed.post("/spawn-session", {
4228
- schema: {
4229
- body: z.z.object({
4230
- directory: z.z.string(),
4231
- sessionId: z.z.string().optional()
4232
- })
4233
- }
4234
- }, async (request, reply) => {
4235
- const { directory, sessionId } = request.body;
4236
- types$1.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
4237
- const session = await spawnSession(directory, sessionId);
4238
- if (session) {
4239
- return {
4240
- success: true,
4241
- pid: session.pid,
4242
- sessionId: session.happySessionId || "pending"
4243
- };
4244
- } else {
4245
- reply.code(500);
4246
- return { error: "Failed to spawn session" };
4247
- }
4248
- });
4249
- typed.post("/stop", async (request, reply) => {
4250
- types$1.logger.debug("[CONTROL SERVER] Stop daemon request received");
4251
- setTimeout(() => {
4252
- types$1.logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
4253
- requestShutdown();
4254
- }, 50);
4255
- return { status: "stopping" };
4256
- });
4257
- app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
4258
- if (err) {
4259
- types$1.logger.debug("[CONTROL SERVER] Failed to start:", err);
4260
- throw err;
4261
- }
4262
- const port = parseInt(address.split(":").pop());
4263
- types$1.logger.debug(`[CONTROL SERVER] Started on port ${port}`);
4264
- resolve({
4265
- port,
4266
- stop: async () => {
4267
- types$1.logger.debug("[CONTROL SERVER] Stopping server");
4268
- await app.close();
4269
- types$1.logger.debug("[CONTROL SERVER] Server stopped");
4270
- }
4271
- });
4272
- });
4273
- });
4057
+ function spawnHappyCLI(args, options = {}) {
4058
+ const projectRoot = projectPath();
4059
+ const entrypoint = node_path.join(projectRoot, "dist", "index.mjs");
4060
+ let directory;
4061
+ if ("cwd" in options) {
4062
+ directory = options.cwd;
4063
+ } else {
4064
+ directory = process.cwd();
4065
+ }
4066
+ const fullCommand = `happy ${args.join(" ")}`;
4067
+ types$1.logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
4068
+ const nodeArgs = [
4069
+ "--no-warnings",
4070
+ "--no-deprecation",
4071
+ entrypoint,
4072
+ ...args
4073
+ ];
4074
+ return child_process.spawn("node", nodeArgs, options);
4274
4075
  }
4275
4076
 
4077
+ const initialMachineMetadata = {
4078
+ host: os$1.hostname(),
4079
+ platform: os$1.platform(),
4080
+ happyCliVersion: packageJson.version,
4081
+ homeDir: os$1.homedir(),
4082
+ happyHomeDir: types$1.configuration.happyHomeDir
4083
+ };
4276
4084
  async function startDaemon() {
4277
4085
  types$1.logger.debug("[DAEMON RUN] Starting daemon process...");
4278
4086
  types$1.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
@@ -4281,6 +4089,7 @@ async function startDaemon() {
4281
4089
  try {
4282
4090
  process.kill(runningDaemon.pid, 0);
4283
4091
  types$1.logger.debug("[DAEMON RUN] Daemon already running");
4092
+ console.log(`Daemon already running (PID: ${runningDaemon.pid})`);
4284
4093
  process.exit(0);
4285
4094
  } catch {
4286
4095
  types$1.logger.debug("[DAEMON RUN] Stale state found, cleaning up");
@@ -4335,9 +4144,7 @@ async function startDaemon() {
4335
4144
  "--started-by",
4336
4145
  "daemon"
4337
4146
  ];
4338
- const fullCommand = `${happyBinPath} ${args.join(" ")}`;
4339
- types$1.logger.debug(`[DAEMON RUN] Spawning: ${fullCommand} in ${directory}`);
4340
- const happyProcess = child_process.spawn(happyBinPath, args, {
4147
+ const happyProcess = spawnHappyCLI(args, {
4341
4148
  cwd: directory,
4342
4149
  detached: true,
4343
4150
  // Sessions stay alive when daemon stops
@@ -4442,13 +4249,6 @@ async function startDaemon() {
4442
4249
  };
4443
4250
  await writeDaemonState(fileState);
4444
4251
  types$1.logger.debug("[DAEMON RUN] Daemon state written");
4445
- const initialMetadata = {
4446
- host: os$1.hostname(),
4447
- platform: os$1.platform(),
4448
- happyCliVersion: packageJson.version,
4449
- homeDir: os$1.homedir(),
4450
- happyHomeDir: types$1.configuration.happyHomeDir
4451
- };
4452
4252
  const initialDaemonState = {
4453
4253
  status: "offline",
4454
4254
  pid: process.pid,
@@ -4456,9 +4256,9 @@ async function startDaemon() {
4456
4256
  startedAt: Date.now()
4457
4257
  };
4458
4258
  const api = new types$1.ApiClient(credentials.token, credentials.secret);
4459
- const machine = await api.createOrReturnExistingAsIs({
4259
+ const machine = await api.createMachineOrGetExistingAsIs({
4460
4260
  machineId,
4461
- metadata: initialMetadata,
4261
+ metadata: initialMachineMetadata,
4462
4262
  daemonState: initialDaemonState
4463
4263
  });
4464
4264
  types$1.logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
@@ -4523,6 +4323,247 @@ async function startDaemon() {
4523
4323
  }
4524
4324
  }
4525
4325
 
4326
+ async function start(credentials, options = {}) {
4327
+ const workingDirectory = process.cwd();
4328
+ const sessionTag = node_crypto.randomUUID();
4329
+ types$1.logger.debugLargeJson("[START] Happy process started", getEnvironmentInfo());
4330
+ types$1.logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
4331
+ if (options.startedBy === "daemon" && options.startingMode === "local") {
4332
+ types$1.logger.debug("Daemon spawn requested with local mode - forcing remote mode");
4333
+ options.startingMode = "remote";
4334
+ }
4335
+ const api = new types$1.ApiClient(credentials.token, credentials.secret);
4336
+ let state = {};
4337
+ const settings = await readSettings();
4338
+ let machineId = settings?.machineId;
4339
+ if (!machineId) {
4340
+ console.error(`[START] No machine ID found in settings, which is unexepcted since authAndSetupMachineIfNeeded should have created it, using 'unknown' id instead`);
4341
+ machineId = "unknown";
4342
+ }
4343
+ types$1.logger.debug(`Using machineId: ${machineId}`);
4344
+ let metadata = {
4345
+ path: workingDirectory,
4346
+ host: os.hostname(),
4347
+ version: packageJson.version,
4348
+ os: os.platform(),
4349
+ machineId,
4350
+ homeDir: os.homedir(),
4351
+ happyHomeDir: types$1.configuration.happyHomeDir,
4352
+ startedFromDaemon: options.startedBy === "daemon",
4353
+ hostPid: process.pid,
4354
+ startedBy: options.startedBy || "terminal"
4355
+ };
4356
+ const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
4357
+ types$1.logger.debug(`Session created: ${response.id}`);
4358
+ await api.createMachineOrGetExistingAsIs({
4359
+ machineId,
4360
+ metadata: initialMachineMetadata
4361
+ });
4362
+ try {
4363
+ const daemonState = await getDaemonState();
4364
+ if (daemonState?.httpPort) {
4365
+ await notifyDaemonSessionStarted(response.id, metadata);
4366
+ types$1.logger.debug(`[START] Reported session ${response.id} to daemon`);
4367
+ }
4368
+ } catch (error) {
4369
+ types$1.logger.debug("[START] Failed to report to daemon (may not be running):", error);
4370
+ }
4371
+ extractSDKMetadataAsync(async (sdkMetadata) => {
4372
+ types$1.logger.debug("[start] SDK metadata extracted, updating session:", sdkMetadata);
4373
+ try {
4374
+ api.sessionSyncClient(response).updateMetadata((currentMetadata) => ({
4375
+ ...currentMetadata,
4376
+ tools: sdkMetadata.tools,
4377
+ slashCommands: sdkMetadata.slashCommands
4378
+ }));
4379
+ types$1.logger.debug("[start] Session metadata updated with SDK capabilities");
4380
+ } catch (error) {
4381
+ types$1.logger.debug("[start] Failed to update session metadata:", error);
4382
+ }
4383
+ });
4384
+ const session = api.sessionSyncClient(response);
4385
+ const logPath = await types$1.logger.logFilePathPromise;
4386
+ types$1.logger.infoDeveloper(`Session: ${response.id}`);
4387
+ types$1.logger.infoDeveloper(`Logs: ${logPath}`);
4388
+ session.updateAgentState((currentState) => ({
4389
+ ...currentState,
4390
+ controlledByUser: options.startingMode !== "remote"
4391
+ }));
4392
+ const caffeinateStarted = startCaffeinate();
4393
+ if (caffeinateStarted) {
4394
+ types$1.logger.infoDeveloper("Sleep prevention enabled (macOS)");
4395
+ }
4396
+ const messageQueue = new MessageQueue2((mode) => hashObject(mode));
4397
+ registerHandlers(session);
4398
+ let currentPermissionMode = options.permissionMode;
4399
+ let currentModel = options.model;
4400
+ let currentFallbackModel = void 0;
4401
+ let currentCustomSystemPrompt = void 0;
4402
+ let currentAppendSystemPrompt = void 0;
4403
+ let currentAllowedTools = void 0;
4404
+ let currentDisallowedTools = void 0;
4405
+ session.onUserMessage((message) => {
4406
+ let messagePermissionMode = currentPermissionMode;
4407
+ if (message.meta?.permissionMode) {
4408
+ const validModes = ["default", "acceptEdits", "bypassPermissions", "plan"];
4409
+ if (validModes.includes(message.meta.permissionMode)) {
4410
+ messagePermissionMode = message.meta.permissionMode;
4411
+ currentPermissionMode = messagePermissionMode;
4412
+ types$1.logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
4413
+ } else {
4414
+ types$1.logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
4415
+ }
4416
+ } else {
4417
+ types$1.logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
4418
+ }
4419
+ let messageModel = currentModel;
4420
+ if (message.meta?.hasOwnProperty("model")) {
4421
+ messageModel = message.meta.model || void 0;
4422
+ currentModel = messageModel;
4423
+ types$1.logger.debug(`[loop] Model updated from user message: ${messageModel || "reset to default"}`);
4424
+ } else {
4425
+ types$1.logger.debug(`[loop] User message received with no model override, using current: ${currentModel || "default"}`);
4426
+ }
4427
+ let messageCustomSystemPrompt = currentCustomSystemPrompt;
4428
+ if (message.meta?.hasOwnProperty("customSystemPrompt")) {
4429
+ messageCustomSystemPrompt = message.meta.customSystemPrompt || void 0;
4430
+ currentCustomSystemPrompt = messageCustomSystemPrompt;
4431
+ types$1.logger.debug(`[loop] Custom system prompt updated from user message: ${messageCustomSystemPrompt ? "set" : "reset to none"}`);
4432
+ } else {
4433
+ types$1.logger.debug(`[loop] User message received with no custom system prompt override, using current: ${currentCustomSystemPrompt ? "set" : "none"}`);
4434
+ }
4435
+ let messageFallbackModel = currentFallbackModel;
4436
+ if (message.meta?.hasOwnProperty("fallbackModel")) {
4437
+ messageFallbackModel = message.meta.fallbackModel || void 0;
4438
+ currentFallbackModel = messageFallbackModel;
4439
+ types$1.logger.debug(`[loop] Fallback model updated from user message: ${messageFallbackModel || "reset to none"}`);
4440
+ } else {
4441
+ types$1.logger.debug(`[loop] User message received with no fallback model override, using current: ${currentFallbackModel || "none"}`);
4442
+ }
4443
+ let messageAppendSystemPrompt = currentAppendSystemPrompt;
4444
+ if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
4445
+ messageAppendSystemPrompt = message.meta.appendSystemPrompt || void 0;
4446
+ currentAppendSystemPrompt = messageAppendSystemPrompt;
4447
+ types$1.logger.debug(`[loop] Append system prompt updated from user message: ${messageAppendSystemPrompt ? "set" : "reset to none"}`);
4448
+ } else {
4449
+ types$1.logger.debug(`[loop] User message received with no append system prompt override, using current: ${currentAppendSystemPrompt ? "set" : "none"}`);
4450
+ }
4451
+ let messageAllowedTools = currentAllowedTools;
4452
+ if (message.meta?.hasOwnProperty("allowedTools")) {
4453
+ messageAllowedTools = message.meta.allowedTools || void 0;
4454
+ currentAllowedTools = messageAllowedTools;
4455
+ types$1.logger.debug(`[loop] Allowed tools updated from user message: ${messageAllowedTools ? messageAllowedTools.join(", ") : "reset to none"}`);
4456
+ } else {
4457
+ types$1.logger.debug(`[loop] User message received with no allowed tools override, using current: ${currentAllowedTools ? currentAllowedTools.join(", ") : "none"}`);
4458
+ }
4459
+ let messageDisallowedTools = currentDisallowedTools;
4460
+ if (message.meta?.hasOwnProperty("disallowedTools")) {
4461
+ messageDisallowedTools = message.meta.disallowedTools || void 0;
4462
+ currentDisallowedTools = messageDisallowedTools;
4463
+ types$1.logger.debug(`[loop] Disallowed tools updated from user message: ${messageDisallowedTools ? messageDisallowedTools.join(", ") : "reset to none"}`);
4464
+ } else {
4465
+ types$1.logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
4466
+ }
4467
+ const specialCommand = parseSpecialCommand(message.content.text);
4468
+ if (specialCommand.type === "compact") {
4469
+ types$1.logger.debug("[start] Detected /compact command");
4470
+ const enhancedMode2 = {
4471
+ permissionMode: messagePermissionMode || "default",
4472
+ model: messageModel,
4473
+ fallbackModel: messageFallbackModel,
4474
+ customSystemPrompt: messageCustomSystemPrompt,
4475
+ appendSystemPrompt: messageAppendSystemPrompt,
4476
+ allowedTools: messageAllowedTools,
4477
+ disallowedTools: messageDisallowedTools
4478
+ };
4479
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
4480
+ types$1.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
4481
+ return;
4482
+ }
4483
+ if (specialCommand.type === "clear") {
4484
+ types$1.logger.debug("[start] Detected /clear command");
4485
+ const enhancedMode2 = {
4486
+ permissionMode: messagePermissionMode || "default",
4487
+ model: messageModel,
4488
+ fallbackModel: messageFallbackModel,
4489
+ customSystemPrompt: messageCustomSystemPrompt,
4490
+ appendSystemPrompt: messageAppendSystemPrompt,
4491
+ allowedTools: messageAllowedTools,
4492
+ disallowedTools: messageDisallowedTools
4493
+ };
4494
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
4495
+ types$1.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
4496
+ return;
4497
+ }
4498
+ const enhancedMode = {
4499
+ permissionMode: messagePermissionMode || "default",
4500
+ model: messageModel,
4501
+ fallbackModel: messageFallbackModel,
4502
+ customSystemPrompt: messageCustomSystemPrompt,
4503
+ appendSystemPrompt: messageAppendSystemPrompt,
4504
+ allowedTools: messageAllowedTools,
4505
+ disallowedTools: messageDisallowedTools
4506
+ };
4507
+ messageQueue.push(message.content.text, enhancedMode);
4508
+ types$1.logger.debugLargeJson("User message pushed to queue:", message);
4509
+ });
4510
+ const cleanup = async () => {
4511
+ types$1.logger.debug("[START] Received termination signal, cleaning up...");
4512
+ try {
4513
+ if (session) {
4514
+ session.sendSessionDeath();
4515
+ await session.flush();
4516
+ await session.close();
4517
+ }
4518
+ stopCaffeinate();
4519
+ types$1.logger.debug("[START] Cleanup complete, exiting");
4520
+ process.exit(0);
4521
+ } catch (error) {
4522
+ types$1.logger.debug("[START] Error during cleanup:", error);
4523
+ process.exit(1);
4524
+ }
4525
+ };
4526
+ process.on("SIGTERM", cleanup);
4527
+ process.on("SIGINT", cleanup);
4528
+ process.on("uncaughtException", (error) => {
4529
+ types$1.logger.debug("[START] Uncaught exception:", error);
4530
+ cleanup();
4531
+ });
4532
+ process.on("unhandledRejection", (reason) => {
4533
+ types$1.logger.debug("[START] Unhandled rejection:", reason);
4534
+ cleanup();
4535
+ });
4536
+ await loop({
4537
+ path: workingDirectory,
4538
+ model: options.model,
4539
+ permissionMode: options.permissionMode,
4540
+ startingMode: options.startingMode,
4541
+ messageQueue,
4542
+ api,
4543
+ onModeChange: (newMode) => {
4544
+ session.sendSessionEvent({ type: "switch", mode: newMode });
4545
+ session.updateAgentState((currentState) => ({
4546
+ ...currentState,
4547
+ controlledByUser: newMode === "local"
4548
+ }));
4549
+ },
4550
+ onSessionReady: (sessionInstance) => {
4551
+ },
4552
+ mcpServers: {},
4553
+ session,
4554
+ claudeEnvVars: options.claudeEnvVars,
4555
+ claudeArgs: options.claudeArgs
4556
+ });
4557
+ session.sendSessionDeath();
4558
+ types$1.logger.debug("Waiting for socket to flush...");
4559
+ await session.flush();
4560
+ types$1.logger.debug("Closing session...");
4561
+ await session.close();
4562
+ stopCaffeinate();
4563
+ types$1.logger.debug("Stopped sleep prevention");
4564
+ process.exit(0);
4565
+ }
4566
+
4526
4567
  function trimIdent(text) {
4527
4568
  const lines = text.split("\n");
4528
4569
  while (lines.length > 0 && lines[0].trim() === "") {
@@ -4868,6 +4909,34 @@ async function handleAuthStatus() {
4868
4909
  }
4869
4910
  }
4870
4911
 
4912
+ const DaemonPrompt = ({ onSelect }) => {
4913
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
4914
+ const options = [
4915
+ { value: true, label: "Yes (recommended)", key: "Y" },
4916
+ { value: false, label: "No", key: "N" }
4917
+ ];
4918
+ ink.useInput((input, key) => {
4919
+ const upperInput = input.toUpperCase();
4920
+ if (key.upArrow || key.leftArrow) {
4921
+ setSelectedIndex(0);
4922
+ } else if (key.downArrow || key.rightArrow) {
4923
+ setSelectedIndex(1);
4924
+ } else if (key.return) {
4925
+ onSelect(options[selectedIndex].value);
4926
+ } else if (upperInput === "Y") {
4927
+ onSelect(true);
4928
+ } else if (upperInput === "N") {
4929
+ onSelect(false);
4930
+ } else if (key.escape || key.ctrl && input === "c") {
4931
+ onSelect(false);
4932
+ }
4933
+ });
4934
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(ink.Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { bold: true, color: "cyan" }, "\u{1F680} Happy Daemon Setup")), /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, null, "\u{1F4F1} Happy can run a background service that allows you to:"), /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan" }, " \u2022 Spawn new conversations from your phone"), /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan" }, " \u2022 Continue closed conversations remotely"), /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan" }, " \u2022 Work with Claude while your computer has internet")), /* @__PURE__ */ React.createElement(ink.Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, null, "Would you like Happy to start this service automatically?")), /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, options.map((option, index) => {
4935
+ const isSelected = selectedIndex === index;
4936
+ return /* @__PURE__ */ React.createElement(ink.Box, { key: option.key }, /* @__PURE__ */ React.createElement(ink.Text, { color: isSelected ? "green" : "gray" }, isSelected ? "\u203A " : " ", "[", option.key, "] ", option.label));
4937
+ })), /* @__PURE__ */ React.createElement(ink.Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { dimColor: true }, "Press Y/N or use arrows + Enter to select")));
4938
+ };
4939
+
4871
4940
  (async () => {
4872
4941
  const args = process.argv.slice(2);
4873
4942
  types$1.logger.debug("Starting happy CLI with args: ", process.argv);
@@ -4944,8 +5013,7 @@ async function handleAuthStatus() {
4944
5013
  }
4945
5014
  return;
4946
5015
  } else if (daemonSubcommand === "start") {
4947
- const happyBinPath = node_path.join(projectPath(), "bin", "happy.mjs");
4948
- const child = child_process.spawn(happyBinPath, ["daemon", "start-sync"], {
5016
+ const child = spawnHappyCLI(["daemon", "start-sync"], {
4949
5017
  detached: true,
4950
5018
  stdio: "ignore",
4951
5019
  env: process.env
@@ -5133,38 +5201,38 @@ ${chalk.bold.cyan("Claude Code Options (from `claude --help`):")}
5133
5201
  const result = await authAndSetupMachineIfNeeded();
5134
5202
  credentials = result.credentials;
5135
5203
  }
5204
+ const isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
5136
5205
  let settings = await readSettings();
5137
- if (settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
5138
- console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
5139
- const rl = node_readline.createInterface({
5140
- input: process.stdin,
5141
- output: process.stdout
5142
- });
5143
- console.log(chalk.cyan("\n\u{1F4F1} Happy can run a background service that allows you to:"));
5144
- console.log(chalk.cyan(" \u2022 Spawn new conversations from your phone"));
5145
- console.log(chalk.cyan(" \u2022 Continue closed conversations remotely"));
5146
- console.log(chalk.cyan(" \u2022 Work with Claude while your computer has internet\n"));
5147
- const answer = await new Promise((resolve) => {
5148
- rl.question(chalk.green("Would you like Happy to start this service automatically? (recommended) [Y/n]: "), resolve);
5206
+ if (isExperimentalEnabled && settings && settings.daemonAutoStartWhenRunningHappy === void 0) {
5207
+ const shouldAutoStart = await new Promise((resolve) => {
5208
+ let hasResolved = false;
5209
+ const onSelect = (autoStart) => {
5210
+ if (!hasResolved) {
5211
+ hasResolved = true;
5212
+ app.unmount();
5213
+ resolve(autoStart);
5214
+ }
5215
+ };
5216
+ const app = ink.render(React.createElement(DaemonPrompt, { onSelect }), {
5217
+ exitOnCtrlC: false,
5218
+ patchConsole: false
5219
+ });
5149
5220
  });
5150
- rl.close();
5151
- const shouldAutoStart = answer.toLowerCase() !== "n";
5152
5221
  settings = await updateSettings((settings2) => ({
5153
5222
  ...settings2,
5154
5223
  daemonAutoStartWhenRunningHappy: shouldAutoStart
5155
5224
  }));
5156
5225
  if (shouldAutoStart) {
5157
- console.log(chalk.green("\u2713 Happy will start the background service automatically"));
5226
+ console.log(chalk.green("\n\u2713 Happy will start the background service automatically"));
5158
5227
  console.log(chalk.gray(" The service will run whenever you use the happy command"));
5159
5228
  } else {
5160
- console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
5229
+ console.log(chalk.yellow("\n You can enable this later by running: happy daemon install"));
5161
5230
  }
5162
5231
  }
5163
- if (settings && settings.daemonAutoStartWhenRunningHappy) {
5232
+ if (isExperimentalEnabled && settings && settings.daemonAutoStartWhenRunningHappy) {
5164
5233
  types$1.logger.debug("Starting Happy background service...");
5165
5234
  if (!await isDaemonRunning()) {
5166
- const happyBinPath = node_path.join(projectPath(), "bin", "happy.mjs");
5167
- const daemonProcess = child_process.spawn(happyBinPath, ["daemon", "start-sync"], {
5235
+ const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
5168
5236
  detached: true,
5169
5237
  stdio: "ignore",
5170
5238
  env: process.env