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