@zonease/happy 0.6.42 → 0.6.43

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 (88) hide show
  1. package/dist/{api-CIE4_cWd.cjs → api-B-PlVQY6.cjs} +49 -3
  2. package/dist/{api-Ew6xr81i.mjs → api-BUCtd3cB.mjs} +49 -3
  3. package/dist/{auth-G9fZpVJ2.cjs → auth-BqGkUXvq.cjs} +2 -2
  4. package/dist/{auth-BzYevJo2.mjs → auth-DQH6q_q-.mjs} +4 -4
  5. package/dist/{auth-Dor79zTL.cjs → auth-OL3nNcDX.cjs} +4 -4
  6. package/dist/{auth-Q2STUNI-.mjs → auth-qM2eojWx.mjs} +2 -2
  7. package/dist/{browser-qDIIAL7S.cjs → browser-CY7VNU0C.cjs} +1 -1
  8. package/dist/{browser-BI7s-V8E.mjs → browser-D5RN_Axa.mjs} +1 -1
  9. package/dist/{claudeLocal-Db2tDC4X.mjs → claudeLocal-BXK_vQXS.mjs} +1 -1
  10. package/dist/{claudeLocal-f4uBLJaM.cjs → claudeLocal-HAXk0deK.cjs} +1 -1
  11. package/dist/{config-CHjDs8AS.mjs → config-2wMjzcHl.mjs} +1 -1
  12. package/dist/{config-CSBlEik9.cjs → config-CaKxWGVe.cjs} +1 -1
  13. package/dist/{connect-O_nA5BOC.mjs → connect-BAunPyp_.mjs} +4 -4
  14. package/dist/{connect-CJoTliii.cjs → connect-Dt1vY1BC.cjs} +4 -4
  15. package/dist/{controlClient-DoS9JqQC.mjs → controlClient-BzzTNSfl.mjs} +1 -1
  16. package/dist/{controlClient-CEz_p1zc.cjs → controlClient-yTkpwcOq.cjs} +1 -1
  17. package/dist/{cursorApprovalHook-DRAHfTXU.cjs → cursorApprovalHook-B2chnVni.cjs} +29 -11
  18. package/dist/{cursorApprovalHook-B0v1kGa7.mjs → cursorApprovalHook-BDLPzI5l.mjs} +29 -11
  19. package/dist/{cursorApprovalServer-JjPkdjGR.mjs → cursorApprovalServer-Ba1yOUCA.mjs} +109 -10
  20. package/dist/{cursorApprovalServer-DzY8yJlI.cjs → cursorApprovalServer-xwxVGL3u.cjs} +109 -8
  21. package/dist/{deviceKeys-B4aqjnE6.mjs → deviceKeys-CQyFFPtt.mjs} +2 -2
  22. package/dist/{deviceKeys-DyUn0bEx.cjs → deviceKeys-CdW-HPnz.cjs} +2 -2
  23. package/dist/{doctor-BJSuVRUZ.cjs → doctor-D4xWXiE7.cjs} +2 -2
  24. package/dist/{doctor-DRnft_5-.mjs → doctor-DDT2TXHH.mjs} +2 -2
  25. package/dist/{downloadImage-Cijmufpr.cjs → downloadImage-3A6zY5AX.cjs} +1 -372
  26. package/dist/downloadImage-BqNp0l7y.mjs +485 -0
  27. package/dist/{enroll-CysDL6ee.mjs → enroll-BlZ0giJI.mjs} +3 -3
  28. package/dist/{enroll-C2gunHdt.cjs → enroll-DeYs89Ic.cjs} +3 -3
  29. package/dist/{evidence-DsQ-h-_W.cjs → evidence-Bn1JXtdz.cjs} +1 -1
  30. package/dist/{evidence-DPutCuUr.mjs → evidence-PUnN9yPK.mjs} +1 -1
  31. package/dist/{formatImageMessage-6aV9YFWr.mjs → formatImageMessage-BRHeLf0-.mjs} +1 -1
  32. package/dist/{formatImageMessage-BPK_Ivfe.cjs → formatImageMessage-bVFGFDJl.cjs} +1 -1
  33. package/dist/index.cjs +50 -46
  34. package/dist/index.mjs +50 -46
  35. package/dist/{install-BsTA2tZn.mjs → install-BLPNuSeM.mjs} +2 -2
  36. package/dist/{install-DBg0xQ3g.mjs → install-BOIcR_Wr.mjs} +1 -1
  37. package/dist/{install-Tyhs6rUp.cjs → install-Jrs6YUu8.cjs} +1 -1
  38. package/dist/{install-DlOppA-e.cjs → install-Q-YMwwwg.cjs} +2 -2
  39. package/dist/lib.cjs +3 -3
  40. package/dist/lib.d.cts +1 -0
  41. package/dist/lib.d.mts +1 -0
  42. package/dist/lib.mjs +3 -3
  43. package/dist/{loadableToolOutput-DPGTtKJM.cjs → loadableToolOutput-B3qpMqXT.cjs} +2 -2
  44. package/dist/{loadableToolOutput-2dMPAp4N.mjs → loadableToolOutput-DzReWITb.mjs} +2 -2
  45. package/dist/{persistence-BcQmjobZ.cjs → persistence-D7zNFI-d.cjs} +1 -1
  46. package/dist/{persistence-D9tMm6bj.mjs → persistence-DbLMvLvk.mjs} +1 -1
  47. package/dist/{push-Cnm6JQMD.cjs → push-RT6Y0Hr3.cjs} +6 -6
  48. package/dist/{push-Dxsugkes.mjs → push-z5wQ4Ki_.mjs} +6 -6
  49. package/dist/registerKillSessionHandler-DmfnJ0zq.mjs +407 -0
  50. package/dist/registerKillSessionHandler-MoLdSQxM.cjs +411 -0
  51. package/dist/{run-BEflQaQ2.mjs → run-BFnSUK1o.mjs} +10 -8
  52. package/dist/{run-CiPWWhXt.cjs → run-C5StYRIB.cjs} +10 -8
  53. package/dist/{runClaude-BVXsyCHb.cjs → runClaude-ChRp6f5G.cjs} +26 -26
  54. package/dist/{runClaude-sPkSqHWY.mjs → runClaude-CjENBdrX.mjs} +21 -21
  55. package/dist/{runCodex-C4rHkIyw.mjs → runCodex-BzQWlRoj.mjs} +26 -26
  56. package/dist/{runCodex-BAwhdmSA.cjs → runCodex-DcQGyotH.cjs} +28 -28
  57. package/dist/{runCursor-DiHLWMs8.mjs → runCursor-B73z6MCN.mjs} +210 -34
  58. package/dist/{runCursor-CD5wC3gA.cjs → runCursor-BlPAxaTy.cjs} +210 -34
  59. package/dist/{runGemini-B-93gOqB.mjs → runGemini-BPv4UzuF.mjs} +17 -17
  60. package/dist/{runGemini-Cto-adir.cjs → runGemini-JFqaIC3u.cjs} +19 -19
  61. package/dist/{runOneShot-BfIzE9W4.cjs → runOneShot-2iV11e9L.cjs} +2 -2
  62. package/dist/{runOneShot-DGjlYVhR.mjs → runOneShot-DTukim2A.mjs} +2 -2
  63. package/dist/{serve-CqL6NWDr.cjs → serve-BDwuAIK4.cjs} +6 -6
  64. package/dist/{serve-ttlqF5jn.mjs → serve-BnlYj5ek.mjs} +6 -6
  65. package/dist/{session-BZhGpQUJ.cjs → session-Cp9LEESM.cjs} +5 -5
  66. package/dist/{session-Co0REU9e.mjs → session-gfeFU8CA.mjs} +5 -5
  67. package/dist/{sessionKeyCache-BwFUGgMs.cjs → sessionKeyCache-3Pme04yL.cjs} +1 -1
  68. package/dist/{sessionKeyCache-pXTjpETo.mjs → sessionKeyCache-DtBS_Sb3.mjs} +1 -1
  69. package/dist/{sessionList-DaufR-sA.mjs → sessionList-B1w4BUdv.mjs} +2 -2
  70. package/dist/{sessionList-fG5uAPzU.cjs → sessionList-BR_5UDZJ.cjs} +2 -2
  71. package/dist/{sessionSummary-CjZPt6nr.cjs → sessionSummary-DLweYgHO.cjs} +3 -3
  72. package/dist/{sessionSummary-Dv6dPABg.mjs → sessionSummary-E-a2bg8W.mjs} +3 -3
  73. package/dist/{setupOfflineReconnection-DW5il97B.mjs → setupOfflineReconnection-DHwthFtd.mjs} +5 -3
  74. package/dist/{setupOfflineReconnection-B6coH9QN.cjs → setupOfflineReconnection-Dkp00dC9.cjs} +7 -5
  75. package/dist/{spawnHappyCLI-Cfu7C1uM.cjs → spawnHappyCLI-C2uY9WV-.cjs} +1 -1
  76. package/dist/{spawnHappyCLI-DhUeycct.mjs → spawnHappyCLI-Dt7fbmbk.mjs} +1 -1
  77. package/dist/{ssh-8tPIKk-i.mjs → ssh-C-nEZiGP.mjs} +5 -5
  78. package/dist/{ssh-C4YTSHXL.cjs → ssh-CsnS89Lr.cjs} +5 -5
  79. package/dist/{task-CH-oidFy.mjs → task-B-8_QGGX.mjs} +6 -6
  80. package/dist/{task-DFW-PecP.cjs → task-lrLb4FCL.cjs} +6 -6
  81. package/dist/{uninstall-CeioRZNI.mjs → uninstall-CirMt_WC.mjs} +2 -2
  82. package/dist/{uninstall-yTSL3X6Y.cjs → uninstall-DMjUlEXT.cjs} +2 -2
  83. package/dist/{update-Derd2hoX.mjs → update-CfXbYshs.mjs} +3 -3
  84. package/dist/{update-Bju28vNP.cjs → update-mx2p28IP.cjs} +3 -3
  85. package/package.json +1 -1
  86. package/dist/downloadImage-BpVF7A79.mjs +0 -854
  87. package/dist/gitWorktree-C6jLoaMw.cjs +0 -39
  88. package/dist/gitWorktree-CzJDU2BV.mjs +0 -37
