@siteboon/claude-code-ui 1.25.0 → 1.25.2
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/assets/{index-BmWWsL1A.js → index-DF_FFT3b.js} +255 -239
- package/dist/assets/index-WNTmA_ug.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/cursor-cli.js +189 -115
- package/server/index.js +12 -2
- package/server/routes/commands.js +4 -4
- package/server/routes/git.js +325 -89
- package/server/utils/commandParser.js +2 -2
- package/server/utils/frontmatter.js +18 -0
- package/dist/assets/index-CO53aUoS.css +0 -32
package/server/cursor-cli.js
CHANGED
|
@@ -1,84 +1,124 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import crossSpawn from 'cross-spawn';
|
|
3
|
-
import { promises as fs } from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import os from 'os';
|
|
6
3
|
|
|
7
4
|
// Use cross-spawn on Windows for better command execution
|
|
8
5
|
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
9
6
|
|
|
10
7
|
let activeCursorProcesses = new Map(); // Track active processes by session ID
|
|
11
8
|
|
|
9
|
+
const WORKSPACE_TRUST_PATTERNS = [
|
|
10
|
+
/workspace trust required/i,
|
|
11
|
+
/do you trust the contents of this directory/i,
|
|
12
|
+
/working with untrusted contents/i,
|
|
13
|
+
/pass --trust,\s*--yolo,\s*or -f/i
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function isWorkspaceTrustPrompt(text = '') {
|
|
17
|
+
if (!text || typeof text !== 'string') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return WORKSPACE_TRUST_PATTERNS.some((pattern) => pattern.test(text));
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
async function spawnCursor(command, options = {}, ws) {
|
|
13
25
|
return new Promise(async (resolve, reject) => {
|
|
14
|
-
const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model
|
|
26
|
+
const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model } = options;
|
|
15
27
|
let capturedSessionId = sessionId; // Track session ID throughout the process
|
|
16
28
|
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
|
17
|
-
let
|
|
18
|
-
|
|
29
|
+
let hasRetriedWithTrust = false;
|
|
30
|
+
let settled = false;
|
|
31
|
+
|
|
19
32
|
// Use tools settings passed from frontend, or defaults
|
|
20
33
|
const settings = toolsSettings || {
|
|
21
34
|
allowedShellCommands: [],
|
|
22
35
|
skipPermissions: false
|
|
23
36
|
};
|
|
24
|
-
|
|
37
|
+
|
|
25
38
|
// Build Cursor CLI command
|
|
26
|
-
const
|
|
27
|
-
|
|
39
|
+
const baseArgs = [];
|
|
40
|
+
|
|
28
41
|
// Build flags allowing both resume and prompt together (reply in existing session)
|
|
29
42
|
// Treat presence of sessionId as intention to resume, regardless of resume flag
|
|
30
43
|
if (sessionId) {
|
|
31
|
-
|
|
44
|
+
baseArgs.push('--resume=' + sessionId);
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
if (command && command.trim()) {
|
|
35
48
|
// Provide a prompt (works for both new and resumed sessions)
|
|
36
|
-
|
|
49
|
+
baseArgs.push('-p', command);
|
|
37
50
|
|
|
38
51
|
// Add model flag if specified (only meaningful for new sessions; harmless on resume)
|
|
39
52
|
if (!sessionId && model) {
|
|
40
|
-
|
|
53
|
+
baseArgs.push('--model', model);
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
// Request streaming JSON when we are providing a prompt
|
|
44
|
-
|
|
57
|
+
baseArgs.push('--output-format', 'stream-json');
|
|
45
58
|
}
|
|
46
|
-
|
|
59
|
+
|
|
47
60
|
// Add skip permissions flag if enabled
|
|
48
61
|
if (skipPermissions || settings.skipPermissions) {
|
|
49
|
-
|
|
50
|
-
console.log('
|
|
62
|
+
baseArgs.push('-f');
|
|
63
|
+
console.log('Using -f flag (skip permissions)');
|
|
51
64
|
}
|
|
52
|
-
|
|
65
|
+
|
|
53
66
|
// Use cwd (actual project directory) instead of projectPath
|
|
54
67
|
const workingDir = cwd || projectPath || process.cwd();
|
|
55
|
-
|
|
56
|
-
console.log('Spawning Cursor CLI:', 'cursor-agent', args.join(' '));
|
|
57
|
-
console.log('Working directory:', workingDir);
|
|
58
|
-
console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
|
|
59
|
-
|
|
60
|
-
const cursorProcess = spawnFunction('cursor-agent', args, {
|
|
61
|
-
cwd: workingDir,
|
|
62
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
-
env: { ...process.env } // Inherit all environment variables
|
|
64
|
-
});
|
|
65
|
-
|
|
68
|
+
|
|
66
69
|
// Store process reference for potential abort
|
|
67
70
|
const processKey = capturedSessionId || Date.now().toString();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
|
|
72
|
+
const settleOnce = (callback) => {
|
|
73
|
+
if (settled) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
settled = true;
|
|
77
|
+
callback();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const runCursorProcess = (args, runReason = 'initial') => {
|
|
81
|
+
const isTrustRetry = runReason === 'trust-retry';
|
|
82
|
+
let runSawWorkspaceTrustPrompt = false;
|
|
83
|
+
let stdoutLineBuffer = '';
|
|
84
|
+
|
|
85
|
+
if (isTrustRetry) {
|
|
86
|
+
console.log('Retrying Cursor CLI with --trust after workspace trust prompt');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('Spawning Cursor CLI:', 'cursor-agent', args.join(' '));
|
|
90
|
+
console.log('Working directory:', workingDir);
|
|
91
|
+
console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
|
|
92
|
+
|
|
93
|
+
const cursorProcess = spawnFunction('cursor-agent', args, {
|
|
94
|
+
cwd: workingDir,
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
+
env: { ...process.env } // Inherit all environment variables
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
activeCursorProcesses.set(processKey, cursorProcess);
|
|
100
|
+
|
|
101
|
+
const shouldSuppressForTrustRetry = (text) => {
|
|
102
|
+
if (hasRetriedWithTrust || args.includes('--trust')) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (!isWorkspaceTrustPrompt(text)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
runSawWorkspaceTrustPrompt = true;
|
|
110
|
+
return true;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const processCursorOutputLine = (line) => {
|
|
114
|
+
if (!line || !line.trim()) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
78
118
|
try {
|
|
79
119
|
const response = JSON.parse(line);
|
|
80
|
-
console.log('
|
|
81
|
-
|
|
120
|
+
console.log('Parsed JSON response:', response);
|
|
121
|
+
|
|
82
122
|
// Handle different message types
|
|
83
123
|
switch (response.type) {
|
|
84
124
|
case 'system':
|
|
@@ -86,14 +126,14 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
86
126
|
// Capture session ID
|
|
87
127
|
if (response.session_id && !capturedSessionId) {
|
|
88
128
|
capturedSessionId = response.session_id;
|
|
89
|
-
console.log('
|
|
90
|
-
|
|
129
|
+
console.log('Captured session ID:', capturedSessionId);
|
|
130
|
+
|
|
91
131
|
// Update process key with captured session ID
|
|
92
132
|
if (processKey !== capturedSessionId) {
|
|
93
133
|
activeCursorProcesses.delete(processKey);
|
|
94
134
|
activeCursorProcesses.set(capturedSessionId, cursorProcess);
|
|
95
135
|
}
|
|
96
|
-
|
|
136
|
+
|
|
97
137
|
// Set session ID on writer (for API endpoint compatibility)
|
|
98
138
|
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
99
139
|
ws.setSessionId(capturedSessionId);
|
|
@@ -110,7 +150,7 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
110
150
|
});
|
|
111
151
|
}
|
|
112
152
|
}
|
|
113
|
-
|
|
153
|
+
|
|
114
154
|
// Send system info to frontend
|
|
115
155
|
ws.send({
|
|
116
156
|
type: 'cursor-system',
|
|
@@ -119,7 +159,7 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
119
159
|
});
|
|
120
160
|
}
|
|
121
161
|
break;
|
|
122
|
-
|
|
162
|
+
|
|
123
163
|
case 'user':
|
|
124
164
|
// Forward user message
|
|
125
165
|
ws.send({
|
|
@@ -128,13 +168,12 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
128
168
|
sessionId: capturedSessionId || sessionId || null
|
|
129
169
|
});
|
|
130
170
|
break;
|
|
131
|
-
|
|
171
|
+
|
|
132
172
|
case 'assistant':
|
|
133
173
|
// Accumulate assistant message chunks
|
|
134
174
|
if (response.message && response.message.content && response.message.content.length > 0) {
|
|
135
175
|
const textContent = response.message.content[0].text;
|
|
136
|
-
|
|
137
|
-
|
|
176
|
+
|
|
138
177
|
// Send as Claude-compatible format for frontend
|
|
139
178
|
ws.send({
|
|
140
179
|
type: 'claude-response',
|
|
@@ -149,23 +188,14 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
149
188
|
});
|
|
150
189
|
}
|
|
151
190
|
break;
|
|
152
|
-
|
|
191
|
+
|
|
153
192
|
case 'result':
|
|
154
193
|
// Session complete
|
|
155
194
|
console.log('Cursor session result:', response);
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
type: 'claude-response',
|
|
161
|
-
data: {
|
|
162
|
-
type: 'content_block_stop'
|
|
163
|
-
},
|
|
164
|
-
sessionId: capturedSessionId || sessionId || null
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Send completion event
|
|
195
|
+
|
|
196
|
+
// Do not emit an extra content_block_stop here.
|
|
197
|
+
// The UI already finalizes the streaming message in cursor-result handling,
|
|
198
|
+
// and emitting both can produce duplicate assistant messages.
|
|
169
199
|
ws.send({
|
|
170
200
|
type: 'cursor-result',
|
|
171
201
|
sessionId: capturedSessionId || sessionId,
|
|
@@ -173,7 +203,7 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
173
203
|
success: response.subtype === 'success'
|
|
174
204
|
});
|
|
175
205
|
break;
|
|
176
|
-
|
|
206
|
+
|
|
177
207
|
default:
|
|
178
208
|
// Forward any other message types
|
|
179
209
|
ws.send({
|
|
@@ -183,7 +213,12 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
183
213
|
});
|
|
184
214
|
}
|
|
185
215
|
} catch (parseError) {
|
|
186
|
-
console.log('
|
|
216
|
+
console.log('Non-JSON response:', line);
|
|
217
|
+
|
|
218
|
+
if (shouldSuppressForTrustRetry(line)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
187
222
|
// If not JSON, send as raw text
|
|
188
223
|
ws.send({
|
|
189
224
|
type: 'cursor-output',
|
|
@@ -191,67 +226,106 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
191
226
|
sessionId: capturedSessionId || sessionId || null
|
|
192
227
|
});
|
|
193
228
|
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Handle stdout (streaming JSON responses)
|
|
232
|
+
cursorProcess.stdout.on('data', (data) => {
|
|
233
|
+
const rawOutput = data.toString();
|
|
234
|
+
console.log('Cursor CLI stdout:', rawOutput);
|
|
235
|
+
|
|
236
|
+
// Stream chunks can split JSON objects across packets; keep trailing partial line.
|
|
237
|
+
stdoutLineBuffer += rawOutput;
|
|
238
|
+
const completeLines = stdoutLineBuffer.split(/\r?\n/);
|
|
239
|
+
stdoutLineBuffer = completeLines.pop() || '';
|
|
240
|
+
|
|
241
|
+
completeLines.forEach((line) => {
|
|
242
|
+
processCursorOutputLine(line.trim());
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Handle stderr
|
|
247
|
+
cursorProcess.stderr.on('data', (data) => {
|
|
248
|
+
const stderrText = data.toString();
|
|
249
|
+
console.error('Cursor CLI stderr:', stderrText);
|
|
250
|
+
|
|
251
|
+
if (shouldSuppressForTrustRetry(stderrText)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
ws.send({
|
|
256
|
+
type: 'cursor-error',
|
|
257
|
+
error: stderrText,
|
|
258
|
+
sessionId: capturedSessionId || sessionId || null
|
|
259
|
+
});
|
|
204
260
|
});
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
261
|
+
|
|
262
|
+
// Handle process completion
|
|
263
|
+
cursorProcess.on('close', async (code) => {
|
|
264
|
+
console.log(`Cursor CLI process exited with code ${code}`);
|
|
265
|
+
|
|
266
|
+
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
267
|
+
activeCursorProcesses.delete(finalSessionId);
|
|
268
|
+
|
|
269
|
+
// Flush any final unterminated stdout line before completion handling.
|
|
270
|
+
if (stdoutLineBuffer.trim()) {
|
|
271
|
+
processCursorOutputLine(stdoutLineBuffer.trim());
|
|
272
|
+
stdoutLineBuffer = '';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
runSawWorkspaceTrustPrompt &&
|
|
277
|
+
code !== 0 &&
|
|
278
|
+
!hasRetriedWithTrust &&
|
|
279
|
+
!args.includes('--trust')
|
|
280
|
+
) {
|
|
281
|
+
hasRetriedWithTrust = true;
|
|
282
|
+
runCursorProcess([...args, '--trust'], 'trust-retry');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
ws.send({
|
|
287
|
+
type: 'claude-complete',
|
|
288
|
+
sessionId: finalSessionId,
|
|
289
|
+
exitCode: code,
|
|
290
|
+
isNewSession: !sessionId && !!command // Flag to indicate this was a new session
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (code === 0) {
|
|
294
|
+
settleOnce(() => resolve());
|
|
295
|
+
} else {
|
|
296
|
+
settleOnce(() => reject(new Error(`Cursor CLI exited with code ${code}`)));
|
|
297
|
+
}
|
|
220
298
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
ws.send({
|
|
238
|
-
type: 'cursor-error',
|
|
239
|
-
error: error.message,
|
|
240
|
-
sessionId: capturedSessionId || sessionId || null
|
|
299
|
+
|
|
300
|
+
// Handle process errors
|
|
301
|
+
cursorProcess.on('error', (error) => {
|
|
302
|
+
console.error('Cursor CLI process error:', error);
|
|
303
|
+
|
|
304
|
+
// Clean up process reference on error
|
|
305
|
+
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
306
|
+
activeCursorProcesses.delete(finalSessionId);
|
|
307
|
+
|
|
308
|
+
ws.send({
|
|
309
|
+
type: 'cursor-error',
|
|
310
|
+
error: error.message,
|
|
311
|
+
sessionId: capturedSessionId || sessionId || null
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
settleOnce(() => reject(error));
|
|
241
315
|
});
|
|
242
316
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
317
|
+
// Close stdin since Cursor doesn't need interactive input
|
|
318
|
+
cursorProcess.stdin.end();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
runCursorProcess(baseArgs, 'initial');
|
|
248
322
|
});
|
|
249
323
|
}
|
|
250
324
|
|
|
251
325
|
function abortCursorSession(sessionId) {
|
|
252
326
|
const process = activeCursorProcesses.get(sessionId);
|
|
253
327
|
if (process) {
|
|
254
|
-
console.log(
|
|
328
|
+
console.log(`Aborting Cursor session: ${sessionId}`);
|
|
255
329
|
process.kill('SIGTERM');
|
|
256
330
|
activeCursorProcesses.delete(sessionId);
|
|
257
331
|
return true;
|
package/server/index.js
CHANGED
|
@@ -1730,8 +1730,14 @@ function handleShellConnection(ws) {
|
|
|
1730
1730
|
shellCommand = 'cursor-agent';
|
|
1731
1731
|
}
|
|
1732
1732
|
} else if (provider === 'codex') {
|
|
1733
|
+
// Use codex command; attempt to resume and fall back to a new session when the resume fails.
|
|
1733
1734
|
if (hasSession && sessionId) {
|
|
1734
|
-
|
|
1735
|
+
if (os.platform() === 'win32') {
|
|
1736
|
+
// PowerShell syntax for fallback
|
|
1737
|
+
shellCommand = `codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
|
|
1738
|
+
} else {
|
|
1739
|
+
shellCommand = `codex resume "${sessionId}" || codex`;
|
|
1740
|
+
}
|
|
1735
1741
|
} else {
|
|
1736
1742
|
shellCommand = 'codex';
|
|
1737
1743
|
}
|
|
@@ -1765,7 +1771,11 @@ function handleShellConnection(ws) {
|
|
|
1765
1771
|
// Claude (default provider)
|
|
1766
1772
|
const command = initialCommand || 'claude';
|
|
1767
1773
|
if (hasSession && sessionId) {
|
|
1768
|
-
|
|
1774
|
+
if (os.platform() === 'win32') {
|
|
1775
|
+
shellCommand = `claude --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
|
|
1776
|
+
} else {
|
|
1777
|
+
shellCommand = `claude --resume "${sessionId}" || claude`;
|
|
1778
|
+
}
|
|
1769
1779
|
} else {
|
|
1770
1780
|
shellCommand = command;
|
|
1771
1781
|
}
|
|
@@ -3,8 +3,8 @@ import { promises as fs } from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import os from 'os';
|
|
6
|
-
import matter from 'gray-matter';
|
|
7
6
|
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
|
|
7
|
+
import { parseFrontmatter } from '../utils/frontmatter.js';
|
|
8
8
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
@@ -38,7 +38,7 @@ async function scanCommandsDirectory(dir, baseDir, namespace) {
|
|
|
38
38
|
// Parse markdown file for metadata
|
|
39
39
|
try {
|
|
40
40
|
const content = await fs.readFile(fullPath, 'utf8');
|
|
41
|
-
const { data: frontmatter, content: commandContent } =
|
|
41
|
+
const { data: frontmatter, content: commandContent } = parseFrontmatter(content);
|
|
42
42
|
|
|
43
43
|
// Calculate relative path from baseDir for command name
|
|
44
44
|
const relativePath = path.relative(baseDir, fullPath);
|
|
@@ -475,7 +475,7 @@ router.post('/load', async (req, res) => {
|
|
|
475
475
|
|
|
476
476
|
// Read and parse the command file
|
|
477
477
|
const content = await fs.readFile(commandPath, 'utf8');
|
|
478
|
-
const { data: metadata, content: commandContent } =
|
|
478
|
+
const { data: metadata, content: commandContent } = parseFrontmatter(content);
|
|
479
479
|
|
|
480
480
|
res.json({
|
|
481
481
|
path: commandPath,
|
|
@@ -560,7 +560,7 @@ router.post('/execute', async (req, res) => {
|
|
|
560
560
|
}
|
|
561
561
|
}
|
|
562
562
|
const content = await fs.readFile(commandPath, 'utf8');
|
|
563
|
-
const { data: metadata, content: commandContent } =
|
|
563
|
+
const { data: metadata, content: commandContent } = parseFrontmatter(content);
|
|
564
564
|
// Basic argument replacement (will be enhanced in command parser utility)
|
|
565
565
|
let processedContent = commandContent;
|
|
566
566
|
|