happy-coder 0.1.13 → 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.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var types = require('./types-hotUTaWz.cjs');
4
+ var types = require('./types-Cg4664gs.cjs');
5
5
  var node_crypto = require('node:crypto');
6
6
  var claudeCode = require('@anthropic-ai/claude-code');
7
7
  var node_fs = require('node:fs');
@@ -21,7 +21,6 @@ var util = require('util');
21
21
  var crypto = require('crypto');
22
22
  var path = require('path');
23
23
  var url = require('url');
24
- var httpProxy = require('http-proxy');
25
24
  var tweetnacl = require('tweetnacl');
26
25
  var axios = require('axios');
27
26
  var qrcode = require('qrcode-terminal');
@@ -261,18 +260,32 @@ async function claudeRemote(opts) {
261
260
  });
262
261
  }
263
262
  printDivider();
263
+ let thinking = false;
264
+ const updateThinking = (newThinking) => {
265
+ if (thinking !== newThinking) {
266
+ thinking = newThinking;
267
+ types.logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
268
+ if (opts.onThinkingChange) {
269
+ opts.onThinkingChange(thinking);
270
+ }
271
+ }
272
+ };
264
273
  try {
265
274
  types.logger.debug(`[claudeRemote] Starting to iterate over response`);
266
275
  for await (const message of response) {
267
276
  types.logger.debugLargeJson(`[claudeRemote] Message ${message.type}`, message);
268
277
  formatClaudeMessage(message, opts.onAssistantResult);
269
278
  if (message.type === "system" && message.subtype === "init") {
279
+ updateThinking(true);
270
280
  types.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${message.session_id}`);
271
281
  const projectDir = getProjectPath(opts.path);
272
282
  const found = await awaitFileExist(node_path.join(projectDir, `${message.session_id}.jsonl`));
273
283
  types.logger.debug(`[claudeRemote] Session file found: ${message.session_id} ${found}`);
274
284
  opts.onSessionFound(message.session_id);
275
285
  }
286
+ if (message.type === "result") {
287
+ updateThinking(false);
288
+ }
276
289
  }
277
290
  types.logger.debug(`[claudeRemote] Finished iterating over response`);
278
291
  } catch (e) {
@@ -285,6 +298,7 @@ async function claudeRemote(opts) {
285
298
  throw e;
286
299
  }
287
300
  } finally {
301
+ updateThinking(false);
288
302
  if (opts.interruptController) {
289
303
  opts.interruptController.unregister();
290
304
  }
@@ -348,22 +362,70 @@ async function claudeLocal(opts) {
348
362
  input: child.stdio[3],
349
363
  crlfDelay: Infinity
350
364
  });
351
- rl.on("line", (line) => {
352
- 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);
353
- if (sessionMatch) {
354
- detectedIdsRandomUUID.add(sessionMatch[0]);
355
- if (resolvedSessionId) {
356
- return;
365
+ const activeFetches = /* @__PURE__ */ new Map();
366
+ let thinking = false;
367
+ let stopThinkingTimeout = null;
368
+ const updateThinking = (newThinking) => {
369
+ if (thinking !== newThinking) {
370
+ thinking = newThinking;
371
+ types.logger.debug(`[ClaudeLocal] Thinking state changed to: ${thinking}`);
372
+ if (opts.onThinkingChange) {
373
+ opts.onThinkingChange(thinking);
357
374
  }
358
- if (detectedIdsFileSystem.has(sessionMatch[0])) {
359
- resolvedSessionId = sessionMatch[0];
360
- opts.onSessionFound(sessionMatch[0]);
375
+ }
376
+ };
377
+ rl.on("line", (line) => {
378
+ try {
379
+ const message = JSON.parse(line);
380
+ switch (message.type) {
381
+ case "uuid":
382
+ detectedIdsRandomUUID.add(message.value);
383
+ if (!resolvedSessionId && detectedIdsFileSystem.has(message.value)) {
384
+ resolvedSessionId = message.value;
385
+ opts.onSessionFound(message.value);
386
+ }
387
+ break;
388
+ case "fetch-start":
389
+ types.logger.debug(`[ClaudeLocal] Fetch start: ${message.method} ${message.hostname}${message.path} (id: ${message.id})`);
390
+ activeFetches.set(message.id, {
391
+ hostname: message.hostname,
392
+ path: message.path,
393
+ startTime: message.timestamp
394
+ });
395
+ if (stopThinkingTimeout) {
396
+ clearTimeout(stopThinkingTimeout);
397
+ stopThinkingTimeout = null;
398
+ }
399
+ updateThinking(true);
400
+ break;
401
+ case "fetch-end":
402
+ types.logger.debug(`[ClaudeLocal] Fetch end: id ${message.id}`);
403
+ activeFetches.delete(message.id);
404
+ if (activeFetches.size === 0 && thinking && !stopThinkingTimeout) {
405
+ stopThinkingTimeout = setTimeout(() => {
406
+ if (activeFetches.size === 0) {
407
+ updateThinking(false);
408
+ }
409
+ stopThinkingTimeout = null;
410
+ }, 500);
411
+ }
412
+ break;
413
+ default:
414
+ types.logger.debug(`[ClaudeLocal] Unknown message type: ${message.type}`);
361
415
  }
416
+ } catch (e) {
417
+ types.logger.debug(`[ClaudeLocal] Non-JSON line from fd3: ${line}`);
362
418
  }
363
419
  });
364
420
  rl.on("error", (err) => {
365
421
  console.error("Error reading from fd 3:", err);
366
422
  });
423
+ child.on("exit", () => {
424
+ if (stopThinkingTimeout) {
425
+ clearTimeout(stopThinkingTimeout);
426
+ }
427
+ updateThinking(false);
428
+ });
367
429
  }
368
430
  child.on("error", (error) => {
369
431
  });
@@ -819,6 +881,7 @@ async function loop(opts) {
819
881
  path: opts.path,
820
882
  sessionId,
821
883
  onSessionFound,
884
+ onThinkingChange: opts.onThinkingChange,
822
885
  abort: interactiveAbortController.signal,
823
886
  claudeEnvVars: opts.claudeEnvVars,
824
887
  claudeArgs: opts.claudeArgs
@@ -881,6 +944,7 @@ async function loop(opts) {
881
944
  mcpServers: opts.mcpServers,
882
945
  permissionPromptToolName: opts.permissionPromptToolName,
883
946
  onSessionFound,
947
+ onThinkingChange: opts.onThinkingChange,
884
948
  messages: currentMessageQueue,
885
949
  onAssistantResult: opts.onAssistantResult,
886
950
  interruptController: opts.interruptController,
@@ -1009,7 +1073,7 @@ class InterruptController {
1009
1073
  }
1010
1074
  }
1011
1075
 
1012
- var version = "0.1.13";
1076
+ var version = "0.1.14";
1013
1077
  var packageJson = {
1014
1078
  version: version};
1015
1079
 
@@ -1294,148 +1358,77 @@ function registerHandlers(session, interruptController, permissionCallbacks, onS
1294
1358
  });
1295
1359
  }
1296
1360
 
1297
- async function startHTTPDirectProxy(options) {
1298
- const proxy = httpProxy.createProxyServer({
1299
- target: options.target,
1300
- changeOrigin: true,
1301
- secure: false
1302
- });
1303
- proxy.on("error", (err, req, res) => {
1304
- types.logger.debug(`[HTTPProxy] Proxy error: ${err.message} for ${req.method} ${req.url}`);
1305
- if (res instanceof node_http.ServerResponse && !res.headersSent) {
1306
- res.writeHead(500, { "Content-Type": "text/plain" });
1307
- res.end("Proxy error");
1308
- }
1309
- });
1310
- proxy.on("proxyReq", (proxyReq, req, res) => {
1311
- if (options.onRequest) {
1312
- options.onRequest(req, proxyReq);
1313
- }
1314
- });
1315
- proxy.on("proxyRes", (proxyRes, req, res) => {
1316
- if (options.onResponse) {
1317
- options.onResponse(req, proxyRes);
1318
- }
1319
- });
1320
- const server = node_http.createServer((req, res) => {
1321
- proxy.web(req, res);
1322
- });
1323
- const url = await new Promise((resolve, reject) => {
1324
- server.listen(0, "127.0.0.1", () => {
1325
- const addr = server.address();
1326
- if (addr && typeof addr === "object") {
1327
- const proxyUrl = `http://127.0.0.1:${addr.port}`;
1328
- types.logger.debug(`[HTTPProxy] Started on ${proxyUrl} --> ${options.target}`);
1329
- resolve(proxyUrl);
1330
- } else {
1331
- reject(new Error("Failed to get server address"));
1332
- }
1333
- });
1334
- });
1335
- return url;
1361
+ const defaultSettings = {
1362
+ onboardingCompleted: false
1363
+ };
1364
+ async function readSettings() {
1365
+ if (!node_fs.existsSync(types.configuration.settingsFile)) {
1366
+ return { ...defaultSettings };
1367
+ }
1368
+ try {
1369
+ const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
1370
+ return JSON.parse(content);
1371
+ } catch {
1372
+ return { ...defaultSettings };
1373
+ }
1336
1374
  }
