@agentconnect/host 0.2.0

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.
@@ -0,0 +1,866 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import { buildInstallCommand, buildLoginCommand, buildStatusCommand, checkCommandVersion, commandExists, createLineParser, debugLog, resolveWindowsCommand, resolveCommandPath, resolveCommandRealPath, runCommand, } from './utils.js';
4
+ const INSTALL_UNIX = 'curl https://cursor.com/install -fsS | bash';
5
+ const DEFAULT_LOGIN = 'cursor-agent login';
6
+ const DEFAULT_STATUS = 'cursor-agent status';
7
+ const CURSOR_MODELS_COMMAND = 'cursor-agent models';
8
+ const CURSOR_MODELS_CACHE_TTL_MS = 60_000;
9
+ const CURSOR_UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
10
+ let cursorModelsCache = null;
11
+ let cursorModelsCacheAt = 0;
12
+ let cursorUpdateCache = null;
13
+ let cursorUpdatePromise = null;
14
+ function trimOutput(value, limit = 400) {
15
+ const cleaned = value.trim();
16
+ if (!cleaned)
17
+ return '';
18
+ if (cleaned.length <= limit)
19
+ return cleaned;
20
+ return `${cleaned.slice(0, limit)}...`;
21
+ }
22
+ function normalizePath(value) {
23
+ const normalized = value.replace(/\\/g, '/');
24
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
25
+ }
26
+ function parseSemver(value) {
27
+ if (!value)
28
+ return null;
29
+ const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
30
+ if (!match)
31
+ return null;
32
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
33
+ }
34
+ function compareSemver(a, b) {
35
+ if (a[0] !== b[0])
36
+ return a[0] - b[0];
37
+ if (a[1] !== b[1])
38
+ return a[1] - b[1];
39
+ return a[2] - b[2];
40
+ }
41
+ async function fetchBrewCaskVersion(cask) {
42
+ if (!commandExists('brew'))
43
+ return null;
44
+ const result = await runCommand('brew', ['info', '--json=v2', '--cask', cask]);
45
+ if (result.code !== 0)
46
+ return null;
47
+ try {
48
+ const parsed = JSON.parse(result.stdout);
49
+ const version = parsed?.casks?.[0]?.version;
50
+ return typeof version === 'string' ? version : null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function getCursorUpdateAction(commandPath) {
57
+ if (!commandPath)
58
+ return null;
59
+ const normalized = normalizePath(commandPath);
60
+ if (normalized.includes('/cellar/') ||
61
+ normalized.includes('/caskroom/') ||
62
+ normalized.includes('/homebrew/')) {
63
+ return {
64
+ command: 'brew',
65
+ args: ['upgrade', '--cask', 'cursor'],
66
+ source: 'brew',
67
+ commandLabel: 'brew upgrade --cask cursor',
68
+ };
69
+ }
70
+ if (normalized.includes('/.local/bin/') ||
71
+ normalized.includes('/.local/share/cursor-agent/versions/')) {
72
+ return {
73
+ command: 'bash',
74
+ args: ['-lc', INSTALL_UNIX],
75
+ source: 'script',
76
+ commandLabel: INSTALL_UNIX,
77
+ };
78
+ }
79
+ return null;
80
+ }
81
+ export function getCursorCommand() {
82
+ const override = process.env.AGENTCONNECT_CURSOR_COMMAND;
83
+ const base = override || 'cursor-agent';
84
+ const resolved = resolveCommandPath(base);
85
+ return resolved || resolveWindowsCommand(base);
86
+ }
87
+ function getCursorApiKey() {
88
+ return process.env.CURSOR_API_KEY || process.env.AGENTCONNECT_CURSOR_API_KEY || '';
89
+ }
90
+ function getCursorDefaultModel() {
91
+ return process.env.AGENTCONNECT_CURSOR_MODEL?.trim() || '';
92
+ }
93
+ function resolveCursorEndpoint() {
94
+ return process.env.AGENTCONNECT_CURSOR_ENDPOINT?.trim() || '';
95
+ }
96
+ function withCursorEndpoint(args) {
97
+ const endpoint = resolveCursorEndpoint();
98
+ if (!endpoint)
99
+ return args;
100
+ if (args.includes('--endpoint'))
101
+ return args;
102
+ return [...args, '--endpoint', endpoint];
103
+ }
104
+ function resolveCursorModel(model, fallback) {
105
+ if (!model)
106
+ return fallback;
107
+ const raw = String(model);
108
+ if (raw === 'cursor' || raw === 'cursor-default')
109
+ return fallback;
110
+ if (raw.startsWith('cursor:'))
111
+ return raw.slice('cursor:'.length);
112
+ if (raw.startsWith('cursor/'))
113
+ return raw.slice('cursor/'.length);
114
+ return raw;
115
+ }
116
+ function formatCursorDefaultLabel(fallback) {
117
+ if (!fallback)
118
+ return 'Default';
119
+ return `Default · ${fallback}`;
120
+ }
121
+ function normalizeCursorModelId(value) {
122
+ if (value.startsWith('cursor:') || value.startsWith('cursor/'))
123
+ return value;
124
+ return `cursor:${value}`;
125
+ }
126
+ function normalizeCursorModelDisplay(value) {
127
+ if (value.startsWith('cursor:'))
128
+ return value.slice('cursor:'.length);
129
+ if (value.startsWith('cursor/'))
130
+ return value.slice('cursor/'.length);
131
+ return value;
132
+ }
133
+ async function listCursorModelsFromCli() {
134
+ const command = getCursorCommand();
135
+ if (!commandExists(command))
136
+ return [];
137
+ const modelsCommand = buildStatusCommand('AGENTCONNECT_CURSOR_MODELS_COMMAND', CURSOR_MODELS_COMMAND);
138
+ if (!modelsCommand.command)
139
+ return [];
140
+ const resolvedCommand = resolveWindowsCommand(modelsCommand.command);
141
+ const result = await runCommand(resolvedCommand, withCursorEndpoint(modelsCommand.args), {
142
+ env: buildCursorEnv(),
143
+ });
144
+ const output = `${result.stdout}\n${result.stderr}`.trim();
145
+ if (!output)
146
+ return [];
147
+ const parsed = safeJsonParse(output);
148
+ if (Array.isArray(parsed)) {
149
+ const models = parsed
150
+ .map((entry) => {
151
+ if (typeof entry === 'string' && entry.trim()) {
152
+ const value = entry.trim();
153
+ return {
154
+ id: normalizeCursorModelId(value),
155
+ provider: 'cursor',
156
+ displayName: normalizeCursorModelDisplay(value),
157
+ };
158
+ }
159
+ if (entry && typeof entry === 'object') {
160
+ const record = entry;
161
+ const idRaw = typeof record.id === 'string' ? record.id.trim() : '';
162
+ const nameRaw = typeof record.name === 'string' ? record.name.trim() : '';
163
+ const displayRaw = typeof record.displayName === 'string' ? record.displayName.trim() : '';
164
+ const value = idRaw || nameRaw || displayRaw;
165
+ if (!value)
166
+ return null;
167
+ return {
168
+ id: normalizeCursorModelId(value),
169
+ provider: 'cursor',
170
+ displayName: normalizeCursorModelDisplay(displayRaw || nameRaw || value),
171
+ };
172
+ }
173
+ return null;
174
+ })
175
+ .filter(Boolean);
176
+ return models;
177
+ }
178
+ const lines = output
179
+ .split('\n')
180
+ .map((line) => line.trim())
181
+ .filter(Boolean)
182
+ .filter((line) => !line.toLowerCase().startsWith('model'))
183
+ .filter((line) => !/^[-=]{2,}$/.test(line));
184
+ return lines.map((line) => {
185
+ const cleaned = line.replace(/^[-*•]\s*/, '');
186
+ const value = cleaned.split(/\s+/)[0] || cleaned;
187
+ return {
188
+ id: normalizeCursorModelId(value),
189
+ provider: 'cursor',
190
+ displayName: normalizeCursorModelDisplay(value),
191
+ };
192
+ });
193
+ }
194
+ export async function listCursorModels() {
195
+ if (cursorModelsCache && Date.now() - cursorModelsCacheAt < CURSOR_MODELS_CACHE_TTL_MS) {
196
+ return cursorModelsCache;
197
+ }
198
+ const fallback = getCursorDefaultModel();
199
+ const base = [
200
+ {
201
+ id: 'cursor-default',
202
+ provider: 'cursor',
203
+ displayName: formatCursorDefaultLabel(fallback),
204
+ },
205
+ ];
206
+ const envModels = process.env.AGENTCONNECT_CURSOR_MODELS;
207
+ if (envModels) {
208
+ try {
209
+ const parsed = JSON.parse(envModels);
210
+ if (Array.isArray(parsed)) {
211
+ for (const entry of parsed) {
212
+ if (typeof entry === 'string' && entry.trim()) {
213
+ const trimmed = entry.trim();
214
+ base.push({
215
+ id: normalizeCursorModelId(trimmed),
216
+ provider: 'cursor',
217
+ displayName: normalizeCursorModelDisplay(trimmed),
218
+ });
219
+ }
220
+ }
221
+ }
222
+ }
223
+ catch {
224
+ // ignore invalid json
225
+ }
226
+ }
227
+ const cliModels = await listCursorModelsFromCli();
228
+ base.push(...cliModels);
229
+ const seen = new Set();
230
+ const list = base.filter((entry) => {
231
+ const key = `${entry.provider}:${entry.id}`;
232
+ if (seen.has(key))
233
+ return false;
234
+ seen.add(key);
235
+ return true;
236
+ });
237
+ cursorModelsCache = list;
238
+ cursorModelsCacheAt = Date.now();
239
+ return list;
240
+ }
241
+ function normalizeCursorStatusOutput(output) {
242
+ const text = output.toLowerCase();
243
+ if (text.includes('not authenticated') ||
244
+ text.includes('not logged in') ||
245
+ text.includes('login required') ||
246
+ text.includes('please login') ||
247
+ text.includes('please log in') ||
248
+ text.includes('unauthorized')) {
249
+ return false;
250
+ }
251
+ if (text.includes('authenticated') ||
252
+ text.includes('logged in') ||
253
+ text.includes('signed in') ||
254
+ text.includes('account')) {
255
+ return true;
256
+ }
257
+ return null;
258
+ }
259
+ function getCursorUpdateSnapshot(commandPath) {
260
+ if (cursorUpdateCache && Date.now() - cursorUpdateCache.checkedAt < CURSOR_UPDATE_CACHE_TTL_MS) {
261
+ const action = getCursorUpdateAction(commandPath);
262
+ return {
263
+ updateAvailable: cursorUpdateCache.updateAvailable,
264
+ latestVersion: cursorUpdateCache.latestVersion,
265
+ updateCheckedAt: cursorUpdateCache.checkedAt,
266
+ updateSource: action?.source ?? 'unknown',
267
+ updateCommand: action?.commandLabel,
268
+ updateMessage: cursorUpdateCache.updateMessage,
269
+ };
270
+ }
271
+ return {};
272
+ }
273
+ function ensureCursorUpdateCheck(currentVersion, commandPath) {
274
+ if (cursorUpdateCache && Date.now() - cursorUpdateCache.checkedAt < CURSOR_UPDATE_CACHE_TTL_MS) {
275
+ return;
276
+ }
277
+ if (cursorUpdatePromise)
278
+ return;
279
+ cursorUpdatePromise = (async () => {
280
+ const action = getCursorUpdateAction(commandPath || null);
281
+ let latest = null;
282
+ let updateAvailable;
283
+ let updateMessage;
284
+ if (action?.source === 'brew') {
285
+ latest = await fetchBrewCaskVersion('cursor');
286
+ }
287
+ if (latest && currentVersion) {
288
+ const a = parseSemver(currentVersion);
289
+ const b = parseSemver(latest);
290
+ if (a && b) {
291
+ updateAvailable = compareSemver(a, b) < 0;
292
+ updateMessage = updateAvailable
293
+ ? `Update available: ${currentVersion} -> ${latest}`
294
+ : `Up to date (${currentVersion})`;
295
+ }
296
+ }
297
+ else if (!action) {
298
+ updateMessage = 'Update check unavailable';
299
+ }
300
+ debugLog('Cursor', 'update-check', {
301
+ updateAvailable,
302
+ message: updateMessage,
303
+ });
304
+ cursorUpdateCache = {
305
+ checkedAt: Date.now(),
306
+ updateAvailable,
307
+ latestVersion: latest ?? undefined,
308
+ updateMessage,
309
+ };
310
+ })().finally(() => {
311
+ cursorUpdatePromise = null;
312
+ });
313
+ }
314
+ export async function ensureCursorInstalled() {
315
+ const command = getCursorCommand();
316
+ const versionCheck = await checkCommandVersion(command, [['--version'], ['-V']]);
317
+ debugLog('Cursor', 'install-check', {
318
+ command,
319
+ versionOk: versionCheck.ok,
320
+ version: versionCheck.version,
321
+ });
322
+ if (versionCheck.ok) {
323
+ return { installed: true, version: versionCheck.version || undefined };
324
+ }
325
+ if (commandExists(command)) {
326
+ return { installed: true, version: undefined };
327
+ }
328
+ const override = buildInstallCommand('AGENTCONNECT_CURSOR_INSTALL', '');
329
+ let install = override;
330
+ let packageManager = override.command ? 'unknown' : 'unknown';
331
+ if (!install.command) {
332
+ if (process.platform !== 'win32' && commandExists('bash') && commandExists('curl')) {
333
+ install = { command: 'bash', args: ['-lc', INSTALL_UNIX] };
334
+ packageManager = 'script';
335
+ }
336
+ }
337
+ if (!install.command) {
338
+ return { installed: false, version: undefined, packageManager };
339
+ }
340
+ await runCommand(install.command, install.args, { shell: process.platform === 'win32' });
341
+ const after = await checkCommandVersion(command, [['--version'], ['-V']]);
342
+ return {
343
+ installed: after.ok,
344
+ version: after.version || undefined,
345
+ packageManager,
346
+ };
347
+ }
348
+ export async function getCursorStatus() {
349
+ const command = getCursorCommand();
350
+ const versionCheck = await checkCommandVersion(command, [['--version'], ['-V']]);
351
+ const installed = versionCheck.ok || commandExists(command);
352
+ let loggedIn = false;
353
+ if (installed) {
354
+ const status = buildStatusCommand('AGENTCONNECT_CURSOR_STATUS', DEFAULT_STATUS);
355
+ if (status.command) {
356
+ const statusCommand = resolveWindowsCommand(status.command);
357
+ const result = await runCommand(statusCommand, withCursorEndpoint(status.args), {
358
+ env: buildCursorEnv(),
359
+ });
360
+ const output = `${result.stdout}\n${result.stderr}`;
361
+ const parsed = normalizeCursorStatusOutput(output);
362
+ loggedIn = parsed ?? result.code === 0;
363
+ }
364
+ if (!loggedIn) {
365
+ loggedIn = Boolean(getCursorApiKey().trim());
366
+ }
367
+ }
368
+ if (installed) {
369
+ const resolved = resolveCommandRealPath(command);
370
+ ensureCursorUpdateCheck(versionCheck.version, resolved || null);
371
+ }
372
+ const resolved = resolveCommandRealPath(command);
373
+ const updateInfo = installed ? getCursorUpdateSnapshot(resolved || null) : {};
374
+ return { installed, loggedIn, version: versionCheck.version || undefined, ...updateInfo };
375
+ }
376
+ export async function getCursorFastStatus() {
377
+ const command = getCursorCommand();
378
+ const installed = commandExists(command);
379
+ if (!installed) {
380
+ return { installed: false, loggedIn: false };
381
+ }
382
+ return { installed: true, loggedIn: true };
383
+ }
384
+ export async function updateCursor() {
385
+ const command = getCursorCommand();
386
+ if (!commandExists(command)) {
387
+ return { installed: false, loggedIn: false };
388
+ }
389
+ const resolved = resolveCommandRealPath(command);
390
+ const updateOverride = buildStatusCommand('AGENTCONNECT_CURSOR_UPDATE', '');
391
+ const action = updateOverride.command ? null : getCursorUpdateAction(resolved || null);
392
+ const updateCommand = updateOverride.command || action?.command || '';
393
+ const updateArgs = updateOverride.command ? updateOverride.args : action?.args || [];
394
+ if (!updateCommand) {
395
+ throw new Error('No update command available. Please update Cursor manually.');
396
+ }
397
+ const cmd = resolveWindowsCommand(updateCommand);
398
+ debugLog('Cursor', 'update-run', { command: cmd, args: updateArgs });
399
+ const result = await runCommand(cmd, updateArgs, { env: buildCursorEnv() });
400
+ debugLog('Cursor', 'update-result', {
401
+ code: result.code,
402
+ stdout: trimOutput(result.stdout),
403
+ stderr: trimOutput(result.stderr),
404
+ });
405
+ if (result.code !== 0 && result.code !== null) {
406
+ const message = trimOutput(`${result.stdout}\n${result.stderr}`, 800) || 'Update failed';
407
+ throw new Error(message);
408
+ }
409
+ cursorUpdateCache = null;
410
+ cursorUpdatePromise = null;
411
+ return getCursorStatus();
412
+ }
413
+ function buildCursorEnv() {
414
+ const env = { ...process.env };
415
+ const apiKey = getCursorApiKey().trim();
416
+ if (apiKey) {
417
+ env.CURSOR_API_KEY = apiKey;
418
+ }
419
+ return env;
420
+ }
421
+ export async function loginCursor(options) {
422
+ if (typeof options?.apiKey === 'string') {
423
+ process.env.CURSOR_API_KEY = options.apiKey;
424
+ }
425
+ if (typeof options?.baseUrl === 'string') {
426
+ process.env.AGENTCONNECT_CURSOR_ENDPOINT = options.baseUrl;
427
+ }
428
+ if (typeof options?.model === 'string') {
429
+ process.env.AGENTCONNECT_CURSOR_MODEL = options.model;
430
+ cursorModelsCache = null;
431
+ cursorModelsCacheAt = 0;
432
+ }
433
+ if (Array.isArray(options?.models)) {
434
+ process.env.AGENTCONNECT_CURSOR_MODELS = JSON.stringify(options.models.filter(Boolean));
435
+ cursorModelsCache = null;
436
+ cursorModelsCacheAt = 0;
437
+ }
438
+ if (!options?.apiKey) {
439
+ const login = buildLoginCommand('AGENTCONNECT_CURSOR_LOGIN', DEFAULT_LOGIN);
440
+ if (login.command) {
441
+ const command = resolveWindowsCommand(login.command);
442
+ await runCommand(command, withCursorEndpoint(login.args), { env: buildCursorEnv() });
443
+ }
444
+ }
445
+ const timeoutMs = Number(process.env.AGENTCONNECT_CURSOR_LOGIN_TIMEOUT_MS || 20_000);
446
+ const pollIntervalMs = Number(process.env.AGENTCONNECT_CURSOR_LOGIN_POLL_MS || 1_000);
447
+ const start = Date.now();
448
+ let status = await getCursorStatus();
449
+ while (!status.loggedIn && Date.now() - start < timeoutMs) {
450
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
451
+ status = await getCursorStatus();
452
+ }
453
+ return { loggedIn: status.loggedIn };
454
+ }
455
+ function safeJsonParse(line) {
456
+ try {
457
+ return JSON.parse(line);
458
+ }
459
+ catch {
460
+ return null;
461
+ }
462
+ }
463
+ function extractSessionId(ev) {
464
+ const id = ev.session_id ?? ev.sessionId;
465
+ return typeof id === 'string' ? id : null;
466
+ }
467
+ function extractUsage(ev) {
468
+ const usage = ev.usage ?? ev.token_usage ?? ev.tokenUsage ?? ev.tokens;
469
+ if (!usage || typeof usage !== 'object')
470
+ return null;
471
+ const toNumber = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
472
+ const input = toNumber(usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens);
473
+ const output = toNumber(usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens);
474
+ const total = toNumber(usage.total_tokens ?? usage.totalTokens);
475
+ const out = {};
476
+ if (input !== undefined)
477
+ out.input_tokens = input;
478
+ if (output !== undefined)
479
+ out.output_tokens = output;
480
+ if (total !== undefined)
481
+ out.total_tokens = total;
482
+ return Object.keys(out).length ? out : null;
483
+ }
484
+ function extractTextFromContent(content) {
485
+ if (!content)
486
+ return '';
487
+ if (typeof content === 'string')
488
+ return content;
489
+ if (Array.isArray(content)) {
490
+ return content
491
+ .map((part) => {
492
+ if (!part)
493
+ return '';
494
+ if (typeof part === 'string')
495
+ return part;
496
+ if (typeof part === 'object') {
497
+ const text = part.text;
498
+ if (typeof text === 'string')
499
+ return text;
500
+ }
501
+ return '';
502
+ })
503
+ .join('');
504
+ }
505
+ if (typeof content === 'object') {
506
+ const text = content.text;
507
+ if (typeof text === 'string')
508
+ return text;
509
+ }
510
+ return '';
511
+ }
512
+ function extractToolCall(ev) {
513
+ if (ev.type !== 'tool_call')
514
+ return null;
515
+ const toolCall = ev.tool_call && typeof ev.tool_call === 'object' ? ev.tool_call : null;
516
+ if (!toolCall)
517
+ return null;
518
+ const keys = Object.keys(toolCall);
519
+ if (!keys.length)
520
+ return null;
521
+ const name = keys[0];
522
+ const entry = toolCall[name];
523
+ const record = entry && typeof entry === 'object' ? entry : null;
524
+ const input = record?.args ?? record?.input ?? undefined;
525
+ const output = record?.output ?? record?.result ?? undefined;
526
+ const subtype = typeof ev.subtype === 'string' ? ev.subtype : '';
527
+ const phase = subtype === 'completed'
528
+ ? 'completed'
529
+ : subtype === 'started'
530
+ ? 'start'
531
+ : subtype === 'error'
532
+ ? 'error'
533
+ : subtype === 'delta'
534
+ ? 'delta'
535
+ : undefined;
536
+ return {
537
+ name,
538
+ input,
539
+ output,
540
+ callId: typeof ev.call_id === 'string' ? ev.call_id : undefined,
541
+ phase,
542
+ };
543
+ }
544
+ function extractTextFromMessage(message) {
545
+ if (!message || typeof message !== 'object')
546
+ return '';
547
+ const content = message.content;
548
+ return extractTextFromContent(content);
549
+ }
550
+ function extractAssistantDelta(ev) {
551
+ if (ev.type !== 'assistant' && ev.message?.role !== 'assistant')
552
+ return null;
553
+ if (typeof ev.text === 'string')
554
+ return ev.text;
555
+ if (typeof ev.delta === 'string')
556
+ return ev.delta;
557
+ if (ev.message) {
558
+ const text = extractTextFromMessage(ev.message);
559
+ return text || null;
560
+ }
561
+ if (ev.content) {
562
+ const text = extractTextFromContent(ev.content);
563
+ return text || null;
564
+ }
565
+ return null;
566
+ }
567
+ function extractResultText(ev) {
568
+ if (typeof ev.result === 'string')
569
+ return ev.result;
570
+ if (ev.result && typeof ev.result === 'object') {
571
+ const result = ev.result;
572
+ if (typeof result.text === 'string')
573
+ return result.text;
574
+ if (result.message) {
575
+ const text = extractTextFromMessage(result.message);
576
+ if (text)
577
+ return text;
578
+ }
579
+ if (result.content) {
580
+ const text = extractTextFromContent(result.content);
581
+ if (text)
582
+ return text;
583
+ }
584
+ }
585
+ if (ev.message) {
586
+ const text = extractTextFromMessage(ev.message);
587
+ if (text)
588
+ return text;
589
+ }
590
+ return null;
591
+ }
592
+ function isErrorEvent(ev) {
593
+ if (ev.type === 'error')
594
+ return true;
595
+ if (ev.type === 'result' && ev.subtype) {
596
+ const subtype = String(ev.subtype).toLowerCase();
597
+ if (subtype.includes('error') || subtype.includes('failed'))
598
+ return true;
599
+ }
600
+ return false;
601
+ }
602
+ function extractErrorMessage(ev) {
603
+ if (typeof ev.error === 'string')
604
+ return ev.error;
605
+ if (ev.error && typeof ev.error === 'object' && typeof ev.error.message === 'string') {
606
+ return ev.error.message;
607
+ }
608
+ if (typeof ev.text === 'string' && ev.type === 'error')
609
+ return ev.text;
610
+ if (typeof ev.result === 'string' && ev.type === 'result')
611
+ return ev.result;
612
+ return null;
613
+ }
614
+ export function runCursorPrompt({ prompt, resumeSessionId, model, repoRoot, cwd, providerDetailLevel, onEvent, signal, }) {
615
+ return new Promise((resolve) => {
616
+ const command = getCursorCommand();
617
+ const resolvedRepoRoot = repoRoot ? path.resolve(repoRoot) : null;
618
+ const resolvedCwd = cwd ? path.resolve(cwd) : null;
619
+ const runDir = resolvedCwd || resolvedRepoRoot || process.cwd();
620
+ const args = ['--print', '--output-format', 'stream-json'];
621
+ if (resumeSessionId) {
622
+ args.push('--resume', resumeSessionId);
623
+ }
624
+ const fallbackModel = getCursorDefaultModel();
625
+ const resolvedModel = resolveCursorModel(model, fallbackModel);
626
+ if (resolvedModel) {
627
+ args.push('--model', resolvedModel);
628
+ }
629
+ const endpoint = resolveCursorEndpoint();
630
+ if (endpoint) {
631
+ args.push('--endpoint', endpoint);
632
+ }
633
+ args.push(prompt);
634
+ const argsPreview = [...args];
635
+ if (argsPreview.length > 0) {
636
+ argsPreview[argsPreview.length - 1] = '[prompt]';
637
+ }
638
+ debugLog('Cursor', 'spawn', {
639
+ command,
640
+ args: argsPreview,
641
+ cwd: runDir,
642
+ model: resolvedModel || null,
643
+ endpoint: endpoint || null,
644
+ resume: resumeSessionId || null,
645
+ apiKeyConfigured: Boolean(getCursorApiKey().trim()),
646
+ promptChars: prompt.length,
647
+ });
648
+ const child = spawn(command, args, {
649
+ cwd: runDir,
650
+ env: buildCursorEnv(),
651
+ stdio: ['ignore', 'pipe', 'pipe'],
652
+ });
653
+ if (signal) {
654
+ signal.addEventListener('abort', () => {
655
+ child.kill('SIGTERM');
656
+ });
657
+ }
658
+ let aggregated = '';
659
+ let finalSessionId = null;
660
+ let didFinalize = false;
661
+ let sawError = false;
662
+ let sawJson = false;
663
+ let rawOutput = '';
664
+ const stdoutLines = [];
665
+ const stderrLines = [];
666
+ const includeRaw = providerDetailLevel === 'raw';
667
+ const buildProviderDetail = (eventType, data, raw) => {
668
+ const detail = { eventType };
669
+ if (data && Object.keys(data).length)
670
+ detail.data = data;
671
+ if (includeRaw && raw !== undefined)
672
+ detail.raw = raw;
673
+ return detail;
674
+ };
675
+ const emit = (event) => {
676
+ if (finalSessionId) {
677
+ onEvent({ ...event, providerSessionId: finalSessionId });
678
+ }
679
+ else {
680
+ onEvent(event);
681
+ }
682
+ };
683
+ const pushLine = (list, line) => {
684
+ if (!line)
685
+ return;
686
+ list.push(line);
687
+ if (list.length > 12)
688
+ list.shift();
689
+ };
690
+ const emitError = (message, providerDetail) => {
691
+ if (sawError)
692
+ return;
693
+ sawError = true;
694
+ emit({ type: 'error', message, providerDetail });
695
+ };
696
+ const emitFinal = (text) => {
697
+ if (didFinalize)
698
+ return;
699
+ didFinalize = true;
700
+ emit({ type: 'final', text });
701
+ };
702
+ const handleEvent = (ev) => {
703
+ if (ev?.type === 'system' && ev?.subtype === 'init') {
704
+ debugLog('Cursor', 'init', {
705
+ apiKeySource: ev.apiKeySource || null,
706
+ cwd: ev.cwd || null,
707
+ model: ev.model || null,
708
+ permissionMode: ev.permissionMode || null,
709
+ sessionId: ev.session_id ?? ev.sessionId ?? null,
710
+ });
711
+ emit({
712
+ type: 'detail',
713
+ provider: 'cursor',
714
+ providerDetail: buildProviderDetail('system.init', {
715
+ apiKeySource: ev.apiKeySource,
716
+ cwd: ev.cwd,
717
+ model: ev.model,
718
+ permissionMode: ev.permissionMode,
719
+ }, ev),
720
+ });
721
+ }
722
+ const sid = extractSessionId(ev);
723
+ if (sid)
724
+ finalSessionId = sid;
725
+ if (ev?.type === 'thinking') {
726
+ const subtype = typeof ev.subtype === 'string' ? ev.subtype : '';
727
+ const phase = subtype === 'completed'
728
+ ? 'completed'
729
+ : subtype === 'started'
730
+ ? 'start'
731
+ : subtype === 'error'
732
+ ? 'error'
733
+ : 'delta';
734
+ emit({
735
+ type: 'thinking',
736
+ provider: 'cursor',
737
+ phase,
738
+ text: typeof ev.text === 'string' ? ev.text : '',
739
+ timestampMs: typeof ev.timestamp_ms === 'number' ? ev.timestamp_ms : undefined,
740
+ providerDetail: buildProviderDetail(subtype ? `thinking.${subtype}` : 'thinking', {
741
+ subtype: subtype || undefined,
742
+ }, ev),
743
+ });
744
+ }
745
+ if (ev?.type === 'assistant' || ev?.type === 'user') {
746
+ const role = ev.message?.role === 'assistant' || ev.message?.role === 'user'
747
+ ? ev.message?.role
748
+ : ev.type;
749
+ const rawContent = ev.message?.content ?? ev.content;
750
+ const content = extractTextFromContent(rawContent);
751
+ emit({
752
+ type: 'message',
753
+ provider: 'cursor',
754
+ role,
755
+ content,
756
+ contentParts: rawContent ?? null,
757
+ providerDetail: buildProviderDetail(ev.type, {}, ev),
758
+ });
759
+ }
760
+ const toolCall = extractToolCall(ev);
761
+ if (toolCall) {
762
+ emit({
763
+ type: 'tool_call',
764
+ provider: 'cursor',
765
+ name: toolCall.name,
766
+ callId: toolCall.callId,
767
+ input: toolCall.input,
768
+ output: toolCall.output,
769
+ phase: toolCall.phase,
770
+ providerDetail: buildProviderDetail(ev.subtype ? `tool_call.${ev.subtype}` : 'tool_call', {
771
+ name: toolCall.name,
772
+ callId: toolCall.callId,
773
+ subtype: ev.subtype,
774
+ }, ev),
775
+ });
776
+ }
777
+ const usage = extractUsage(ev);
778
+ if (usage) {
779
+ emit({
780
+ type: 'usage',
781
+ inputTokens: usage.input_tokens,
782
+ outputTokens: usage.output_tokens,
783
+ });
784
+ }
785
+ if (isErrorEvent(ev)) {
786
+ const message = extractErrorMessage(ev) || 'Cursor run failed';
787
+ emitError(message, buildProviderDetail(ev.subtype ? `error.${ev.subtype}` : 'error', {}, ev));
788
+ return;
789
+ }
790
+ const delta = extractAssistantDelta(ev);
791
+ if (delta) {
792
+ aggregated += delta;
793
+ emit({ type: 'delta', text: delta, providerDetail: buildProviderDetail('delta', {}, ev) });
794
+ }
795
+ if (ev.type === 'result') {
796
+ const resultText = extractResultText(ev);
797
+ if (!aggregated && resultText) {
798
+ aggregated = resultText;
799
+ }
800
+ if (!sawError) {
801
+ emitFinal(aggregated || resultText || '');
802
+ emit({
803
+ type: 'detail',
804
+ provider: 'cursor',
805
+ providerDetail: buildProviderDetail('result', {
806
+ subtype: ev.subtype,
807
+ duration_ms: typeof ev.duration_ms === 'number'
808
+ ? ev.duration_ms
809
+ : undefined,
810
+ request_id: typeof ev.request_id === 'string'
811
+ ? ev.request_id
812
+ : undefined,
813
+ is_error: typeof ev.is_error === 'boolean'
814
+ ? ev.is_error
815
+ : undefined,
816
+ }, ev),
817
+ });
818
+ }
819
+ }
820
+ };
821
+ const handleLine = (line, source) => {
822
+ const trimmed = line.trim();
823
+ if (!trimmed)
824
+ return;
825
+ const payload = trimmed.startsWith('data: ') ? trimmed.slice(6).trim() : trimmed;
826
+ const parsed = safeJsonParse(payload);
827
+ if (!parsed || typeof parsed !== 'object') {
828
+ emit({ type: 'raw_line', line });
829
+ if (source === 'stdout') {
830
+ rawOutput += `${line}\n`;
831
+ pushLine(stdoutLines, line);
832
+ }
833
+ else {
834
+ pushLine(stderrLines, line);
835
+ }
836
+ return;
837
+ }
838
+ sawJson = true;
839
+ handleEvent(parsed);
840
+ };
841
+ const stdoutParser = createLineParser((line) => handleLine(line, 'stdout'));
842
+ const stderrParser = createLineParser((line) => handleLine(line, 'stderr'));
843
+ child.stdout?.on('data', stdoutParser);
844
+ child.stderr?.on('data', stderrParser);
845
+ child.on('close', (code) => {
846
+ if (!didFinalize) {
847
+ if (code && code !== 0) {
848
+ const hint = stderrLines.at(-1) || stdoutLines.at(-1) || '';
849
+ const suffix = hint ? `: ${hint}` : '';
850
+ debugLog('Cursor', 'exit', { code, stderr: stderrLines, stdout: stdoutLines });
851
+ emitError(`Cursor CLI exited with code ${code}${suffix}`);
852
+ }
853
+ else if (!sawError) {
854
+ const fallback = !sawJson ? rawOutput.trim() : '';
855
+ emitFinal(aggregated || fallback);
856
+ }
857
+ }
858
+ resolve({ sessionId: finalSessionId });
859
+ });
860
+ child.on('error', (err) => {
861
+ debugLog('Cursor', 'spawn-error', { message: err?.message });
862
+ emitError(err?.message ?? 'Cursor failed to start');
863
+ resolve({ sessionId: finalSessionId });
864
+ });
865
+ });
866
+ }