happy-coder 0.1.9 → 0.1.10

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 backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, e as encodeBase64, f as encodeBase64Url, g as decodeBase64, h as delay, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-fXgEaaqP.mjs';
2
+ import { l as logger, d as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, e as encodeBase64, f as encodeBase64Url, g as decodeBase64, h as delay, j as encrypt, k as decrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DnQGY77F.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';
@@ -8,7 +8,7 @@ import { resolve, join, dirname } from 'node:path';
8
8
  import { spawn } from 'node:child_process';
9
9
  import { createInterface } from 'node:readline';
10
10
  import { fileURLToPath, URL as URL$1 } from 'node:url';
11
- import { watch as watch$1, readFile, mkdir, writeFile } from 'node:fs/promises';
11
+ import { watch as watch$1, readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
12
12
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
13
  import { createServer, request as request$1 } from 'node:http';
14
14
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -16,12 +16,18 @@ import * as z from 'zod';
16
16
  import { z as z$1 } from 'zod';
17
17
  import { request } from 'node:https';
18
18
  import net from 'node:net';
19
+ import { exec, spawn as spawn$1, execSync } from 'child_process';
20
+ import { promisify } from 'util';
21
+ import { readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
22
+ import crypto, { createHash } from 'crypto';
23
+ import { join as join$1 } from 'path';
19
24
  import tweetnacl from 'tweetnacl';
20
25
  import axios from 'axios';
21
26
  import qrcode from 'qrcode-terminal';
22
- import 'fs';
23
- import 'node:events';
24
- import 'socket.io-client';
27
+ import { EventEmitter } from 'node:events';
28
+ import { io } from 'socket.io-client';
29
+ import { homedir as homedir$1, hostname } from 'os';
30
+ import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
25
31
  import 'expo-server-sdk';
26
32
 
27
33
  function formatClaudeMessage(message, onAssistantResult) {
@@ -552,14 +558,14 @@ function createSessionScanner(opts) {
552
558
  processedMessages.add(key);
553
559
  logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
554
560
  logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
555
- if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
561
+ if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
556
562
  const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
557
563
  if (currentCounter && currentCounter > 0) {
558
564
  seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
559
565
  continue;
560
566
  }
561
567
  }
562
- opts.onMessage(parsed.data);
568
+ opts.onMessage(message);
563
569
  } catch (e) {
564
570
  continue;
565
571
  }
@@ -862,7 +868,7 @@ class InterruptController {
862
868
  }
863
869
  }
864
870
 
865
- var version = "0.1.9";
871
+ var version = "0.1.10";
866
872
  var packageJson = {
867
873
  version: version};
868
874
 
@@ -1013,12 +1019,235 @@ async function startAnthropicActivityProxy(onClaudeActivity) {
1013
1019
  };
1014
1020
  }
1015
1021
 
1022
+ const execAsync = promisify(exec);
1023
+ function registerHandlers(session, interruptController, permissionCallbacks) {
1024
+ session.setHandler("abort", async () => {
1025
+ logger.info("Abort request - interrupting Claude");
1026
+ await interruptController.interrupt();
1027
+ });
1028
+ if (permissionCallbacks) {
1029
+ session.setHandler("permission", async (message) => {
1030
+ logger.info("Permission response" + JSON.stringify(message));
1031
+ const id = message.id;
1032
+ const resolve = permissionCallbacks.requests.get(id);
1033
+ if (resolve) {
1034
+ if (!message.approved) {
1035
+ logger.debug("Permission denied, interrupting Claude");
1036
+ await interruptController.interrupt();
1037
+ }
1038
+ resolve({ approved: message.approved, reason: message.reason });
1039
+ permissionCallbacks.requests.delete(id);
1040
+ } else {
1041
+ logger.info("Permission request stale, likely timed out");
1042
+ return;
1043
+ }
1044
+ session.updateAgentState((currentState) => {
1045
+ let r = { ...currentState.requests };
1046
+ delete r[id];
1047
+ return {
1048
+ ...currentState,
1049
+ requests: r
1050
+ };
1051
+ });
1052
+ });
1053
+ }
1054
+ session.setHandler("bash", async (data) => {
1055
+ logger.info("Shell command request:", data.command);
1056
+ try {
1057
+ const options = {
1058
+ cwd: data.cwd,
1059
+ timeout: data.timeout || 3e4
1060
+ // Default 30 seconds timeout
1061
+ };
1062
+ const { stdout, stderr } = await execAsync(data.command, options);
1063
+ return {
1064
+ success: true,
1065
+ stdout: stdout || "",
1066
+ stderr: stderr || "",
1067
+ exitCode: 0
1068
+ };
1069
+ } catch (error) {
1070
+ const execError = error;
1071
+ if (execError.code === "ETIMEDOUT" || execError.killed) {
1072
+ return {
1073
+ success: false,
1074
+ stdout: execError.stdout || "",
1075
+ stderr: execError.stderr || "",
1076
+ exitCode: typeof execError.code === "number" ? execError.code : -1,
1077
+ error: "Command timed out"
1078
+ };
1079
+ }
1080
+ return {
1081
+ success: false,
1082
+ stdout: execError.stdout || "",
1083
+ stderr: execError.stderr || execError.message || "Command failed",
1084
+ exitCode: typeof execError.code === "number" ? execError.code : 1,
1085
+ error: execError.message || "Command failed"
1086
+ };
1087
+ }
1088
+ });
1089
+ session.setHandler("readFile", async (data) => {
1090
+ logger.info("Read file request:", data.path);
1091
+ try {
1092
+ const buffer = await readFile$1(data.path);
1093
+ const content = buffer.toString("base64");
1094
+ return { success: true, content };
1095
+ } catch (error) {
1096
+ logger.debug("Failed to read file:", error);
1097
+ return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
1098
+ }
1099
+ });
1100
+ session.setHandler("writeFile", async (data) => {
1101
+ logger.info("Write file request:", data.path);
1102
+ try {
1103
+ if (data.expectedHash !== null && data.expectedHash !== void 0) {
1104
+ try {
1105
+ const existingBuffer = await readFile$1(data.path);
1106
+ const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
1107
+ if (existingHash !== data.expectedHash) {
1108
+ return {
1109
+ success: false,
1110
+ error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
1111
+ };
1112
+ }
1113
+ } catch (error) {
1114
+ const nodeError = error;
1115
+ if (nodeError.code !== "ENOENT") {
1116
+ throw error;
1117
+ }
1118
+ return {
1119
+ success: false,
1120
+ error: "File does not exist but hash was provided"
1121
+ };
1122
+ }
1123
+ } else {
1124
+ try {
1125
+ await stat(data.path);
1126
+ return {
1127
+ success: false,
1128
+ error: "File already exists but was expected to be new"
1129
+ };
1130
+ } catch (error) {
1131
+ const nodeError = error;
1132
+ if (nodeError.code !== "ENOENT") {
1133
+ throw error;
1134
+ }
1135
+ }
1136
+ }
1137
+ const buffer = Buffer.from(data.content, "base64");
1138
+ await writeFile(data.path, buffer);
1139
+ const hash = createHash("sha256").update(buffer).digest("hex");
1140
+ return { success: true, hash };
1141
+ } catch (error) {
1142
+ logger.debug("Failed to write file:", error);
1143
+ return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
1144
+ }
1145
+ });
1146
+ session.setHandler("listDirectory", async (data) => {
1147
+ logger.info("List directory request:", data.path);
1148
+ try {
1149
+ const entries = await readdir(data.path, { withFileTypes: true });
1150
+ const directoryEntries = await Promise.all(
1151
+ entries.map(async (entry) => {
1152
+ const fullPath = join$1(data.path, entry.name);
1153
+ let type = "other";
1154
+ let size;
1155
+ let modified;
1156
+ if (entry.isDirectory()) {
1157
+ type = "directory";
1158
+ } else if (entry.isFile()) {
1159
+ type = "file";
1160
+ }
1161
+ try {
1162
+ const stats = await stat(fullPath);
1163
+ size = stats.size;
1164
+ modified = stats.mtime.getTime();
1165
+ } catch (error) {
1166
+ logger.debug(`Failed to stat ${fullPath}:`, error);
1167
+ }
1168
+ return {
1169
+ name: entry.name,
1170
+ type,
1171
+ size,
1172
+ modified
1173
+ };
1174
+ })
1175
+ );
1176
+ directoryEntries.sort((a, b) => {
1177
+ if (a.type === "directory" && b.type !== "directory") return -1;
1178
+ if (a.type !== "directory" && b.type === "directory") return 1;
1179
+ return a.name.localeCompare(b.name);
1180
+ });
1181
+ return { success: true, entries: directoryEntries };
1182
+ } catch (error) {
1183
+ logger.debug("Failed to list directory:", error);
1184
+ return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
1185
+ }
1186
+ });
1187
+ session.setHandler("getDirectoryTree", async (data) => {
1188
+ logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1189
+ async function buildTree(path, name, currentDepth) {
1190
+ try {
1191
+ const stats = await stat(path);
1192
+ const node = {
1193
+ name,
1194
+ path,
1195
+ type: stats.isDirectory() ? "directory" : "file",
1196
+ size: stats.size,
1197
+ modified: stats.mtime.getTime()
1198
+ };
1199
+ if (stats.isDirectory() && currentDepth < data.maxDepth) {
1200
+ const entries = await readdir(path, { withFileTypes: true });
1201
+ const children = [];
1202
+ await Promise.all(
1203
+ entries.map(async (entry) => {
1204
+ if (entry.isSymbolicLink()) {
1205
+ logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
1206
+ return;
1207
+ }
1208
+ const childPath = join$1(path, entry.name);
1209
+ const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
1210
+ if (childNode) {
1211
+ children.push(childNode);
1212
+ }
1213
+ })
1214
+ );
1215
+ children.sort((a, b) => {
1216
+ if (a.type === "directory" && b.type !== "directory") return -1;
1217
+ if (a.type !== "directory" && b.type === "directory") return 1;
1218
+ return a.name.localeCompare(b.name);
1219
+ });
1220
+ node.children = children;
1221
+ }
1222
+ return node;
1223
+ } catch (error) {
1224
+ logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
1225
+ return null;
1226
+ }
1227
+ }
1228
+ try {
1229
+ if (data.maxDepth < 0) {
1230
+ return { success: false, error: "maxDepth must be non-negative" };
1231
+ }
1232
+ const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
1233
+ const tree = await buildTree(data.path, baseName, 0);
1234
+ if (!tree) {
1235
+ return { success: false, error: "Failed to access the specified path" };
1236
+ }
1237
+ return { success: true, tree };
1238
+ } catch (error) {
1239
+ logger.debug("Failed to get directory tree:", error);
1240
+ return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
1241
+ }
1242
+ });
1243
+ }
1244
+
1016
1245
  async function start(credentials, options = {}) {
1017
1246
  const workingDirectory = process.cwd();
1018
1247
  const sessionTag = randomUUID();
1019
1248
  const api = new ApiClient(credentials.token, credentials.secret);
1020
1249
  let state = {};
1021
- let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
1250
+ let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
1022
1251
  const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1023
1252
  logger.debug(`Session created: ${response.id}`);
1024
1253
  const session = api.session(response);
@@ -1041,7 +1270,7 @@ async function start(credentials, options = {}) {
1041
1270
  process.env.HTTPS_PROXY = antropicActivityProxy.url;
1042
1271
  logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
1043
1272
  const logPath = await logger.logFilePathPromise;
1044
- logger.info(`Session: ${response.id}`);
1273
+ logger.infoDeveloper(`Session: ${response.id}`);
1045
1274
  logger.infoDeveloper(`Logs: ${logPath}`);
1046
1275
  const interruptController = new InterruptController();
1047
1276
  let requests = /* @__PURE__ */ new Map();
@@ -1095,29 +1324,7 @@ async function start(credentials, options = {}) {
1095
1324
  promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
1096
1325
  return promise;
1097
1326
  });
1098
- session.setHandler("permission", (message) => {
1099
- logger.info("Permission response" + JSON.stringify(message));
1100
- const id = message.id;
1101
- const resolve = requests.get(id);
1102
- if (resolve) {
1103
- resolve({ approved: message.approved, reason: message.reason });
1104
- } else {
1105
- logger.info("Permission request stale, likely timed out");
1106
- return;
1107
- }
1108
- session.updateAgentState((currentState) => {
1109
- let r = { ...currentState.requests };
1110
- delete r[id];
1111
- return {
1112
- ...currentState,
1113
- requests: r
1114
- };
1115
- });
1116
- });
1117
- session.setHandler("abort", async () => {
1118
- logger.info("Abort request - interrupting Claude");
1119
- await interruptController.interrupt();
1120
- });
1327
+ registerHandlers(session, interruptController, { requests });
1121
1328
  const onAssistantResult = async (result) => {
1122
1329
  try {
1123
1330
  const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
@@ -1155,12 +1362,32 @@ async function start(credentials, options = {}) {
1155
1362
  });
1156
1363
  clearInterval(pingInterval);
1157
1364
  if (antropicActivityProxy) {
1158
- logger.info("[AnthropicProxy] Shutting down activity monitoring proxy");
1365
+ logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
1159
1366
  antropicActivityProxy.cleanup();
1160
1367
  }
1161
1368
  process.exit(0);
1162
1369
  }
1163
1370
 
1371
+ const defaultSettings = {
1372
+ onboardingCompleted: false
1373
+ };
1374
+ async function readSettings() {
1375
+ if (!existsSync(configuration.settingsFile)) {
1376
+ return { ...defaultSettings };
1377
+ }
1378
+ try {
1379
+ const content = await readFile(configuration.settingsFile, "utf8");
1380
+ return JSON.parse(content);
1381
+ } catch {
1382
+ return { ...defaultSettings };
1383
+ }
1384
+ }
1385
+ async function writeSettings(settings) {
1386
+ if (!existsSync(configuration.happyDir)) {
1387
+ await mkdir(configuration.happyDir, { recursive: true });
1388
+ }
1389
+ await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
1390
+ }
1164
1391
  const credentialsSchema = z.object({
1165
1392
  secret: z.string().base64(),
1166
1393
  token: z.string()
@@ -1184,7 +1411,7 @@ async function writeCredentials(credentials) {
1184
1411
  if (!existsSync(configuration.happyDir)) {
1185
1412
  await mkdir(configuration.happyDir, { recursive: true });
1186
1413
  }
1187
- await writeFile(configuration.privateKeyFile, JSON.stringify({
1414
+ await writeFile$1(configuration.privateKeyFile, JSON.stringify({
1188
1415
  secret: encodeBase64(credentials.secret),
1189
1416
  token: credentials.token
1190
1417
  }, null, 2));
@@ -1262,6 +1489,355 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1262
1489
  return decrypted;
1263
1490
  }
1264
1491
 
1492
+ class ApiDaemonSession extends EventEmitter {
1493
+ socket;
1494
+ machineIdentity;
1495
+ keepAliveInterval = null;
1496
+ token;
1497
+ secret;
1498
+ constructor(token, secret, machineIdentity) {
1499
+ super();
1500
+ this.token = token;
1501
+ this.secret = secret;
1502
+ this.machineIdentity = machineIdentity;
1503
+ const socket = io(configuration.serverUrl, {
1504
+ auth: {
1505
+ token: this.token,
1506
+ clientType: "machine-scoped",
1507
+ machineId: this.machineIdentity.machineId
1508
+ },
1509
+ path: "/v1/user-machine-daemon",
1510
+ reconnection: true,
1511
+ reconnectionAttempts: Infinity,
1512
+ reconnectionDelay: 1e3,
1513
+ reconnectionDelayMax: 5e3,
1514
+ transports: ["websocket"],
1515
+ withCredentials: true,
1516
+ autoConnect: false
1517
+ });
1518
+ socket.on("connect", () => {
1519
+ logger.debug("[DAEMON] Connected to server");
1520
+ this.emit("connected");
1521
+ socket.emit("machine-connect", {
1522
+ token: this.token,
1523
+ machineIdentity: encodeBase64(encrypt(this.machineIdentity, this.secret))
1524
+ });
1525
+ this.startKeepAlive();
1526
+ });
1527
+ socket.on("disconnect", () => {
1528
+ logger.debug("[DAEMON] Disconnected from server");
1529
+ this.emit("disconnected");
1530
+ this.stopKeepAlive();
1531
+ });
1532
+ socket.on("spawn-session", async (encryptedData, callback) => {
1533
+ let requestData;
1534
+ try {
1535
+ requestData = decrypt(decodeBase64(encryptedData), this.secret);
1536
+ logger.debug("[DAEMON] Received spawn-session request", requestData);
1537
+ const args = [
1538
+ "--directory",
1539
+ requestData.directory,
1540
+ "--happy-starting-mode",
1541
+ requestData.startingMode
1542
+ ];
1543
+ if (requestData.metadata) {
1544
+ args.push("--metadata", requestData.metadata);
1545
+ }
1546
+ if (requestData.startingMode === "interactive" && process.platform === "darwin") {
1547
+ const script = `
1548
+ tell application "Terminal"
1549
+ activate
1550
+ do script "cd ${requestData.directory} && happy ${args.join(" ")}"
1551
+ end tell
1552
+ `;
1553
+ spawn$1("osascript", ["-e", script], { detached: true });
1554
+ } else {
1555
+ const child = spawn$1("happy", args, {
1556
+ detached: true,
1557
+ stdio: "ignore",
1558
+ cwd: requestData.directory
1559
+ });
1560
+ child.unref();
1561
+ }
1562
+ const result = { success: true };
1563
+ socket.emit("session-spawn-result", {
1564
+ requestId: requestData.requestId,
1565
+ result: encodeBase64(encrypt(result, this.secret))
1566
+ });
1567
+ callback(encodeBase64(encrypt({ success: true }, this.secret)));
1568
+ } catch (error) {
1569
+ logger.debug("[DAEMON] Failed to spawn session", error);
1570
+ const errorResult = {
1571
+ success: false,
1572
+ error: error instanceof Error ? error.message : "Unknown error"
1573
+ };
1574
+ socket.emit("session-spawn-result", {
1575
+ requestId: requestData?.requestId || "",
1576
+ result: encodeBase64(encrypt(errorResult, this.secret))
1577
+ });
1578
+ callback(encodeBase64(encrypt(errorResult, this.secret)));
1579
+ }
1580
+ });
1581
+ socket.on("daemon-command", (data) => {
1582
+ switch (data.command) {
1583
+ case "shutdown":
1584
+ this.shutdown();
1585
+ break;
1586
+ case "status":
1587
+ this.emit("status-request");
1588
+ break;
1589
+ }
1590
+ });
1591
+ this.socket = socket;
1592
+ }
1593
+ startKeepAlive() {
1594
+ this.stopKeepAlive();
1595
+ this.keepAliveInterval = setInterval(() => {
1596
+ this.socket.volatile.emit("machine-alive", {
1597
+ time: Date.now()
1598
+ });
1599
+ }, 2e4);
1600
+ }
1601
+ stopKeepAlive() {
1602
+ if (this.keepAliveInterval) {
1603
+ clearInterval(this.keepAliveInterval);
1604
+ this.keepAliveInterval = null;
1605
+ }
1606
+ }
1607
+ connect() {
1608
+ this.socket.connect();
1609
+ }
1610
+ shutdown() {
1611
+ this.stopKeepAlive();
1612
+ this.socket.close();
1613
+ this.emit("shutdown");
1614
+ }
1615
+ }
1616
+
1617
+ const DAEMON_PID_FILE = join$1(homedir$1(), ".happy", "daemon-pid");
1618
+ async function startDaemon() {
1619
+ if (isDaemonRunning()) {
1620
+ console.log("Happy daemon is already running");
1621
+ process.exit(0);
1622
+ }
1623
+ logger.info("Happy CLI daemon started successfully");
1624
+ writePidFile();
1625
+ process.on("SIGINT", stopDaemon);
1626
+ process.on("SIGTERM", stopDaemon);
1627
+ process.on("exit", stopDaemon);
1628
+ try {
1629
+ const settings = await readSettings() || { onboardingCompleted: false };
1630
+ if (!settings.machineId) {
1631
+ settings.machineId = crypto.randomUUID();
1632
+ settings.machineHost = hostname();
1633
+ await writeSettings(settings);
1634
+ }
1635
+ const machineIdentity = {
1636
+ machineId: settings.machineId,
1637
+ machineHost: settings.machineHost || hostname(),
1638
+ platform: process.platform,
1639
+ version: process.env.npm_package_version || "unknown"
1640
+ };
1641
+ let credentials = await readCredentials();
1642
+ if (!credentials) {
1643
+ logger.debug("[DAEMON] No credentials found, running auth");
1644
+ await doAuth();
1645
+ credentials = await readCredentials();
1646
+ if (!credentials) {
1647
+ throw new Error("Failed to authenticate");
1648
+ }
1649
+ }
1650
+ const { token, secret } = credentials;
1651
+ const daemon = new ApiDaemonSession(token, secret, machineIdentity);
1652
+ daemon.on("connected", () => {
1653
+ logger.debug("[DAEMON] Successfully connected to server");
1654
+ });
1655
+ daemon.on("disconnected", () => {
1656
+ logger.debug("[DAEMON] Disconnected from server");
1657
+ });
1658
+ daemon.on("shutdown", () => {
1659
+ logger.debug("[DAEMON] Shutdown requested");
1660
+ stopDaemon();
1661
+ process.exit(0);
1662
+ });
1663
+ daemon.connect();
1664
+ setInterval(() => {
1665
+ }, 1e3);
1666
+ } catch (error) {
1667
+ logger.debug("[DAEMON] Failed to start daemon", error);
1668
+ stopDaemon();
1669
+ process.exit(1);
1670
+ }
1671
+ process.on("SIGINT", () => process.exit(0));
1672
+ process.on("SIGTERM", () => process.exit(0));
1673
+ process.on("exit", () => process.exit(0));
1674
+ while (true) {
1675
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1676
+ }
1677
+ }
1678
+ function isDaemonRunning() {
1679
+ try {
1680
+ if (!existsSync$1(DAEMON_PID_FILE)) {
1681
+ console.log("No PID file found");
1682
+ return false;
1683
+ }
1684
+ const pid = parseInt(readFileSync$1(DAEMON_PID_FILE, "utf-8"));
1685
+ try {
1686
+ process.kill(pid, 0);
1687
+ return true;
1688
+ } catch (error) {
1689
+ console.log("Process not running", error);
1690
+ unlinkSync(DAEMON_PID_FILE);
1691
+ return false;
1692
+ }
1693
+ } catch {
1694
+ return false;
1695
+ }
1696
+ }
1697
+ function writePidFile() {
1698
+ const happyDir = join$1(homedir$1(), ".happy");
1699
+ if (!existsSync$1(happyDir)) {
1700
+ mkdirSync$1(happyDir, { recursive: true });
1701
+ }
1702
+ writeFileSync(DAEMON_PID_FILE, process.pid.toString());
1703
+ }
1704
+ function stopDaemon() {
1705
+ try {
1706
+ if (existsSync$1(DAEMON_PID_FILE)) {
1707
+ logger.debug("[DAEMON] Stopping daemon");
1708
+ process.kill(parseInt(readFileSync$1(DAEMON_PID_FILE, "utf-8")), "SIGTERM");
1709
+ unlinkSync(DAEMON_PID_FILE);
1710
+ }
1711
+ } catch (error) {
1712
+ logger.debug("[DAEMON] Error cleaning up PID file", error);
1713
+ }
1714
+ }
1715
+
1716
+ function trimIdent(text) {
1717
+ const lines = text.split("\n");
1718
+ while (lines.length > 0 && lines[0].trim() === "") {
1719
+ lines.shift();
1720
+ }
1721
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
1722
+ lines.pop();
1723
+ }
1724
+ const minSpaces = lines.reduce((min, line) => {
1725
+ if (line.trim() === "") {
1726
+ return min;
1727
+ }
1728
+ const leadingSpaces = line.match(/^\s*/)[0].length;
1729
+ return Math.min(min, leadingSpaces);
1730
+ }, Infinity);
1731
+ const trimmedLines = lines.map((line) => line.slice(minSpaces));
1732
+ return trimmedLines.join("\n");
1733
+ }
1734
+
1735
+ const PLIST_LABEL$1 = "com.happy-cli.daemon";
1736
+ const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
1737
+ const USER_HOME = process.env.HOME || process.env.USERPROFILE;
1738
+ async function install$1() {
1739
+ try {
1740
+ if (existsSync$1(PLIST_FILE$1)) {
1741
+ logger.info("Daemon plist already exists. Uninstalling first...");
1742
+ execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
1743
+ }
1744
+ const happyPath = process.argv[0];
1745
+ const scriptPath = process.argv[1];
1746
+ const plistContent = trimIdent(`
1747
+ <?xml version="1.0" encoding="UTF-8"?>
1748
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1749
+ <plist version="1.0">
1750
+ <dict>
1751
+ <key>Label</key>
1752
+ <string>${PLIST_LABEL$1}</string>
1753
+
1754
+ <key>ProgramArguments</key>
1755
+ <array>
1756
+ <string>${happyPath}</string>
1757
+ <string>${scriptPath}</string>
1758
+ <string>happy-daemon</string>
1759
+ </array>
1760
+
1761
+ <key>EnvironmentVariables</key>
1762
+ <dict>
1763
+ <key>HAPPY_DAEMON_MODE</key>
1764
+ <string>true</string>
1765
+ </dict>
1766
+
1767
+ <key>RunAtLoad</key>
1768
+ <true/>
1769
+
1770
+ <key>KeepAlive</key>
1771
+ <true/>
1772
+
1773
+ <key>StandardErrorPath</key>
1774
+ <string>${USER_HOME}/.happy/daemon.err</string>
1775
+
1776
+ <key>StandardOutPath</key>
1777
+ <string>${USER_HOME}/.happy/daemon.log</string>
1778
+
1779
+ <key>WorkingDirectory</key>
1780
+ <string>/tmp</string>
1781
+ </dict>
1782
+ </plist>
1783
+ `);
1784
+ writeFileSync(PLIST_FILE$1, plistContent);
1785
+ chmodSync(PLIST_FILE$1, 420);
1786
+ logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
1787
+ execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
1788
+ logger.info("Daemon installed and started successfully");
1789
+ logger.info("Check logs at ~/.happy/daemon.log");
1790
+ } catch (error) {
1791
+ logger.debug("Failed to install daemon:", error);
1792
+ throw error;
1793
+ }
1794
+ }
1795
+
1796
+ async function install() {
1797
+ if (process.platform !== "darwin") {
1798
+ throw new Error("Daemon installation is currently only supported on macOS");
1799
+ }
1800
+ if (process.getuid && process.getuid() !== 0) {
1801
+ throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
1802
+ }
1803
+ logger.info("Installing Happy CLI daemon for macOS...");
1804
+ await install$1();
1805
+ }
1806
+
1807
+ const PLIST_LABEL = "com.happy-cli.daemon";
1808
+ const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
1809
+ async function uninstall$1() {
1810
+ try {
1811
+ if (!existsSync$1(PLIST_FILE)) {
1812
+ logger.info("Daemon plist not found. Nothing to uninstall.");
1813
+ return;
1814
+ }
1815
+ try {
1816
+ execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
1817
+ logger.info("Daemon stopped successfully");
1818
+ } catch (error) {
1819
+ logger.info("Failed to unload daemon (it might not be running)");
1820
+ }
1821
+ unlinkSync(PLIST_FILE);
1822
+ logger.info(`Removed daemon plist from ${PLIST_FILE}`);
1823
+ logger.info("Daemon uninstalled successfully");
1824
+ } catch (error) {
1825
+ logger.debug("Failed to uninstall daemon:", error);
1826
+ throw error;
1827
+ }
1828
+ }
1829
+
1830
+ async function uninstall() {
1831
+ if (process.platform !== "darwin") {
1832
+ throw new Error("Daemon uninstallation is currently only supported on macOS");
1833
+ }
1834
+ if (process.getuid && process.getuid() !== 0) {
1835
+ throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
1836
+ }
1837
+ logger.info("Uninstalling Happy CLI daemon for macOS...");
1838
+ await uninstall$1();
1839
+ }
1840
+
1265
1841
  (async () => {
1266
1842
  const args = process.argv.slice(2);
1267
1843
  let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
@@ -1281,39 +1857,40 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
1281
1857
  }
1282
1858
  return;
1283
1859
  } else if (subcommand === "daemon") {
1284
- if (process.env.HAPPY_DAEMON_MODE) {
1285
- const { run } = await import('./run-FBXkmmN7.mjs');
1286
- await run();
1860
+ const daemonSubcommand = args[1];
1861
+ if (daemonSubcommand === "start") {
1862
+ await startDaemon();
1863
+ process.exit(0);
1864
+ } else if (daemonSubcommand === "stop") {
1865
+ await stopDaemon();
1866
+ process.exit(0);
1867
+ } else if (daemonSubcommand === "install") {
1868
+ try {
1869
+ await install();
1870
+ } catch (error) {
1871
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1872
+ process.exit(1);
1873
+ }
1874
+ } else if (daemonSubcommand === "uninstall") {
1875
+ try {
1876
+ await uninstall();
1877
+ } catch (error) {
1878
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1879
+ process.exit(1);
1880
+ }
1287
1881
  } else {
1288
- const daemonSubcommand = args[1];
1289
- if (daemonSubcommand === "install") {
1290
- const { install } = await import('./install-HKe7dyS4.mjs');
1291
- try {
1292
- await install();
1293
- } catch (error) {
1294
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1295
- process.exit(1);
1296
- }
1297
- } else if (daemonSubcommand === "uninstall") {
1298
- const { uninstall } = await import('./uninstall-CLkTtlMv.mjs');
1299
- try {
1300
- await uninstall();
1301
- } catch (error) {
1302
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
1303
- process.exit(1);
1304
- }
1305
- } else {
1306
- console.log(`
1882
+ console.log(`
1307
1883
  ${chalk.bold("happy daemon")} - Daemon management
1308
1884
 
1309
1885
  ${chalk.bold("Usage:")}
1886
+ happy daemon start Start the daemon
1887
+ happy daemon stop Stop the daemon
1310
1888
  sudo happy daemon install Install the daemon (requires sudo)
1311
1889
  sudo happy daemon uninstall Uninstall the daemon (requires sudo)
1312
1890
 
1313
1891
  ${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
1314
1892
  Currently only supported on macOS.
1315
1893
  `);
1316
- }
1317
1894
  }
1318
1895
  return;
1319
1896
  } else {
@@ -1349,7 +1926,6 @@ ${chalk.bold("happy")} - Claude Code session sharing
1349
1926
  ${chalk.bold("Usage:")}
1350
1927
  happy [options]
1351
1928
  happy logout Logs out of your account and removes data directory
1352
- happy daemon Manage the background daemon (macOS only)
1353
1929
 
1354
1930
  ${chalk.bold("Options:")}
1355
1931
  -h, --help Show this help message
@@ -1362,9 +1938,8 @@ ${chalk.bold("Options:")}
1362
1938
  --local < global | local >
1363
1939
  Will use .happy folder in the current directory for storing your private key and debug logs.
1364
1940
  You will require re-login each time you run this in a new directory.
1365
-
1366
- --happy-starting-mode <mode> Start in specified mode (interactive or remote)
1367
- Default: interactive
1941
+ --happy-starting-mode <interactive|remote>
1942
+ Set the starting mode for new sessions (default: remote)
1368
1943
 
1369
1944
  ${chalk.bold("Examples:")}
1370
1945
  happy Start a session with default settings