1337
-
1338
- async function startClaudeActivityTracker(onThinking) {
1339
- types.logger.debug(`[ClaudeActivityTracker] Starting activity tracker`);
1340
- let requestCounter = 0;
1341
- const activeRequests = /* @__PURE__ */ new Map();
1342
- let stopThinkingTimeout = null;
1343
- let isThinking = false;
1344
- const REQUEST_TIMEOUT = 5 * 60 * 1e3;
1345
- const checkAndStopThinking = () => {
1346
- if (activeRequests.size === 0 && isThinking && !stopThinkingTimeout) {
1347
- stopThinkingTimeout = setTimeout(() => {
1348
- if (isThinking && activeRequests.size === 0) {
1349
- isThinking = false;
1350
- onThinking(false);
1351
- }
1352
- stopThinkingTimeout = null;
1353
- }, 500);
1354
- }
1355
- };
1356
- const proxyUrl = await startHTTPDirectProxy({
1357
- target: process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1358
- onRequest: (req, proxyReq) => {
1359
- if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1360
- const requestId = ++requestCounter;
1361
- req._requestId = requestId;
1362
- if (stopThinkingTimeout) {
1363
- clearTimeout(stopThinkingTimeout);
1364
- stopThinkingTimeout = null;
1365
- }
1366
- const timeout = setTimeout(() => {
1367
- activeRequests.delete(requestId);
1368
- checkAndStopThinking();
1369
- }, REQUEST_TIMEOUT);
1370
- activeRequests.set(requestId, timeout);
1371
- if (!isThinking) {
1372
- isThinking = true;
1373
- onThinking(true);
1374
- }
1375
- }
1376
- },
1377
- onResponse: (req, proxyRes) => {
1378
- proxyRes.headers["connection"] = "close";
1379
- if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
1380
- const requestId = req._requestId;
1381
- const timeout = activeRequests.get(requestId);
1382
- if (timeout) {
1383
- clearTimeout(timeout);
1384
- }
1385
- let cleaned = false;
1386
- const cleanupRequest = () => {
1387
- if (!cleaned) {
1388
- cleaned = true;
1389
- activeRequests.delete(requestId);
1390
- checkAndStopThinking();
1391
- }
1392
- };
1393
- proxyRes.on("end", () => {
1394
- cleanupRequest();
1395
- });
1396
- proxyRes.on("error", (err) => {
1397
- cleanupRequest();
1398
- });
1399
- proxyRes.on("aborted", () => {
1400
- cleanupRequest();
1401
- });
1402
- proxyRes.on("close", () => {
1403
- cleanupRequest();
1404
- });
1405
- req.on("close", () => {
1406
- cleanupRequest();
1407
- });
1408
- }
1409
- }
1410
- });
1411
- const reset = () => {
1412
- for (const [requestId, timeout] of activeRequests) {
1413
- clearTimeout(timeout);
1414
- }
1415
- activeRequests.clear();
1416
- if (stopThinkingTimeout) {
1417
- clearTimeout(stopThinkingTimeout);
1418
- stopThinkingTimeout = null;
1419
- }
1420
- if (isThinking) {
1421
- isThinking = false;
1422
- onThinking(false);
1423
- }
1424
- };
1425
- return {
1426
- proxyUrl,
1427
- reset
1428
- };
1375
+ async function writeSettings(settings) {
1376
+ if (!node_fs.existsSync(types.configuration.happyDir)) {
1377
+ await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
1378
+ }
1379
+ await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
1380
+ }
1381
+ const credentialsSchema = z__namespace.object({
1382
+ secret: z__namespace.string().base64(),
1383
+ token: z__namespace.string()
1384
+ });
1385
+ async function readCredentials() {
1386
+ if (!node_fs.existsSync(types.configuration.privateKeyFile)) {
1387
+ return null;
1388
+ }
1389
+ try {
1390
+ const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
1391
+ const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
1392
+ return {
1393
+ secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
1394
+ token: credentials.token
1395
+ };
1396
+ } catch {
1397
+ return null;
1398
+ }
1399
+ }
1400
+ async function writeCredentials(credentials) {
1401
+ if (!node_fs.existsSync(types.configuration.happyDir)) {
1402
+ await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
1403
+ }
1404
+ await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
1405
+ secret: types.encodeBase64(credentials.secret),
1406
+ token: credentials.token
1407
+ }, null, 2));
1429
1408
  }
