happy-coder 0.1.12 → 0.1.14

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,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { l as logger, d as delay, e as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, f as encodeBase64, g as encodeBase64Url, h as decodeBase64, j as encrypt, k as decrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-D39L8JSd.mjs';
2
+ import { l as logger, d as delay, e as backoff, R as RawJSONLinesSchema, c as configuration, f as encodeBase64, A as ApiClient, g as encodeBase64Url, h as decodeBase64, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DD9P_5rj.mjs';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
4
  import { query, AbortError } from '@anthropic-ai/claude-code';
5
5
  import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
@@ -11,7 +11,7 @@ import { createInterface } from 'node:readline';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
13
13
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
- import { createServer, ServerResponse } from 'node:http';
14
+ import { createServer } from 'node:http';
15
15
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
16
16
  import * as z from 'zod';
17
17
  import { z as z$1 } from 'zod';
@@ -20,14 +20,13 @@ import { promisify } from 'util';
20
20
  import crypto, { createHash } from 'crypto';
21
21
  import { dirname as dirname$1, join as join$1 } from 'path';
22
22
  import { fileURLToPath as fileURLToPath$1 } from 'url';
23
- import httpProxy from 'http-proxy';
24
23
  import tweetnacl from 'tweetnacl';
25
24
  import axios from 'axios';
26
25
  import qrcode from 'qrcode-terminal';
27
26
  import { EventEmitter } from 'node:events';
28
27
  import { io } from 'socket.io-client';
29
28
  import { hostname, homedir as homedir$1 } from 'os';
30
- import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
29
+ import { closeSync, existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, openSync, writeSync, writeFileSync, chmodSync } from 'fs';
31
30
  import 'expo-server-sdk';
32
31
 
33
32
  function formatClaudeMessage(message, onAssistantResult) {
@@ -240,18 +239,32 @@ async function claudeRemote(opts) {
240
239
  });
241
240
  }
242
241
  printDivider();
242
+ let thinking = false;
243
+ const updateThinking = (newThinking) => {
244
+ if (thinking !== newThinking) {
245
+ thinking = newThinking;
246
+ logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
247
+ if (opts.onThinkingChange) {
248
+ opts.onThinkingChange(thinking);
249
+ }
250
+ }
251
+ };
243
252
  try {
244
253
  logger.debug(`[claudeRemote] Starting to iterate over response`);
245
254
  for await (const message of response) {
246
255
  logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
247
256
  formatClaudeMessage(message, opts.onAssistantResult);
248
257
  if (message.type === "system" && message.subtype === "init") {
258
+ updateThinking(true);
249
259
  logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
250
260
  const projectDir = getProjectPath(opts.path);
251
261
  const found = await awaitFileExist(join(projectDir, `${message.session_id}.jsonl`));
252
262
  logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
253
263
  opts.onSessionFound(message.session_id);
254
264
  }
265
+ if (message.type === "result") {
266
+ updateThinking(false);
267
+ }
255
268
  }
256
269
  logger.debug(`[claudeRemote] Finished iterating over response`);
257
270
  } catch (e) {
@@ -264,6 +277,7 @@ async function claudeRemote(opts) {
264
277
  throw e;
265
278
  }
266
279
  } finally {
280
+ updateThinking(false);
267
281
  if (opts.interruptController) {
268
282
  opts.interruptController.unregister();
269
283
  }
@@ -327,22 +341,70 @@ async function claudeLocal(opts) {
327
341
  input: child.stdio[3],
328
342
  crlfDelay: Infinity
329
343
  });
330
- rl.on("line", (line) => {
331
- const sessionMatch = line.match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i);
332
- if (sessionMatch) {
333
- detectedIdsRandomUUID.add(sessionMatch[0]);
334
- if (resolvedSessionId) {
335
- return;
344
+ const activeFetches = /* @__PURE__ */ new Map();
345
+ let thinking = false;
346
+ let stopThinkingTimeout = null;
347
+ const updateThinking = (newThinking) => {
348
+ if (thinking !== newThinking) {
349
+ thinking = newThinking;
350
+ logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
351
+ if (opts.onThinkingChange) {
352
+ opts.onThinkingChange(thinking);
336
353
  }
337
- if (detectedIdsFileSystem.has(sessionMatch[0])) {
338
- resolvedSessionId = sessionMatch[0];
339
- opts.onSessionFound(sessionMatch[0]);
354
+ }
355
+ };
356
+ rl.on("line", (line) => {
357
+ try {
358
+ const message = JSON.parse(line);
359
+ switch (message.type) {
360
+ case "uuid":
361
+ detectedIdsRandomUUID.add(message.value);
362
+ if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
363
+ resolvedSessionId = message.value;
364
+ opts.onSessionFound(message.value);
365
+ }
366
+ break;
367
+ case "fetch-start":
368
+ logger.debug(`[ClaudeLocal] Fetch start: ${message.method} ${message.hostname}${message.path} (id: ${message.id})`);
369
+ activeFetches.set(message.id, {
370
+ hostname: message.hostname,
371
+ path: message.path,
372
+ startTime: message.timestamp
373
+ });
374
+ if (stopThinkingTimeout) {
375
+ clearTimeout(stopThinkingTimeout);
376
+ stopThinkingTimeout = null;
377
+ }
378
+ updateThinking(true);
379
+ break;
380
+ case "fetch-end":
381
+ logger.debug(`[ClaudeLocal] Fetch end: id ${message.id}`);
382
+ activeFetches.delete(message.id);
383
+ if (activeFetches.size === 0 && thinking && !stopThinkingTimeout) {
384
+ stopThinkingTimeout = setTimeout(() => {
385
+ if (activeFetches.size === 0) {
386
+ updateThinking(false);
387
+ }
388
+ stopThinkingTimeout = null;
389
+ }, 500);
390
+ }
391
+ break;
392
+ default:
393
+ logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
340
394
  }
395
+ } catch (e) {
396
+ logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
341
397
  }
342
398
  });
343
399
  rl.on("error", (err) => {
344
400
  console.error("Error reading from fd 3:", err);
345
401
  });
402
+ child.on("exit", () => {
403
+ if (stopThinkingTimeout) {
404
+ clearTimeout(stopThinkingTimeout);
405
+ }
406
+ updateThinking(false);
407
+ });
346
408
  }
347
409
  child.on("error", (error) => {
348
410
  });
@@ -798,6 +860,7 @@ async function loop(opts) {
798
860
  path: opts.path,
799
861
  sessionId,
800
862
  onSessionFound,
863
+ onThinkingChange: opts.onThinkingChange,
801
864
  abort: interactiveAbortController.signal,
802
865
  claudeEnvVars: opts.claudeEnvVars,
803
866
  claudeArgs: opts.claudeArgs
@@ -860,6 +923,7 @@ async function loop(opts) {
860
923
  mcpServers: opts.mcpServers,
861
924
  permissionPromptToolName: opts.permissionPromptToolName,
862
925
  onSessionFound,
926
+ onThinkingChange: opts.onThinkingChange,
863
927
  messages: currentMessageQueue,
864
928
  onAssistantResult: opts.onAssistantResult,
865
929
  interruptController: opts.interruptController,
@@ -988,7 +1052,7 @@ class InterruptController {
988
1052
  }
989
1053
  }
990
1054
 
991
- var version = "0.1.12";
1055
+ var version = "0.1.14";
992
1056
  var packageJson = {
993
1057
  version: version};
994
1058
 
@@ -1273,148 +1337,77 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1273
1337
  });
1274
1338
  }
1275
1339
 
1276
- async function startHTTPDirectProxy(options) {
1277
- const proxy = httpProxy.createProxyServer({
1278
- target: options.target,
1279
- changeOrigin: true,
1280
- secure: false
1281
- });
1282
- proxy.on("error", (err, req, res) => {
1283
- logger.debug(`[HTTPProxy] Proxy error: ${err.message} for ${req.method} ${req.url}`);
1284
- if (res instanceof ServerResponse && !res.headersSent) {
1285
- res.writeHead(500, { "Content-Type": "text/plain" });
1286
- res.end("Proxy error");
1287
- }
1288
- });
1289
- proxy.on("proxyReq", (proxyReq, req, res) => {
1290
- if (options.onRequest) {
1291
- options.onRequest(req, proxyReq);
1292
- }
1293
- });
1294
- proxy.on("proxyRes", (proxyRes, req, res) => {
1295
- if (options.onResponse) {
1296
- options.onResponse(req, proxyRes);
1297
- }
1298
- });
1299
- const server = createServer((req, res) => {
1300
- proxy.web(req, res);
1301
- });
1302
- const url = await new Promise((resolve, reject) => {
1303
- server.listen(0, "127.0.0.1", () => {
1304
- const addr = server.address();
1305
- if (addr && typeof addr === "object") {
1306
- const proxyUrl = `http://127.0.0.1:${addr.port}`;
1307
- logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
1308
- resolve(proxyUrl);
1309
- } else {
1310
- reject(new Error("Failed to get server address"));
1311
- }
1312
- });
1313
- });
1314
- return url;
1340
+ const defaultSettings = {
1341
+ onboardingCompleted: false
1342
+ };
1343
+ async function readSettings() {
1344
+ if (!existsSync(configuration.settingsFile)) {
1345
+ return { ...defaultSettings };
1346
+ }
1347
+ try {
1348
+ const content = await readFile(configuration.settingsFile, "utf8");
1349
+ return JSON.parse(content);
1350
+ } catch {
1351
+ return { ...defaultSettings };
1352
+ }
1315
1353
  }
1316
-
1317
- async function startClaudeActivityTracker(onThinking) {
1318
- logger.debug(`[ClaudeActivityTracker] Starting activity tracker`);
1319
- let requestCounter = 0;
1320
- const activeRequests = /* @__PURE__ */ new Map();
1321
- let stopThinkingTimeout = null;
1322
- let isThinking = false;
1323
- const REQUEST_TIMEOUT = 5 * 60 * 1e3;
1324
- const checkAndStopThinking = () => {
1325
- if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1326
- stopThinkingTimeout = setTimeout(() => {
1327
- if (isThinking && activeRequests.size === 0) {
1328
- isThinking = false;
1329
- onThinking(false);
1330
- }
1331
- stopThinkingTimeout = null;
1332
- }, 500);
1333
- }
1334
- };
1335
- const proxyUrl = await startHTTPDirectProxy({
1336
- target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1337
- onRequest: (req, proxyReq) => {
1338
- if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1339
- const requestId = ++requestCounter;
1340
- req._requestId = requestId;
1341
- if (stopThinkingTimeout) {
1342
- clearTimeout(stopThinkingTimeout);
1343
- stopThinkingTimeout = null;
1344
- }
1345
- const timeout = setTimeout(() => {
1346
- activeRequests.delete(requestId);
1347
- checkAndStopThinking();
1348
- }, REQUEST_TIMEOUT);
1349
- activeRequests.set(requestId, timeout);
1350
- if (!isThinking) {
1351
- isThinking = true;
1352
- onThinking(true);
1353
- }
1354
- }
1355
- },
1356
- onResponse: (req, proxyRes) => {
1357
- proxyRes.headers["connection"] = "close";
1358
- if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1359
- const requestId = req._requestId;
1360
- const timeout = activeRequests.get(requestId);
1361
- if (timeout) {
1362
- clearTimeout(timeout);
1363
- }
1364
- let cleaned = false;
1365
- const cleanupRequest = () => {
1366
- if (!cleaned) {
1367
- cleaned = true;
1368
- activeRequests.delete(requestId);
1369
- checkAndStopThinking();
1370
- }
1371
- };
1372
- proxyRes.on("end", () => {
1373
- cleanupRequest();
1374
- });
1375
- proxyRes.on("error", (err) => {
1376
- cleanupRequest();
1377
- });
1378
- proxyRes.on("aborted", () => {
1379
- cleanupRequest();
1380
- });
1381
- proxyRes.on("close", () => {
1382
- cleanupRequest();
1383
- });
1384
- req.on("close", () => {
1385
- cleanupRequest();
1386
- });
1387
- }
1388
- }
1389
- });
1390
- const reset = () => {
1391
- for (const [requestId, timeout] of activeRequests) {
1392
- clearTimeout(timeout);
1393
- }
1394
- activeRequests.clear();
1395
- if (stopThinkingTimeout) {
1396
- clearTimeout(stopThinkingTimeout);
1397
- stopThinkingTimeout = null;
1398
- }
1399
- if (isThinking) {
1400
- isThinking = false;
1401
- onThinking(false);
1402
- }
1403
- };
1404
- return {
1405
- proxyUrl,
1406
- reset
1407
- };
1354
+ async function writeSettings(settings) {
1355
+ if (!existsSync(configuration.happyDir)) {
1356
+ await mkdir(configuration.happyDir, { recursive: true });
1357
+ }
1358
+ await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
1359
+ }
1360
+ const credentialsSchema = z.object({
1361
+ secret: z.string().base64(),
1362
+ token: z.string()
1363
+ });
1364
+ async function readCredentials() {
1365
+ if (!existsSync(configuration.privateKeyFile)) {
1366
+ return null;
1367
+ }
1368
+ try {
1369
+ const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
1370
+ const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
1371
+ return {
1372
+ secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
1373
+ token: credentials.token
1374
+ };
1375
+ } catch {
1376
+ return null;
1377
+ }
1378
+ }
1379
+ async function writeCredentials(credentials) {
1380
+ if (!existsSync(configuration.happyDir)) {
1381
+ await mkdir(configuration.happyDir, { recursive: true });
1382
+ }
1383
+ await writeFile$1(configuration.privateKeyFile, JSON.stringify({
1384
+ secret: encodeBase64(credentials.secret),
1385
+ token: credentials.token
1386
+ }, null, 2));
1408
1387
  }