package/dist/lib.d.mts CHANGED
@@ -1073,6 +1073,7 @@ type Metadata = {
1073
1073
  }> | null;
1074
1074
  machineId?: string;
1075
1075
  claudeSessionId?: string;
1076
+ cursorSessionId?: string;
1076
1077
  tools?: string[];
1077
1078
  slashCommands?: string[];
1078
1079
  skills?: Array<{
package/dist/lib.mjs CHANGED
@@ -1,5 +1,5 @@
1
- export { A as ApiClient, a as ApiSessionClient } from './api-Ew6xr81i.mjs';
2
- export { c as configuration, l as logger } from './persistence-D9tMm6bj.mjs';
1
+ export { A as ApiClient, a as ApiSessionClient } from './api-BUCtd3cB.mjs';
2
+ export { c as configuration, l as logger } from './persistence-DbLMvLvk.mjs';
3
3
  export { R as RawJSONLinesSchema } from './types-BoJh4Qfv.mjs';
4
4
  import 'axios';
5
5
  import 'node:events';
@@ -28,6 +28,6 @@ import 'tweetnacl';
28
28
  import '@stablelib/base64';
29
29
  import 'node:readline';
30
30
  import 'expo-server-sdk';
31
- import './sessionKeyCache-pXTjpETo.mjs';
31
+ import './sessionKeyCache-DtBS_Sb3.mjs';
32
32
  import 'chalk';
33
33
  import 'fs';
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var node_crypto = require('node:crypto');
4
- var logger = require('./persistence-BcQmjobZ.cjs');
5
- var api = require('./api-CIE4_cWd.cjs');
4
+ var logger = require('./persistence-D7zNFI-d.cjs');
5
+ var api = require('./api-B-PlVQY6.cjs');
6
6
 
7
7
  class BaseReasoningProcessor {
8
8
  accumulator = "";
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { l as logger } from './persistence-D9tMm6bj.mjs';
3
- import { s as saveToolOutputRecord } from './api-Ew6xr81i.mjs';
2
+ import { l as logger } from './persistence-DbLMvLvk.mjs';
3
+ import { s as saveToolOutputRecord } from './api-BUCtd3cB.mjs';
4
4
 
5
5
  class BaseReasoningProcessor {
6
6
  accumulator = "";
@@ -35,7 +35,7 @@ function isDebug() {
35
35
  }
36
36
 
37
37
  var name = "@zonease/happy";
38
- var version = "0.6.42";
38
+ var version = "0.6.43";
39
39
  var description = "Mobile and Web client for Claude Code, Codex, and Gemini";
40
40
  var author = "ZonEaseTech";
41
41
  var license = "MIT";
@@ -14,7 +14,7 @@ function isDebug() {
14
14
  }
15
15
 
16
16
  var name = "@zonease/happy";
17
- var version = "0.6.42";
17
+ var version = "0.6.43";
18
18
  var description = "Mobile and Web client for Claude Code, Codex, and Gemini";
19
19
  var author = "ZonEaseTech";
20
20
  var license = "MIT";
@@ -2,8 +2,8 @@
2
2
 
3
3
  var promises = require('node:fs/promises');
4
4
  var socket_ioClient = require('socket.io-client');
5
- var logger = require('./persistence-BcQmjobZ.cjs');
6
- var ssh = require('./ssh-C4YTSHXL.cjs');
5
+ var logger = require('./persistence-D7zNFI-d.cjs');
6
+ var ssh = require('./ssh-CsnS89Lr.cjs');
7
7
  require('node:fs');
8
8
  require('node:os');
9
9
  require('node:path');
@@ -12,16 +12,16 @@ require('node:crypto');
12
12
  require('tweetnacl');
13
13
  require('chalk');
14
14
  require('fs');
15
- require('./deviceKeys-DyUn0bEx.cjs');
15
+ require('./deviceKeys-CdW-HPnz.cjs');
16
16
  require('axios');
17
- require('./auth-G9fZpVJ2.cjs');
17
+ require('./auth-BqGkUXvq.cjs');
18
18
  require('qrcode-terminal');
19
19
  require('./time-Bj2HRkKH.cjs');
20
- require('./browser-qDIIAL7S.cjs');
20
+ require('./browser-CY7VNU0C.cjs');
21
21
  require('open');
22
22
  require('react');
23
23
  require('ink');
24
- require('./controlClient-CEz_p1zc.cjs');
24
+ require('./controlClient-yTkpwcOq.cjs');
25
25
  require('./projectPath-ChLhdOyt.cjs');
26
26
  require('path');
27
27
  require('url');
@@ -1,7 +1,7 @@
1
1
  import { stat, readFile } from 'node:fs/promises';
2
2
  import { io } from 'socket.io-client';
3
- import { c as configuration, e as encodeBase64, f as encrypt, g as decrypt, h as decodeBase64 } from './persistence-D9tMm6bj.mjs';
4
- import { resolveDeviceForCommand } from './ssh-8tPIKk-i.mjs';
3
+ import { c as configuration, e as encodeBase64, f as encrypt, g as decrypt, h as decodeBase64 } from './persistence-DbLMvLvk.mjs';
4
+ import { resolveDeviceForCommand } from './ssh-C-nEZiGP.mjs';
5
5
  import 'node:fs';
6
6
  import 'node:os';
7
7
  import 'node:path';
@@ -10,16 +10,16 @@ import 'node:crypto';
10
10
  import 'tweetnacl';
11
11
  import 'chalk';
12
12
  import 'fs';
13
- import './deviceKeys-B4aqjnE6.mjs';
13
+ import './deviceKeys-CQyFFPtt.mjs';
14
14
  import 'axios';
15
- import './auth-Q2STUNI-.mjs';
15
+ import './auth-qM2eojWx.mjs';
16
16
  import 'qrcode-terminal';
17
17
  import './time-DgsBLJA_.mjs';
18
- import './browser-BI7s-V8E.mjs';
18
+ import './browser-D5RN_Axa.mjs';
19
19
  import 'open';
20
20
  import 'react';
21
21
  import 'ink';
22
- import './controlClient-DoS9JqQC.mjs';
22
+ import './controlClient-BzzTNSfl.mjs';
23
23
  import './projectPath-BTBIMB57.mjs';
24
24
  import 'path';
25
25
  import 'url';
@@ -0,0 +1,407 @@
1
+ import { join, resolve, dirname } from 'node:path';
2
+ import { p as projectPath } from './projectPath-BTBIMB57.mjs';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { createServer } from 'node:http';
5
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
7
+ import { z } from 'zod';
8
+ import { l as logger, r as readCredentials } from './persistence-DbLMvLvk.mjs';
9
+ import { l as listDevices, d as deviceExec } from './deviceKeys-CQyFFPtt.mjs';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { s as shouldEnableOrchestratorTools } from './prompt-BsAZsPbo.mjs';
12
+ import './mcpToolSchemas-CjeTfrAs.mjs';
13
+ import './index-DTV7drqs.mjs';
14
+ import { execSync } from 'node:child_process';
15
+ import 'axios';
16
+ import 'node:events';
17
+ import 'socket.io-client';
18
+ import './api-BUCtd3cB.mjs';
19
+ import 'tweetnacl';
20
+ import 'ws';
21
+ import 'crypto';
22
+ import '@stablelib/base64';
23
+ import 'node:os';
24
+ import 'node:fs/promises';
25
+ import 'node:fs';
26
+ import 'node:readline';
27
+ import 'expo-server-sdk';
28
+ import 'chalk';
29
+ import './types-BoJh4Qfv.mjs';
30
+
31
+ function createMcpServer(client, options) {
32
+ const mcp = new McpServer({
33
+ name: "Happy MCP",
34
+ version: "1.0.0"
35
+ });
36
+ const handler = async (title) => {
37
+ logger.debug("[happyMCP] Changing title to:", title);
38
+ if (client.isTitlePinned) {
39
+ return { success: false, error: "Title is pinned by user and cannot be changed automatically" };
40
+ }
41
+ try {
42
+ client.sendClaudeSessionMessage({
43
+ type: "summary",
44
+ summary: title,
45
+ leafUuid: randomUUID()
46
+ });
47
+ return { success: true };
48
+ } catch (error) {
49
+ return { success: false, error: String(error) };
50
+ }
51
+ };
52
+ mcp.registerTool("device_list", {
53
+ description: "List enrolled Happy devices (servers and computers) with their online state. Use before device_exec when you need a device id.",
54
+ title: "List Devices",
55
+ inputSchema: {}
56
+ }, async () => {
57
+ const credentials = await readCredentials();
58
+ if (!credentials) {
59
+ return { content: [{ type: "text", text: "No Happy credentials on this machine." }] };
60
+ }
61
+ try {
62
+ const devices = await listDevices(credentials);
63
+ const target = client.targetDevices.map((device) => device.id);
64
+ const lines = devices.map((device) => {
65
+ const marks = [device.active ? "online" : "offline"];
66
+ if (target.includes(device.id)) marks.push("session default");
67
+ return `${device.id} ${device.name}${device.platform ? ` (${device.platform})` : ""} [${marks.join(", ")}]${device.description ? ` \u2014 ${device.description}` : ""}`;
68
+ });
69
+ return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No devices enrolled yet." }] };
70
+ } catch (error) {
71
+ return { content: [{ type: "text", text: `Failed to list devices: ${error instanceof Error ? error.message : String(error)}` }] };
72
+ }
73
+ });
74
+ mcp.registerTool("device_exec", {
75
+ 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.",
76
+ title: "Run Command on Device",
77
+ inputSchema: {
78
+ command: z.string().describe("Shell command to run on the device"),
79
+ deviceId: z.string().optional().describe("Target device id. Defaults to the session's selected device."),
80
+ cwd: z.string().optional().describe("Working directory on the device"),
81
+ timeout: z.number().optional().describe("Timeout in milliseconds (default 60000)")
82
+ }
83
+ }, async (args) => {
84
+ const credentials = await readCredentials();
85
+ if (!credentials) {
86
+ return { content: [{ type: "text", text: "No Happy credentials on this machine." }] };
87
+ }
88
+ const targets = client.targetDevices;
89
+ const deviceId = args.deviceId || (targets.length === 1 ? targets[0].id : null);
90
+ if (!deviceId && targets.length > 1) {
91
+ return { content: [{ type: "text", text: `This session targets several devices \u2014 pass deviceId: ${targets.map((device) => `${device.name} (${device.id})`).join(", ")}` }] };
92
+ }
93
+ if (!deviceId) {
94
+ return { content: [{ type: "text", text: "No device selected for this session. Pick one in the app or pass deviceId (see device_list)." }] };
95
+ }
96
+ try {
97
+ const result = await deviceExec(client.rpcSocket, credentials, deviceId, args.command, {
98
+ cwd: args.cwd,
99
+ timeout: args.timeout,
100
+ deviceKeyBase64: targets.find((device) => device.id === deviceId)?.key ?? null
101
+ });
102
+ const parts = [`exit code: ${result.exitCode}`];
103
+ if (result.stdout) parts.push(`stdout:
104
+ ${result.stdout}`);
105
+ if (result.stderr) parts.push(`stderr:
106
+ ${result.stderr}`);
107
+ return { content: [{ type: "text", text: parts.join("\n\n") }] };
108
+ } catch (error) {
109
+ return { content: [{ type: "text", text: `Device command failed: ${error instanceof Error ? error.message : String(error)}` }] };
110
+ }
111
+ });
112
+ mcp.registerTool("change_title", {
113
+ description: "Change the title of the current chat session",
114
+ title: "Change Chat Title",
115
+ inputSchema: {
116
+ title: z.string().describe("The new title for the chat session")
117
+ }
118
+ }, async (args) => {
119
+ const response = await handler(args.title);
120
+ logger.debug("[happyMCP] Response:", response);
121
+ if (response.success) {
122
+ return {
123
+ content: [
124
+ {
125
+ type: "text",
126
+ text: `Successfully changed chat title to: "${args.title}"`
127
+ }
128
+ ],
129
+ isError: false
130
+ };
131
+ } else {
132
+ return {
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: `Failed to change chat title: ${response.error || "Unknown error"}`
137
+ }
138
+ ],
139
+ isError: true
140
+ };
141
+ }
142
+ });
143
+ mcp.registerTool("preview_html", {
144
+ description: "Preview an HTML page in the client app. The HTML must be a complete, self-contained document with all CSS and JS inlined.",
145
+ title: "Preview HTML",
146
+ inputSchema: {
147
+ html: z.string().describe("Complete self-contained HTML document string"),
148
+ title: z.string().optional().describe("Display title for the preview")
149
+ }
150
+ }, async (args) => {
151
+ logger.debug("[happyMCP] Preview HTML:", args.title || "Untitled");
152
+ return {
153
+ content: [{
154
+ type: "text",
155
+ text: `HTML preview ready: ${args.title || "Untitled"}`
156
+ }],
157
+ isError: false
158
+ };
159
+ });
160
+ return mcp;
161
+ }
162
+ async function startHappyServer(client) {
163
+ const transports = /* @__PURE__ */ new Map();
164
+ const enableOrchestratorTools = shouldEnableOrchestratorTools();
165
+ const toolNames = ["change_title", "preview_html", "device_list", "device_exec"];
166
+ const originalConsoleError = console.error;
167
+ console.error = (...args) => {
168
+ logger.debug("[happyMCP] console.error:", ...args);
169
+ originalConsoleError.apply(console, args);
170
+ };
171
+ const server = createServer(async (req, res) => {
172
+ const sessionId = req.headers["mcp-session-id"];
173
+ logger.debug("[happyMCP] Received request:", req.method, req.url, "sessionId:", sessionId);
174
+ try {
175
+ if (req.method === "POST") {
176
+ const body = await readRequestBody(req);
177
+ const parsedBody = JSON.parse(body);
178
+ let transport;
179
+ if (sessionId && transports.has(sessionId)) {
180
+ transport = transports.get(sessionId);
181
+ logger.debug("[happyMCP] Reusing transport for session:", sessionId);
182
+ } else if (!sessionId && isInitializeRequest(parsedBody)) {
183
+ logger.debug("[happyMCP] New initialize request, creating transport");
184
+ transport = new StreamableHTTPServerTransport({
185
+ sessionIdGenerator: () => randomUUID(),
186
+ onsessioninitialized: (newSessionId) => {
187
+ logger.debug("[happyMCP] Session initialized:", newSessionId);
188
+ transports.set(newSessionId, transport);
189
+ }
190
+ });
191
+ transport.onclose = () => {
192
+ const sid = transport.sessionId;
193
+ if (sid && transports.has(sid)) {
194
+ logger.debug("[happyMCP] Transport closed, removing session:", sid);
195
+ transports.delete(sid);
196
+ }
197
+ };
198
+ transport.onerror = (error) => {
199
+ logger.debug("[happyMCP] Transport error:", error);
200
+ };
201
+ const mcp = createMcpServer(client, { enableOrchestratorTools });
202
+ await mcp.connect(transport);
203
+ await transport.handleRequest(req, res, parsedBody);
204
+ logger.debug("[happyMCP] Initialize request handled successfully");
205
+ return;
206
+ } else {
207
+ logger.debug("[happyMCP] Bad request: no session ID and not initialize");
208
+ res.writeHead(400, { "Content-Type": "application/json" });
209
+ res.end(JSON.stringify({
210
+ jsonrpc: "2.0",
211
+ error: {
212
+ code: -32e3,
213
+ message: "Bad Request: No valid session ID provided"
214
+ },
215
+ id: null
216
+ }));
217
+ return;
218
+ }
219
+ await transport.handleRequest(req, res, parsedBody);
220
+ logger.debug("[happyMCP] Request handled successfully");
221
+ } else if (req.method === "GET" || req.method === "DELETE") {
222
+ if (!sessionId || !transports.has(sessionId)) {
223
+ logger.debug("[happyMCP] Bad request: invalid session for GET/DELETE");
224
+ res.writeHead(400, { "Content-Type": "text/plain" });
225
+ res.end("Invalid or missing session ID");
226
+ return;
227
+ }
228
+ const transport = transports.get(sessionId);
229
+ await transport.handleRequest(req, res);
230
+ logger.debug("[happyMCP] GET/DELETE request handled successfully");
231
+ } else {
232
+ res.writeHead(405, { "Content-Type": "text/plain" });
233
+ res.end("Method not allowed");
234
+ }
235
+ } catch (error) {
236
+ logger.debug("[happyMCP] Error handling request:", error);
237
+ if (!res.headersSent) {
238
+ res.writeHead(500).end();
239
+ }
240
+ }
241
+ });
242
+ const baseUrl = await new Promise((resolve) => {
243
+ server.listen(0, "127.0.0.1", () => {
244
+ const addr = server.address();
245
+ resolve(new URL(`http://127.0.0.1:${addr.port}`));
246
+ });
247
+ });
248
+ logger.debug("[happyMCP] Server started at:", baseUrl.toString());
249
+ return {
250
+ url: baseUrl.toString(),
251
+ toolNames,
252
+ stop: async () => {
253
+ logger.debug("[happyMCP] Stopping server");
254
+ for (const [sessionId, transport] of transports) {
255
+ logger.debug("[happyMCP] Closing transport for session:", sessionId);
256
+ try {
257
+ await transport.close();
258
+ } catch (error) {
259
+ logger.debug("[happyMCP] Error closing transport:", error);
260
+ }
261
+ }
262
+ transports.clear();
263
+ server.close();
264
+ }
265
+ };
266
+ }
267
+ function readRequestBody(req) {
268
+ return new Promise((resolve, reject) => {
269
+ let body = "";
270
+ req.on("data", (chunk) => body += chunk);
271
+ req.on("end", () => resolve(body));
272
+ req.on("error", reject);
273
+ });
274
+ }
275
+
276
+ const getBridgeCommand = () => join(projectPath(), "bin", "happy-mcp.mjs");
277
+ function adaptForClaude(def) {
278
+ return { type: "http", url: def.url, ...def.headers && { headers: def.headers } };
279
+ }
280
+ function adaptForStdio(def) {
281
+ const args = ["--url", def.url];
282
+ if (def.headers) {
283
+ for (const [k, v] of Object.entries(def.headers)) {
284
+ args.push("--header", `${k}: ${v}`);
285
+ }
286
+ }
287
+ return { command: getBridgeCommand(), args };
288
+ }
289
+ function adaptForHttp(def) {
290
+ return { type: "http", url: def.url, headers: def.headers || {} };
291
+ }
292
+ async function createMcpContext(session) {
293
+ const happyServer = await startHappyServer(session);
294
+ const servers = {
295
+ happy: {
296
+ url: happyServer.url,
297
+ toolNames: happyServer.toolNames,
298
+ stop: () => happyServer.stop()
299
+ }
300
+ };
301
+ const extraServersJson = process.env.HAPPY_EXTRA_MCP_SERVERS;
302
+ if (extraServersJson) {
303
+ try {
304
+ const extraServers = JSON.parse(extraServersJson);
305
+ for (const srv of extraServers) {
306
+ servers[srv.name] = {
307
+ url: srv.url,
308
+ toolNames: [],
309
+ // External MCP — tools discovered by agent, not whitelisted
310
+ headers: srv.headers,
311
+ stop: () => {
312
+ }
313
+ // No lifecycle management for external servers
314
+ };
315
+ }
316
+ } catch (e) {
317
+ console.warn("Failed to parse HAPPY_EXTRA_MCP_SERVERS:", e);
318
+ }
319
+ }
320
+ return {
321
+ configForClaude() {
322
+ const result = {};
323
+ for (const [name, def] of Object.entries(servers)) {
324
+ result[name] = adaptForClaude(def);
325
+ }
326
+ return result;
327
+ },
328
+ configForStdio() {
329
+ const result = {};
330
+ for (const [name, def] of Object.entries(servers)) {
331
+ result[name] = adaptForStdio(def);
332
+ }
333
+ return result;
334
+ },
335
+ configForHttp() {
336
+ const result = {};
337
+ for (const [name, def] of Object.entries(servers)) {
338
+ result[name] = adaptForHttp(def);
339
+ }
340
+ return result;
341
+ },
342
+ allowedToolNames() {
343
+ return Object.entries(servers).flatMap(
344
+ ([name, def]) => def.toolNames.map((tool) => `mcp__${name}__${tool}`)
345
+ );
346
+ },
347
+ normalizeToolName(rawName) {
348
+ for (const [name, def] of Object.entries(servers)) {
349
+ if (def.toolNames.includes(rawName)) {
350
+ return `mcp:${name}:${rawName}`;
351
+ }
352
+ }
353
+ return rawName;
354
+ },
355
+ stop() {
356
+ for (const def of Object.values(servers)) {
357
+ def.stop();
358
+ }
359
+ }
360
+ };
361
+ }
362
+
363
+ function detectGitWorktree(cwd) {
364
+ try {
365
+ const gitDir = execSync("git rev-parse --git-dir", {
366
+ cwd,
367
+ encoding: "utf8",
368
+ stdio: ["ignore", "pipe", "ignore"]
369
+ }).trim();
370
+ const gitCommonDir = execSync("git rev-parse --git-common-dir", {
371
+ cwd,
372
+ encoding: "utf8",
373
+ stdio: ["ignore", "pipe", "ignore"]
374
+ }).trim();
375
+ const absGitDir = resolve(cwd, gitDir);
376
+ const absCommonDir = resolve(cwd, gitCommonDir);
377
+ if (absGitDir === absCommonDir) {
378
+ return { isWorktree: false };
379
+ }
380
+ const basePath = dirname(absCommonDir);
381
+ const branchName = execSync("git rev-parse --abbrev-ref HEAD", {
382
+ cwd,
383
+ encoding: "utf8",
384
+ stdio: ["ignore", "pipe", "ignore"]
385
+ }).trim();
386
+ return {
387
+ isWorktree: true,
388
+ worktreeBasePath: basePath,
389
+ worktreeBranchName: branchName || void 0
390
+ };
391
+ } catch {
392
+ return { isWorktree: false };
393
+ }
394
+ }
395
+
396
+ function registerKillSessionHandler(rpcHandlerManager, killThisHappy) {
397
+ rpcHandlerManager.registerHandler("killSession", async () => {
398
+ logger.debug("Kill session request received");
399
+ void killThisHappy();
400
+ return {
401
+ success: true,
402
+ message: "Killing happy-cli process"
403
+ };
404
+ });
405
+ }
406
+
407
+ export { createMcpContext as c, detectGitWorktree as d, registerKillSessionHandler as r };