@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,897 @@
1
+ 'use strict';
2
+
3
+ var logger = require('./persistence-CBTtC_dm.cjs');
4
+ var crypto = require('crypto');
5
+ var path = require('node:path');
6
+ var projectPath = require('./projectPath-ChLhdOyt.cjs');
7
+ var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
8
+ var node_http = require('node:http');
9
+ var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
10
+ var types_js = require('@modelcontextprotocol/sdk/types.js');
11
+ var z = require('zod');
12
+ var deviceKeys = require('./deviceKeys-C540DcXl.cjs');
13
+ var node_crypto = require('node:crypto');
14
+ var prompt = require('./prompt-BTWDZZO0.cjs');
15
+ require('./mcpToolSchemas-DK42yoZ_.cjs');
16
+ require('./index-CDTNgfUf.cjs');
17
+ var node_child_process = require('node:child_process');
18
+ var axios = require('axios');
19
+ require('node:events');
20
+ require('socket.io-client');
21
+ require('./api-C7FyJfZ1.cjs');
22
+ require('tweetnacl');
23
+ require('ws');
24
+ require('@stablelib/base64');
25
+ require('node:os');
26
+ require('node:fs/promises');
27
+ require('node:fs');
28
+ require('node:readline');
29
+ require('expo-server-sdk');
30
+ require('chalk');
31
+ require('./types-DWeB7aqh.cjs');
32
+
33
+ function parseCompact(message) {
34
+ const trimmed = message.trim();
35
+ if (trimmed === "/compact") {
36
+ return {
37
+ isCompact: true,
38
+ originalMessage: trimmed
39
+ };
40
+ }
41
+ if (trimmed.startsWith("/compact ")) {
42
+ return {
43
+ isCompact: true,
44
+ originalMessage: trimmed
45
+ };
46
+ }
47
+ return {
48
+ isCompact: false,
49
+ originalMessage: message
50
+ };
51
+ }
52
+ function parseClear(message) {
53
+ const trimmed = message.trim();
54
+ return {
55
+ isClear: trimmed === "/clear"
56
+ };
57
+ }
58
+ function parseSpecialCommand(message) {
59
+ const compactResult = parseCompact(message);
60
+ if (compactResult.isCompact) {
61
+ return {
62
+ type: "compact",
63
+ originalMessage: compactResult.originalMessage
64
+ };
65
+ }
66
+ const clearResult = parseClear(message);
67
+ if (clearResult.isClear) {
68
+ return {
69
+ type: "clear"
70
+ };
71
+ }
72
+ return {
73
+ type: null
74
+ };
75
+ }
76
+
77
+ class MessageQueue2 {
78
+ queue = [];
79
+ // Made public for testing
80
+ waiter = null;
81
+ closed = false;
82
+ onMessageHandler = null;
83
+ modeHasher;
84
+ constructor(modeHasher, onMessageHandler = null) {
85
+ this.modeHasher = modeHasher;
86
+ this.onMessageHandler = onMessageHandler;
87
+ logger.logger.debug(`[MessageQueue2] Initialized`);
88
+ }
89
+ /**
90
+ * Set a handler that will be called when a message arrives
91
+ */
92
+ setOnMessage(handler) {
93
+ this.onMessageHandler = handler;
94
+ }
95
+ /**
96
+ * Push a message to the queue with a mode.
97
+ */
98
+ push(message, mode) {
99
+ if (this.closed) {
100
+ throw new Error("Cannot push to closed queue");
101
+ }
102
+ const modeHash = this.modeHasher(mode);
103
+ logger.logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
104
+ this.queue.push({
105
+ message,
106
+ mode,
107
+ modeHash,
108
+ isolate: false
109
+ });
110
+ if (this.onMessageHandler) {
111
+ this.onMessageHandler(message, mode);
112
+ }
113
+ if (this.waiter) {
114
+ logger.logger.debug(`[MessageQueue2] Notifying waiter`);
115
+ const waiter = this.waiter;
116
+ this.waiter = null;
117
+ waiter(true);
118
+ }
119
+ logger.logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
120
+ }
121
+ /**
122
+ * Push a message immediately without batching delay.
123
+ * Does not clear the queue or enforce isolation.
124
+ */
125
+ pushImmediate(message, mode) {
126
+ if (this.closed) {
127
+ throw new Error("Cannot push to closed queue");
128
+ }
129
+ const modeHash = this.modeHasher(mode);
130
+ logger.logger.debug(`[MessageQueue2] pushImmediate() called with mode hash: ${modeHash}`);
131
+ this.queue.push({
132
+ message,
133
+ mode,
134
+ modeHash,
135
+ isolate: false
136
+ });
137
+ if (this.onMessageHandler) {
138
+ this.onMessageHandler(message, mode);
139
+ }
140
+ if (this.waiter) {
141
+ logger.logger.debug(`[MessageQueue2] Notifying waiter for immediate message`);
142
+ const waiter = this.waiter;
143
+ this.waiter = null;
144
+ waiter(true);
145
+ }
146
+ logger.logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
147
+ }
148
+ /**
149
+ * Push a message that must be processed in complete isolation.
150
+ * Clears any pending messages and ensures this message is never batched with others.
151
+ * Used for special commands that require dedicated processing.
152
+ */
153
+ pushIsolateAndClear(message, mode) {
154
+ if (this.closed) {
155
+ throw new Error("Cannot push to closed queue");
156
+ }
157
+ const modeHash = this.modeHasher(mode);
158
+ logger.logger.debug(`[MessageQueue2] pushIsolateAndClear() called with mode hash: ${modeHash} - clearing ${this.queue.length} pending messages`);
159
+ this.queue = [];
160
+ this.queue.push({
161
+ message,
162
+ mode,
163
+ modeHash,
164
+ isolate: true
165
+ });
166
+ if (this.onMessageHandler) {
167
+ this.onMessageHandler(message, mode);
168
+ }
169
+ if (this.waiter) {
170
+ logger.logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
171
+ const waiter = this.waiter;
172
+ this.waiter = null;
173
+ waiter(true);
174
+ }
175
+ logger.logger.debug(`[MessageQueue2] pushIsolateAndClear() completed. Queue size: ${this.queue.length}`);
176
+ }
177
+ /**
178
+ * Push a message to the beginning of the queue with a mode.
179
+ */
180
+ unshift(message, mode) {
181
+ if (this.closed) {
182
+ throw new Error("Cannot unshift to closed queue");
183
+ }
184
+ const modeHash = this.modeHasher(mode);
185
+ logger.logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
186
+ this.queue.unshift({
187
+ message,
188
+ mode,
189
+ modeHash,
190
+ isolate: false
191
+ });
192
+ if (this.onMessageHandler) {
193
+ this.onMessageHandler(message, mode);
194
+ }
195
+ if (this.waiter) {
196
+ logger.logger.debug(`[MessageQueue2] Notifying waiter`);
197
+ const waiter = this.waiter;
198
+ this.waiter = null;
199
+ waiter(true);
200
+ }
201
+ logger.logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
202
+ }
203
+ /**
204
+ * Reset the queue - clears all messages and resets to empty state
205
+ */
206
+ reset() {
207
+ logger.logger.debug(`[MessageQueue2] reset() called. Clearing ${this.queue.length} messages`);
208
+ this.queue = [];
209
+ this.closed = false;
210
+ this.waiter = null;
211
+ }
212
+ /**
213
+ * Close the queue - no more messages can be pushed
214
+ */
215
+ close() {
216
+ logger.logger.debug(`[MessageQueue2] close() called`);
217
+ this.closed = true;
218
+ if (this.waiter) {
219
+ const waiter = this.waiter;
220
+ this.waiter = null;
221
+ waiter(false);
222
+ }
223
+ }
224
+ /**
225
+ * Check if the queue is closed
226
+ */
227
+ isClosed() {
228
+ return this.closed;
229
+ }
230
+ /**
231
+ * Get the current queue size
232
+ */
233
+ size() {
234
+ return this.queue.length;
235
+ }
236
+ /**
237
+ * Wait for messages and return all messages with the same mode.
238
+ * For string messages, multiple messages are joined with newlines.
239
+ * For object messages (e.g., mixed content with images), returns them one at a time.
240
+ * Returns { message: M, mode: T } or null if aborted/closed
241
+ */
242
+ async waitForMessagesAndGetAsString(abortSignal) {
243
+ if (this.queue.length > 0) {
244
+ return this.collectBatch();
245
+ }
246
+ if (this.closed || abortSignal?.aborted) {
247
+ return null;
248
+ }
249
+ const hasMessages = await this.waitForMessages(abortSignal);
250
+ if (!hasMessages) {
251
+ return null;
252
+ }
253
+ return this.collectBatch();
254
+ }
255
+ /**
256
+ * Collect a batch of messages with the same mode, respecting isolation requirements.
257
+ * For string messages, joins them with newlines.
258
+ * For object messages (like mixed content with images), returns one at a time.
259
+ */
260
+ collectBatch() {
261
+ if (this.queue.length === 0) {
262
+ return null;
263
+ }
264
+ const firstItem = this.queue[0];
265
+ let mode = firstItem.mode;
266
+ let isolate = firstItem.isolate ?? false;
267
+ const targetModeHash = firstItem.modeHash;
268
+ if (firstItem.isolate) {
269
+ const item = this.queue.shift();
270
+ logger.logger.debug(`[MessageQueue2] Collected isolated message with mode hash: ${targetModeHash}`);
271
+ return {
272
+ message: item.message,
273
+ mode,
274
+ hash: targetModeHash,
275
+ isolate
276
+ };
277
+ }
278
+ if (typeof firstItem.message !== "string") {
279
+ const item = this.queue.shift();
280
+ logger.logger.debug(`[MessageQueue2] Collected single object message with mode hash: ${targetModeHash}`);
281
+ return {
282
+ message: item.message,
283
+ mode,
284
+ hash: targetModeHash,
285
+ isolate
286
+ };
287
+ }
288
+ const sameModeMessages = [];
289
+ while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash && !this.queue[0].isolate && typeof this.queue[0].message === "string") {
290
+ const item = this.queue.shift();
291
+ sameModeMessages.push(item.message);
292
+ }
293
+ logger.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
294
+ const combinedMessage = sameModeMessages.join("\n");
295
+ return {
296
+ message: combinedMessage,
297
+ mode,
298
+ hash: targetModeHash,
299
+ isolate
300
+ };
301
+ }
302
+ /**
303
+ * Wait for messages to arrive
304
+ */
305
+ waitForMessages(abortSignal) {
306
+ return new Promise((resolve) => {
307
+ let abortHandler = null;
308
+ if (abortSignal) {
309
+ abortHandler = () => {
310
+ logger.logger.debug("[MessageQueue2] Wait aborted");
311
+ if (this.waiter === waiterFunc) {
312
+ this.waiter = null;
313
+ }
314
+ resolve(false);
315
+ };
316
+ abortSignal.addEventListener("abort", abortHandler);
317
+ }
318
+ const waiterFunc = (hasMessages) => {
319
+ if (abortHandler && abortSignal) {
320
+ abortSignal.removeEventListener("abort", abortHandler);
321
+ }
322
+ resolve(hasMessages);
323
+ };
324
+ if (this.queue.length > 0) {
325
+ if (abortHandler && abortSignal) {
326
+ abortSignal.removeEventListener("abort", abortHandler);
327
+ }
328
+ resolve(true);
329
+ return;
330
+ }
331
+ if (this.closed || abortSignal?.aborted) {
332
+ if (abortHandler && abortSignal) {
333
+ abortSignal.removeEventListener("abort", abortHandler);
334
+ }
335
+ resolve(false);
336
+ return;
337
+ }
338
+ this.waiter = waiterFunc;
339
+ logger.logger.debug("[MessageQueue2] Waiting for messages...");
340
+ });
341
+ }
342
+ }
343
+
344
+ function deterministicStringify(obj, options = {}) {
345
+ const {
346
+ undefinedBehavior = "omit",
347
+ sortArrays = false,
348
+ replacer,
349
+ includeSymbols = false
350
+ } = options;
351
+ const seen = /* @__PURE__ */ new WeakSet();
352
+ function processValue(value, key) {
353
+ if (replacer && key !== void 0) {
354
+ value = replacer(key, value);
355
+ }
356
+ if (value === null) return null;
357
+ if (value === void 0) {
358
+ switch (undefinedBehavior) {
359
+ case "omit":
360
+ return void 0;
361
+ case "null":
362
+ return null;
363
+ case "throw":
364
+ throw new Error(`Undefined value at key: ${key}`);
365
+ }
366
+ }
367
+ if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
368
+ return value;
369
+ }
370
+ if (value instanceof Date) {
371
+ return value.toISOString();
372
+ }
373
+ if (value instanceof RegExp) {
374
+ return value.toString();
375
+ }
376
+ if (typeof value === "function") {
377
+ return void 0;
378
+ }
379
+ if (typeof value === "symbol") {
380
+ return includeSymbols ? value.toString() : void 0;
381
+ }
382
+ if (typeof value === "bigint") {
383
+ return value.toString() + "n";
384
+ }
385
+ if (seen.has(value)) {
386
+ throw new Error("Circular reference detected");
387
+ }
388
+ seen.add(value);
389
+ if (Array.isArray(value)) {
390
+ const processed2 = value.map((item, index) => processValue(item, String(index))).filter((item) => item !== void 0);
391
+ if (sortArrays) {
392
+ processed2.sort((a, b) => {
393
+ const aStr = JSON.stringify(processValue(a));
394
+ const bStr = JSON.stringify(processValue(b));
395
+ return aStr.localeCompare(bStr);
396
+ });
397
+ }
398
+ seen.delete(value);
399
+ return processed2;
400
+ }
401
+ if (value.constructor === Object || value.constructor === void 0) {
402
+ const processed2 = {};
403
+ const keys = Object.keys(value).sort();
404
+ for (const k of keys) {
405
+ const processedValue = processValue(value[k], k);
406
+ if (processedValue !== void 0) {
407
+ processed2[k] = processedValue;
408
+ }
409
+ }
410
+ seen.delete(value);
411
+ return processed2;
412
+ }
413
+ try {
414
+ const plain = { ...value };
415
+ seen.delete(value);
416
+ return processValue(plain, key);
417
+ } catch {
418
+ seen.delete(value);
419
+ return String(value);
420
+ }
421
+ }
422
+ const processed = processValue(obj);
423
+ return JSON.stringify(processed);
424
+ }
425
+ function hashObject(obj, options, encoding = "hex") {
426
+ const jsonString = deterministicStringify(obj, options);
427
+ return crypto.createHash("sha256").update(jsonString).digest(encoding);
428
+ }
429
+
430
+ function createMcpServer(client, options) {
431
+ const mcp = new mcp_js.McpServer({
432
+ name: "Happy MCP",
433
+ version: "1.0.0"
434
+ });
435
+ const handler = async (title) => {
436
+ logger.logger.debug("[happyMCP] Changing title to:", title);
437
+ if (client.isTitlePinned) {
438
+ return { success: false, error: "Title is pinned by user and cannot be changed automatically" };
439
+ }
440
+ try {
441
+ client.sendClaudeSessionMessage({
442
+ type: "summary",
443
+ summary: title,
444
+ leafUuid: node_crypto.randomUUID()
445
+ });
446
+ return { success: true };
447
+ } catch (error) {
448
+ return { success: false, error: String(error) };
449
+ }
450
+ };
451
+ mcp.registerTool("device_list", {
452
+ description: "List enrolled Happy devices (servers and computers) with their online state. Use before device_exec when you need a device id.",
453
+ title: "List Devices",
454
+ inputSchema: {}
455
+ }, async () => {
456
+ const credentials = await logger.readCredentials();
457
+ if (!credentials) {
458
+ return { content: [{ type: "text", text: "No Happy credentials on this machine." }] };
459
+ }
460
+ try {
461
+ const devices = await deviceKeys.listDevices(credentials);
462
+ const target = client.targetDevices.map((device) => device.id);
463
+ const lines = devices.map((device) => {
464
+ const marks = [device.active ? "online" : "offline"];
465
+ if (target.includes(device.id)) marks.push("session default");
466
+ return `${device.id} ${device.name}${device.platform ? ` (${device.platform})` : ""} [${marks.join(", ")}]${device.description ? ` \u2014 ${device.description}` : ""}`;
467
+ });
468
+ return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No devices enrolled yet." }] };
469
+ } catch (error) {
470
+ return { content: [{ type: "text", text: `Failed to list devices: ${error instanceof Error ? error.message : String(error)}` }] };
471
+ }
472
+ });
473
+ mcp.registerTool("device_exec", {
474
+ description: "Run a shell command on an enrolled device (a server or computer registered in Happy). Omit deviceId to use the device the user selected for this session. Returns stdout, stderr and exit code.",
475
+ title: "Run Command on Device",
476
+ inputSchema: {
477
+ command: z.z.string().describe("Shell command to run on the device"),
478
+ deviceId: z.z.string().optional().describe("Target device id. Defaults to the session's selected device."),
479
+ cwd: z.z.string().optional().describe("Working directory on the device"),
480
+ timeout: z.z.number().optional().describe("Timeout in milliseconds (default 60000)")
481
+ }
482
+ }, async (args) => {
483
+ const credentials = await logger.readCredentials();
484
+ if (!credentials) {
485
+ return { content: [{ type: "text", text: "No Happy credentials on this machine." }] };
486
+ }
487
+ const targets = client.targetDevices;
488
+ const deviceId = args.deviceId || (targets.length === 1 ? targets[0].id : null);
489
+ if (!deviceId && targets.length > 1) {
490
+ return { content: [{ type: "text", text: `This session targets several devices \u2014 pass deviceId: ${targets.map((device) => `${device.name} (${device.id})`).join(", ")}` }] };
491
+ }
492
+ if (!deviceId) {
493
+ return { content: [{ type: "text", text: "No device selected for this session. Pick one in the app or pass deviceId (see device_list)." }] };
494
+ }
495
+ try {
496
+ const result = await deviceKeys.deviceExec(client.rpcSocket, credentials, deviceId, args.command, {
497
+ cwd: args.cwd,
498
+ timeout: args.timeout,
499
+ deviceKeyBase64: targets.find((device) => device.id === deviceId)?.key ?? null
500
+ });
501
+ const parts = [`exit code: ${result.exitCode}`];
502
+ if (result.stdout) parts.push(`stdout:
503
+ ${result.stdout}`);
504
+ if (result.stderr) parts.push(`stderr:
505
+ ${result.stderr}`);
506
+ return { content: [{ type: "text", text: parts.join("\n\n") }] };
507
+ } catch (error) {
508
+ return { content: [{ type: "text", text: `Device command failed: ${error instanceof Error ? error.message : String(error)}` }] };
509
+ }
510
+ });
511
+ mcp.registerTool("change_title", {
512
+ description: "Change the title of the current chat session",
513
+ title: "Change Chat Title",
514
+ inputSchema: {
515
+ title: z.z.string().describe("The new title for the chat session")
516
+ }
517
+ }, async (args) => {
518
+ const response = await handler(args.title);
519
+ logger.logger.debug("[happyMCP] Response:", response);
520
+ if (response.success) {
521
+ return {
522
+ content: [
523
+ {
524
+ type: "text",
525
+ text: `Successfully changed chat title to: "${args.title}"`
526
+ }
527
+ ],
528
+ isError: false
529
+ };
530
+ } else {
531
+ return {
532
+ content: [
533
+ {
534
+ type: "text",
535
+ text: `Failed to change chat title: ${response.error || "Unknown error"}`
536
+ }
537
+ ],
538
+ isError: true
539
+ };
540
+ }
541
+ });
542
+ mcp.registerTool("preview_html", {
543
+ description: "Preview an HTML page in the client app. The HTML must be a complete, self-contained document with all CSS and JS inlined.",
544
+ title: "Preview HTML",
545
+ inputSchema: {
546
+ html: z.z.string().describe("Complete self-contained HTML document string"),
547
+ title: z.z.string().optional().describe("Display title for the preview")
548
+ }
549
+ }, async (args) => {
550
+ logger.logger.debug("[happyMCP] Preview HTML:", args.title || "Untitled");
551
+ return {
552
+ content: [{
553
+ type: "text",
554
+ text: `HTML preview ready: ${args.title || "Untitled"}`
555
+ }],
556
+ isError: false
557
+ };
558
+ });
559
+ return mcp;
560
+ }
561
+ async function startHappyServer(client) {
562
+ const transports = /* @__PURE__ */ new Map();
563
+ const enableOrchestratorTools = prompt.shouldEnableOrchestratorTools();
564
+ const toolNames = ["change_title", "preview_html", "device_list", "device_exec"];
565
+ const originalConsoleError = console.error;
566
+ console.error = (...args) => {
567
+ logger.logger.debug("[happyMCP] console.error:", ...args);
568
+ originalConsoleError.apply(console, args);
569
+ };
570
+ const server = node_http.createServer(async (req, res) => {
571
+ const sessionId = req.headers["mcp-session-id"];
572
+ logger.logger.debug("[happyMCP] Received request:", req.method, req.url, "sessionId:", sessionId);
573
+ try {
574
+ if (req.method === "POST") {
575
+ const body = await readRequestBody(req);
576
+ const parsedBody = JSON.parse(body);
577
+ let transport;
578
+ if (sessionId && transports.has(sessionId)) {
579
+ transport = transports.get(sessionId);
580
+ logger.logger.debug("[happyMCP] Reusing transport for session:", sessionId);
581
+ } else if (!sessionId && types_js.isInitializeRequest(parsedBody)) {
582
+ logger.logger.debug("[happyMCP] New initialize request, creating transport");
583
+ transport = new streamableHttp_js.StreamableHTTPServerTransport({
584
+ sessionIdGenerator: () => node_crypto.randomUUID(),
585
+ onsessioninitialized: (newSessionId) => {
586
+ logger.logger.debug("[happyMCP] Session initialized:", newSessionId);
587
+ transports.set(newSessionId, transport);
588
+ }
589
+ });
590
+ transport.onclose = () => {
591
+ const sid = transport.sessionId;
592
+ if (sid && transports.has(sid)) {
593
+ logger.logger.debug("[happyMCP] Transport closed, removing session:", sid);
594
+ transports.delete(sid);
595
+ }
596
+ };
597
+ transport.onerror = (error) => {
598
+ logger.logger.debug("[happyMCP] Transport error:", error);
599
+ };
600
+ const mcp = createMcpServer(client, { enableOrchestratorTools });
601
+ await mcp.connect(transport);
602
+ await transport.handleRequest(req, res, parsedBody);
603
+ logger.logger.debug("[happyMCP] Initialize request handled successfully");
604
+ return;
605
+ } else {
606
+ logger.logger.debug("[happyMCP] Bad request: no session ID and not initialize");
607
+ res.writeHead(400, { "Content-Type": "application/json" });
608
+ res.end(JSON.stringify({
609
+ jsonrpc: "2.0",
610
+ error: {
611
+ code: -32e3,
612
+ message: "Bad Request: No valid session ID provided"
613
+ },
614
+ id: null
615
+ }));
616
+ return;
617
+ }
618
+ await transport.handleRequest(req, res, parsedBody);
619
+ logger.logger.debug("[happyMCP] Request handled successfully");
620
+ } else if (req.method === "GET" || req.method === "DELETE") {
621
+ if (!sessionId || !transports.has(sessionId)) {
622
+ logger.logger.debug("[happyMCP] Bad request: invalid session for GET/DELETE");
623
+ res.writeHead(400, { "Content-Type": "text/plain" });
624
+ res.end("Invalid or missing session ID");
625
+ return;
626
+ }
627
+ const transport = transports.get(sessionId);
628
+ await transport.handleRequest(req, res);
629
+ logger.logger.debug("[happyMCP] GET/DELETE request handled successfully");
630
+ } else {
631
+ res.writeHead(405, { "Content-Type": "text/plain" });
632
+ res.end("Method not allowed");
633
+ }
634
+ } catch (error) {
635
+ logger.logger.debug("[happyMCP] Error handling request:", error);
636
+ if (!res.headersSent) {
637
+ res.writeHead(500).end();
638
+ }
639
+ }
640
+ });
641
+ const baseUrl = await new Promise((resolve) => {
642
+ server.listen(0, "127.0.0.1", () => {
643
+ const addr = server.address();
644
+ resolve(new URL(`http://127.0.0.1:${addr.port}`));
645
+ });
646
+ });
647
+ logger.logger.debug("[happyMCP] Server started at:", baseUrl.toString());
648
+ return {
649
+ url: baseUrl.toString(),
650
+ toolNames,
651
+ stop: async () => {
652
+ logger.logger.debug("[happyMCP] Stopping server");
653
+ for (const [sessionId, transport] of transports) {
654
+ logger.logger.debug("[happyMCP] Closing transport for session:", sessionId);
655
+ try {
656
+ await transport.close();
657
+ } catch (error) {
658
+ logger.logger.debug("[happyMCP] Error closing transport:", error);
659
+ }
660
+ }
661
+ transports.clear();
662
+ server.close();
663
+ }
664
+ };
665
+ }
666
+ function readRequestBody(req) {
667
+ return new Promise((resolve, reject) => {
668
+ let body = "";
669
+ req.on("data", (chunk) => body += chunk);
670
+ req.on("end", () => resolve(body));
671
+ req.on("error", reject);
672
+ });
673
+ }
674
+
675
+ const getBridgeCommand = () => path.join(projectPath.projectPath(), "bin", "happy-mcp.mjs");
676
+ function adaptForClaude(def) {
677
+ return { type: "http", url: def.url, ...def.headers && { headers: def.headers } };
678
+ }
679
+ function adaptForStdio(def) {
680
+ const args = ["--url", def.url];
681
+ if (def.headers) {
682
+ for (const [k, v] of Object.entries(def.headers)) {
683
+ args.push("--header", `${k}: ${v}`);
684
+ }
685
+ }
686
+ return { command: getBridgeCommand(), args };
687
+ }
688
+ function adaptForHttp(def) {
689
+ return { type: "http", url: def.url, headers: def.headers || {} };
690
+ }
691
+ async function createMcpContext(session) {
692
+ const happyServer = await startHappyServer(session);
693
+ const servers = {
694
+ happy: {
695
+ url: happyServer.url,
696
+ toolNames: happyServer.toolNames,
697
+ stop: () => happyServer.stop()
698
+ }
699
+ };
700
+ const extraServersJson = process.env.HAPPY_EXTRA_MCP_SERVERS;
701
+ if (extraServersJson) {
702
+ try {
703
+ const extraServers = JSON.parse(extraServersJson);
704
+ for (const srv of extraServers) {
705
+ servers[srv.name] = {
706
+ url: srv.url,
707
+ toolNames: [],
708
+ // External MCP — tools discovered by agent, not whitelisted
709
+ headers: srv.headers,
710
+ stop: () => {
711
+ }
712
+ // No lifecycle management for external servers
713
+ };
714
+ }
715
+ } catch (e) {
716
+ console.warn("Failed to parse HAPPY_EXTRA_MCP_SERVERS:", e);
717
+ }
718
+ }
719
+ return {
720
+ configForClaude() {
721
+ const result = {};
722
+ for (const [name, def] of Object.entries(servers)) {
723
+ result[name] = adaptForClaude(def);
724
+ }
725
+ return result;
726
+ },
727
+ configForStdio() {
728
+ const result = {};
729
+ for (const [name, def] of Object.entries(servers)) {
730
+ result[name] = adaptForStdio(def);
731
+ }
732
+ return result;
733
+ },
734
+ configForHttp() {
735
+ const result = {};
736
+ for (const [name, def] of Object.entries(servers)) {
737
+ result[name] = adaptForHttp(def);
738
+ }
739
+ return result;
740
+ },
741
+ allowedToolNames() {
742
+ return Object.entries(servers).flatMap(
743
+ ([name, def]) => def.toolNames.map((tool) => `mcp__${name}__${tool}`)
744
+ );
745
+ },
746
+ normalizeToolName(rawName) {
747
+ for (const [name, def] of Object.entries(servers)) {
748
+ if (def.toolNames.includes(rawName)) {
749
+ return `mcp:${name}:${rawName}`;
750
+ }
751
+ }
752
+ return rawName;
753
+ },
754
+ stop() {
755
+ for (const def of Object.values(servers)) {
756
+ def.stop();
757
+ }
758
+ }
759
+ };
760
+ }
761
+
762
+ function detectGitWorktree(cwd) {
763
+ try {
764
+ const gitDir = node_child_process.execSync("git rev-parse --git-dir", {
765
+ cwd,
766
+ encoding: "utf8",
767
+ stdio: ["ignore", "pipe", "ignore"]
768
+ }).trim();
769
+ const gitCommonDir = node_child_process.execSync("git rev-parse --git-common-dir", {
770
+ cwd,
771
+ encoding: "utf8",
772
+ stdio: ["ignore", "pipe", "ignore"]
773
+ }).trim();
774
+ const absGitDir = path.resolve(cwd, gitDir);
775
+ const absCommonDir = path.resolve(cwd, gitCommonDir);
776
+ if (absGitDir === absCommonDir) {
777
+ return { isWorktree: false };
778
+ }
779
+ const basePath = path.dirname(absCommonDir);
780
+ const branchName = node_child_process.execSync("git rev-parse --abbrev-ref HEAD", {
781
+ cwd,
782
+ encoding: "utf8",
783
+ stdio: ["ignore", "pipe", "ignore"]
784
+ }).trim();
785
+ return {
786
+ isWorktree: true,
787
+ worktreeBasePath: basePath,
788
+ worktreeBranchName: branchName || void 0
789
+ };
790
+ } catch {
791
+ return { isWorktree: false };
792
+ }
793
+ }
794
+
795
+ class MessageBuffer {
796
+ messages = [];
797
+ listeners = [];
798
+ nextId = 1;
799
+ addMessage(content, type = "assistant") {
800
+ const message = {
801
+ id: `msg-${this.nextId++}`,
802
+ timestamp: /* @__PURE__ */ new Date(),
803
+ content,
804
+ type
805
+ };
806
+ this.messages.push(message);
807
+ this.notifyListeners();
808
+ }
809
+ /**
810
+ * Update the last message of a specific type by appending content to it
811
+ * Useful for streaming responses where deltas should accumulate in one message
812
+ */
813
+ updateLastMessage(contentDelta, type = "assistant") {
814
+ for (let i = this.messages.length - 1; i >= 0; i--) {
815
+ if (this.messages[i].type === type) {
816
+ const oldMessage = this.messages[i];
817
+ const updatedMessage = {
818
+ ...oldMessage,
819
+ content: oldMessage.content + contentDelta
820
+ };
821
+ this.messages[i] = updatedMessage;
822
+ this.notifyListeners();
823
+ return;
824
+ }
825
+ }
826
+ this.addMessage(contentDelta, type);
827
+ }
828
+ /**
829
+ * Remove the last message of a specific type
830
+ * Useful for removing placeholder messages like "Thinking..." when actual response starts
831
+ */
832
+ removeLastMessage(type) {
833
+ for (let i = this.messages.length - 1; i >= 0; i--) {
834
+ if (this.messages[i].type === type) {
835
+ this.messages.splice(i, 1);
836
+ this.notifyListeners();
837
+ return true;
838
+ }
839
+ }
840
+ return false;
841
+ }
842
+ getMessages() {
843
+ return [...this.messages];
844
+ }
845
+ clear() {
846
+ this.messages = [];
847
+ this.nextId = 1;
848
+ this.notifyListeners();
849
+ }
850
+ onUpdate(listener) {
851
+ this.listeners.push(listener);
852
+ return () => {
853
+ const index = this.listeners.indexOf(listener);
854
+ if (index > -1) {
855
+ this.listeners.splice(index, 1);
856
+ }
857
+ };
858
+ }
859
+ notifyListeners() {
860
+ const messages = this.getMessages();
861
+ this.listeners.forEach((listener) => listener(messages));
862
+ }
863
+ }
864
+
865
+ function registerKillSessionHandler(rpcHandlerManager, killThisHappy) {
866
+ rpcHandlerManager.registerHandler("killSession", async () => {
867
+ logger.logger.debug("Kill session request received");
868
+ void killThisHappy();
869
+ return {
870
+ success: true,
871
+ message: "Killing happy-cli process"
872
+ };
873
+ });
874
+ }
875
+
876
+ async function downloadImage(url) {
877
+ const response = await axios.get(url, {
878
+ responseType: "arraybuffer"
879
+ });
880
+ const buffer = Buffer.from(response.data);
881
+ const base64 = buffer.toString("base64");
882
+ let mimeType = response.headers["content-type"] || "image/jpeg";
883
+ if (mimeType.includes(";")) {
884
+ mimeType = mimeType.split(";")[0].trim();
885
+ }
886
+ return { base64, mimeType };
887
+ }
888
+
889
+ exports.MessageBuffer = MessageBuffer;
890
+ exports.MessageQueue2 = MessageQueue2;
891
+ exports.createMcpContext = createMcpContext;
892
+ exports.detectGitWorktree = detectGitWorktree;
893
+ exports.downloadImage = downloadImage;
894
+ exports.hashObject = hashObject;
895
+ exports.parseClear = parseClear;
896
+ exports.parseSpecialCommand = parseSpecialCommand;
897
+ exports.registerKillSessionHandler = registerKillSessionHandler;