cyrus-cursor-runner 0.2.22
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/LICENSE +201 -0
- package/dist/CursorRunner.d.ts +64 -0
- package/dist/CursorRunner.d.ts.map +1 -0
- package/dist/CursorRunner.js +1483 -0
- package/dist/CursorRunner.js.map +1 -0
- package/dist/SimpleCursorRunner.d.ts +30 -0
- package/dist/SimpleCursorRunner.d.ts.map +1 -0
- package/dist/SimpleCursorRunner.js +154 -0
- package/dist/SimpleCursorRunner.js.map +1 -0
- package/dist/formatter.d.ts +9 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +147 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
5
|
+
import { join, parse as pathParse, relative as pathRelative, resolve, } from "node:path";
|
|
6
|
+
import { cwd } from "node:process";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
import { CursorMessageFormatter } from "./formatter.js";
|
|
9
|
+
/** cursor-agent version we have tested against; set cursorAgentVersion in config to override */
|
|
10
|
+
const TESTED_CURSOR_AGENT_VERSION = "2026.02.13-41ac335";
|
|
11
|
+
const CURSOR_MCP_CONFIG_DOCS_URL = "https://cursor.com/docs/context/mcp#configuration-locations";
|
|
12
|
+
const CURSOR_CLI_PERMISSIONS_DOCS_URL = "https://cursor.com/docs/cli/reference/permissions";
|
|
13
|
+
function toFiniteNumber(value) {
|
|
14
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
15
|
+
}
|
|
16
|
+
function safeStringify(value) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(value, null, 2);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function createAssistantToolUseMessage(toolUseId, toolName, toolInput, messageId = crypto.randomUUID()) {
|
|
25
|
+
const contentBlocks = [
|
|
26
|
+
{
|
|
27
|
+
type: "tool_use",
|
|
28
|
+
id: toolUseId,
|
|
29
|
+
name: toolName,
|
|
30
|
+
input: toolInput,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
return {
|
|
34
|
+
id: messageId,
|
|
35
|
+
type: "message",
|
|
36
|
+
role: "assistant",
|
|
37
|
+
content: contentBlocks,
|
|
38
|
+
model: "cursor-agent",
|
|
39
|
+
stop_reason: null,
|
|
40
|
+
stop_sequence: null,
|
|
41
|
+
usage: {
|
|
42
|
+
input_tokens: 0,
|
|
43
|
+
output_tokens: 0,
|
|
44
|
+
cache_creation_input_tokens: 0,
|
|
45
|
+
cache_read_input_tokens: 0,
|
|
46
|
+
cache_creation: null,
|
|
47
|
+
},
|
|
48
|
+
container: null,
|
|
49
|
+
context_management: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function createUserToolResultMessage(toolUseId, result, isError) {
|
|
53
|
+
const contentBlocks = [
|
|
54
|
+
{
|
|
55
|
+
type: "tool_result",
|
|
56
|
+
tool_use_id: toolUseId,
|
|
57
|
+
content: result,
|
|
58
|
+
is_error: isError,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
return {
|
|
62
|
+
role: "user",
|
|
63
|
+
content: contentBlocks,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function createAssistantBetaMessage(content, messageId = crypto.randomUUID()) {
|
|
67
|
+
const contentBlocks = [
|
|
68
|
+
{ type: "text", text: content },
|
|
69
|
+
];
|
|
70
|
+
return {
|
|
71
|
+
id: messageId,
|
|
72
|
+
type: "message",
|
|
73
|
+
role: "assistant",
|
|
74
|
+
content: contentBlocks,
|
|
75
|
+
model: "cursor-agent",
|
|
76
|
+
stop_reason: null,
|
|
77
|
+
stop_sequence: null,
|
|
78
|
+
usage: {
|
|
79
|
+
input_tokens: 0,
|
|
80
|
+
output_tokens: 0,
|
|
81
|
+
cache_creation_input_tokens: 0,
|
|
82
|
+
cache_read_input_tokens: 0,
|
|
83
|
+
cache_creation: null,
|
|
84
|
+
},
|
|
85
|
+
container: null,
|
|
86
|
+
context_management: null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function createResultUsage(parsed) {
|
|
90
|
+
return {
|
|
91
|
+
input_tokens: parsed.inputTokens,
|
|
92
|
+
output_tokens: parsed.outputTokens,
|
|
93
|
+
cache_creation_input_tokens: 0,
|
|
94
|
+
cache_read_input_tokens: parsed.cachedInputTokens,
|
|
95
|
+
cache_creation: {
|
|
96
|
+
ephemeral_1h_input_tokens: 0,
|
|
97
|
+
ephemeral_5m_input_tokens: 0,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function normalizeError(error) {
|
|
102
|
+
if (error instanceof Error) {
|
|
103
|
+
return error.message;
|
|
104
|
+
}
|
|
105
|
+
if (typeof error === "string") {
|
|
106
|
+
return error;
|
|
107
|
+
}
|
|
108
|
+
return "Cursor execution failed";
|
|
109
|
+
}
|
|
110
|
+
function normalizeCursorModel(model) {
|
|
111
|
+
if (!model) {
|
|
112
|
+
return model;
|
|
113
|
+
}
|
|
114
|
+
// Preserve backward compatibility for selector aliases that Cursor CLI no longer accepts.
|
|
115
|
+
if (model.toLowerCase() === "gpt-5") {
|
|
116
|
+
return "auto";
|
|
117
|
+
}
|
|
118
|
+
return model;
|
|
119
|
+
}
|
|
120
|
+
function extractTextFromMessageContent(content) {
|
|
121
|
+
if (!Array.isArray(content)) {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
const text = content
|
|
125
|
+
.map((block) => {
|
|
126
|
+
if (!block || typeof block !== "object") {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
const blockObj = block;
|
|
130
|
+
return getStringValue(blockObj, "text") || "";
|
|
131
|
+
})
|
|
132
|
+
.join("")
|
|
133
|
+
.trim();
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
function inferCommandToolName(command) {
|
|
137
|
+
const normalized = command.toLowerCase();
|
|
138
|
+
if (/\brg\b|\bgrep\b/.test(normalized)) {
|
|
139
|
+
return "Grep";
|
|
140
|
+
}
|
|
141
|
+
if (/\bglob\.glob\b|\bfind\b.+\s-name\s/.test(normalized)) {
|
|
142
|
+
return "Glob";
|
|
143
|
+
}
|
|
144
|
+
if (/\bcat\b/.test(normalized) && !/>/.test(normalized)) {
|
|
145
|
+
return "Read";
|
|
146
|
+
}
|
|
147
|
+
if (/<<\s*['"]?eof['"]?\s*>/i.test(command) ||
|
|
148
|
+
/\becho\b.+>/.test(normalized)) {
|
|
149
|
+
return "Write";
|
|
150
|
+
}
|
|
151
|
+
return "Bash";
|
|
152
|
+
}
|
|
153
|
+
function normalizeFilePath(path, workingDirectory) {
|
|
154
|
+
if (!path) {
|
|
155
|
+
return path;
|
|
156
|
+
}
|
|
157
|
+
if (workingDirectory && path.startsWith(workingDirectory)) {
|
|
158
|
+
const relativePath = pathRelative(workingDirectory, path);
|
|
159
|
+
if (relativePath && relativePath !== ".") {
|
|
160
|
+
return relativePath;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return path;
|
|
164
|
+
}
|
|
165
|
+
function summarizeFileChanges(item, workingDirectory) {
|
|
166
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
167
|
+
if (!changes.length) {
|
|
168
|
+
return item.status === "failed" ? "Patch failed" : "No file changes";
|
|
169
|
+
}
|
|
170
|
+
return changes
|
|
171
|
+
.map((change) => {
|
|
172
|
+
if (!change || typeof change !== "object") {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const mapped = change;
|
|
176
|
+
const path = typeof mapped.path === "string" ? mapped.path : "";
|
|
177
|
+
const kind = typeof mapped.kind === "string" ? mapped.kind : "update";
|
|
178
|
+
const filePath = normalizeFilePath(path, workingDirectory);
|
|
179
|
+
return `${kind} ${filePath}`;
|
|
180
|
+
})
|
|
181
|
+
.filter((line) => Boolean(line))
|
|
182
|
+
.join("\n");
|
|
183
|
+
}
|
|
184
|
+
function isTodoCompleted(status) {
|
|
185
|
+
const s = status.toLowerCase();
|
|
186
|
+
return s === "completed" || s === "todo_status_completed";
|
|
187
|
+
}
|
|
188
|
+
function isTodoInProgress(status) {
|
|
189
|
+
const s = status.toLowerCase();
|
|
190
|
+
return s === "in_progress" || s === "todo_status_in_progress";
|
|
191
|
+
}
|
|
192
|
+
function summarizeTodoList(item) {
|
|
193
|
+
const todos = Array.isArray(item.items) ? item.items : [];
|
|
194
|
+
if (!todos.length) {
|
|
195
|
+
return "No todos";
|
|
196
|
+
}
|
|
197
|
+
return todos
|
|
198
|
+
.map((todo) => {
|
|
199
|
+
if (!todo || typeof todo !== "object") {
|
|
200
|
+
return "- [ ] task";
|
|
201
|
+
}
|
|
202
|
+
const mapped = todo;
|
|
203
|
+
const text = typeof mapped.content === "string"
|
|
204
|
+
? mapped.content
|
|
205
|
+
: typeof mapped.description === "string"
|
|
206
|
+
? mapped.description
|
|
207
|
+
: "task";
|
|
208
|
+
const status = typeof mapped.status === "string"
|
|
209
|
+
? mapped.status.toLowerCase()
|
|
210
|
+
: "pending";
|
|
211
|
+
const marker = isTodoCompleted(status) ? "[x]" : "[ ]";
|
|
212
|
+
const suffix = isTodoInProgress(status) ? " (in progress)" : "";
|
|
213
|
+
return `- ${marker} ${text}${suffix}`;
|
|
214
|
+
})
|
|
215
|
+
.join("\n");
|
|
216
|
+
}
|
|
217
|
+
function getStringValue(object, ...keys) {
|
|
218
|
+
for (const key of keys) {
|
|
219
|
+
const value = object[key];
|
|
220
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
function parseToolPattern(toolPattern) {
|
|
227
|
+
const trimmed = toolPattern.trim();
|
|
228
|
+
if (!trimmed) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const match = trimmed.match(/^([A-Za-z]+)(?:\((.*)\))?$/);
|
|
232
|
+
if (!match) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
name: match[1] || "",
|
|
237
|
+
argument: match[2]?.trim() ?? null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function normalizeShellCommandBase(argument) {
|
|
241
|
+
if (!argument || argument === "*" || argument === "**") {
|
|
242
|
+
return "*";
|
|
243
|
+
}
|
|
244
|
+
const firstRule = argument.split(",")[0]?.trim();
|
|
245
|
+
if (!firstRule) {
|
|
246
|
+
return "*";
|
|
247
|
+
}
|
|
248
|
+
const beforeColon = firstRule.split(":")[0]?.trim();
|
|
249
|
+
return beforeColon || "*";
|
|
250
|
+
}
|
|
251
|
+
function normalizePathPattern(argument) {
|
|
252
|
+
if (!argument) {
|
|
253
|
+
// Keep file access scoped to workspace paths by default.
|
|
254
|
+
return "./**";
|
|
255
|
+
}
|
|
256
|
+
const trimmed = argument.trim();
|
|
257
|
+
if (!trimmed) {
|
|
258
|
+
return "./**";
|
|
259
|
+
}
|
|
260
|
+
// Cursor treats broad globs as permissive; anchor wildcard defaults to workspace.
|
|
261
|
+
if (trimmed === "**") {
|
|
262
|
+
return "./**";
|
|
263
|
+
}
|
|
264
|
+
return trimmed;
|
|
265
|
+
}
|
|
266
|
+
function toCursorPath(path) {
|
|
267
|
+
return path.replace(/\\/g, "/");
|
|
268
|
+
}
|
|
269
|
+
function isWildcardPathArgument(argument) {
|
|
270
|
+
if (!argument) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
const trimmed = argument.trim();
|
|
274
|
+
return trimmed.length === 0 || trimmed === "**";
|
|
275
|
+
}
|
|
276
|
+
function isBroadReadToolPattern(toolPattern) {
|
|
277
|
+
const parsed = parseToolPattern(toolPattern);
|
|
278
|
+
if (!parsed) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const toolName = parsed.name.toLowerCase();
|
|
282
|
+
if (!(toolName === "read" || toolName === "glob" || toolName === "grep")) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return isWildcardPathArgument(parsed.argument);
|
|
286
|
+
}
|
|
287
|
+
function isBroadWriteToolPattern(toolPattern) {
|
|
288
|
+
const parsed = parseToolPattern(toolPattern);
|
|
289
|
+
if (!parsed) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
const toolName = parsed.name.toLowerCase();
|
|
293
|
+
if (!(toolName === "edit" ||
|
|
294
|
+
toolName === "write" ||
|
|
295
|
+
toolName === "multiedit" ||
|
|
296
|
+
toolName === "notebookedit" ||
|
|
297
|
+
toolName === "todowrite")) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
return isWildcardPathArgument(parsed.argument);
|
|
301
|
+
}
|
|
302
|
+
function buildWorkspaceSiblingDenyPermissions(workspacePath, permission) {
|
|
303
|
+
const resolvedWorkspacePath = resolve(workspacePath);
|
|
304
|
+
const parsed = pathParse(resolvedWorkspacePath);
|
|
305
|
+
if (!parsed.root) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
const segments = resolvedWorkspacePath
|
|
309
|
+
.slice(parsed.root.length)
|
|
310
|
+
.split(/[\\/]+/)
|
|
311
|
+
.filter(Boolean);
|
|
312
|
+
if (segments.length === 0) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
const denyPermissions = new Set();
|
|
316
|
+
let parentPath = parsed.root;
|
|
317
|
+
for (const segment of segments) {
|
|
318
|
+
let siblingEntries;
|
|
319
|
+
try {
|
|
320
|
+
siblingEntries = readdirSync(parentPath, { withFileTypes: true });
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
for (const sibling of siblingEntries) {
|
|
326
|
+
if (!sibling.isDirectory() || sibling.name === segment) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const siblingPath = join(parentPath, sibling.name);
|
|
330
|
+
denyPermissions.add(`${permission}(${toCursorPath(siblingPath)}/**)`);
|
|
331
|
+
}
|
|
332
|
+
parentPath = join(parentPath, segment);
|
|
333
|
+
}
|
|
334
|
+
return [...denyPermissions];
|
|
335
|
+
}
|
|
336
|
+
function buildSystemRootDenyPermissions(workspacePath, permission) {
|
|
337
|
+
const workspace = toCursorPath(resolve(workspacePath));
|
|
338
|
+
const rootCandidates = [
|
|
339
|
+
"/etc",
|
|
340
|
+
"/bin",
|
|
341
|
+
"/sbin",
|
|
342
|
+
"/usr",
|
|
343
|
+
"/opt",
|
|
344
|
+
"/System",
|
|
345
|
+
"/Library",
|
|
346
|
+
"/Applications",
|
|
347
|
+
"/dev",
|
|
348
|
+
"/proc",
|
|
349
|
+
"/sys",
|
|
350
|
+
"/Volumes",
|
|
351
|
+
"/home",
|
|
352
|
+
];
|
|
353
|
+
const denies = [];
|
|
354
|
+
for (const rootPath of rootCandidates) {
|
|
355
|
+
if (workspace === rootPath || workspace.startsWith(`${rootPath}/`)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
denies.push(`${permission}(${rootPath}/**)`);
|
|
359
|
+
}
|
|
360
|
+
return denies;
|
|
361
|
+
}
|
|
362
|
+
function normalizeMcpPermissionPart(value) {
|
|
363
|
+
if (!value) {
|
|
364
|
+
return "*";
|
|
365
|
+
}
|
|
366
|
+
const trimmed = value.trim();
|
|
367
|
+
return trimmed || "*";
|
|
368
|
+
}
|
|
369
|
+
function mapClaudeMcpToolPatternToCursorPermission(toolPattern) {
|
|
370
|
+
const trimmed = toolPattern.trim();
|
|
371
|
+
if (!trimmed.toLowerCase().startsWith("mcp__")) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
const parts = trimmed.split("__");
|
|
375
|
+
if (parts.length < 2) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const server = normalizeMcpPermissionPart(parts[1] || null);
|
|
379
|
+
const tool = parts.length >= 3
|
|
380
|
+
? normalizeMcpPermissionPart(parts.slice(2).join("__"))
|
|
381
|
+
: "*";
|
|
382
|
+
return `Mcp(${server}:${tool})`;
|
|
383
|
+
}
|
|
384
|
+
function mapClaudeToolPatternToCursorPermission(toolPattern) {
|
|
385
|
+
const mappedMcpPermission = mapClaudeMcpToolPatternToCursorPermission(toolPattern);
|
|
386
|
+
if (mappedMcpPermission) {
|
|
387
|
+
return mappedMcpPermission;
|
|
388
|
+
}
|
|
389
|
+
const parsed = parseToolPattern(toolPattern);
|
|
390
|
+
if (!parsed) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const toolName = parsed.name.toLowerCase();
|
|
394
|
+
if (toolName === "bash" || toolName === "shell") {
|
|
395
|
+
return `Shell(${normalizeShellCommandBase(parsed.argument)})`;
|
|
396
|
+
}
|
|
397
|
+
if (toolName === "read" || toolName === "glob" || toolName === "grep") {
|
|
398
|
+
return `Read(${normalizePathPattern(parsed.argument)})`;
|
|
399
|
+
}
|
|
400
|
+
if (toolName === "edit" ||
|
|
401
|
+
toolName === "write" ||
|
|
402
|
+
toolName === "multiedit" ||
|
|
403
|
+
toolName === "notebookedit" ||
|
|
404
|
+
toolName === "todowrite") {
|
|
405
|
+
return `Write(${normalizePathPattern(parsed.argument)})`;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
function parseMcpServersFromCursorListOutput(output) {
|
|
410
|
+
const servers = new Set();
|
|
411
|
+
for (const line of output.split(/\r?\n/)) {
|
|
412
|
+
const match = line.match(/^\s*([A-Za-z0-9._-]+)\s*:/);
|
|
413
|
+
const serverName = match?.[1]?.trim();
|
|
414
|
+
if (serverName) {
|
|
415
|
+
servers.add(serverName);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return [...servers];
|
|
419
|
+
}
|
|
420
|
+
function getProjectionForItem(item, workingDirectory) {
|
|
421
|
+
const itemId = getStringValue(item, "id", "tool_id", "item_id");
|
|
422
|
+
if (!itemId) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const itemType = getStringValue(item, "type");
|
|
426
|
+
const status = getStringValue(item, "status") || "completed";
|
|
427
|
+
const isError = status === "failed";
|
|
428
|
+
if (itemType === "command_execution") {
|
|
429
|
+
const command = getStringValue(item, "command") || "";
|
|
430
|
+
const output = getStringValue(item, "aggregated_output", "output") || "";
|
|
431
|
+
const exitCodeValue = item.exit_code;
|
|
432
|
+
const exitCode = toFiniteNumber(exitCodeValue);
|
|
433
|
+
const toolName = inferCommandToolName(command);
|
|
434
|
+
const toolInput = {
|
|
435
|
+
command,
|
|
436
|
+
description: command,
|
|
437
|
+
};
|
|
438
|
+
const result = output ||
|
|
439
|
+
(isError
|
|
440
|
+
? `Command failed${exitCode ? ` (exit ${exitCode})` : ""}`
|
|
441
|
+
: "Command completed");
|
|
442
|
+
return {
|
|
443
|
+
toolUseId: itemId,
|
|
444
|
+
toolName,
|
|
445
|
+
toolInput,
|
|
446
|
+
result,
|
|
447
|
+
isError,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (itemType === "file_change") {
|
|
451
|
+
const summary = summarizeFileChanges(item, workingDirectory);
|
|
452
|
+
return {
|
|
453
|
+
toolUseId: itemId,
|
|
454
|
+
toolName: "Edit",
|
|
455
|
+
toolInput: { description: summary },
|
|
456
|
+
result: summary,
|
|
457
|
+
isError,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (itemType === "web_search") {
|
|
461
|
+
const query = getStringValue(item, "query") || "web search";
|
|
462
|
+
const actionValue = item.action;
|
|
463
|
+
let toolInput = { query };
|
|
464
|
+
let result = query;
|
|
465
|
+
if (actionValue && typeof actionValue === "object") {
|
|
466
|
+
const action = actionValue;
|
|
467
|
+
const url = getStringValue(action, "url");
|
|
468
|
+
if (url) {
|
|
469
|
+
toolInput = { url };
|
|
470
|
+
result = url;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
toolUseId: itemId,
|
|
475
|
+
toolName: "WebSearch",
|
|
476
|
+
toolInput,
|
|
477
|
+
result,
|
|
478
|
+
isError,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (itemType === "mcp_tool_call") {
|
|
482
|
+
const server = getStringValue(item, "server") || "mcp";
|
|
483
|
+
const tool = getStringValue(item, "tool") || "tool";
|
|
484
|
+
const args = item.arguments && typeof item.arguments === "object"
|
|
485
|
+
? item.arguments
|
|
486
|
+
: {};
|
|
487
|
+
const result = getStringValue(item, "result") ||
|
|
488
|
+
safeStringify(item.result || "MCP tool completed");
|
|
489
|
+
return {
|
|
490
|
+
toolUseId: itemId,
|
|
491
|
+
toolName: `mcp__${server}__${tool}`,
|
|
492
|
+
toolInput: args,
|
|
493
|
+
result,
|
|
494
|
+
isError,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (itemType === "todo_list") {
|
|
498
|
+
const summary = summarizeTodoList(item);
|
|
499
|
+
return {
|
|
500
|
+
toolUseId: itemId,
|
|
501
|
+
toolName: "TodoWrite",
|
|
502
|
+
toolInput: { todos: item.items },
|
|
503
|
+
result: summary,
|
|
504
|
+
isError,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
function extractToolResultFromPayload(payload) {
|
|
510
|
+
const resultValue = payload.result && typeof payload.result === "object"
|
|
511
|
+
? payload.result
|
|
512
|
+
: null;
|
|
513
|
+
if (!resultValue) {
|
|
514
|
+
return { text: "Tool completed", isError: false };
|
|
515
|
+
}
|
|
516
|
+
if (resultValue.success && typeof resultValue.success === "object") {
|
|
517
|
+
const success = resultValue.success;
|
|
518
|
+
const output = getStringValue(success, "interleavedOutput", "stdout", "markdown", "text") || safeStringify(success);
|
|
519
|
+
return { text: output, isError: false };
|
|
520
|
+
}
|
|
521
|
+
const failure = resultValue.failure && typeof resultValue.failure === "object"
|
|
522
|
+
? resultValue.failure
|
|
523
|
+
: null;
|
|
524
|
+
if (failure) {
|
|
525
|
+
return {
|
|
526
|
+
text: getStringValue(failure, "message", "stderr") || safeStringify(failure),
|
|
527
|
+
isError: true,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return { text: safeStringify(resultValue), isError: false };
|
|
531
|
+
}
|
|
532
|
+
function getProjectionForToolCallEvent(event, workingDirectory) {
|
|
533
|
+
const toolUseId = getStringValue(event, "call_id");
|
|
534
|
+
if (!toolUseId) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const toolCallRaw = event.tool_call && typeof event.tool_call === "object"
|
|
538
|
+
? event.tool_call
|
|
539
|
+
: null;
|
|
540
|
+
if (!toolCallRaw) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
const variantKey = Object.keys(toolCallRaw)[0];
|
|
544
|
+
if (!variantKey) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const payloadValue = toolCallRaw[variantKey];
|
|
548
|
+
if (!payloadValue || typeof payloadValue !== "object") {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const payload = payloadValue;
|
|
552
|
+
const args = payload.args && typeof payload.args === "object"
|
|
553
|
+
? payload.args
|
|
554
|
+
: {};
|
|
555
|
+
let toolName = "Tool";
|
|
556
|
+
let toolInput = {};
|
|
557
|
+
let resultText = "Tool completed";
|
|
558
|
+
if (variantKey === "shellToolCall") {
|
|
559
|
+
const command = getStringValue(args, "command") || "";
|
|
560
|
+
toolName = inferCommandToolName(command);
|
|
561
|
+
toolInput = { command, description: command };
|
|
562
|
+
}
|
|
563
|
+
else if (variantKey === "readToolCall") {
|
|
564
|
+
toolName = "Read";
|
|
565
|
+
toolInput = {
|
|
566
|
+
path: normalizeFilePath(getStringValue(args, "path") || "", workingDirectory),
|
|
567
|
+
limit: args.limit,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
else if (variantKey === "grepToolCall") {
|
|
571
|
+
toolName = "Grep";
|
|
572
|
+
toolInput = {
|
|
573
|
+
pattern: getStringValue(args, "pattern") || "",
|
|
574
|
+
path: normalizeFilePath(getStringValue(args, "path") || "", workingDirectory),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
else if (variantKey === "globToolCall") {
|
|
578
|
+
toolName = "Glob";
|
|
579
|
+
toolInput = {
|
|
580
|
+
glob: getStringValue(args, "globPattern") || "",
|
|
581
|
+
path: normalizeFilePath(getStringValue(args, "targetDirectory") || "", workingDirectory),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
else if (variantKey === "editToolCall") {
|
|
585
|
+
toolName = "Edit";
|
|
586
|
+
toolInput = {
|
|
587
|
+
path: normalizeFilePath(getStringValue(args, "path") || "", workingDirectory),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
else if (variantKey === "deleteToolCall") {
|
|
591
|
+
toolName = "Edit";
|
|
592
|
+
toolInput = {
|
|
593
|
+
description: `delete ${normalizeFilePath(getStringValue(args, "path") || "", workingDirectory)}`,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
else if (variantKey === "semSearchToolCall") {
|
|
597
|
+
toolName = "ToolSearch";
|
|
598
|
+
toolInput = { query: getStringValue(args, "query") || "" };
|
|
599
|
+
}
|
|
600
|
+
else if (variantKey === "readLintsToolCall") {
|
|
601
|
+
toolName = "Read";
|
|
602
|
+
toolInput = { paths: args.paths };
|
|
603
|
+
}
|
|
604
|
+
else if (variantKey === "mcpToolCall") {
|
|
605
|
+
const provider = getStringValue(args, "providerIdentifier") || "mcp";
|
|
606
|
+
const namedTool = getStringValue(args, "toolName") ||
|
|
607
|
+
getStringValue(args, "name") ||
|
|
608
|
+
"tool";
|
|
609
|
+
toolName = `mcp__${provider}__${namedTool}`;
|
|
610
|
+
toolInput =
|
|
611
|
+
args.args && typeof args.args === "object"
|
|
612
|
+
? args.args
|
|
613
|
+
: {};
|
|
614
|
+
}
|
|
615
|
+
else if (variantKey === "listMcpResourcesToolCall") {
|
|
616
|
+
toolName = "mcp__list_resources";
|
|
617
|
+
toolInput = {};
|
|
618
|
+
}
|
|
619
|
+
else if (variantKey === "webFetchToolCall") {
|
|
620
|
+
toolName = "WebFetch";
|
|
621
|
+
toolInput = { url: getStringValue(args, "url") || "" };
|
|
622
|
+
}
|
|
623
|
+
else if (variantKey === "updateTodosToolCall") {
|
|
624
|
+
toolName = "TodoWrite";
|
|
625
|
+
toolInput = { todos: args.todos };
|
|
626
|
+
resultText = summarizeTodoList({ items: args.todos });
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
toolName = variantKey.replace(/ToolCall$/, "");
|
|
630
|
+
toolInput = args;
|
|
631
|
+
}
|
|
632
|
+
const extracted = extractToolResultFromPayload(payload);
|
|
633
|
+
if (resultText === "Tool completed" || extracted.isError) {
|
|
634
|
+
resultText = extracted.text;
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
toolUseId,
|
|
638
|
+
toolName,
|
|
639
|
+
toolInput,
|
|
640
|
+
result: resultText,
|
|
641
|
+
isError: extracted.isError,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function extractUsageFromEvent(event) {
|
|
645
|
+
const usageRaw = event.usage && typeof event.usage === "object"
|
|
646
|
+
? event.usage
|
|
647
|
+
: null;
|
|
648
|
+
if (!usageRaw) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
inputTokens: toFiniteNumber(usageRaw.input_tokens),
|
|
653
|
+
outputTokens: toFiniteNumber(usageRaw.output_tokens),
|
|
654
|
+
cachedInputTokens: toFiniteNumber(usageRaw.cached_input_tokens),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
export class CursorRunner extends EventEmitter {
|
|
658
|
+
supportsStreamingInput = false;
|
|
659
|
+
config;
|
|
660
|
+
sessionInfo = null;
|
|
661
|
+
messages = [];
|
|
662
|
+
formatter;
|
|
663
|
+
process = null;
|
|
664
|
+
readlineInterface = null;
|
|
665
|
+
pendingResultMessage = null;
|
|
666
|
+
hasInitMessage = false;
|
|
667
|
+
lastAssistantText = null;
|
|
668
|
+
wasStopped = false;
|
|
669
|
+
startTimestampMs = 0;
|
|
670
|
+
lastUsage = {
|
|
671
|
+
inputTokens: 0,
|
|
672
|
+
outputTokens: 0,
|
|
673
|
+
cachedInputTokens: 0,
|
|
674
|
+
};
|
|
675
|
+
errorMessages = [];
|
|
676
|
+
emittedToolUseIds = new Set();
|
|
677
|
+
fallbackOutputLines = [];
|
|
678
|
+
logStream = null;
|
|
679
|
+
mcpConfigRestoreState = null;
|
|
680
|
+
permissionsConfigRestoreState = null;
|
|
681
|
+
constructor(config) {
|
|
682
|
+
super();
|
|
683
|
+
this.config = config;
|
|
684
|
+
this.formatter = new CursorMessageFormatter();
|
|
685
|
+
if (config.onMessage)
|
|
686
|
+
this.on("message", config.onMessage);
|
|
687
|
+
if (config.onError)
|
|
688
|
+
this.on("error", config.onError);
|
|
689
|
+
if (config.onComplete)
|
|
690
|
+
this.on("complete", config.onComplete);
|
|
691
|
+
}
|
|
692
|
+
async start(prompt) {
|
|
693
|
+
return this.startWithPrompt(prompt);
|
|
694
|
+
}
|
|
695
|
+
async startStreaming(initialPrompt) {
|
|
696
|
+
return this.startWithPrompt(null, initialPrompt);
|
|
697
|
+
}
|
|
698
|
+
addStreamMessage(_content) {
|
|
699
|
+
throw new Error("CursorRunner does not support streaming input messages");
|
|
700
|
+
}
|
|
701
|
+
completeStream() {
|
|
702
|
+
// No-op: CursorRunner does not support streaming input.
|
|
703
|
+
}
|
|
704
|
+
async startWithPrompt(stringPrompt, streamingInitialPrompt) {
|
|
705
|
+
if (this.isRunning()) {
|
|
706
|
+
throw new Error("Cursor session already running");
|
|
707
|
+
}
|
|
708
|
+
const sessionId = this.config.resumeSessionId || crypto.randomUUID();
|
|
709
|
+
this.sessionInfo = {
|
|
710
|
+
sessionId,
|
|
711
|
+
startedAt: new Date(),
|
|
712
|
+
isRunning: true,
|
|
713
|
+
};
|
|
714
|
+
this.messages = [];
|
|
715
|
+
this.pendingResultMessage = null;
|
|
716
|
+
this.hasInitMessage = false;
|
|
717
|
+
this.lastAssistantText = null;
|
|
718
|
+
this.wasStopped = false;
|
|
719
|
+
this.startTimestampMs = Date.now();
|
|
720
|
+
this.lastUsage = {
|
|
721
|
+
inputTokens: 0,
|
|
722
|
+
outputTokens: 0,
|
|
723
|
+
cachedInputTokens: 0,
|
|
724
|
+
};
|
|
725
|
+
this.errorMessages = [];
|
|
726
|
+
this.emittedToolUseIds.clear();
|
|
727
|
+
this.fallbackOutputLines = [];
|
|
728
|
+
this.setupLogging(sessionId);
|
|
729
|
+
this.syncProjectMcpConfig();
|
|
730
|
+
this.enableCursorMcpServers();
|
|
731
|
+
this.syncProjectPermissionsConfig();
|
|
732
|
+
// Test/CI fallback: allow deterministic mock runs when cursor-agent cannot execute.
|
|
733
|
+
if (process.env.CYRUS_CURSOR_MOCK === "1") {
|
|
734
|
+
this.emitInitMessage();
|
|
735
|
+
this.handleEvent({
|
|
736
|
+
type: "message",
|
|
737
|
+
role: "assistant",
|
|
738
|
+
content: "Cursor mock session completed",
|
|
739
|
+
});
|
|
740
|
+
this.pendingResultMessage = this.createSuccessResultMessage("Cursor mock session completed");
|
|
741
|
+
this.finalizeSession();
|
|
742
|
+
return this.sessionInfo;
|
|
743
|
+
}
|
|
744
|
+
const cursorPath = this.config.cursorPath || "cursor-agent";
|
|
745
|
+
const expectedVersion = this.config.cursorAgentVersion ?? TESTED_CURSOR_AGENT_VERSION;
|
|
746
|
+
const versionError = this.checkCursorAgentVersion(cursorPath, expectedVersion);
|
|
747
|
+
if (versionError) {
|
|
748
|
+
this.finalizeSession(new Error(versionError));
|
|
749
|
+
return this.sessionInfo;
|
|
750
|
+
}
|
|
751
|
+
const prompt = (stringPrompt ?? streamingInitialPrompt ?? "").trim();
|
|
752
|
+
const args = this.buildArgs(prompt);
|
|
753
|
+
const spawnLine = `[CursorRunner] Spawn: ${cursorPath} ${args.join(" ")}`;
|
|
754
|
+
console.log(spawnLine);
|
|
755
|
+
if (this.logStream) {
|
|
756
|
+
this.logStream.write(`${spawnLine}\n`);
|
|
757
|
+
}
|
|
758
|
+
const child = spawn(cursorPath, args, {
|
|
759
|
+
cwd: this.config.workingDirectory || cwd(),
|
|
760
|
+
env: this.buildEnv(),
|
|
761
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
762
|
+
});
|
|
763
|
+
this.process = child;
|
|
764
|
+
this.readlineInterface = createInterface({
|
|
765
|
+
input: child.stdout,
|
|
766
|
+
crlfDelay: Infinity,
|
|
767
|
+
});
|
|
768
|
+
this.readlineInterface.on("line", (line) => this.handleStdoutLine(line));
|
|
769
|
+
child.stderr?.on("data", (data) => {
|
|
770
|
+
const text = data.toString().trim();
|
|
771
|
+
if (!text)
|
|
772
|
+
return;
|
|
773
|
+
this.errorMessages.push(text);
|
|
774
|
+
});
|
|
775
|
+
let caughtError;
|
|
776
|
+
try {
|
|
777
|
+
await new Promise((resolve, reject) => {
|
|
778
|
+
child.on("close", (code) => {
|
|
779
|
+
if (code === 0 || this.wasStopped) {
|
|
780
|
+
resolve();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
reject(new Error(`cursor-agent exited with code ${code}`));
|
|
784
|
+
});
|
|
785
|
+
child.on("error", reject);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
caughtError = error;
|
|
790
|
+
}
|
|
791
|
+
finally {
|
|
792
|
+
this.finalizeSession(caughtError);
|
|
793
|
+
}
|
|
794
|
+
return this.sessionInfo;
|
|
795
|
+
}
|
|
796
|
+
buildCursorPermissionsConfig() {
|
|
797
|
+
// Cursor CLI permission tokens reference:
|
|
798
|
+
// https://cursor.com/docs/cli/reference/permissions
|
|
799
|
+
const allowedTools = this.config.allowedTools || [];
|
|
800
|
+
const disallowedTools = this.config.disallowedTools || [];
|
|
801
|
+
const workspacePath = this.config.workingDirectory;
|
|
802
|
+
const allow = [
|
|
803
|
+
...new Set(allowedTools
|
|
804
|
+
.map(mapClaudeToolPatternToCursorPermission)
|
|
805
|
+
.filter((value) => Boolean(value))),
|
|
806
|
+
];
|
|
807
|
+
const autoScopeDenyPermissions = new Set();
|
|
808
|
+
if (workspacePath) {
|
|
809
|
+
if (allowedTools.some(isBroadReadToolPattern)) {
|
|
810
|
+
for (const permission of buildWorkspaceSiblingDenyPermissions(workspacePath, "Read")) {
|
|
811
|
+
autoScopeDenyPermissions.add(permission);
|
|
812
|
+
}
|
|
813
|
+
for (const permission of buildSystemRootDenyPermissions(workspacePath, "Read")) {
|
|
814
|
+
autoScopeDenyPermissions.add(permission);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (allowedTools.some(isBroadWriteToolPattern)) {
|
|
818
|
+
for (const permission of buildWorkspaceSiblingDenyPermissions(workspacePath, "Write")) {
|
|
819
|
+
autoScopeDenyPermissions.add(permission);
|
|
820
|
+
}
|
|
821
|
+
for (const permission of buildSystemRootDenyPermissions(workspacePath, "Write")) {
|
|
822
|
+
autoScopeDenyPermissions.add(permission);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const mappedDisallowedPermissions = disallowedTools
|
|
827
|
+
.map(mapClaudeToolPatternToCursorPermission)
|
|
828
|
+
.filter((value) => Boolean(value));
|
|
829
|
+
const deny = [
|
|
830
|
+
...new Set([...mappedDisallowedPermissions, ...autoScopeDenyPermissions].flat()),
|
|
831
|
+
];
|
|
832
|
+
return {
|
|
833
|
+
permissions: { allow, deny },
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
buildCursorMcpServersConfig() {
|
|
837
|
+
const servers = {};
|
|
838
|
+
for (const [serverName, rawConfig] of Object.entries(this.config.mcpConfig || {})) {
|
|
839
|
+
const configAny = rawConfig;
|
|
840
|
+
if (typeof configAny.listTools === "function" ||
|
|
841
|
+
typeof configAny.callTool === "function") {
|
|
842
|
+
console.warn(`[CursorRunner] Skipping MCP server '${serverName}' because in-process SDK server instances cannot be serialized to .cursor/mcp.json`);
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
const mapped = {};
|
|
846
|
+
if (typeof configAny.command === "string") {
|
|
847
|
+
mapped.command = configAny.command;
|
|
848
|
+
}
|
|
849
|
+
if (Array.isArray(configAny.args)) {
|
|
850
|
+
mapped.args = configAny.args;
|
|
851
|
+
}
|
|
852
|
+
if (configAny.env &&
|
|
853
|
+
typeof configAny.env === "object" &&
|
|
854
|
+
!Array.isArray(configAny.env)) {
|
|
855
|
+
mapped.env = configAny.env;
|
|
856
|
+
}
|
|
857
|
+
if (typeof configAny.cwd === "string") {
|
|
858
|
+
mapped.cwd = configAny.cwd;
|
|
859
|
+
}
|
|
860
|
+
if (typeof configAny.url === "string") {
|
|
861
|
+
mapped.url = configAny.url;
|
|
862
|
+
}
|
|
863
|
+
if (configAny.headers &&
|
|
864
|
+
typeof configAny.headers === "object" &&
|
|
865
|
+
!Array.isArray(configAny.headers)) {
|
|
866
|
+
mapped.headers = configAny.headers;
|
|
867
|
+
}
|
|
868
|
+
if (typeof configAny.timeout === "number") {
|
|
869
|
+
mapped.timeout = configAny.timeout;
|
|
870
|
+
}
|
|
871
|
+
if (!mapped.command && !mapped.url) {
|
|
872
|
+
console.warn(`[CursorRunner] Skipping MCP server '${serverName}' because it has no serializable command/url transport`);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
servers[serverName] = mapped;
|
|
876
|
+
}
|
|
877
|
+
return servers;
|
|
878
|
+
}
|
|
879
|
+
syncProjectMcpConfig() {
|
|
880
|
+
const workspacePath = this.config.workingDirectory;
|
|
881
|
+
if (!workspacePath) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const inlineServers = this.buildCursorMcpServersConfig();
|
|
885
|
+
if (Object.keys(inlineServers).length === 0) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const cursorDir = join(workspacePath, ".cursor");
|
|
889
|
+
const configPath = join(cursorDir, "mcp.json");
|
|
890
|
+
let existingConfig = { mcpServers: {} };
|
|
891
|
+
try {
|
|
892
|
+
if (existsSync(configPath)) {
|
|
893
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
894
|
+
if (parsed && typeof parsed === "object") {
|
|
895
|
+
existingConfig = parsed;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
// If existing config is malformed, overwrite with a valid mcpServers object.
|
|
901
|
+
}
|
|
902
|
+
const existingServers = existingConfig.mcpServers &&
|
|
903
|
+
typeof existingConfig.mcpServers === "object" &&
|
|
904
|
+
!Array.isArray(existingConfig.mcpServers)
|
|
905
|
+
? existingConfig.mcpServers
|
|
906
|
+
: {};
|
|
907
|
+
const nextConfig = {
|
|
908
|
+
...existingConfig,
|
|
909
|
+
mcpServers: {
|
|
910
|
+
...existingServers,
|
|
911
|
+
...inlineServers,
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
915
|
+
const backupPath = existsSync(configPath)
|
|
916
|
+
? `${configPath}.cyrus-backup-${Date.now()}-${process.pid}`
|
|
917
|
+
: null;
|
|
918
|
+
try {
|
|
919
|
+
if (backupPath) {
|
|
920
|
+
renameSync(configPath, backupPath);
|
|
921
|
+
}
|
|
922
|
+
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, "\t")}\n`, "utf8");
|
|
923
|
+
this.mcpConfigRestoreState = {
|
|
924
|
+
configPath,
|
|
925
|
+
backupPath,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
if (backupPath && existsSync(backupPath)) {
|
|
930
|
+
try {
|
|
931
|
+
renameSync(backupPath, configPath);
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
// Best effort rollback; start() will surface the original failure.
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
939
|
+
console.log(`[CursorRunner] Synced project MCP servers at ${configPath} (servers=${Object.keys(nextConfig.mcpServers).length}, backup=${backupPath ? "yes" : "no"}; docs: ${CURSOR_MCP_CONFIG_DOCS_URL})`);
|
|
940
|
+
}
|
|
941
|
+
enableCursorMcpServers() {
|
|
942
|
+
const workspacePath = this.config.workingDirectory;
|
|
943
|
+
if (!workspacePath) {
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const mcpCommand = process.env.CURSOR_MCP_COMMAND || "agent";
|
|
947
|
+
const listResult = spawnSync(mcpCommand, ["mcp", "list"], {
|
|
948
|
+
cwd: workspacePath,
|
|
949
|
+
env: this.buildEnv(),
|
|
950
|
+
encoding: "utf8",
|
|
951
|
+
});
|
|
952
|
+
if (listResult.error?.code === "ENOENT") {
|
|
953
|
+
console.warn(`[CursorRunner] Skipping MCP enable preflight: '${mcpCommand}' command not found`);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const discoveredServers = (listResult.status ?? 1) === 0
|
|
957
|
+
? parseMcpServersFromCursorListOutput(typeof listResult.stdout === "string" ? listResult.stdout : "")
|
|
958
|
+
: [];
|
|
959
|
+
if ((listResult.status ?? 1) !== 0 && !listResult.error) {
|
|
960
|
+
const detail = typeof listResult.stderr === "string" && listResult.stderr.trim()
|
|
961
|
+
? listResult.stderr.trim()
|
|
962
|
+
: `exit ${listResult.status ?? "unknown"}`;
|
|
963
|
+
console.warn(`[CursorRunner] MCP list preflight failed: '${mcpCommand} mcp list' (${detail})`);
|
|
964
|
+
}
|
|
965
|
+
// Cursor MCP enable preflight combines discovered servers and run-time inline config names.
|
|
966
|
+
// MCP location/reference: https://cursor.com/docs/context/mcp#configuration-locations
|
|
967
|
+
const inlineServers = Object.keys(this.config.mcpConfig || {});
|
|
968
|
+
const allServers = [
|
|
969
|
+
...new Set([...discoveredServers, ...inlineServers]),
|
|
970
|
+
].sort((a, b) => a.localeCompare(b));
|
|
971
|
+
for (const serverName of allServers) {
|
|
972
|
+
const enableResult = spawnSync(mcpCommand, ["mcp", "enable", serverName], {
|
|
973
|
+
cwd: workspacePath,
|
|
974
|
+
env: this.buildEnv(),
|
|
975
|
+
encoding: "utf8",
|
|
976
|
+
});
|
|
977
|
+
if (enableResult.error?.code ===
|
|
978
|
+
"ENOENT") {
|
|
979
|
+
console.warn(`[CursorRunner] Failed enabling MCP server '${serverName}': '${mcpCommand}' command not found`);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if ((enableResult.status ?? 1) !== 0 || enableResult.error) {
|
|
983
|
+
const detail = enableResult.error
|
|
984
|
+
? enableResult.error.message
|
|
985
|
+
: typeof enableResult.stderr === "string" &&
|
|
986
|
+
enableResult.stderr.trim()
|
|
987
|
+
? enableResult.stderr.trim()
|
|
988
|
+
: `exit ${enableResult.status ?? "unknown"}`;
|
|
989
|
+
console.warn(`[CursorRunner] Failed enabling MCP server '${serverName}' via '${mcpCommand} mcp enable ${serverName}': ${detail}`);
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
console.log(`[CursorRunner] Enabled MCP server '${serverName}' via '${mcpCommand} mcp enable ${serverName}'`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
syncProjectPermissionsConfig() {
|
|
996
|
+
const workspacePath = this.config.workingDirectory;
|
|
997
|
+
if (!workspacePath) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const mappedPermissions = this.buildCursorPermissionsConfig();
|
|
1001
|
+
const cursorDir = join(workspacePath, ".cursor");
|
|
1002
|
+
const configPath = join(cursorDir, "cli.json");
|
|
1003
|
+
let existingConfig = {
|
|
1004
|
+
permissions: { allow: [], deny: [] },
|
|
1005
|
+
};
|
|
1006
|
+
try {
|
|
1007
|
+
if (existsSync(configPath)) {
|
|
1008
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
1009
|
+
if (parsed && typeof parsed === "object") {
|
|
1010
|
+
existingConfig = parsed;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
// If existing config is malformed, overwrite with a valid permissions object.
|
|
1016
|
+
}
|
|
1017
|
+
const nextConfig = {
|
|
1018
|
+
...existingConfig,
|
|
1019
|
+
permissions: mappedPermissions.permissions,
|
|
1020
|
+
};
|
|
1021
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
1022
|
+
const backupPath = existsSync(configPath)
|
|
1023
|
+
? `${configPath}.cyrus-backup-${Date.now()}-${process.pid}`
|
|
1024
|
+
: null;
|
|
1025
|
+
try {
|
|
1026
|
+
if (backupPath) {
|
|
1027
|
+
renameSync(configPath, backupPath);
|
|
1028
|
+
}
|
|
1029
|
+
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, "\t")}\n`, "utf8");
|
|
1030
|
+
this.permissionsConfigRestoreState = {
|
|
1031
|
+
configPath,
|
|
1032
|
+
backupPath,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
if (backupPath && existsSync(backupPath)) {
|
|
1037
|
+
try {
|
|
1038
|
+
renameSync(backupPath, configPath);
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
// Best effort rollback; start() will surface the original failure.
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
throw error;
|
|
1045
|
+
}
|
|
1046
|
+
console.log(`[CursorRunner] Synced project permissions at ${configPath} (allow=${nextConfig.permissions.allow.length}, deny=${nextConfig.permissions.deny.length}, backup=${backupPath ? "yes" : "no"}; docs: ${CURSOR_CLI_PERMISSIONS_DOCS_URL})`);
|
|
1047
|
+
}
|
|
1048
|
+
restoreProjectPermissionsConfig() {
|
|
1049
|
+
const restoreState = this.permissionsConfigRestoreState;
|
|
1050
|
+
if (!restoreState) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
if (restoreState.backupPath) {
|
|
1055
|
+
if (existsSync(restoreState.configPath)) {
|
|
1056
|
+
unlinkSync(restoreState.configPath);
|
|
1057
|
+
}
|
|
1058
|
+
if (existsSync(restoreState.backupPath)) {
|
|
1059
|
+
renameSync(restoreState.backupPath, restoreState.configPath);
|
|
1060
|
+
console.log(`[CursorRunner] Restored original project permissions at ${restoreState.configPath}`);
|
|
1061
|
+
}
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (existsSync(restoreState.configPath)) {
|
|
1065
|
+
unlinkSync(restoreState.configPath);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1070
|
+
console.warn(`[CursorRunner] Failed to restore project permissions config at ${restoreState.configPath}: ${detail}`);
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
this.permissionsConfigRestoreState = null;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
restoreProjectMcpConfig() {
|
|
1077
|
+
const restoreState = this.mcpConfigRestoreState;
|
|
1078
|
+
if (!restoreState) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
if (restoreState.backupPath) {
|
|
1083
|
+
if (existsSync(restoreState.configPath)) {
|
|
1084
|
+
unlinkSync(restoreState.configPath);
|
|
1085
|
+
}
|
|
1086
|
+
if (existsSync(restoreState.backupPath)) {
|
|
1087
|
+
renameSync(restoreState.backupPath, restoreState.configPath);
|
|
1088
|
+
console.log(`[CursorRunner] Restored original project MCP config at ${restoreState.configPath}`);
|
|
1089
|
+
}
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (existsSync(restoreState.configPath)) {
|
|
1093
|
+
unlinkSync(restoreState.configPath);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1098
|
+
console.warn(`[CursorRunner] Failed to restore project MCP config at ${restoreState.configPath}: ${detail}`);
|
|
1099
|
+
}
|
|
1100
|
+
finally {
|
|
1101
|
+
this.mcpConfigRestoreState = null;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
checkCursorAgentVersion(cursorPath, expectedVersion) {
|
|
1105
|
+
const result = spawnSync(cursorPath, ["--version"], {
|
|
1106
|
+
encoding: "utf8",
|
|
1107
|
+
env: this.buildEnv(),
|
|
1108
|
+
});
|
|
1109
|
+
const actualVersion = result.stdout?.trim() || result.stderr?.trim() || "";
|
|
1110
|
+
if (!actualVersion) {
|
|
1111
|
+
return `cursor-agent version check failed: no output from \`${cursorPath} --version\``;
|
|
1112
|
+
}
|
|
1113
|
+
const normalizedActual = actualVersion.trim();
|
|
1114
|
+
const normalizedExpected = expectedVersion.trim();
|
|
1115
|
+
if (normalizedActual !== normalizedExpected) {
|
|
1116
|
+
return `cursor-agent version mismatch: expected \`${normalizedExpected}\` (tested), got \`${normalizedActual}\`. Set CYRUS_CURSOR_AGENT_VERSION to your version to skip this check, or upgrade cursor-agent to the tested version.`;
|
|
1117
|
+
}
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
buildArgs(prompt) {
|
|
1121
|
+
const args = ["--print", "--output-format", "stream-json"];
|
|
1122
|
+
const normalizedModel = normalizeCursorModel(this.config.model);
|
|
1123
|
+
// needed or else it errors
|
|
1124
|
+
args.push("--trust");
|
|
1125
|
+
if (normalizedModel) {
|
|
1126
|
+
args.push("--model", normalizedModel);
|
|
1127
|
+
}
|
|
1128
|
+
if (this.config.resumeSessionId) {
|
|
1129
|
+
args.push("--resume", this.config.resumeSessionId);
|
|
1130
|
+
}
|
|
1131
|
+
if (this.config.workingDirectory) {
|
|
1132
|
+
args.push("--workspace", this.config.workingDirectory);
|
|
1133
|
+
}
|
|
1134
|
+
if (this.config.sandbox) {
|
|
1135
|
+
args.push("--sandbox", this.config.sandbox);
|
|
1136
|
+
}
|
|
1137
|
+
if (this.config.approveMcps ?? true) {
|
|
1138
|
+
args.push("--approve-mcps");
|
|
1139
|
+
}
|
|
1140
|
+
if (prompt) {
|
|
1141
|
+
args.push(prompt);
|
|
1142
|
+
}
|
|
1143
|
+
return args;
|
|
1144
|
+
}
|
|
1145
|
+
buildEnv() {
|
|
1146
|
+
const env = { ...process.env };
|
|
1147
|
+
if (this.config.cursorApiKey) {
|
|
1148
|
+
env.CURSOR_API_KEY = this.config.cursorApiKey;
|
|
1149
|
+
}
|
|
1150
|
+
return env;
|
|
1151
|
+
}
|
|
1152
|
+
handleStdoutLine(line) {
|
|
1153
|
+
const trimmed = line.trim();
|
|
1154
|
+
if (!trimmed) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (this.logStream) {
|
|
1158
|
+
this.logStream.write(`${trimmed}\n`);
|
|
1159
|
+
}
|
|
1160
|
+
const parsed = this.parseJsonLine(trimmed);
|
|
1161
|
+
if (!parsed) {
|
|
1162
|
+
this.fallbackOutputLines.push(trimmed);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
this.handleEvent(parsed);
|
|
1166
|
+
}
|
|
1167
|
+
parseJsonLine(line) {
|
|
1168
|
+
if (!(line.startsWith("{") || line.startsWith("["))) {
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
try {
|
|
1172
|
+
const parsed = JSON.parse(line);
|
|
1173
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
return parsed;
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
handleEvent(event) {
|
|
1183
|
+
this.emit("streamEvent", event);
|
|
1184
|
+
const eventObj = event;
|
|
1185
|
+
const type = getStringValue(eventObj, "type");
|
|
1186
|
+
if (!type) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (type === "init" ||
|
|
1190
|
+
(type === "system" && getStringValue(eventObj, "subtype") === "init")) {
|
|
1191
|
+
const sessionId = getStringValue(eventObj, "session_id") || this.sessionInfo?.sessionId;
|
|
1192
|
+
if (sessionId && this.sessionInfo) {
|
|
1193
|
+
this.sessionInfo.sessionId = sessionId;
|
|
1194
|
+
}
|
|
1195
|
+
this.emitInitMessage();
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (type === "message") {
|
|
1199
|
+
this.emitInitMessage();
|
|
1200
|
+
this.handleMessageEvent(eventObj);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (type === "assistant") {
|
|
1204
|
+
this.emitInitMessage();
|
|
1205
|
+
const messageObj = eventObj.message;
|
|
1206
|
+
const content = messageObj && typeof messageObj === "object"
|
|
1207
|
+
? extractTextFromMessageContent(messageObj.content)
|
|
1208
|
+
: "";
|
|
1209
|
+
if (content) {
|
|
1210
|
+
this.handleMessageEvent({
|
|
1211
|
+
role: "assistant",
|
|
1212
|
+
content,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (type === "item.started" || type === "item.completed") {
|
|
1218
|
+
this.emitInitMessage();
|
|
1219
|
+
const item = eventObj.item;
|
|
1220
|
+
if (item && typeof item === "object") {
|
|
1221
|
+
this.handleItemEvent(type, item);
|
|
1222
|
+
}
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (type === "tool_call") {
|
|
1226
|
+
this.emitInitMessage();
|
|
1227
|
+
this.handleToolCallEvent(eventObj);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (type === "turn.completed" || type === "result") {
|
|
1231
|
+
const usage = extractUsageFromEvent(eventObj);
|
|
1232
|
+
if (usage) {
|
|
1233
|
+
this.lastUsage = usage;
|
|
1234
|
+
}
|
|
1235
|
+
const stopReason = getStringValue(eventObj, "stop_reason");
|
|
1236
|
+
if (stopReason?.toLowerCase().includes("max")) {
|
|
1237
|
+
const result = this.createErrorResultMessage(`Cursor turn limit reached: ${stopReason}`);
|
|
1238
|
+
this.pendingResultMessage = result;
|
|
1239
|
+
}
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (type === "error") {
|
|
1243
|
+
const message = getStringValue(eventObj, "message") || "Cursor execution failed";
|
|
1244
|
+
this.errorMessages.push(message);
|
|
1245
|
+
this.pendingResultMessage = this.createErrorResultMessage(message);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
handleMessageEvent(event) {
|
|
1249
|
+
const role = getStringValue(event, "role");
|
|
1250
|
+
const content = getStringValue(event, "content") || "";
|
|
1251
|
+
if (!content) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (role === "assistant") {
|
|
1255
|
+
this.lastAssistantText = content;
|
|
1256
|
+
const message = {
|
|
1257
|
+
type: "assistant",
|
|
1258
|
+
message: createAssistantBetaMessage(content),
|
|
1259
|
+
parent_tool_use_id: null,
|
|
1260
|
+
uuid: crypto.randomUUID(),
|
|
1261
|
+
session_id: this.sessionInfo?.sessionId || "pending",
|
|
1262
|
+
};
|
|
1263
|
+
this.pushMessage(message);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (role === "user") {
|
|
1267
|
+
const message = {
|
|
1268
|
+
type: "user",
|
|
1269
|
+
message: {
|
|
1270
|
+
role: "user",
|
|
1271
|
+
content: [{ type: "text", text: content }],
|
|
1272
|
+
},
|
|
1273
|
+
parent_tool_use_id: null,
|
|
1274
|
+
session_id: this.sessionInfo?.sessionId || "pending",
|
|
1275
|
+
};
|
|
1276
|
+
this.pushMessage(message);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
handleItemEvent(type, item) {
|
|
1280
|
+
const projection = getProjectionForItem(item, this.config.workingDirectory);
|
|
1281
|
+
if (!projection) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (type === "item.started") {
|
|
1285
|
+
this.emitToolUse(projection);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
this.emitToolUse(projection);
|
|
1289
|
+
this.emitToolResult(projection);
|
|
1290
|
+
}
|
|
1291
|
+
handleToolCallEvent(event) {
|
|
1292
|
+
const projection = getProjectionForToolCallEvent(event, this.config.workingDirectory);
|
|
1293
|
+
if (!projection) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const subtype = getStringValue(event, "subtype") || "started";
|
|
1297
|
+
if (subtype === "started") {
|
|
1298
|
+
this.emitToolUse(projection);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (subtype === "completed" || subtype === "failed") {
|
|
1302
|
+
this.emitToolUse(projection);
|
|
1303
|
+
this.emitToolResult({
|
|
1304
|
+
...projection,
|
|
1305
|
+
isError: projection.isError || subtype === "failed",
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
emitToolUse(projection) {
|
|
1310
|
+
if (this.emittedToolUseIds.has(projection.toolUseId)) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
this.emittedToolUseIds.add(projection.toolUseId);
|
|
1314
|
+
const message = {
|
|
1315
|
+
type: "assistant",
|
|
1316
|
+
message: createAssistantToolUseMessage(projection.toolUseId, projection.toolName, projection.toolInput),
|
|
1317
|
+
parent_tool_use_id: null,
|
|
1318
|
+
uuid: crypto.randomUUID(),
|
|
1319
|
+
session_id: this.sessionInfo?.sessionId || "pending",
|
|
1320
|
+
};
|
|
1321
|
+
this.pushMessage(message);
|
|
1322
|
+
}
|
|
1323
|
+
emitToolResult(projection) {
|
|
1324
|
+
const message = {
|
|
1325
|
+
type: "user",
|
|
1326
|
+
message: createUserToolResultMessage(projection.toolUseId, projection.result, projection.isError),
|
|
1327
|
+
parent_tool_use_id: projection.toolUseId,
|
|
1328
|
+
session_id: this.sessionInfo?.sessionId || "pending",
|
|
1329
|
+
};
|
|
1330
|
+
this.pushMessage(message);
|
|
1331
|
+
}
|
|
1332
|
+
emitInitMessage() {
|
|
1333
|
+
if (this.hasInitMessage) {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
this.hasInitMessage = true;
|
|
1337
|
+
const sessionId = this.sessionInfo?.sessionId || crypto.randomUUID();
|
|
1338
|
+
const permissionModeByCursorConfig = {
|
|
1339
|
+
never: "dontAsk",
|
|
1340
|
+
"on-request": "default",
|
|
1341
|
+
"on-failure": "default",
|
|
1342
|
+
untrusted: "default",
|
|
1343
|
+
};
|
|
1344
|
+
const initMessage = {
|
|
1345
|
+
type: "system",
|
|
1346
|
+
subtype: "init",
|
|
1347
|
+
cwd: this.config.workingDirectory || cwd(),
|
|
1348
|
+
session_id: sessionId,
|
|
1349
|
+
tools: this.config.allowedTools || [],
|
|
1350
|
+
mcp_servers: [],
|
|
1351
|
+
model: this.config.model || "gpt-5",
|
|
1352
|
+
permissionMode: this.config.askForApproval
|
|
1353
|
+
? permissionModeByCursorConfig[this.config.askForApproval]
|
|
1354
|
+
: "default",
|
|
1355
|
+
apiKeySource: this.config.cursorApiKey ? "user" : "project",
|
|
1356
|
+
claude_code_version: "cursor-agent",
|
|
1357
|
+
slash_commands: [],
|
|
1358
|
+
output_style: "default",
|
|
1359
|
+
skills: [],
|
|
1360
|
+
plugins: [],
|
|
1361
|
+
uuid: crypto.randomUUID(),
|
|
1362
|
+
agents: undefined,
|
|
1363
|
+
};
|
|
1364
|
+
this.pushMessage(initMessage);
|
|
1365
|
+
}
|
|
1366
|
+
createSuccessResultMessage(result) {
|
|
1367
|
+
const durationMs = Math.max(Date.now() - this.startTimestampMs, 0);
|
|
1368
|
+
return {
|
|
1369
|
+
type: "result",
|
|
1370
|
+
subtype: "success",
|
|
1371
|
+
duration_ms: durationMs,
|
|
1372
|
+
duration_api_ms: 0,
|
|
1373
|
+
is_error: false,
|
|
1374
|
+
num_turns: 1,
|
|
1375
|
+
result,
|
|
1376
|
+
stop_reason: null,
|
|
1377
|
+
total_cost_usd: 0,
|
|
1378
|
+
usage: createResultUsage(this.lastUsage),
|
|
1379
|
+
modelUsage: {},
|
|
1380
|
+
permission_denials: [],
|
|
1381
|
+
uuid: crypto.randomUUID(),
|
|
1382
|
+
session_id: this.sessionInfo?.sessionId || "pending",
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
createErrorResultMessage(errorMessage) {
|
|
1386
|
+
const durationMs = Math.max(Date.now() - this.startTimestampMs, 0);
|
|
1387
|
+
return {
|
|
1388
|
+
type: "result",
|
|
1389
|
+
subtype: "error_during_execution",
|
|
1390
|
+
duration_ms: durationMs,
|
|
1391
|
+
duration_api_ms: 0,
|
|
1392
|
+
is_error: true,
|
|
1393
|
+
num_turns: 1,
|
|
1394
|
+
errors: [errorMessage],
|
|
1395
|
+
stop_reason: null,
|
|
1396
|
+
total_cost_usd: 0,
|
|
1397
|
+
usage: createResultUsage(this.lastUsage),
|
|
1398
|
+
modelUsage: {},
|
|
1399
|
+
permission_denials: [],
|
|
1400
|
+
uuid: crypto.randomUUID(),
|
|
1401
|
+
session_id: this.sessionInfo?.sessionId || "pending",
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
pushMessage(message) {
|
|
1405
|
+
this.messages.push(message);
|
|
1406
|
+
this.emit("message", message);
|
|
1407
|
+
}
|
|
1408
|
+
setupLogging(sessionId) {
|
|
1409
|
+
try {
|
|
1410
|
+
const logsDir = join(this.config.cyrusHome, "logs");
|
|
1411
|
+
mkdirSync(logsDir, { recursive: true });
|
|
1412
|
+
this.logStream = createWriteStream(join(logsDir, `cursor-${sessionId}.jsonl`), { flags: "a" });
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
this.logStream = null;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
finalizeSession(error) {
|
|
1419
|
+
if (!this.sessionInfo) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
this.emitInitMessage();
|
|
1423
|
+
this.sessionInfo.isRunning = false;
|
|
1424
|
+
this.restoreProjectMcpConfig();
|
|
1425
|
+
this.restoreProjectPermissionsConfig();
|
|
1426
|
+
let resultMessage;
|
|
1427
|
+
if (this.pendingResultMessage) {
|
|
1428
|
+
resultMessage = this.pendingResultMessage;
|
|
1429
|
+
}
|
|
1430
|
+
else if (error || this.errorMessages.length > 0) {
|
|
1431
|
+
const message = normalizeError(error) ||
|
|
1432
|
+
this.errorMessages.at(-1) ||
|
|
1433
|
+
"Cursor execution failed";
|
|
1434
|
+
resultMessage = this.createErrorResultMessage(message);
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
const fallbackOutput = this.fallbackOutputLines.join("\n").trim();
|
|
1438
|
+
resultMessage = this.createSuccessResultMessage(this.lastAssistantText ||
|
|
1439
|
+
fallbackOutput ||
|
|
1440
|
+
"Cursor session completed successfully");
|
|
1441
|
+
}
|
|
1442
|
+
this.pushMessage(resultMessage);
|
|
1443
|
+
this.emit("complete", [...this.messages]);
|
|
1444
|
+
if (error || this.errorMessages.length > 0) {
|
|
1445
|
+
const err = error instanceof Error
|
|
1446
|
+
? error
|
|
1447
|
+
: new Error(this.errorMessages.at(-1) || "Cursor execution failed");
|
|
1448
|
+
this.emit("error", err);
|
|
1449
|
+
}
|
|
1450
|
+
this.cleanupRuntimeState();
|
|
1451
|
+
}
|
|
1452
|
+
cleanupRuntimeState() {
|
|
1453
|
+
if (this.readlineInterface) {
|
|
1454
|
+
this.readlineInterface.close();
|
|
1455
|
+
this.readlineInterface = null;
|
|
1456
|
+
}
|
|
1457
|
+
if (this.logStream) {
|
|
1458
|
+
this.logStream.end();
|
|
1459
|
+
this.logStream = null;
|
|
1460
|
+
}
|
|
1461
|
+
this.process = null;
|
|
1462
|
+
this.pendingResultMessage = null;
|
|
1463
|
+
}
|
|
1464
|
+
stop() {
|
|
1465
|
+
this.wasStopped = true;
|
|
1466
|
+
if (this.process && !this.process.killed) {
|
|
1467
|
+
this.process.kill();
|
|
1468
|
+
}
|
|
1469
|
+
if (this.sessionInfo) {
|
|
1470
|
+
this.sessionInfo.isRunning = false;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
isRunning() {
|
|
1474
|
+
return this.sessionInfo?.isRunning ?? false;
|
|
1475
|
+
}
|
|
1476
|
+
getMessages() {
|
|
1477
|
+
return [...this.messages];
|
|
1478
|
+
}
|
|
1479
|
+
getFormatter() {
|
|
1480
|
+
return this.formatter;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
//# sourceMappingURL=CursorRunner.js.map
|