1409
1388
 
1410
1389
  async function start(credentials, options = {}) {
1411
1390
  const workingDirectory = process.cwd();
1412
1391
  const sessionTag = randomUUID();
1392
+ if (options.daemonSpawn && options.startingMode === "local") {
1393
+ logger.debug("Daemon spawn requested with local mode - forcing remote mode");
1394
+ options.startingMode = "remote";
1395
+ }
1413
1396
  const api = new ApiClient(credentials.token, credentials.secret);
1414
1397
  let state = {};
1415
- let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
1398
+ const settings = await readSettings() || { };
1399
+ let metadata = {
1400
+ path: workingDirectory,
1401
+ host: os.hostname(),
1402
+ version: packageJson.version,
1403
+ os: os.platform(),
1404
+ machineId: settings.machineId
1405
+ };
1416
1406
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1417
1407
  logger.debug(`Session created: ${response.id}`);
1408
+ if (options.daemonSpawn) {
1409
+ console.log(`daemon:sessionIdCreated:${response.id}`);
1410
+ }
1418
1411
  const session = api.session(response);
1419
1412
  const pushClient = api.push();
1420
1413
  let thinking = false;
@@ -1422,11 +1415,6 @@ async function start(credentials, options = {}) {
1422
1415
  let pingInterval = setInterval(() => {
1423
1416
  session.keepAlive(thinking, mode);
1424
1417
  }, 2e3);
1425
- const activityTracker = await startClaudeActivityTracker((newThinking) => {
1426
- thinking = newThinking;
1427
- session.keepAlive(thinking, mode);
1428
- });
1429
- process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
1430
1418
  const logPath = await logger.logFilePathPromise;
1431
1419
  logger.infoDeveloper(`Session: ${response.id}`);
1432
1420
  logger.infoDeveloper(`Logs: ${logPath}`);
@@ -1558,7 +1546,6 @@ async function start(credentials, options = {}) {
1558
1546
  },
1559
1547
  onProcessStart: (processMode) => {
1560
1548
  logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
1561
- activityTracker.reset();
1562
1549
  logger.debug("Starting process - clearing any stale permission requests");
1563
1550
  for (const [id, resolve] of requests) {
1564
1551
  logger.debug(`Rejecting stale permission request: ${id}`);
@@ -1568,13 +1555,14 @@ async function start(credentials, options = {}) {
1568
1555
  },
1569
1556
  onProcessStop: (processMode) => {
1570
1557
  logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
1571
- activityTracker.reset();
1572
1558
  logger.debug("Stopping process - clearing any stale permission requests");
1573
1559
  for (const [id, resolve] of requests) {
1574
1560
  logger.debug(`Rejecting stale permission request: ${id}`);
1575
1561
  resolve({ approved: false, reason: "Process restarted" });
1576
1562
  }
1577
1563
  requests.clear();
1564
+ thinking = false;
1565
+ session.keepAlive(thinking, mode);
1578
1566
  },
1579
1567
  mcpServers: {
1580
1568
  "permission": {
@@ -1587,61 +1575,16 @@ async function start(credentials, options = {}) {
1587
1575
  onAssistantResult,
1588
1576
  interruptController,
1589
1577
  claudeEnvVars: options.claudeEnvVars,
1590
- claudeArgs: options.claudeArgs
1578
+ claudeArgs: options.claudeArgs,
1579
+ onThinkingChange: (newThinking) => {
1580
+ thinking = newThinking;
1581
+ session.keepAlive(thinking, mode);
1582
+ }
1591
1583
  });
1592
1584
  clearInterval(pingInterval);
1593
1585
  process.exit(0);
1594
1586
  }
1595
1587
 
1596
- const defaultSettings = {
1597
- onboardingCompleted: false
1598
- };
1599
- async function readSettings() {
1600
- if (!existsSync(configuration.settingsFile)) {
1601
- return { ...defaultSettings };
1602
- }
1603
- try {
1604
- const content = await readFile(configuration.settingsFile, "utf8");
1605
- return JSON.parse(content);
1606
- } catch {
1607
- return { ...defaultSettings };
1608
- }
1609
- }
1610
- async function writeSettings(settings) {
1611
- if (!existsSync(configuration.happyDir)) {
1612
- await mkdir(configuration.happyDir, { recursive: true });
1613
- }
1614
- await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
1615
- }
1616
- const credentialsSchema = z.object({
1617
- secret: z.string().base64(),
1618
- token: z.string()
1619
- });
1620
- async function readCredentials() {
1621
- if (!existsSync(configuration.privateKeyFile)) {
1622
- return null;
1623
- }
1624
- try {
1625
- const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
1626
- const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
1627
- return {
1628
- secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
1629
- token: credentials.token
1630
- };
1631
- } catch {
1632
- return null;
1633
- }
1634
- }
1635
- async function writeCredentials(credentials) {
1636
- if (!existsSync(configuration.happyDir)) {
1637
- await mkdir(configuration.happyDir, { recursive: true });
1638
- }
1639
- await writeFile$1(configuration.privateKeyFile, JSON.stringify({
1640
- secret: encodeBase64(credentials.secret),
1641
- token: credentials.token
1642
- }, null, 2));
1643
- }
1644
-
1645
1588
  function displayQRCode(url) {
1646
1589
  console.log("=".repeat(80));
1647
1590
  console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
@@ -1669,10 +1612,8 @@ async function doAuth() {
1669
1612
  console.log("Please, authenticate using mobile app");
1670
1613
  const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
1671
1614
  displayQRCode(authUrl);
1672
- if (process.env.DEBUG === "1") {
1673
- console.log("\n\u{1F4CB} For manual entry, copy this URL:");
1674
- console.log(authUrl);
1675
- }
1615
+ console.log("\n\u{1F4CB} For manual entry, copy this URL:");
1616
+ console.log(authUrl);
1676
1617
  let credentials = null;
1677
1618
  while (true) {
1678
1619
  try {
@@ -1720,18 +1661,20 @@ class ApiDaemonSession extends EventEmitter {
1720
1661
  keepAliveInterval = null;
1721
1662
  token;
1722
1663
  secret;
1664
+ spawnedProcesses = /* @__PURE__ */ new Set();
1723
1665
  constructor(token, secret, machineIdentity) {
1724
1666
  super();
1725
1667
  this.token = token;
1726
1668
  this.secret = secret;
1727
1669
  this.machineIdentity = machineIdentity;
1670
+ logger.daemonDebug(`Connecting to server: ${configuration.serverUrl}`);
1728
1671
  const socket = io(configuration.serverUrl, {
1729
1672
  auth: {
1730
1673
  token: this.token,
1731
1674
  clientType: "machine-scoped",
1732
1675
  machineId: this.machineIdentity.machineId
1733
1676
  },
1734
- path: "/v1/user-machine-daemon",
1677
+ path: "/v1/updates",
1735
1678
  reconnection: true,
1736
1679
  reconnectionAttempts: Infinity,
1737
1680
  reconnectionDelay: 1e3,
@@ -1741,68 +1684,146 @@ class ApiDaemonSession extends EventEmitter {
1741
1684
  autoConnect: false
1742
1685
  });
1743
1686
  socket.on("connect", () => {
1744
- logger.debug("[DAEMON] Connected to server");
1687
+ logger.daemonDebug("Socket connected");
1688
+ logger.daemonDebug(`Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
1689
+ const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
1690
+ socket.emit("rpc-register", { method: rpcMethod });
1691
+ logger.daemonDebug(`Emitted RPC registration: ${rpcMethod}`);
1745
1692
  this.emit("connected");
1746
- socket.emit("machine-connect", {
1747
- token: this.token,
1748
- machineIdentity: encodeBase64(encrypt(this.machineIdentity, this.secret))
1749
- });
1750
1693
  this.startKeepAlive();
1751
1694
  });
1752
- socket.on("disconnect", () => {
1753
- logger.debug("[DAEMON] Disconnected from server");
1754
- this.emit("disconnected");
1755
- this.stopKeepAlive();
1756
- });
1757
- socket.on("spawn-session", async (encryptedData, callback) => {
1758
- let requestData;
1759
- try {
1760
- requestData = decrypt(decodeBase64(encryptedData), this.secret);
1761
- logger.debug("[DAEMON] Received spawn-session request", requestData);
1762
- const args = [
1763
- "--directory",
1764
- requestData.directory,
1765
- "--happy-starting-mode",
1766
- requestData.startingMode
1767
- ];
1768
- if (requestData.metadata) {
1769
- args.push("--metadata", requestData.metadata);
1770
- }
1771
- if (requestData.startingMode === "interactive" && process.platform === "darwin") {
1772
- const script = `
1773
- tell application "Terminal"
1774
- activate
1775
- do script "cd ${requestData.directory} && happy ${args.join(" ")}"
1776
- end tell
1777
- `;
1778
- spawn$1("osascript", ["-e", script], { detached: true });
1779
- } else {
1780
- const child = spawn$1("happy", args, {
1695
+ socket.on("rpc-request", async (data, callback) => {
1696
+ logger.daemonDebug(`Received RPC request: ${JSON.stringify(data)}`);
1697
+ const expectedMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
1698
+ if (data.method === expectedMethod) {
1699
+ logger.daemonDebug("Processing spawn-happy-session RPC");
1700
+ try {
1701
+ const { directory } = data.params || {};
1702
+ if (!directory) {
1703
+ throw new Error("Directory is required");
1704
+ }
1705
+ const args = [
1706
+ "--daemon-spawn",
1707
+ "--happy-starting-mode",
1708
+ "remote"
1709
+ // ALWAYS force remote mode for daemon spawns
1710
+ ];
1711
+ if (configuration.installationLocation === "local") {
1712
+ args.push("--local");
1713
+ }
1714
+ if (configuration.serverUrl !== "https://handy-api.korshakov.org") {
1715
+ args.push("--happy-server-url", configuration.serverUrl);
1716
+ }
1717
+ logger.daemonDebug(`Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
1718
+ const happyPath = process.argv[1];
1719
+ const isTypeScript = happyPath.endsWith(".ts");
1720
+ const happyProcess = isTypeScript ? spawn$1("npx", ["tsx", happyPath, ...args], {
1721
+ cwd: directory,
1722
+ env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
1781
1723
  detached: true,
1782
- stdio: "ignore",
1783
- cwd: requestData.directory
1724
+ stdio: ["ignore", "pipe", "pipe"]
1725
+ // We need stdout
1726
+ }) : spawn$1(process.argv[0], [happyPath, ...args], {
1727
+ cwd: directory,
1728
+ env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
1729
+ detached: true,
1730
+ stdio: ["ignore", "pipe", "pipe"]
1731
+ // We need stdout
1732
+ });
1733
+ this.spawnedProcesses.add(happyProcess);
1734
+ let sessionId = null;
1735
+ let output = "";
1736
+ let timeoutId = null;
1737
+ const cleanup = () => {
1738
+ happyProcess.stdout.removeAllListeners("data");
1739
+ happyProcess.stderr.removeAllListeners("data");
1740
+ happyProcess.removeAllListeners("error");
1741
+ happyProcess.removeAllListeners("exit");
1742
+ if (timeoutId) {
1743
+ clearTimeout(timeoutId);
1744
+ timeoutId = null;
1745
+ }
1746
+ };
1747
+ happyProcess.stdout.on("data", (data2) => {
1748
+ output += data2.toString();
1749
+ const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
1750
+ if (match && !sessionId) {
1751
+ sessionId = match[1];
1752
+ logger.daemonDebug(`Session spawned successfully: ${sessionId}`);
1753
+ callback({ sessionId });
1754
+ cleanup();
1755
+ happyProcess.unref();
1756
+ }
1784
1757
  });
1785
- child.unref();
1758
+ happyProcess.stderr.on("data", (data2) => {
1759
+ logger.daemonDebug(`Spawned process stderr: ${data2.toString()}`);
1760
+ });
1761
+ happyProcess.on("error", (error) => {
1762
+ logger.daemonDebug("Error spawning session:", error);
1763
+ if (!sessionId) {
1764
+ callback({ error: `Failed to spawn: ${error.message}` });
1765
+ cleanup();
1766
+ this.spawnedProcesses.delete(happyProcess);
1767
+ }
1768
+ });
1769
+ happyProcess.on("exit", (code, signal) => {
1770
+ logger.daemonDebug(`Spawned process exited with code ${code}, signal ${signal}`);
1771
+ this.spawnedProcesses.delete(happyProcess);
1772
+ if (!sessionId) {
1773
+ callback({ error: `Process exited before session ID received` });
1774
+ cleanup();
1775
+ }
1776
+ });
1777
+ timeoutId = setTimeout(() => {
1778
+ if (!sessionId) {
1779
+ logger.daemonDebug("Timeout waiting for session ID");
1780
+ callback({ error: "Timeout waiting for session" });
1781
+ cleanup();
1782
+ happyProcess.kill();
1783
+ this.spawnedProcesses.delete(happyProcess);
1784
+ }
1785
+ }, 1e4);
1786
+ } catch (error) {
1787
+ logger.daemonDebug("Error spawning session:", error);
1788
+ callback({ error: error instanceof Error ? error.message : "Unknown error" });
1786
1789
  }
1787
- const result = { success: true };
1788
- socket.emit("session-spawn-result", {
1789
- requestId: requestData.requestId,
1790
- result: encodeBase64(encrypt(result, this.secret))
1791
- });
1792
- callback(encodeBase64(encrypt({ success: true }, this.secret)));
1793
- } catch (error) {
1794
- logger.debug("[DAEMON] Failed to spawn session", error);
1795
- const errorResult = {
1796
- success: false,
1797
- error: error instanceof Error ? error.message : "Unknown error"
1798
- };
1799
- socket.emit("session-spawn-result", {
1800
- requestId: requestData?.requestId || "",
1801
- result: encodeBase64(encrypt(errorResult, this.secret))
1802
- });
1803
- callback(encodeBase64(encrypt(errorResult, this.secret)));
1790
+ } else {
1791
+ logger.daemonDebug(`Unknown RPC method: ${data.method}`);
1792
+ callback({ error: `Unknown method: ${data.method}` });
1804
1793
  }
1805
1794
  });
1795
+ socket.on("disconnect", (reason) => {
1796
+ logger.daemonDebug(`Disconnected from server. Reason: ${reason}`);
1797
+ this.emit("disconnected");
1798
+ this.stopKeepAlive();
1799
+ });
1800
+ socket.on("reconnect", () => {
1801
+ logger.daemonDebug("Reconnected to server");
1802
+ const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
1803
+ socket.emit("rpc-register", { method: rpcMethod });
1804
+ logger.daemonDebug(`Re-registered RPC method: ${rpcMethod}`);
1805
+ });
1806
+ socket.on("rpc-registered", (data) => {
1807
+ logger.daemonDebug(`RPC registration confirmed: ${data.method}`);
1808
+ });
1809
+ socket.on("rpc-unregistered", (data) => {
1810
+ logger.daemonDebug(`RPC unregistered: ${data.method}`);
1811
+ });
1812
+ socket.on("rpc-error", (data) => {
1813
+ logger.daemonDebug(`RPC error: ${JSON.stringify(data)}`);
1814
+ });
1815
+ socket.onAny((event, ...args) => {
1816
+ if (!event.startsWith("machine-alive")) {
1817
+ logger.daemonDebug(`Socket event: ${event}, args: ${JSON.stringify(args)}`);
1818
+ }
1819
+ });
1820
+ socket.on("connect_error", (error) => {
1821
+ logger.daemonDebug(`Connection error: ${error.message}`);
1822
+ logger.daemonDebug(`Error: ${JSON.stringify(error, null, 2)}`);
1823
+ });
1824
+ socket.on("error", (error) => {
1825
+ logger.daemonDebug(`Socket error: ${error}`);
1826
+ });
1806
1827
  socket.on("daemon-command", (data) => {
1807
1828
  switch (data.command) {
1808
1829
  case "shutdown":
@@ -1818,6 +1839,7 @@ class ApiDaemonSession extends EventEmitter {
1818
1839
  startKeepAlive() {
1819
1840
  this.stopKeepAlive();
1820
1841
  this.keepAliveInterval = setInterval(() => {
1842
+ logger.daemonDebug("Sending keep-alive ping");
1821
1843
  this.socket.volatile.emit("machine-alive", {
1822
1844
  time: Date.now()
1823
1845
  });
@@ -1833,22 +1855,42 @@ class ApiDaemonSession extends EventEmitter {
1833
1855
  this.socket.connect();
1834
1856
  }
1835
1857
  shutdown() {
1858
+ logger.daemonDebug(`Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
1859
+ for (const process2 of this.spawnedProcesses) {
1860
+ try {
1861
+ logger.daemonDebug(`Killing spawned process with PID: ${process2.pid}`);
1862
+ process2.kill("SIGTERM");
1863
+ setTimeout(() => {
1864
+ try {
1865
+ process2.kill("SIGKILL");
1866
+ } catch (e) {
1867
+ }
1868
+ }, 1e3);
1869
+ } catch (error) {
1870
+ logger.daemonDebug(`Error killing process: ${error}`);
1871
+ }
1872
+ }
1873
+ this.spawnedProcesses.clear();
1836
1874
  this.stopKeepAlive();
1837
1875
  this.socket.close();
1838
1876
  this.emit("shutdown");
1839
1877
  }
1840
1878
  }
1841
1879
 
1880
+ let pidFileFd = null;
1842
1881
  async function startDaemon() {
1843
- console.log("[DAEMON] Starting daemon process...");
1882
+ if (process.platform !== "darwin") {
1883
+ console.error("ERROR: Daemon is only supported on macOS");
1884
+ process.exit(1);
1885
+ }
1886
+ logger.daemonDebug("Starting daemon process...");
1887
+ logger.daemonDebug(`Server URL: ${configuration.serverUrl}`);
1844
1888
  if (await isDaemonRunning()) {
1845
- console.log("Happy daemon is already running");
1889
+ logger.daemonDebug("Happy daemon is already running");
1846
1890
  process.exit(0);
1847
1891
  }
1848
- console.log("[DAEMON] Writing PID file with PID:", process.pid);
1849
- writePidFile();
1850
- console.log("[DAEMON] PID file written successfully");
1851
- logger.info("Happy CLI daemon started successfully");
1892
+ pidFileFd = writePidFile();
1893
+ logger.daemonDebug("PID file written");
1852
1894
  process.on("SIGINT", () => {
1853
1895
  stopDaemon().catch(console.error);
1854
1896
  });
@@ -1873,7 +1915,7 @@ async function startDaemon() {
1873
1915
  };
1874
1916
  let credentials = await readCredentials();
1875
1917
  if (!credentials) {
1876
- logger.debug("[DAEMON] No credentials found, running auth");
1918
+ logger.daemonDebug("No credentials found, running auth");
1877
1919
  await doAuth();
1878
1920
  credentials = await readCredentials();
1879
1921
  if (!credentials) {
@@ -1881,64 +1923,64 @@ async function startDaemon() {
1881
1923
  }
1882
1924
  }
1883
1925
  const { token, secret } = credentials;
1884
- const daemon = new ApiDaemonSession(token, secret, machineIdentity);
1926
+ const daemon = new ApiDaemonSession(
1927
+ token,
1928
+ secret,
1929
+ machineIdentity
1930
+ );
1885
1931
  daemon.on("connected", () => {
1886
- logger.debug("[DAEMON] Successfully connected to server");
1932
+ logger.daemonDebug("Connected to server event received");
1887
1933
  });
1888
1934
  daemon.on("disconnected", () => {
1889
- logger.debug("[DAEMON] Disconnected from server");
1935
+ logger.daemonDebug("Disconnected from server event received");
1890
1936
  });
1891
1937
  daemon.on("shutdown", () => {
1892
- logger.debug("[DAEMON] Shutdown requested");
1938
+ logger.daemonDebug("Shutdown requested");
1893
1939
  stopDaemon();
1894
1940
  process.exit(0);
1895
1941
  });
1896
1942
  daemon.connect();
1897
- setInterval(() => {
1898
- }, 1e3);
1943
+ logger.daemonDebug("Daemon started successfully");
1899
1944
  } catch (error) {
1900
- logger.debug("[DAEMON] Failed to start daemon", error);
1945
+ logger.daemonDebug("Failed to start daemon", error);
1901
1946
  stopDaemon();
1902
1947
  process.exit(1);
1903
1948
  }
1904
- process.on("SIGINT", () => process.exit(0));
1905
- process.on("SIGTERM", () => process.exit(0));
1906
- process.on("exit", () => process.exit(0));
1907
1949
  while (true) {
1908
1950
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1909
1951
  }
1910
1952
  }
1911
1953
  async function isDaemonRunning() {
1912
1954
  try {
1913
- console.log("[isDaemonRunning] Checking if daemon is running...");
1955
+ logger.daemonDebug("[isDaemonRunning] Checking if daemon is running...");
1914
1956
  if (existsSync$1(configuration.daemonPidFile)) {
1915
- console.log("[isDaemonRunning] PID file exists");
1957
+ logger.daemonDebug("[isDaemonRunning] PID file exists");
1916
1958
  const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
1917
- console.log("[isDaemonRunning] PID from file:", pid);
1959
+ logger.daemonDebug("[isDaemonRunning] PID from file:", pid);
1918
1960
  try {
1919
1961
  process.kill(pid, 0);
1920
- console.log("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
1962
+ logger.daemonDebug("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
1921
1963
  const isHappyDaemon = await isProcessHappyDaemon(pid);
1922
- console.log("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
1964
+ logger.daemonDebug("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
1923
1965
  if (isHappyDaemon) {
1924
1966
  return true;
1925
1967
  } else {
1926
- console.log("[isDaemonRunning] PID is not a happy daemon, cleaning up");
1927
- logger.debug(`[DAEMON] PID ${pid} is not a happy daemon, cleaning up`);
1968
+ logger.daemonDebug("[isDaemonRunning] PID is not a happy daemon, cleaning up");
1969
+ logger.debug(`PID ${pid} is not a happy daemon, cleaning up`);
1928
1970
  unlinkSync(configuration.daemonPidFile);
1929
1971
  }
1930
1972
  } catch (error) {
1931
- console.log("[isDaemonRunning] Process not running, cleaning up stale PID file");
1932
- logger.debug("[DAEMON] Process not running, cleaning up stale PID file");
1973
+ logger.daemonDebug("[isDaemonRunning] Process not running, cleaning up stale PID file");
1974
+ logger.debug("Process not running, cleaning up stale PID file");
1933
1975
  unlinkSync(configuration.daemonPidFile);
1934
1976
  }
1935
1977
  } else {
1936
- console.log("[isDaemonRunning] No PID file found");
1978
+ logger.daemonDebug("[isDaemonRunning] No PID file found");
1937
1979
  }
1938
1980
  return false;
1939
1981
  } catch (error) {
1940
- console.log("[isDaemonRunning] Error:", error);
1941
- logger.debug("[DAEMON] Error checking daemon status", error);
1982
+ logger.daemonDebug("[isDaemonRunning] Error:", error);
1983
+ logger.debug("Error checking daemon status", error);
1942
1984
  return false;
1943
1985
  }
1944
1986
  }
@@ -1948,20 +1990,46 @@ function writePidFile() {
1948
1990
  mkdirSync$1(happyDir, { recursive: true });
1949
1991
  }
1950
1992
  try {
1951
- writeFileSync(configuration.daemonPidFile, process.pid.toString(), { flag: "wx" });
1993
+ const fd = openSync(configuration.daemonPidFile, "wx");
1994
+ writeSync(fd, process.pid.toString());
1995
+ return fd;
1952
1996
  } catch (error) {
1953
1997
  if (error.code === "EEXIST") {
1954
- logger.debug("[DAEMON] PID file already exists, another daemon may be starting");
1955
- throw new Error("Daemon PID file already exists");
1998
+ try {
1999
+ const fd = openSync(configuration.daemonPidFile, "r+");
2000
+ const existingPid = readFileSync$1(configuration.daemonPidFile, "utf-8").trim();
2001
+ closeSync(fd);
2002
+ try {
2003
+ process.kill(parseInt(existingPid), 0);
2004
+ logger.daemonDebug("PID file exists and process is running");
2005
+ logger.daemonDebug("Happy daemon is already running");
2006
+ process.exit(0);
2007
+ } catch {
2008
+ logger.daemonDebug("PID file exists but process is dead, cleaning up");
2009
+ unlinkSync(configuration.daemonPidFile);
2010
+ return writePidFile();
2011
+ }
2012
+ } catch (lockError) {
2013
+ logger.daemonDebug("Cannot acquire write lock on PID file, daemon is running");
2014
+ logger.daemonDebug("Happy daemon is already running");
2015
+ process.exit(0);
2016
+ }
1956
2017
  }
1957
2018
  throw error;
1958
2019
  }
1959
2020
  }
1960
2021
  async function stopDaemon() {
1961
2022
  try {
2023
+ if (pidFileFd !== null) {
2024
+ try {
2025
+ closeSync(pidFileFd);
2026
+ } catch {
2027
+ }
2028
+ pidFileFd = null;
2029
+ }
1962
2030
  if (existsSync$1(configuration.daemonPidFile)) {
1963
2031
  const pid = parseInt(readFileSync$1(configuration.daemonPidFile, "utf-8"));
1964
- logger.debug(`[DAEMON] Stopping daemon with PID ${pid}`);
2032
+ logger.debug(`Stopping daemon with PID ${pid}`);
1965
2033
  try {
1966
2034
  process.kill(pid, "SIGTERM");
1967
2035
  await new Promise((resolve) => setTimeout(resolve, 1e3));
@@ -1971,12 +2039,12 @@ async function stopDaemon() {
1971
2039
  } catch {
1972
2040
  }
1973
2041
  } catch (error) {
1974
- logger.debug("[DAEMON] Process already dead or inaccessible", error);
2042
+ logger.debug("Process already dead or inaccessible", error);
1975
2043
  }
1976
2044
  unlinkSync(configuration.daemonPidFile);
1977
2045
  }
1978
2046
  } catch (error) {
1979
- logger.debug("[DAEMON] Error stopping daemon", error);
2047
+ logger.debug("Error stopping daemon", error);
1980
2048
  }
1981
2049
  }
1982
2050
  async function isProcessHappyDaemon(pid) {
@@ -2124,7 +2192,12 @@ async function uninstall() {
2124
2192
  (async () => {
2125
2193
  const args = process.argv.slice(2);
2126
2194
  let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
2127
- initializeConfiguration(installationLocation);
2195
+ let serverUrl;
2196
+ const serverUrlIndex = args.indexOf("--happy-server-url");
2197
+ if (serverUrlIndex !== -1 && serverUrlIndex + 1 < args.length) {
2198
+ serverUrl = args[serverUrlIndex + 1];
2199
+ }
2200
+ initializeConfiguration(installationLocation, serverUrl);
2128
2201
  initLoggerWithGlobalConfiguration();
2129
2202
  logger.debug("Starting happy CLI with args: ", process.argv);
2130
2203
  const subcommand = args[0];
@@ -2206,6 +2279,10 @@ Currently only supported on macOS.
2206
2279
  } else if (arg === "--claude-arg") {
2207
2280
  const claudeArg = args[++i];
2208
2281
  options.claudeArgs = [...options.claudeArgs || [], claudeArg];
2282
+ } else if (arg === "--daemon-spawn") {
2283
+ options.daemonSpawn = true;
2284
+ } else if (arg === "--happy-server-url") {
2285
+ i++;
2209
2286
  } else {
2210
2287
  console.error(chalk.red(`Unknown argument: ${arg}`));
2211
2288
  process.exit(1);
@@ -2241,6 +2318,8 @@ ${chalk.bold("Options:")}
2241
2318
  You will require re-login each time you run this in a new directory.
2242
2319
  --happy-starting-mode <interactive|remote>
2243
2320
  Set the starting mode for new sessions (default: remote)
2321
+ --happy-server-url <url>
2322
+ Set the server URL (overrides HANDY_SERVER_URL environment variable)
2244
2323
 
2245
2324
  ${chalk.bold("Examples:")}
2246
2325
  happy Start a session with default settings
@@ -2267,7 +2346,71 @@ ${chalk.bold("Examples:")}
2267
2346
  }
2268
2347
  credentials = res;
2269
2348
  }
2270
- await readSettings() || { };
2349
+ const settings = await readSettings() || { onboardingCompleted: false };
2350
+ process.env.EXPERIMENTAL_FEATURES !== void 0;
2351
+ if (settings.daemonAutoStartWhenRunningHappy === void 0) {
2352
+ console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
2353
+ const rl = createInterface({
2354
+ input: process.stdin,
2355
+ output: process.stdout
2356
+ });
2357
+ console.log(chalk.cyan("\n\u{1F4F1} Happy can run a background service that allows you to:"));
2358
+ console.log(chalk.cyan(" \u2022 Spawn new conversations from your phone"));
2359
+ console.log(chalk.cyan(" \u2022 Continue closed conversations remotely"));
2360
+ console.log(chalk.cyan(" \u2022 Work with Claude while your computer has internet\n"));
2361
+ const answer = await new Promise((resolve) => {
2362
+ rl.question(chalk.green("Would you like Happy to start this service automatically? (recommended) [Y/n]: "), resolve);
2363
+ });
2364
+ rl.close();
2365
+ const shouldAutoStart = answer.toLowerCase() !== "n";
2366
+ settings.daemonAutoStartWhenRunningHappy = shouldAutoStart;
2367
+ if (shouldAutoStart) {
2368
+ console.log(chalk.green("\u2713 Happy will start the background service automatically"));
2369
+ console.log(chalk.gray(" The service will run whenever you use the happy command"));
2370
+ } else {
2371
+ console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
2372
+ }
2373
+ await writeSettings(settings);
2374
+ }
2375
+ if (settings.daemonAutoStartWhenRunningHappy) {
2376
+ console.log("Starting Happy background service...");
2377
+ if (!await isDaemonRunning()) {
2378
+ const happyPath = process.argv[1];
2379
+ const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
2380
+ const daemonArgs = ["daemon", "start"];
2381
+ if (serverUrl) {
2382
+ daemonArgs.push("--happy-server-url", serverUrl);
2383
+ }
2384
+ if (installationLocation === "local") {
2385
+ daemonArgs.push("--local");
2386
+ }
2387
+ const daemonProcess = isBuiltBinary ? spawn$1(happyPath, daemonArgs, {
2388
+ detached: true,
2389
+ stdio: ["ignore", "inherit", "inherit"],
2390
+ // Show stdout/stderr for debugging
2391
+ env: {
2392
+ ...process.env,
2393
+ HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
2394
+ // Pass through server URL
2395
+ HANDY_LOCAL: process.env.HANDY_LOCAL
2396
+ // Pass through local flag
2397
+ }
2398
+ }) : spawn$1("npx", ["tsx", happyPath, ...daemonArgs], {
2399
+ detached: true,
2400
+ stdio: ["ignore", "inherit", "inherit"],
2401
+ // Show stdout/stderr for debugging
2402
+ env: {
2403
+ ...process.env,
2404
+ HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
2405
+ // Pass through server URL
2406
+ HANDY_LOCAL: process.env.HANDY_LOCAL
2407
+ // Pass through local flag
2408
+ }
2409
+ });
2410
+ daemonProcess.unref();
2411
+ await new Promise((resolve) => setTimeout(resolve, 200));
2412
+ }
2413
+ }
2271
2414
  try {
2272
2415
  await start(credentials, options);
2273
2416
  } catch (error) {