@zonease/happy 0.6.38 → 0.6.40

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.
Files changed (112) hide show
  1. package/bin/happy.mjs +10 -0
  2. package/dist/{types-DvTsFaqR.cjs → api-C7FyJfZ1.cjs} +400 -1964
  3. package/dist/{types-BWpRU1VN.mjs → api-D6WL40-U.mjs} +220 -1729
  4. package/dist/auth-C7ZScWEw.cjs +274 -0
  5. package/dist/auth-DCsIoZLT.cjs +183 -0
  6. package/dist/auth-DO9GexAR.mjs +181 -0
  7. package/dist/auth-DxJ3VvrX.mjs +270 -0
  8. package/dist/browser-AoVGRX7y.mjs +20 -0
  9. package/dist/browser-DzgO4oaD.cjs +22 -0
  10. package/dist/claudeLocal-C6BdzhDx.cjs +352 -0
  11. package/dist/claudeLocal-CWNGpJ0-.mjs +344 -0
  12. package/dist/codex/happyMcpStdioBridge.cjs +4 -1
  13. package/dist/codex/happyMcpStdioBridge.mjs +4 -1
  14. package/dist/common-BdNgghUJ.mjs +75 -0
  15. package/dist/common-CZvSRU4p.cjs +83 -0
  16. package/dist/{config-0Xnc8ARM.cjs → config-CclB3QW9.cjs} +21 -34
  17. package/dist/{config-_GW4YBCP.mjs → config-DZk-mJ0K.mjs} +7 -20
  18. package/dist/connect-BXLUKJ5P.mjs +679 -0
  19. package/dist/connect-k9CqDJ-T.cjs +681 -0
  20. package/dist/{constants-B4x1K7by.cjs → constants-E_UWYTo3.cjs} +2 -2
  21. package/dist/{constants-BO9OAFOT.mjs → constants-pmXZGibX.mjs} +1 -1
  22. package/dist/controlClient-B23M-WPp.cjs +177 -0
  23. package/dist/controlClient-DB5tDU7V.mjs +167 -0
  24. package/dist/deviceKeys-BTdo34Uz.mjs +175 -0
  25. package/dist/deviceKeys-C540DcXl.cjs +182 -0
  26. package/dist/diagnose-B3D1a_v0.cjs +60 -0
  27. package/dist/diagnose-CuRKXxt1.mjs +58 -0
  28. package/dist/doctor-BQGLhrcI.cjs +214 -0
  29. package/dist/doctor-C08m5Z9f.cjs +78 -0
  30. package/dist/doctor-CU4JLGQ1.mjs +211 -0
  31. package/dist/doctor-ON7pN5G5.mjs +74 -0
  32. package/dist/downloadImage-DZXI3mXL.mjs +887 -0
  33. package/dist/downloadImage-cQ1Z7hkC.cjs +897 -0
  34. package/dist/enroll-B55BQR56.mjs +113 -0
  35. package/dist/enroll-Bo-CNio1.cjs +115 -0
  36. package/dist/evidence-DQp8QWcv.cjs +129 -0
  37. package/dist/evidence-DTYRAaOZ.mjs +127 -0
  38. package/dist/formatImageMessage-CixDYY6G.cjs +46 -0
  39. package/dist/formatImageMessage-DbKSSjMx.mjs +43 -0
  40. package/dist/github-DKcyF1zp.mjs +57 -0
  41. package/dist/github-h9GhJJ_C.cjs +59 -0
  42. package/dist/index-CDTNgfUf.cjs +578 -0
  43. package/dist/index-CPKdvi7J.mjs +554 -0
  44. package/dist/index.cjs +812 -40
  45. package/dist/index.mjs +813 -40
  46. package/dist/install-B1e6rdy7.mjs +94 -0
  47. package/dist/install-CgUPGIoF.mjs +95 -0
  48. package/dist/install-D7Ir7ciZ.cjs +96 -0
  49. package/dist/install-DSDufaPd.cjs +98 -0
  50. package/dist/lib.cjs +18 -12
  51. package/dist/lib.d.cts +77 -0
  52. package/dist/lib.d.mts +77 -0
  53. package/dist/lib.mjs +13 -7
  54. package/dist/{loadableToolOutput-va3RLplh.mjs → loadableToolOutput-Bfei9wPq.mjs} +4 -2
  55. package/dist/{loadableToolOutput-5vdQB98M.cjs → loadableToolOutput-CbLGtvST.cjs} +63 -61
  56. package/dist/mcpToolSchemas-CjeTfrAs.mjs +62 -0
  57. package/dist/mcpToolSchemas-DK42yoZ_.cjs +64 -0
  58. package/dist/persistence-CBTtC_dm.cjs +996 -0
  59. package/dist/persistence-DKfF81pd.mjs +944 -0
  60. package/dist/projectPath-BTBIMB57.mjs +10 -0
  61. package/dist/projectPath-ChLhdOyt.cjs +13 -0
  62. package/dist/prompt-BTWDZZO0.cjs +25 -0
  63. package/dist/prompt-BsAZsPbo.mjs +22 -0
  64. package/dist/release-BMEzNONE.mjs +168 -0
  65. package/dist/release-EhOJeAZe.cjs +171 -0
  66. package/dist/run-BVZsMNwN.mjs +2164 -0
  67. package/dist/run-CURx7LhK.cjs +2188 -0
  68. package/dist/runClaude-6Gi0sYQy.mjs +3763 -0
  69. package/dist/runClaude-CL3Kt5ex.cjs +3766 -0
  70. package/dist/{runCodex-DiKzBf9q.mjs → runCodex-ClwEgJ7M.mjs} +36 -20
  71. package/dist/{runCodex-skq0j05d.cjs → runCodex-YKyVBkJ3.cjs} +169 -153
  72. package/dist/{runGemini-BeqrVq8R.mjs → runGemini-BPWxVr60.mjs} +34 -17
  73. package/dist/{runGemini-CVp-CEIh.cjs → runGemini-Xz2W1spn.cjs} +212 -195
  74. package/dist/{runOneShot-ltCpHbBB.mjs → runOneShot-CdCWiiLr.mjs} +14 -38
  75. package/dist/{runOneShot-BQNkNW61.cjs → runOneShot-DjZyCvDI.cjs} +31 -55
  76. package/dist/{serve-BNdoEDVx.mjs → serve-B48JttQ3.mjs} +12 -35
  77. package/dist/{serve-BECdtivY.cjs → serve-B8vaWQA0.cjs} +35 -58
  78. package/dist/session-BXboDn2V.mjs +108 -0
  79. package/dist/session-BZO25zxp.cjs +110 -0
  80. package/dist/sessionKeyCache-B6cGadYL.mjs +33 -0
  81. package/dist/sessionKeyCache-CDmexJ9p.cjs +36 -0
  82. package/dist/sessionList-CgAYZpNj.mjs +51 -0
  83. package/dist/sessionList-lQ-nqEhc.cjs +54 -0
  84. package/dist/sessionSummary-CcORiQSq.cjs +393 -0
  85. package/dist/sessionSummary-DCWQoaDP.mjs +387 -0
  86. package/dist/spawnHappyCLI-KiazE3kL.cjs +70 -0
  87. package/dist/spawnHappyCLI-yBIP_Oqi.mjs +66 -0
  88. package/dist/ssh-q5uZhqBf.mjs +359 -0
  89. package/dist/ssh-yMllSHu5.cjs +365 -0
  90. package/dist/systemdScope-BeOtAkhk.mjs +25 -0
  91. package/dist/systemdScope-DW3-UoKB.cjs +29 -0
  92. package/dist/task-_nr7J19j.cjs +167 -0
  93. package/dist/task-wNX1N2i4.mjs +165 -0
  94. package/dist/time-Bj2HRkKH.cjs +33 -0
  95. package/dist/time-DgsBLJA_.mjs +29 -0
  96. package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
  97. package/dist/trimIdent-BAjmv_TG.mjs +20 -0
  98. package/dist/types-BoJh4Qfv.mjs +57 -0
  99. package/dist/types-DWeB7aqh.cjs +59 -0
  100. package/dist/uninstall-Buaz7d-X.cjs +77 -0
  101. package/dist/uninstall-Dxf6_4mQ.mjs +75 -0
  102. package/dist/update-CGlIfa8e.cjs +111 -0
  103. package/dist/update-CnnoX2cV.mjs +109 -0
  104. package/dist/workflowCommon-B0d01Y_a.mjs +56 -0
  105. package/dist/workflowCommon-Bl568-rt.cjs +62 -0
  106. package/dist/workflowReports-BEWuQesA.cjs +113 -0
  107. package/dist/workflowReports-Dd8-4-8f.mjs +108 -0
  108. package/package.json +1 -1
  109. package/dist/index-BbCDDvoB.cjs +0 -11376
  110. package/dist/index-vvZXdswy.mjs +0 -11336
  111. package/dist/mcpToolSchemas-CdPXJQmb.mjs +0 -175
  112. package/dist/mcpToolSchemas-f-kttjFw.cjs +0 -186