1430
1409
 
1431
1410
  async function start(credentials, options = {}) {
1432
1411
  const workingDirectory = process.cwd();
1433
1412
  const sessionTag = node_crypto.randomUUID();
1413
+ if (options.daemonSpawn && options.startingMode === "local") {
1414
+ types.logger.debug("Daemon spawn requested with local mode - forcing remote mode");
1415
+ options.startingMode = "remote";
1416
+ }
1434
1417
  const api = new types.ApiClient(credentials.token, credentials.secret);
1435
1418
  let state = {};
1436
- let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
1419
+ const settings = await readSettings() || { };
1420
+ let metadata = {
1421
+ path: workingDirectory,
1422
+ host: os.hostname(),
1423
+ version: packageJson.version,
1424
+ os: os.platform(),
1425
+ machineId: settings.machineId
1426
+ };
1437
1427
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1438
1428
  types.logger.debug(`Session created: ${response.id}`);
1429
+ if (options.daemonSpawn) {
1430
+ console.log(`daemon:sessionIdCreated:${response.id}`);
1431
+ }
1439
1432
  const session = api.session(response);
1440
1433
  const pushClient = api.push();
1441
1434
  let thinking = false;
@@ -1443,11 +1436,6 @@ async function start(credentials, options = {}) {
1443
1436
  let pingInterval = setInterval(() => {
1444
1437
  session.keepAlive(thinking, mode);
1445
1438
  }, 2e3);
1446
- const activityTracker = await startClaudeActivityTracker((newThinking) => {
1447
- thinking = newThinking;
1448
- session.keepAlive(thinking, mode);
1449
- });
1450
- process.env.ANTHROPIC_BASE_URL = activityTracker.proxyUrl;
1451
1439
  const logPath = await types.logger.logFilePathPromise;
1452
1440
  types.logger.infoDeveloper(`Session: ${response.id}`);
1453
1441
  types.logger.infoDeveloper(`Logs: ${logPath}`);
@@ -1579,7 +1567,6 @@ async function start(credentials, options = {}) {
1579
1567
  },
1580
1568
  onProcessStart: (processMode) => {
1581
1569
  types.logger.debug(`[Process Lifecycle] Starting ${processMode} mode`);
1582
- activityTracker.reset();
1583
1570
  types.logger.debug("Starting process - clearing any stale permission requests");
1584
1571
  for (const [id, resolve] of requests) {
1585
1572
  types.logger.debug(`Rejecting stale permission request: ${id}`);
@@ -1589,13 +1576,14 @@ async function start(credentials, options = {}) {
1589
1576
  },
1590
1577
  onProcessStop: (processMode) => {
1591
1578
  types.logger.debug(`[Process Lifecycle] Stopped ${processMode} mode`);
1592
- activityTracker.reset();
1593
1579
  types.logger.debug("Stopping process - clearing any stale permission requests");
1594
1580
  for (const [id, resolve] of requests) {
1595
1581
  types.logger.debug(`Rejecting stale permission request: ${id}`);
1596
1582
  resolve({ approved: false, reason: "Process restarted" });
1597
1583
  }
1598
1584
  requests.clear();
1585
+ thinking = false;
1586
+ session.keepAlive(thinking, mode);
1599
1587
  },
1600
1588
  mcpServers: {
1601
1589
  "permission": {
@@ -1608,61 +1596,16 @@ async function start(credentials, options = {}) {
1608
1596
  onAssistantResult,
1609
1597
  interruptController,
1610
1598
  claudeEnvVars: options.claudeEnvVars,
1611
- claudeArgs: options.claudeArgs
1599
+ claudeArgs: options.claudeArgs,
1600
+ onThinkingChange: (newThinking) => {
1601
+ thinking = newThinking;
1602
+ session.keepAlive(thinking, mode);
1603
+ }
1612
1604
  });
1613
1605
  clearInterval(pingInterval);
1614
1606
  process.exit(0);
1615
1607
  }
1616
1608
 
1617
- const defaultSettings = {
1618
- onboardingCompleted: false
1619
- };
1620
- async function readSettings() {
1621
- if (!node_fs.existsSync(types.configuration.settingsFile)) {
1622
- return { ...defaultSettings };
1623
- }
1624
- try {
1625
- const content = await promises$1.readFile(types.configuration.settingsFile, "utf8");
1626
- return JSON.parse(content);
1627
- } catch {
1628
- return { ...defaultSettings };
1629
- }
1630
- }
1631
- async function writeSettings(settings) {
1632
- if (!node_fs.existsSync(types.configuration.happyDir)) {
1633
- await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
1634
- }
1635
- await promises$1.writeFile(types.configuration.settingsFile, JSON.stringify(settings, null, 2));
1636
- }
1637
- const credentialsSchema = z__namespace.object({
1638
- secret: z__namespace.string().base64(),
1639
- token: z__namespace.string()
1640
- });
1641
- async function readCredentials() {
1642
- if (!node_fs.existsSync(types.configuration.privateKeyFile)) {
1643
- return null;
1644
- }
1645
- try {
1646
- const keyBase64 = await promises$1.readFile(types.configuration.privateKeyFile, "utf8");
1647
- const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
1648
- return {
1649
- secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
1650
- token: credentials.token
1651
- };
1652
- } catch {
1653
- return null;
1654
- }
1655
- }
1656
- async function writeCredentials(credentials) {
1657
- if (!node_fs.existsSync(types.configuration.happyDir)) {
1658
- await promises$1.mkdir(types.configuration.happyDir, { recursive: true });
1659
- }
1660
- await promises$1.writeFile(types.configuration.privateKeyFile, JSON.stringify({
1661
- secret: types.encodeBase64(credentials.secret),
1662
- token: credentials.token
1663
- }, null, 2));
1664
- }
1665
-
1666
1609
  function displayQRCode(url) {
1667
1610
  console.log("=".repeat(80));
1668
1611
  console.log("\u{1F4F1} To authenticate, scan this QR code with your mobile device:");
@@ -1690,10 +1633,8 @@ async function doAuth() {
1690
1633
  console.log("Please, authenticate using mobile app");
1691
1634
  const authUrl = "happy://terminal?" + types.encodeBase64Url(keypair.publicKey);
1692
1635
  displayQRCode(authUrl);
1693
- if (process.env.DEBUG === "1") {
1694
- console.log("\n\u{1F4CB} For manual entry, copy this URL:");
1695
- console.log(authUrl);
1696
- }
1636
+ console.log("\n\u{1F4CB} For manual entry, copy this URL:");
1637
+ console.log(authUrl);
1697
1638
  let credentials = null;
1698
1639
  while (true) {
1699
1640
  try {
@@ -1741,18 +1682,20 @@ class ApiDaemonSession extends node_events.EventEmitter {
1741
1682
  keepAliveInterval = null;
1742
1683
  token;
1743
1684
  secret;
1685
+ spawnedProcesses = /* @__PURE__ */ new Set();
1744
1686
  constructor(token, secret, machineIdentity) {
1745
1687
  super();
1746
1688
  this.token = token;
1747
1689
  this.secret = secret;
1748
1690
  this.machineIdentity = machineIdentity;
1691
+ types.logger.daemonDebug(`Connecting to server: ${types.configuration.serverUrl}`);
1749
1692
  const socket = socket_ioClient.io(types.configuration.serverUrl, {
1750
1693
  auth: {
1751
1694
  token: this.token,
1752
1695
  clientType: "machine-scoped",
1753
1696
  machineId: this.machineIdentity.machineId
1754
1697
  },
1755
- path: "/v1/user-machine-daemon",
1698
+ path: "/v1/updates",
1756
1699
  reconnection: true,
1757
1700
  reconnectionAttempts: Infinity,
1758
1701
  reconnectionDelay: 1e3,
@@ -1762,68 +1705,146 @@ class ApiDaemonSession extends node_events.EventEmitter {
1762
1705
  autoConnect: false
1763
1706
  });
1764
1707
  socket.on("connect", () => {
1765
- types.logger.debug("[DAEMON] Connected to server");
1708
+ types.logger.daemonDebug("Socket connected");
1709
+ types.logger.daemonDebug(`Connected with auth - token: ${this.token.substring(0, 10)}..., machineId: ${this.machineIdentity.machineId}`);
1710
+ const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
1711
+ socket.emit("rpc-register", { method: rpcMethod });
1712
+ types.logger.daemonDebug(`Emitted RPC registration: ${rpcMethod}`);
1766
1713
  this.emit("connected");
1767
- socket.emit("machine-connect", {
1768
- token: this.token,
1769
- machineIdentity: types.encodeBase64(types.encrypt(this.machineIdentity, this.secret))
1770
- });
1771
1714
  this.startKeepAlive();
1772
1715
  });
1773
- socket.on("disconnect", () => {
1774
- types.logger.debug("[DAEMON] Disconnected from server");
1775
- this.emit("disconnected");
1776
- this.stopKeepAlive();
1777
- });
1778
- socket.on("spawn-session", async (encryptedData, callback) => {
1779
- let requestData;
1780
- try {
1781
- requestData = types.decrypt(types.decodeBase64(encryptedData), this.secret);
1782
- types.logger.debug("[DAEMON] Received spawn-session request", requestData);
1783
- const args = [
1784
- "--directory",
1785
- requestData.directory,
1786
- "--happy-starting-mode",
1787
- requestData.startingMode
1788
- ];
1789
- if (requestData.metadata) {
1790
- args.push("--metadata", requestData.metadata);
1791
- }
1792
- if (requestData.startingMode === "interactive" && process.platform === "darwin") {
1793
- const script = `
1794
- tell application "Terminal"
1795
- activate
1796
- do script "cd ${requestData.directory} && happy ${args.join(" ")}"
1797
- end tell
1798
- `;
1799
- child_process.spawn("osascript", ["-e", script], { detached: true });
1800
- } else {
1801
- const child = child_process.spawn("happy", args, {
1716
+ socket.on("rpc-request", async (data, callback) => {
1717
+ types.logger.daemonDebug(`Received RPC request: ${JSON.stringify(data)}`);
1718
+ const expectedMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
1719
+ if (data.method === expectedMethod) {
1720
+ types.logger.daemonDebug("Processing spawn-happy-session RPC");
1721
+ try {
1722
+ const { directory } = data.params || {};
1723
+ if (!directory) {
1724
+ throw new Error("Directory is required");
1725
+ }
1726
+ const args = [
1727
+ "--daemon-spawn",
1728
+ "--happy-starting-mode",
1729
+ "remote"
1730
+ // ALWAYS force remote mode for daemon spawns
1731
+ ];
1732
+ if (types.configuration.installationLocation === "local") {
1733
+ args.push("--local");
1734
+ }
1735
+ if (types.configuration.serverUrl !== "https://handy-api.korshakov.org") {
1736
+ args.push("--happy-server-url", types.configuration.serverUrl);
1737
+ }
1738
+ types.logger.daemonDebug(`Spawning happy in directory: ${directory} with args: ${args.join(" ")}`);
1739
+ const happyPath = process.argv[1];
1740
+ const isTypeScript = happyPath.endsWith(".ts");
1741
+ const happyProcess = isTypeScript ? child_process.spawn("npx", ["tsx", happyPath, ...args], {
1742
+ cwd: directory,
1743
+ env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
1744
+ detached: true,
1745
+ stdio: ["ignore", "pipe", "pipe"]
1746
+ // We need stdout
1747
+ }) : child_process.spawn(process.argv[0], [happyPath, ...args], {
1748
+ cwd: directory,
1749
+ env: { ...process.env, EXPERIMENTAL_FEATURES: "1" },
1802
1750
  detached: true,
1803
- stdio: "ignore",
1804
- cwd: requestData.directory
1751
+ stdio: ["ignore", "pipe", "pipe"]
1752
+ // We need stdout
1753
+ });
1754
+ this.spawnedProcesses.add(happyProcess);
1755
+ let sessionId = null;
1756
+ let output = "";
1757
+ let timeoutId = null;
1758
+ const cleanup = () => {
1759
+ happyProcess.stdout.removeAllListeners("data");
1760
+ happyProcess.stderr.removeAllListeners("data");
1761
+ happyProcess.removeAllListeners("error");
1762
+ happyProcess.removeAllListeners("exit");
1763
+ if (timeoutId) {
1764
+ clearTimeout(timeoutId);
1765
+ timeoutId = null;
1766
+ }
1767
+ };
1768
+ happyProcess.stdout.on("data", (data2) => {
1769
+ output += data2.toString();
1770
+ const match = output.match(/daemon:sessionIdCreated:(.+?)[\n\r]/);
1771
+ if (match && !sessionId) {
1772
+ sessionId = match[1];
1773
+ types.logger.daemonDebug(`Session spawned successfully: ${sessionId}`);
1774
+ callback({ sessionId });
1775
+ cleanup();
1776
+ happyProcess.unref();
1777
+ }
1778
+ });
1779
+ happyProcess.stderr.on("data", (data2) => {
1780
+ types.logger.daemonDebug(`Spawned process stderr: ${data2.toString()}`);
1781
+ });
1782
+ happyProcess.on("error", (error) => {
1783
+ types.logger.daemonDebug("Error spawning session:", error);
1784
+ if (!sessionId) {
1785
+ callback({ error: `Failed to spawn: ${error.message}` });
1786
+ cleanup();
1787
+ this.spawnedProcesses.delete(happyProcess);
1788
+ }
1805
1789
  });
1806
- child.unref();
1790
+ happyProcess.on("exit", (code, signal) => {
1791
+ types.logger.daemonDebug(`Spawned process exited with code ${code}, signal ${signal}`);
1792
+ this.spawnedProcesses.delete(happyProcess);
1793
+ if (!sessionId) {
1794
+ callback({ error: `Process exited before session ID received` });
1795
+ cleanup();
1796
+ }
1797
+ });
1798
+ timeoutId = setTimeout(() => {
1799
+ if (!sessionId) {
1800
+ types.logger.daemonDebug("Timeout waiting for session ID");
1801
+ callback({ error: "Timeout waiting for session" });
1802
+ cleanup();
1803
+ happyProcess.kill();
1804
+ this.spawnedProcesses.delete(happyProcess);
1805
+ }
1806
+ }, 1e4);
1807
+ } catch (error) {
1808
+ types.logger.daemonDebug("Error spawning session:", error);
1809
+ callback({ error: error instanceof Error ? error.message : "Unknown error" });
1807
1810
  }
1808
- const result = { success: true };
1809
- socket.emit("session-spawn-result", {
1810
- requestId: requestData.requestId,
1811
- result: types.encodeBase64(types.encrypt(result, this.secret))
1812
- });
1813
- callback(types.encodeBase64(types.encrypt({ success: true }, this.secret)));
1814
- } catch (error) {
1815
- types.logger.debug("[DAEMON] Failed to spawn session", error);
1816
- const errorResult = {
1817
- success: false,
1818
- error: error instanceof Error ? error.message : "Unknown error"
1819
- };
1820
- socket.emit("session-spawn-result", {
1821
- requestId: requestData?.requestId || "",
1822
- result: types.encodeBase64(types.encrypt(errorResult, this.secret))
1823
- });
1824
- callback(types.encodeBase64(types.encrypt(errorResult, this.secret)));
1811
+ } else {
1812
+ types.logger.daemonDebug(`Unknown RPC method: ${data.method}`);
1813
+ callback({ error: `Unknown method: ${data.method}` });
1814
+ }
1815
+ });
1816
+ socket.on("disconnect", (reason) => {
1817
+ types.logger.daemonDebug(`Disconnected from server. Reason: ${reason}`);
1818
+ this.emit("disconnected");
1819
+ this.stopKeepAlive();
1820
+ });
1821
+ socket.on("reconnect", () => {
1822
+ types.logger.daemonDebug("Reconnected to server");
1823
+ const rpcMethod = `${this.machineIdentity.machineId}:spawn-happy-session`;
1824
+ socket.emit("rpc-register", { method: rpcMethod });
1825
+ types.logger.daemonDebug(`Re-registered RPC method: ${rpcMethod}`);
1826
+ });
1827
+ socket.on("rpc-registered", (data) => {
1828
+ types.logger.daemonDebug(`RPC registration confirmed: ${data.method}`);
1829
+ });
1830
+ socket.on("rpc-unregistered", (data) => {
1831
+ types.logger.daemonDebug(`RPC unregistered: ${data.method}`);
1832
+ });
1833
+ socket.on("rpc-error", (data) => {
1834
+ types.logger.daemonDebug(`RPC error: ${JSON.stringify(data)}`);
1835
+ });
1836
+ socket.onAny((event, ...args) => {
1837
+ if (!event.startsWith("machine-alive")) {
1838
+ types.logger.daemonDebug(`Socket event: ${event}, args: ${JSON.stringify(args)}`);
1825
1839
  }
1826
1840
  });
1841
+ socket.on("connect_error", (error) => {
1842
+ types.logger.daemonDebug(`Connection error: ${error.message}`);
1843
+ types.logger.daemonDebug(`Error: ${JSON.stringify(error, null, 2)}`);
1844
+ });
1845
+ socket.on("error", (error) => {
1846
+ types.logger.daemonDebug(`Socket error: ${error}`);
1847
+ });
1827
1848
  socket.on("daemon-command", (data) => {
1828
1849
  switch (data.command) {
1829
1850
  case "shutdown":
@@ -1839,6 +1860,7 @@ class ApiDaemonSession extends node_events.EventEmitter {
1839
1860
  startKeepAlive() {
1840
1861
  this.stopKeepAlive();
1841
1862
  this.keepAliveInterval = setInterval(() => {
1863
+ types.logger.daemonDebug("Sending keep-alive ping");
1842
1864
  this.socket.volatile.emit("machine-alive", {
1843
1865
  time: Date.now()
1844
1866
  });
@@ -1854,22 +1876,42 @@ class ApiDaemonSession extends node_events.EventEmitter {
1854
1876
  this.socket.connect();
1855
1877
  }
1856
1878
  shutdown() {
1879
+ types.logger.daemonDebug(`Shutting down daemon, killing ${this.spawnedProcesses.size} spawned processes`);
1880
+ for (const process2 of this.spawnedProcesses) {
1881
+ try {
1882
+ types.logger.daemonDebug(`Killing spawned process with PID: ${process2.pid}`);
1883
+ process2.kill("SIGTERM");
1884
+ setTimeout(() => {
1885
+ try {
1886
+ process2.kill("SIGKILL");
1887
+ } catch (e) {
1888
+ }
1889
+ }, 1e3);
1890
+ } catch (error) {
1891
+ types.logger.daemonDebug(`Error killing process: ${error}`);
1892
+ }
1893
+ }
1894
+ this.spawnedProcesses.clear();
1857
1895
  this.stopKeepAlive();
1858
1896
  this.socket.close();
1859
1897
  this.emit("shutdown");
1860
1898
  }
1861
1899
  }
1862
1900
 
1901
+ let pidFileFd = null;
1863
1902
  async function startDaemon() {
1864
- console.log("[DAEMON] Starting daemon process...");
1903
+ if (process.platform !== "darwin") {
1904
+ console.error("ERROR: Daemon is only supported on macOS");
1905
+ process.exit(1);
1906
+ }
1907
+ types.logger.daemonDebug("Starting daemon process...");
1908
+ types.logger.daemonDebug(`Server URL: ${types.configuration.serverUrl}`);
1865
1909
  if (await isDaemonRunning()) {
1866
- console.log("Happy daemon is already running");
1910
+ types.logger.daemonDebug("Happy daemon is already running");
1867
1911
  process.exit(0);
1868
1912
  }
1869
- console.log("[DAEMON] Writing PID file with PID:", process.pid);
1870
- writePidFile();
1871
- console.log("[DAEMON] PID file written successfully");
1872
- types.logger.info("Happy CLI daemon started successfully");
1913
+ pidFileFd = writePidFile();
1914
+ types.logger.daemonDebug("PID file written");
1873
1915
  process.on("SIGINT", () => {
1874
1916
  stopDaemon().catch(console.error);
1875
1917
  });
@@ -1894,7 +1936,7 @@ async function startDaemon() {
1894
1936
  };
1895
1937
  let credentials = await readCredentials();
1896
1938
  if (!credentials) {
1897
- types.logger.debug("[DAEMON] No credentials found, running auth");
1939
+ types.logger.daemonDebug("No credentials found, running auth");
1898
1940
  await doAuth();
1899
1941
  credentials = await readCredentials();
1900
1942
  if (!credentials) {
@@ -1902,64 +1944,64 @@ async function startDaemon() {
1902
1944
  }
1903
1945
  }
1904
1946
  const { token, secret } = credentials;
1905
- const daemon = new ApiDaemonSession(token, secret, machineIdentity);
1947
+ const daemon = new ApiDaemonSession(
1948
+ token,
1949
+ secret,
1950
+ machineIdentity
1951
+ );
1906
1952
  daemon.on("connected", () => {
1907
- types.logger.debug("[DAEMON] Successfully connected to server");
1953
+ types.logger.daemonDebug("Connected to server event received");
1908
1954
  });
1909
1955
  daemon.on("disconnected", () => {
1910
- types.logger.debug("[DAEMON] Disconnected from server");
1956
+ types.logger.daemonDebug("Disconnected from server event received");
1911
1957
  });
1912
1958
  daemon.on("shutdown", () => {
1913
- types.logger.debug("[DAEMON] Shutdown requested");
1959
+ types.logger.daemonDebug("Shutdown requested");
1914
1960
  stopDaemon();
1915
1961
  process.exit(0);
1916
1962
  });
1917
1963
  daemon.connect();
1918
- setInterval(() => {
1919
- }, 1e3);
1964
+ types.logger.daemonDebug("Daemon started successfully");
1920
1965
  } catch (error) {
1921
- types.logger.debug("[DAEMON] Failed to start daemon", error);
1966
+ types.logger.daemonDebug("Failed to start daemon", error);
1922
1967
  stopDaemon();
1923
1968
  process.exit(1);
1924
1969
  }
1925
- process.on("SIGINT", () => process.exit(0));
1926
- process.on("SIGTERM", () => process.exit(0));
1927
- process.on("exit", () => process.exit(0));
1928
1970
  while (true) {
1929
1971
  await new Promise((resolve) => setTimeout(resolve, 1e3));
1930
1972
  }
1931
1973
  }
1932
1974
  async function isDaemonRunning() {
1933
1975
  try {
1934
- console.log("[isDaemonRunning] Checking if daemon is running...");
1976
+ types.logger.daemonDebug("[isDaemonRunning] Checking if daemon is running...");
1935
1977
  if (fs.existsSync(types.configuration.daemonPidFile)) {
1936
- console.log("[isDaemonRunning] PID file exists");
1978
+ types.logger.daemonDebug("[isDaemonRunning] PID file exists");
1937
1979
  const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
1938
- console.log("[isDaemonRunning] PID from file:", pid);
1980
+ types.logger.daemonDebug("[isDaemonRunning] PID from file:", pid);
1939
1981
  try {
1940
1982
  process.kill(pid, 0);
1941
- console.log("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
1983
+ types.logger.daemonDebug("[isDaemonRunning] Process exists, checking if it's a happy daemon...");
1942
1984
  const isHappyDaemon = await isProcessHappyDaemon(pid);
1943
- console.log("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
1985
+ types.logger.daemonDebug("[isDaemonRunning] isHappyDaemon:", isHappyDaemon);
1944
1986
  if (isHappyDaemon) {
1945
1987
  return true;
1946
1988
  } else {
1947
- console.log("[isDaemonRunning] PID is not a happy daemon, cleaning up");
1948
- types.logger.debug(`[DAEMON] PID ${pid} is not a happy daemon, cleaning up`);
1989
+ types.logger.daemonDebug("[isDaemonRunning] PID is not a happy daemon, cleaning up");
1990
+ types.logger.debug(`PID ${pid} is not a happy daemon, cleaning up`);
1949
1991
  fs.unlinkSync(types.configuration.daemonPidFile);
1950
1992
  }
1951
1993
  } catch (error) {
1952
- console.log("[isDaemonRunning] Process not running, cleaning up stale PID file");
1953
- types.logger.debug("[DAEMON] Process not running, cleaning up stale PID file");
1994
+ types.logger.daemonDebug("[isDaemonRunning] Process not running, cleaning up stale PID file");
1995
+ types.logger.debug("Process not running, cleaning up stale PID file");
1954
1996
  fs.unlinkSync(types.configuration.daemonPidFile);
1955
1997
  }
1956
1998
  } else {
1957
- console.log("[isDaemonRunning] No PID file found");
1999
+ types.logger.daemonDebug("[isDaemonRunning] No PID file found");
1958
2000
  }
1959
2001
  return false;
1960
2002
  } catch (error) {
1961
- console.log("[isDaemonRunning] Error:", error);
1962
- types.logger.debug("[DAEMON] Error checking daemon status", error);
2003
+ types.logger.daemonDebug("[isDaemonRunning] Error:", error);
2004
+ types.logger.debug("Error checking daemon status", error);
1963
2005
  return false;
1964
2006
  }
1965
2007
  }
@@ -1969,20 +2011,46 @@ function writePidFile() {
1969
2011
  fs.mkdirSync(happyDir, { recursive: true });
1970
2012
  }
1971
2013
  try {
1972
- fs.writeFileSync(types.configuration.daemonPidFile, process.pid.toString(), { flag: "wx" });
2014
+ const fd = fs.openSync(types.configuration.daemonPidFile, "wx");
2015
+ fs.writeSync(fd, process.pid.toString());
2016
+ return fd;
1973
2017
  } catch (error) {
1974
2018
  if (error.code === "EEXIST") {
1975
- types.logger.debug("[DAEMON] PID file already exists, another daemon may be starting");
1976
- throw new Error("Daemon PID file already exists");
2019
+ try {
2020
+ const fd = fs.openSync(types.configuration.daemonPidFile, "r+");
2021
+ const existingPid = fs.readFileSync(types.configuration.daemonPidFile, "utf-8").trim();
2022
+ fs.closeSync(fd);
2023
+ try {
2024
+ process.kill(parseInt(existingPid), 0);
2025
+ types.logger.daemonDebug("PID file exists and process is running");
2026
+ types.logger.daemonDebug("Happy daemon is already running");
2027
+ process.exit(0);
2028
+ } catch {
2029
+ types.logger.daemonDebug("PID file exists but process is dead, cleaning up");
2030
+ fs.unlinkSync(types.configuration.daemonPidFile);
2031
+ return writePidFile();
2032
+ }
2033
+ } catch (lockError) {
2034
+ types.logger.daemonDebug("Cannot acquire write lock on PID file, daemon is running");
2035
+ types.logger.daemonDebug("Happy daemon is already running");
2036
+ process.exit(0);
2037
+ }
1977
2038
  }
1978
2039
  throw error;
1979
2040
  }
1980
2041
  }
1981
2042
  async function stopDaemon() {
1982
2043
  try {
2044
+ if (pidFileFd !== null) {
2045
+ try {
2046
+ fs.closeSync(pidFileFd);
2047
+ } catch {
2048
+ }
2049
+ pidFileFd = null;
2050
+ }
1983
2051
  if (fs.existsSync(types.configuration.daemonPidFile)) {
1984
2052
  const pid = parseInt(fs.readFileSync(types.configuration.daemonPidFile, "utf-8"));
1985
- types.logger.debug(`[DAEMON] Stopping daemon with PID ${pid}`);
2053
+ types.logger.debug(`Stopping daemon with PID ${pid}`);
1986
2054
  try {
1987
2055
  process.kill(pid, "SIGTERM");
1988
2056
  await new Promise((resolve) => setTimeout(resolve, 1e3));
@@ -1992,12 +2060,12 @@ async function stopDaemon() {
1992
2060
  } catch {
1993
2061
  }
1994
2062
  } catch (error) {
1995
- types.logger.debug("[DAEMON] Process already dead or inaccessible", error);
2063
+ types.logger.debug("Process already dead or inaccessible", error);
1996
2064
  }
1997
2065
  fs.unlinkSync(types.configuration.daemonPidFile);
1998
2066
  }
1999
2067
  } catch (error) {
2000
- types.logger.debug("[DAEMON] Error stopping daemon", error);
2068
+ types.logger.debug("Error stopping daemon", error);
2001
2069
  }
2002
2070
  }
2003
2071
  async function isProcessHappyDaemon(pid) {
@@ -2145,7 +2213,12 @@ async function uninstall() {
2145
2213
  (async () => {
2146
2214
  const args = process.argv.slice(2);
2147
2215
  let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
2148
- types.initializeConfiguration(installationLocation);
2216
+ let serverUrl;
2217
+ const serverUrlIndex = args.indexOf("--happy-server-url");
2218
+ if (serverUrlIndex !== -1 && serverUrlIndex + 1 < args.length) {
2219
+ serverUrl = args[serverUrlIndex + 1];
2220
+ }
2221
+ types.initializeConfiguration(installationLocation, serverUrl);
2149
2222
  types.initLoggerWithGlobalConfiguration();
2150
2223
  types.logger.debug("Starting happy CLI with args: ", process.argv);
2151
2224
  const subcommand = args[0];
@@ -2227,6 +2300,10 @@ Currently only supported on macOS.
2227
2300
  } else if (arg === "--claude-arg") {
2228
2301
  const claudeArg = args[++i];
2229
2302
  options.claudeArgs = [...options.claudeArgs || [], claudeArg];
2303
+ } else if (arg === "--daemon-spawn") {
2304
+ options.daemonSpawn = true;
2305
+ } else if (arg === "--happy-server-url") {
2306
+ i++;
2230
2307
  } else {
2231
2308
  console.error(chalk.red(`Unknown argument: ${arg}`));
2232
2309
  process.exit(1);
@@ -2262,6 +2339,8 @@ ${chalk.bold("Options:")}
2262
2339
  You will require re-login each time you run this in a new directory.
2263
2340
  --happy-starting-mode <interactive|remote>
2264
2341
  Set the starting mode for new sessions (default: remote)
2342
+ --happy-server-url <url>
2343
+ Set the server URL (overrides HANDY_SERVER_URL environment variable)
2265
2344
 
2266
2345
  ${chalk.bold("Examples:")}
2267
2346
  happy Start a session with default settings
@@ -2288,7 +2367,71 @@ ${chalk.bold("Examples:")}
2288
2367
  }
2289
2368
  credentials = res;
2290
2369
  }
2291
- await readSettings() || { };
2370
+ const settings = await readSettings() || { onboardingCompleted: false };
2371
+ process.env.EXPERIMENTAL_FEATURES !== void 0;
2372
+ if (settings.daemonAutoStartWhenRunningHappy === void 0) {
2373
+ console.log(chalk.cyan("\n\u{1F680} Happy Daemon Setup\n"));
2374
+ const rl = node_readline.createInterface({
2375
+ input: process.stdin,
2376
+ output: process.stdout
2377
+ });
2378
+ console.log(chalk.cyan("\n\u{1F4F1} Happy can run a background service that allows you to:"));
2379
+ console.log(chalk.cyan(" \u2022 Spawn new conversations from your phone"));
2380
+ console.log(chalk.cyan(" \u2022 Continue closed conversations remotely"));
2381
+ console.log(chalk.cyan(" \u2022 Work with Claude while your computer has internet\n"));
2382
+ const answer = await new Promise((resolve) => {
2383
+ rl.question(chalk.green("Would you like Happy to start this service automatically? (recommended) [Y/n]: "), resolve);
2384
+ });
2385
+ rl.close();
2386
+ const shouldAutoStart = answer.toLowerCase() !== "n";
2387
+ settings.daemonAutoStartWhenRunningHappy = shouldAutoStart;
2388
+ if (shouldAutoStart) {
2389
+ console.log(chalk.green("\u2713 Happy will start the background service automatically"));
2390
+ console.log(chalk.gray(" The service will run whenever you use the happy command"));
2391
+ } else {
2392
+ console.log(chalk.yellow(" You can enable this later by running: happy daemon install"));
2393
+ }
2394
+ await writeSettings(settings);
2395
+ }
2396
+ if (settings.daemonAutoStartWhenRunningHappy) {
2397
+ console.log("Starting Happy background service...");
2398
+ if (!await isDaemonRunning()) {
2399
+ const happyPath = process.argv[1];
2400
+ const isBuiltBinary = happyPath.endsWith("/bin/happy") || happyPath.endsWith("\\bin\\happy");
2401
+ const daemonArgs = ["daemon", "start"];
2402
+ if (serverUrl) {
2403
+ daemonArgs.push("--happy-server-url", serverUrl);
2404
+ }
2405
+ if (installationLocation === "local") {
2406
+ daemonArgs.push("--local");
2407
+ }
2408
+ const daemonProcess = isBuiltBinary ? child_process.spawn(happyPath, daemonArgs, {
2409
+ detached: true,
2410
+ stdio: ["ignore", "inherit", "inherit"],
2411
+ // Show stdout/stderr for debugging
2412
+ env: {
2413
+ ...process.env,
2414
+ HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
2415
+ // Pass through server URL
2416
+ HANDY_LOCAL: process.env.HANDY_LOCAL
2417
+ // Pass through local flag
2418
+ }
2419
+ }) : child_process.spawn("npx", ["tsx", happyPath, ...daemonArgs], {
2420
+ detached: true,
2421
+ stdio: ["ignore", "inherit", "inherit"],
2422
+ // Show stdout/stderr for debugging
2423
+ env: {
2424
+ ...process.env,
2425
+ HANDY_SERVER_URL: serverUrl || process.env.HANDY_SERVER_URL,
2426
+ // Pass through server URL
2427
+ HANDY_LOCAL: process.env.HANDY_LOCAL
2428
+ // Pass through local flag
2429
+ }
2430
+ });
2431
+ daemonProcess.unref();
2432
+ await new Promise((resolve) => setTimeout(resolve, 200));
2433
+ }
2434
+ }
2292
2435
  try {
2293
2436
  await start(credentials, options);
2294
2437
  } catch (error) {