@zonease/happy 0.6.41 → 0.6.42
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-XeHlmApX.cjs → api-CIE4_cWd.cjs} +4 -4
- package/dist/{api-K2-jr9ar.mjs → api-Ew6xr81i.mjs} +4 -4
- package/dist/{auth-CjLkoM3m.mjs → auth-BzYevJo2.mjs} +4 -4
- package/dist/{auth-BbvPcnkY.cjs → auth-Dor79zTL.cjs} +4 -4
- package/dist/{auth-BQvEuSs5.cjs → auth-G9fZpVJ2.cjs} +2 -2
- package/dist/{auth-DS8fzlin.mjs → auth-Q2STUNI-.mjs} +2 -2
- package/dist/{browser-C05Ak6RL.mjs → browser-BI7s-V8E.mjs} +1 -1
- package/dist/{browser-C05SP_Tt.cjs → browser-qDIIAL7S.cjs} +1 -1
- package/dist/{claudeLocal-DJNRlHIC.mjs → claudeLocal-Db2tDC4X.mjs} +1 -1
- package/dist/{claudeLocal-CpDhLCcZ.cjs → claudeLocal-f4uBLJaM.cjs} +1 -1
- package/dist/{config-DWWtjKQx.mjs → config-CHjDs8AS.mjs} +1 -1
- package/dist/{config-B0mns9fF.cjs → config-CSBlEik9.cjs} +1 -1
- package/dist/{connect-BxCScVNX.cjs → connect-CJoTliii.cjs} +5 -5
- package/dist/{connect-DXQC1CJe.mjs → connect-O_nA5BOC.mjs} +5 -5
- package/dist/{controlClient-DgELDZgM.cjs → controlClient-CEz_p1zc.cjs} +1 -1
- package/dist/{controlClient-DgjVRN8D.mjs → controlClient-DoS9JqQC.mjs} +1 -1
- package/dist/cursorApprovalHook-B0v1kGa7.mjs +95 -0
- package/dist/cursorApprovalHook-DRAHfTXU.cjs +97 -0
- package/dist/cursorApprovalServer-DzY8yJlI.cjs +167 -0
- package/dist/cursorApprovalServer-JjPkdjGR.mjs +164 -0
- package/dist/{deviceKeys-CejCdWpw.mjs → deviceKeys-B4aqjnE6.mjs} +2 -2
- package/dist/{deviceKeys-BpIAWz13.cjs → deviceKeys-DyUn0bEx.cjs} +2 -2
- package/dist/{doctor-DznasgP3.cjs → doctor-BJSuVRUZ.cjs} +2 -2
- package/dist/{doctor-B-LCMVN9.mjs → doctor-DRnft_5-.mjs} +2 -2
- package/dist/{downloadImage-ia5SzlCV.mjs → downloadImage-BpVF7A79.mjs} +7 -40
- package/dist/{downloadImage-B_ygLAXY.cjs → downloadImage-Cijmufpr.cjs} +5 -39
- package/dist/{enroll-BvfpRUGJ.cjs → enroll-C2gunHdt.cjs} +3 -3
- package/dist/{enroll-HjUqyGnD.mjs → enroll-CysDL6ee.mjs} +3 -3
- package/dist/{evidence-BEQd1dCh.mjs → evidence-DPutCuUr.mjs} +1 -1
- package/dist/{evidence-CVgTjBrq.cjs → evidence-DsQ-h-_W.cjs} +1 -1
- package/dist/{formatImageMessage-DVs57t3x.mjs → formatImageMessage-6aV9YFWr.mjs} +1 -1
- package/dist/{formatImageMessage-CmJTP_hU.cjs → formatImageMessage-BPK_Ivfe.cjs} +1 -1
- package/dist/gitWorktree-C6jLoaMw.cjs +39 -0
- package/dist/gitWorktree-CzJDU2BV.mjs +37 -0
- package/dist/{index-CDTNgfUf.cjs → index-BeKrqPYR.cjs} +25 -1
- package/dist/{index-CPKdvi7J.mjs → index-DTV7drqs.mjs} +25 -1
- package/dist/index.cjs +91 -41
- package/dist/index.mjs +91 -41
- package/dist/{install-UTz_vYvB.mjs → install-BsTA2tZn.mjs} +2 -2
- package/dist/{install-B3mMGISS.mjs → install-DBg0xQ3g.mjs} +1 -1
- package/dist/{install-2Yl_L582.cjs → install-DlOppA-e.cjs} +2 -2
- package/dist/{install-Ck22tYHP.cjs → install-Tyhs6rUp.cjs} +1 -1
- package/dist/lib.cjs +4 -4
- package/dist/lib.d.cts +3 -2
- package/dist/lib.d.mts +3 -2
- package/dist/lib.mjs +4 -4
- package/dist/{loadableToolOutput-4EvraaNY.mjs → loadableToolOutput-2dMPAp4N.mjs} +3 -456
- package/dist/{loadableToolOutput-BZYlaqHn.cjs → loadableToolOutput-DPGTtKJM.cjs} +2 -458
- package/dist/{persistence-DrfN7TzY.cjs → persistence-BcQmjobZ.cjs} +7 -4
- package/dist/{persistence-CIUFyOwz.mjs → persistence-D9tMm6bj.mjs} +7 -4
- package/dist/{push-CCOXCcM_.cjs → push-Cnm6JQMD.cjs} +6 -6
- package/dist/{push-USXj_pv0.mjs → push-Dxsugkes.mjs} +6 -6
- package/dist/{run-Dy6KX6m-.mjs → run-BEflQaQ2.mjs} +12 -9
- package/dist/{run-D3CopmEb.cjs → run-CiPWWhXt.cjs} +12 -9
- package/dist/{runClaude-aHzvx6Ha.cjs → runClaude-BVXsyCHb.cjs} +19 -18
- package/dist/{runClaude-CWux2RDq.mjs → runClaude-sPkSqHWY.mjs} +16 -15
- package/dist/{runCodex-G-Aq531U.cjs → runCodex-BAwhdmSA.cjs} +19 -17
- package/dist/{runCodex-D0ZxSj3O.mjs → runCodex-C4rHkIyw.mjs} +16 -14
- package/dist/runCursor-CD5wC3gA.cjs +578 -0
- package/dist/runCursor-DiHLWMs8.mjs +576 -0
- package/dist/{runGemini-DZC2i6vA.mjs → runGemini-B-93gOqB.mjs} +18 -16
- package/dist/{runGemini-DxGgDAIM.cjs → runGemini-Cto-adir.cjs} +21 -19
- package/dist/{runOneShot-BgT-JNMA.cjs → runOneShot-BfIzE9W4.cjs} +3 -3
- package/dist/{runOneShot-cxlk7VKA.mjs → runOneShot-DGjlYVhR.mjs} +3 -3
- package/dist/{serve-Dr3wPrtK.cjs → serve-CqL6NWDr.cjs} +7 -7
- package/dist/{serve-CKyvJNs0.mjs → serve-ttlqF5jn.mjs} +7 -7
- package/dist/{session-dYzbOo_W.cjs → session-BZhGpQUJ.cjs} +5 -5
- package/dist/{session-vf2ZjFto.mjs → session-Co0REU9e.mjs} +5 -5
- package/dist/{sessionKeyCache-dDquDzn0.cjs → sessionKeyCache-BwFUGgMs.cjs} +1 -1
- package/dist/{sessionKeyCache-D2u3JUys.mjs → sessionKeyCache-pXTjpETo.mjs} +1 -1
- package/dist/{sessionList-C1k2PNg_.mjs → sessionList-DaufR-sA.mjs} +2 -2
- package/dist/{sessionList-DmmtC7uR.cjs → sessionList-fG5uAPzU.cjs} +2 -2
- package/dist/{sessionSummary-DGLoVOxd.cjs → sessionSummary-CjZPt6nr.cjs} +3 -3
- package/dist/{sessionSummary-5a1QtFVS.mjs → sessionSummary-Dv6dPABg.mjs} +3 -3
- package/dist/setupOfflineReconnection-B6coH9QN.cjs +461 -0
- package/dist/setupOfflineReconnection-DW5il97B.mjs +457 -0
- package/dist/{spawnHappyCLI-CarRI4GM.cjs → spawnHappyCLI-Cfu7C1uM.cjs} +1 -1
- package/dist/{spawnHappyCLI-D3EkTFIn.mjs → spawnHappyCLI-DhUeycct.mjs} +1 -1
- package/dist/{ssh-B0hvEYIl.mjs → ssh-8tPIKk-i.mjs} +5 -5
- package/dist/{ssh-CBgVwKsB.cjs → ssh-C4YTSHXL.cjs} +5 -5
- package/dist/{task-CRo6W-3_.mjs → task-CH-oidFy.mjs} +6 -6
- package/dist/{task-CMunPKCV.cjs → task-DFW-PecP.cjs} +6 -6
- package/dist/{uninstall-BALE2up0.mjs → uninstall-CeioRZNI.mjs} +2 -2
- package/dist/{uninstall-BJ3eEGM4.cjs → uninstall-yTSL3X6Y.cjs} +2 -2
- package/dist/{update-Bb3iijy8.cjs → update-Bju28vNP.cjs} +3 -3
- package/dist/{update-BfjtLrhO.mjs → update-Derd2hoX.mjs} +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
|
+
var chalk = require('chalk');
|
|
5
|
+
var api = require('./api-CIE4_cWd.cjs');
|
|
6
|
+
var logger = require('./persistence-BcQmjobZ.cjs');
|
|
7
|
+
var run = require('./run-CiPWWhXt.cjs');
|
|
8
|
+
var setupOfflineReconnection = require('./setupOfflineReconnection-B6coH9QN.cjs');
|
|
9
|
+
var node_child_process = require('node:child_process');
|
|
10
|
+
var cursorApprovalServer = require('./cursorApprovalServer-DzY8yJlI.cjs');
|
|
11
|
+
require('axios');
|
|
12
|
+
require('node:events');
|
|
13
|
+
require('socket.io-client');
|
|
14
|
+
require('zod');
|
|
15
|
+
require('./index-BeKrqPYR.cjs');
|
|
16
|
+
require('@paralleldrive/cuid2');
|
|
17
|
+
require('./time-Bj2HRkKH.cjs');
|
|
18
|
+
require('node:fs');
|
|
19
|
+
require('node:path');
|
|
20
|
+
require('node:fs/promises');
|
|
21
|
+
require('node:os');
|
|
22
|
+
require('node:util');
|
|
23
|
+
require('child_process');
|
|
24
|
+
require('util');
|
|
25
|
+
require('fs/promises');
|
|
26
|
+
require('path');
|
|
27
|
+
require('./projectPath-ChLhdOyt.cjs');
|
|
28
|
+
require('url');
|
|
29
|
+
require('node:module');
|
|
30
|
+
require('ws');
|
|
31
|
+
require('crypto');
|
|
32
|
+
require('tweetnacl');
|
|
33
|
+
require('@stablelib/base64');
|
|
34
|
+
require('node:readline');
|
|
35
|
+
require('expo-server-sdk');
|
|
36
|
+
require('./sessionKeyCache-BwFUGgMs.cjs');
|
|
37
|
+
require('fs');
|
|
38
|
+
require('os');
|
|
39
|
+
require('tmp');
|
|
40
|
+
require('./auth-G9fZpVJ2.cjs');
|
|
41
|
+
require('qrcode-terminal');
|
|
42
|
+
require('./browser-qDIIAL7S.cjs');
|
|
43
|
+
require('open');
|
|
44
|
+
require('react');
|
|
45
|
+
require('ink');
|
|
46
|
+
require('./doctor-BJSuVRUZ.cjs');
|
|
47
|
+
require('./controlClient-CEz_p1zc.cjs');
|
|
48
|
+
require('./doctor-C08m5Z9f.cjs');
|
|
49
|
+
require('ps-list');
|
|
50
|
+
require('cross-spawn');
|
|
51
|
+
require('./spawnHappyCLI-Cfu7C1uM.cjs');
|
|
52
|
+
require('./deviceKeys-DyUn0bEx.cjs');
|
|
53
|
+
require('./install-Tyhs6rUp.cjs');
|
|
54
|
+
require('./trimIdent-4gvk9Wk6.cjs');
|
|
55
|
+
require('fastify');
|
|
56
|
+
require('fastify-type-provider-zod');
|
|
57
|
+
require('./common-CZvSRU4p.cjs');
|
|
58
|
+
require('./gitWorktree-C6jLoaMw.cjs');
|
|
59
|
+
require('node:http');
|
|
60
|
+
|
|
61
|
+
function textFromMessageContent(message) {
|
|
62
|
+
if (!message || typeof message !== "object") return null;
|
|
63
|
+
const content = message.content;
|
|
64
|
+
if (!Array.isArray(content)) return null;
|
|
65
|
+
const parts = [];
|
|
66
|
+
for (const block of content) {
|
|
67
|
+
if (block && typeof block === "object" && block.type === "text") {
|
|
68
|
+
const text = block.text;
|
|
69
|
+
if (typeof text === "string") parts.push(text);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return parts.length > 0 ? parts.join("") : null;
|
|
73
|
+
}
|
|
74
|
+
function unwrapToolCall(toolCall) {
|
|
75
|
+
if (!toolCall || typeof toolCall !== "object") return null;
|
|
76
|
+
for (const [key, value] of Object.entries(toolCall)) {
|
|
77
|
+
if (!key.endsWith("ToolCall") || !value || typeof value !== "object") continue;
|
|
78
|
+
const inner = value;
|
|
79
|
+
return {
|
|
80
|
+
toolName: key.replace(/ToolCall$/, ""),
|
|
81
|
+
args: inner.args && typeof inner.args === "object" ? inner.args : {},
|
|
82
|
+
result: inner.result ?? null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function editDiffFromResult(result) {
|
|
88
|
+
if (!result || typeof result !== "object") return null;
|
|
89
|
+
const success = result.success;
|
|
90
|
+
if (!success || typeof success !== "object") return null;
|
|
91
|
+
const diff = success.diffString;
|
|
92
|
+
if (typeof diff !== "string" || diff.length === 0) return null;
|
|
93
|
+
const path = success.path;
|
|
94
|
+
return { diff, path: typeof path === "string" ? path : null };
|
|
95
|
+
}
|
|
96
|
+
function parseCursorSessionInit(event) {
|
|
97
|
+
if (event.type !== "system" || event.subtype !== "init") return null;
|
|
98
|
+
const sessionId = event.session_id;
|
|
99
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
100
|
+
const str = (key) => typeof event[key] === "string" ? event[key] : null;
|
|
101
|
+
return {
|
|
102
|
+
sessionId,
|
|
103
|
+
model: str("model"),
|
|
104
|
+
cwd: str("cwd"),
|
|
105
|
+
permissionMode: str("permissionMode"),
|
|
106
|
+
apiKeySource: str("apiKeySource")
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function mapCursorStreamEvent(event) {
|
|
110
|
+
switch (event.type) {
|
|
111
|
+
case "system": {
|
|
112
|
+
if (event.subtype !== "init") return [];
|
|
113
|
+
const init = parseCursorSessionInit(event);
|
|
114
|
+
return [{
|
|
115
|
+
type: "status",
|
|
116
|
+
status: "running",
|
|
117
|
+
detail: init?.model ? `cursor-agent ready (${init.model})` : "cursor-agent ready"
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
// The user turn is echoed back to us; Happy already has that text from
|
|
121
|
+
// the send path, so re-emitting it would duplicate the bubble.
|
|
122
|
+
case "user":
|
|
123
|
+
return [];
|
|
124
|
+
case "thinking": {
|
|
125
|
+
if (event.subtype !== "delta") return [];
|
|
126
|
+
const text = typeof event.text === "string" ? event.text : null;
|
|
127
|
+
if (!text) return [];
|
|
128
|
+
return [{ type: "event", name: "thinking", payload: { textDelta: text } }];
|
|
129
|
+
}
|
|
130
|
+
case "assistant": {
|
|
131
|
+
const text = textFromMessageContent(event.message);
|
|
132
|
+
if (!text) return [];
|
|
133
|
+
return [{ type: "model-output", fullText: text }];
|
|
134
|
+
}
|
|
135
|
+
case "tool_call": {
|
|
136
|
+
const callId = typeof event.call_id === "string" ? event.call_id : null;
|
|
137
|
+
const unwrapped = unwrapToolCall(event.tool_call);
|
|
138
|
+
if (!callId || !unwrapped) return [];
|
|
139
|
+
if (event.subtype === "started") {
|
|
140
|
+
return [{ type: "tool-call", toolName: unwrapped.toolName, args: unwrapped.args, callId }];
|
|
141
|
+
}
|
|
142
|
+
if (event.subtype !== "completed") return [];
|
|
143
|
+
const messages = [{
|
|
144
|
+
type: "tool-result",
|
|
145
|
+
toolName: unwrapped.toolName,
|
|
146
|
+
result: unwrapped.result,
|
|
147
|
+
callId
|
|
148
|
+
}];
|
|
149
|
+
const edit = editDiffFromResult(unwrapped.result);
|
|
150
|
+
if (edit) {
|
|
151
|
+
messages.push({
|
|
152
|
+
type: "fs-edit",
|
|
153
|
+
description: edit.path ? `Edited ${edit.path}` : "Edited file",
|
|
154
|
+
diff: edit.diff,
|
|
155
|
+
...edit.path ? { path: edit.path } : {}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return messages;
|
|
159
|
+
}
|
|
160
|
+
case "result": {
|
|
161
|
+
const messages = [];
|
|
162
|
+
const usage = event.usage;
|
|
163
|
+
if (usage && typeof usage === "object") {
|
|
164
|
+
messages.push({ type: "token-count", ...usage });
|
|
165
|
+
}
|
|
166
|
+
const isError = event.is_error === true;
|
|
167
|
+
messages.push({
|
|
168
|
+
type: "status",
|
|
169
|
+
status: isError ? "error" : "idle",
|
|
170
|
+
detail: typeof event.subtype === "string" ? event.subtype : void 0
|
|
171
|
+
});
|
|
172
|
+
return messages;
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function drainJsonLines(buffer) {
|
|
179
|
+
const events = [];
|
|
180
|
+
const lines = buffer.split("\n");
|
|
181
|
+
const rest = lines.pop() ?? "";
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const trimmed = line.trim();
|
|
184
|
+
if (trimmed.length === 0) continue;
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(trimmed);
|
|
187
|
+
if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
|
|
188
|
+
events.push(parsed);
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { events, rest };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const CANCEL_GRACE_MS = 2e3;
|
|
197
|
+
class CursorCliBackend {
|
|
198
|
+
options;
|
|
199
|
+
handlers = /* @__PURE__ */ new Set();
|
|
200
|
+
/** stdin is closed: the CLI takes its prompt as an argument, not on stdin. */
|
|
201
|
+
child = null;
|
|
202
|
+
/** Cursor's own chat id, used to resume on the next turn. */
|
|
203
|
+
chatId = null;
|
|
204
|
+
stdoutBuffer = "";
|
|
205
|
+
turnComplete = Promise.resolve();
|
|
206
|
+
resolveTurn = null;
|
|
207
|
+
disposed = false;
|
|
208
|
+
constructor(options) {
|
|
209
|
+
this.options = options;
|
|
210
|
+
}
|
|
211
|
+
onMessage(handler) {
|
|
212
|
+
this.handlers.add(handler);
|
|
213
|
+
}
|
|
214
|
+
offMessage(handler) {
|
|
215
|
+
this.handlers.delete(handler);
|
|
216
|
+
}
|
|
217
|
+
emit(message) {
|
|
218
|
+
for (const handler of this.handlers) {
|
|
219
|
+
try {
|
|
220
|
+
handler(message);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
logger.logger.debug(`[cursor] message handler threw: ${error}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async startSession(initialPrompt) {
|
|
227
|
+
if (this.disposed) throw new Error("CursorCliBackend has been disposed");
|
|
228
|
+
if (initialPrompt !== void 0) {
|
|
229
|
+
await this.runTurn(initialPrompt);
|
|
230
|
+
}
|
|
231
|
+
return { sessionId: this.chatId ?? "" };
|
|
232
|
+
}
|
|
233
|
+
async sendPrompt(_sessionId, prompt, options) {
|
|
234
|
+
if (options?.images?.length) {
|
|
235
|
+
throw new Error("CursorCliBackend does not support image prompts yet");
|
|
236
|
+
}
|
|
237
|
+
await this.runTurn(prompt);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* One turn is one child process: the CLI's print mode runs a single prompt
|
|
241
|
+
* and exits. Continuity comes from `--resume <chatId>`, which is why the
|
|
242
|
+
* chat id from the first turn has to be kept.
|
|
243
|
+
*/
|
|
244
|
+
async runTurn(prompt) {
|
|
245
|
+
if (this.child) {
|
|
246
|
+
throw new Error("CursorCliBackend is already running a turn");
|
|
247
|
+
}
|
|
248
|
+
const args = ["-p", prompt, "--output-format", "stream-json", "--trust"];
|
|
249
|
+
if (this.options.model) args.push("--model", this.options.model);
|
|
250
|
+
if (this.chatId) args.push("--resume", this.chatId);
|
|
251
|
+
if (this.options.extraArgs?.length) args.push(...this.options.extraArgs);
|
|
252
|
+
const command = this.options.command ?? "cursor-agent";
|
|
253
|
+
logger.logger.debug(`[cursor] spawn ${command} ${this.chatId ? `(resume ${this.chatId})` : "(new chat)"}`);
|
|
254
|
+
this.emit({ type: "status", status: "starting" });
|
|
255
|
+
this.stdoutBuffer = "";
|
|
256
|
+
this.turnComplete = new Promise((resolve) => {
|
|
257
|
+
this.resolveTurn = resolve;
|
|
258
|
+
});
|
|
259
|
+
const child = node_child_process.spawn(command, args, {
|
|
260
|
+
cwd: this.options.cwd,
|
|
261
|
+
env: { ...process.env, ...this.options.env },
|
|
262
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
263
|
+
});
|
|
264
|
+
this.child = child;
|
|
265
|
+
child.stdout.setEncoding("utf8");
|
|
266
|
+
child.stdout.on("data", (chunk) => this.consumeStdout(chunk));
|
|
267
|
+
let lastStderr = "";
|
|
268
|
+
child.stderr.setEncoding("utf8");
|
|
269
|
+
child.stderr.on("data", (chunk) => {
|
|
270
|
+
const text = chunk.trim();
|
|
271
|
+
if (text.length === 0) return;
|
|
272
|
+
lastStderr = text.split("\n").slice(-3).join(" ").slice(0, 300);
|
|
273
|
+
logger.logger.debug(`[cursor] stderr: ${text}`);
|
|
274
|
+
});
|
|
275
|
+
child.on("error", (error) => {
|
|
276
|
+
this.emit({ type: "status", status: "error", detail: `failed to start cursor-agent: ${error.message}` });
|
|
277
|
+
this.finishTurn();
|
|
278
|
+
});
|
|
279
|
+
child.on("close", (code) => {
|
|
280
|
+
this.consumeStdout("\n");
|
|
281
|
+
if (code !== 0) {
|
|
282
|
+
const reason = lastStderr.length > 0 ? `: ${lastStderr}` : "";
|
|
283
|
+
this.emit({ type: "status", status: "error", detail: `cursor-agent exited with code ${code}${reason}` });
|
|
284
|
+
}
|
|
285
|
+
logger.logger.debug(`[cursor] turn finished with code ${code}`);
|
|
286
|
+
this.finishTurn();
|
|
287
|
+
});
|
|
288
|
+
await this.turnComplete;
|
|
289
|
+
}
|
|
290
|
+
consumeStdout(chunk) {
|
|
291
|
+
this.stdoutBuffer += chunk;
|
|
292
|
+
const { events, rest } = drainJsonLines(this.stdoutBuffer);
|
|
293
|
+
this.stdoutBuffer = rest;
|
|
294
|
+
for (const event of events) {
|
|
295
|
+
const init = parseCursorSessionInit(event);
|
|
296
|
+
if (init) {
|
|
297
|
+
this.chatId = init.sessionId;
|
|
298
|
+
logger.logger.debug(`[cursor] chat id ${init.sessionId} model ${init.model ?? "(default)"}`);
|
|
299
|
+
}
|
|
300
|
+
for (const message of mapCursorStreamEvent(event)) {
|
|
301
|
+
this.emit(message);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
finishTurn() {
|
|
306
|
+
this.child = null;
|
|
307
|
+
const resolve = this.resolveTurn;
|
|
308
|
+
this.resolveTurn = null;
|
|
309
|
+
resolve?.();
|
|
310
|
+
}
|
|
311
|
+
async cancel(_sessionId) {
|
|
312
|
+
const child = this.child;
|
|
313
|
+
if (!child) return;
|
|
314
|
+
logger.logger.debug("[cursor] cancelling current turn");
|
|
315
|
+
child.kill("SIGTERM");
|
|
316
|
+
const killed = await Promise.race([
|
|
317
|
+
this.turnComplete.then(() => true),
|
|
318
|
+
new Promise((resolve) => setTimeout(() => resolve(false), CANCEL_GRACE_MS))
|
|
319
|
+
]);
|
|
320
|
+
if (!killed && this.child) {
|
|
321
|
+
this.child.kill("SIGKILL");
|
|
322
|
+
}
|
|
323
|
+
this.emit({ type: "status", status: "stopped", detail: "cancelled" });
|
|
324
|
+
}
|
|
325
|
+
async waitForResponseComplete(timeoutMs = 12e4) {
|
|
326
|
+
await Promise.race([
|
|
327
|
+
this.turnComplete,
|
|
328
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("cursor-agent turn timed out")), timeoutMs))
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
async dispose() {
|
|
332
|
+
this.disposed = true;
|
|
333
|
+
if (this.child) {
|
|
334
|
+
await this.cancel(this.chatId ?? "");
|
|
335
|
+
}
|
|
336
|
+
this.handlers.clear();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function agentMessageToAcp(message, newId) {
|
|
341
|
+
switch (message.type) {
|
|
342
|
+
case "model-output": {
|
|
343
|
+
const text = message.fullText ?? message.textDelta;
|
|
344
|
+
if (!text) return [];
|
|
345
|
+
return [{ type: "message", message: text }];
|
|
346
|
+
}
|
|
347
|
+
case "event": {
|
|
348
|
+
if (message.name !== "thinking") return [];
|
|
349
|
+
const payload = message.payload;
|
|
350
|
+
const text = payload && typeof payload.textDelta === "string" ? payload.textDelta : null;
|
|
351
|
+
if (!text) return [];
|
|
352
|
+
return [{ type: "thinking", text }];
|
|
353
|
+
}
|
|
354
|
+
case "tool-call":
|
|
355
|
+
return [{
|
|
356
|
+
type: "tool-call",
|
|
357
|
+
callId: message.callId,
|
|
358
|
+
name: message.toolName,
|
|
359
|
+
input: message.args,
|
|
360
|
+
id: newId()
|
|
361
|
+
}];
|
|
362
|
+
case "tool-result":
|
|
363
|
+
return [{
|
|
364
|
+
type: "tool-result",
|
|
365
|
+
callId: message.callId,
|
|
366
|
+
output: message.result,
|
|
367
|
+
id: newId()
|
|
368
|
+
}];
|
|
369
|
+
case "fs-edit":
|
|
370
|
+
return [{
|
|
371
|
+
type: "file-edit",
|
|
372
|
+
description: message.description,
|
|
373
|
+
// The app keys file edits by path; an edit without one would
|
|
374
|
+
// render as an orphan card, so fall back to a visible marker.
|
|
375
|
+
filePath: message.path ?? "(unknown path)",
|
|
376
|
+
...message.diff ? { diff: message.diff } : {},
|
|
377
|
+
id: newId()
|
|
378
|
+
}];
|
|
379
|
+
case "terminal-output":
|
|
380
|
+
return [{ type: "terminal-output", data: message.data, callId: newId() }];
|
|
381
|
+
case "token-count": {
|
|
382
|
+
const { type: _type, ...rest } = message;
|
|
383
|
+
return [{ type: "token_count", ...rest }];
|
|
384
|
+
}
|
|
385
|
+
case "status": {
|
|
386
|
+
switch (message.status) {
|
|
387
|
+
case "starting":
|
|
388
|
+
return [{ type: "task_started", id: newId() }];
|
|
389
|
+
case "idle":
|
|
390
|
+
return [{ type: "task_complete", id: newId() }];
|
|
391
|
+
case "stopped":
|
|
392
|
+
return [{ type: "turn_aborted", id: newId() }];
|
|
393
|
+
case "error":
|
|
394
|
+
return [
|
|
395
|
+
{ type: "message", message: message.detail ?? "cursor-agent failed" },
|
|
396
|
+
{ type: "task_complete", id: newId() }
|
|
397
|
+
];
|
|
398
|
+
default:
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
default:
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const ALWAYS_ALLOWED_TOOLS = ["change_title", "happy__change_title", "preview_html", "happy__preview_html"];
|
|
408
|
+
class CursorPermissionHandler extends setupOfflineReconnection.BasePermissionHandler {
|
|
409
|
+
/** Set once the user answers "approve for session" or picks a yolo mode. */
|
|
410
|
+
approveEverything = false;
|
|
411
|
+
constructor(session, pushClient) {
|
|
412
|
+
super(session, pushClient);
|
|
413
|
+
}
|
|
414
|
+
getLogPrefix() {
|
|
415
|
+
return "[cursor]";
|
|
416
|
+
}
|
|
417
|
+
getAgentName() {
|
|
418
|
+
return "Cursor";
|
|
419
|
+
}
|
|
420
|
+
/** Skip the round trip for tools that are part of Happy's own plumbing. */
|
|
421
|
+
shouldAutoApprove(toolName) {
|
|
422
|
+
const lower = toolName.toLowerCase();
|
|
423
|
+
return ALWAYS_ALLOWED_TOOLS.some((name) => lower.includes(name.toLowerCase()));
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Blocks until the phone answers. The hook process on the other end is
|
|
427
|
+
* happy to wait — Cursor imposes no deadline of its own beyond the
|
|
428
|
+
* `timeout` we write into hooks.json.
|
|
429
|
+
*/
|
|
430
|
+
async requestApproval(toolCallId, toolName, input) {
|
|
431
|
+
if (this.approveEverything || this.shouldAutoApprove(toolName)) {
|
|
432
|
+
logger.logger.debug(`${this.getLogPrefix()} auto-approving ${toolName}`);
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
const result = await new Promise((resolve, reject) => {
|
|
436
|
+
this.pendingRequests.set(toolCallId, { resolve, reject, toolName, input });
|
|
437
|
+
this.addPendingRequestToState(toolCallId, toolName, input);
|
|
438
|
+
logger.logger.debug(`${this.getLogPrefix()} awaiting approval for ${toolName} (${toolCallId})`);
|
|
439
|
+
});
|
|
440
|
+
if (result.decision === "approved_for_session") {
|
|
441
|
+
this.approveEverything = true;
|
|
442
|
+
}
|
|
443
|
+
return result.decision === "approved" || result.decision === "approved_for_session";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const KEEP_ALIVE_INTERVAL_MS = 2e3;
|
|
448
|
+
const SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
449
|
+
async function runCursor(opts) {
|
|
450
|
+
const api$1 = await api.ApiClient.create(opts.credentials);
|
|
451
|
+
const settings = await logger.readSettings();
|
|
452
|
+
const machineId = settings?.machineId;
|
|
453
|
+
if (!machineId) {
|
|
454
|
+
console.error(chalk.red("No machine ID found in settings. Run `happy` once to finish setup."));
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
await api$1.getOrCreateMachine({ machineId, metadata: run.initialMachineMetadata });
|
|
458
|
+
const { state, metadata } = setupOfflineReconnection.createSessionMetadata({
|
|
459
|
+
flavor: "cursor",
|
|
460
|
+
machineId,
|
|
461
|
+
startedBy: opts.startedBy
|
|
462
|
+
});
|
|
463
|
+
const sessionTag = node_crypto.randomUUID();
|
|
464
|
+
const response = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
465
|
+
let session;
|
|
466
|
+
const { session: initialSession } = setupOfflineReconnection.setupOfflineReconnection({
|
|
467
|
+
api: api$1,
|
|
468
|
+
sessionTag,
|
|
469
|
+
metadata,
|
|
470
|
+
state,
|
|
471
|
+
response,
|
|
472
|
+
onSessionSwap: (newSession) => {
|
|
473
|
+
session = newSession;
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
session = initialSession;
|
|
477
|
+
const permissionHandler = new CursorPermissionHandler(session, api$1.push());
|
|
478
|
+
const approvalBridge = new cursorApprovalServer.CursorApprovalBridge({ cwd: process.cwd(), handler: permissionHandler });
|
|
479
|
+
let approvalReady = false;
|
|
480
|
+
try {
|
|
481
|
+
await approvalBridge.start();
|
|
482
|
+
approvalReady = true;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
logger.logger.debug(`[cursor] approval bridge failed to start: ${error}`);
|
|
485
|
+
}
|
|
486
|
+
const backend = new CursorCliBackend({
|
|
487
|
+
cwd: process.cwd(),
|
|
488
|
+
model: opts.model ?? null
|
|
489
|
+
});
|
|
490
|
+
const queue = [];
|
|
491
|
+
let draining = false;
|
|
492
|
+
let thinking = false;
|
|
493
|
+
backend.onMessage((message) => {
|
|
494
|
+
if (message.type === "status") {
|
|
495
|
+
thinking = message.status === "starting" || message.status === "running";
|
|
496
|
+
}
|
|
497
|
+
for (const acp of agentMessageToAcp(message, () => node_crypto.randomUUID())) {
|
|
498
|
+
session.sendAgentMessage("cursor", acp);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
const drain = async () => {
|
|
502
|
+
if (draining) return;
|
|
503
|
+
draining = true;
|
|
504
|
+
try {
|
|
505
|
+
while (queue.length > 0) {
|
|
506
|
+
const prompt = queue.shift();
|
|
507
|
+
logger.logger.debug(`[cursor] running turn (${queue.length} queued behind it)`);
|
|
508
|
+
try {
|
|
509
|
+
await backend.sendPrompt("", prompt);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
512
|
+
session.sendAgentMessage("cursor", { type: "message", message: `Cursor turn failed: ${detail}` });
|
|
513
|
+
logger.logger.debug(`[cursor] turn failed: ${detail}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} finally {
|
|
517
|
+
draining = false;
|
|
518
|
+
thinking = false;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
session.onUserMessage((message) => {
|
|
522
|
+
const text = message.content?.text;
|
|
523
|
+
if (typeof text !== "string" || text.trim().length === 0) return;
|
|
524
|
+
if (message.meta?.hasOwnProperty("model")) {
|
|
525
|
+
logger.logger.debug("[cursor] per-message model override is not wired yet, ignoring");
|
|
526
|
+
}
|
|
527
|
+
queue.push(text);
|
|
528
|
+
void drain();
|
|
529
|
+
});
|
|
530
|
+
const keepAlive = setInterval(() => {
|
|
531
|
+
session.keepAlive(thinking, "remote");
|
|
532
|
+
}, KEEP_ALIVE_INTERVAL_MS);
|
|
533
|
+
session.sendSessionEvent({ type: "ready" });
|
|
534
|
+
console.log(chalk.green("Cursor session ready.") + " Open Happy on your phone to drive it.");
|
|
535
|
+
if (response?.id) {
|
|
536
|
+
console.log(chalk.gray(` id ${response.id}`));
|
|
537
|
+
} else {
|
|
538
|
+
console.log(chalk.yellow(" id (offline \u2014 reconnecting in the background)"));
|
|
539
|
+
}
|
|
540
|
+
console.log(chalk.gray(` cwd ${process.cwd()}`));
|
|
541
|
+
console.log(chalk.gray(` model ${opts.model ?? "(Cursor default)"}`));
|
|
542
|
+
if (approvalReady) {
|
|
543
|
+
console.log(chalk.gray(" approval tool calls are sent to your phone before they run"));
|
|
544
|
+
} else {
|
|
545
|
+
console.log(chalk.yellow(" approval UNAVAILABLE \u2014 the bridge did not start, so tools run unchecked"));
|
|
546
|
+
}
|
|
547
|
+
if (opts.initialPrompt) {
|
|
548
|
+
queue.push(opts.initialPrompt);
|
|
549
|
+
void drain();
|
|
550
|
+
}
|
|
551
|
+
let shuttingDown = false;
|
|
552
|
+
const shutdown = async () => {
|
|
553
|
+
if (shuttingDown) return;
|
|
554
|
+
shuttingDown = true;
|
|
555
|
+
clearInterval(keepAlive);
|
|
556
|
+
logger.logger.debug("[cursor] shutting down");
|
|
557
|
+
try {
|
|
558
|
+
await backend.dispose();
|
|
559
|
+
await approvalBridge.stop();
|
|
560
|
+
} finally {
|
|
561
|
+
session.sendSessionDeath();
|
|
562
|
+
await session.flush();
|
|
563
|
+
await session.close();
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
const shutdownAndExit = () => {
|
|
567
|
+
void Promise.race([
|
|
568
|
+
shutdown(),
|
|
569
|
+
new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
|
|
570
|
+
]).finally(() => process.exit(0));
|
|
571
|
+
};
|
|
572
|
+
process.on("SIGINT", shutdownAndExit);
|
|
573
|
+
process.on("SIGTERM", shutdownAndExit);
|
|
574
|
+
await new Promise(() => {
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
exports.runCursor = runCursor;
|