happy-imou-cloud 2.0.8 → 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-CN-WqYd_.cjs');
5
- var persistence = require('./persistence-DBGkO8gB.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-DM6z3aeG.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-DM6z3aeG.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";
992
1068
  }
1069
+ if (processStatus === "indeterminate") {
1070
+ return "indeterminate";
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,15 +1223,30 @@ function getEnvironmentInfo() {
1130
1223
  terminal: process.env.TERM
1131
1224
  };
1132
1225
  }
1226
+ function resolveDaemonSpawnDiagnostics(projectRoot, fileExists = fs.existsSync) {
1227
+ const wrapperCandidates = [
1228
+ path.join(projectRoot, "bin", "happy-cloud.mjs"),
1229
+ path.join(projectRoot, "bin", "happy.mjs")
1230
+ ];
1231
+ const cliEntrypoint = path.join(projectRoot, "dist", "index.mjs");
1232
+ const wrapperPath = wrapperCandidates.find((candidate) => fileExists(candidate)) ?? wrapperCandidates[0];
1233
+ return {
1234
+ projectRoot,
1235
+ wrapperPath,
1236
+ cliEntrypoint,
1237
+ wrapperExists: fileExists(wrapperPath),
1238
+ cliEntrypointExists: fileExists(cliEntrypoint)
1239
+ };
1240
+ }
1133
1241
  function getLogFiles(logDir) {
1134
- if (!node_fs.existsSync(logDir)) {
1242
+ if (!fs.existsSync(logDir)) {
1135
1243
  return [];
1136
1244
  }
1137
1245
  try {
1138
- return node_fs.readdirSync(logDir).filter((file) => file.endsWith(".log")).map((file) => {
1139
- const path = node_path.join(logDir, file);
1140
- const stats = node_fs.statSync(path);
1141
- 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 };
1142
1250
  }).sort((a, b) => b.modified.getTime() - a.modified.getTime());
1143
1251
  } catch {
1144
1252
  return [];
@@ -1156,14 +1264,12 @@ async function runDoctorCommand(filter) {
1156
1264
  console.log(`Node.js Version: ${chalk.green(process.version)}`);
1157
1265
  console.log("");
1158
1266
  console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
1159
- const projectRoot = projectPath();
1160
- const wrapperPath = node_path.join(projectRoot, "bin", "happy.mjs");
1161
- const cliEntrypoint = node_path.join(projectRoot, "dist", "index.mjs");
1162
- console.log(`Project Root: ${chalk.blue(projectRoot)}`);
1163
- console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
1164
- console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
1165
- console.log(`Wrapper Exists: ${node_fs.existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
1166
- console.log(`CLI Exists: ${node_fs.existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
1267
+ const diagnostics = resolveDaemonSpawnDiagnostics(projectPath());
1268
+ console.log(`Project Root: ${chalk.blue(diagnostics.projectRoot)}`);
1269
+ console.log(`Wrapper Script: ${chalk.blue(diagnostics.wrapperPath)}`);
1270
+ console.log(`CLI Entrypoint: ${chalk.blue(diagnostics.cliEntrypoint)}`);
1271
+ console.log(`Wrapper Exists: ${diagnostics.wrapperExists ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
1272
+ console.log(`CLI Exists: ${diagnostics.cliEntrypointExists ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
1167
1273
  console.log("");
1168
1274
  console.log(chalk.bold("\u2699\uFE0F Configuration"));
1169
1275
  console.log(`Happy Home: ${chalk.blue(api.configuration.happyCloudHomeDir)}`);
@@ -1199,9 +1305,9 @@ async function runDoctorCommand(filter) {
1199
1305
  }
1200
1306
  console.log(chalk.bold("\n\u{1F916} Daemon Status"));
1201
1307
  try {
1202
- const isRunning = await checkIfDaemonRunningAndCleanupStaleState();
1308
+ const daemonStatus = await getDaemonRuntimeStatus();
1203
1309
  const state = await persistence.readDaemonState();
1204
- if (isRunning && state) {
1310
+ if (daemonStatus === "running" && state) {
1205
1311
  console.log(chalk.green("\u2713 Daemon is running"));
1206
1312
  console.log(` PID: ${state.pid}`);
1207
1313
  console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
@@ -1209,7 +1315,14 @@ async function runDoctorCommand(filter) {
1209
1315
  if (state.httpPort) {
1210
1316
  console.log(` HTTP Port: ${state.httpPort}`);
1211
1317
  }
1212
- } 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) {
1213
1326
  console.log(chalk.yellow("\u26A0\uFE0F Daemon state exists but process not running (stale)"));
1214
1327
  } else {
1215
1328
  console.log(chalk.red("\u274C Daemon is not running"));
@@ -1231,6 +1344,7 @@ async function runDoctorCommand(filter) {
1231
1344
  const typeLabels = {
1232
1345
  "current": "\u{1F4CD} Current Process",
1233
1346
  "daemon": "\u{1F916} Daemon",
1347
+ "daemon-indeterminate": "\u26A0\uFE0F Possible Daemon (identity unconfirmed)",
1234
1348
  "daemon-version-check": "\u{1F50D} Daemon Version Check (stuck)",
1235
1349
  "daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
1236
1350
  "user-session": "\u{1F464} User Sessions",
@@ -1245,7 +1359,7 @@ async function runDoctorCommand(filter) {
1245
1359
  console.log(chalk.blue(`
1246
1360
  ${typeLabels[type] || type}:`));
1247
1361
  processes.forEach(({ pid, command }) => {
1248
- 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;
1249
1363
  console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
1250
1364
  });
1251
1365
  });
@@ -1329,7 +1443,7 @@ const isBun = () => getRuntime() === "bun";
1329
1443
 
1330
1444
  function spawnHappyCLI(args, options = {}) {
1331
1445
  const projectRoot = projectPath();
1332
- const entrypoint = node_path.join(projectRoot, "dist", "index.mjs");
1446
+ const entrypoint = path.join(projectRoot, "dist", "index.mjs");
1333
1447
  let directory;
1334
1448
  if ("cwd" in options) {
1335
1449
  directory = options.cwd;
@@ -1344,7 +1458,7 @@ function spawnHappyCLI(args, options = {}) {
1344
1458
  entrypoint,
1345
1459
  ...args
1346
1460
  ];
1347
- if (!node_fs.existsSync(entrypoint)) {
1461
+ if (!fs.existsSync(entrypoint)) {
1348
1462
  const errorMessage = `Entrypoint ${entrypoint} does not exist`;
1349
1463
  api.logger.debug(`[SPAWN HAPPY CLOUD CLI] ${errorMessage}`);
1350
1464
  throw new Error(errorMessage);
@@ -2275,6 +2389,283 @@ function buildDaemonChildEnv(baseEnv, extraEnv) {
2275
2389
  return childEnv;
2276
2390
  }
2277
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
+
2278
2669
  const initialMachineMetadata = {
2279
2670
  host: os$1.hostname(),
2280
2671
  platform: os$1.platform(),
@@ -2404,7 +2795,7 @@ async function startDaemon() {
2404
2795
  const { directory, sessionId, machineId: machineId2, approvedNewDirectoryCreation = true } = options;
2405
2796
  let directoryCreated = false;
2406
2797
  try {
2407
- await fs$1.access(directory);
2798
+ await fs$2.access(directory);
2408
2799
  api.logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
2409
2800
  } catch (error) {
2410
2801
  api.logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
@@ -2416,7 +2807,7 @@ async function startDaemon() {
2416
2807
  };
2417
2808
  }
2418
2809
  try {
2419
- await fs$1.mkdir(directory, { recursive: true });
2810
+ await fs$2.mkdir(directory, { recursive: true });
2420
2811
  api.logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
2421
2812
  directoryCreated = true;
2422
2813
  } catch (mkdirError) {
@@ -2445,7 +2836,7 @@ async function startDaemon() {
2445
2836
  if (options.token) {
2446
2837
  if (options.agent === "codex") {
2447
2838
  const codexHomeDir = tmp__namespace.dirSync();
2448
- fs$1.writeFile(path.join(codexHomeDir.name, "auth.json"), options.token);
2839
+ fs$2.writeFile(path$1.join(codexHomeDir.name, "auth.json"), options.token);
2449
2840
  authEnv.CODEX_HOME = codexHomeDir.name;
2450
2841
  } else {
2451
2842
  authEnv.CLAUDE_CODE_OAUTH_TOKEN = options.token;
@@ -2498,20 +2889,18 @@ async function startDaemon() {
2498
2889
  errorMessage: spawnError.errorMessage
2499
2890
  };
2500
2891
  }
2501
- const tmuxAvailable = await isTmuxAvailable();
2502
- let useTmux = tmuxAvailable;
2503
2892
  let tmuxSessionName = extraEnv.TMUX_SESSION_NAME;
2504
- if (!tmuxAvailable || tmuxSessionName === void 0) {
2505
- useTmux = false;
2506
- if (tmuxSessionName !== void 0) {
2507
- api.logger.debug(`[DAEMON RUN] tmux session name specified but tmux not available, falling back to regular spawning`);
2508
- }
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`);
2509
2898
  }
2510
2899
  if (useTmux && tmuxSessionName !== void 0) {
2511
2900
  const sessionDesc = tmuxSessionName || "current/most recent session";
2512
2901
  api.logger.debug(`[DAEMON RUN] Attempting to spawn session in tmux: ${sessionDesc}`);
2513
2902
  const tmux = getTmuxUtilities(tmuxSessionName);
2514
- const cliPath = path.join(projectPath(), "dist", "index.mjs");
2903
+ const cliPath = path$1.join(projectPath(), "dist", "index.mjs");
2515
2904
  const agent = resolveDaemonSpawnAgent(options.agent);
2516
2905
  const fullCommand = `node --no-warnings --no-deprecation ${cliPath} ${buildDaemonSpawnArgs(agent).join(" ")}`;
2517
2906
  const windowName = `happy-${Date.now()}-${agent}`;
@@ -2710,6 +3099,17 @@ async function startDaemon() {
2710
3099
  onHappySessionWebhook,
2711
3100
  port: api.HAPPY_CLOUD_DAEMON_PORT
2712
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
+ }
2713
3113
  const fileState = {
2714
3114
  pid: process.pid,
2715
3115
  httpPort: controlPort,
@@ -2749,52 +3149,69 @@ async function startDaemon() {
2749
3149
  if (process.env.DEBUG) {
2750
3150
  api.logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
2751
3151
  }
2752
- for (const [pid, _] of pidToTrackedSession.entries()) {
2753
- try {
2754
- process.kill(pid, 0);
2755
- } catch (error) {
2756
- api.logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
2757
- pidToTrackedSession.delete(pid);
2758
- }
2759
- }
2760
- const projectVersion = JSON.parse(fs.readFileSync(path.join(projectPath(), "package.json"), "utf-8")).version;
2761
- if (projectVersion !== api.configuration.currentCliVersion) {
2762
- api.logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
2763
- clearInterval(restartOnStaleVersionAndHeartbeat);
2764
- try {
2765
- spawnHappyCLI(["daemon", "start"], {
2766
- detached: true,
2767
- stdio: "ignore"
2768
- });
2769
- } catch (error) {
2770
- 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);
2771
- }
2772
- api.logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
2773
- await new Promise((resolve) => setTimeout(resolve, 1e4));
2774
- process.exit(0);
2775
- }
2776
- const daemonState = await persistence.readDaemonState();
2777
- if (daemonState && daemonState.pid !== process.pid) {
2778
- api.logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
2779
- requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
2780
- }
2781
3152
  try {
2782
- const updatedState = {
2783
- pid: process.pid,
2784
- httpPort: controlPort,
2785
- startTime: fileState.startTime,
2786
- startedWithCliVersion: api.packageJson.version,
2787
- lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
2788
- daemonLogPath: fileState.daemonLogPath
2789
- };
2790
- persistence.writeDaemonState(updatedState);
2791
- if (process.env.DEBUG) {
2792
- 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
+ );
2793
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
+ });
2794
3207
  } catch (error) {
2795
- 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;
2796
3214
  }
2797
- heartbeatRunning = false;
2798
3215
  }, heartbeatIntervalMs);
2799
3216
  const cleanupAndShutdown = async (source, errorMessage) => {
2800
3217
  api.logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
@@ -2850,7 +3267,7 @@ const PLIST_LABEL$1 = "com.happy-cloud.daemon";
2850
3267
  const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
2851
3268
  async function install$1() {
2852
3269
  try {
2853
- if (fs.existsSync(PLIST_FILE$1)) {
3270
+ if (fs$1.existsSync(PLIST_FILE$1)) {
2854
3271
  api.logger.info("Daemon plist already exists. Uninstalling first...");
2855
3272
  child_process.execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
2856
3273
  }
@@ -2894,8 +3311,8 @@ async function install$1() {
2894
3311
  </dict>
2895
3312
  </plist>
2896
3313
  `);
2897
- fs.writeFileSync(PLIST_FILE$1, plistContent);
2898
- fs.chmodSync(PLIST_FILE$1, 420);
3314
+ fs$1.writeFileSync(PLIST_FILE$1, plistContent);
3315
+ fs$1.chmodSync(PLIST_FILE$1, 420);
2899
3316
  api.logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
2900
3317
  child_process.execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
2901
3318
  api.logger.info("Daemon installed and started successfully");
@@ -2921,7 +3338,7 @@ const PLIST_LABEL = "com.happy-cli.daemon";
2921
3338
  const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
2922
3339
  async function uninstall$1() {
2923
3340
  try {
2924
- if (!fs.existsSync(PLIST_FILE)) {
3341
+ if (!fs$1.existsSync(PLIST_FILE)) {
2925
3342
  api.logger.info("Daemon plist not found. Nothing to uninstall.");
2926
3343
  return;
2927
3344
  }
@@ -2931,7 +3348,7 @@ async function uninstall$1() {
2931
3348
  } catch (error) {
2932
3349
  api.logger.info("Failed to unload daemon (it might not be running)");
2933
3350
  }
2934
- fs.unlinkSync(PLIST_FILE);
3351
+ fs$1.unlinkSync(PLIST_FILE);
2935
3352
  api.logger.info(`Removed daemon plist from ${PLIST_FILE}`);
2936
3353
  api.logger.info("Daemon uninstalled successfully");
2937
3354
  } catch (error) {
@@ -3071,8 +3488,8 @@ async function handleAuthLogout(args = []) {
3071
3488
  console.log(chalk.gray("Stopped daemon"));
3072
3489
  } catch {
3073
3490
  }
3074
- if (node_fs.existsSync(happyDir)) {
3075
- node_fs.rmSync(happyDir, { recursive: true, force: true });
3491
+ if (fs.existsSync(happyDir)) {
3492
+ fs.rmSync(happyDir, { recursive: true, force: true });
3076
3493
  }
3077
3494
  console.log(chalk.green("\u2713 Successfully logged out"));
3078
3495
  console.log(chalk.gray(' Run "hicloud auth login" to authenticate again'));
@@ -3107,9 +3524,11 @@ async function handleAuthStatus() {
3107
3524
  console.log(chalk.gray(`
3108
3525
  Data directory: ${api.configuration.happyCloudHomeDir}`));
3109
3526
  try {
3110
- const running = await checkIfDaemonRunningAndCleanupStaleState();
3111
- if (running) {
3527
+ const daemonStatus = await getDaemonRuntimeStatus();
3528
+ if (daemonStatus === "running") {
3112
3529
  console.log(chalk.green("\u2713 Daemon running"));
3530
+ } else if (daemonStatus === "indeterminate") {
3531
+ console.log(chalk.yellow("\u26A0\uFE0F Daemon status indeterminate"));
3113
3532
  } else {
3114
3533
  console.log(chalk.gray("\u2717 Daemon not running"));
3115
3534
  }
@@ -3745,10 +4164,10 @@ async function handleConnectStatus() {
3745
4164
  }
3746
4165
  function updateLocalGeminiCredentials(tokens) {
3747
4166
  try {
3748
- const geminiDir = path.join(os$1.homedir(), ".gemini");
3749
- const credentialsPath = path.join(geminiDir, "oauth_creds.json");
3750
- if (!fs.existsSync(geminiDir)) {
3751
- 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 });
3752
4171
  }
3753
4172
  const credentials = {
3754
4173
  access_token: tokens.access_token,
@@ -3758,7 +4177,7 @@ function updateLocalGeminiCredentials(tokens) {
3758
4177
  ...tokens.id_token && { id_token: tokens.id_token },
3759
4178
  ...tokens.expires_in && { expires_in: tokens.expires_in }
3760
4179
  };
3761
- fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), "utf-8");
4180
+ fs$1.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), "utf-8");
3762
4181
  console.log(chalk.gray(` Updated local credentials: ${credentialsPath}`));
3763
4182
  } catch (error) {
3764
4183
  console.log(chalk.yellow(` \u26A0\uFE0F Could not update local credentials: ${error}`));
@@ -3766,22 +4185,22 @@ function updateLocalGeminiCredentials(tokens) {
3766
4185
  }
3767
4186
 
3768
4187
  function getProjectPath(workingDirectory, claudeConfigDirOverride) {
3769
- const projectId = node_path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
4188
+ const projectId = path.resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
3770
4189
  const claudeConfigDirRaw = process.env.CLAUDE_CONFIG_DIR ?? "";
3771
4190
  const claudeConfigDirTrimmed = claudeConfigDirRaw.trim();
3772
- const claudeConfigDir = claudeConfigDirTrimmed ? claudeConfigDirTrimmed : node_path.join(os.homedir(), ".claude");
3773
- return node_path.join(claudeConfigDir, "projects", projectId);
4191
+ const claudeConfigDir = claudeConfigDirTrimmed ? claudeConfigDirTrimmed : path.join(os.homedir(), ".claude");
4192
+ return path.join(claudeConfigDir, "projects", projectId);
3774
4193
  }
3775
4194
 
3776
- function claudeCheckSession(sessionId, path, transcriptPath) {
3777
- const projectDir = getProjectPath(path);
3778
- const sessionFile = transcriptPath ?? node_path.join(projectDir, `${sessionId}.jsonl`);
3779
- 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);
3780
4199
  if (!sessionExists) {
3781
4200
  api.logger.debug(`[claudeCheckSession] Path ${sessionFile} does not exist`);
3782
4201
  return false;
3783
4202
  }
3784
- const sessionData = node_fs.readFileSync(sessionFile, "utf-8").split("\n");
4203
+ const sessionData = fs.readFileSync(sessionFile, "utf-8").split("\n");
3785
4204
  const hasGoodMessage = !!sessionData.find((v, index) => {
3786
4205
  if (!v.trim()) return false;
3787
4206
  try {
@@ -3802,7 +4221,7 @@ function claudeFindLastSession(workingDirectory) {
3802
4221
  try {
3803
4222
  const projectDir = getProjectPath(workingDirectory);
3804
4223
  const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3805
- 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) => {
3806
4225
  const sessionId = f.replace(".jsonl", "");
3807
4226
  if (!uuidPattern.test(sessionId)) {
3808
4227
  return null;
@@ -3811,7 +4230,7 @@ function claudeFindLastSession(workingDirectory) {
3811
4230
  return {
3812
4231
  name: f,
3813
4232
  sessionId,
3814
- mtime: node_fs.statSync(node_path.join(projectDir, f)).mtime.getTime()
4233
+ mtime: fs.statSync(path.join(projectDir, f)).mtime.getTime()
3815
4234
  };
3816
4235
  }
3817
4236
  return null;
@@ -3831,10 +4250,10 @@ class ExitCodeError extends Error {
3831
4250
  this.exitCode = exitCode;
3832
4251
  }
3833
4252
  }
3834
- 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"));
3835
4254
  async function claudeLocal(opts) {
3836
4255
  const projectDir = getProjectPath(opts.path);
3837
- node_fs.mkdirSync(projectDir, { recursive: true });
4256
+ fs.mkdirSync(projectDir, { recursive: true });
3838
4257
  const hasContinueFlag = opts.claudeArgs?.includes("--continue");
3839
4258
  const hasResumeFlag = opts.claudeArgs?.includes("--resume");
3840
4259
  const hasUserSessionControl = hasContinueFlag || hasResumeFlag;
@@ -3953,7 +4372,7 @@ async function claudeLocal(opts) {
3953
4372
  args.push("--settings", opts.hookSettingsPath);
3954
4373
  api.logger.debug(`[ClaudeLocal] Using hook settings: ${opts.hookSettingsPath}`);
3955
4374
  }
3956
- if (!claudeCliPath || !node_fs.existsSync(claudeCliPath)) {
4375
+ if (!claudeCliPath || !fs.existsSync(claudeCliPath)) {
3957
4376
  throw new Error("Claude local launcher not found. Please ensure HAPPY_PROJECT_ROOT is set correctly for development.");
3958
4377
  }
3959
4378
  const env = {
@@ -6301,17 +6720,17 @@ function readGeminiLocalConfig() {
6301
6720
  let googleCloudProject = null;
6302
6721
  let googleCloudProjectEmail = null;
6303
6722
  const possiblePaths = [
6304
- path.join(os$1.homedir(), ".gemini", "oauth_creds.json"),
6723
+ path$1.join(os$1.homedir(), ".gemini", "oauth_creds.json"),
6305
6724
  // Main OAuth credentials file
6306
- path.join(os$1.homedir(), ".gemini", "config.json"),
6307
- path.join(os$1.homedir(), ".config", "gemini", "config.json"),
6308
- path.join(os$1.homedir(), ".gemini", "auth.json"),
6309
- 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")
6310
6729
  ];
6311
6730
  for (const configPath of possiblePaths) {
6312
- if (fs.existsSync(configPath)) {
6731
+ if (fs$1.existsSync(configPath)) {
6313
6732
  try {
6314
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
6733
+ const config = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
6315
6734
  if (!token) {
6316
6735
  const foundToken = config.access_token || config.token || config.apiKey || config.GEMINI_API_KEY;
6317
6736
  if (foundToken && typeof foundToken === "string") {
@@ -6383,22 +6802,22 @@ function determineGeminiModel(explicitModel, localConfig) {
6383
6802
  }
6384
6803
  function saveGeminiModelToConfig(model) {
6385
6804
  try {
6386
- const configDir = path.join(os$1.homedir(), ".gemini");
6387
- const configPath = path.join(configDir, "config.json");
6388
- if (!fs.existsSync(configDir)) {
6389
- 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 });
6390
6809
  }
6391
6810
  let config = {};
6392
- if (fs.existsSync(configPath)) {
6811
+ if (fs$1.existsSync(configPath)) {
6393
6812
  try {
6394
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
6813
+ config = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
6395
6814
  } catch (error) {
6396
6815
  api.logger.debug(`[Gemini] Failed to read existing config, creating new one`);
6397
6816
  config = {};
6398
6817
  }
6399
6818
  }
6400
6819
  config.model = model;
6401
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6820
+ fs$1.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6402
6821
  api.logger.debug(`[Gemini] Saved model "${model}" to ${configPath}`);
6403
6822
  } catch (error) {
6404
6823
  api.logger.debug(`[Gemini] Failed to save model to config:`, error);
@@ -6406,15 +6825,15 @@ function saveGeminiModelToConfig(model) {
6406
6825
  }
6407
6826
  function saveGoogleCloudProjectToConfig(projectId, email) {
6408
6827
  try {
6409
- const configDir = path.join(os$1.homedir(), ".gemini");
6410
- const configPath = path.join(configDir, "config.json");
6411
- if (!fs.existsSync(configDir)) {
6412
- 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 });
6413
6832
  }
6414
6833
  let config = {};
6415
- if (fs.existsSync(configPath)) {
6834
+ if (fs$1.existsSync(configPath)) {
6416
6835
  try {
6417
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
6836
+ config = JSON.parse(fs$1.readFileSync(configPath, "utf-8"));
6418
6837
  } catch {
6419
6838
  config = {};
6420
6839
  }
@@ -6423,7 +6842,7 @@ function saveGoogleCloudProjectToConfig(projectId, email) {
6423
6842
  if (email) {
6424
6843
  config.googleCloudProjectEmail = email;
6425
6844
  }
6426
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6845
+ fs$1.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
6427
6846
  api.logger.debug(`[Gemini] Saved Google Cloud Project "${projectId}"${email ? ` for ${email}` : ""} to ${configPath}`);
6428
6847
  } catch (error) {
6429
6848
  api.logger.debug(`[Gemini] Failed to save Google Cloud Project to config:`, error);
@@ -6531,11 +6950,11 @@ function readFirstEnv(...names) {
6531
6950
  return "";
6532
6951
  }
6533
6952
  function normalizeCommandPath(command) {
6534
- if (node_path.isAbsolute(command)) {
6953
+ if (path.isAbsolute(command)) {
6535
6954
  return command;
6536
6955
  }
6537
- const resolved = node_path.resolve(process.cwd(), command);
6538
- return node_fs.existsSync(resolved) ? resolved : command;
6956
+ const resolved = path.resolve(process.cwd(), command);
6957
+ return fs.existsSync(resolved) ? resolved : command;
6539
6958
  }
6540
6959
  function resolveCommandOnPath(command) {
6541
6960
  const pathValue = typeof process.env.PATH === "string" ? process.env.PATH : "";
@@ -6543,13 +6962,13 @@ function resolveCommandOnPath(command) {
6543
6962
  return null;
6544
6963
  }
6545
6964
  const extensions = process.platform === "win32" ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((value) => value.trim().toLowerCase()).filter(Boolean) : [""];
6546
- for (const dir of pathValue.split(node_path.delimiter)) {
6965
+ for (const dir of pathValue.split(path.delimiter)) {
6547
6966
  const trimmedDir = dir.trim();
6548
6967
  if (!trimmedDir) {
6549
6968
  continue;
6550
6969
  }
6551
- const directCandidate = node_path.join(trimmedDir, command);
6552
- if (node_fs.existsSync(directCandidate)) {
6970
+ const directCandidate = path.join(trimmedDir, command);
6971
+ if (fs.existsSync(directCandidate)) {
6553
6972
  return directCandidate;
6554
6973
  }
6555
6974
  if (process.platform !== "win32") {
@@ -6560,8 +6979,8 @@ function resolveCommandOnPath(command) {
6560
6979
  continue;
6561
6980
  }
6562
6981
  for (const extension of extensions) {
6563
- const candidate = node_path.join(trimmedDir, `${command}${extension.toLowerCase()}`);
6564
- if (node_fs.existsSync(candidate)) {
6982
+ const candidate = path.join(trimmedDir, `${command}${extension.toLowerCase()}`);
6983
+ if (fs.existsSync(candidate)) {
6565
6984
  return candidate;
6566
6985
  }
6567
6986
  }
@@ -6660,8 +7079,8 @@ function validateCodexAcpSpawn(options = {}) {
6660
7079
  const normalizedCommand = spawn.command.trim();
6661
7080
  const commandLower = normalizedCommand.toLowerCase();
6662
7081
  const npxMode = readCodexAcpNpxMode();
6663
- if (node_path.isAbsolute(normalizedCommand)) {
6664
- if (!node_fs.existsSync(normalizedCommand)) {
7082
+ if (path.isAbsolute(normalizedCommand)) {
7083
+ if (!fs.existsSync(normalizedCommand)) {
6665
7084
  return {
6666
7085
  ok: false,
6667
7086
  errorMessage: `Codex ACP is enabled, but the resolved command does not exist: ${normalizedCommand}`
@@ -6690,6 +7109,189 @@ function validateCodexAcpSpawn(options = {}) {
6690
7109
  return { ok: true, spawn };
6691
7110
  }
6692
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
+
6693
7295
  class CodexAcpTransport extends CodexTransport {
6694
7296
  constructor(initTimeoutMs) {
6695
7297
  super();
@@ -6719,10 +7321,10 @@ function createCodexBackend(options) {
6719
7321
  cwd: options.cwd,
6720
7322
  command: spawn.command,
6721
7323
  args: spawn.args,
6722
- env: {
7324
+ env: buildCodexAcpEnv({
6723
7325
  ...options.env,
6724
7326
  NODE_ENV: "production"
6725
- },
7327
+ }),
6726
7328
  permissionHandler: options.permissionHandler,
6727
7329
  selectionHandler: options.selectionHandler,
6728
7330
  transportHandler: resolveCodexTransport(spawn.command)
@@ -6872,6 +7474,24 @@ function createDefaultRuntimeShell() {
6872
7474
  return new RuntimeShell();
6873
7475
  }
6874
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
+
6875
7495
  function isRuntimeProvider(value) {
6876
7496
  return value === "claude" || value === "codex" || value === "gemini" || value === "cursor";
6877
7497
  }
@@ -6916,26 +7536,20 @@ async function ensureUnifiedDaemonStarted() {
6916
7536
  env: process.env
6917
7537
  });
6918
7538
  daemonProcess.unref();
6919
- for (let i = 0; i < 100; i++) {
6920
- if (await isDaemonRunningCurrentlyInstalledHappyVersion()) {
6921
- return;
6922
- }
6923
- if (await isDaemonControlServerResponsive(500)) {
6924
- return;
6925
- }
6926
- await new Promise((resolve) => setTimeout(resolve, 100));
7539
+ if (await waitForDaemonReady()) {
7540
+ return;
6927
7541
  }
6928
7542
  throw new Error("Failed to start Happy background service.");
6929
7543
  }
6930
7544
  async function executeUnifiedProvider(opts) {
6931
7545
  const credentials = await ensureUnifiedRuntimePrerequisites(opts.credentials);
6932
7546
  if (opts.provider === "claude") {
6933
- const { runClaude } = await Promise.resolve().then(function () { return require('./runClaude-CPhWaFrX.cjs'); });
7547
+ const { runClaude } = await Promise.resolve().then(function () { return require('./runClaude-C1W_Nw0C.cjs'); });
6934
7548
  await runClaude(credentials, opts.claudeOptions ?? {});
6935
7549
  return;
6936
7550
  }
6937
7551
  if (opts.provider === "codex") {
6938
- const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex--QLrOs8X.cjs'); });
7552
+ const { runCodex } = await Promise.resolve().then(function () { return require('./runCodex-D2VCWVEK.cjs'); });
6939
7553
  await runCodex({
6940
7554
  credentials,
6941
7555
  startedBy: opts.startedBy,
@@ -6945,7 +7559,7 @@ async function executeUnifiedProvider(opts) {
6945
7559
  return;
6946
7560
  }
6947
7561
  if (opts.provider === "gemini") {
6948
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-CDyhCucw.cjs'); });
7562
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-CiGnjflq.cjs'); });
6949
7563
  await runGemini({
6950
7564
  credentials,
6951
7565
  startedBy: opts.startedBy
@@ -6987,7 +7601,7 @@ function shouldRunMainClaudeFlow(opts) {
6987
7601
  return;
6988
7602
  } else if (subcommand === "runtime") {
6989
7603
  if (args[1] === "providers") {
6990
- const { renderRuntimeProviders } = await Promise.resolve().then(function () { return require('./command-DjIfRZQS.cjs'); });
7604
+ const { renderRuntimeProviders } = await Promise.resolve().then(function () { return require('./command-Df7u5eAT.cjs'); });
6991
7605
  console.log(renderRuntimeProviders());
6992
7606
  return;
6993
7607
  }
@@ -7165,8 +7779,8 @@ function shouldRunMainClaudeFlow(opts) {
7165
7779
  const projectId = args[3];
7166
7780
  try {
7167
7781
  const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return config; });
7168
- const { readCredentials: readCredentials2 } = await Promise.resolve().then(function () { return require('./persistence-DBGkO8gB.cjs'); });
7169
- const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./api-CN-WqYd_.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; });
7170
7784
  let userEmail = void 0;
7171
7785
  try {
7172
7786
  const credentials = await readCredentials2();
@@ -7313,14 +7927,7 @@ function shouldRunMainClaudeFlow(opts) {
7313
7927
  env: process.env
7314
7928
  });
7315
7929
  child.unref();
7316
- let started = false;
7317
- for (let i = 0; i < 100; i++) {
7318
- if (await checkIfDaemonRunningAndCleanupStaleState() && await isDaemonControlServerResponsive(500)) {
7319
- started = true;
7320
- break;
7321
- }
7322
- await new Promise((resolve) => setTimeout(resolve, 100));
7323
- }
7930
+ const started = await waitForDaemonReady();
7324
7931
  if (started) {
7325
7932
  console.log("Daemon started successfully");
7326
7933
  } else {
@@ -7329,7 +7936,7 @@ function shouldRunMainClaudeFlow(opts) {
7329
7936
  if (latest) {
7330
7937
  console.error(`Latest daemon log: ${latest.path}`);
7331
7938
  try {
7332
- const logContent = node_fs.readFileSync(latest.path, "utf-8");
7939
+ const logContent = fs.readFileSync(latest.path, "utf-8");
7333
7940
  if (logContent.includes("EADDRINUSE")) {
7334
7941
  console.error("Daemon control port is already in use. Retry after stopping the stale daemon or run `hicloud doctor clean`.");
7335
7942
  }
@@ -7602,8 +8209,8 @@ exports.getInitialGeminiModel = getInitialGeminiModel;
7602
8209
  exports.getProjectPath = getProjectPath;
7603
8210
  exports.initialMachineMetadata = initialMachineMetadata;
7604
8211
  exports.isBun = isBun;
7605
- exports.notifyDaemonSessionStarted = notifyDaemonSessionStarted;
7606
8212
  exports.projectPath = projectPath;
8213
+ exports.publishSessionRegistration = publishSessionRegistration;
7607
8214
  exports.readGeminiLocalConfig = readGeminiLocalConfig;
7608
8215
  exports.saveGeminiModelToConfig = saveGeminiModelToConfig;
7609
8216
  exports.startCaffeinate = startCaffeinate;