happy-imou-cloud 2.0.9 → 2.0.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.
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var api = require('./api-CUTdFiFP.cjs');
5
- var persistence = require('./persistence-CxvL0cwp.cjs');
4
+ var api = require('./api-DUE5TJBE.cjs');
5
+ var persistence = require('./persistence-D7JtnrYA.cjs');
6
6
  var z = require('zod');
7
- var fs$1 = require('fs/promises');
7
+ var fs$2 = require('fs/promises');
8
8
  var os$1 = require('os');
9
9
  var tmp = require('tmp');
10
10
  var node_crypto = require('node:crypto');
@@ -14,23 +14,23 @@ var qrcode = require('qrcode-terminal');
14
14
  var promises = require('node:fs/promises');
15
15
  var node_module = require('node:module');
16
16
  var os = require('node:os');
17
- var node_path = require('node:path');
17
+ var path = require('node:path');
18
18
  var open = require('open');
19
19
  var React = require('react');
20
20
  var ink = require('ink');
21
21
  var child_process = require('child_process');
22
- var path = require('path');
22
+ var path$1 = require('path');
23
23
  var url = require('url');
24
- var fs = require('fs');
24
+ var fs$1 = require('fs');
25
25
  var node_child_process = require('node:child_process');
26
26
  var psList = require('ps-list');
27
27
  var spawn = require('cross-spawn');
28
- var node_fs = require('node:fs');
28
+ var fs = require('node:fs');
29
29
  var fastify = require('fastify');
30
30
  var fastifyTypeProviderZod = require('fastify-type-provider-zod');
31
+ var crypto = require('crypto');
31
32
  var node_readline = require('node:readline');
32
33
  var http = require('http');
33
- var crypto = require('crypto');
34
34
  var util = require('util');
35
35
  var sdk = require('@agentclientprotocol/sdk');
36
36
 
@@ -70,7 +70,7 @@ async function openBrowser(url) {
70
70
  }
71
71
  }
72
72
 
73
- const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-D4OdFq68.cjs', document.baseURI).href)));
73
+ const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-Cuvfa15L.cjs', document.baseURI).href)));
74
74
  const QRCode = require$1("qrcode-terminal/vendor/QRCode");
75
75
  const QRErrorCorrectLevel = require$1("qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel");
76
76
  const pendingTempFiles = /* @__PURE__ */ new Set();
@@ -164,7 +164,7 @@ async function openWindowsQrCode(value) {
164
164
  "</body>",
165
165
  "</html>"
166
166
  ].join("");
167
- const filePath = node_path.join(os.tmpdir(), `happy-auth-qrcode-${node_crypto.randomUUID()}.html`);
167
+ const filePath = path.join(os.tmpdir(), `happy-auth-qrcode-${node_crypto.randomUUID()}.html`);
168
168
  await promises.writeFile(filePath, html, "utf8");
169
169
  const opened = await openBrowser(filePath);
170
170
  if (opened) {
@@ -693,18 +693,28 @@ function setupCleanupHandlers() {
693
693
  });
694
694
  }
695
695
 
696
- const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-D4OdFq68.cjs', document.baseURI).href))));
696
+ const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-Cuvfa15L.cjs', document.baseURI).href))));
697
697
  function projectPath() {
698
- const path$1 = path.resolve(__dirname$1, "..");
699
- return path$1;
698
+ const path = path$1.resolve(__dirname$1, "..");
699
+ return path;
700
700
  }
701
701
 