@@ -0,0 +1,2164 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+ import * as tmp from 'tmp';
4
+ import { A as ApiClient } from './api-D6WL40-U.mjs';
5
+ import { c as configuration, l as logger, p as packageJson, v as acquireDaemonLock, a as readSettings, i as isDebug, g as encodeBase64, x as writeDaemonState, j as readDaemonState, y as releaseDaemonLock, z as validateProfileForAgent, A as getProfileEnvironmentVariables } from './persistence-DKfF81pd.mjs';
6
+ import { authAndSetupMachineIfNeeded } from './auth-DxJ3VvrX.mjs';
7
+ import { spawn, execSync, exec } from 'child_process';
8
+ import { getEnvironmentInfo } from './doctor-CU4JLGQ1.mjs';
9
+ import { s as spawnHappyCLI } from './spawnHappyCLI-yBIP_Oqi.mjs';
10
+ import { isDaemonRunningCurrentlyInstalledHappyVersion, stopDaemon, cleanupDaemonState } from './controlClient-DB5tDU7V.mjs';
11
+ import { a as readCachedDeviceKey, d as deviceExec } from './deviceKeys-BTdo34Uz.mjs';
12
+ import { P as PLIST_LABEL } from './install-CgUPGIoF.mjs';
13
+ import fastify from 'fastify';
14
+ import { createReadStream } from 'node:fs';
15
+ import { stat } from 'node:fs/promises';
16
+ import path from 'node:path';
17
+ import { z } from 'zod';
18
+ import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
19
+ import { readFileSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { p as projectPath } from './projectPath-BTBIMB57.mjs';
22
+ import { b as buildOutputSummary, m as mapFinishStatus, a as buildOrchestratorEnv, c as appendOutputChunk } from './common-BdNgghUJ.mjs';
23
+
24
+ let caffeinateProcess = null;
25
+ function startCaffeinate() {
26
+ if (configuration.disableCaffeinate) {
27
+ logger.debug("[caffeinate] Caffeinate disabled via HAPPY_DISABLE_CAFFEINATE environment variable");
28
+ return false;
29
+ }
30
+ if (process.platform !== "darwin") {
31
+ logger.debug("[caffeinate] Not on macOS, skipping caffeinate");
32
+ return false;
33
+ }
34
+ if (caffeinateProcess && !caffeinateProcess.killed) {
35
+ logger.debug("[caffeinate] Caffeinate already running");
36
+ return true;
37
+ }
38
+ try {
39
+ caffeinateProcess = spawn("caffeinate", ["-im"], {
40
+ stdio: "ignore",
41
+ detached: false
42
+ });
43
+ caffeinateProcess.on("error", (error) => {
44
+ logger.debug("[caffeinate] Error starting caffeinate:", error);
45
+ caffeinateProcess = null;
46
+ });
47
+ caffeinateProcess.on("exit", (code, signal) => {
48
+ logger.debug(`[caffeinate] Process exited with code ${code}, signal ${signal}`);
49
+ caffeinateProcess = null;
50
+ });
51
+ logger.debug(`[caffeinate] Started with PID ${caffeinateProcess.pid}`);
52
+ setupCleanupHandlers();
53
+ return true;
54
+ } catch (error) {
55
+ logger.debug("[caffeinate] Failed to start caffeinate:", error);
56
+ return false;
57
+ }
58
+ }
59
+ let isStopping = false;
60
+ async function stopCaffeinate() {
61
+ if (isStopping) {
62
+ logger.debug("[caffeinate] Already stopping, skipping");
63
+ return;
64
+ }
65
+ if (caffeinateProcess && !caffeinateProcess.killed) {
66
+ isStopping = true;
67
+ logger.debug(`[caffeinate] Stopping caffeinate process PID ${caffeinateProcess.pid}`);
68
+ try {
69
+ caffeinateProcess.kill("SIGTERM");
70
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
71
+ if (caffeinateProcess && !caffeinateProcess.killed) {
72
+ logger.debug("[caffeinate] Force killing caffeinate process");
73
+ caffeinateProcess.kill("SIGKILL");
74
+ }
75
+ caffeinateProcess = null;
76
+ isStopping = false;
77
+ } catch (error) {
78
+ logger.debug("[caffeinate] Error stopping caffeinate:", error);
79
+ isStopping = false;
80
+ }
81
+ }
82
+ }
83
+ let cleanupHandlersSet = false;
84
+ function setupCleanupHandlers() {
85
+ if (cleanupHandlersSet) {
86
+ return;
87
+ }
88
+ cleanupHandlersSet = true;
89
+ const cleanup = () => {
90
+ stopCaffeinate();
91
+ };
92
+ process.on("exit", cleanup);
93
+ process.on("SIGINT", cleanup);
94
+ process.on("SIGTERM", cleanup);
95
+ process.on("SIGUSR1", cleanup);
96
+ process.on("SIGUSR2", cleanup);
97
+ process.on("uncaughtException", (error) => {
98
+ logger.debug("[caffeinate] Uncaught exception, cleaning up:", error);
99
+ cleanup();
100
+ });
101
+ process.on("unhandledRejection", (reason, promise) => {
102
+ logger.debug("[caffeinate] Unhandled rejection, cleaning up:", reason);
103
+ cleanup();
104
+ });
105
+ }
106
+
107
+ const VIDEO_CONTENT_TYPES = {
108
+ ".webm": "video/webm",
109
+ ".mp4": "video/mp4",
110
+ ".m4v": "video/mp4",
111
+ ".mov": "video/quicktime",
112
+ ".ogv": "video/ogg"
113
+ };
114
+ function getFileStreamContentType(filePath) {
115
+ const extension = path.extname(filePath).toLowerCase();
116
+ return VIDEO_CONTENT_TYPES[extension] ?? "application/octet-stream";
117
+ }
118
+ function getFileStreamCorsHeaders() {
119
+ return {
120
+ "Access-Control-Allow-Origin": "*",
121
+ "Access-Control-Allow-Headers": "Range, Access-Control-Request-Private-Network",
122
+ "Access-Control-Expose-Headers": "Accept-Ranges, Content-Length, Content-Range, Content-Type",
123
+ "Access-Control-Allow-Private-Network": "true"
124
+ };
125
+ }
126
+ function parseFileStreamRange(rangeHeader, size) {
127
+ if (!Number.isSafeInteger(size) || size <= 0) {
128
+ return { ok: false };
129
+ }
130
+ if (!rangeHeader) {
131
+ return { ok: true, partial: false, start: 0, end: size - 1 };
132
+ }
133
+ const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
134
+ if (!match) {
135
+ return { ok: false };
136
+ }
137
+ const [, rawStart, rawEnd] = match;
138
+ if (!rawStart && !rawEnd) {
139
+ return { ok: false };
140
+ }
141
+ let start;
142
+ let end;
143
+ if (!rawStart) {
144
+ const suffixLength = Number.parseInt(rawEnd, 10);
145
+ if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0) {
146
+ return { ok: false };
147
+ }
148
+ start = Math.max(size - suffixLength, 0);
149
+ end = size - 1;
150
+ } else {
151
+ start = Number.parseInt(rawStart, 10);
152
+ end = rawEnd ? Number.parseInt(rawEnd, 10) : size - 1;
153
+ }
154
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || start >= size) {
155
+ return { ok: false };
156
+ }
157
+ return { ok: true, partial: true, start, end: Math.min(end, size - 1) };
158
+ }
159
+
160
+ function startDaemonControlServer({
161
+ getChildren,
162
+ stopSession,
163
+ spawnSession,
164
+ requestShutdown,
165
+ onHappySessionWebhook,
166
+ execOnDevice
167
+ }) {
168
+ return new Promise((resolve) => {
169
+ const app = fastify({
170
+ logger: false
171
+ // We use our own logger
172
+ });
173
+ app.setValidatorCompiler(validatorCompiler);
174
+ app.setSerializerCompiler(serializerCompiler);
175
+ const typed = app.withTypeProvider();
176
+ const addFileStreamCorsHeaders = (reply) => {
177
+ for (const [key, value] of Object.entries(getFileStreamCorsHeaders())) {
178
+ reply.header(key, value);
179
+ }
180
+ };
181
+ typed.options("/file-stream", async (_request, reply) => {
182
+ addFileStreamCorsHeaders(reply);
183
+ reply.header("Access-Control-Allow-Methods", "GET, OPTIONS");
184
+ reply.code(204);
185
+ return "";
186
+ });
187
+ typed.get("/file-stream", {
188
+ schema: {
189
+ querystring: z.object({
190
+ path: z.string().min(1)
191
+ })
192
+ }
193
+ }, async (request, reply) => {
194
+ addFileStreamCorsHeaders(reply);
195
+ const filePath = request.query.path;
196
+ if (!path.isAbsolute(filePath)) {
197
+ reply.code(400);
198
+ return { error: "path must be absolute" };
199
+ }
200
+ let stats;
201
+ try {
202
+ stats = await stat(filePath);
203
+ } catch (error) {
204
+ logger.debug(`[CONTROL SERVER] File stream stat failed for ${filePath}:`, error);
205
+ reply.code(404);
206
+ return { error: "file not found" };
207
+ }
208
+ if (!stats.isFile()) {
209
+ reply.code(400);
210
+ return { error: "path is not a file" };
211
+ }
212
+ const range = parseFileStreamRange(request.headers.range, stats.size);
213
+ if (!range.ok) {
214
+ reply.code(416);
215
+ reply.header("Content-Range", `bytes */${stats.size}`);
216
+ return { error: "range not satisfiable" };
217
+ }
218
+ const contentLength = range.end - range.start + 1;
219
+ reply.code(range.partial ? 206 : 200);
220
+ reply.header("Content-Type", getFileStreamContentType(filePath));
221
+ reply.header("Accept-Ranges", "bytes");
222
+ reply.header("Content-Length", String(contentLength));
223
+ reply.header("Cache-Control", "no-store");
224
+ reply.header("Content-Disposition", `inline; filename="${path.basename(filePath).replace(/["\\]/g, "_")}"`);
225
+ if (range.partial) {
226
+ reply.header("Content-Range", `bytes ${range.start}-${range.end}/${stats.size}`);
227
+ }
228
+ return reply.send(createReadStream(filePath, { start: range.start, end: range.end }));
229
+ });
230
+ typed.post("/session-started", {
231
+ schema: {
232
+ body: z.object({
233
+ sessionId: z.string(),
234
+ metadata: z.any()
235
+ // Metadata type from API
236
+ }),
237
+ response: {
238
+ 200: z.object({
239
+ status: z.literal("ok")
240
+ })
241
+ }
242
+ }
243
+ }, async (request) => {
244
+ const { sessionId, metadata } = request.body;
245
+ logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
246
+ onHappySessionWebhook(sessionId, metadata);
247
+ return { status: "ok" };
248
+ });
249
+ typed.post("/list", {
250
+ schema: {
251
+ response: {
252
+ 200: z.object({
253
+ children: z.array(z.object({
254
+ startedBy: z.string(),
255
+ happySessionId: z.string(),
256
+ pid: z.number()
257
+ }))
258
+ })
259
+ }
260
+ }
261
+ }, async () => {
262
+ const children = getChildren();
263
+ logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
264
+ return {
265
+ children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
266
+ startedBy: child.startedBy,
267
+ happySessionId: child.happySessionId,
268
+ pid: child.pid
269
+ }))
270
+ };
271
+ });
272
+ typed.post("/device-exec", {
273
+ schema: {
274
+ body: z.object({
275
+ deviceId: z.string(),
276
+ command: z.string(),
277
+ timeoutMs: z.number().optional()
278
+ })
279
+ }
280
+ }, async (request, reply) => {
281
+ const { deviceId, command, timeoutMs } = request.body;
282
+ logger.debug(`[CONTROL SERVER] Device exec request for ${deviceId}`);
283
+ try {
284
+ return { ok: true, result: await execOnDevice(deviceId, command, timeoutMs ?? 3e5) };
285
+ } catch (error) {
286
+ return reply.code(200).send({ ok: false, error: error instanceof Error ? error.message : String(error) });
287
+ }
288
+ });
289
+ typed.post("/stop-session", {
290
+ schema: {
291
+ body: z.object({
292
+ sessionId: z.string()
293
+ }),
294
+ response: {
295
+ 200: z.object({
296
+ success: z.boolean()
297
+ })
298
+ }
299
+ }
300
+ }, async (request) => {
301
+ const { sessionId } = request.body;
302
+ logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
303
+ const success = stopSession(sessionId);
304
+ return { success };
305
+ });
306
+ typed.post("/spawn-session", {
307
+ schema: {
308
+ body: z.object({
309
+ directory: z.string(),
310
+ sessionId: z.string().optional()
311
+ }),
312
+ response: {
313
+ 200: z.object({
314
+ success: z.boolean(),
315
+ sessionId: z.string().optional(),
316
+ approvedNewDirectoryCreation: z.boolean().optional()
317
+ }),
318
+ 409: z.object({
319
+ success: z.boolean(),
320
+ requiresUserApproval: z.boolean().optional(),
321
+ actionRequired: z.string().optional(),
322
+ directory: z.string().optional()
323
+ }),
324
+ 500: z.object({
325
+ success: z.boolean(),
326
+ error: z.string().optional()
327
+ })
328
+ }
329
+ }
330
+ }, async (request, reply) => {
331
+ const { directory, sessionId } = request.body;
332
+ logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}`);
333
+ const result = await spawnSession({ directory, sessionId });
334
+ switch (result.type) {
335
+ case "success":
336
+ if (!result.sessionId) {
337
+ reply.code(500);
338
+ return {
339
+ success: false,
340
+ error: "Failed to spawn session: no session ID returned"
341
+ };
342
+ }
343
+ return {
344
+ success: true,
345
+ sessionId: result.sessionId,
346
+ approvedNewDirectoryCreation: true
347
+ };
348
+ case "requestToApproveDirectoryCreation":
349
+ reply.code(409);
350
+ return {
351
+ success: false,
352
+ requiresUserApproval: true,
353
+ actionRequired: "CREATE_DIRECTORY",
354
+ directory: result.directory
355
+ };
356
+ case "error":
357
+ reply.code(500);
358
+ return {
359
+ success: false,
360
+ error: result.errorMessage
361
+ };
362
+ }
363
+ });
364
+ typed.post("/stop", {
365
+ schema: {
366
+ response: {
367
+ 200: z.object({
368
+ status: z.string()
369
+ })
370
+ }
371
+ }
372
+ }, async () => {
373
+ logger.debug("[CONTROL SERVER] Stop daemon request received");
374
+ setTimeout(() => {
375
+ logger.debug("[CONTROL SERVER] Triggering daemon shutdown");
376
+ requestShutdown();
377
+ }, 50);
378
+ return { status: "stopping" };
379
+ });
380
+ app.listen({ port: 0, host: "127.0.0.1" }, (err, address) => {
381
+ if (err) {
382
+ logger.debug("[CONTROL SERVER] Failed to start:", err);
383
+ throw err;
384
+ }
385
+ const port = parseInt(address.split(":").pop());
386
+ logger.debug(`[CONTROL SERVER] Started on port ${port}`);
387
+ resolve({
388
+ port,
389
+ stop: async () => {
390
+ logger.debug("[CONTROL SERVER] Stopping server");
391
+ await app.close();
392
+ logger.debug("[CONTROL SERVER] Server stopped");
393
+ }
394
+ });
395
+ });
396
+ });
397
+ }
398
+
399
+ class TmuxSessionIdentifierError extends Error {
400
+ constructor(message) {
401
+ super(message);
402
+ this.name = "TmuxSessionIdentifierError";
403
+ }
404
+ }
405
+ function parseTmuxSessionIdentifier(identifier) {
406
+ if (!identifier || typeof identifier !== "string") {
407
+ throw new TmuxSessionIdentifierError("Session identifier must be a non-empty string");
408
+ }
409
+ const parts = identifier.split(":");
410
+ if (parts.length === 0 || !parts[0]) {
411
+ throw new TmuxSessionIdentifierError("Invalid session identifier: missing session name");
412
+ }
413
+ const result = {
414
+ session: parts[0].trim()
415
+ };
416
+ if (!/^[a-zA-Z0-9._-]+$/.test(result.session)) {
417
+ throw new TmuxSessionIdentifierError(`Invalid session name: "${result.session}". Only alphanumeric characters, dots, hyphens, and underscores are allowed.`);
418
+ }
419
+ if (parts.length > 1) {
420
+ const windowAndPane = parts[1].split(".");
421
+ result.window = windowAndPane[0]?.trim();
422
+ if (result.window && !/^[a-zA-Z0-9._-]+$/.test(result.window)) {
423
+ throw new TmuxSessionIdentifierError(`Invalid window name: "${result.window}". Only alphanumeric characters, dots, hyphens, and underscores are allowed.`);
424
+ }
425
+ if (windowAndPane.length > 1) {
426
+ result.pane = windowAndPane[1]?.trim();
427
+ if (result.pane && !/^[0-9]+$/.test(result.pane)) {
428
+ throw new TmuxSessionIdentifierError(`Invalid pane identifier: "${result.pane}". Only numeric values are allowed.`);
429
+ }
430
+ }
431
+ }
432
+ return result;
433
+ }
434
+ function formatTmuxSessionIdentifier(identifier) {
435
+ if (!identifier.session) {
436
+ throw new TmuxSessionIdentifierError("Session identifier must have a session name");
437
+ }
438
+ let result = identifier.session;
439
+ if (identifier.window) {
440
+ result += `:${identifier.window}`;
441
+ if (identifier.pane) {
442
+ result += `.${identifier.pane}`;
443
+ }
444
+ }
445
+ return result;
446
+ }
447
+ const WIN_OPS = {
448
+ // Navigation and window management
449
+ "new-window": "new-window",
450
+ "new": "new-window",
451
+ "nw": "new-window",
452
+ "select-window": "select-window -t",
453
+ "sw": "select-window -t",
454
+ "window": "select-window -t",
455
+ "w": "select-window -t",
456
+ "next-window": "next-window",
457
+ "n": "next-window",
458
+ "prev-window": "previous-window",
459
+ "p": "previous-window",
460
+ "pw": "previous-window",
461
+ // Pane management
462
+ "split-window": "split-window",
463
+ "split": "split-window",
464
+ "sp": "split-window",
465
+ "vsplit": "split-window -h",
466
+ "vsp": "split-window -h",
467
+ "select-pane": "select-pane -t",
468
+ "pane": "select-pane -t",
469
+ "next-pane": "select-pane -t :.+",
470
+ "np": "select-pane -t :.+",
471
+ "prev-pane": "select-pane -t :.-",
472
+ "pp": "select-pane -t :.-",
473
+ // Session management
474
+ "new-session": "new-session",
475
+ "ns": "new-session",
476
+ "new-sess": "new-session",
477
+ "attach-session": "attach-session -t",
478
+ "attach": "attach-session -t",
479
+ "as": "attach-session -t",
480
+ "detach-client": "detach-client",
481
+ "detach": "detach-client",
482
+ "dc": "detach-client",
483
+ // Layout and display
484
+ "select-layout": "select-layout",
485
+ "layout": "select-layout",
486
+ "sl": "select-layout",
487
+ "clock-mode": "clock-mode",
488
+ "clock": "clock-mode",
489
+ // Copy mode
490
+ "copy-mode": "copy-mode",
491
+ "copy": "copy-mode",
492
+ // Search and navigation in copy mode
493
+ "search-forward": "search-forward",
494
+ "search-backward": "search-backward",
495
+ // Misc operations
496
+ "list-windows": "list-windows",
497
+ "lw": "list-windows",
498
+ "list-sessions": "list-sessions",
499
+ "ls": "list-sessions",
500
+ "list-panes": "list-panes",
501
+ "lp": "list-panes",
502
+ "rename-window": "rename-window",
503
+ "rename": "rename-window",
504
+ "kill-window": "kill-window",
505
+ "kw": "kill-window",
506
+ "kill-pane": "kill-pane",
507
+ "kp": "kill-pane",
508
+ "kill-session": "kill-session",
509
+ "ks": "kill-session",
510
+ // Display and info
511
+ "display-message": "display-message",
512
+ "display": "display-message",
513
+ "dm": "display-message",
514
+ "show-options": "show-options",
515
+ "show": "show-options",
516
+ "so": "show-options",
517
+ // Control and scripting
518
+ "send-keys": "send-keys",
519
+ "send": "send-keys",
520
+ "sk": "send-keys",
521
+ "capture-pane": "capture-pane",
522
+ "capture": "capture-pane",
523
+ "cp": "capture-pane",
524
+ "pipe-pane": "pipe-pane",
525
+ "pipe": "pipe-pane",
526
+ // Buffer operations
527
+ "list-buffers": "list-buffers",
528
+ "lb": "list-buffers",
529
+ "save-buffer": "save-buffer",
530
+ "sb": "save-buffer",
531
+ "delete-buffer": "delete-buffer",
532
+ "db": "delete-buffer",
533
+ // Advanced operations
534
+ "resize-pane": "resize-pane",
535
+ "resize": "resize-pane",
536
+ "rp": "resize-pane",
537
+ "swap-pane": "swap-pane",
538
+ "swap": "swap-pane",
539
+ "join-pane": "join-pane",
540
+ "join": "join-pane",
541
+ "break-pane": "break-pane",
542
+ "break": "break-pane"
543
+ };
544
+ const COMMANDS_SUPPORTING_TARGET = /* @__PURE__ */ new Set([
545
+ "send-keys",
546
+ "capture-pane",
547
+ "new-window",
548
+ "kill-window",
549
+ "select-window",
550
+ "split-window",
551
+ "select-pane",
552
+ "kill-pane",
553
+ "select-layout",
554
+ "display-message",
555
+ "attach-session",
556
+ "detach-client",
557
+ "new-session",
558
+ "kill-session",
559
+ "list-windows",
560
+ "list-panes"
561
+ ]);
562
+ const CONTROL_SEQUENCES = /* @__PURE__ */ new Set([
563
+ "C-m",
564
+ "C-c",
565
+ "C-l",
566
+ "C-u",
567
+ "C-w",
568
+ "C-a",
569
+ "C-b",
570
+ "C-d",
571
+ "C-e",
572
+ "C-f",
573
+ "C-g",
574
+ "C-h",
575
+ "C-i",
576
+ "C-j",
577
+ "C-k",
578
+ "C-n",
579
+ "C-o",
580
+ "C-p",
581
+ "C-q",
582
+ "C-r",
583
+ "C-s",
584
+ "C-t",
585
+ "C-v",
586
+ "C-x",
587
+ "C-y",
588
+ "C-z",
589
+ "C-\\",
590
+ "C-]",
591
+ "C-[",
592
+ "C-]"
593
+ ]);
594
+ class TmuxUtilities {
595
+ /** Default session name to prevent interference */
596
+ static DEFAULT_SESSION_NAME = "happy";
597
+ controlState = "normal" /* NORMAL */;
598
+ sessionName;
599
+ constructor(sessionName) {
600
+ this.sessionName = sessionName || TmuxUtilities.DEFAULT_SESSION_NAME;
601
+ }
602
+ /**
603
+ * Detect tmux environment from TMUX environment variable
604
+ */
605
+ detectTmuxEnvironment() {
606
+ const tmuxEnv = process.env.TMUX;
607
+ if (!tmuxEnv) {
608
+ return null;
609
+ }
610
+ try {
611
+ const parts = tmuxEnv.split(",");
612
+ if (parts.length >= 3) {
613
+ const socketPath = parts[0];
614
+ const pathParts = parts[1].split("/");
615
+ const sessionAndWindow = pathParts[pathParts.length - 1] || parts[1];
616
+ const pane = parts[2];
617
+ let session;
618
+ let window;
619
+ if (sessionAndWindow.includes(".")) {
620
+ const parts2 = sessionAndWindow.split(".", 2);
621
+ session = parts2[0];
622
+ window = parts2[1] || "0";
623
+ } else {
624
+ session = sessionAndWindow;
625
+ window = "0";
626
+ }
627
+ return {
628
+ session,
629
+ window,
630
+ pane,
631
+ socket_path: socketPath
632
+ };
633
+ }
634
+ } catch (error) {
635
+ logger.debug("[TMUX] Failed to parse TMUX environment variable:", error);
636
+ }
637
+ return null;
638
+ }
639
+ /**
640
+ * Execute tmux command with proper session targeting and socket handling
641
+ */
642
+ async executeTmuxCommand(cmd, session, window, pane, socketPath) {
643
+ const targetSession = session || this.sessionName;
644
+ let baseCmd = ["tmux"];
645
+ if (socketPath) {
646
+ baseCmd = ["tmux", "-S", socketPath];
647
+ }
648
+ if (cmd.length > 0 && cmd[0] === "send-keys") {
649
+ const fullCmd = [...baseCmd, cmd[0]];
650
+ let target = targetSession;
651
+ if (window) target += `:${window}`;
652
+ if (pane) target += `.${pane}`;
653
+ fullCmd.push("-t", target);
654
+ fullCmd.push(...cmd.slice(1));
655
+ return this.executeCommand(fullCmd);
656
+ } else {
657
+ const fullCmd = [...baseCmd, ...cmd];
658
+ if (cmd.length > 0 && COMMANDS_SUPPORTING_TARGET.has(cmd[0])) {
659
+ let target = targetSession;
660
+ if (window) target += `:${window}`;
661
+ if (pane) target += `.${pane}`;
662
+ fullCmd.push("-t", target);
663
+ }
664
+ return this.executeCommand(fullCmd);
665
+ }
666
+ }
667
+ /**
668
+ * Execute command with subprocess and return result
669
+ */
670
+ async executeCommand(cmd) {
671
+ try {
672
+ const result = await this.runCommand(cmd);
673
+ return {
674
+ returncode: result.exitCode,
675
+ stdout: result.stdout || "",
676
+ stderr: result.stderr || "",
677
+ command: cmd
678
+ };
679
+ } catch (error) {
680
+ logger.debug("[TMUX] Command execution failed:", error);
681
+ return null;
682
+ }
683
+ }
684
+ /**
685
+ * Run command using Node.js child_process.spawn
686
+ */
687
+ runCommand(args, options = {}) {
688
+ return new Promise((resolve, reject) => {
689
+ const child = spawn(args[0], args.slice(1), {
690
+ stdio: ["ignore", "pipe", "pipe"],
691
+ timeout: 5e3,
692
+ shell: false,
693
+ ...options
694
+ });
695
+ let stdout = "";
696
+ let stderr = "";
697
+ child.stdout?.on("data", (data) => {
698
+ stdout += data.toString();
699
+ });
700
+ child.stderr?.on("data", (data) => {
701
+ stderr += data.toString();
702
+ });
703
+ child.on("close", (code) => {
704
+ resolve({
705
+ exitCode: code || 0,
706
+ stdout,
707
+ stderr
708
+ });
709
+ });
710
+ child.on("error", (error) => {
711
+ reject(error);
712
+ });
713
+ });
714
+ }
715
+ /**
716
+ * Parse control sequences in text (^ for escape, ^^ for literal ^)
717
+ */
718
+ parseControlSequences(text) {
719
+ const result = [];
720
+ let i = 0;
721
+ let localState = this.controlState;
722
+ while (i < text.length) {
723
+ const char = text[i];
724
+ if (localState === "normal" /* NORMAL */) {
725
+ if (char === "^") {
726
+ if (i + 1 < text.length && text[i + 1] === "^") {
727
+ result.push("^");
728
+ i += 2;
729
+ } else {
730
+ localState = "escape" /* ESCAPE */;
731
+ i += 1;
732
+ }
733
+ } else {
734
+ result.push(char);
735
+ i += 1;
736
+ }
737
+ } else if (localState === "escape" /* ESCAPE */) {
738
+ result.push(char);
739
+ i += 1;
740
+ localState = "normal" /* NORMAL */;
741
+ } else {
742
+ result.push(char);
743
+ i += 1;
744
+ }
745
+ }
746
+ this.controlState = localState;
747
+ return [result.join(""), localState];
748
+ }
749
+ /**
750
+ * Execute window operation using WIN_OPS dispatch with type safety
751
+ */
752
+ async executeWinOp(operation, args = [], session, window, pane) {
753
+ const tmuxCmd = WIN_OPS[operation];
754
+ if (!tmuxCmd) {
755
+ logger.debug(`[TMUX] Unknown operation: ${operation}`);
756
+ return false;
757
+ }
758
+ const cmdParts = tmuxCmd.split(" ");
759
+ cmdParts.push(...args);
760
+ const result = await this.executeTmuxCommand(cmdParts, session, window, pane);
761
+ return result !== null && result.returncode === 0;
762
+ }
763
+ /**
764
+ * Ensure session exists, create if needed
765
+ */
766
+ async ensureSessionExists(sessionName) {
767
+ const targetSession = sessionName || this.sessionName;
768
+ const result = await this.executeTmuxCommand(["has-session", "-t", targetSession]);
769
+ if (result && result.returncode === 0) {
770
+ return true;
771
+ }
772
+ const createResult = await this.executeTmuxCommand(["new-session", "-d", "-s", targetSession]);
773
+ return createResult !== null && createResult.returncode === 0;
774
+ }
775
+ /**
776
+ * Capture current input from tmux pane
777
+ */
778
+ async captureCurrentInput(session, window, pane) {
779
+ const result = await this.executeTmuxCommand(["capture-pane", "-p"], session, window, pane);
780
+ if (result && result.returncode === 0) {
781
+ const lines = result.stdout.trim().split("\n");
782
+ return lines[lines.length - 1] || "";
783
+ }
784
+ return "";
785
+ }
786
+ /**
787
+ * Check if user is actively typing
788
+ */
789
+ async isUserTyping(checkInterval = 500, maxChecks = 3, session, window, pane) {
790
+ const initialInput = await this.captureCurrentInput(session, window, pane);
791
+ for (let i = 0; i < maxChecks - 1; i++) {
792
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
793
+ const currentInput = await this.captureCurrentInput(session, window, pane);
794
+ if (currentInput !== initialInput) {
795
+ return true;
796
+ }
797
+ }
798
+ return false;
799
+ }
800
+ /**
801
+ * Send keys to tmux pane with proper control sequence handling and type safety
802
+ */
803
+ async sendKeys(keys, session, window, pane) {
804
+ if (!keys || typeof keys !== "string") {
805
+ logger.debug("[TMUX] Invalid keys provided to sendKeys");
806
+ return false;
807
+ }
808
+ if (CONTROL_SEQUENCES.has(keys)) {
809
+ const result = await this.executeTmuxCommand(["send-keys", keys], session, window, pane);
810
+ return result !== null && result.returncode === 0;
811
+ } else {
812
+ const result = await this.executeTmuxCommand(["send-keys", keys], session, window, pane);
813
+ return result !== null && result.returncode === 0;
814
+ }
815
+ }
816
+ /**
817
+ * Send multiple keys to tmux pane with proper control sequence handling
818
+ */
819
+ async sendMultipleKeys(keys, session, window, pane) {
820
+ if (!Array.isArray(keys) || keys.length === 0) {
821
+ logger.debug("[TMUX] Invalid keys array provided to sendMultipleKeys");
822
+ return false;
823
+ }
824
+ for (const key of keys) {
825
+ const success = await this.sendKeys(key, session, window, pane);
826
+ if (!success) {
827
+ return false;
828
+ }
829
+ }
830
+ return true;
831
+ }
832
+ /**
833
+ * Get comprehensive session information
834
+ */
835
+ async getSessionInfo(sessionName) {
836
+ const targetSession = sessionName || this.sessionName;
837
+ const envInfo = this.detectTmuxEnvironment();
838
+ const info = {
839
+ target_session: targetSession,
840
+ session: targetSession,
841
+ window: "unknown",
842
+ pane: "unknown",
843
+ socket_path: void 0,
844
+ tmux_active: envInfo !== null,
845
+ current_session: envInfo?.session,
846
+ available_sessions: []
847
+ };
848
+ if (envInfo && envInfo.session === targetSession) {
849
+ info.window = envInfo.window;
850
+ info.pane = envInfo.pane;
851
+ info.socket_path = envInfo.socket_path;
852
+ } else if (envInfo) {
853
+ info.env_session = envInfo.session;
854
+ info.env_window = envInfo.window;
855
+ info.env_pane = envInfo.pane;
856
+ }
857
+ const result = await this.executeTmuxCommand(["list-sessions"]);
858
+ if (result && result.returncode === 0) {
859
+ info.available_sessions = result.stdout.trim().split("\n").filter((line) => line.trim()).map((line) => line.split(":")[0]);
860
+ }
861
+ return info;
862
+ }
863
+ /**
864
+ * Spawn process in tmux session with environment variables.
865
+ *
866
+ * IMPORTANT: Unlike Node.js spawn(), env is a separate parameter.
867
+ * This is intentional because:
868
+ * - Tmux windows inherit environment from the tmux server
869
+ * - Only NEW or DIFFERENT variables need to be set via -e flag
870
+ * - Passing all of process.env would create 50+ unnecessary -e flags
871
+ *
872
+ * @param args - Command and arguments to execute (as array, will be joined)
873
+ * @param options - Spawn options (tmux-specific, excludes env)
874
+ * @param env - Environment variables to set in window (only pass what's different!)
875
+ * @returns Result with success status and session identifier
876
+ */
877
+ async spawnInTmux(args, options = {}, env) {
878
+ try {
879
+ const tmuxCheck = await this.executeTmuxCommand(["list-sessions"]);
880
+ if (!tmuxCheck) {
881
+ throw new Error("tmux not available");
882
+ }
883
+ let sessionName = options.sessionName !== void 0 && options.sessionName !== "" ? options.sessionName : null;
884
+ if (!sessionName) {
885
+ const listResult = await this.executeTmuxCommand(["list-sessions", "-F", "#{session_name}"]);
886
+ if (listResult && listResult.returncode === 0 && listResult.stdout.trim()) {
887
+ const firstSession = listResult.stdout.trim().split("\n")[0];
888
+ sessionName = firstSession;
889
+ logger.debug(`[TMUX] Using first existing session: ${sessionName}`);
890
+ } else {
891
+ sessionName = "happy";
892
+ logger.debug(`[TMUX] No existing sessions, using default: ${sessionName}`);
893
+ }
894
+ }
895
+ const windowName = options.windowName || `happy-${Date.now()}`;
896
+ await this.ensureSessionExists(sessionName);
897
+ const fullCommand = args.join(" ");
898
+ const createWindowArgs = ["new-window", "-n", windowName];
899
+ if (options.cwd) {
900
+ const cwdPath = typeof options.cwd === "string" ? options.cwd : options.cwd.pathname;
901
+ createWindowArgs.push("-c", cwdPath);
902
+ }
903
+ if (env && Object.keys(env).length > 0) {
904
+ for (const [key, value] of Object.entries(env)) {
905
+ if (value === void 0 || value === null) {
906
+ logger.warn(`[TMUX] Skipping undefined/null environment variable: ${key}`);
907
+ continue;
908
+ }
909
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
910
+ logger.warn(`[TMUX] Skipping invalid environment variable name: ${key}`);
911
+ continue;
912
+ }
913
+ createWindowArgs.push("-e", `${key}=${value}`);
914
+ }
915
+ logger.debug(`[TMUX] Setting ${Object.keys(env).length} environment variables in tmux window`);
916
+ }
917
+ createWindowArgs.push(fullCommand);
918
+ createWindowArgs.push("-P");
919
+ createWindowArgs.push("-F", "#{pane_pid}");
920
+ const createResult = await this.executeTmuxCommand(createWindowArgs, sessionName);
921
+ if (!createResult || createResult.returncode !== 0) {
922
+ throw new Error(`Failed to create tmux window: ${createResult?.stderr}`);
923
+ }
924
+ const panePid = parseInt(createResult.stdout.trim());
925
+ if (isNaN(panePid)) {
926
+ throw new Error(`Failed to extract PID from tmux output: ${createResult.stdout}`);
927
+ }
928
+ logger.debug(`[TMUX] Spawned command in tmux session ${sessionName}, window ${windowName}, PID ${panePid}`);
929
+ const sessionIdentifier = {
930
+ session: sessionName,
931
+ window: windowName
932
+ };
933
+ return {
934
+ success: true,
935
+ sessionId: formatTmuxSessionIdentifier(sessionIdentifier),
936
+ pid: panePid
937
+ };
938
+ } catch (error) {
939
+ logger.debug("[TMUX] Failed to spawn in tmux:", error);
940
+ return {
941
+ success: false,
942
+ error: error instanceof Error ? error.message : String(error)
943
+ };
944
+ }
945
+ }
946
+ /**
947
+ * Get session info for a given session identifier string
948
+ */
949
+ async getSessionInfoFromString(sessionIdentifier) {
950
+ try {
951
+ const parsed = parseTmuxSessionIdentifier(sessionIdentifier);
952
+ const info = await this.getSessionInfo(parsed.session);
953
+ return info;
954
+ } catch (error) {
955
+ if (error instanceof TmuxSessionIdentifierError) {
956
+ logger.debug(`[TMUX] Invalid session identifier: ${error.message}`);
957
+ } else {
958
+ logger.debug("[TMUX] Error getting session info:", error);
959
+ }
960
+ return null;
961
+ }
962
+ }
963
+ /**
964
+ * Kill a tmux window safely with proper error handling
965
+ */
966
+ async killWindow(sessionIdentifier) {
967
+ try {
968
+ const parsed = parseTmuxSessionIdentifier(sessionIdentifier);
969
+ if (!parsed.window) {
970
+ throw new TmuxSessionIdentifierError(`Window identifier required: ${sessionIdentifier}`);
971
+ }
972
+ const result = await this.executeWinOp("kill-window", [parsed.window], parsed.session);
973
+ return result;
974
+ } catch (error) {
975
+ if (error instanceof TmuxSessionIdentifierError) {
976
+ logger.debug(`[TMUX] Invalid window identifier: ${error.message}`);
977
+ } else {
978
+ logger.debug("[TMUX] Error killing window:", error);
979
+ }
980
+ return false;
981
+ }
982
+ }
983
+ /**
984
+ * List windows in a session
985
+ */
986
+ async listWindows(sessionName) {
987
+ const targetSession = sessionName || this.sessionName;
988
+ const result = await this.executeTmuxCommand(["list-windows", "-t", targetSession]);
989
+ if (!result || result.returncode !== 0) {
990
+ return [];
991
+ }
992
+ const windows = [];
993
+ const lines = result.stdout.trim().split("\n");
994
+ for (const line of lines) {
995
+ const match = line.match(/^\d+:\s+(\w+)/);
996
+ if (match) {
997
+ windows.push(match[1]);
998
+ }
999
+ }
1000
+ return windows;
1001
+ }
1002
+ }
1003
+ let _tmuxUtils = null;
1004
+ function getTmuxUtilities(sessionName) {
1005
+ if (!_tmuxUtils || sessionName && sessionName !== _tmuxUtils.sessionName) {
1006
+ _tmuxUtils = new TmuxUtilities(sessionName);
1007
+ }
1008
+ return _tmuxUtils;
1009
+ }
1010
+ async function isTmuxAvailable() {
1011
+ try {
1012
+ const utils = new TmuxUtilities();
1013
+ const result = await utils.executeTmuxCommand(["list-sessions"]);
1014
+ return result !== null;
1015
+ } catch {
1016
+ return false;
1017
+ }
1018
+ }
1019
+
1020
+ function expandEnvironmentVariables(envVars, sourceEnv = process.env) {
1021
+ const expanded = {};
1022
+ const undefinedVars = [];
1023
+ for (const [key, value] of Object.entries(envVars)) {
1024
+ const expandedValue = value.replace(/\$\{([^}]+)\}/g, (match, expr) => {
1025
+ const colonDashIndex = expr.indexOf(":-");
1026
+ let varName;
1027
+ let defaultValue;
1028
+ if (colonDashIndex !== -1) {
1029
+ varName = expr.substring(0, colonDashIndex);
1030
+ defaultValue = expr.substring(colonDashIndex + 2);
1031
+ } else {
1032
+ varName = expr;
1033
+ }
1034
+ const resolvedValue = sourceEnv[varName];
1035
+ if (resolvedValue !== void 0) {
1036
+ const isSensitive = varName.toLowerCase().includes("token") || varName.toLowerCase().includes("key") || varName.toLowerCase().includes("secret");
1037
+ const displayValue = isSensitive ? resolvedValue ? `<${resolvedValue.length} chars>` : "<empty>" : resolvedValue;
1038
+ logger.debug(`[EXPAND ENV] Expanded ${varName} from daemon env: ${displayValue}`);
1039
+ if (resolvedValue === "") {
1040
+ logger.warn(`[EXPAND ENV] WARNING: ${varName} is set but EMPTY in daemon environment`);
1041
+ }
1042
+ return resolvedValue;
1043
+ } else if (defaultValue !== void 0) {
1044
+ logger.debug(`[EXPAND ENV] Using default value for ${varName}: ${defaultValue}`);
1045
+ return defaultValue;
1046
+ } else {
1047
+ undefinedVars.push(varName);
1048
+ return match;
1049
+ }
1050
+ });
1051
+ expanded[key] = expandedValue;
1052
+ }
1053
+ if (undefinedVars.length > 0) {
1054
+ logger.warn(`[EXPAND ENV] Undefined variables referenced in profile environment: ${undefinedVars.join(", ")}`);
1055
+ logger.warn(`[EXPAND ENV] Session may fail to authenticate. Set these in daemon environment before launching:`);
1056
+ undefinedVars.forEach((varName) => {
1057
+ logger.warn(`[EXPAND ENV] ${varName}=<your-value>`);
1058
+ });
1059
+ }
1060
+ return expanded;
1061
+ }
1062
+
1063
+ const GEMINI_TEXT_KEYS = /* @__PURE__ */ new Set([
1064
+ "text",
1065
+ "content",
1066
+ "output",
1067
+ "message",
1068
+ "response",
1069
+ "result",
1070
+ "answer",
1071
+ "final_text"
1072
+ ]);
1073
+ const GEMINI_META_KEYS = /* @__PURE__ */ new Set([
1074
+ "session_id",
1075
+ "sessionId",
1076
+ "id",
1077
+ "usage",
1078
+ "metadata",
1079
+ "model",
1080
+ "role",
1081
+ "timestamp"
1082
+ ]);
1083
+ const ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
1084
+ function stripAnsi(value) {
1085
+ return value.replace(ANSI_ESCAPE_REGEX, "");
1086
+ }
1087
+ function extractCodexSessionId(output) {
1088
+ const cleaned = stripAnsi(output);
1089
+ const match = cleaned.match(/session id:\s*([^\s]+)/i);
1090
+ if (!match) {
1091
+ return null;
1092
+ }
1093
+ const token = match[1].trim().replace(/^['"`(\[]+|[)\]"'`,.;:]+$/g, "");
1094
+ if (!/^[0-9a-zA-Z-]{8,}$/.test(token)) {
1095
+ return null;
1096
+ }
1097
+ return token;
1098
+ }
1099
+ function extractGeminiSessionId(stdout) {
1100
+ const trimmed = stdout.trim();
1101
+ if (!trimmed) {
1102
+ return null;
1103
+ }
1104
+ try {
1105
+ const parsed = JSON.parse(trimmed);
1106
+ if (typeof parsed.session_id === "string") {
1107
+ const sessionId = parsed.session_id.trim();
1108
+ if (sessionId.length > 0) {
1109
+ return sessionId;
1110
+ }
1111
+ }
1112
+ } catch (_error) {
1113
+ }
1114
+ for (const line of stdout.split(/\r?\n/)) {
1115
+ const result = extractGeminiSessionIdFromJsonLine(line);
1116
+ if (result) {
1117
+ return result;
1118
+ }
1119
+ }
1120
+ return null;
1121
+ }
1122
+ function extractGeminiSessionIdFromJsonLine(line) {
1123
+ const trimmed = line.trim();
1124
+ if (!trimmed) {
1125
+ return null;
1126
+ }
1127
+ try {
1128
+ const parsed = JSON.parse(trimmed);
1129
+ if (typeof parsed.session_id === "string") {
1130
+ const sessionId = parsed.session_id.trim();
1131
+ return sessionId.length > 0 ? sessionId : null;
1132
+ }
1133
+ } catch (_error) {
1134
+ return null;
1135
+ }
1136
+ return null;
1137
+ }
1138
+ function collectGeminiTextCandidates(value, out) {
1139
+ if (typeof value === "string") {
1140
+ const trimmed = value.trim();
1141
+ if (trimmed.length > 0) {
1142
+ out.push(trimmed);
1143
+ }
1144
+ return;
1145
+ }
1146
+ if (Array.isArray(value)) {
1147
+ for (const item of value) {
1148
+ collectGeminiTextCandidates(item, out);
1149
+ }
1150
+ return;
1151
+ }
1152
+ if (!value || typeof value !== "object") {
1153
+ return;
1154
+ }
1155
+ for (const [key, child] of Object.entries(value)) {
1156
+ if (GEMINI_META_KEYS.has(key)) {
1157
+ continue;
1158
+ }
1159
+ if (GEMINI_TEXT_KEYS.has(key)) {
1160
+ collectGeminiTextCandidates(child, out);
1161
+ continue;
1162
+ }
1163
+ if (key === "parts" && Array.isArray(child)) {
1164
+ collectGeminiTextCandidates(child, out);
1165
+ continue;
1166
+ }
1167
+ collectGeminiTextCandidates(child, out);
1168
+ }
1169
+ }
1170
+ function normalizeGeminiOutputText(stdout) {
1171
+ const trimmed = stdout.trim();
1172
+ if (!trimmed) {
1173
+ return "";
1174
+ }
1175
+ const candidates = [];
1176
+ let sawJson = false;
1177
+ try {
1178
+ const parsed = JSON.parse(trimmed);
1179
+ sawJson = true;
1180
+ collectGeminiTextCandidates(parsed, candidates);
1181
+ } catch (_error) {
1182
+ }
1183
+ if (!sawJson) {
1184
+ for (const line of stdout.split(/\r?\n/)) {
1185
+ const trimmedLine = line.trim();
1186
+ if (!trimmedLine) continue;
1187
+ try {
1188
+ const parsed = JSON.parse(trimmedLine);
1189
+ sawJson = true;
1190
+ collectGeminiTextCandidates(parsed, candidates);
1191
+ } catch (_error) {
1192
+ }
1193
+ }
1194
+ }
1195
+ if (!sawJson) {
1196
+ return trimmed;
1197
+ }
1198
+ const unique = [...new Set(candidates)];
1199
+ return unique.join("\n").trim();
1200
+ }
1201
+
1202
+ const SESSION_SCOPED_HAPPY_ENV_KEYS = [
1203
+ "HAPPY_CLAUDE_RESUME_SESSION_ID",
1204
+ "HAPPY_CLAUDE_BACKFILL",
1205
+ "HAPPY_CLAUDE_BACKFILL_MAX_MESSAGES",
1206
+ "HAPPY_CLAUDE_BACKFILL_MAX_USER_MESSAGES",
1207
+ "HAPPY_CLAUDE_BACKFILL_MAX_BYTES",
1208
+ "HAPPY_CLAUDE_SKIP_FORK_SESSION",
1209
+ "HAPPY_GEMINI_RESUME_SESSION_ID",
1210
+ "HAPPY_GEMINI_BACKFILL",
1211
+ "HAPPY_CODEX_RESUME_FILE",
1212
+ "HAPPY_CODEX_BACKFILL",
1213
+ "HAPPY_SESSION_TITLE",
1214
+ "HAPPY_WORKTREE_BASE_PATH",
1215
+ "HAPPY_WORKTREE_BRANCH_NAME",
1216
+ "HAPPY_WORKSPACE_REPOS",
1217
+ "HAPPY_WORKSPACE_PATH",
1218
+ "HAPPY_EXTRA_MCP_SERVERS"
1219
+ ];
1220
+ function stripSessionScopedHappyEnv(env) {
1221
+ const cleaned = { ...env };
1222
+ for (const key of SESSION_SCOPED_HAPPY_ENV_KEYS) {
1223
+ delete cleaned[key];
1224
+ }
1225
+ return cleaned;
1226
+ }
1227
+ const ORCHESTRATOR_WATCHDOG_GRACE_MS = 5e3;
1228
+ const ORCHESTRATOR_OUTPUT_CAPTURE_LIMIT = 64e3;
1229
+ const ORCHESTRATOR_SHUTDOWN_WAIT_MS = 8e3;
1230
+ const initialMachineMetadata = {
1231
+ host: os.hostname(),
1232
+ platform: os.platform(),
1233
+ happyCliVersion: packageJson.version,
1234
+ homeDir: os.homedir(),
1235
+ happyHomeDir: configuration.happyHomeDir,
1236
+ happyLibDir: projectPath()
1237
+ };
1238
+ async function buildMachineMetadata() {
1239
+ const settings = await readSettings();
1240
+ return { ...initialMachineMetadata, deviceMode: settings.deviceMode === true };
1241
+ }
1242
+ async function getProfileEnvironmentVariablesForAgent(profileId, agentType) {
1243
+ try {
1244
+ const settings = await readSettings();
1245
+ const profile = settings.profiles.find((p) => p.id === profileId);
1246
+ if (!profile) {
1247
+ logger.debug(`[DAEMON RUN] Profile ${profileId} not found`);
1248
+ return {};
1249
+ }
1250
+ if (!validateProfileForAgent(profile, agentType)) {
1251
+ logger.debug(`[DAEMON RUN] Profile ${profileId} not compatible with agent ${agentType}`);
1252
+ return {};
1253
+ }
1254
+ const envVars = getProfileEnvironmentVariables(profile);
1255
+ logger.debug(`[DAEMON RUN] Loaded ${Object.keys(envVars).length} environment variables from profile ${profileId} for agent ${agentType}`);
1256
+ return envVars;
1257
+ } catch (error) {
1258
+ logger.debug("[DAEMON RUN] Failed to get profile environment variables:", error);
1259
+ return {};
1260
+ }
1261
+ }
1262
+ async function startDaemon() {
1263
+ for (const key of SESSION_SCOPED_HAPPY_ENV_KEYS) {
1264
+ if (process.env[key] !== void 0) {
1265
+ logger.debug(`[DAEMON RUN] Stripping leaked session-scoped env on entry: ${key}`);
1266
+ delete process.env[key];
1267
+ }
1268
+ }
1269
+ let requestShutdown;
1270
+ let resolvesWhenShutdownRequested = new Promise((resolve) => {
1271
+ requestShutdown = (source, errorMessage) => {
1272
+ logger.debug(`[DAEMON RUN] Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`);
1273
+ setTimeout(async () => {
1274
+ logger.debug("[DAEMON RUN] Startup malfunctioned, forcing exit with code 1");
1275
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
1276
+ process.exit(1);
1277
+ }, 1e3);
1278
+ resolve({ source, errorMessage });
1279
+ };
1280
+ });
1281
+ process.on("SIGINT", () => {
1282
+ logger.debug("[DAEMON RUN] Received SIGINT");
1283
+ requestShutdown("os-signal");
1284
+ });
1285
+ process.on("SIGTERM", () => {
1286
+ logger.debug("[DAEMON RUN] Received SIGTERM");
1287
+ requestShutdown("os-signal");
1288
+ });
1289
+ process.on("uncaughtException", (error) => {
1290
+ logger.debug("[DAEMON RUN] FATAL: Uncaught exception", error);
1291
+ logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
1292
+ requestShutdown("exception", error.message);
1293
+ });
1294
+ process.on("unhandledRejection", (reason, promise) => {
1295
+ logger.debug("[DAEMON RUN] FATAL: Unhandled promise rejection", reason);
1296
+ logger.debug(`[DAEMON RUN] Rejected promise:`, promise);
1297
+ const error = reason instanceof Error ? reason : new Error(`Unhandled promise rejection: ${reason}`);
1298
+ logger.debug(`[DAEMON RUN] Stack trace: ${error.stack}`);
1299
+ requestShutdown("exception", error.message);
1300
+ });
1301
+ process.on("exit", (code) => {
1302
+ logger.debug(`[DAEMON RUN] Process exiting with code: ${code}`);
1303
+ });
1304
+ process.on("beforeExit", (code) => {
1305
+ logger.debug(`[DAEMON RUN] Process about to exit with code: ${code}`);
1306
+ });
1307
+ logger.debug("[DAEMON RUN] Starting daemon process...");
1308
+ logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
1309
+ const runningDaemonVersionMatches = await isDaemonRunningCurrentlyInstalledHappyVersion();
1310
+ if (!runningDaemonVersionMatches) {
1311
+ logger.debug("[DAEMON RUN] Daemon version mismatch detected, restarting daemon with current CLI version");
1312
+ await stopDaemon();
1313
+ } else {
1314
+ logger.debug("[DAEMON RUN] Daemon version matches, keeping existing daemon");
1315
+ console.log("Daemon already running with matching version");
1316
+ process.exit(0);
1317
+ }
1318
+ const daemonLockHandle = await acquireDaemonLock(5, 200);
1319
+ if (!daemonLockHandle) {
1320
+ logger.debug("[DAEMON RUN] Daemon lock file already held, another daemon is running");
1321
+ process.exit(0);
1322
+ }
1323
+ try {
1324
+ const caffeinateStarted = startCaffeinate();
1325
+ if (caffeinateStarted) {
1326
+ logger.debug("[DAEMON RUN] Sleep prevention enabled");
1327
+ }
1328
+ const { credentials, machineId } = await authAndSetupMachineIfNeeded();
1329
+ logger.debug("[DAEMON RUN] Auth and machine setup complete");
1330
+ let api;
1331
+ const pidToTrackedSession = /* @__PURE__ */ new Map();
1332
+ const pidToAwaiter = /* @__PURE__ */ new Map();
1333
+ const getCurrentChildren = () => Array.from(pidToTrackedSession.values());
1334
+ const executionIdToManagedExecution = /* @__PURE__ */ new Map();
1335
+ const clearExecutionTimers = (execution) => {
1336
+ if (execution.watchdogTimer) {
1337
+ clearTimeout(execution.watchdogTimer);
1338
+ execution.watchdogTimer = null;
1339
+ }
1340
+ if (execution.killTimer) {
1341
+ clearTimeout(execution.killTimer);
1342
+ execution.killTimer = null;
1343
+ }
1344
+ };
1345
+ const requestExecutionTermination = (execution) => {
1346
+ if (execution.child.killed) {
1347
+ return;
1348
+ }
1349
+ try {
1350
+ execution.child.kill("SIGTERM");
1351
+ } catch (error) {
1352
+ logger.debug(`[ORCHESTRATOR] Failed to send SIGTERM for execution ${execution.payload.executionId}`, error);
1353
+ }
1354
+ if (execution.killTimer) {
1355
+ return;
1356
+ }
1357
+ execution.killTimer = setTimeout(() => {
1358
+ execution.killTimer = null;
1359
+ if (execution.child.exitCode !== null || execution.child.signalCode !== null) {
1360
+ return;
1361
+ }
1362
+ try {
1363
+ execution.child.kill("SIGKILL");
1364
+ } catch (error) {
1365
+ logger.debug(`[ORCHESTRATOR] Failed to send SIGKILL for execution ${execution.payload.executionId}`, error);
1366
+ }
1367
+ }, ORCHESTRATOR_WATCHDOG_GRACE_MS);
1368
+ };
1369
+ const reportExecutionFinish = async (execution, opts) => {
1370
+ if (execution.finishReportPromise) {
1371
+ await execution.finishReportPromise;
1372
+ return;
1373
+ }
1374
+ execution.finishReportPromise = (async () => {
1375
+ const normalizedStdout = execution.payload.provider === "gemini" ? normalizeGeminiOutputText(execution.stdout) : execution.stdout.trim();
1376
+ const outputText = [normalizedStdout, execution.stderr.trim()].filter(Boolean).join("\n");
1377
+ const outputSummary = buildOutputSummary(normalizedStdout, execution.stderr);
1378
+ const normalizedChildSessionId = execution.detectedChildSessionId?.trim();
1379
+ try {
1380
+ await api.reportOrchestratorExecutionFinish({
1381
+ executionId: execution.payload.executionId,
1382
+ dispatchToken: execution.payload.dispatchToken,
1383
+ status: opts.status,
1384
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1385
+ exitCode: opts.exitCode,
1386
+ signal: opts.signal,
1387
+ outputSummary: outputSummary ?? void 0,
1388
+ outputText: outputText || void 0,
1389
+ childSessionId: normalizedChildSessionId ? normalizedChildSessionId : void 0,
1390
+ errorCode: opts.errorCode,
1391
+ errorMessage: opts.errorMessage
1392
+ });
1393
+ } catch (error) {
1394
+ logger.debug(`[ORCHESTRATOR] Failed to report finish for execution ${execution.payload.executionId}`, error);
1395
+ } finally {
1396
+ clearExecutionTimers(execution);
1397
+ executionIdToManagedExecution.delete(execution.payload.executionId);
1398
+ }
1399
+ })();
1400
+ await execution.finishReportPromise;
1401
+ };
1402
+ const finalizeExecution = async (executionId, exitCode, signal) => {
1403
+ const execution = executionIdToManagedExecution.get(executionId);
1404
+ if (!execution) {
1405
+ return;
1406
+ }
1407
+ if (!execution.detectedChildSessionId && execution.payload.provider === "gemini" && execution.payload.executionType === "initial") {
1408
+ const parsed = extractGeminiSessionId(execution.stdout);
1409
+ if (parsed) {
1410
+ execution.detectedChildSessionId = parsed;
1411
+ }
1412
+ }
1413
+ const status = mapFinishStatus({
1414
+ watchdogTriggered: execution.watchdogTriggered,
1415
+ cancelRequested: execution.cancelRequested,
1416
+ exitCode
1417
+ });
1418
+ await execution.startReportPromise;
1419
+ await reportExecutionFinish(execution, {
1420
+ status,
1421
+ exitCode,
1422
+ signal,
1423
+ errorCode: status === "timeout" ? "WATCHDOG_TIMEOUT" : status === "failed" ? "PROCESS_EXIT_NON_ZERO" : void 0,
1424
+ errorMessage: status === "timeout" ? `Execution exceeded timeout (${execution.payload.timeoutMs}ms)` : status === "failed" ? `Process exited with code ${exitCode ?? "unknown"}${signal ? ` (signal ${signal})` : ""}` : void 0
1425
+ });
1426
+ };
1427
+ const handleOrchestratorDispatch = async (payload) => {
1428
+ const existing = executionIdToManagedExecution.get(payload.executionId);
1429
+ if (existing) {
1430
+ if (existing.payload.dispatchToken !== payload.dispatchToken) {
1431
+ throw new Error(`Execution ${payload.executionId} already running with different dispatchToken`);
1432
+ }
1433
+ return { accepted: true, duplicate: true };
1434
+ }
1435
+ const oneshotEnv = buildOrchestratorEnv(payload);
1436
+ const child = spawnHappyCLI(["orchestrator-oneshot", "--provider", payload.provider], {
1437
+ cwd: os.homedir(),
1438
+ detached: false,
1439
+ stdio: ["ignore", "pipe", "pipe"],
1440
+ env: {
1441
+ ...stripSessionScopedHappyEnv(process.env),
1442
+ ...oneshotEnv
1443
+ }
1444
+ });
1445
+ const execution = {
1446
+ payload,
1447
+ child,
1448
+ startedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
1449
+ cancelRequested: false,
1450
+ watchdogTriggered: false,
1451
+ detectedChildSessionId: payload.childSessionId ?? null,
1452
+ startReportPromise: Promise.resolve(),
1453
+ finishReportPromise: null,
1454
+ stdout: "",
1455
+ stderr: "",
1456
+ stdoutLineBuffer: "",
1457
+ watchdogTimer: null,
1458
+ killTimer: null
1459
+ };
1460
+ execution.startReportPromise = api.reportOrchestratorExecutionStart({
1461
+ executionId: payload.executionId,
1462
+ dispatchToken: payload.dispatchToken,
1463
+ startedAt: execution.startedAtIso,
1464
+ pid: child.pid
1465
+ }).catch((error) => {
1466
+ logger.debug(`[ORCHESTRATOR] Failed to report start for execution ${payload.executionId}`, error);
1467
+ });
1468
+ executionIdToManagedExecution.set(payload.executionId, execution);
1469
+ child.stdout?.on("data", (chunk) => {
1470
+ const text = chunk.toString();
1471
+ execution.stdout = appendOutputChunk(execution.stdout, text, ORCHESTRATOR_OUTPUT_CAPTURE_LIMIT);
1472
+ if (!execution.detectedChildSessionId) {
1473
+ if (payload.provider === "codex" && payload.executionType === "initial") {
1474
+ const probe = `${execution.stdoutLineBuffer}${text}`;
1475
+ const parsed = extractCodexSessionId(probe);
1476
+ if (parsed) {
1477
+ execution.detectedChildSessionId = parsed;
1478
+ }
1479
+ execution.stdoutLineBuffer = probe.slice(-256);
1480
+ } else if (payload.provider === "gemini" && payload.executionType === "initial") {
1481
+ execution.stdoutLineBuffer += text;
1482
+ const lines = execution.stdoutLineBuffer.split(/\r?\n/);
1483
+ execution.stdoutLineBuffer = lines.pop() ?? "";
1484
+ for (const line of lines) {
1485
+ const parsed = extractGeminiSessionIdFromJsonLine(line);
1486
+ if (parsed) {
1487
+ execution.detectedChildSessionId = parsed;
1488
+ break;
1489
+ }
1490
+ }
1491
+ }
1492
+ }
1493
+ });
1494
+ child.stderr?.on("data", (chunk) => {
1495
+ const text = chunk.toString();
1496
+ execution.stderr = appendOutputChunk(execution.stderr, text, ORCHESTRATOR_OUTPUT_CAPTURE_LIMIT);
1497
+ if (!execution.detectedChildSessionId && payload.provider === "codex" && payload.executionType === "initial") {
1498
+ const probe = `${execution.stdoutLineBuffer}${text}`;
1499
+ const parsed = extractCodexSessionId(probe);
1500
+ if (parsed) {
1501
+ execution.detectedChildSessionId = parsed;
1502
+ }
1503
+ execution.stdoutLineBuffer = probe.slice(-256);
1504
+ }
1505
+ });
1506
+ child.once("error", async (error) => {
1507
+ execution.stderr = appendOutputChunk(execution.stderr, `
1508
+ ${error.message}
1509
+ `, ORCHESTRATOR_OUTPUT_CAPTURE_LIMIT);
1510
+ await execution.startReportPromise;
1511
+ await reportExecutionFinish(execution, {
1512
+ status: execution.watchdogTriggered ? "timeout" : execution.cancelRequested ? "cancelled" : "failed",
1513
+ exitCode: execution.child.exitCode,
1514
+ signal: execution.child.signalCode,
1515
+ errorCode: "SPAWN_ERROR",
1516
+ errorMessage: error.message
1517
+ });
1518
+ });
1519
+ child.once("exit", async (code, signal) => {
1520
+ await finalizeExecution(payload.executionId, code, signal);
1521
+ });
1522
+ execution.watchdogTimer = setTimeout(() => {
1523
+ execution.watchdogTimer = null;
1524
+ execution.watchdogTriggered = true;
1525
+ requestExecutionTermination(execution);
1526
+ }, payload.timeoutMs);
1527
+ return { accepted: true };
1528
+ };
1529
+ const handleOrchestratorCancel = async (payload) => {
1530
+ const execution = executionIdToManagedExecution.get(payload.executionId);
1531
+ if (!execution) {
1532
+ return { accepted: true, notFound: true };
1533
+ }
1534
+ if (execution.payload.dispatchToken !== payload.dispatchToken) {
1535
+ throw new Error(`dispatchToken mismatch for execution ${payload.executionId}`);
1536
+ }
1537
+ execution.cancelRequested = true;
1538
+ requestExecutionTermination(execution);
1539
+ return { accepted: true };
1540
+ };
1541
+ const stopAllOrchestratorExecutions = async () => {
1542
+ const executions = Array.from(executionIdToManagedExecution.values());
1543
+ if (executions.length === 0) {
1544
+ return;
1545
+ }
1546
+ logger.debug(`[ORCHESTRATOR] Stopping ${executions.length} running orchestrator execution(s)`);
1547
+ for (const execution of executions) {
1548
+ execution.cancelRequested = true;
1549
+ requestExecutionTermination(execution);
1550
+ }
1551
+ const deadline = Date.now() + ORCHESTRATOR_SHUTDOWN_WAIT_MS;
1552
+ while (executionIdToManagedExecution.size > 0 && Date.now() < deadline) {
1553
+ await new Promise((resolve) => setTimeout(resolve, 100));
1554
+ }
1555
+ };
1556
+ const onHappySessionWebhook = (sessionId, sessionMetadata) => {
1557
+ logger.debugLargeJson(`[DAEMON RUN] Session reported`, sessionMetadata);
1558
+ const pid = sessionMetadata.hostPid;
1559
+ if (!pid) {
1560
+ logger.debug(`[DAEMON RUN] Session webhook missing hostPid for sessionId: ${sessionId}`);
1561
+ return;
1562
+ }
1563
+ logger.debug(`[DAEMON RUN] Session webhook: ${sessionId}, PID: ${pid}, started by: ${sessionMetadata.startedBy || "unknown"}`);
1564
+ logger.debug(`[DAEMON RUN] Current tracked sessions before webhook: ${Array.from(pidToTrackedSession.keys()).join(", ")}`);
1565
+ const existingSession = pidToTrackedSession.get(pid);
1566
+ if (existingSession && existingSession.startedBy === "daemon") {
1567
+ existingSession.happySessionId = sessionId;
1568
+ existingSession.happySessionMetadataFromLocalWebhook = sessionMetadata;
1569
+ logger.debug(`[DAEMON RUN] Updated daemon-spawned session ${sessionId} with metadata`);
1570
+ const awaiter = pidToAwaiter.get(pid);
1571
+ if (awaiter) {
1572
+ pidToAwaiter.delete(pid);
1573
+ awaiter(existingSession);
1574
+ logger.debug(`[DAEMON RUN] Resolved session awaiter for PID ${pid}`);
1575
+ }
1576
+ } else if (!existingSession) {
1577
+ const trackedSession = {
1578
+ startedBy: "happy directly - likely by user from terminal",
1579
+ happySessionId: sessionId,
1580
+ happySessionMetadataFromLocalWebhook: sessionMetadata,
1581
+ pid
1582
+ };
1583
+ pidToTrackedSession.set(pid, trackedSession);
1584
+ logger.debug(`[DAEMON RUN] Registered externally-started session ${sessionId}`);
1585
+ }
1586
+ };
1587
+ const spawnSession = async (options) => {
1588
+ logger.debugLargeJson("[DAEMON RUN] Spawning session", options);
1589
+ const { directory, sessionId, resumeSessionId: rawResumeSessionId, sessionTitle, skipForkSession, machineId: machineId2, approvedNewDirectoryCreation = true, intent } = options;
1590
+ const isClaudeAgent = !options.agent || options.agent === "claude";
1591
+ const resumeSessionId = intent === "resume" ? rawResumeSessionId : void 0;
1592
+ if (rawResumeSessionId && intent !== "resume") {
1593
+ logger.debug('[DAEMON RUN] Ignoring resumeSessionId because intent is not "resume"', { rawResumeSessionId, intent });
1594
+ }
1595
+ let directoryCreated = false;
1596
+ try {
1597
+ await fs.access(directory);
1598
+ logger.debug(`[DAEMON RUN] Directory exists: ${directory}`);
1599
+ } catch (error) {
1600
+ logger.debug(`[DAEMON RUN] Directory doesn't exist, creating: ${directory}`);
1601
+ if (!approvedNewDirectoryCreation) {
1602
+ logger.debug(`[DAEMON RUN] Directory creation not approved for: ${directory}`);
1603
+ return {
1604
+ type: "requestToApproveDirectoryCreation",
1605
+ directory
1606
+ };
1607
+ }
1608
+ try {
1609
+ await fs.mkdir(directory, { recursive: true });
1610
+ logger.debug(`[DAEMON RUN] Successfully created directory: ${directory}`);
1611
+ directoryCreated = true;
1612
+ } catch (mkdirError) {
1613
+ let errorMessage = `Unable to create directory at '${directory}'. `;
1614
+ if (mkdirError.code === "EACCES") {
1615
+ errorMessage += `Permission denied. You don't have write access to create a folder at this location. Try using a different path or check your permissions.`;
1616
+ } else if (mkdirError.code === "ENOTDIR") {
1617
+ errorMessage += `A file already exists at this path or in the parent path. Cannot create a directory here. Please choose a different location.`;
1618
+ } else if (mkdirError.code === "ENOSPC") {
1619
+ errorMessage += `No space left on device. Your disk is full. Please free up some space and try again.`;
1620
+ } else if (mkdirError.code === "EROFS") {
1621
+ errorMessage += `The file system is read-only. Cannot create directories here. Please choose a writable location.`;
1622
+ } else {
1623
+ errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
1624
+ }
1625
+ logger.debug(`[DAEMON RUN] Directory creation failed: ${errorMessage}`);
1626
+ return {
1627
+ type: "error",
1628
+ errorMessage
1629
+ };
1630
+ }
1631
+ }
1632
+ try {
1633
+ const authEnv = {};
1634
+ if (options.token) {
1635
+ if (options.agent === "codex") {
1636
+ const codexHomeDir = tmp.dirSync();
1637
+ fs.writeFile(join(codexHomeDir.name, "auth.json"), options.token);
1638
+ authEnv.CODEX_HOME = codexHomeDir.name;
1639
+ } else {
1640
+ authEnv.CLAUDE_CODE_OAUTH_TOKEN = options.token;
1641
+ }
1642
+ }
1643
+ let profileEnv = {};
1644
+ if (options.environmentVariables && Object.keys(options.environmentVariables).length > 0) {
1645
+ profileEnv = options.environmentVariables;
1646
+ logger.info(`[DAEMON RUN] Using GUI-provided profile environment variables (${Object.keys(profileEnv).length} vars)`);
1647
+ logger.debug(`[DAEMON RUN] GUI profile env var keys: ${Object.keys(profileEnv).join(", ")}`);
1648
+ } else {
1649
+ try {
1650
+ const settings = await readSettings();
1651
+ if (settings.activeProfileId) {
1652
+ logger.debug(`[DAEMON RUN] No GUI profile provided, loading CLI local active profile: ${settings.activeProfileId}`);
1653
+ profileEnv = await getProfileEnvironmentVariablesForAgent(
1654
+ settings.activeProfileId,
1655
+ options.agent || "claude"
1656
+ );
1657
+ logger.debug(`[DAEMON RUN] Loaded ${Object.keys(profileEnv).length} environment variables from CLI local profile for agent ${options.agent || "claude"}`);
1658
+ logger.debug(`[DAEMON RUN] CLI profile env var keys: ${Object.keys(profileEnv).join(", ")}`);
1659
+ } else {
1660
+ logger.debug("[DAEMON RUN] No CLI local active profile set");
1661
+ }
1662
+ } catch (error) {
1663
+ logger.debug("[DAEMON RUN] Failed to load CLI local profile environment variables:", error);
1664
+ }
1665
+ }
1666
+ let extraEnv = { ...profileEnv, ...authEnv };
1667
+ if (resumeSessionId && isClaudeAgent) {
1668
+ extraEnv.HAPPY_CLAUDE_BACKFILL = "1";
1669
+ extraEnv.HAPPY_CLAUDE_BACKFILL_MAX_MESSAGES = "200";
1670
+ extraEnv.HAPPY_CLAUDE_BACKFILL_MAX_USER_MESSAGES = "20";
1671
+ extraEnv.HAPPY_CLAUDE_RESUME_SESSION_ID = resumeSessionId;
1672
+ if (skipForkSession) {
1673
+ extraEnv.HAPPY_CLAUDE_SKIP_FORK_SESSION = "1";
1674
+ }
1675
+ }
1676
+ if (resumeSessionId && options.agent === "gemini") {
1677
+ extraEnv.HAPPY_GEMINI_RESUME_SESSION_ID = resumeSessionId;
1678
+ extraEnv.HAPPY_GEMINI_BACKFILL = "1";
1679
+ }
1680
+ if (resumeSessionId && options.agent === "codex") {
1681
+ extraEnv.HAPPY_CODEX_RESUME_FILE = resumeSessionId;
1682
+ extraEnv.HAPPY_CODEX_BACKFILL = "1";
1683
+ }
1684
+ if (sessionTitle) {
1685
+ extraEnv.HAPPY_SESSION_TITLE = sessionTitle;
1686
+ }
1687
+ if (options.worktreeBasePath) {
1688
+ extraEnv.HAPPY_WORKTREE_BASE_PATH = options.worktreeBasePath;
1689
+ }
1690
+ if (options.worktreeBranchName) {
1691
+ extraEnv.HAPPY_WORKTREE_BRANCH_NAME = options.worktreeBranchName;
1692
+ }
1693
+ if (options.workspaceRepos && options.workspaceRepos.length > 0) {
1694
+ extraEnv.HAPPY_WORKSPACE_REPOS = JSON.stringify(options.workspaceRepos);
1695
+ }
1696
+ if (options.workspacePath) {
1697
+ extraEnv.HAPPY_WORKSPACE_PATH = options.workspacePath;
1698
+ }
1699
+ if (options.mcpServers && options.mcpServers.length > 0) {
1700
+ extraEnv.HAPPY_EXTRA_MCP_SERVERS = JSON.stringify(options.mcpServers);
1701
+ }
1702
+ logger.debug(`[DAEMON RUN] Final environment variable keys (before expansion) (${Object.keys(extraEnv).length}): ${Object.keys(extraEnv).join(", ")}`);
1703
+ extraEnv = expandEnvironmentVariables(extraEnv, process.env);
1704
+ logger.debug(`[DAEMON RUN] After variable expansion: ${Object.keys(extraEnv).join(", ")}`);
1705
+ const potentialAuthVars = ["ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "OPENAI_API_KEY", "CODEX_HOME", "AZURE_OPENAI_API_KEY", "TOGETHER_API_KEY"];
1706
+ const unexpandedAuthVars = potentialAuthVars.filter((varName) => {
1707
+ const value = extraEnv[varName];
1708
+ return value && typeof value === "string" && value.includes("${");
1709
+ });
1710
+ if (unexpandedAuthVars.length > 0) {
1711
+ const missingVarDetails = unexpandedAuthVars.map((authVar) => {
1712
+ const value = extraEnv[authVar];
1713
+ const unresolvedMatch = value?.match(/\$\{([A-Z_][A-Z0-9_]*)(:-[^}]*)?\}/);
1714
+ const missingVar = unresolvedMatch ? unresolvedMatch[1] : "unknown";
1715
+ return `${authVar} references \${${missingVar}} which is not defined`;
1716
+ });
1717
+ const errorMessage = `Authentication will fail - environment variables not found in daemon: ${missingVarDetails.join("; ")}. Ensure these variables are set in the daemon's environment (not just your shell) before starting sessions.`;
1718
+ logger.warn(`[DAEMON RUN] ${errorMessage}`);
1719
+ return {
1720
+ type: "error",
1721
+ errorMessage
1722
+ };
1723
+ }
1724
+ if (options.repoScripts && options.repoScripts.length > 0) {
1725
+ const sequentialScripts = options.repoScripts.filter((s) => s.setupScript && !s.parallelSetup);
1726
+ const parallelScripts = options.repoScripts.filter((s) => s.setupScript && s.parallelSetup);
1727
+ for (const script of sequentialScripts) {
1728
+ logger.info(`[DAEMON] Running setup script for ${script.repoDisplayName}...`);
1729
+ try {
1730
+ execSync(script.setupScript, { cwd: script.worktreePath, stdio: "pipe", timeout: 3e5 });
1731
+ logger.info(`[DAEMON] Setup script completed for ${script.repoDisplayName}`);
1732
+ } catch (err) {
1733
+ logger.warn(`[DAEMON] Setup script failed for ${script.repoDisplayName}: ${err.message}`);
1734
+ }
1735
+ }
1736
+ for (const script of parallelScripts) {
1737
+ logger.info(`[DAEMON] Running parallel setup for ${script.repoDisplayName}...`);
1738
+ const child = exec(script.setupScript, { cwd: script.worktreePath });
1739
+ child.on("exit", (code) => {
1740
+ if (code === 0) {
1741
+ logger.info(`[DAEMON] Parallel setup completed for ${script.repoDisplayName}`);
1742
+ } else {
1743
+ logger.warn(`[DAEMON] Parallel setup failed for ${script.repoDisplayName} (exit code ${code})`);
1744
+ }
1745
+ });
1746
+ }
1747
+ }
1748
+ const tmuxAvailable = await isTmuxAvailable();
1749
+ let useTmux = tmuxAvailable;
1750
+ let tmuxSessionName = extraEnv.TMUX_SESSION_NAME;
1751
+ if (!tmuxAvailable || tmuxSessionName === void 0) {
1752
+ useTmux = false;
1753
+ if (tmuxSessionName !== void 0) {
1754
+ logger.debug(`[DAEMON RUN] tmux session name specified but tmux not available, falling back to regular spawning`);
1755
+ }
1756
+ }
1757
+ if (useTmux && tmuxSessionName !== void 0) {
1758
+ const sessionDesc = tmuxSessionName || "current/most recent session";
1759
+ logger.debug(`[DAEMON RUN] Attempting to spawn session in tmux: ${sessionDesc}`);
1760
+ const tmux = getTmuxUtilities(tmuxSessionName);
1761
+ const cliPath = join(projectPath(), "dist", "index.mjs");
1762
+ const agent = options.agent === "gemini" ? "gemini" : options.agent === "codex" ? "codex" : "claude";
1763
+ const forkFlag = skipForkSession ? "" : " --fork-session";
1764
+ const resumeArgs = resumeSessionId && isClaudeAgent ? ` --resume ${resumeSessionId}${forkFlag}` : "";
1765
+ const fullCommand = `node --no-warnings --no-deprecation ${cliPath} ${agent} --happy-starting-mode remote --started-by daemon${resumeArgs}`;
1766
+ const windowName = `happy-${Date.now()}-${agent}`;
1767
+ const tmuxEnv = {};
1768
+ const sanitizedDaemonEnv = stripSessionScopedHappyEnv(process.env);
1769
+ for (const [key, value] of Object.entries(sanitizedDaemonEnv)) {
1770
+ if (value !== void 0) {
1771
+ tmuxEnv[key] = value;
1772
+ }
1773
+ }
1774
+ Object.assign(tmuxEnv, extraEnv);
1775
+ const tmuxResult = await tmux.spawnInTmux([fullCommand], {
1776
+ sessionName: tmuxSessionName,
1777
+ windowName,
1778
+ cwd: directory
1779
+ }, tmuxEnv);
1780
+ if (tmuxResult.success) {
1781
+ logger.debug(`[DAEMON RUN] Successfully spawned in tmux session: ${tmuxResult.sessionId}, PID: ${tmuxResult.pid}`);
1782
+ if (!tmuxResult.pid) {
1783
+ throw new Error("Tmux window created but no PID returned");
1784
+ }
1785
+ const trackedSession = {
1786
+ startedBy: "daemon",
1787
+ pid: tmuxResult.pid,
1788
+ // Real PID from tmux -P flag
1789
+ tmuxSessionId: tmuxResult.sessionId,
1790
+ directoryCreated,
1791
+ message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session in tmux session '${tmuxSessionName}'. Use 'tmux attach -t ${tmuxSessionName}' to view the session.` : `Spawned new session in tmux session '${tmuxSessionName}'. Use 'tmux attach -t ${tmuxSessionName}' to view the session.`,
1792
+ repoScripts: options.repoScripts
1793
+ };
1794
+ pidToTrackedSession.set(tmuxResult.pid, trackedSession);
1795
+ logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${tmuxResult.pid} (tmux)`);
1796
+ return new Promise((resolve) => {
1797
+ const timeout = setTimeout(() => {
1798
+ pidToAwaiter.delete(tmuxResult.pid);
1799
+ logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${tmuxResult.pid} (tmux)`);
1800
+ resolve({
1801
+ type: "error",
1802
+ errorMessage: `Session webhook timeout for PID ${tmuxResult.pid} (tmux)`
1803
+ });
1804
+ }, 15e3);
1805
+ pidToAwaiter.set(tmuxResult.pid, (completedSession) => {
1806
+ clearTimeout(timeout);
1807
+ logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook (tmux)`);
1808
+ resolve({
1809
+ type: "success",
1810
+ sessionId: completedSession.happySessionId
1811
+ });
1812
+ });
1813
+ });
1814
+ } else {
1815
+ logger.debug(`[DAEMON RUN] Failed to spawn in tmux: ${tmuxResult.error}, falling back to regular spawning`);
1816
+ useTmux = false;
1817
+ }
1818
+ }
1819
+ if (!useTmux) {
1820
+ logger.debug(`[DAEMON RUN] Using regular process spawning`);
1821
+ let agentCommand;
1822
+ switch (options.agent) {
1823
+ case "claude":
1824
+ case void 0:
1825
+ agentCommand = "claude";
1826
+ break;
1827
+ case "codex":
1828
+ agentCommand = "codex";
1829
+ break;
1830
+ case "gemini":
1831
+ agentCommand = "gemini";
1832
+ break;
1833
+ default:
1834
+ return {
1835
+ type: "error",
1836
+ errorMessage: `Unsupported agent type: '${options.agent}'. Please update your CLI to the latest version.`
1837
+ };
1838
+ }
1839
+ const args = [
1840
+ agentCommand,
1841
+ "--happy-starting-mode",
1842
+ "remote",
1843
+ "--started-by",
1844
+ "daemon"
1845
+ ];
1846
+ if (resumeSessionId && isClaudeAgent) {
1847
+ args.push("--resume", resumeSessionId);
1848
+ if (!skipForkSession) {
1849
+ args.push("--fork-session");
1850
+ }
1851
+ }
1852
+ const happyProcess = spawnHappyCLI(args, {
1853
+ cwd: directory,
1854
+ detached: true,
1855
+ // Sessions stay alive when daemon stops
1856
+ stdio: ["ignore", "pipe", "pipe"],
1857
+ // Capture stdout/stderr for debugging
1858
+ env: {
1859
+ // Defense-in-depth: even if startDaemon's entry sanitization is
1860
+ // bypassed somehow, the spawn site re-strips session-scoped
1861
+ // HAPPY_* keys before extraEnv adds them back deterministically.
1862
+ ...stripSessionScopedHappyEnv(process.env),
1863
+ ...extraEnv
1864
+ }
1865
+ });
1866
+ if (isDebug()) {
1867
+ happyProcess.stdout?.on("data", (data) => {
1868
+ logger.debug(`[DAEMON RUN] Child stdout: ${data.toString()}`);
1869
+ });
1870
+ happyProcess.stderr?.on("data", (data) => {
1871
+ logger.debug(`[DAEMON RUN] Child stderr: ${data.toString()}`);
1872
+ });
1873
+ }
1874
+ if (!happyProcess.pid) {
1875
+ logger.debug("[DAEMON RUN] Failed to spawn process - no PID returned");
1876
+ return {
1877
+ type: "error",
1878
+ errorMessage: "Failed to spawn Happy process - no PID returned"
1879
+ };
1880
+ }
1881
+ logger.debug(`[DAEMON RUN] Spawned process with PID ${happyProcess.pid}`);
1882
+ const trackedSession = {
1883
+ startedBy: "daemon",
1884
+ pid: happyProcess.pid,
1885
+ childProcess: happyProcess,
1886
+ directoryCreated,
1887
+ message: directoryCreated ? `The path '${directory}' did not exist. We created a new folder and spawned a new session there.` : void 0,
1888
+ repoScripts: options.repoScripts
1889
+ };
1890
+ pidToTrackedSession.set(happyProcess.pid, trackedSession);
1891
+ happyProcess.on("exit", (code, signal) => {
1892
+ logger.debug(`[DAEMON RUN] Child PID ${happyProcess.pid} exited with code ${code}, signal ${signal}`);
1893
+ if (happyProcess.pid) {
1894
+ onChildExited(happyProcess.pid);
1895
+ }
1896
+ });
1897
+ happyProcess.on("error", (error) => {
1898
+ logger.debug(`[DAEMON RUN] Child process error:`, error);
1899
+ if (happyProcess.pid) {
1900
+ onChildExited(happyProcess.pid);
1901
+ }
1902
+ });
1903
+ logger.debug(`[DAEMON RUN] Waiting for session webhook for PID ${happyProcess.pid}`);
1904
+ return new Promise((resolve) => {
1905
+ const timeout = setTimeout(() => {
1906
+ pidToAwaiter.delete(happyProcess.pid);
1907
+ logger.debug(`[DAEMON RUN] Session webhook timeout for PID ${happyProcess.pid}`);
1908
+ resolve({
1909
+ type: "error",
1910
+ errorMessage: `Session webhook timeout for PID ${happyProcess.pid}`
1911
+ });
1912
+ }, 15e3);
1913
+ pidToAwaiter.set(happyProcess.pid, (completedSession) => {
1914
+ clearTimeout(timeout);
1915
+ logger.debug(`[DAEMON RUN] Session ${completedSession.happySessionId} fully spawned with webhook`);
1916
+ resolve({
1917
+ type: "success",
1918
+ sessionId: completedSession.happySessionId
1919
+ });
1920
+ });
1921
+ });
1922
+ }
1923
+ return {
1924
+ type: "error",
1925
+ errorMessage: "Unexpected error in session spawning"
1926
+ };
1927
+ } catch (error) {
1928
+ const errorMessage = error instanceof Error ? error.message : String(error);
1929
+ logger.debug("[DAEMON RUN] Failed to spawn session:", error);
1930
+ return {
1931
+ type: "error",
1932
+ errorMessage: `Failed to spawn session: ${errorMessage}`
1933
+ };
1934
+ }
1935
+ };
1936
+ const stopSession = (sessionId) => {
1937
+ logger.debug(`[DAEMON RUN] Attempting to stop session ${sessionId}`);
1938
+ for (const [pid, session] of pidToTrackedSession.entries()) {
1939
+ if (session.happySessionId === sessionId || sessionId.startsWith("PID-") && pid === parseInt(sessionId.replace("PID-", ""))) {
1940
+ if (session.startedBy === "daemon" && session.childProcess) {
1941
+ try {
1942
+ session.childProcess.kill("SIGTERM");
1943
+ logger.debug(`[DAEMON RUN] Sent SIGTERM to daemon-spawned session ${sessionId}`);
1944
+ } catch (error) {
1945
+ logger.debug(`[DAEMON RUN] Failed to kill session ${sessionId}:`, error);
1946
+ }
1947
+ } else {
1948
+ try {
1949
+ process.kill(pid, "SIGTERM");
1950
+ logger.debug(`[DAEMON RUN] Sent SIGTERM to external session PID ${pid}`);
1951
+ } catch (error) {
1952
+ logger.debug(`[DAEMON RUN] Failed to kill external session PID ${pid}:`, error);
1953
+ }
1954
+ }
1955
+ pidToTrackedSession.delete(pid);
1956
+ logger.debug(`[DAEMON RUN] Removed session ${sessionId} from tracking`);
1957
+ return true;
1958
+ }
1959
+ }
1960
+ logger.debug(`[DAEMON RUN] Session ${sessionId} not found`);
1961
+ return false;
1962
+ };
1963
+ const runCleanupScripts = (session) => {
1964
+ if (!session.repoScripts || session.repoScripts.length === 0) return;
1965
+ for (const script of session.repoScripts) {
1966
+ if (!script.cleanupScript) continue;
1967
+ try {
1968
+ const status = execSync("git status --porcelain", {
1969
+ cwd: script.worktreePath,
1970
+ encoding: "utf-8",
1971
+ timeout: 1e4
1972
+ });
1973
+ if (status.trim()) {
1974
+ logger.info(`[DAEMON] Running cleanup script for ${script.repoDisplayName}...`);
1975
+ execSync(script.cleanupScript, {
1976
+ cwd: script.worktreePath,
1977
+ stdio: "pipe",
1978
+ timeout: 3e5
1979
+ });
1980
+ logger.info(`[DAEMON] Cleanup script completed for ${script.repoDisplayName}`);
1981
+ } else {
1982
+ logger.debug(`[DAEMON] No changes in ${script.repoDisplayName}, skipping cleanup`);
1983
+ }
1984
+ } catch (err) {
1985
+ logger.warn(`[DAEMON] Cleanup script failed for ${script.repoDisplayName}: ${err.message}`);
1986
+ }
1987
+ }
1988
+ };
1989
+ const onChildExited = (pid) => {
1990
+ logger.debug(`[DAEMON RUN] Removing exited process PID ${pid} from tracking`);
1991
+ const session = pidToTrackedSession.get(pid);
1992
+ if (session) {
1993
+ runCleanupScripts(session);
1994
+ }
1995
+ pidToTrackedSession.delete(pid);
1996
+ };
1997
+ let connectedMachine = null;
1998
+ const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
1999
+ getChildren: getCurrentChildren,
2000
+ stopSession,
2001
+ spawnSession,
2002
+ requestShutdown: () => requestShutdown("happy-cli"),
2003
+ onHappySessionWebhook,
2004
+ execOnDevice: async (deviceId, command, timeoutMs) => {
2005
+ if (!connectedMachine) {
2006
+ throw new Error("Daemon is not connected yet");
2007
+ }
2008
+ const material = await readCachedDeviceKey(deviceId);
2009
+ if (!material) {
2010
+ throw new Error(`No cached key for device ${deviceId}`);
2011
+ }
2012
+ return await deviceExec(connectedMachine.rpcSocket, credentials, deviceId, command, {
2013
+ deviceKeyBase64: encodeBase64(material.key),
2014
+ deviceKeyVariant: material.variant,
2015
+ timeout: timeoutMs,
2016
+ skipMachineLookup: true
2017
+ });
2018
+ }
2019
+ });
2020
+ const fileState = {
2021
+ pid: process.pid,
2022
+ httpPort: controlPort,
2023
+ startTime: (/* @__PURE__ */ new Date()).toLocaleString(),
2024
+ startedWithCliVersion: packageJson.version,
2025
+ daemonLogPath: logger.logFilePath
2026
+ };
2027
+ writeDaemonState(fileState);
2028
+ logger.debug("[DAEMON RUN] Daemon state written");
2029
+ const initialDaemonState = {
2030
+ status: "offline",
2031
+ pid: process.pid,
2032
+ httpPort: controlPort,
2033
+ startedAt: Date.now()
2034
+ };
2035
+ api = await ApiClient.create(credentials);
2036
+ const machineMetadata = await buildMachineMetadata();
2037
+ const machine = await api.getOrCreateMachine({
2038
+ machineId,
2039
+ metadata: machineMetadata,
2040
+ daemonState: initialDaemonState
2041
+ });
2042
+ logger.debug(`[DAEMON RUN] Machine registered: ${machine.id}`);
2043
+ const apiMachine = api.machineSyncClient(machine, initialDaemonState);
2044
+ apiMachine.setRPCHandlers({
2045
+ spawnSession,
2046
+ stopSession,
2047
+ requestShutdown: () => requestShutdown("happy-app"),
2048
+ orchestratorDispatch: handleOrchestratorDispatch,
2049
+ orchestratorCancel: handleOrchestratorCancel
2050
+ });
2051
+ apiMachine.connect();
2052
+ connectedMachine = apiMachine;
2053
+ apiMachine.updateMachineMetadata((current) => ({ ...current, ...machineMetadata })).catch((error) => {
2054
+ logger.debug("[DAEMON RUN] Failed to update machine metadata", error);
2055
+ });
2056
+ const heartbeatIntervalMsRaw = Number(process.env.HAPPY_DAEMON_HEARTBEAT_INTERVAL);
2057
+ const heartbeatIntervalMs = Number.isFinite(heartbeatIntervalMsRaw) && heartbeatIntervalMsRaw > 0 ? heartbeatIntervalMsRaw : 6e4;
2058
+ let heartbeatRunning = false;
2059
+ const restartOnStaleVersionAndHeartbeat = setInterval(async () => {
2060
+ if (heartbeatRunning) {
2061
+ return;
2062
+ }
2063
+ heartbeatRunning = true;
2064
+ if (isDebug()) {
2065
+ logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
2066
+ }
2067
+ for (const [pid, _] of pidToTrackedSession.entries()) {
2068
+ try {
2069
+ process.kill(pid, 0);
2070
+ } catch (error) {
2071
+ logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
2072
+ onChildExited(pid);
2073
+ }
2074
+ }
2075
+ let projectVersion;
2076
+ try {
2077
+ projectVersion = JSON.parse(readFileSync(join(projectPath(), "package.json"), "utf-8")).version;
2078
+ } catch (error) {
2079
+ logger.debug("[DAEMON RUN] Failed to read package.json for version check, skipping this heartbeat", error);
2080
+ heartbeatRunning = false;
2081
+ return;
2082
+ }
2083
+ if (projectVersion !== configuration.currentCliVersion) {
2084
+ logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
2085
+ clearInterval(restartOnStaleVersionAndHeartbeat);
2086
+ const supervised = process.env.HAPPY_DAEMON_SUPERVISED === "1" || process.env.XPC_SERVICE_NAME === PLIST_LABEL;
2087
+ if (supervised) {
2088
+ logger.debug("[DAEMON RUN] Supervised daemon is outdated, exiting non-zero so the service manager restarts us with the new version");
2089
+ process.exit(1);
2090
+ }
2091
+ try {
2092
+ spawnHappyCLI(["daemon", "start"], {
2093
+ detached: true,
2094
+ stdio: "ignore"
2095
+ });
2096
+ } catch (error) {
2097
+ 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);
2098
+ }
2099
+ logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
2100
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
2101
+ process.exit(0);
2102
+ }
2103
+ const daemonState = await readDaemonState();
2104
+ if (daemonState && daemonState.pid !== process.pid) {
2105
+ logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
2106
+ requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
2107
+ }
2108
+ try {
2109
+ const updatedState = {
2110
+ pid: process.pid,
2111
+ httpPort: controlPort,
2112
+ startTime: fileState.startTime,
2113
+ startedWithCliVersion: packageJson.version,
2114
+ lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
2115
+ daemonLogPath: fileState.daemonLogPath
2116
+ };
2117
+ writeDaemonState(updatedState);
2118
+ if (isDebug()) {
2119
+ logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
2120
+ }
2121
+ } catch (error) {
2122
+ logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
2123
+ }
2124
+ heartbeatRunning = false;
2125
+ }, heartbeatIntervalMs);
2126
+ const cleanupAndShutdown = async (source, errorMessage) => {
2127
+ logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
2128
+ if (restartOnStaleVersionAndHeartbeat) {
2129
+ clearInterval(restartOnStaleVersionAndHeartbeat);
2130
+ logger.debug("[DAEMON RUN] Health check interval cleared");
2131
+ }
2132
+ await apiMachine.updateDaemonState((state) => ({
2133
+ ...state,
2134
+ status: "shutting-down",
2135
+ shutdownRequestedAt: Date.now(),
2136
+ shutdownSource: source
2137
+ }));
2138
+ await new Promise((resolve) => setTimeout(resolve, 100));
2139
+ await stopAllOrchestratorExecutions();
2140
+ apiMachine.shutdown();
2141
+ await stopControlServer();
2142
+ await cleanupDaemonState();
2143
+ await stopCaffeinate();
2144
+ await releaseDaemonLock(daemonLockHandle);
2145
+ logger.debug("[DAEMON RUN] Cleanup completed, exiting process");
2146
+ process.exit(0);
2147
+ };
2148
+ logger.debug("[DAEMON RUN] Daemon started successfully, waiting for shutdown request");
2149
+ const shutdownRequest = await resolvesWhenShutdownRequested;
2150
+ await cleanupAndShutdown(shutdownRequest.source, shutdownRequest.errorMessage);
2151
+ } catch (error) {
2152
+ logger.debug("[DAEMON RUN][FATAL] Failed somewhere unexpectedly - exiting with code 1", error);
2153
+ process.exit(1);
2154
+ }
2155
+ }
2156
+
2157
+ var run = /*#__PURE__*/Object.freeze({
2158
+ __proto__: null,
2159
+ buildMachineMetadata: buildMachineMetadata,
2160
+ initialMachineMetadata: initialMachineMetadata,
2161
+ startDaemon: startDaemon
2162
+ });
2163
+
2164
+ export { startCaffeinate as a, initialMachineMetadata as i, run as r, stopCaffeinate as s };