@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.
- package/dist/{api-XeHlmApX.cjs → api-B-PlVQY6.cjs} +50 -4
- package/dist/{api-K2-jr9ar.mjs → api-BUCtd3cB.mjs} +51 -5
- package/dist/{auth-BQvEuSs5.cjs → auth-BqGkUXvq.cjs} +2 -2
- package/dist/{auth-CjLkoM3m.mjs → auth-DQH6q_q-.mjs} +4 -4
- package/dist/{auth-BbvPcnkY.cjs → auth-OL3nNcDX.cjs} +4 -4
- package/dist/{auth-DS8fzlin.mjs → auth-qM2eojWx.mjs} +2 -2
- package/dist/{browser-C05SP_Tt.cjs → browser-CY7VNU0C.cjs} +1 -1
- package/dist/{browser-C05Ak6RL.mjs → browser-D5RN_Axa.mjs} +1 -1
- package/dist/{claudeLocal-DJNRlHIC.mjs → claudeLocal-BXK_vQXS.mjs} +1 -1
- package/dist/{claudeLocal-CpDhLCcZ.cjs → claudeLocal-HAXk0deK.cjs} +1 -1
- package/dist/{config-DWWtjKQx.mjs → config-2wMjzcHl.mjs} +1 -1
- package/dist/{config-B0mns9fF.cjs → config-CaKxWGVe.cjs} +1 -1
- package/dist/{connect-DXQC1CJe.mjs → connect-BAunPyp_.mjs} +5 -5
- package/dist/{connect-BxCScVNX.cjs → connect-Dt1vY1BC.cjs} +5 -5
- package/dist/{controlClient-DgjVRN8D.mjs → controlClient-BzzTNSfl.mjs} +1 -1
- package/dist/{controlClient-DgELDZgM.cjs → controlClient-yTkpwcOq.cjs} +1 -1
- package/dist/cursorApprovalHook-B2chnVni.cjs +115 -0
- package/dist/cursorApprovalHook-BDLPzI5l.mjs +113 -0
- package/dist/cursorApprovalServer-Ba1yOUCA.mjs +263 -0
- package/dist/cursorApprovalServer-xwxVGL3u.cjs +268 -0
- package/dist/{deviceKeys-CejCdWpw.mjs → deviceKeys-CQyFFPtt.mjs} +2 -2
- package/dist/{deviceKeys-BpIAWz13.cjs → deviceKeys-CdW-HPnz.cjs} +2 -2
- package/dist/{doctor-DznasgP3.cjs → doctor-D4xWXiE7.cjs} +2 -2
- package/dist/{doctor-B-LCMVN9.mjs → doctor-DDT2TXHH.mjs} +2 -2
- package/dist/downloadImage-3A6zY5AX.cjs +492 -0
- package/dist/downloadImage-BqNp0l7y.mjs +485 -0
- package/dist/{enroll-HjUqyGnD.mjs → enroll-BlZ0giJI.mjs} +3 -3
- package/dist/{enroll-BvfpRUGJ.cjs → enroll-DeYs89Ic.cjs} +3 -3
- package/dist/{evidence-CVgTjBrq.cjs → evidence-Bn1JXtdz.cjs} +1 -1
- package/dist/{evidence-BEQd1dCh.mjs → evidence-PUnN9yPK.mjs} +1 -1
- package/dist/{formatImageMessage-DVs57t3x.mjs → formatImageMessage-BRHeLf0-.mjs} +1 -1
- package/dist/{formatImageMessage-CmJTP_hU.cjs → formatImageMessage-bVFGFDJl.cjs} +1 -1
- package/dist/{index-CDTNgfUf.cjs → index-BeKrqPYR.cjs} +25 -1
- package/dist/{index-CPKdvi7J.mjs → index-DTV7drqs.mjs} +25 -1
- package/dist/index.cjs +95 -41
- package/dist/index.mjs +95 -41
- package/dist/{install-UTz_vYvB.mjs → install-BLPNuSeM.mjs} +2 -2
- package/dist/{install-B3mMGISS.mjs → install-BOIcR_Wr.mjs} +1 -1
- package/dist/{install-Ck22tYHP.cjs → install-Jrs6YUu8.cjs} +1 -1
- package/dist/{install-2Yl_L582.cjs → install-Q-YMwwwg.cjs} +2 -2
- package/dist/lib.cjs +4 -4
- package/dist/lib.d.cts +4 -2
- package/dist/lib.d.mts +4 -2
- package/dist/lib.mjs +4 -4
- package/dist/{loadableToolOutput-BZYlaqHn.cjs → loadableToolOutput-B3qpMqXT.cjs} +2 -458
- package/dist/{loadableToolOutput-4EvraaNY.mjs → loadableToolOutput-DzReWITb.mjs} +3 -456
- package/dist/{persistence-DrfN7TzY.cjs → persistence-D7zNFI-d.cjs} +7 -4
- package/dist/{persistence-CIUFyOwz.mjs → persistence-DbLMvLvk.mjs} +7 -4
- package/dist/{push-CCOXCcM_.cjs → push-RT6Y0Hr3.cjs} +6 -6
- package/dist/{push-USXj_pv0.mjs → push-z5wQ4Ki_.mjs} +6 -6
- package/dist/{downloadImage-ia5SzlCV.mjs → registerKillSessionHandler-DmfnJ0zq.mjs} +7 -487
- package/dist/{downloadImage-B_ygLAXY.cjs → registerKillSessionHandler-MoLdSQxM.cjs} +6 -492
- package/dist/{run-Dy6KX6m-.mjs → run-BFnSUK1o.mjs} +14 -9
- package/dist/{run-D3CopmEb.cjs → run-C5StYRIB.cjs} +14 -9
- package/dist/{runClaude-aHzvx6Ha.cjs → runClaude-ChRp6f5G.cjs} +27 -26
- package/dist/{runClaude-CWux2RDq.mjs → runClaude-CjENBdrX.mjs} +22 -21
- package/dist/{runCodex-D0ZxSj3O.mjs → runCodex-BzQWlRoj.mjs} +27 -25
- package/dist/{runCodex-G-Aq531U.cjs → runCodex-DcQGyotH.cjs} +32 -30
- package/dist/runCursor-B73z6MCN.mjs +752 -0
- package/dist/runCursor-BlPAxaTy.cjs +754 -0
- package/dist/{runGemini-DZC2i6vA.mjs → runGemini-BPv4UzuF.mjs} +18 -16
- package/dist/{runGemini-DxGgDAIM.cjs → runGemini-JFqaIC3u.cjs} +23 -21
- package/dist/{runOneShot-BgT-JNMA.cjs → runOneShot-2iV11e9L.cjs} +3 -3
- package/dist/{runOneShot-cxlk7VKA.mjs → runOneShot-DTukim2A.mjs} +3 -3
- package/dist/{serve-Dr3wPrtK.cjs → serve-BDwuAIK4.cjs} +7 -7
- package/dist/{serve-CKyvJNs0.mjs → serve-BnlYj5ek.mjs} +7 -7
- package/dist/{session-dYzbOo_W.cjs → session-Cp9LEESM.cjs} +5 -5
- package/dist/{session-vf2ZjFto.mjs → session-gfeFU8CA.mjs} +5 -5
- package/dist/{sessionKeyCache-dDquDzn0.cjs → sessionKeyCache-3Pme04yL.cjs} +1 -1
- package/dist/{sessionKeyCache-D2u3JUys.mjs → sessionKeyCache-DtBS_Sb3.mjs} +1 -1
- package/dist/{sessionList-C1k2PNg_.mjs → sessionList-B1w4BUdv.mjs} +2 -2
- package/dist/{sessionList-DmmtC7uR.cjs → sessionList-BR_5UDZJ.cjs} +2 -2
- package/dist/{sessionSummary-DGLoVOxd.cjs → sessionSummary-DLweYgHO.cjs} +3 -3
- package/dist/{sessionSummary-5a1QtFVS.mjs → sessionSummary-E-a2bg8W.mjs} +3 -3
- package/dist/setupOfflineReconnection-DHwthFtd.mjs +459 -0
- package/dist/setupOfflineReconnection-Dkp00dC9.cjs +463 -0
- package/dist/{spawnHappyCLI-CarRI4GM.cjs → spawnHappyCLI-C2uY9WV-.cjs} +1 -1
- package/dist/{spawnHappyCLI-D3EkTFIn.mjs → spawnHappyCLI-Dt7fbmbk.mjs} +1 -1
- package/dist/{ssh-B0hvEYIl.mjs → ssh-C-nEZiGP.mjs} +5 -5
- package/dist/{ssh-CBgVwKsB.cjs → ssh-CsnS89Lr.cjs} +5 -5
- package/dist/{task-CRo6W-3_.mjs → task-B-8_QGGX.mjs} +6 -6
- package/dist/{task-CMunPKCV.cjs → task-lrLb4FCL.cjs} +6 -6
- package/dist/{uninstall-BALE2up0.mjs → uninstall-CirMt_WC.mjs} +2 -2
- package/dist/{uninstall-BJ3eEGM4.cjs → uninstall-DMjUlEXT.cjs} +2 -2
- package/dist/{update-BfjtLrhO.mjs → update-CfXbYshs.mjs} +3 -3
- package/dist/{update-Bb3iijy8.cjs → update-mx2p28IP.cjs} +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
|
+
var chalk = require('chalk');
|
|
5
|
+
var api = require('./api-B-PlVQY6.cjs');
|
|
6
|
+
var logger = require('./persistence-D7zNFI-d.cjs');
|
|
7
|
+
var run = require('./run-C5StYRIB.cjs');
|
|
8
|
+
var setupOfflineReconnection = require('./setupOfflineReconnection-Dkp00dC9.cjs');
|
|
9
|
+
var controlClient = require('./controlClient-yTkpwcOq.cjs');
|
|
10
|
+
var registerKillSessionHandler = require('./registerKillSessionHandler-MoLdSQxM.cjs');
|
|
11
|
+
var node_child_process = require('node:child_process');
|
|
12
|
+
var cursorApprovalServer = require('./cursorApprovalServer-xwxVGL3u.cjs');
|
|
13
|
+
require('axios');
|
|
14
|
+
require('node:events');
|
|
15
|
+
require('socket.io-client');
|
|
16
|
+
require('zod');
|
|
17
|
+
require('./index-BeKrqPYR.cjs');
|
|
18
|
+
require('@paralleldrive/cuid2');
|
|
19
|
+
require('./time-Bj2HRkKH.cjs');
|
|
20
|
+
require('node:fs');
|
|
21
|
+
require('node:path');
|
|
22
|
+
require('node:fs/promises');
|
|
23
|
+
require('node:os');
|
|
24
|
+
require('node:util');
|
|
25
|
+
require('child_process');
|
|
26
|
+
require('util');
|
|
27
|
+
require('fs/promises');
|
|
28
|
+
require('path');
|
|
29
|
+
require('./projectPath-ChLhdOyt.cjs');
|
|
30
|
+
require('url');
|
|
31
|
+
require('node:module');
|
|
32
|
+
require('ws');
|
|
33
|
+
require('crypto');
|
|
34
|
+
require('tweetnacl');
|
|
35
|
+
require('@stablelib/base64');
|
|
36
|
+
require('node:readline');
|
|
37
|
+
require('expo-server-sdk');
|
|
38
|
+
require('./sessionKeyCache-3Pme04yL.cjs');
|
|
39
|
+
require('fs');
|
|
40
|
+
require('os');
|
|
41
|
+
require('tmp');
|
|
42
|
+
require('./auth-BqGkUXvq.cjs');
|
|
43
|
+
require('qrcode-terminal');
|
|
44
|
+
require('./browser-CY7VNU0C.cjs');
|
|
45
|
+
require('open');
|
|
46
|
+
require('react');
|
|
47
|
+
require('ink');
|
|
48
|
+
require('./doctor-D4xWXiE7.cjs');
|
|
49
|
+
require('./doctor-C08m5Z9f.cjs');
|
|
50
|
+
require('ps-list');
|
|
51
|
+
require('cross-spawn');
|
|
52
|
+
require('./spawnHappyCLI-C2uY9WV-.cjs');
|
|
53
|
+
require('./deviceKeys-CdW-HPnz.cjs');
|
|
54
|
+
require('./install-Jrs6YUu8.cjs');
|
|
55
|
+
require('./trimIdent-4gvk9Wk6.cjs');
|
|
56
|
+
require('fastify');
|
|
57
|
+
require('fastify-type-provider-zod');
|
|
58
|
+
require('./common-CZvSRU4p.cjs');
|
|
59
|
+
require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
60
|
+
require('node:http');
|
|
61
|
+
require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
62
|
+
require('@modelcontextprotocol/sdk/types.js');
|
|
63
|
+
require('./prompt-BTWDZZO0.cjs');
|
|
64
|
+
require('./mcpToolSchemas-DK42yoZ_.cjs');
|
|
65
|
+
require('./types-DWeB7aqh.cjs');
|
|
66
|
+
|
|
67
|
+
function createCursorStreamState() {
|
|
68
|
+
return { thinkingBuffer: "" };
|
|
69
|
+
}
|
|
70
|
+
function textFromMessageContent(message) {
|
|
71
|
+
if (!message || typeof message !== "object") return null;
|
|
72
|
+
const content = message.content;
|
|
73
|
+
if (!Array.isArray(content)) return null;
|
|
74
|
+
const parts = [];
|
|
75
|
+
for (const block of content) {
|
|
76
|
+
if (block && typeof block === "object" && block.type === "text") {
|
|
77
|
+
const text = block.text;
|
|
78
|
+
if (typeof text === "string") parts.push(text);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return parts.length > 0 ? parts.join("") : null;
|
|
82
|
+
}
|
|
83
|
+
function unwrapToolCall(toolCall) {
|
|
84
|
+
if (!toolCall || typeof toolCall !== "object") return null;
|
|
85
|
+
for (const [key, value] of Object.entries(toolCall)) {
|
|
86
|
+
if (!key.endsWith("ToolCall") || !value || typeof value !== "object") continue;
|
|
87
|
+
const inner = value;
|
|
88
|
+
const normalized = cursorApprovalServer.normalizeCursorTool(key.replace(/ToolCall$/, ""), inner.args);
|
|
89
|
+
return {
|
|
90
|
+
toolName: normalized.name,
|
|
91
|
+
args: normalized.input,
|
|
92
|
+
result: inner.result ?? null
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function editDiffFromResult(result) {
|
|
98
|
+
if (!result || typeof result !== "object") return null;
|
|
99
|
+
const success = result.success;
|
|
100
|
+
if (!success || typeof success !== "object") return null;
|
|
101
|
+
const diff = success.diffString;
|
|
102
|
+
if (typeof diff !== "string" || diff.length === 0) return null;
|
|
103
|
+
const path = success.path;
|
|
104
|
+
return { diff, path: typeof path === "string" ? path : null };
|
|
105
|
+
}
|
|
106
|
+
function parseCursorSessionInit(event) {
|
|
107
|
+
if (event.type !== "system" || event.subtype !== "init") return null;
|
|
108
|
+
const sessionId = event.session_id;
|
|
109
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
110
|
+
const str = (key) => typeof event[key] === "string" ? event[key] : null;
|
|
111
|
+
return {
|
|
112
|
+
sessionId,
|
|
113
|
+
model: str("model"),
|
|
114
|
+
cwd: str("cwd"),
|
|
115
|
+
permissionMode: str("permissionMode"),
|
|
116
|
+
apiKeySource: str("apiKeySource")
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function mapCursorStreamEvent(event, state) {
|
|
120
|
+
switch (event.type) {
|
|
121
|
+
case "system": {
|
|
122
|
+
if (event.subtype !== "init") return [];
|
|
123
|
+
const init = parseCursorSessionInit(event);
|
|
124
|
+
return [{
|
|
125
|
+
type: "status",
|
|
126
|
+
status: "running",
|
|
127
|
+
detail: init?.model ? `cursor-agent ready (${init.model})` : "cursor-agent ready"
|
|
128
|
+
}];
|
|
129
|
+
}
|
|
130
|
+
// The user turn is echoed back to us; Happy already has that text from
|
|
131
|
+
// the send path, so re-emitting it would duplicate the bubble.
|
|
132
|
+
case "user":
|
|
133
|
+
return [];
|
|
134
|
+
case "thinking": {
|
|
135
|
+
const text = typeof event.text === "string" ? event.text : "";
|
|
136
|
+
if (event.subtype === "delta") {
|
|
137
|
+
if (!text) return [];
|
|
138
|
+
if (!state) return [{ type: "event", name: "thinking", payload: { textDelta: text } }];
|
|
139
|
+
state.thinkingBuffer += text;
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
if (event.subtype === "completed" && state?.thinkingBuffer) {
|
|
143
|
+
const full = state.thinkingBuffer;
|
|
144
|
+
state.thinkingBuffer = "";
|
|
145
|
+
return [{ type: "event", name: "thinking", payload: { textDelta: full } }];
|
|
146
|
+
}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
case "assistant": {
|
|
150
|
+
const text = textFromMessageContent(event.message);
|
|
151
|
+
if (!text) return [];
|
|
152
|
+
return [{ type: "model-output", fullText: text }];
|
|
153
|
+
}
|
|
154
|
+
case "tool_call": {
|
|
155
|
+
const callId = typeof event.call_id === "string" ? event.call_id : null;
|
|
156
|
+
const unwrapped = unwrapToolCall(event.tool_call);
|
|
157
|
+
if (!callId || !unwrapped) return [];
|
|
158
|
+
if (event.subtype === "started") {
|
|
159
|
+
return [{ type: "tool-call", toolName: unwrapped.toolName, args: unwrapped.args, callId }];
|
|
160
|
+
}
|
|
161
|
+
if (event.subtype !== "completed") return [];
|
|
162
|
+
const messages = [{
|
|
163
|
+
type: "tool-result",
|
|
164
|
+
toolName: unwrapped.toolName,
|
|
165
|
+
result: unwrapped.result,
|
|
166
|
+
callId
|
|
167
|
+
}];
|
|
168
|
+
const edit = editDiffFromResult(unwrapped.result);
|
|
169
|
+
if (edit) {
|
|
170
|
+
messages.push({
|
|
171
|
+
type: "fs-edit",
|
|
172
|
+
description: edit.path ? `Edited ${edit.path}` : "Edited file",
|
|
173
|
+
diff: edit.diff,
|
|
174
|
+
...edit.path ? { path: edit.path } : {}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return messages;
|
|
178
|
+
}
|
|
179
|
+
case "result": {
|
|
180
|
+
const messages = [];
|
|
181
|
+
const usage = event.usage;
|
|
182
|
+
if (usage && typeof usage === "object") {
|
|
183
|
+
messages.push({ type: "token-count", ...usage });
|
|
184
|
+
}
|
|
185
|
+
const isError = event.is_error === true;
|
|
186
|
+
messages.push({
|
|
187
|
+
type: "status",
|
|
188
|
+
status: isError ? "error" : "idle",
|
|
189
|
+
detail: typeof event.subtype === "string" ? event.subtype : void 0
|
|
190
|
+
});
|
|
191
|
+
return messages;
|
|
192
|
+
}
|
|
193
|
+
default:
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function drainJsonLines(buffer) {
|
|
198
|
+
const events = [];
|
|
199
|
+
const lines = buffer.split("\n");
|
|
200
|
+
const rest = lines.pop() ?? "";
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
if (trimmed.length === 0) continue;
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(trimmed);
|
|
206
|
+
if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
|
|
207
|
+
events.push(parsed);
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { events, rest };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const CANCEL_GRACE_MS = 2e3;
|
|
216
|
+
const THINKING_FLUSH_MS = 800;
|
|
217
|
+
class CursorCliBackend {
|
|
218
|
+
options;
|
|
219
|
+
handlers = /* @__PURE__ */ new Set();
|
|
220
|
+
/** stdin is closed: the CLI takes its prompt as an argument, not on stdin. */
|
|
221
|
+
child = null;
|
|
222
|
+
/** Cursor's own chat id, used to resume on the next turn. */
|
|
223
|
+
chatId = null;
|
|
224
|
+
/** Current model id; null leaves the choice to Cursor. */
|
|
225
|
+
model = null;
|
|
226
|
+
stdoutBuffer = "";
|
|
227
|
+
streamState = createCursorStreamState();
|
|
228
|
+
thinkingFlushTimer = null;
|
|
229
|
+
turnComplete = Promise.resolve();
|
|
230
|
+
resolveTurn = null;
|
|
231
|
+
disposed = false;
|
|
232
|
+
constructor(options) {
|
|
233
|
+
this.options = options;
|
|
234
|
+
this.chatId = options.resumeChatId ?? null;
|
|
235
|
+
this.model = options.model ?? null;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The app sends the chosen model with every message, so it can change
|
|
239
|
+
* between turns. Each turn spawns its own process, which is where the new
|
|
240
|
+
* value takes effect.
|
|
241
|
+
*/
|
|
242
|
+
setModel(model) {
|
|
243
|
+
this.model = model;
|
|
244
|
+
}
|
|
245
|
+
/** Cursor's own chat id, once it is known. */
|
|
246
|
+
getChatId() {
|
|
247
|
+
return this.chatId;
|
|
248
|
+
}
|
|
249
|
+
onMessage(handler) {
|
|
250
|
+
this.handlers.add(handler);
|
|
251
|
+
}
|
|
252
|
+
offMessage(handler) {
|
|
253
|
+
this.handlers.delete(handler);
|
|
254
|
+
}
|
|
255
|
+
emit(message) {
|
|
256
|
+
for (const handler of this.handlers) {
|
|
257
|
+
try {
|
|
258
|
+
handler(message);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
logger.logger.debug(`[cursor] message handler threw: ${error}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async startSession(initialPrompt) {
|
|
265
|
+
if (this.disposed) throw new Error("CursorCliBackend has been disposed");
|
|
266
|
+
if (initialPrompt !== void 0) {
|
|
267
|
+
await this.runTurn(initialPrompt);
|
|
268
|
+
}
|
|
269
|
+
return { sessionId: this.chatId ?? "" };
|
|
270
|
+
}
|
|
271
|
+
async sendPrompt(_sessionId, prompt, options) {
|
|
272
|
+
if (options?.images?.length) {
|
|
273
|
+
throw new Error("CursorCliBackend does not support image prompts yet");
|
|
274
|
+
}
|
|
275
|
+
await this.runTurn(prompt);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* One turn is one child process: the CLI's print mode runs a single prompt
|
|
279
|
+
* and exits. Continuity comes from `--resume <chatId>`, which is why the
|
|
280
|
+
* chat id from the first turn has to be kept.
|
|
281
|
+
*/
|
|
282
|
+
async runTurn(prompt) {
|
|
283
|
+
if (this.child) {
|
|
284
|
+
throw new Error("CursorCliBackend is already running a turn");
|
|
285
|
+
}
|
|
286
|
+
const args = ["-p", prompt, "--output-format", "stream-json", "--trust", "--force"];
|
|
287
|
+
if (this.model) args.push("--model", this.model);
|
|
288
|
+
if (this.chatId) args.push("--resume", this.chatId);
|
|
289
|
+
if (this.options.extraArgs?.length) args.push(...this.options.extraArgs);
|
|
290
|
+
const command = this.options.command ?? "cursor-agent";
|
|
291
|
+
logger.logger.debug(`[cursor] spawn ${command} model=${this.model ?? "(Cursor default)"} ${this.chatId ? `(resume ${this.chatId})` : "(new chat)"}`);
|
|
292
|
+
this.emit({ type: "status", status: "starting" });
|
|
293
|
+
this.stdoutBuffer = "";
|
|
294
|
+
this.flushThinking();
|
|
295
|
+
this.streamState = createCursorStreamState();
|
|
296
|
+
this.turnComplete = new Promise((resolve) => {
|
|
297
|
+
this.resolveTurn = resolve;
|
|
298
|
+
});
|
|
299
|
+
const child = node_child_process.spawn(command, args, {
|
|
300
|
+
cwd: this.options.cwd,
|
|
301
|
+
env: { ...process.env, ...this.options.env },
|
|
302
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
303
|
+
});
|
|
304
|
+
this.child = child;
|
|
305
|
+
child.stdout.setEncoding("utf8");
|
|
306
|
+
child.stdout.on("data", (chunk) => this.consumeStdout(chunk));
|
|
307
|
+
let lastStderr = "";
|
|
308
|
+
child.stderr.setEncoding("utf8");
|
|
309
|
+
child.stderr.on("data", (chunk) => {
|
|
310
|
+
const text = chunk.trim();
|
|
311
|
+
if (text.length === 0) return;
|
|
312
|
+
lastStderr = text.split("\n").slice(-3).join(" ").slice(0, 300);
|
|
313
|
+
logger.logger.debug(`[cursor] stderr: ${text}`);
|
|
314
|
+
});
|
|
315
|
+
child.on("error", (error) => {
|
|
316
|
+
this.emit({ type: "status", status: "error", detail: `failed to start cursor-agent: ${error.message}` });
|
|
317
|
+
this.finishTurn();
|
|
318
|
+
});
|
|
319
|
+
child.on("close", (code) => {
|
|
320
|
+
this.consumeStdout("\n");
|
|
321
|
+
this.flushThinking();
|
|
322
|
+
if (code !== 0) {
|
|
323
|
+
const reason = lastStderr.length > 0 ? `: ${lastStderr}` : "";
|
|
324
|
+
this.emit({ type: "status", status: "error", detail: `cursor-agent exited with code ${code}${reason}` });
|
|
325
|
+
}
|
|
326
|
+
logger.logger.debug(`[cursor] turn finished with code ${code}`);
|
|
327
|
+
this.finishTurn();
|
|
328
|
+
});
|
|
329
|
+
await this.turnComplete;
|
|
330
|
+
}
|
|
331
|
+
/** Emits whatever reasoning has accumulated, if any. */
|
|
332
|
+
flushThinking() {
|
|
333
|
+
if (this.thinkingFlushTimer) {
|
|
334
|
+
clearTimeout(this.thinkingFlushTimer);
|
|
335
|
+
this.thinkingFlushTimer = null;
|
|
336
|
+
}
|
|
337
|
+
const text = this.streamState.thinkingBuffer;
|
|
338
|
+
if (!text) return;
|
|
339
|
+
this.streamState.thinkingBuffer = "";
|
|
340
|
+
this.emit({ type: "event", name: "thinking", payload: { textDelta: text } });
|
|
341
|
+
}
|
|
342
|
+
consumeStdout(chunk) {
|
|
343
|
+
this.stdoutBuffer += chunk;
|
|
344
|
+
const { events, rest } = drainJsonLines(this.stdoutBuffer);
|
|
345
|
+
this.stdoutBuffer = rest;
|
|
346
|
+
for (const event of events) {
|
|
347
|
+
const init = parseCursorSessionInit(event);
|
|
348
|
+
if (init) {
|
|
349
|
+
this.chatId = init.sessionId;
|
|
350
|
+
logger.logger.debug(`[cursor] chat id ${init.sessionId} model ${init.model ?? "(default)"}`);
|
|
351
|
+
}
|
|
352
|
+
for (const message of mapCursorStreamEvent(event, this.streamState)) {
|
|
353
|
+
if (message.type === "event" && message.name === "thinking" && this.thinkingFlushTimer) {
|
|
354
|
+
clearTimeout(this.thinkingFlushTimer);
|
|
355
|
+
this.thinkingFlushTimer = null;
|
|
356
|
+
}
|
|
357
|
+
this.emit(message);
|
|
358
|
+
}
|
|
359
|
+
if (this.streamState.thinkingBuffer && !this.thinkingFlushTimer) {
|
|
360
|
+
this.thinkingFlushTimer = setTimeout(() => {
|
|
361
|
+
this.thinkingFlushTimer = null;
|
|
362
|
+
this.flushThinking();
|
|
363
|
+
}, THINKING_FLUSH_MS);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
finishTurn() {
|
|
368
|
+
this.child = null;
|
|
369
|
+
const resolve = this.resolveTurn;
|
|
370
|
+
this.resolveTurn = null;
|
|
371
|
+
resolve?.();
|
|
372
|
+
}
|
|
373
|
+
async cancel(_sessionId) {
|
|
374
|
+
const child = this.child;
|
|
375
|
+
if (!child) return;
|
|
376
|
+
logger.logger.debug("[cursor] cancelling current turn");
|
|
377
|
+
child.kill("SIGTERM");
|
|
378
|
+
const killed = await Promise.race([
|
|
379
|
+
this.turnComplete.then(() => true),
|
|
380
|
+
new Promise((resolve) => setTimeout(() => resolve(false), CANCEL_GRACE_MS))
|
|
381
|
+
]);
|
|
382
|
+
if (!killed && this.child) {
|
|
383
|
+
this.child.kill("SIGKILL");
|
|
384
|
+
}
|
|
385
|
+
this.emit({ type: "status", status: "stopped", detail: "cancelled" });
|
|
386
|
+
}
|
|
387
|
+
async waitForResponseComplete(timeoutMs = 12e4) {
|
|
388
|
+
await Promise.race([
|
|
389
|
+
this.turnComplete,
|
|
390
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("cursor-agent turn timed out")), timeoutMs))
|
|
391
|
+
]);
|
|
392
|
+
}
|
|
393
|
+
async dispose() {
|
|
394
|
+
this.disposed = true;
|
|
395
|
+
if (this.child) {
|
|
396
|
+
await this.cancel(this.chatId ?? "");
|
|
397
|
+
}
|
|
398
|
+
this.handlers.clear();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function agentMessageToAcp(message, newId) {
|
|
403
|
+
switch (message.type) {
|
|
404
|
+
case "model-output": {
|
|
405
|
+
const text = message.fullText ?? message.textDelta;
|
|
406
|
+
if (!text) return [];
|
|
407
|
+
return [{ type: "message", message: text }];
|
|
408
|
+
}
|
|
409
|
+
case "event": {
|
|
410
|
+
if (message.name !== "thinking") return [];
|
|
411
|
+
const payload = message.payload;
|
|
412
|
+
const text = payload && typeof payload.textDelta === "string" ? payload.textDelta : null;
|
|
413
|
+
if (!text) return [];
|
|
414
|
+
return [{ type: "thinking", text }];
|
|
415
|
+
}
|
|
416
|
+
case "tool-call":
|
|
417
|
+
return [{
|
|
418
|
+
type: "tool-call",
|
|
419
|
+
callId: message.callId,
|
|
420
|
+
name: message.toolName,
|
|
421
|
+
input: message.args,
|
|
422
|
+
id: newId()
|
|
423
|
+
}];
|
|
424
|
+
case "tool-result":
|
|
425
|
+
return [{
|
|
426
|
+
type: "tool-result",
|
|
427
|
+
callId: message.callId,
|
|
428
|
+
output: message.result,
|
|
429
|
+
id: newId()
|
|
430
|
+
}];
|
|
431
|
+
case "fs-edit":
|
|
432
|
+
return [{
|
|
433
|
+
type: "file-edit",
|
|
434
|
+
description: message.description,
|
|
435
|
+
// The app keys file edits by path; an edit without one would
|
|
436
|
+
// render as an orphan card, so fall back to a visible marker.
|
|
437
|
+
filePath: message.path ?? "(unknown path)",
|
|
438
|
+
...message.diff ? { diff: message.diff } : {},
|
|
439
|
+
id: newId()
|
|
440
|
+
}];
|
|
441
|
+
case "terminal-output":
|
|
442
|
+
return [{ type: "terminal-output", data: message.data, callId: newId() }];
|
|
443
|
+
case "token-count": {
|
|
444
|
+
const { type: _type, ...rest } = message;
|
|
445
|
+
return [{ type: "token_count", ...rest }];
|
|
446
|
+
}
|
|
447
|
+
case "status": {
|
|
448
|
+
switch (message.status) {
|
|
449
|
+
case "starting":
|
|
450
|
+
return [{ type: "task_started", id: newId() }];
|
|
451
|
+
case "idle":
|
|
452
|
+
return [{ type: "task_complete", id: newId() }];
|
|
453
|
+
case "stopped":
|
|
454
|
+
return [{ type: "turn_aborted", id: newId() }];
|
|
455
|
+
case "error":
|
|
456
|
+
return [
|
|
457
|
+
{ type: "message", message: message.detail ?? "cursor-agent failed" },
|
|
458
|
+
{ type: "task_complete", id: newId() }
|
|
459
|
+
];
|
|
460
|
+
default:
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
default:
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const ALWAYS_ALLOWED_TOOLS = ["change_title", "happy__change_title", "preview_html", "happy__preview_html"];
|
|
470
|
+
const UNATTENDED_MODES = ["yolo", "safe-yolo", "bypassPermissions"];
|
|
471
|
+
class CursorPermissionHandler extends setupOfflineReconnection.BasePermissionHandler {
|
|
472
|
+
/** Set by "allow all tools for this session", or by an unattended mode. */
|
|
473
|
+
approveEverything = false;
|
|
474
|
+
/** Tools the user answered "don't ask again" for. */
|
|
475
|
+
allowedTools = /* @__PURE__ */ new Set();
|
|
476
|
+
currentPermissionMode = null;
|
|
477
|
+
constructor(session, pushClient) {
|
|
478
|
+
super(session, pushClient);
|
|
479
|
+
}
|
|
480
|
+
getLogPrefix() {
|
|
481
|
+
return "[cursor]";
|
|
482
|
+
}
|
|
483
|
+
getAgentName() {
|
|
484
|
+
return "Cursor";
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* The app sends the mode with each message. In an unattended mode asking
|
|
488
|
+
* would contradict what the session badge promises, so tools just run.
|
|
489
|
+
*/
|
|
490
|
+
setPermissionMode(mode) {
|
|
491
|
+
if (mode === this.currentPermissionMode) return;
|
|
492
|
+
this.currentPermissionMode = mode;
|
|
493
|
+
logger.logger.debug(`${this.getLogPrefix()} permission mode is now ${mode ?? "default"}`);
|
|
494
|
+
}
|
|
495
|
+
isUnattended() {
|
|
496
|
+
return this.currentPermissionMode !== null && UNATTENDED_MODES.includes(this.currentPermissionMode);
|
|
497
|
+
}
|
|
498
|
+
shouldAutoApprove(toolName) {
|
|
499
|
+
if (this.approveEverything || this.isUnattended()) return true;
|
|
500
|
+
if (this.allowedTools.has(toolName)) return true;
|
|
501
|
+
const lower = toolName.toLowerCase();
|
|
502
|
+
return ALWAYS_ALLOWED_TOOLS.some((name) => lower.includes(name.toLowerCase()));
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Blocks until the phone answers. The hook process on the other end is
|
|
506
|
+
* happy to wait — Cursor imposes no deadline of its own beyond the
|
|
507
|
+
* `timeout` we write into hooks.json.
|
|
508
|
+
*/
|
|
509
|
+
async requestApproval(toolCallId, toolName, input) {
|
|
510
|
+
if (this.shouldAutoApprove(toolName)) {
|
|
511
|
+
logger.logger.debug(`${this.getLogPrefix()} auto-approving ${toolName}`);
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
const result = await new Promise((resolve, reject) => {
|
|
515
|
+
this.pendingRequests.set(toolCallId, { resolve, reject, toolName, input });
|
|
516
|
+
this.addPendingRequestToState(toolCallId, toolName, input);
|
|
517
|
+
logger.logger.debug(`${this.getLogPrefix()} awaiting approval for ${toolName} (${toolCallId})`);
|
|
518
|
+
});
|
|
519
|
+
for (const allowed of result.allowTools ?? []) {
|
|
520
|
+
this.allowedTools.add(allowed);
|
|
521
|
+
}
|
|
522
|
+
if (result.decision === "approved_for_session" || result.mode && UNATTENDED_MODES.includes(result.mode)) {
|
|
523
|
+
this.approveEverything = true;
|
|
524
|
+
}
|
|
525
|
+
return result.decision === "approved" || result.decision === "approved_for_session";
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const KEEP_ALIVE_INTERVAL_MS = 2e3;
|
|
530
|
+
const SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
531
|
+
async function runCursor(opts) {
|
|
532
|
+
const api$1 = await api.ApiClient.create(opts.credentials);
|
|
533
|
+
const settings = await logger.readSettings();
|
|
534
|
+
const machineId = settings?.machineId;
|
|
535
|
+
if (!machineId) {
|
|
536
|
+
console.error(chalk.red("No machine ID found in settings. Run `happy` once to finish setup."));
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
await api$1.getOrCreateMachine({ machineId, metadata: run.initialMachineMetadata });
|
|
540
|
+
const { state, metadata } = setupOfflineReconnection.createSessionMetadata({
|
|
541
|
+
flavor: "cursor",
|
|
542
|
+
machineId,
|
|
543
|
+
startedBy: opts.startedBy
|
|
544
|
+
});
|
|
545
|
+
const sessionTag = node_crypto.randomUUID();
|
|
546
|
+
const response = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
547
|
+
let session;
|
|
548
|
+
const { session: initialSession } = setupOfflineReconnection.setupOfflineReconnection({
|
|
549
|
+
api: api$1,
|
|
550
|
+
sessionTag,
|
|
551
|
+
metadata,
|
|
552
|
+
state,
|
|
553
|
+
response,
|
|
554
|
+
onSessionSwap: (newSession) => {
|
|
555
|
+
session = newSession;
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
session = initialSession;
|
|
559
|
+
if (response) {
|
|
560
|
+
try {
|
|
561
|
+
const result = await controlClient.notifyDaemonSessionStarted(response.id, metadata);
|
|
562
|
+
if (result?.error) {
|
|
563
|
+
logger.logger.debug(`[cursor] daemon did not accept the session report: ${result.error}`);
|
|
564
|
+
}
|
|
565
|
+
} catch (error) {
|
|
566
|
+
logger.logger.debug(`[cursor] could not report session to daemon: ${error}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const mcp = await registerKillSessionHandler.createMcpContext(session);
|
|
570
|
+
const mcpStdio = mcp.configForStdio().happy;
|
|
571
|
+
const mcpUrlArg = mcpStdio?.args?.indexOf("--url") ?? -1;
|
|
572
|
+
const mcpUrl = mcpUrlArg >= 0 ? mcpStdio.args[mcpUrlArg + 1] : null;
|
|
573
|
+
const mcpBridge = mcpStdio ? { command: mcpStdio.command, args: mcpStdio.args ?? [] } : null;
|
|
574
|
+
const permissionHandler = new CursorPermissionHandler(session, api$1.push());
|
|
575
|
+
const approvalBridge = new cursorApprovalServer.CursorApprovalBridge({ cwd: process.cwd(), handler: permissionHandler, mcpBridge });
|
|
576
|
+
let approvalReady = false;
|
|
577
|
+
try {
|
|
578
|
+
await approvalBridge.start();
|
|
579
|
+
approvalReady = true;
|
|
580
|
+
} catch (error) {
|
|
581
|
+
logger.logger.debug(`[cursor] approval bridge failed to start: ${error}`);
|
|
582
|
+
}
|
|
583
|
+
const backend = new CursorCliBackend({
|
|
584
|
+
cwd: process.cwd(),
|
|
585
|
+
model: opts.model ?? null,
|
|
586
|
+
resumeChatId: opts.resumeChatId ?? null,
|
|
587
|
+
// Cursor asks before loading an MCP server; nobody is at the terminal.
|
|
588
|
+
extraArgs: mcpBridge ? ["--approve-mcps"] : void 0
|
|
589
|
+
});
|
|
590
|
+
let publishedChatId = opts.resumeChatId ?? null;
|
|
591
|
+
if (publishedChatId) {
|
|
592
|
+
session.updateMetadata((current) => ({ ...current, cursorSessionId: publishedChatId }));
|
|
593
|
+
}
|
|
594
|
+
const queue = [];
|
|
595
|
+
let draining = false;
|
|
596
|
+
let thinking = false;
|
|
597
|
+
let reportedModel = null;
|
|
598
|
+
backend.onMessage((message) => {
|
|
599
|
+
const chatId = backend.getChatId();
|
|
600
|
+
if (chatId && chatId !== publishedChatId) {
|
|
601
|
+
publishedChatId = chatId;
|
|
602
|
+
session.updateMetadata((current) => ({ ...current, cursorSessionId: chatId }));
|
|
603
|
+
void approvalBridge.registerChat(chatId);
|
|
604
|
+
}
|
|
605
|
+
if (message.type === "token-count") {
|
|
606
|
+
const usage = message;
|
|
607
|
+
const num = (key) => typeof usage[key] === "number" ? usage[key] : 0;
|
|
608
|
+
const input = num("inputTokens");
|
|
609
|
+
const output = num("outputTokens");
|
|
610
|
+
const cacheRead = num("cacheReadTokens");
|
|
611
|
+
const cacheWrite = num("cacheWriteTokens");
|
|
612
|
+
const total = input + output + cacheRead + cacheWrite;
|
|
613
|
+
if (total > 0) {
|
|
614
|
+
session.sendUsageReport({
|
|
615
|
+
key: "cursor-session",
|
|
616
|
+
tokens: { total, input, output, cache_read: cacheRead, cache_creation: cacheWrite },
|
|
617
|
+
// Cursor bills against its own subscription and reports no
|
|
618
|
+
// per-request price, so there is nothing honest to put here.
|
|
619
|
+
cost: { total: 0 }
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (message.type === "status") {
|
|
624
|
+
thinking = message.status === "starting" || message.status === "running";
|
|
625
|
+
const named = message.detail?.match(/^cursor-agent ready \((.+)\)$/)?.[1];
|
|
626
|
+
if (named && named !== reportedModel) {
|
|
627
|
+
reportedModel = named;
|
|
628
|
+
session.updateMetadata((current) => ({ ...current, model: named }));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
for (const acp of agentMessageToAcp(message, () => node_crypto.randomUUID())) {
|
|
632
|
+
session.sendAgentMessage("cursor", acp);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
const drain = async () => {
|
|
636
|
+
if (draining) return;
|
|
637
|
+
draining = true;
|
|
638
|
+
try {
|
|
639
|
+
while (queue.length > 0) {
|
|
640
|
+
const prompt = queue.shift();
|
|
641
|
+
logger.logger.debug(`[cursor] running turn (${queue.length} queued behind it)`);
|
|
642
|
+
try {
|
|
643
|
+
await backend.sendPrompt("", prompt);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
646
|
+
session.sendAgentMessage("cursor", { type: "message", message: `Cursor turn failed: ${detail}` });
|
|
647
|
+
logger.logger.debug(`[cursor] turn failed: ${detail}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} finally {
|
|
651
|
+
draining = false;
|
|
652
|
+
thinking = false;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
const handleAbort = async () => {
|
|
656
|
+
logger.logger.debug(`[cursor] abort requested (${queue.length} queued)`);
|
|
657
|
+
queue.length = 0;
|
|
658
|
+
try {
|
|
659
|
+
await backend.cancel("");
|
|
660
|
+
} catch (error) {
|
|
661
|
+
logger.logger.debug(`[cursor] abort failed: ${error}`);
|
|
662
|
+
}
|
|
663
|
+
permissionHandler.reset();
|
|
664
|
+
thinking = false;
|
|
665
|
+
session.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
666
|
+
};
|
|
667
|
+
session.onUserMessage((message) => {
|
|
668
|
+
const text = message.content?.text;
|
|
669
|
+
if (typeof text !== "string" || text.trim().length === 0) return;
|
|
670
|
+
if (message.meta?.permissionMode) {
|
|
671
|
+
permissionHandler.setPermissionMode(message.meta.permissionMode);
|
|
672
|
+
}
|
|
673
|
+
if (message.meta?.hasOwnProperty("model")) {
|
|
674
|
+
const chosen = message.meta.model || null;
|
|
675
|
+
backend.setModel(chosen);
|
|
676
|
+
logger.logger.debug(`[cursor] model set to ${chosen ?? "(Cursor default)"}`);
|
|
677
|
+
}
|
|
678
|
+
queue.push(text);
|
|
679
|
+
void drain();
|
|
680
|
+
});
|
|
681
|
+
const keepAlive = setInterval(() => {
|
|
682
|
+
session.keepAlive(thinking, "remote");
|
|
683
|
+
}, KEEP_ALIVE_INTERVAL_MS);
|
|
684
|
+
session.sendSessionEvent({ type: "ready" });
|
|
685
|
+
console.log(chalk.green("Cursor session ready.") + " Open Happy on your phone to drive it.");
|
|
686
|
+
if (response?.id) {
|
|
687
|
+
console.log(chalk.gray(` id ${response.id}`));
|
|
688
|
+
} else {
|
|
689
|
+
console.log(chalk.yellow(" id (offline \u2014 reconnecting in the background)"));
|
|
690
|
+
}
|
|
691
|
+
console.log(chalk.gray(` cwd ${process.cwd()}`));
|
|
692
|
+
console.log(chalk.gray(` model ${opts.model ?? "(Cursor default)"}`));
|
|
693
|
+
if (mcpUrl) {
|
|
694
|
+
console.log(chalk.gray(` tools Happy MCP on ${mcpUrl}`));
|
|
695
|
+
} else {
|
|
696
|
+
console.log(chalk.yellow(" tools Happy MCP unavailable \u2014 change_title, preview_html and device tools are missing"));
|
|
697
|
+
}
|
|
698
|
+
if (approvalReady) {
|
|
699
|
+
console.log(chalk.gray(" approval tool calls are sent to your phone before they run"));
|
|
700
|
+
} else {
|
|
701
|
+
console.log(chalk.yellow(" approval UNAVAILABLE \u2014 the bridge did not start, so tools run unchecked"));
|
|
702
|
+
}
|
|
703
|
+
if (opts.initialPrompt) {
|
|
704
|
+
queue.push(opts.initialPrompt);
|
|
705
|
+
void drain();
|
|
706
|
+
}
|
|
707
|
+
let shuttingDown = false;
|
|
708
|
+
const shutdown = async (archived = false) => {
|
|
709
|
+
if (shuttingDown) return;
|
|
710
|
+
shuttingDown = true;
|
|
711
|
+
clearInterval(keepAlive);
|
|
712
|
+
thinking = false;
|
|
713
|
+
logger.logger.debug(`[cursor] shutting down${archived ? " (archived)" : ""}`);
|
|
714
|
+
if (archived) {
|
|
715
|
+
session.updateMetadata((current) => ({
|
|
716
|
+
...current,
|
|
717
|
+
lifecycleState: "archived",
|
|
718
|
+
lifecycleStateSince: Date.now(),
|
|
719
|
+
archivedBy: "cli",
|
|
720
|
+
archiveReason: "User terminated"
|
|
721
|
+
}));
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
permissionHandler.reset();
|
|
725
|
+
await backend.dispose();
|
|
726
|
+
await approvalBridge.stop();
|
|
727
|
+
mcp.stop();
|
|
728
|
+
} finally {
|
|
729
|
+
session.sendSessionDeath();
|
|
730
|
+
await session.flush();
|
|
731
|
+
await session.close();
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
const shutdownAndExit = () => {
|
|
735
|
+
void Promise.race([
|
|
736
|
+
shutdown(),
|
|
737
|
+
new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
|
|
738
|
+
]).finally(() => process.exit(0));
|
|
739
|
+
};
|
|
740
|
+
process.on("SIGINT", shutdownAndExit);
|
|
741
|
+
process.on("SIGTERM", shutdownAndExit);
|
|
742
|
+
session.rpcHandlerManager.registerHandler("abort", handleAbort);
|
|
743
|
+
registerKillSessionHandler.registerKillSessionHandler(session.rpcHandlerManager, async () => {
|
|
744
|
+
await Promise.race([
|
|
745
|
+
shutdown(true),
|
|
746
|
+
new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
|
|
747
|
+
]);
|
|
748
|
+
process.exit(0);
|
|
749
|
+
});
|
|
750
|
+
await new Promise(() => {
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
exports.runCursor = runCursor;
|