@zonease/happy 0.6.41 → 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 (87) hide show
  1. package/dist/{api-XeHlmApX.cjs → api-B-PlVQY6.cjs} +50 -4
  2. package/dist/{api-K2-jr9ar.mjs → api-BUCtd3cB.mjs} +51 -5
  3. package/dist/{auth-BQvEuSs5.cjs → auth-BqGkUXvq.cjs} +2 -2
  4. package/dist/{auth-CjLkoM3m.mjs → auth-DQH6q_q-.mjs} +4 -4
  5. package/dist/{auth-BbvPcnkY.cjs → auth-OL3nNcDX.cjs} +4 -4
  6. package/dist/{auth-DS8fzlin.mjs → auth-qM2eojWx.mjs} +2 -2
  7. package/dist/{browser-C05SP_Tt.cjs → browser-CY7VNU0C.cjs} +1 -1
  8. package/dist/{browser-C05Ak6RL.mjs → browser-D5RN_Axa.mjs} +1 -1
  9. package/dist/{claudeLocal-DJNRlHIC.mjs → claudeLocal-BXK_vQXS.mjs} +1 -1
  10. package/dist/{claudeLocal-CpDhLCcZ.cjs → claudeLocal-HAXk0deK.cjs} +1 -1
  11. package/dist/{config-DWWtjKQx.mjs → config-2wMjzcHl.mjs} +1 -1
  12. package/dist/{config-B0mns9fF.cjs → config-CaKxWGVe.cjs} +1 -1
  13. package/dist/{connect-DXQC1CJe.mjs → connect-BAunPyp_.mjs} +5 -5
  14. package/dist/{connect-BxCScVNX.cjs → connect-Dt1vY1BC.cjs} +5 -5
  15. package/dist/{controlClient-DgjVRN8D.mjs → controlClient-BzzTNSfl.mjs} +1 -1
  16. package/dist/{controlClient-DgELDZgM.cjs → controlClient-yTkpwcOq.cjs} +1 -1
  17. package/dist/cursorApprovalHook-B2chnVni.cjs +115 -0
  18. package/dist/cursorApprovalHook-BDLPzI5l.mjs +113 -0
  19. package/dist/cursorApprovalServer-Ba1yOUCA.mjs +263 -0
  20. package/dist/cursorApprovalServer-xwxVGL3u.cjs +268 -0
  21. package/dist/{deviceKeys-CejCdWpw.mjs → deviceKeys-CQyFFPtt.mjs} +2 -2
  22. package/dist/{deviceKeys-BpIAWz13.cjs → deviceKeys-CdW-HPnz.cjs} +2 -2
  23. package/dist/{doctor-DznasgP3.cjs → doctor-D4xWXiE7.cjs} +2 -2
  24. package/dist/{doctor-B-LCMVN9.mjs → doctor-DDT2TXHH.mjs} +2 -2
  25. package/dist/downloadImage-3A6zY5AX.cjs +492 -0
  26. package/dist/downloadImage-BqNp0l7y.mjs +485 -0
  27. package/dist/{enroll-HjUqyGnD.mjs → enroll-BlZ0giJI.mjs} +3 -3
  28. package/dist/{enroll-BvfpRUGJ.cjs → enroll-DeYs89Ic.cjs} +3 -3
  29. package/dist/{evidence-CVgTjBrq.cjs → evidence-Bn1JXtdz.cjs} +1 -1
  30. package/dist/{evidence-BEQd1dCh.mjs → evidence-PUnN9yPK.mjs} +1 -1
  31. package/dist/{formatImageMessage-DVs57t3x.mjs → formatImageMessage-BRHeLf0-.mjs} +1 -1
  32. package/dist/{formatImageMessage-CmJTP_hU.cjs → formatImageMessage-bVFGFDJl.cjs} +1 -1
  33. package/dist/{index-CDTNgfUf.cjs → index-BeKrqPYR.cjs} +25 -1
  34. package/dist/{index-CPKdvi7J.mjs → index-DTV7drqs.mjs} +25 -1
  35. package/dist/index.cjs +95 -41
  36. package/dist/index.mjs +95 -41
  37. package/dist/{install-UTz_vYvB.mjs → install-BLPNuSeM.mjs} +2 -2
  38. package/dist/{install-B3mMGISS.mjs → install-BOIcR_Wr.mjs} +1 -1
  39. package/dist/{install-Ck22tYHP.cjs → install-Jrs6YUu8.cjs} +1 -1
  40. package/dist/{install-2Yl_L582.cjs → install-Q-YMwwwg.cjs} +2 -2
  41. package/dist/lib.cjs +4 -4
  42. package/dist/lib.d.cts +4 -2
  43. package/dist/lib.d.mts +4 -2
  44. package/dist/lib.mjs +4 -4
  45. package/dist/{loadableToolOutput-BZYlaqHn.cjs → loadableToolOutput-B3qpMqXT.cjs} +2 -458
  46. package/dist/{loadableToolOutput-4EvraaNY.mjs → loadableToolOutput-DzReWITb.mjs} +3 -456
  47. package/dist/{persistence-DrfN7TzY.cjs → persistence-D7zNFI-d.cjs} +7 -4
  48. package/dist/{persistence-CIUFyOwz.mjs → persistence-DbLMvLvk.mjs} +7 -4
  49. package/dist/{push-CCOXCcM_.cjs → push-RT6Y0Hr3.cjs} +6 -6
  50. package/dist/{push-USXj_pv0.mjs → push-z5wQ4Ki_.mjs} +6 -6
  51. package/dist/{downloadImage-ia5SzlCV.mjs → registerKillSessionHandler-DmfnJ0zq.mjs} +7 -487
  52. package/dist/{downloadImage-B_ygLAXY.cjs → registerKillSessionHandler-MoLdSQxM.cjs} +6 -492
  53. package/dist/{run-Dy6KX6m-.mjs → run-BFnSUK1o.mjs} +14 -9
  54. package/dist/{run-D3CopmEb.cjs → run-C5StYRIB.cjs} +14 -9
  55. package/dist/{runClaude-aHzvx6Ha.cjs → runClaude-ChRp6f5G.cjs} +27 -26
  56. package/dist/{runClaude-CWux2RDq.mjs → runClaude-CjENBdrX.mjs} +22 -21
  57. package/dist/{runCodex-D0ZxSj3O.mjs → runCodex-BzQWlRoj.mjs} +27 -25
  58. package/dist/{runCodex-G-Aq531U.cjs → runCodex-DcQGyotH.cjs} +32 -30
  59. package/dist/runCursor-B73z6MCN.mjs +752 -0
  60. package/dist/runCursor-BlPAxaTy.cjs +754 -0
  61. package/dist/{runGemini-DZC2i6vA.mjs → runGemini-BPv4UzuF.mjs} +18 -16
  62. package/dist/{runGemini-DxGgDAIM.cjs → runGemini-JFqaIC3u.cjs} +23 -21
  63. package/dist/{runOneShot-BgT-JNMA.cjs → runOneShot-2iV11e9L.cjs} +3 -3
  64. package/dist/{runOneShot-cxlk7VKA.mjs → runOneShot-DTukim2A.mjs} +3 -3
  65. package/dist/{serve-Dr3wPrtK.cjs → serve-BDwuAIK4.cjs} +7 -7
  66. package/dist/{serve-CKyvJNs0.mjs → serve-BnlYj5ek.mjs} +7 -7
  67. package/dist/{session-dYzbOo_W.cjs → session-Cp9LEESM.cjs} +5 -5
  68. package/dist/{session-vf2ZjFto.mjs → session-gfeFU8CA.mjs} +5 -5
  69. package/dist/{sessionKeyCache-dDquDzn0.cjs → sessionKeyCache-3Pme04yL.cjs} +1 -1
  70. package/dist/{sessionKeyCache-D2u3JUys.mjs → sessionKeyCache-DtBS_Sb3.mjs} +1 -1
  71. package/dist/{sessionList-C1k2PNg_.mjs → sessionList-B1w4BUdv.mjs} +2 -2
  72. package/dist/{sessionList-DmmtC7uR.cjs → sessionList-BR_5UDZJ.cjs} +2 -2
  73. package/dist/{sessionSummary-DGLoVOxd.cjs → sessionSummary-DLweYgHO.cjs} +3 -3
  74. package/dist/{sessionSummary-5a1QtFVS.mjs → sessionSummary-E-a2bg8W.mjs} +3 -3
  75. package/dist/setupOfflineReconnection-DHwthFtd.mjs +459 -0
  76. package/dist/setupOfflineReconnection-Dkp00dC9.cjs +463 -0
  77. package/dist/{spawnHappyCLI-CarRI4GM.cjs → spawnHappyCLI-C2uY9WV-.cjs} +1 -1
  78. package/dist/{spawnHappyCLI-D3EkTFIn.mjs → spawnHappyCLI-Dt7fbmbk.mjs} +1 -1
  79. package/dist/{ssh-B0hvEYIl.mjs → ssh-C-nEZiGP.mjs} +5 -5
  80. package/dist/{ssh-CBgVwKsB.cjs → ssh-CsnS89Lr.cjs} +5 -5
  81. package/dist/{task-CRo6W-3_.mjs → task-B-8_QGGX.mjs} +6 -6
  82. package/dist/{task-CMunPKCV.cjs → task-lrLb4FCL.cjs} +6 -6
  83. package/dist/{uninstall-BALE2up0.mjs → uninstall-CirMt_WC.mjs} +2 -2
  84. package/dist/{uninstall-BJ3eEGM4.cjs → uninstall-DMjUlEXT.cjs} +2 -2
  85. package/dist/{update-BfjtLrhO.mjs → update-CfXbYshs.mjs} +3 -3
  86. package/dist/{update-Bb3iijy8.cjs → update-mx2p28IP.cjs} +3 -3
  87. package/package.json +1 -1
