@zonease/happy 0.6.42 → 0.6.44
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.
- package/dist/{api-CIE4_cWd.cjs → api-Ct7Z1lwF.cjs} +65 -5
- package/dist/{api-Ew6xr81i.mjs → api-D3j3YL3m.mjs} +65 -5
- package/dist/{auth-G9fZpVJ2.cjs → auth-B9kOli6i.cjs} +2 -2
- package/dist/{auth-BzYevJo2.mjs → auth-C2ZyAGMu.mjs} +4 -4
- package/dist/{auth-Q2STUNI-.mjs → auth-DPe89SQx.mjs} +2 -2
- package/dist/{auth-Dor79zTL.cjs → auth-W1aK_a-P.cjs} +4 -4
- package/dist/{browser-qDIIAL7S.cjs → browser-BPEhi6te.cjs} +1 -1
- package/dist/{browser-BI7s-V8E.mjs → browser-Byx5gq-W.mjs} +1 -1
- package/dist/{claudeLocal-Db2tDC4X.mjs → claudeLocal-BtxBYzs7.mjs} +1 -1
- package/dist/{claudeLocal-f4uBLJaM.cjs → claudeLocal-CcarNVE9.cjs} +1 -1
- package/dist/{config-CHjDs8AS.mjs → config-DJLOlgjr.mjs} +1 -1
- package/dist/{config-CSBlEik9.cjs → config-Djr2JMWO.cjs} +1 -1
- package/dist/{connect-O_nA5BOC.mjs → connect-PrUuHNLU.mjs} +4 -4
- package/dist/{connect-CJoTliii.cjs → connect-rcgRx9US.cjs} +4 -4
- package/dist/{controlClient-DoS9JqQC.mjs → controlClient-DSiES8Un.mjs} +1 -1
- package/dist/{controlClient-CEz_p1zc.cjs → controlClient-FDsMeGVn.cjs} +1 -1
- package/dist/{cursorApprovalHook-DRAHfTXU.cjs → cursorApprovalHook-CTJiiPO6.cjs} +29 -11
- package/dist/{cursorApprovalHook-B0v1kGa7.mjs → cursorApprovalHook-DGYeNaLe.mjs} +29 -11
- package/dist/{cursorApprovalServer-DzY8yJlI.cjs → cursorApprovalServer-C_wqI7wm.cjs} +109 -8
- package/dist/{cursorApprovalServer-JjPkdjGR.mjs → cursorApprovalServer-fjPBXSAb.mjs} +109 -10
- package/dist/{deviceKeys-B4aqjnE6.mjs → deviceKeys-BYHzWSyg.mjs} +2 -2
- package/dist/{deviceKeys-DyUn0bEx.cjs → deviceKeys-DPut1U07.cjs} +2 -2
- package/dist/{doctor-DRnft_5-.mjs → doctor-BA28TuMy.mjs} +2 -2
- package/dist/{doctor-BJSuVRUZ.cjs → doctor-B_W7odmC.cjs} +2 -2
- package/dist/{downloadImage-Cijmufpr.cjs → downloadImage-B7MKUI0u.cjs} +1 -372
- package/dist/downloadImage-DKWzC11M.mjs +485 -0
- package/dist/{enroll-CysDL6ee.mjs → enroll-Bc3Lke1H.mjs} +3 -3
- package/dist/{enroll-C2gunHdt.cjs → enroll-Blqx_Kg8.cjs} +3 -3
- package/dist/{evidence-DPutCuUr.mjs → evidence-D8nvQ8pQ.mjs} +1 -1
- package/dist/{evidence-DsQ-h-_W.cjs → evidence-jfqT04hc.cjs} +1 -1
- package/dist/{formatImageMessage-6aV9YFWr.mjs → formatImageMessage-BFWnadBR.mjs} +1 -1
- package/dist/{formatImageMessage-BPK_Ivfe.cjs → formatImageMessage-CZeSzoFO.cjs} +1 -1
- package/dist/index.cjs +50 -46
- package/dist/index.mjs +50 -46
- package/dist/{install-DBg0xQ3g.mjs → install-BvuadnKW.mjs} +1 -1
- package/dist/{install-BsTA2tZn.mjs → install-ChK8Eb58.mjs} +2 -2
- package/dist/{install-Tyhs6rUp.cjs → install-DFmp8KKp.cjs} +1 -1
- package/dist/{install-DlOppA-e.cjs → install-nJv8rYWA.cjs} +2 -2
- package/dist/lib.cjs +3 -3
- package/dist/lib.d.cts +1 -0
- package/dist/lib.d.mts +1 -0
- package/dist/lib.mjs +3 -3
- package/dist/{loadableToolOutput-DPGTtKJM.cjs → loadableToolOutput-CqMfcEh7.cjs} +2 -2
- package/dist/{loadableToolOutput-2dMPAp4N.mjs → loadableToolOutput-D6crYom_.mjs} +2 -2
- package/dist/{persistence-D9tMm6bj.mjs → persistence-BXtirP1k.mjs} +1 -1
- package/dist/{persistence-BcQmjobZ.cjs → persistence-DOgft4tZ.cjs} +1 -1
- package/dist/{push-Dxsugkes.mjs → push-188EjPTP.mjs} +6 -6
- package/dist/{push-Cnm6JQMD.cjs → push-DykEVhRR.cjs} +6 -6
- package/dist/registerKillSessionHandler-6loIZYD9.mjs +407 -0
- package/dist/registerKillSessionHandler-ks8iPY9a.cjs +411 -0
- package/dist/{run-CiPWWhXt.cjs → run-DSJ7IIWW.cjs} +10 -8
- package/dist/{run-BEflQaQ2.mjs → run-Dp4_De-r.mjs} +10 -8
- package/dist/{runClaude-BVXsyCHb.cjs → runClaude-C3be35z0.cjs} +26 -26
- package/dist/{runClaude-sPkSqHWY.mjs → runClaude-gj4urJvB.mjs} +21 -21
- package/dist/{runCodex-C4rHkIyw.mjs → runCodex-DRZidxNB.mjs} +26 -26
- package/dist/{runCodex-BAwhdmSA.cjs → runCodex-p_fToj9T.cjs} +28 -28
- package/dist/{runCursor-CD5wC3gA.cjs → runCursor-CJlqkSYR.cjs} +210 -34
- package/dist/{runCursor-DiHLWMs8.mjs → runCursor-SFd9G7X_.mjs} +210 -34
- package/dist/{runGemini-B-93gOqB.mjs → runGemini-BOsfh6dX.mjs} +17 -17
- package/dist/{runGemini-Cto-adir.cjs → runGemini-lxvXq70R.cjs} +19 -19
- package/dist/{runOneShot-BfIzE9W4.cjs → runOneShot-BExFkHmA.cjs} +2 -2
- package/dist/{runOneShot-DGjlYVhR.mjs → runOneShot-CtzlkF1l.mjs} +2 -2
- package/dist/{serve-CqL6NWDr.cjs → serve-Bl1Caf6x.cjs} +6 -6
- package/dist/{serve-ttlqF5jn.mjs → serve-DfvSXAD2.mjs} +6 -6
- package/dist/{session-BZhGpQUJ.cjs → session--R6FqsxL.cjs} +5 -5
- package/dist/{session-Co0REU9e.mjs → session-BSzNJiAb.mjs} +5 -5
- package/dist/{sessionKeyCache-BwFUGgMs.cjs → sessionKeyCache-CVkksG68.cjs} +1 -1
- package/dist/{sessionKeyCache-pXTjpETo.mjs → sessionKeyCache-DYiCIvYC.mjs} +1 -1
- package/dist/{sessionList-fG5uAPzU.cjs → sessionList-C1q9fy5j.cjs} +2 -2
- package/dist/{sessionList-DaufR-sA.mjs → sessionList-D3pCseut.mjs} +2 -2
- package/dist/{sessionSummary-CjZPt6nr.cjs → sessionSummary-CUZxEBEZ.cjs} +3 -3
- package/dist/{sessionSummary-Dv6dPABg.mjs → sessionSummary-WpOlgXe1.mjs} +3 -3
- package/dist/{setupOfflineReconnection-DW5il97B.mjs → setupOfflineReconnection-BeWF3SGN.mjs} +5 -3
- package/dist/{setupOfflineReconnection-B6coH9QN.cjs → setupOfflineReconnection-CixoiWTS.cjs} +7 -5
- package/dist/{spawnHappyCLI-DhUeycct.mjs → spawnHappyCLI-Cyc-69jO.mjs} +1 -1
- package/dist/{spawnHappyCLI-Cfu7C1uM.cjs → spawnHappyCLI-hRc7u16s.cjs} +1 -1
- package/dist/{ssh-C4YTSHXL.cjs → ssh-B_TYtrys.cjs} +5 -5
- package/dist/{ssh-8tPIKk-i.mjs → ssh-BsxAgWgG.mjs} +5 -5
- package/dist/{task-DFW-PecP.cjs → task-BInh42e3.cjs} +6 -6
- package/dist/{task-CH-oidFy.mjs → task-BS4usaxx.mjs} +6 -6
- package/dist/{uninstall-CeioRZNI.mjs → uninstall-BKuDaZKB.mjs} +2 -2
- package/dist/{uninstall-yTSL3X6Y.cjs → uninstall-DNFGxNLl.cjs} +2 -2
- package/dist/{update-Bju28vNP.cjs → update-B9LoI5l8.cjs} +3 -3
- package/dist/{update-Derd2hoX.mjs → update-DC300VOW.mjs} +3 -3
- package/package.json +1 -1
- package/dist/downloadImage-BpVF7A79.mjs +0 -854
- package/dist/gitWorktree-C6jLoaMw.cjs +0 -39
- package/dist/gitWorktree-CzJDU2BV.mjs +0 -37
|
@@ -5,11 +5,62 @@ var node_crypto = require('node:crypto');
|
|
|
5
5
|
var promises = require('node:fs/promises');
|
|
6
6
|
var fs = require('node:fs');
|
|
7
7
|
var path = require('node:path');
|
|
8
|
-
var
|
|
8
|
+
var os = require('node:os');
|
|
9
|
+
var logger = require('./persistence-DOgft4tZ.cjs');
|
|
9
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
|
+
}
|
|
10
59
|
const APPROVAL_ENDPOINT_FILE = ".happy-approval.json";
|
|
11
60
|
const HOOKS_FILE = "hooks.json";
|
|
12
61
|
const HOOKS_BACKUP_FILE = "hooks.json.happy-backup";
|
|
62
|
+
const MCP_FILE = "mcp.json";
|
|
63
|
+
const MCP_BACKUP_FILE = "mcp.json.happy-backup";
|
|
13
64
|
const HOOK_TIMEOUT_SECONDS = 900;
|
|
14
65
|
class CursorApprovalBridge {
|
|
15
66
|
cwd;
|
|
@@ -18,12 +69,17 @@ class CursorApprovalBridge {
|
|
|
18
69
|
server = null;
|
|
19
70
|
port = 0;
|
|
20
71
|
hadExistingHooks = false;
|
|
72
|
+
hadExistingMcp = false;
|
|
73
|
+
mcpBridge;
|
|
21
74
|
/** True when .cursor/ did not exist before this session created it. */
|
|
22
75
|
createdCursorDir = false;
|
|
23
76
|
started = false;
|
|
77
|
+
/** Chat ids this session has registered, so they can be cleaned up. */
|
|
78
|
+
registeredChatIds = /* @__PURE__ */ new Set();
|
|
24
79
|
constructor(options) {
|
|
25
80
|
this.cwd = options.cwd;
|
|
26
81
|
this.handler = options.handler;
|
|
82
|
+
this.mcpBridge = options.mcpBridge ?? null;
|
|
27
83
|
}
|
|
28
84
|
get cursorDir() {
|
|
29
85
|
return path.join(this.cwd, ".cursor");
|
|
@@ -34,6 +90,23 @@ class CursorApprovalBridge {
|
|
|
34
90
|
this.started = true;
|
|
35
91
|
logger.logger.debug(`[cursor] approval bridge listening on 127.0.0.1:${this.port}`);
|
|
36
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
|
+
}
|
|
37
110
|
listen() {
|
|
38
111
|
return new Promise((resolve, reject) => {
|
|
39
112
|
const server = node_http.createServer((req, res) => {
|
|
@@ -80,9 +153,12 @@ class CursorApprovalBridge {
|
|
|
80
153
|
logger.logger.debug("[cursor] rejecting approval request with a bad token");
|
|
81
154
|
return false;
|
|
82
155
|
}
|
|
83
|
-
const
|
|
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
|
+
);
|
|
84
160
|
const toolCallId = typeof parsed.tool_use_id === "string" && parsed.tool_use_id.length > 0 ? parsed.tool_use_id : `cursor-${node_crypto.randomUUID()}`;
|
|
85
|
-
return this.handler.requestApproval(toolCallId, toolName,
|
|
161
|
+
return this.handler.requestApproval(toolCallId, toolName, toolInput);
|
|
86
162
|
}
|
|
87
163
|
/**
|
|
88
164
|
* Writes .cursor/hooks.json pointing at this CLI. Any file already there is
|
|
@@ -98,11 +174,6 @@ class CursorApprovalBridge {
|
|
|
98
174
|
this.hadExistingHooks = true;
|
|
99
175
|
logger.logger.debug("[cursor] existing hooks.json moved aside for the session");
|
|
100
176
|
}
|
|
101
|
-
await promises.writeFile(path.join(this.cursorDir, APPROVAL_ENDPOINT_FILE), JSON.stringify({
|
|
102
|
-
port: this.port,
|
|
103
|
-
token: this.token,
|
|
104
|
-
pid: process.pid
|
|
105
|
-
}), "utf8");
|
|
106
177
|
const hookCommand = `${process.execPath} ${process.argv[1]} cursor-approval-hook`;
|
|
107
178
|
await promises.writeFile(hooksPath, JSON.stringify({
|
|
108
179
|
version: 1,
|
|
@@ -115,8 +186,24 @@ class CursorApprovalBridge {
|
|
|
115
186
|
]
|
|
116
187
|
}
|
|
117
188
|
}, null, 2), "utf8");
|
|
189
|
+
await this.installMcpConfig();
|
|
118
190
|
await this.excludeFromGit();
|
|
119
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
|
+
}
|
|
120
207
|
/** Keep the session's scaffolding out of `git status`. */
|
|
121
208
|
async excludeFromGit() {
|
|
122
209
|
const excludePath = path.join(this.cwd, ".git", "info", "exclude");
|
|
@@ -128,6 +215,7 @@ class CursorApprovalBridge {
|
|
|
128
215
|
excludePath,
|
|
129
216
|
`${current}${current.endsWith("\n") ? "" : "\n"}# added by happy cursor session
|
|
130
217
|
.cursor/hooks.json
|
|
218
|
+
.cursor/mcp.json
|
|
131
219
|
.cursor/.happy-approval.json
|
|
132
220
|
`,
|
|
133
221
|
"utf8"
|
|
@@ -140,6 +228,10 @@ class CursorApprovalBridge {
|
|
|
140
228
|
if (!this.started) return;
|
|
141
229
|
this.started = false;
|
|
142
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();
|
|
143
235
|
await promises.rm(path.join(this.cursorDir, APPROVAL_ENDPOINT_FILE), { force: true });
|
|
144
236
|
const hooksPath = path.join(this.cursorDir, HOOKS_FILE);
|
|
145
237
|
const backupPath = path.join(this.cursorDir, HOOKS_BACKUP_FILE);
|
|
@@ -148,6 +240,13 @@ class CursorApprovalBridge {
|
|
|
148
240
|
} else {
|
|
149
241
|
await promises.rm(hooksPath, { force: true });
|
|
150
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
|
+
}
|
|
151
250
|
if (this.createdCursorDir && (await promises.readdir(this.cursorDir)).length === 0) {
|
|
152
251
|
await promises.rmdir(this.cursorDir);
|
|
153
252
|
}
|
|
@@ -165,3 +264,5 @@ class CursorApprovalBridge {
|
|
|
165
264
|
|
|
166
265
|
exports.APPROVAL_ENDPOINT_FILE = APPROVAL_ENDPOINT_FILE;
|
|
167
266
|
exports.CursorApprovalBridge = CursorApprovalBridge;
|
|
267
|
+
exports.approvalRegistryDir = approvalRegistryDir;
|
|
268
|
+
exports.normalizeCursorTool = normalizeCursorTool;
|
|
@@ -1,13 +1,64 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdir,
|
|
3
|
+
import { mkdir, writeFile, rename, readFile, rm, readdir, rmdir } from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import {
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { l as logger } from './persistence-BXtirP1k.mjs';
|
|
7
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
|
+
}
|
|
8
57
|
const APPROVAL_ENDPOINT_FILE = ".happy-approval.json";
|
|
9
58
|
const HOOKS_FILE = "hooks.json";
|
|
10
59
|
const HOOKS_BACKUP_FILE = "hooks.json.happy-backup";
|
|
60
|
+
const MCP_FILE = "mcp.json";
|
|
61
|
+
const MCP_BACKUP_FILE = "mcp.json.happy-backup";
|
|
11
62
|
const HOOK_TIMEOUT_SECONDS = 900;
|
|
12
63
|
class CursorApprovalBridge {
|
|
13
64
|
cwd;
|
|
@@ -16,12 +67,17 @@ class CursorApprovalBridge {
|
|
|
16
67
|
server = null;
|
|
17
68
|
port = 0;
|
|
18
69
|
hadExistingHooks = false;
|
|
70
|
+
hadExistingMcp = false;
|
|
71
|
+
mcpBridge;
|
|
19
72
|
/** True when .cursor/ did not exist before this session created it. */
|
|
20
73
|
createdCursorDir = false;
|
|
21
74
|
started = false;
|
|
75
|
+
/** Chat ids this session has registered, so they can be cleaned up. */
|
|
76
|
+
registeredChatIds = /* @__PURE__ */ new Set();
|
|
22
77
|
constructor(options) {
|
|
23
78
|
this.cwd = options.cwd;
|
|
24
79
|
this.handler = options.handler;
|
|
80
|
+
this.mcpBridge = options.mcpBridge ?? null;
|
|
25
81
|
}
|
|
26
82
|
get cursorDir() {
|
|
27
83
|
return join(this.cwd, ".cursor");
|
|
@@ -32,6 +88,23 @@ class CursorApprovalBridge {
|
|
|
32
88
|
this.started = true;
|
|
33
89
|
logger.debug(`[cursor] approval bridge listening on 127.0.0.1:${this.port}`);
|
|
34
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
|
+
}
|
|
35
108
|
listen() {
|
|
36
109
|
return new Promise((resolve, reject) => {
|
|
37
110
|
const server = createServer((req, res) => {
|
|
@@ -78,9 +151,12 @@ class CursorApprovalBridge {
|
|
|
78
151
|
logger.debug("[cursor] rejecting approval request with a bad token");
|
|
79
152
|
return false;
|
|
80
153
|
}
|
|
81
|
-
const
|
|
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
|
+
);
|
|
82
158
|
const toolCallId = typeof parsed.tool_use_id === "string" && parsed.tool_use_id.length > 0 ? parsed.tool_use_id : `cursor-${randomUUID()}`;
|
|
83
|
-
return this.handler.requestApproval(toolCallId, toolName,
|
|
159
|
+
return this.handler.requestApproval(toolCallId, toolName, toolInput);
|
|
84
160
|
}
|
|
85
161
|
/**
|
|
86
162
|
* Writes .cursor/hooks.json pointing at this CLI. Any file already there is
|
|
@@ -96,11 +172,6 @@ class CursorApprovalBridge {
|
|
|
96
172
|
this.hadExistingHooks = true;
|
|
97
173
|
logger.debug("[cursor] existing hooks.json moved aside for the session");
|
|
98
174
|
}
|
|
99
|
-
await writeFile(join(this.cursorDir, APPROVAL_ENDPOINT_FILE), JSON.stringify({
|
|
100
|
-
port: this.port,
|
|
101
|
-
token: this.token,
|
|
102
|
-
pid: process.pid
|
|
103
|
-
}), "utf8");
|
|
104
175
|
const hookCommand = `${process.execPath} ${process.argv[1]} cursor-approval-hook`;
|
|
105
176
|
await writeFile(hooksPath, JSON.stringify({
|
|
106
177
|
version: 1,
|
|
@@ -113,8 +184,24 @@ class CursorApprovalBridge {
|
|
|
113
184
|
]
|
|
114
185
|
}
|
|
115
186
|
}, null, 2), "utf8");
|
|
187
|
+
await this.installMcpConfig();
|
|
116
188
|
await this.excludeFromGit();
|
|
117
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
|
+
}
|
|
118
205
|
/** Keep the session's scaffolding out of `git status`. */
|
|
119
206
|
async excludeFromGit() {
|
|
120
207
|
const excludePath = join(this.cwd, ".git", "info", "exclude");
|
|
@@ -126,6 +213,7 @@ class CursorApprovalBridge {
|
|
|
126
213
|
excludePath,
|
|
127
214
|
`${current}${current.endsWith("\n") ? "" : "\n"}# added by happy cursor session
|
|
128
215
|
.cursor/hooks.json
|
|
216
|
+
.cursor/mcp.json
|
|
129
217
|
.cursor/.happy-approval.json
|
|
130
218
|
`,
|
|
131
219
|
"utf8"
|
|
@@ -138,6 +226,10 @@ class CursorApprovalBridge {
|
|
|
138
226
|
if (!this.started) return;
|
|
139
227
|
this.started = false;
|
|
140
228
|
try {
|
|
229
|
+
for (const chatId of this.registeredChatIds) {
|
|
230
|
+
await rm(join(approvalRegistryDir(), `${chatId}.json`), { force: true });
|
|
231
|
+
}
|
|
232
|
+
this.registeredChatIds.clear();
|
|
141
233
|
await rm(join(this.cursorDir, APPROVAL_ENDPOINT_FILE), { force: true });
|
|
142
234
|
const hooksPath = join(this.cursorDir, HOOKS_FILE);
|
|
143
235
|
const backupPath = join(this.cursorDir, HOOKS_BACKUP_FILE);
|
|
@@ -146,6 +238,13 @@ class CursorApprovalBridge {
|
|
|
146
238
|
} else {
|
|
147
239
|
await rm(hooksPath, { force: true });
|
|
148
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
|
+
}
|
|
149
248
|
if (this.createdCursorDir && (await readdir(this.cursorDir)).length === 0) {
|
|
150
249
|
await rmdir(this.cursorDir);
|
|
151
250
|
}
|
|
@@ -161,4 +260,4 @@ class CursorApprovalBridge {
|
|
|
161
260
|
}
|
|
162
261
|
}
|
|
163
262
|
|
|
164
|
-
export { APPROVAL_ENDPOINT_FILE as A, CursorApprovalBridge as C };
|
|
263
|
+
export { APPROVAL_ENDPOINT_FILE as A, CursorApprovalBridge as C, approvalRegistryDir as a, normalizeCursorTool as n };
|
|
@@ -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-
|
|
3
|
-
import { decryptWithEphemeralKey } from './auth-
|
|
2
|
+
import { h as decodeBase64, e as encodeBase64, f as encrypt, g as decrypt, c as configuration } from './persistence-BXtirP1k.mjs';
|
|
3
|
+
import { decryptWithEphemeralKey } from './auth-DPe89SQx.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-
|
|
5
|
-
var auth = require('./auth-
|
|
4
|
+
var logger = require('./persistence-DOgft4tZ.cjs');
|
|
5
|
+
var auth = require('./auth-B9kOli6i.cjs');
|
|
6
6
|
var tweetnacl = require('tweetnacl');
|
|
7
7
|
var fs = require('node:fs');
|
|
8
8
|
var promises = require('node:fs/promises');
|
|
@@ -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-
|
|
3
|
-
import { checkIfDaemonRunningAndCleanupStaleState } from './controlClient-
|
|
2
|
+
import { p as packageJson, c as configuration, a as readSettings, r as readCredentials, m as readDaemonState } from './persistence-BXtirP1k.mjs';
|
|
3
|
+
import { checkIfDaemonRunningAndCleanupStaleState } from './controlClient-DSiES8Un.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';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var logger = require('./persistence-
|
|
5
|
-
var controlClient = require('./controlClient-
|
|
4
|
+
var logger = require('./persistence-DOgft4tZ.cjs');
|
|
5
|
+
var controlClient = require('./controlClient-FDsMeGVn.cjs');
|
|
6
6
|
var doctor = require('./doctor-C08m5Z9f.cjs');
|
|
7
7
|
var fs = require('node:fs');
|
|
8
8
|
var path = require('node:path');
|