702
+ function classifyHappyProcessLookup(proc) {
703
+ const match = classifyHappyProcess(proc);
704
+ if (match) {
705
+ return match;
706
+ }
707
+ if (process.platform === "win32" && isWindowsCliHostProcess(proc.name) && proc.cmd.trim().length === 0) {
708
+ return "indeterminate";
709
+ }
710
+ return null;
711
+ }
702
712
  function getDaemonPid() {
703
713
  try {
704
- if (!node_fs.existsSync(api.configuration.daemonStateFile)) {
714
+ if (!fs.existsSync(api.configuration.daemonStateFile)) {
705
715
  return null;
706
716
  }
707
- const state = JSON.parse(node_fs.readFileSync(api.configuration.daemonStateFile, "utf-8"));
717
+ const state = JSON.parse(fs.readFileSync(api.configuration.daemonStateFile, "utf-8"));
708
718
  return typeof state.pid === "number" ? state.pid : null;
709
719
  } catch {
710
720
  return null;
@@ -779,7 +789,7 @@ function findWindowsDaemonProcess(processes) {
779
789
  return {
780
790
  pid: daemonPid,
781
791
  command: `${daemonProcess.name || "unknown"} (PID ${daemonPid})`,
782
- type: "daemon"
792
+ type: "daemon-indeterminate"
783
793
  };
784
794
  }
785
795
  function findWindowsHappyProcesses(processes) {
@@ -796,6 +806,40 @@ function findWindowsHappyProcesses(processes) {
796
806
  }
797
807
  return Array.from(matches.values());
798
808
  }
809
+ function toProcessSnapshot(proc) {
810
+ return {
811
+ pid: proc.pid,
812
+ name: proc.name || "",
813
+ cmd: proc.cmd || ""
814
+ };
815
+ }
816
+ async function findHappyProcessByPid(pid) {
817
+ try {
818
+ if (process.platform === "win32") {
819
+ const windowsProcesses = getWindowsProcessSnapshots();
820
+ if (windowsProcesses.length > 0) {
821
+ const proc3 = windowsProcesses.find((candidate) => candidate.pid === pid);
822
+ if (!proc3) {
823
+ return null;
824
+ }
825
+ return classifyHappyProcessLookup(proc3);
826
+ }
827
+ const fallbackProcesses = (await psList()).map(toProcessSnapshot);
828
+ const proc2 = fallbackProcesses.find((candidate) => candidate.pid === pid);
829
+ if (!proc2) {
830
+ return null;
831
+ }
832
+ return classifyHappyProcessLookup(proc2);
833
+ }
834
+ const proc = (await psList()).map(toProcessSnapshot).find((candidate) => candidate.pid === pid);
835
+ if (!proc) {
836
+ return null;
837
+ }
838
+ return classifyHappyProcess(proc);
839
+ } catch (error) {
840
+ return "indeterminate";
841
+ }
842
+ }
799
843
  async function findAllHappyProcesses() {
800
844
  try {
801
845
  if (process.platform === "win32") {
@@ -803,15 +847,8 @@ async function findAllHappyProcesses() {
803
847
  if (windowsProcesses.length > 0) {
804
848
  return findWindowsHappyProcesses(windowsProcesses);
805
849
  }
806
- const fallbackProcesses = await psList();
807
- const fallbackDaemon = findWindowsDaemonProcess(
808
- fallbackProcesses.map((proc) => ({
809
- pid: proc.pid,
810
- name: proc.name || "",
811
- cmd: proc.cmd || ""
812
- }))
813
- );
814
- return fallbackDaemon ? [fallbackDaemon] : [];
850
+ const fallbackProcesses = (await psList()).map(toProcessSnapshot);
851
+ return findWindowsHappyProcesses(fallbackProcesses);
815
852
  }
816
853
  const processes = await psList();
817
854
  const allProcesses = [];
@@ -868,6 +905,46 @@ async function killRunawayHappyProcesses() {
868
905
  return { killed, errors };
869
906
  }
870
907
 
908
+ const CONFIRMED_DAEMON_PROCESS_TYPES = /* @__PURE__ */ new Set([
909
+ "daemon",
910
+ "dev-daemon"
911
+ ]);
912
+ function currentProcessLooksLikeDaemon() {
913
+ return process.argv.join(" ").includes("daemon start-sync");
914
+ }
915
+ async function inspectDaemonStateProcess(state) {
916
+ if (!state) {
917
+ return "stale";
918
+ }
919
+ try {
920
+ process.kill(state.pid, 0);
921
+ } catch {
922
+ api.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
923
+ await cleanupDaemonState();
924
+ return "stale";
925
+ }
926
+ const processInfo = await findHappyProcessByPid(state.pid);
927
+ if (processInfo === "indeterminate") {
928
+ api.logger.debug(`[DAEMON RUN] Daemon PID ${state.pid} is alive but process identity is indeterminate`);
929
+ return "indeterminate";
930
+ }
931
+ if (!processInfo) {
932
+ api.logger.debug(`[DAEMON RUN] PID ${state.pid} is alive but is not a Happy daemon process, cleaning up state`);
933
+ await cleanupDaemonState();
934
+ return "stale";
935
+ }
936
+ if (processInfo.type === "current" && currentProcessLooksLikeDaemon()) {
937
+ return "confirmed";
938
+ }
939
+ if (!CONFIRMED_DAEMON_PROCESS_TYPES.has(processInfo.type)) {
940
+ api.logger.debug(
941
+ `[DAEMON RUN] PID ${state.pid} resolved to Happy process type ${processInfo.type}, not a daemon; cleaning up state`
942
+ );
943
+ await cleanupDaemonState();
944
+ return "stale";
945
+ }
946
+ return "confirmed";
947
+ }
871
948
  async function daemonPost(path, body) {
872
949
  const state = await persistence.readDaemonState();
873
950
  if (!state?.httpPort) {
@@ -877,9 +954,8 @@ async function daemonPost(path, body) {
877
954
  error: errorMessage
878
955
  };
879
956
  }
880
- try {
881
- process.kill(state.pid, 0);
882
- } catch (error) {
957
+ const processStatus = await inspectDaemonStateProcess(state);
958
+ if (processStatus === "stale") {
883
959
  const errorMessage = "Daemon is not running, file is stale";
884
960
  api.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
885
961
  return {
@@ -978,18 +1054,22 @@ async function stopDaemonHttp() {
978
1054
  await daemonPost("/stop");
979
1055
  }
980
1056
  async function checkIfDaemonRunningAndCleanupStaleState() {
1057
+ const runtimeStatus = await getDaemonRuntimeStatus();
1058
+ return runtimeStatus !== "not-running";
1059
+ }
1060
+ async function getDaemonRuntimeStatus() {
981
1061
  const state = await persistence.readDaemonState();
982
1062
  if (!state) {
983
- return false;
1063
+ return "not-running";
984
1064
  }
985
- try {
986
- process.kill(state.pid, 0);
987
- return true;
988
- } catch {
989
- api.logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
990
- await cleanupDaemonState();
991
- return false;
1065
+ const processStatus = await inspectDaemonStateProcess(state);
1066
+ if (processStatus === "confirmed") {
1067
+ return "running";
1068
+ }
1069
+ if (processStatus === "indeterminate") {
1070
+ return "indeterminate";
992
1071
  }
1072
+ return "not-running";
993
1073
  }
994
1074
  async function isDaemonControlServerResponsive(timeoutMs = 1e3) {
995
1075
  const state = await persistence.readDaemonState();
@@ -997,11 +1077,9 @@ async function isDaemonControlServerResponsive(timeoutMs = 1e3) {
997
1077
  api.logger.debug("[DAEMON CONTROL] No daemon state or control port found for readiness check");
998
1078
  return false;
999
1079
  }
1000
- try {
1001
- process.kill(state.pid, 0);
1002
- } catch {
1003
- api.logger.debug("[DAEMON CONTROL] Daemon PID not running during readiness check, cleaning up state");
1004
- await cleanupDaemonState();
1080
+ const processStatus = await inspectDaemonStateProcess(state);
1081
+ if (processStatus === "stale") {
1082
+ api.logger.debug("[DAEMON CONTROL] Daemon state became stale during readiness check");
1005
1083
  return false;
1006
1084
  }
1007
1085
  try {
@@ -1017,7 +1095,7 @@ async function isDaemonControlServerResponsive(timeoutMs = 1e3) {
1017
1095
  return false;
1018
1096
  }
1019
1097
  }
1020
- async function isDaemonRunningCurrentlyInstalledHappyVersion() {
1098
+ async function isDaemonRunningCurrentlyInstalledHappyVersion(readinessProbeTimeoutMs = 1e3) {
1021
1099
  api.logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
1022
1100
  const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
1023
1101
  if (!runningDaemon) {
@@ -1030,14 +1108,14 @@ async function isDaemonRunningCurrentlyInstalledHappyVersion() {
1030
1108
  return false;
1031
1109
  }
1032
1110
  try {
1033
- const packageJsonPath = path.join(projectPath(), "package.json");
1034
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
1111
+ const packageJsonPath = path$1.join(projectPath(), "package.json");
1112
+ const packageJson = JSON.parse(fs$1.readFileSync(packageJsonPath, "utf-8"));
1035
1113
  const currentCliVersion = packageJson.version;
1036
1114
  api.logger.debug(`[DAEMON CONTROL] Current CLI version: ${currentCliVersion}, Daemon started with version: ${state.startedWithCliVersion}`);
1037
1115
  if (currentCliVersion !== state.startedWithCliVersion) {
1038
1116
  return false;
1039
1117
  }
1040
- return await isDaemonControlServerResponsive();
1118
+ return await isDaemonControlServerResponsive(readinessProbeTimeoutMs);
1041
1119
  } catch (error) {
1042
1120
  api.logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
1043
1121
  return false;
@@ -1070,6 +1148,17 @@ async function stopDaemon() {
1070
1148
  return;
1071
1149
  }
1072
1150
  api.logger.debug(`Stopping daemon with PID ${state.pid}`);
1151
+ const processStatus = await inspectDaemonStateProcess(state);
1152
+ if (processStatus === "stale") {
1153
+ api.logger.debug("Daemon state was stale while stopping, trying known control port/orphan cleanup");
1154
+ const stoppedByKnownPort = await stopDaemonOnKnownPort();
1155
+ if (stoppedByKnownPort) {
1156
+ api.logger.debug(`Requested daemon stop via known control port ${api.HAPPY_CLOUD_DAEMON_PORT}`);
1157
+ return;
1158
+ }
1159
+ await killOrphanDaemonProcesses();
1160
+ return;
1161
+ }
1073
1162
  try {
1074
1163
  await stopDaemonHttp();
1075
1164
  await waitForProcessDeath(state.pid, 2e3);
@@ -1079,6 +1168,10 @@ async function stopDaemon() {
1079
1168
  } catch (error) {
1080
1169
  api.logger.debug("HTTP stop failed, will force kill", error);
1081
1170
  }
1171
+ if (processStatus === "indeterminate") {
1172
+ api.logger.debug(`Skipping force kill for PID ${state.pid} because daemon identity is indeterminate`);
1173
+ return;
1174
+ }
1082
1175
  try {
1083
1176
  process.kill(state.pid, "SIGKILL");
1084
1177
  await waitForProcessDeath(state.pid, 2e3).catch(() => {
@@ -1130,12 +1223,12 @@ function getEnvironmentInfo() {
1130
1223
  terminal: process.env.TERM
1131
1224
  };
1132
1225
  }
1133
- function resolveDaemonSpawnDiagnostics(projectRoot, fileExists = node_fs.existsSync) {
1226
+ function resolveDaemonSpawnDiagnostics(projectRoot, fileExists = fs.existsSync) {
1134
1227
  const wrapperCandidates = [
1135
- node_path.join(projectRoot, "bin", "happy-cloud.mjs"),
1136
- node_path.join(projectRoot, "bin", "happy.mjs")
1228
+ path.join(projectRoot, "bin", "happy-cloud.mjs"),
1229
+ path.join(projectRoot, "bin", "happy.mjs")
1137
1230
  ];
1138
- const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
1231
+ const cliEntrypoint = path.join(projectRoot, "dist", "index.mjs");
1139
1232
  const wrapperPath = wrapperCandidates.find((candidate) => fileExists(candidate)) ?? wrapperCandidates[0];
1140
1233
  return {
1141
1234
  projectRoot,
@@ -1146,14 +1239,14 @@ function resolveDaemonSpawnDiagnostics(projectRoot, fileExists = node_fs.existsS
1146
1239
  };
1147
1240
  }
1148
1241
  function getLogFiles(logDir) {
1149
- if (!node_fs.existsSync(logDir)) {
1242
+ if (!fs.existsSync(logDir)) {
1150
1243
  return [];
1151
1244
  }
1152
1245
  try {
1153
- return node_fs.readdirSync(logDir).filter((file) => file.endsWith(".log")).map((file) => {
1154
- const path = node_path.join(logDir, file);
1155
- const stats = node_fs.statSync(path);
1156
- return { file, path, modified: stats.mtime };
1246
+ return fs.readdirSync(logDir).filter((file) => file.endsWith(".log")).map((file) => {
1247
+ const path$1 = path.join(logDir, file);
1248
+ const stats = fs.statSync(path$1);
1249
+ return { file, path: path$1, modified: stats.mtime };
1157
1250
  }).sort((a, b) => b.modified.getTime() - a.modified.getTime());
1158
1251
  } catch {
1159
1252
  return [];
@@ -1212,9 +1305,9 @@ async function runDoctorCommand(filter) {
1212
1305
  }
1213
1306
  console.log(chalk.bold("\n\u{1F916} Daemon Status"));
1214
1307
  try {
1215
- const isRunning = await checkIfDaemonRunningAndCleanupStaleState();
1308
+ const daemonStatus = await getDaemonRuntimeStatus();
1216
1309
  const state = await persistence.readDaemonState();
1217
- if (isRunning && state) {
1310
+ if (daemonStatus === "running" && state) {
1218
1311
  console.log(chalk.green("\u2713 Daemon is running"));
1219
1312
  console.log(` PID: ${state.pid}`);
1220
1313
  console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
@@ -1222,7 +1315,14 @@ async function runDoctorCommand(filter) {
1222
1315
  if (state.httpPort) {
1223
1316
  console.log(` HTTP Port: ${state.httpPort}`);
1224
1317
  }
1225
- } else if (state && !isRunning) {
1318
+ } else if (daemonStatus === "indeterminate" && state) {
1319
+ console.log(chalk.yellow("\u26A0\uFE0F Daemon state exists, but process identity could not be confirmed"));
1320
+ console.log(` PID: ${state.pid}`);
1321
+ console.log(` CLI Version: ${state.startedWithCliVersion}`);
1322
+ if (state.httpPort) {
1323
+ console.log(` HTTP Port: ${state.httpPort}`);
1324
+ }
1325
+ } else if (state) {
1226
1326
  console.log(chalk.yellow("\u26A0\uFE0F Daemon state exists but process not running (stale)"));
1227
1327
  } else {
1228
1328
  console.log(chalk.red("\u274C Daemon is not running"));
@@ -1244,6 +1344,7 @@ async function runDoctorCommand(filter) {
1244
1344
  const typeLabels = {
1245
1345
  "current": "\u{1F4CD} Current Process",
1246
1346
  "daemon": "\u{1F916} Daemon",
1347
+ "daemon-indeterminate": "\u26A0\uFE0F Possible Daemon (identity unconfirmed)",
1247
1348
  "daemon-version-check": "\u{1F50D} Daemon Version Check (stuck)",
1248
1349
  "daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
1249
1350
  "user-session": "\u{1F464} User Sessions",
@@ -1258,7 +1359,7 @@ async function runDoctorCommand(filter) {
1258
1359
  console.log(chalk.blue(`
1259
1360
  ${typeLabels[type] || type}:`));
1260
1361
  processes.forEach(({ pid, command }) => {
1261
- const color = type === "current" ? chalk.green : type.startsWith("dev") ? chalk.cyan : type.includes("daemon") ? chalk.blue : chalk.gray;
1362
+ const color = type === "current" ? chalk.green : type === "daemon-indeterminate" ? chalk.yellow : type.startsWith("dev") ? chalk.cyan : type.includes("daemon") ? chalk.blue : chalk.gray;
1262
1363
  console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
1263
1364
  });
1264
1365
  });
@@ -1342,7 +1443,7 @@ const isBun = () => getRuntime() === "bun";
1342
1443
 
1343
1444
  function spawnHappyCLI(args, options = {}) {
1344
1445
  const projectRoot = projectPath();
1345
- const entrypoint = node_path.join(projectRoot, "dist", "index.mjs");
1446
+ const entrypoint = path.join(projectRoot, "dist", "index.mjs");
1346
1447
  let directory;
1347
1448
  if ("cwd" in options) {
1348
1449
  directory = options.cwd;
@@ -1357,7 +1458,7 @@ function spawnHappyCLI(args, options = {}) {
1357
1458
  entrypoint,
1358
1459
  ...args
1359
1460
  ];
1360
- if (!node_fs.existsSync(entrypoint)) {
1461
+ if (!fs.existsSync(entrypoint)) {
1361
1462
  const errorMessage = `Entrypoint ${entrypoint} does not exist`;
1362
1463
  api.logger.debug(`[SPAWN HAPPY CLOUD CLI] ${errorMessage}`);
1363
1464
  throw new Error(errorMessage);
@@ -2288,6 +2389,283 @@ function buildDaemonChildEnv(baseEnv, extraEnv) {
2288
2389
  return childEnv;
2289
2390
  }
2290
2391
 
2392
+ const DIFFERENT_DAEMON_RUNNING_MESSAGE = "A different daemon was started without killing us. We should kill ourselves.";
2393
+ function readProjectCliVersionFromDisk() {
2394
+ const packageJsonPath = path.join(projectPath(), "package.json");
2395
+ try {
2396
+ const parsedPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
2397
+ if (typeof parsedPackageJson.version !== "string" || parsedPackageJson.version.trim().length === 0) {
2398
+ api.logger.warn(
2399
+ `[DAEMON RUN] Skipping daemon version check because ${packageJsonPath} does not contain a valid string version`
2400
+ );
2401
+ return null;
2402
+ }
2403
+ return parsedPackageJson.version;
2404
+ } catch (error) {
2405
+ api.logger.warn(
2406
+ `[DAEMON RUN] Skipping daemon version check because ${packageJsonPath} could not be read or parsed`,
2407
+ error instanceof Error ? error.message : String(error)
2408
+ );
2409
+ return null;
2410
+ }
2411
+ }
2412
+ async function runDaemonHealthCheck({
2413
+ trackedSessionPids,
2414
+ removeTrackedSession,
2415
+ currentCliVersion,
2416
+ readProjectCliVersion = readProjectCliVersionFromDisk,
2417
+ onDaemonOutdated,
2418
+ readDaemonState,
2419
+ daemonPid,
2420
+ requestShutdown,
2421
+ writeHeartbeat
2422
+ }) {
2423
+ for (const pid of trackedSessionPids) {
2424
+ try {
2425
+ process.kill(pid, 0);
2426
+ } catch {
2427
+ api.logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
2428
+ removeTrackedSession(pid);
2429
+ }
2430
+ }
2431
+ const projectVersion = readProjectCliVersion();
2432
+ if (projectVersion && projectVersion !== currentCliVersion) {
2433
+ await onDaemonOutdated();
2434
+ return;
2435
+ }
2436
+ const daemonState = await readDaemonState();
2437
+ if (daemonState && daemonState.pid !== daemonPid) {
2438
+ api.logger.debug(`[DAEMON RUN] ${DIFFERENT_DAEMON_RUNNING_MESSAGE}`);
2439
+ requestShutdown("exception", DIFFERENT_DAEMON_RUNNING_MESSAGE);
2440
+ return;
2441
+ }
2442
+ await writeHeartbeat();
2443
+ }
2444
+
2445
+ async function atomicFileWrite(filePath, content) {
2446
+ const tmpFile = `${filePath}.${crypto.randomUUID()}.tmp`;
2447
+ try {
2448
+ await fs$2.writeFile(tmpFile, content);
2449
+ await fs$2.rename(tmpFile, filePath);
2450
+ } catch (error) {
2451
+ try {
2452
+ await fs$2.unlink(tmpFile);
2453
+ } catch {
2454
+ }
2455
+ throw error;
2456
+ }
2457
+ }
2458
+
2459
+ const SESSION_REGISTRY_VERSION = 1;
2460
+ const SESSION_REGISTRY_DIRNAME = "session-registry";
2461
+ const NON_SESSION_PROCESS_TYPES = /* @__PURE__ */ new Set([
2462
+ "current",
2463
+ "daemon",
2464
+ "daemon-indeterminate",
2465
+ "dev-daemon",
2466
+ "daemon-launcher",
2467
+ "dev-daemon-launcher",
2468
+ "daemon-version-check",
2469
+ "dev-daemon-version-check",
2470
+ "doctor",
2471
+ "dev-doctor"
2472
+ ]);
2473
+ const registeredCleanupPids = /* @__PURE__ */ new Set();
2474
+ function getSessionRegistryDir() {
2475
+ return path.join(api.configuration.happyCloudHomeDir, SESSION_REGISTRY_DIRNAME);
2476
+ }
2477
+ function getSessionRegistryEntryPath(pid) {
2478
+ return path.join(getSessionRegistryDir(), `${pid}.json`);
2479
+ }
2480
+ function normalizeMetadataForPid(metadata, pid) {
2481
+ if (metadata.hostPid === pid) {
2482
+ return metadata;
2483
+ }
2484
+ return {
2485
+ ...metadata,
2486
+ hostPid: pid
2487
+ };
2488
+ }
2489
+ function createTrackedSessionFromRegistryEntry(entry) {
2490
+ const metadata = normalizeMetadataForPid(entry.metadata, entry.pid);
2491
+ return {
2492
+ startedBy: metadata.startedBy === "daemon" ? "daemon" : "happy directly - likely by user from terminal",
2493
+ happySessionId: entry.sessionId,
2494
+ happySessionMetadataFromLocalWebhook: metadata,
2495
+ pid: entry.pid
2496
+ };
2497
+ }
2498
+ async function ensureSessionRegistryDir() {
2499
+ await promises.mkdir(getSessionRegistryDir(), { recursive: true });
2500
+ }
2501
+ async function removeRegistryEntryPath(path) {
2502
+ try {
2503
+ await promises.unlink(path);
2504
+ return true;
2505
+ } catch (error) {
2506
+ if (error?.code === "ENOENT") {
2507
+ return false;
2508
+ }
2509
+ api.logger.debug(`[SESSION REGISTRY] Failed to remove ${path}`, error);
2510
+ return false;
2511
+ }
2512
+ }
2513
+ function removeRegistryEntryPathSync(path) {
2514
+ try {
2515
+ fs.unlinkSync(path);
2516
+ } catch (error) {
2517
+ if (error?.code !== "ENOENT") {
2518
+ api.logger.debug(`[SESSION REGISTRY] Failed to remove ${path} during process cleanup`, error);
2519
+ }
2520
+ }
2521
+ }
2522
+ function installRegistryCleanup(pid) {
2523
+ if (registeredCleanupPids.has(pid)) {
2524
+ return;
2525
+ }
2526
+ registeredCleanupPids.add(pid);
2527
+ let cleaned = false;
2528
+ const cleanup = () => {
2529
+ if (cleaned) {
2530
+ return;
2531
+ }
2532
+ cleaned = true;
2533
+ removeRegistryEntryPathSync(getSessionRegistryEntryPath(pid));
2534
+ };
2535
+ process.once("exit", cleanup);
2536
+ }
2537
+ async function readRegistryEntry(path) {
2538
+ try {
2539
+ const raw = JSON.parse(await promises.readFile(path, "utf8"));
2540
+ if (raw.version !== SESSION_REGISTRY_VERSION || typeof raw.pid !== "number" || typeof raw.sessionId !== "string" || !raw.sessionId || !raw.metadata || typeof raw.metadata !== "object") {
2541
+ api.logger.debug(`[SESSION REGISTRY] Invalid entry schema at ${path}, pruning`);
2542
+ await removeRegistryEntryPath(path);
2543
+ return null;
2544
+ }
2545
+ return {
2546
+ version: SESSION_REGISTRY_VERSION,
2547
+ pid: raw.pid,
2548
+ sessionId: raw.sessionId,
2549
+ metadata: normalizeMetadataForPid(raw.metadata, raw.pid),
2550
+ updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : Date.now()
2551
+ };
2552
+ } catch (error) {
2553
+ api.logger.debug(`[SESSION REGISTRY] Failed to read ${path}, pruning`, error);
2554
+ await removeRegistryEntryPath(path);
2555
+ return null;
2556
+ }
2557
+ }
2558
+ async function resolveLiveHappySessionPids(listLiveHappyProcesses) {
2559
+ try {
2560
+ const liveHappyProcesses = await listLiveHappyProcesses();
2561
+ if (liveHappyProcesses.length === 0) {
2562
+ return null;
2563
+ }
2564
+ const liveSessionProcesses = liveHappyProcesses.filter((proc) => !NON_SESSION_PROCESS_TYPES.has(proc.type));
2565
+ if (liveSessionProcesses.length === 0) {
2566
+ api.logger.debug(
2567
+ "[SESSION REGISTRY] Process discovery did not report any session processes, falling back to PID checks"
2568
+ );
2569
+ return null;
2570
+ }
2571
+ return new Set(
2572
+ liveSessionProcesses.map((proc) => proc.pid)
2573
+ );
2574
+ } catch (error) {
2575
+ api.logger.debug("[SESSION REGISTRY] Failed to list live Happy processes, falling back to PID checks", error);
2576
+ return null;
2577
+ }
2578
+ }
2579
+ async function persistLocalSessionRegistration(sessionId, metadata) {
2580
+ const pid = metadata.hostPid ?? process.pid;
2581
+ const normalizedMetadata = normalizeMetadataForPid(metadata, pid);
2582
+ await ensureSessionRegistryDir();
2583
+ await atomicFileWrite(
2584
+ getSessionRegistryEntryPath(pid),
2585
+ JSON.stringify({
2586
+ version: SESSION_REGISTRY_VERSION,
2587
+ pid,
2588
+ sessionId,
2589
+ metadata: normalizedMetadata,
2590
+ updatedAt: Date.now()
2591
+ }, null, 2)
2592
+ );
2593
+ }
2594
+ async function publishSessionRegistration(sessionId, metadata) {
2595
+ const pid = metadata.hostPid ?? process.pid;
2596
+ try {
2597
+ await persistLocalSessionRegistration(sessionId, metadata);
2598
+ installRegistryCleanup(pid);
2599
+ } catch (error) {
2600
+ api.logger.debug(`[SESSION REGISTRY] Failed to persist local registration for session ${sessionId}`, error);
2601
+ }
2602
+ try {
2603
+ const result = await notifyDaemonSessionStarted(sessionId, normalizeMetadataForPid(metadata, pid));
2604
+ if (result?.error) {
2605
+ api.logger.debug(`[SESSION REGISTRY] Failed to report session ${sessionId} to daemon`, result.error);
2606
+ }
2607
+ } catch (error) {
2608
+ api.logger.debug(`[SESSION REGISTRY] Failed to report session ${sessionId} to daemon`, error);
2609
+ }
2610
+ }
2611
+ async function recoverTrackedSessionsFromLocalRegistry({
2612
+ trackedSessionPids,
2613
+ trackSession,
2614
+ listLiveHappyProcesses = findAllHappyProcesses,
2615
+ lookupHappyProcessByPid = findHappyProcessByPid
2616
+ }) {
2617
+ const registryDir = getSessionRegistryDir();
2618
+ if (!fs.existsSync(registryDir)) {
2619
+ return { recoveredCount: 0, removedStaleCount: 0 };
2620
+ }
2621
+ const alreadyTracked = new Set(trackedSessionPids);
2622
+ const liveHappySessionPids = await resolveLiveHappySessionPids(listLiveHappyProcesses);
2623
+ let recoveredCount = 0;
2624
+ let removedStaleCount = 0;
2625
+ for (const entryFile of await promises.readdir(registryDir)) {
2626
+ if (!entryFile.endsWith(".json")) {
2627
+ continue;
2628
+ }
2629
+ const entryPath = path.join(registryDir, entryFile);
2630
+ const entry = await readRegistryEntry(entryPath);
2631
+ if (!entry) {
2632
+ continue;
2633
+ }
2634
+ let lookupResult;
2635
+ try {
2636
+ lookupResult = liveHappySessionPids?.has(entry.pid) ? { pid: entry.pid, type: "known-session" } : await lookupHappyProcessByPid(entry.pid);
2637
+ } catch (error) {
2638
+ api.logger.debug(
2639
+ `[SESSION REGISTRY] Failed to inspect PID ${entry.pid}, keeping registry entry until next recovery tick`,
2640
+ error
2641
+ );
2642
+ lookupResult = "indeterminate";
2643
+ }
2644
+ const sessionPidIsAlive = lookupResult !== null && lookupResult !== "indeterminate" && !NON_SESSION_PROCESS_TYPES.has(lookupResult.type);
2645
+ const shouldKeepEntryWithoutRecovery = lookupResult === "indeterminate";
2646
+ if (shouldKeepEntryWithoutRecovery) {
2647
+ api.logger.debug(
2648
+ `[SESSION REGISTRY] Keeping registry entry for PID ${entry.pid} because process identity is indeterminate`
2649
+ );
2650
+ continue;
2651
+ }
2652
+ if (!sessionPidIsAlive) {
2653
+ if (await removeRegistryEntryPath(entryPath)) {
2654
+ removedStaleCount++;
2655
+ }
2656
+ continue;
2657
+ }
2658
+ if (alreadyTracked.has(entry.pid)) {
2659
+ continue;
2660
+ }
2661
+ trackSession(entry.pid, createTrackedSessionFromRegistryEntry(entry));
2662
+ alreadyTracked.add(entry.pid);
2663
+ recoveredCount++;
2664
+ api.logger.debug(`[SESSION REGISTRY] Recovered tracked session ${entry.sessionId} for PID ${entry.pid}`);
2665
+ }
2666
+ return { recoveredCount, removedStaleCount };
2667
+ }
2668
+
2291
2669
  const initialMachineMetadata = {
2292
2670
  host: os$1.hostname(),
2293
2671
  platform: os$1.platform(),
@@ -2417,7 +2795,7 @@ async function startDaemon() {
2417
2795
  const { directory, sessionId, machineId: machineId2, approvedNewDirectoryCreation = true } = options;
2418
2796
  let directoryCreated = false;
2419
2797
  try {
2420
- await fs$1.access(directory);
2798
+ await fs$2.access(directory);
2421
2799
  api.logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
2422
2800
  } catch (error) {
2423
2801
  api.logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
@@ -2429,7 +2807,7 @@ async function startDaemon() {
2429
2807
  };
2430
2808
  }
2431
2809
  try {
2432
- await fs$1.mkdir(directory, { recursive: true });
2810
+ await fs$2.mkdir(directory, { recursive: true });
2433
2811
  api.logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
2434
2812
  directoryCreated = true;
2435
2813
  } catch (mkdirError) {
@@ -2458,7 +2836,7 @@ async function startDaemon() {
2458
2836
  if (options.token) {
2459
2837
  if (options.agent === "codex") {
2460
2838
  const codexHomeDir = tmp__namespace.dirSync();
2461
- fs$1.writeFile(path.join(codexHomeDir.name, "auth.json"), options.token);
2839
+ fs$2.writeFile(path$1.join(codexHomeDir.name, "auth.json"), options.token);
2462
2840
  authEnv.CODEX_HOME = codexHomeDir.name;
2463
2841
  } else {
2464
2842
  authEnv.CLAUDE_CODE_OAUTH_TOKEN = options.token;
@@ -2511,20 +2889,18 @@ async function startDaemon() {
2511
2889
  errorMessage: spawnError.errorMessage
2512
2890
  };
2513
2891
  }
2514
- const tmuxAvailable = await isTmuxAvailable();
2515
- let useTmux = tmuxAvailable;
2516
2892
  let tmuxSessionName = extraEnv.TMUX_SESSION_NAME;
2517
- if (!tmuxAvailable || tmuxSessionName === void 0) {
2518
- useTmux = false;
2519
- if (tmuxSessionName !== void 0) {
2520
- api.logger.debug(`[DAEMON RUN] tmux session name specified but tmux not available, falling back to regular spawning`);
2521
- }
2893
+ const tmuxRequested = tmuxSessionName !== void 0;
2894
+ const tmuxAvailable = tmuxRequested ? await isTmuxAvailable() : false;
2895
+ let useTmux = tmuxRequested && tmuxAvailable;
2896
+ if (tmuxRequested && !tmuxAvailable) {
2897
+ api.logger.debug(`[DAEMON RUN] tmux session name specified but tmux not available, falling back to regular spawning`);
2522
2898
  }
2523
2899
  if (useTmux && tmuxSessionName !== void 0) {
2524
2900
  const sessionDesc = tmuxSessionName || "current/most recent session";
2525
2901
  api.logger.debug(`[DAEMON RUN] Attempting to spawn session in tmux: ${sessionDesc}`);
2526
2902
  const tmux = getTmuxUtilities(tmuxSessionName);
2527
- const cliPath = path.join(projectPath(), "dist", "index.mjs");
2903
+ const cliPath = path$1.join(projectPath(), "dist", "index.mjs");
2528
2904
  const agent = resolveDaemonSpawnAgent(options.agent);
2529
2905
  const fullCommand = `node --no-warnings --no-deprecation ${cliPath} ${buildDaemonSpawnArgs(agent).join(" ")}`;
2530
2906
  const windowName = `happy-${Date.now()}-${agent}`;
@@ -2723,6 +3099,17 @@ async function startDaemon() {
2723
3099
  onHappySessionWebhook,
2724
3100
  port: api.HAPPY_CLOUD_DAEMON_PORT
2725
3101
  });
3102
+ const recoveryResult = await recoverTrackedSessionsFromLocalRegistry({
3103
+ trackedSessionPids: pidToTrackedSession.keys(),
3104
+ trackSession: (pid, trackedSession) => {
3105
+ pidToTrackedSession.set(pid, trackedSession);
3106
+ }
3107
+ });
3108
+ if (recoveryResult.recoveredCount > 0 || recoveryResult.removedStaleCount > 0) {
3109
+ api.logger.debug(
3110
+ `[DAEMON RUN] Session registry recovery completed: recovered=${recoveryResult.recoveredCount}, removedStale=${recoveryResult.removedStaleCount}`
3111
+ );
3112
+ }
2726
3113
  const fileState = {
2727
3114
  pid: process.pid,
2728
3115
  httpPort: controlPort,
@@ -2762,52 +3149,69 @@ async function startDaemon() {
2762
3149
  if (process.env.DEBUG) {
2763
3150
  api.logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
2764
3151
  }
2765
- for (const [pid, _] of pidToTrackedSession.entries()) {
2766
- try {
2767
- process.kill(pid, 0);
2768
- } catch (error) {
2769
- api.logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
2770
- pidToTrackedSession.delete(pid);
2771
- }
2772
- }
2773
- const projectVersion = JSON.parse(fs.readFileSync(path.join(projectPath(), "package.json"), "utf-8")).version;
2774
- if (projectVersion !== api.configuration.currentCliVersion) {
2775
- api.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
2776
- clearInterval(restartOnStaleVersionAndHeartbeat);
2777
- try {
2778
- spawnHappyCLI(["daemon", "start"], {
2779
- detached: true,
2780
- stdio: "ignore"
2781
- });
2782
- } catch (error) {
2783
- api.logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
2784
- }
2785
- api.logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
2786
- await new Promise((resolve) => setTimeout(resolve, 1e4));
2787
- process.exit(0);
2788
- }
2789
- const daemonState = await persistence.readDaemonState();
2790
- if (daemonState && daemonState.pid !== process.pid) {
2791
- api.logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
2792
- requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
2793
- }
2794
3152
  try {
2795
- const updatedState = {
2796
- pid: process.pid,
2797
- httpPort: controlPort,
2798
- startTime: fileState.startTime,
2799
- startedWithCliVersion: api.packageJson.version,
2800
- lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
2801
- daemonLogPath: fileState.daemonLogPath
2802
- };
2803
- persistence.writeDaemonState(updatedState);
2804
- if (process.env.DEBUG) {
2805
- api.logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
3153
+ const recoveryResult2 = await recoverTrackedSessionsFromLocalRegistry({
3154
+ trackedSessionPids: pidToTrackedSession.keys(),
3155
+ trackSession: (pid, trackedSession) => {
3156
+ pidToTrackedSession.set(pid, trackedSession);
3157
+ }
3158
+ });
3159
+ if (recoveryResult2.recoveredCount > 0 || recoveryResult2.removedStaleCount > 0) {
3160
+ api.logger.debug(
3161
+ `[DAEMON RUN] Session registry recovery tick completed: recovered=${recoveryResult2.recoveredCount}, removedStale=${recoveryResult2.removedStaleCount}`
3162
+ );
2806
3163
  }
3164
+ await runDaemonHealthCheck({
3165
+ trackedSessionPids: pidToTrackedSession.keys(),
3166
+ removeTrackedSession: (pid) => {
3167
+ pidToTrackedSession.delete(pid);
3168
+ },
3169
+ currentCliVersion: api.configuration.currentCliVersion,
3170
+ onDaemonOutdated: async () => {
3171
+ api.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
3172
+ clearInterval(restartOnStaleVersionAndHeartbeat);
3173
+ try {
3174
+ spawnHappyCLI(["daemon", "start"], {
3175
+ detached: true,
3176
+ stdio: "ignore"
3177
+ });
3178
+ } catch (error) {
3179
+ api.logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
3180
+ }
3181
+ api.logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
3182
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
3183
+ process.exit(0);
3184
+ },
3185
+ readDaemonState: persistence.readDaemonState,
3186
+ daemonPid: process.pid,
3187
+ requestShutdown,
3188
+ writeHeartbeat: async () => {
3189
+ try {
3190
+ const updatedState = {
3191
+ pid: process.pid,
3192
+ httpPort: controlPort,
3193
+ startTime: fileState.startTime,
3194
+ startedWithCliVersion: api.packageJson.version,
3195
+ lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
3196
+ daemonLogPath: fileState.daemonLogPath
3197
+ };
3198
+ persistence.writeDaemonState(updatedState);
3199
+ if (process.env.DEBUG) {
3200
+ api.logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
3201
+ }
3202
+ } catch (error) {
3203
+ api.logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
3204
+ }
3205
+ }
3206
+ });
2807
3207
  } catch (error) {
2808
- api.logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
3208
+ api.logger.warn(
3209
+ "[DAEMON RUN] Health check failed; keeping daemon alive until the next tick",
3210
+ error instanceof Error ? error.message : String(error)
3211
+ );
3212
+ } finally {
3213
+ heartbeatRunning = false;
2809
3214
  }
2810
- heartbeatRunning = false;
2811
3215
  }, heartbeatIntervalMs);
2812
3216
  const cleanupAndShutdown = async (source, errorMessage) => {
2813
3217
  api.logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
@@ -2863,7 +3267,7 @@ const PLIST_LABEL$1 = "com.happy-cloud.daemon";
2863
3267
  const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
2864
3268
  async function install$1() {
2865
3269
  try {
2866
- if (fs.existsSync(PLIST_FILE$1)) {
3270
+ if (fs$1.existsSync(PLIST_FILE$1)) {
2867
3271
  api.logger.info("Daemon plist already exists. Uninstalling first...");
2868
3272
  child_process.execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
2869
3273
  }
@@ -2907,8 +3311,8 @@ async function install$1() {
2907
3311
  </dict>
2908
3312
  </plist>
2909
3313
  `);
2910
- fs.writeFileSync(PLIST_FILE$1, plistContent);
2911
- fs.chmodSync(PLIST_FILE$1, 420);
3314
+ fs$1.writeFileSync(PLIST_FILE$1, plistContent);
3315
+ fs$1.chmodSync(PLIST_FILE$1, 420);
2912
3316
  api.logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
2913
3317
  child_process.execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
2914
3318
  api.logger.info("Daemon installed and started successfully");
@@ -2934,7 +3338,7 @@ const PLIST_LABEL = "com.happy-cli.daemon";
2934
3338
  const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
2935
3339
  async function uninstall$1() {
2936
3340
  try {
2937
- if (!fs.existsSync(PLIST_FILE)) {
3341
+ if (!fs$1.existsSync(PLIST_FILE)) {
2938
3342
  api.logger.info("Daemon plist not found. Nothing to uninstall.");
2939
3343
  return;
2940
3344
  }
@@ -2944,7 +3348,7 @@ async function uninstall$1() {
2944
3348
  } catch (error) {
2945
3349
  api.logger.info("Failed to unload daemon (it might not be running)");
2946
3350
  }
2947
- fs.unlinkSync(PLIST_FILE);
3351
+ fs$1.unlinkSync(PLIST_FILE);
2948
3352
  api.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
2949
3353
  api.logger.info("Daemon uninstalled successfully");
2950
3354
  } catch (error) {
@@ -3084,8 +3488,8 @@ async function handleAuthLogout(args = []) {
3084
3488
  console.log(chalk.gray("Stopped daemon"));
3085
3489
  } catch {
3086
3490
  }
3087
- if (node_fs.existsSync(happyDir)) {
3088
- node_fs.rmSync(happyDir, { recursive: true, force: true });
3491
+ if (fs.existsSync(happyDir)) {
3492
+ fs.rmSync(happyDir, { recursive: true, force: true });
3089
3493
  }
3090
3494
  console.log(chalk.green("\u2713 Successfully logged out"));
3091
3495
  console.log(chalk.gray(' Run "hicloud auth login" to authenticate again'));
@@ -3120,9 +3524,11 @@ async function handleAuthStatus() {
3120
3524
  console.log(chalk.gray(`
3121
3525
  Data directory: ${api.configuration.happyCloudHomeDir}`));
3122
3526
  try {
3123
- const running = await checkIfDaemonRunningAndCleanupStaleState();
3124
- if (running) {
3527
+ const daemonStatus = await getDaemonRuntimeStatus();
3528
+ if (daemonStatus === "running") {
3125
3529
  console.log(chalk.green("\u2713 Daemon running"));
3530
+ } else if (daemonStatus === "indeterminate") {
3531
+ console.log(chalk.yellow("\u26A0\uFE0F Daemon status indeterminate"));
3126
3532
  } else {
3127
3533
  console.log(chalk.gray("\u2717 Daemon not running"));
3128
3534
  }
@@ -3758,10 +4164,10 @@ async function handleConnectStatus() {
3758
4164
  }
3759
4165
  function updateLocalGeminiCredentials(tokens) {
3760
4166
  try {
3761
- const geminiDir = path.join(os$1.homedir(), ".gemini");
3762
- const credentialsPath = path.join(geminiDir, "oauth_creds.json");
3763
- if (!fs.existsSync(geminiDir)) {
3764
- fs.mkdirSync(geminiDir, { recursive: true });
4167
+ const geminiDir = path$1.join(os$1.homedir(), ".gemini");
4168
+ const credentialsPath = path$1.join(geminiDir, "oauth_creds.json");
4169
+ if (!fs$1.existsSync(geminiDir)) {
4170
+ fs$1.mkdirSync(geminiDir, { recursive: true });
3765
4171
  }
3766
4172
  const credentials = {
3767
4173
  access_token: tokens.access_token,
@@ -3771,7 +4177,7 @@ function updateLocalGeminiCredentials(tokens) {
3771
4177
  ...tokens.id_token && { id_token: tokens.id_token },
3772
4178
  ...tokens.expires_in && { expires_in: tokens.expires_in }
3773
4179
  };
3774
- fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), "utf-8");
4180
+ fs$1.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), "utf-8");
3775
4181
  console.log(chalk.gray(` Updated local credentials: ${credentialsPath}`));
3776
4182
  } catch (error) {
3777
4183
  console.log(chalk.yellow(` \u26A0\uFE0F Could not update local credentials: ${error}`));
@@ -3779,22 +4185,22 @@ function updateLocalGeminiCredentials(tokens) {
3779
4185
  }
3780
4186
 
3781
4187
  function getProjectPath(workingDirectory, claudeConfigDirOverride) {
3782
- const projectId = node_path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
4188
+ const projectId = path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
3783
4189
  const claudeConfigDirRaw = process.env.CLAUDE_CONFIG_DIR ?? "";
3784
4190
  const claudeConfigDirTrimmed = claudeConfigDirRaw.trim();
3785
- const claudeConfigDir = claudeConfigDirTrimmed ? claudeConfigDirTrimmed : node_path.join(os.homedir(), ".claude");
3786
- return node_path.join(claudeConfigDir, "projects", projectId);
4191
+ const claudeConfigDir = claudeConfigDirTrimmed ? claudeConfigDirTrimmed : path.join(os.homedir(), ".claude");
4192
+ return path.join(claudeConfigDir, "projects", projectId);
3787
4193
  }
3788
4194
 
3789
- function claudeCheckSession(sessionId, path, transcriptPath) {
3790
- const projectDir = getProjectPath(path);
3791
- const sessionFile = transcriptPath ?? node_path.join(projectDir, `${sessionId}.jsonl`);
3792
- const sessionExists = node_fs.existsSync(sessionFile);
4195
+ function claudeCheckSession(sessionId, path$1, transcriptPath) {
4196
+ const projectDir = getProjectPath(path$1);
4197
+ const sessionFile = transcriptPath ?? path.join(projectDir, `${sessionId}.jsonl`);
4198
+ const sessionExists = fs.existsSync(sessionFile);
3793
4199
  if (!sessionExists) {
3794
4200
  api.logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
3795
4201
  return false;
3796
4202
  }
3797
- const sessionData = node_fs.readFileSync(sessionFile, "utf-8").split("\n");
4203
+ const sessionData = fs.readFileSync(sessionFile, "utf-8").split("\n");
3798
4204
  const hasGoodMessage = !!sessionData.find((v, index) => {
3799
4205
  if (!v.trim()) return false;
3800
4206
  try {
@@ -3815,7 +4221,7 @@ function claudeFindLastSession(workingDirectory) {
3815
4221
  try {
3816
4222
  const projectDir = getProjectPath(workingDirectory);
3817
4223
  const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3818
- const files = node_fs.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
4224
+ const files = fs.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
3819
4225
  const sessionId = f.replace(".jsonl", "");
3820
4226
  if (!uuidPattern.test(sessionId)) {
3821
4227
  return null;
@@ -3824,7 +4230,7 @@ function claudeFindLastSession(workingDirectory) {
3824
4230
  return {
3825
4231
  name: f,
3826
4232
  sessionId,
3827
- mtime: node_fs.statSync(node_path.join(projectDir, f)).mtime.getTime()
4233
+ mtime: fs.statSync(path.join(projectDir, f)).mtime.getTime()
3828
4234
  };
3829
4235
  }
3830
4236
  return null;
@@ -3844,10 +4250,10 @@ class ExitCodeError extends Error {
3844
4250
  this.exitCode = exitCode;
3845
4251
  }
3846
4252
  }
3847
- const claudeCliPath = node_path.resolve(node_path.join(projectPath(), "scripts", "claude_local_launcher.cjs"));
4253
+ const claudeCliPath = path.resolve(path.join(projectPath(), "scripts", "claude_local_launcher.cjs"));
3848
4254
  async function claudeLocal(opts) {
3849
4255
  const projectDir = getProjectPath(opts.path);
3850
- node_fs.mkdirSync(projectDir, { recursive: true });
4256
+ fs.mkdirSync(projectDir, { recursive: true });
3851
4257
  const hasContinueFlag = opts.claudeArgs?.includes("--continue");
3852
4258
  const hasResumeFlag = opts.claudeArgs?.includes("--resume");
3853
4259
  const hasUserSessionControl = hasContinueFlag || hasResumeFlag;
@@ -3966,7 +4372,7 @@ async function claudeLocal(opts) {
3966
4372
  args.push("--settings", opts.hookSettingsPath);
3967
4373
  api.logger.debug(`[ClaudeLocal] Using hook settings: ${opts.hookSettingsPath}`);
3968
4374
  }
3969
- if (!claudeCliPath || !node_fs.existsSync(claudeCliPath)) {
4375
+ if (!claudeCliPath || !fs.existsSync(claudeCliPath)) {
3970
4376
  throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
3971
4377
  }
3972
4378
  const env = {
@@ -6314,17 +6720,17 @@ function readGeminiLocalConfig() {
6314
6720
  let googleCloudProject = null;
6315
6721
  let googleCloudProjectEmail = null;
6316
6722
  const possiblePaths = [
6317
- path.join(os$1.homedir(), ".gemini", "oauth_creds.json"),
6723
+ path$1.join(os$1.homedir(), ".gemini", "oauth_creds.json"),
6318
6724
  // Main OAuth credentials file
6319
- path.join(os$1.homedir(), ".gemini", "config.json"),
6320
- path.join(os$1.homedir(), ".config", "gemini", "config.json"),
6321
- path.join(os$1.homedir(), ".gemini", "auth.json"),
6322
- path.join(os$1.homedir(), ".config", "gemini", "auth.json")
6725
+ path$1.join(os$1.homedir(), ".gemini", "config.json"),
6726
+ path$1.join(os$1.homedir(), ".config", "gemini", "config.json"),
6727
+ path$1.join(os$1.homedir(), ".gemini", "auth.json"),
6728
+ path$1.join(os$1.homedir(), ".config", "gemini", "auth.json")
6323
6729
  ];
6324
6730
  for (const configPath of possiblePaths) {
6325
- if (fs.existsSync(configPath)) {
6731
+ if (fs$1.existsSync(configPath)) {
6326
6732
  try {
6327
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
6733
+ const config = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
6328
6734
  if (!token) {
6329
6735
  const foundToken = config.access_token || config.token || config.apiKey || config.GEMINI_API_KEY;
6330
6736
  if (foundToken && typeof foundToken === "string") {
@@ -6396,22 +6802,22 @@ function determineGeminiModel(explicitModel, localConfig) {
6396
6802
  }
6397
6803
  function saveGeminiModelToConfig(model) {
6398
6804
  try {
6399
- const configDir = path.join(os$1.homedir(), ".gemini");
6400
- const configPath = path.join(configDir, "config.json");
6401
- if (!fs.existsSync(configDir)) {
6402
- fs.mkdirSync(configDir, { recursive: true });
6805
+ const configDir = path$1.join(os$1.homedir(), ".gemini");
6806
+ const configPath = path$1.join(configDir, "config.json");
6807
+ if (!fs$1.existsSync(configDir)) {
6808
+ fs$1.mkdirSync(configDir, { recursive: true });
6403
6809
  }
6404
6810
  let config = {};
6405
- if (fs.existsSync(configPath)) {
6811
+ if (fs$1.existsSync(configPath)) {
6406
6812
  try {
6407
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
6813
+ config = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
6408
6814
  } catch (error) {
6409
6815
  api.logger.debug(`[Gemini] Failed to read existing config, creating new one`);
6410
6816
  config = {};
6411
6817
  }
6412
6818
  }
6413
6819
  config.model = model;
6414
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6820
+ fs$1.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6415
6821
  api.logger.debug(`[Gemini] Saved model "${model}" to ${configPath}`);
6416
6822
  } catch (error) {
6417
6823
  api.logger.debug(`[Gemini] Failed to save model to config:`, error);
@@ -6419,15 +6825,15 @@ function saveGeminiModelToConfig(model) {
6419
6825
  }
6420
6826
  function saveGoogleCloudProjectToConfig(projectId, email) {
6421
6827
  try {
6422
- const configDir = path.join(os$1.homedir(), ".gemini");
6423
- const configPath = path.join(configDir, "config.json");
6424
- if (!fs.existsSync(configDir)) {
6425
- fs.mkdirSync(configDir, { recursive: true });
6828
+ const configDir = path$1.join(os$1.homedir(), ".gemini");
6829
+ const configPath = path$1.join(configDir, "config.json");
6830
+ if (!fs$1.existsSync(configDir)) {
6831
+ fs$1.mkdirSync(configDir, { recursive: true });
6426
6832
  }
6427
6833
  let config = {};
6428
- if (fs.existsSync(configPath)) {
6834
+ if (fs$1.existsSync(configPath)) {
6429
6835
  try {
6430
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
6836
+ config = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
6431
6837
  } catch {
6432
6838
  config = {};
6433
6839
  }
@@ -6436,7 +6842,7 @@ function saveGoogleCloudProjectToConfig(projectId, email) {
6436
6842
  if (email) {
6437
6843
  config.googleCloudProjectEmail = email;
6438
6844
  }
6439
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6845
+ fs$1.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6440
6846
  api.logger.debug(`[Gemini] Saved Google Cloud Project "${projectId}"${email ? ` for ${email}` : ""} to ${configPath}`);
6441
6847
  } catch (error) {
6442
6848
  api.logger.debug(`[Gemini] Failed to save Google Cloud Project to config:`, error);
@@ -6544,11 +6950,11 @@ function readFirstEnv(...names) {
6544
6950
  return "";
6545
6951
  }
6546
6952
  function normalizeCommandPath(command) {
6547
- if (node_path.isAbsolute(command)) {
6953
+ if (path.isAbsolute(command)) {
6548
6954
  return command;
6549
6955
  }
6550
- const resolved = node_path.resolve(process.cwd(), command);
6551
- return node_fs.existsSync(resolved) ? resolved : command;
6956
+ const resolved = path.resolve(process.cwd(), command);
6957
+ return fs.existsSync(resolved) ? resolved : command;
6552
6958
  }
6553
6959
  function resolveCommandOnPath(command) {
6554
6960
  const pathValue = typeof process.env.PATH === "string" ? process.env.PATH : "";
@@ -6556,13 +6962,13 @@ function resolveCommandOnPath(command) {
6556
6962
  return null;
6557
6963
  }
6558
6964
  const extensions = process.platform === "win32" ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((value) => value.trim().toLowerCase()).filter(Boolean) : [""];
6559
- for (const dir of pathValue.split(node_path.delimiter)) {
6965
+ for (const dir of pathValue.split(path.delimiter)) {
6560
6966
  const trimmedDir = dir.trim();
6561
6967
  if (!trimmedDir) {
6562
6968
  continue;
6563
6969
  }
6564
- const directCandidate = node_path.join(trimmedDir, command);
6565
- if (node_fs.existsSync(directCandidate)) {
6970
+ const directCandidate = path.join(trimmedDir, command);
6971
+ if (fs.existsSync(directCandidate)) {
6566
6972
  return directCandidate;
6567
6973
  }
6568
6974
  if (process.platform !== "win32") {
@@ -6573,8 +6979,8 @@ function resolveCommandOnPath(command) {
6573
6979
  continue;
6574
6980
  }
6575
6981
  for (const extension of extensions) {
6576
- const candidate = node_path.join(trimmedDir, `${command}${extension.toLowerCase()}`);
6577
- if (node_fs.existsSync(candidate)) {
6982
+ const candidate = path.join(trimmedDir, `${command}${extension.toLowerCase()}`);
6983
+ if (fs.existsSync(candidate)) {
6578
6984
  return candidate;
6579
6985
  }
6580
6986
  }
@@ -6673,8 +7079,8 @@ function validateCodexAcpSpawn(options = {}) {
6673
7079
  const normalizedCommand = spawn.command.trim();
6674
7080
  const commandLower = normalizedCommand.toLowerCase();
6675
7081
  const npxMode = readCodexAcpNpxMode();
6676
- if (node_path.isAbsolute(normalizedCommand)) {
6677
- if (!node_fs.existsSync(normalizedCommand)) {
7082
+ if (path.isAbsolute(normalizedCommand)) {
7083
+ if (!fs.existsSync(normalizedCommand)) {
6678
7084
  return {
6679
7085
  ok: false,
6680
7086
  errorMessage: `Codex ACP is enabled, but the resolved command does not exist: ${normalizedCommand}`
@@ -6703,6 +7109,189 @@ function validateCodexAcpSpawn(options = {}) {
6703
7109
  return { ok: true, spawn };
6704
7110
  }
6705
7111
 
7112
+ function firstExistingPath(candidates) {
7113
+ for (const candidate of candidates) {
7114
+ try {
7115
+ if (fs.existsSync(candidate)) {
7116
+ return candidate;
7117
+ }
7118
+ } catch {
7119
+ }
7120
+ }
7121
+ return null;
7122
+ }
7123
+ function resolveCodexExecutable() {
7124
+ if (process.platform === "win32") {
7125
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
7126
+ const npmGlobalBin = path.join(appData, "npm");
7127
+ const resolved = firstExistingPath([
7128
+ path.join(npmGlobalBin, "codex.cmd"),
7129
+ path.join(npmGlobalBin, "codex.ps1"),
7130
+ path.join(npmGlobalBin, "codex")
7131
+ ]);
7132
+ if (resolved) {
7133
+ return resolved;
7134
+ }
7135
+ }
7136
+ return "codex";
7137
+ }
7138
+
7139
+ function getCodexPlatformTarget(platform, arch) {
7140
+ if (platform === "win32" && arch === "x64") {
7141
+ return {
7142
+ packageName: "codex-win32-x64",
7143
+ targetTriple: "x86_64-pc-windows-msvc"
7144
+ };
7145
+ }
7146
+ if (platform === "win32" && arch === "arm64") {
7147
+ return {
7148
+ packageName: "codex-win32-arm64",
7149
+ targetTriple: "aarch64-pc-windows-msvc"
7150
+ };
7151
+ }
7152
+ if ((platform === "linux" || platform === "android") && arch === "x64") {
7153
+ return {
7154
+ packageName: "codex-linux-x64",
7155
+ targetTriple: "x86_64-unknown-linux-musl"
7156
+ };
7157
+ }
7158
+ if ((platform === "linux" || platform === "android") && arch === "arm64") {
7159
+ return {
7160
+ packageName: "codex-linux-arm64",
7161
+ targetTriple: "aarch64-unknown-linux-musl"
7162
+ };
7163
+ }
7164
+ if (platform === "darwin" && arch === "x64") {
7165
+ return {
7166
+ packageName: "codex-darwin-x64",
7167
+ targetTriple: "x86_64-apple-darwin"
7168
+ };
7169
+ }
7170
+ if (platform === "darwin" && arch === "arm64") {
7171
+ return {
7172
+ packageName: "codex-darwin-arm64",
7173
+ targetTriple: "aarch64-apple-darwin"
7174
+ };
7175
+ }
7176
+ return null;
7177
+ }
7178
+ function dedupePaths(paths, platform) {
7179
+ const seen = /* @__PURE__ */ new Set();
7180
+ const unique = [];
7181
+ for (const entry of paths) {
7182
+ const normalized = path.normalize(entry);
7183
+ const key = platform === "win32" ? normalized.toLowerCase() : normalized;
7184
+ if (seen.has(key)) {
7185
+ continue;
7186
+ }
7187
+ seen.add(key);
7188
+ unique.push(entry);
7189
+ }
7190
+ return unique;
7191
+ }
7192
+ function resolveCodexShimPath({
7193
+ platform = process.platform,
7194
+ exists = fs.existsSync,
7195
+ resolveExecutable = resolveCodexExecutable,
7196
+ resolveOnPath = resolveCommandOnPath
7197
+ }) {
7198
+ const resolvedExecutable = resolveExecutable();
7199
+ if (path.isAbsolute(resolvedExecutable) && exists(resolvedExecutable)) {
7200
+ return resolvedExecutable;
7201
+ }
7202
+ const commandNames = platform === "win32" ? ["codex.cmd", "codex.ps1", "codex"] : ["codex"];
7203
+ for (const commandName of commandNames) {
7204
+ const resolved = resolveOnPath(commandName);
7205
+ if (resolved) {
7206
+ return resolved;
7207
+ }
7208
+ }
7209
+ return null;
7210
+ }
7211
+ function resolveCodexPackageRoots(executablePath, {
7212
+ platform = process.platform,
7213
+ exists = fs.existsSync,
7214
+ realpath = fs.realpathSync
7215
+ }) {
7216
+ const executableDir = path.dirname(executablePath);
7217
+ const candidates = [
7218
+ path.join(executableDir, "node_modules", "@openai", "codex"),
7219
+ path.join(executableDir, "..", "lib", "node_modules", "@openai", "codex")
7220
+ ];
7221
+ try {
7222
+ const realExecutablePath = realpath(executablePath);
7223
+ candidates.push(path.join(path.dirname(realExecutablePath), ".."));
7224
+ } catch {
7225
+ }
7226
+ return dedupePaths(
7227
+ candidates.filter((candidate) => exists(candidate)),
7228
+ platform
7229
+ );
7230
+ }
7231
+ function resolvePathEnvKey(env) {
7232
+ return Object.prototype.hasOwnProperty.call(env, "Path") && !Object.prototype.hasOwnProperty.call(env, "PATH") ? "Path" : "PATH";
7233
+ }
7234
+ function resolveBundledCodexToolPathDirs(options = {}) {
7235
+ const platform = options.platform ?? process.platform;
7236
+ const arch = options.arch ?? process.arch;
7237
+ const codexTarget = getCodexPlatformTarget(platform, arch);
7238
+ if (!codexTarget) {
7239
+ return [];
7240
+ }
7241
+ const codexShimPath = resolveCodexShimPath(options);
7242
+ if (!codexShimPath) {
7243
+ return [];
7244
+ }
7245
+ const packageRoots = resolveCodexPackageRoots(codexShimPath, options);
7246
+ if (packageRoots.length === 0) {
7247
+ return [];
7248
+ }
7249
+ const exists = options.exists ?? fs.existsSync;
7250
+ const candidates = packageRoots.flatMap((packageRoot) => [
7251
+ path.join(packageRoot, "vendor", codexTarget.targetTriple, "path"),
7252
+ path.join(
7253
+ packageRoot,
7254
+ "node_modules",
7255
+ "@openai",
7256
+ codexTarget.packageName,
7257
+ "vendor",
7258
+ codexTarget.targetTriple,
7259
+ "path"
7260
+ )
7261
+ ]);
7262
+ return dedupePaths(
7263
+ candidates.filter((candidate) => exists(candidate)),
7264
+ platform
7265
+ );
7266
+ }
7267
+ function buildCodexAcpEnv(overrides = {}, options = {}) {
7268
+ const platform = options.platform ?? process.platform;
7269
+ const env = {
7270
+ ...process.env,
7271
+ ...overrides
7272
+ };
7273
+ const pathKey = resolvePathEnvKey(env);
7274
+ const alternatePathKey = pathKey === "PATH" ? "Path" : "PATH";
7275
+ const currentPathValue = env[pathKey] ?? env[alternatePathKey] ?? "";
7276
+ const currentPathEntries = currentPathValue.split(path.delimiter).filter(Boolean);
7277
+ const codexToolDirs = resolveBundledCodexToolPathDirs(options);
7278
+ const mergedPathEntries = dedupePaths(
7279
+ [...codexToolDirs, ...currentPathEntries],
7280
+ platform
7281
+ );
7282
+ if (mergedPathEntries.length > 0) {
7283
+ env[pathKey] = mergedPathEntries.join(path.delimiter);
7284
+ }
7285
+ delete env[alternatePathKey];
7286
+ const stringEnvEntries = [];
7287
+ for (const [key, value] of Object.entries(env)) {
7288
+ if (typeof value === "string") {
7289
+ stringEnvEntries.push([key, value]);
7290
+ }
7291
+ }
7292
+ return Object.fromEntries(stringEnvEntries);
7293
+ }
7294
+
6706
7295
  class CodexAcpTransport extends CodexTransport {
6707
7296
  constructor(initTimeoutMs) {
6708
7297
  super();
@@ -6732,10 +7321,10 @@ function createCodexBackend(options) {
6732
7321
  cwd: options.cwd,
6733
7322
  command: spawn.command,
6734
7323
  args: spawn.args,
6735
- env: {
7324
+ env: buildCodexAcpEnv({
6736
7325
  ...options.env,
6737
7326
  NODE_ENV: "production"
6738
- },
7327
+ }),
6739
7328
  permissionHandler: options.permissionHandler,
6740
7329
  selectionHandler: options.selectionHandler,
6741
7330
  transportHandler: resolveCodexTransport(spawn.command)
@@ -6885,6 +7474,24 @@ function createDefaultRuntimeShell() {
6885
7474
  return new RuntimeShell();
6886
7475
  }
6887
7476
 
7477
+ const DAEMON_STARTUP_TIMEOUT_MS = 3e4;
7478
+ const DAEMON_STARTUP_POLL_INTERVAL_MS = 250;
7479
+ async function waitForDaemonReady(timeoutMs = DAEMON_STARTUP_TIMEOUT_MS, pollIntervalMs = DAEMON_STARTUP_POLL_INTERVAL_MS) {
7480
+ const deadline = Date.now() + timeoutMs;
7481
+ while (Date.now() < deadline) {
7482
+ const remainingMs = deadline - Date.now();
7483
+ if (await isDaemonRunningCurrentlyInstalledHappyVersion(Math.max(1, remainingMs))) {
7484
+ return true;
7485
+ }
7486
+ const sleepMs = Math.min(pollIntervalMs, deadline - Date.now());
7487
+ if (sleepMs <= 0) {
7488
+ break;
7489
+ }
7490
+ await new Promise((resolve) => setTimeout(resolve, sleepMs));
7491
+ }
7492
+ return false;
7493
+ }
7494
+
6888
7495
  function isRuntimeProvider(value) {
6889
7496
  return value === "claude" || value === "codex" || value === "gemini" || value === "cursor";
6890
7497
  }
@@ -6929,26 +7536,20 @@ async function ensureUnifiedDaemonStarted() {
6929
7536
  env: process.env
6930
7537
  });
6931
7538
  daemonProcess.unref();
6932
- for (let i = 0; i < 100; i++) {
6933
- if (await isDaemonRunningCurrentlyInstalledHappyVersion()) {
6934
- return;
6935
- }
6936
- if (await isDaemonControlServerResponsive(500)) {
6937
- return;
6938
- }
6939
- await new Promise((resolve) => setTimeout(resolve, 100));
7539
+ if (await waitForDaemonReady()) {
7540
+ return;
6940
7541
  }
6941
7542
  throw new Error("Failed to start Happy background service.");
6942
7543
  }
6943
7544
  async function executeUnifiedProvider(opts) {
6944
7545
  const credentials = await ensureUnifiedRuntimePrerequisites(opts.credentials);
6945
7546
  if (opts.provider === "claude") {
6946
- const { runClaude } = await Promise.resolve().then(function () { return require('./runClaude-KwIVwFp1.cjs'); });
7547
+ const { runClaude } = await Promise.resolve().then(function () { return require('./runClaude-C1W_Nw0C.cjs'); });
6947
7548
  await runClaude(credentials, opts.claudeOptions ?? {});
6948
7549
  return;
6949
7550
  }
6950
7551
  if (opts.provider === "codex") {
6951
- const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-Ba8COxZe.cjs'); });
7552
+ const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-D2VCWVEK.cjs'); });
6952
7553
  await runCodex({
6953
7554
  credentials,
6954
7555
  startedBy: opts.startedBy,
@@ -6958,7 +7559,7 @@ async function executeUnifiedProvider(opts) {
6958
7559
  return;
6959
7560
  }
6960
7561
  if (opts.provider === "gemini") {
6961
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-DtdLLX9o.cjs'); });
7562
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-CiGnjflq.cjs'); });
6962
7563
  await runGemini({
6963
7564
  credentials,
6964
7565
  startedBy: opts.startedBy
@@ -7000,7 +7601,7 @@ function shouldRunMainClaudeFlow(opts) {
7000
7601
  return;
7001
7602
  } else if (subcommand === "runtime") {
7002
7603
  if (args[1] === "providers") {
7003
- const { renderRuntimeProviders } = await Promise.resolve().then(function () { return require('./command-DLAJZsKX.cjs'); });
7604
+ const { renderRuntimeProviders } = await Promise.resolve().then(function () { return require('./command-Df7u5eAT.cjs'); });
7004
7605
  console.log(renderRuntimeProviders());
7005
7606
  return;
7006
7607
  }
@@ -7178,8 +7779,8 @@ function shouldRunMainClaudeFlow(opts) {
7178
7779
  const projectId = args[3];
7179
7780
  try {
7180
7781
  const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return config; });
7181
- const { readCredentials: readCredentials2 } = await Promise.resolve().then(function () { return require('./persistence-CxvL0cwp.cjs'); });
7182
- const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./api-CUTdFiFP.cjs'); }).then(function (n) { return n.api; });
7782
+ const { readCredentials: readCredentials2 } = await Promise.resolve().then(function () { return require('./persistence-D7JtnrYA.cjs'); });
7783
+ const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./api-DUE5TJBE.cjs'); }).then(function (n) { return n.api; });
7183
7784
  let userEmail = void 0;
7184
7785
  try {
7185
7786
  const credentials = await readCredentials2();
@@ -7326,14 +7927,7 @@ function shouldRunMainClaudeFlow(opts) {
7326
7927
  env: process.env
7327
7928
  });
7328
7929
  child.unref();
7329
- let started = false;
7330
- for (let i = 0; i < 100; i++) {
7331
- if (await checkIfDaemonRunningAndCleanupStaleState() && await isDaemonControlServerResponsive(500)) {
7332
- started = true;
7333
- break;
7334
- }
7335
- await new Promise((resolve) => setTimeout(resolve, 100));
7336
- }
7930
+ const started = await waitForDaemonReady();
7337
7931
  if (started) {
7338
7932
  console.log("Daemon started successfully");
7339
7933
  } else {
@@ -7342,7 +7936,7 @@ function shouldRunMainClaudeFlow(opts) {
7342
7936
  if (latest) {
7343
7937
  console.error(`Latest daemon log: ${latest.path}`);
7344
7938
  try {
7345
- const logContent = node_fs.readFileSync(latest.path, "utf-8");
7939
+ const logContent = fs.readFileSync(latest.path, "utf-8");
7346
7940
  if (logContent.includes("EADDRINUSE")) {
7347
7941
  console.error("Daemon control port is already in use. Retry after stopping the stale daemon or run `hicloud doctor clean`.");
7348
7942
  }
@@ -7615,8 +8209,8 @@ exports.getInitialGeminiModel = getInitialGeminiModel;
7615
8209
  exports.getProjectPath = getProjectPath;
7616
8210
  exports.initialMachineMetadata = initialMachineMetadata;
7617
8211
  exports.isBun = isBun;
7618
- exports.notifyDaemonSessionStarted = notifyDaemonSessionStarted;
7619
8212
  exports.projectPath = projectPath;
8213
+ exports.publishSessionRegistration = publishSessionRegistration;
7620
8214
  exports.readGeminiLocalConfig = readGeminiLocalConfig;
7621
8215
  exports.saveGeminiModelToConfig = saveGeminiModelToConfig;
7622
8216
  exports.startCaffeinate = startCaffeinate;