@@ -0,0 +1,263 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, writeFile, rename, readFile, rm, readdir, rmdir } from 'node:fs/promises';
4
+ import { existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { l as logger } from './persistence-DbLMvLvk.mjs';
8
+
9
+ const TOOL_NAME_MAP = {
10
+ read: "Read",
11
+ write: "Write",
12
+ edit: "Edit",
13
+ grep: "Grep",
14
+ glob: "Glob",
15
+ ls: "LS",
16
+ list: "LS",
17
+ // Cursor calls it Shell; the app's card for running commands is Bash.
18
+ shell: "Bash",
19
+ terminal: "Bash",
20
+ task: "Task",
21
+ todowrite: "TodoWrite",
22
+ websearch: "WebSearch",
23
+ webfetch: "WebFetch"
24
+ };
25
+ function normalizeCursorToolName(rawName) {
26
+ const mapped = TOOL_NAME_MAP[rawName.toLowerCase()];
27
+ if (mapped) return mapped;
28
+ return rawName.charAt(0).toUpperCase() + rawName.slice(1);
29
+ }
30
+ function normalizeCursorToolInput(input) {
31
+ const out = { ...input };
32
+ if (typeof out.path === "string" && out.file_path === void 0) {
33
+ out.file_path = out.path;
34
+ }
35
+ if (typeof out.file_path === "string" && out.path === void 0) {
36
+ out.path = out.file_path;
37
+ }
38
+ if (typeof out.workingDirectory === "string" && out.cwd === void 0) {
39
+ out.cwd = out.workingDirectory;
40
+ }
41
+ if (typeof out.streamContent === "string" && out.content === void 0) {
42
+ out.content = out.streamContent;
43
+ }
44
+ return out;
45
+ }
46
+ function normalizeCursorTool(rawName, rawInput) {
47
+ const input = rawInput && typeof rawInput === "object" ? rawInput : {};
48
+ return {
49
+ name: normalizeCursorToolName(rawName),
50
+ input: normalizeCursorToolInput(input)
51
+ };
52
+ }
53
+
54
+ function approvalRegistryDir() {
55
+ return join(homedir(), ".happy", "cursor-approvals");
56
+ }
57
+ const APPROVAL_ENDPOINT_FILE = ".happy-approval.json";
58
+ const HOOKS_FILE = "hooks.json";
59
+ const HOOKS_BACKUP_FILE = "hooks.json.happy-backup";
60
+ const MCP_FILE = "mcp.json";
61
+ const MCP_BACKUP_FILE = "mcp.json.happy-backup";
62
+ const HOOK_TIMEOUT_SECONDS = 900;
63
+ class CursorApprovalBridge {
64
+ cwd;
65
+ handler;
66
+ token = randomUUID();
67
+ server = null;
68
+ port = 0;
69
+ hadExistingHooks = false;
70
+ hadExistingMcp = false;
71
+ mcpBridge;
72
+ /** True when .cursor/ did not exist before this session created it. */
73
+ createdCursorDir = false;
74
+ started = false;
75
+ /** Chat ids this session has registered, so they can be cleaned up. */
76
+ registeredChatIds = /* @__PURE__ */ new Set();
77
+ constructor(options) {
78
+ this.cwd = options.cwd;
79
+ this.handler = options.handler;
80
+ this.mcpBridge = options.mcpBridge ?? null;
81
+ }
82
+ get cursorDir() {
83
+ return join(this.cwd, ".cursor");
84
+ }
85
+ async start() {
86
+ await this.listen();
87
+ await this.installHooks();
88
+ this.started = true;
89
+ logger.debug(`[cursor] approval bridge listening on 127.0.0.1:${this.port}`);
90
+ }
91
+ /**
92
+ * Claims a chat id for this session. Called as soon as cursor-agent reveals
93
+ * it, which is what lets the hook route an approval back to the session
94
+ * that actually made the call.
95
+ */
96
+ async registerChat(chatId) {
97
+ if (!this.started || this.registeredChatIds.has(chatId)) return;
98
+ const dir = approvalRegistryDir();
99
+ await mkdir(dir, { recursive: true });
100
+ await writeFile(join(dir, `${chatId}.json`), JSON.stringify({
101
+ port: this.port,
102
+ token: this.token,
103
+ pid: process.pid
104
+ }), "utf8");
105
+ this.registeredChatIds.add(chatId);
106
+ logger.debug(`[cursor] chat ${chatId} routed to this session's bridge`);
107
+ }
108
+ listen() {
109
+ return new Promise((resolve, reject) => {
110
+ const server = createServer((req, res) => {
111
+ if (req.method !== "POST" || !req.url?.startsWith("/approve")) {
112
+ res.writeHead(404).end();
113
+ return;
114
+ }
115
+ let body = "";
116
+ req.on("data", (chunk) => {
117
+ body += chunk;
118
+ });
119
+ req.on("end", () => {
120
+ void this.decide(body).then((approved) => {
121
+ res.writeHead(200, { "content-type": "application/json" });
122
+ res.end(JSON.stringify({ approved }));
123
+ }).catch((error) => {
124
+ logger.debug(`[cursor] approval failed: ${error}`);
125
+ res.writeHead(200, { "content-type": "application/json" });
126
+ res.end(JSON.stringify({ approved: false }));
127
+ });
128
+ });
129
+ });
130
+ server.listen(0, "127.0.0.1", () => {
131
+ const address = server.address();
132
+ if (address === null || typeof address === "string") {
133
+ reject(new Error("Failed to determine approval server port"));
134
+ return;
135
+ }
136
+ this.port = address.port;
137
+ this.server = server;
138
+ resolve();
139
+ });
140
+ server.on("error", reject);
141
+ });
142
+ }
143
+ async decide(rawBody) {
144
+ let parsed;
145
+ try {
146
+ parsed = JSON.parse(rawBody);
147
+ } catch {
148
+ return false;
149
+ }
150
+ if (parsed.token !== this.token) {
151
+ logger.debug("[cursor] rejecting approval request with a bad token");
152
+ return false;
153
+ }
154
+ const { name: toolName, input: toolInput } = normalizeCursorTool(
155
+ typeof parsed.tool_name === "string" && parsed.tool_name.length > 0 ? parsed.tool_name : "unknown",
156
+ parsed.tool_input
157
+ );
158
+ const toolCallId = typeof parsed.tool_use_id === "string" && parsed.tool_use_id.length > 0 ? parsed.tool_use_id : `cursor-${randomUUID()}`;
159
+ return this.handler.requestApproval(toolCallId, toolName, toolInput);
160
+ }
161
+ /**
162
+ * Writes .cursor/hooks.json pointing at this CLI. Any file already there is
163
+ * moved aside and restored on stop, so a project with its own hooks is not
164
+ * quietly clobbered.
165
+ */
166
+ async installHooks() {
167
+ this.createdCursorDir = !existsSync(this.cursorDir);
168
+ await mkdir(this.cursorDir, { recursive: true });
169
+ const hooksPath = join(this.cursorDir, HOOKS_FILE);
170
+ if (existsSync(hooksPath)) {
171
+ await rename(hooksPath, join(this.cursorDir, HOOKS_BACKUP_FILE));
172
+ this.hadExistingHooks = true;
173
+ logger.debug("[cursor] existing hooks.json moved aside for the session");
174
+ }
175
+ const hookCommand = `${process.execPath} ${process.argv[1]} cursor-approval-hook`;
176
+ await writeFile(hooksPath, JSON.stringify({
177
+ version: 1,
178
+ hooks: {
179
+ // preToolUse covers every tool, including Read, Write and Shell.
180
+ // It only understands allow/deny — never "ask" — and failClosed
181
+ // has to be explicit because hooks fail open by default.
182
+ preToolUse: [
183
+ { type: "command", command: hookCommand, timeout: HOOK_TIMEOUT_SECONDS, failClosed: true }
184
+ ]
185
+ }
186
+ }, null, 2), "utf8");
187
+ await this.installMcpConfig();
188
+ await this.excludeFromGit();
189
+ }
190
+ /** Points Cursor at Happy's tools (change_title, preview_html, devices…). */
191
+ async installMcpConfig() {
192
+ if (!this.mcpBridge) return;
193
+ const mcpPath = join(this.cursorDir, MCP_FILE);
194
+ if (existsSync(mcpPath)) {
195
+ await rename(mcpPath, join(this.cursorDir, MCP_BACKUP_FILE));
196
+ this.hadExistingMcp = true;
197
+ logger.debug("[cursor] existing mcp.json moved aside for the session");
198
+ }
199
+ await writeFile(mcpPath, JSON.stringify({
200
+ mcpServers: {
201
+ happy: { command: this.mcpBridge.command, args: this.mcpBridge.args }
202
+ }
203
+ }, null, 2), "utf8");
204
+ }
205
+ /** Keep the session's scaffolding out of `git status`. */
206
+ async excludeFromGit() {
207
+ const excludePath = join(this.cwd, ".git", "info", "exclude");
208
+ if (!existsSync(excludePath)) return;
209
+ try {
210
+ const current = await readFile(excludePath, "utf8");
211
+ if (current.includes(".cursor/.happy-approval.json")) return;
212
+ await writeFile(
213
+ excludePath,
214
+ `${current}${current.endsWith("\n") ? "" : "\n"}# added by happy cursor session
215
+ .cursor/hooks.json
216
+ .cursor/mcp.json
217
+ .cursor/.happy-approval.json
218
+ `,
219
+ "utf8"
220
+ );
221
+ } catch (error) {
222
+ logger.debug(`[cursor] could not update .git/info/exclude: ${error}`);
223
+ }
224
+ }
225
+ async stop() {
226
+ if (!this.started) return;
227
+ this.started = false;
228
+ try {
229
+ for (const chatId of this.registeredChatIds) {
230
+ await rm(join(approvalRegistryDir(), `${chatId}.json`), { force: true });
231
+ }
232
+ this.registeredChatIds.clear();
233
+ await rm(join(this.cursorDir, APPROVAL_ENDPOINT_FILE), { force: true });
234
+ const hooksPath = join(this.cursorDir, HOOKS_FILE);
235
+ const backupPath = join(this.cursorDir, HOOKS_BACKUP_FILE);
236
+ if (this.hadExistingHooks && existsSync(backupPath)) {
237
+ await rename(backupPath, hooksPath);
238
+ } else {
239
+ await rm(hooksPath, { force: true });
240
+ }
241
+ const mcpPath = join(this.cursorDir, MCP_FILE);
242
+ const mcpBackup = join(this.cursorDir, MCP_BACKUP_FILE);
243
+ if (this.hadExistingMcp && existsSync(mcpBackup)) {
244
+ await rename(mcpBackup, mcpPath);
245
+ } else if (this.mcpBridge) {
246
+ await rm(mcpPath, { force: true });
247
+ }
248
+ if (this.createdCursorDir && (await readdir(this.cursorDir)).length === 0) {
249
+ await rmdir(this.cursorDir);
250
+ }
251
+ } catch (error) {
252
+ logger.debug(`[cursor] cleanup of .cursor/ failed: ${error}`);
253
+ }
254
+ await new Promise((resolve) => {
255
+ if (!this.server) return resolve();
256
+ this.server.close(() => resolve());
257
+ this.server = null;
258
+ });
259
+ logger.debug("[cursor] approval bridge stopped");
260
+ }
261
+ }
262
+
263
+ export { APPROVAL_ENDPOINT_FILE as A, CursorApprovalBridge as C, approvalRegistryDir as a, normalizeCursorTool as n };
@@ -0,0 +1,268 @@
1
+ 'use strict';
2
+
3
+ var node_http = require('node:http');
4
+ var node_crypto = require('node:crypto');
5
+ var promises = require('node:fs/promises');
6
+ var fs = require('node:fs');
7
+ var path = require('node:path');
8
+ var os = require('node:os');
9
+ var logger = require('./persistence-D7zNFI-d.cjs');
10
+
11
+ const TOOL_NAME_MAP = {
12
+ read: "Read",
13
+ write: "Write",
14
+ edit: "Edit",
15
+ grep: "Grep",
16
+ glob: "Glob",
17
+ ls: "LS",
18
+ list: "LS",
19
+ // Cursor calls it Shell; the app's card for running commands is Bash.
20
+ shell: "Bash",
21
+ terminal: "Bash",
22
+ task: "Task",
23
+ todowrite: "TodoWrite",
24
+ websearch: "WebSearch",
25
+ webfetch: "WebFetch"
26
+ };
27
+ function normalizeCursorToolName(rawName) {
28
+ const mapped = TOOL_NAME_MAP[rawName.toLowerCase()];
29
+ if (mapped) return mapped;
30
+ return rawName.charAt(0).toUpperCase() + rawName.slice(1);
31
+ }
32
+ function normalizeCursorToolInput(input) {
33
+ const out = { ...input };
34
+ if (typeof out.path === "string" && out.file_path === void 0) {
35
+ out.file_path = out.path;
36
+ }
37
+ if (typeof out.file_path === "string" && out.path === void 0) {
38
+ out.path = out.file_path;
39
+ }
40
+ if (typeof out.workingDirectory === "string" && out.cwd === void 0) {
41
+ out.cwd = out.workingDirectory;
42
+ }
43
+ if (typeof out.streamContent === "string" && out.content === void 0) {
44
+ out.content = out.streamContent;
45
+ }
46
+ return out;
47
+ }
48
+ function normalizeCursorTool(rawName, rawInput) {
49
+ const input = rawInput && typeof rawInput === "object" ? rawInput : {};
50
+ return {
51
+ name: normalizeCursorToolName(rawName),
52
+ input: normalizeCursorToolInput(input)
53
+ };
54
+ }
55
+
56
+ function approvalRegistryDir() {
57
+ return path.join(os.homedir(), ".happy", "cursor-approvals");
58
+ }
59
+ const APPROVAL_ENDPOINT_FILE = ".happy-approval.json";
60
+ const HOOKS_FILE = "hooks.json";
61
+ const HOOKS_BACKUP_FILE = "hooks.json.happy-backup";
62
+ const MCP_FILE = "mcp.json";
63
+ const MCP_BACKUP_FILE = "mcp.json.happy-backup";
64
+ const HOOK_TIMEOUT_SECONDS = 900;
65
+ class CursorApprovalBridge {
66
+ cwd;
67
+ handler;
68
+ token = node_crypto.randomUUID();
69
+ server = null;
70
+ port = 0;
71
+ hadExistingHooks = false;
72
+ hadExistingMcp = false;
73
+ mcpBridge;
74
+ /** True when .cursor/ did not exist before this session created it. */
75
+ createdCursorDir = false;
76
+ started = false;
77
+ /** Chat ids this session has registered, so they can be cleaned up. */
78
+ registeredChatIds = /* @__PURE__ */ new Set();
79
+ constructor(options) {
80
+ this.cwd = options.cwd;
81
+ this.handler = options.handler;
82
+ this.mcpBridge = options.mcpBridge ?? null;
83
+ }
84
+ get cursorDir() {
85
+ return path.join(this.cwd, ".cursor");
86
+ }
87
+ async start() {
88
+ await this.listen();
89
+ await this.installHooks();
90
+ this.started = true;
91
+ logger.logger.debug(`[cursor] approval bridge listening on 127.0.0.1:${this.port}`);
92
+ }
93
+ /**
94
+ * Claims a chat id for this session. Called as soon as cursor-agent reveals
95
+ * it, which is what lets the hook route an approval back to the session
96
+ * that actually made the call.
97
+ */
98
+ async registerChat(chatId) {
99
+ if (!this.started || this.registeredChatIds.has(chatId)) return;
100
+ const dir = approvalRegistryDir();
101
+ await promises.mkdir(dir, { recursive: true });
102
+ await promises.writeFile(path.join(dir, `${chatId}.json`), JSON.stringify({
103
+ port: this.port,
104
+ token: this.token,
105
+ pid: process.pid
106
+ }), "utf8");
107
+ this.registeredChatIds.add(chatId);
108
+ logger.logger.debug(`[cursor] chat ${chatId} routed to this session's bridge`);
109
+ }
110
+ listen() {
111
+ return new Promise((resolve, reject) => {
112
+ const server = node_http.createServer((req, res) => {
113
+ if (req.method !== "POST" || !req.url?.startsWith("/approve")) {
114
+ res.writeHead(404).end();
115
+ return;
116
+ }
117
+ let body = "";
118
+ req.on("data", (chunk) => {
119
+ body += chunk;
120
+ });
121
+ req.on("end", () => {
122
+ void this.decide(body).then((approved) => {
123
+ res.writeHead(200, { "content-type": "application/json" });
124
+ res.end(JSON.stringify({ approved }));
125
+ }).catch((error) => {
126
+ logger.logger.debug(`[cursor] approval failed: ${error}`);
127
+ res.writeHead(200, { "content-type": "application/json" });
128
+ res.end(JSON.stringify({ approved: false }));
129
+ });
130
+ });
131
+ });
132
+ server.listen(0, "127.0.0.1", () => {
133
+ const address = server.address();
134
+ if (address === null || typeof address === "string") {
135
+ reject(new Error("Failed to determine approval server port"));
136
+ return;
137
+ }
138
+ this.port = address.port;
139
+ this.server = server;
140
+ resolve();
141
+ });
142
+ server.on("error", reject);
143
+ });
144
+ }
145
+ async decide(rawBody) {
146
+ let parsed;
147
+ try {
148
+ parsed = JSON.parse(rawBody);
149
+ } catch {
150
+ return false;
151
+ }
152
+ if (parsed.token !== this.token) {
153
+ logger.logger.debug("[cursor] rejecting approval request with a bad token");
154
+ return false;
155
+ }
156
+ const { name: toolName, input: toolInput } = normalizeCursorTool(
157
+ typeof parsed.tool_name === "string" && parsed.tool_name.length > 0 ? parsed.tool_name : "unknown",
158
+ parsed.tool_input
159
+ );
160
+ const toolCallId = typeof parsed.tool_use_id === "string" && parsed.tool_use_id.length > 0 ? parsed.tool_use_id : `cursor-${node_crypto.randomUUID()}`;
161
+ return this.handler.requestApproval(toolCallId, toolName, toolInput);
162
+ }
163
+ /**
164
+ * Writes .cursor/hooks.json pointing at this CLI. Any file already there is
165
+ * moved aside and restored on stop, so a project with its own hooks is not
166
+ * quietly clobbered.
167
+ */
168
+ async installHooks() {
169
+ this.createdCursorDir = !fs.existsSync(this.cursorDir);
170
+ await promises.mkdir(this.cursorDir, { recursive: true });
171
+ const hooksPath = path.join(this.cursorDir, HOOKS_FILE);
172
+ if (fs.existsSync(hooksPath)) {
173
+ await promises.rename(hooksPath, path.join(this.cursorDir, HOOKS_BACKUP_FILE));
174
+ this.hadExistingHooks = true;
175
+ logger.logger.debug("[cursor] existing hooks.json moved aside for the session");
176
+ }
177
+ const hookCommand = `${process.execPath} ${process.argv[1]} cursor-approval-hook`;
178
+ await promises.writeFile(hooksPath, JSON.stringify({
179
+ version: 1,
180
+ hooks: {
181
+ // preToolUse covers every tool, including Read, Write and Shell.
182
+ // It only understands allow/deny — never "ask" — and failClosed
183
+ // has to be explicit because hooks fail open by default.
184
+ preToolUse: [
185
+ { type: "command", command: hookCommand, timeout: HOOK_TIMEOUT_SECONDS, failClosed: true }
186
+ ]
187
+ }
188
+ }, null, 2), "utf8");
189
+ await this.installMcpConfig();
190
+ await this.excludeFromGit();
191
+ }
192
+ /** Points Cursor at Happy's tools (change_title, preview_html, devices…). */
193
+ async installMcpConfig() {
194
+ if (!this.mcpBridge) return;
195
+ const mcpPath = path.join(this.cursorDir, MCP_FILE);
196
+ if (fs.existsSync(mcpPath)) {
197
+ await promises.rename(mcpPath, path.join(this.cursorDir, MCP_BACKUP_FILE));
198
+ this.hadExistingMcp = true;
199
+ logger.logger.debug("[cursor] existing mcp.json moved aside for the session");
200
+ }
201
+ await promises.writeFile(mcpPath, JSON.stringify({
202
+ mcpServers: {
203
+ happy: { command: this.mcpBridge.command, args: this.mcpBridge.args }
204
+ }
205
+ }, null, 2), "utf8");
206
+ }
207
+ /** Keep the session's scaffolding out of `git status`. */
208
+ async excludeFromGit() {
209
+ const excludePath = path.join(this.cwd, ".git", "info", "exclude");
210
+ if (!fs.existsSync(excludePath)) return;
211
+ try {
212
+ const current = await promises.readFile(excludePath, "utf8");
213
+ if (current.includes(".cursor/.happy-approval.json")) return;
214
+ await promises.writeFile(
215
+ excludePath,
216
+ `${current}${current.endsWith("\n") ? "" : "\n"}# added by happy cursor session
217
+ .cursor/hooks.json
218
+ .cursor/mcp.json
219
+ .cursor/.happy-approval.json
220
+ `,
221
+ "utf8"
222
+ );
223
+ } catch (error) {
224
+ logger.logger.debug(`[cursor] could not update .git/info/exclude: ${error}`);
225
+ }
226
+ }
227
+ async stop() {
228
+ if (!this.started) return;
229
+ this.started = false;
230
+ try {
231
+ for (const chatId of this.registeredChatIds) {
232
+ await promises.rm(path.join(approvalRegistryDir(), `${chatId}.json`), { force: true });
233
+ }
234
+ this.registeredChatIds.clear();
235
+ await promises.rm(path.join(this.cursorDir, APPROVAL_ENDPOINT_FILE), { force: true });
236
+ const hooksPath = path.join(this.cursorDir, HOOKS_FILE);
237
+ const backupPath = path.join(this.cursorDir, HOOKS_BACKUP_FILE);
238
+ if (this.hadExistingHooks && fs.existsSync(backupPath)) {
239
+ await promises.rename(backupPath, hooksPath);
240
+ } else {
241
+ await promises.rm(hooksPath, { force: true });
242
+ }
243
+ const mcpPath = path.join(this.cursorDir, MCP_FILE);
244
+ const mcpBackup = path.join(this.cursorDir, MCP_BACKUP_FILE);
245
+ if (this.hadExistingMcp && fs.existsSync(mcpBackup)) {
246
+ await promises.rename(mcpBackup, mcpPath);
247
+ } else if (this.mcpBridge) {
248
+ await promises.rm(mcpPath, { force: true });
249
+ }
250
+ if (this.createdCursorDir && (await promises.readdir(this.cursorDir)).length === 0) {
251
+ await promises.rmdir(this.cursorDir);
252
+ }
253
+ } catch (error) {
254
+ logger.logger.debug(`[cursor] cleanup of .cursor/ failed: ${error}`);
255
+ }
256
+ await new Promise((resolve) => {
257
+ if (!this.server) return resolve();
258
+ this.server.close(() => resolve());
259
+ this.server = null;
260
+ });
261
+ logger.logger.debug("[cursor] approval bridge stopped");
262
+ }
263
+ }
264
+
265
+ exports.APPROVAL_ENDPOINT_FILE = APPROVAL_ENDPOINT_FILE;
266
+ exports.CursorApprovalBridge = CursorApprovalBridge;
267
+ exports.approvalRegistryDir = approvalRegistryDir;
268
+ exports.normalizeCursorTool = normalizeCursorTool;
@@ -1,6 +1,6 @@
1
1
  import axios from 'axios';
2
- import { h as decodeBase64, e as encodeBase64, f as encrypt, g as decrypt, c as configuration } from './persistence-CIUFyOwz.mjs';
3
- import { decryptWithEphemeralKey } from './auth-DS8fzlin.mjs';
2
+ import { h as decodeBase64, e as encodeBase64, f as encrypt, g as decrypt, c as configuration } from './persistence-DbLMvLvk.mjs';
3
+ import { decryptWithEphemeralKey } from './auth-qM2eojWx.mjs';
4
4
  import tweetnacl from 'tweetnacl';
5
5
  import { existsSync } from 'node:fs';
6
6
  import { writeFile, mkdir, readFile } from 'node:fs/promises';
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var axios = require('axios');
4
- var logger = require('./persistence-DrfN7TzY.cjs');
5
- var auth = require('./auth-BQvEuSs5.cjs');
4
+ var logger = require('./persistence-D7zNFI-d.cjs');
5
+ var auth = require('./auth-BqGkUXvq.cjs');
6
6
  var tweetnacl = require('tweetnacl');
7
7
  var fs = require('node:fs');
8
8
  var promises = require('node:fs/promises');
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var logger = require('./persistence-DrfN7TzY.cjs');
5
- var controlClient = require('./controlClient-DgELDZgM.cjs');
4
+ var logger = require('./persistence-D7zNFI-d.cjs');
5
+ var controlClient = require('./controlClient-yTkpwcOq.cjs');
6
6
  var doctor = require('./doctor-C08m5Z9f.cjs');
7
7
  var fs = require('node:fs');
8
8
  var path = require('node:path');
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { p as packageJson, c as configuration, a as readSettings, r as readCredentials, m as readDaemonState } from './persistence-CIUFyOwz.mjs';
3
- import { checkIfDaemonRunningAndCleanupStaleState } from './controlClient-DgjVRN8D.mjs';
2
+ import { p as packageJson, c as configuration, a as readSettings, r as readCredentials, m as readDaemonState } from './persistence-DbLMvLvk.mjs';
3
+ import { checkIfDaemonRunningAndCleanupStaleState } from './controlClient-BzzTNSfl.mjs';
4
4
  import { findAllHappyProcesses } from './doctor-ON7pN5G5.mjs';
5
5
  import { existsSync, readdirSync, statSync } from 'node:fs';
6
6
  import { join } from 'node:path';