@siteboon/claude-code-ui 1.13.5 → 1.14.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.
- package/README.md +8 -0
- package/README.zh-CN.md +371 -0
- package/dist/assets/index-D-4Qbt_Z.css +32 -0
- package/dist/assets/index-D9jUnIU2.js +1239 -0
- package/dist/assets/{vendor-codemirror-CnTQH7Pk.js → vendor-codemirror-CJLzwpLB.js} +3 -3
- package/dist/assets/{vendor-react-DVSKlM5e.js → vendor-react-DcyRfQm3.js} +10 -10
- package/dist/index.html +4 -4
- package/package.json +5 -1
- package/server/claude-sdk.js +217 -23
- package/server/cli.js +63 -0
- package/server/cursor-cli.js +17 -9
- package/server/index.js +52 -8
- package/server/openai-codex.js +4 -2
- package/server/projects.js +118 -33
- package/server/routes/codex.js +56 -22
- package/dist/assets/index-Cc6pl7ji.css +0 -32
- package/dist/assets/index-ZLX5JV54.js +0 -1206
- package/server/database/auth.db +0 -0
package/dist/index.html
CHANGED
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Prevent zoom on iOS -->
|
|
27
27
|
<meta name="format-detection" content="telephone=no" />
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
29
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-react-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-D9jUnIU2.js"></script>
|
|
29
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DcyRfQm3.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-CJLzwpLB.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-DfaPXD3y.js">
|
|
32
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
32
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D-4Qbt_Z.css">
|
|
33
33
|
</head>
|
|
34
34
|
<body>
|
|
35
35
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteboon/claude-code-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "A web-based UI for Claude Code CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
"express": "^4.18.2",
|
|
72
72
|
"fuse.js": "^7.0.0",
|
|
73
73
|
"gray-matter": "^4.0.3",
|
|
74
|
+
"i18next": "^25.7.4",
|
|
75
|
+
"i18next-browser-languagedetector": "^8.2.0",
|
|
74
76
|
"jsonwebtoken": "^9.0.2",
|
|
75
77
|
"katex": "^0.16.25",
|
|
76
78
|
"lucide-react": "^0.515.0",
|
|
@@ -81,8 +83,10 @@
|
|
|
81
83
|
"react": "^18.2.0",
|
|
82
84
|
"react-dom": "^18.2.0",
|
|
83
85
|
"react-dropzone": "^14.2.3",
|
|
86
|
+
"react-i18next": "^16.5.3",
|
|
84
87
|
"react-markdown": "^10.1.0",
|
|
85
88
|
"react-router-dom": "^6.8.1",
|
|
89
|
+
"react-syntax-highlighter": "^15.6.1",
|
|
86
90
|
"rehype-katex": "^7.0.1",
|
|
87
91
|
"remark-gfm": "^4.0.0",
|
|
88
92
|
"remark-math": "^6.0.0",
|
package/server/claude-sdk.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
16
|
+
// Used to mint unique approval request IDs when randomUUID is not available.
|
|
17
|
+
// This keeps parallel tool approvals from colliding; it does not add any crypto/security guarantees.
|
|
18
|
+
import crypto from 'crypto';
|
|
16
19
|
import { promises as fs } from 'fs';
|
|
17
20
|
import path from 'path';
|
|
18
21
|
import os from 'os';
|
|
@@ -20,6 +23,124 @@ import { CLAUDE_MODELS } from '../shared/modelConstants.js';
|
|
|
20
23
|
|
|
21
24
|
// Session tracking: Map of session IDs to active query instances
|
|
22
25
|
const activeSessions = new Map();
|
|
26
|
+
// In-memory registry of pending tool approvals keyed by requestId.
|
|
27
|
+
// This does not persist approvals or share across processes; it exists so the
|
|
28
|
+
// SDK can pause tool execution while the UI decides what to do.
|
|
29
|
+
const pendingToolApprovals = new Map();
|
|
30
|
+
|
|
31
|
+
// Default approval timeout kept under the SDK's 60s control timeout.
|
|
32
|
+
// This does not change SDK limits; it only defines how long we wait for the UI,
|
|
33
|
+
// introduced to avoid hanging the run when no decision arrives.
|
|
34
|
+
const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
|
|
35
|
+
|
|
36
|
+
// Generate a stable request ID for UI approval flows.
|
|
37
|
+
// This does not encode tool details or get shown to users; it exists so the UI
|
|
38
|
+
// can respond to the correct pending request without collisions.
|
|
39
|
+
function createRequestId() {
|
|
40
|
+
// if clause is used because randomUUID is not available in older Node.js versions
|
|
41
|
+
if (typeof crypto.randomUUID === 'function') {
|
|
42
|
+
return crypto.randomUUID();
|
|
43
|
+
}
|
|
44
|
+
return crypto.randomBytes(16).toString('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Wait for a UI approval decision, honoring SDK cancellation.
|
|
48
|
+
// This does not auto-approve or auto-deny; it only resolves with UI input,
|
|
49
|
+
// and it cleans up the pending map to avoid leaks, introduced to prevent
|
|
50
|
+
// replying after the SDK cancels the control request.
|
|
51
|
+
function waitForToolApproval(requestId, options = {}) {
|
|
52
|
+
const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel } = options;
|
|
53
|
+
|
|
54
|
+
return new Promise(resolve => {
|
|
55
|
+
let settled = false;
|
|
56
|
+
|
|
57
|
+
const finalize = (decision) => {
|
|
58
|
+
if (settled) return;
|
|
59
|
+
settled = true;
|
|
60
|
+
cleanup();
|
|
61
|
+
resolve(decision);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const cleanup = () => {
|
|
65
|
+
pendingToolApprovals.delete(requestId);
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
if (signal && abortHandler) {
|
|
68
|
+
signal.removeEventListener('abort', abortHandler);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Timeout is local to this process; it does not override SDK timing.
|
|
73
|
+
// It exists to prevent the UI prompt from lingering indefinitely.
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
onCancel?.('timeout');
|
|
76
|
+
finalize(null);
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
|
|
79
|
+
const abortHandler = () => {
|
|
80
|
+
// If the SDK cancels the control request, stop waiting to avoid
|
|
81
|
+
// replying after the process is no longer ready for writes.
|
|
82
|
+
onCancel?.('cancelled');
|
|
83
|
+
finalize({ cancelled: true });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (signal) {
|
|
87
|
+
if (signal.aborted) {
|
|
88
|
+
onCancel?.('cancelled');
|
|
89
|
+
finalize({ cancelled: true });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pendingToolApprovals.set(requestId, (decision) => {
|
|
96
|
+
finalize(decision);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Resolve a pending approval. This does not validate the decision payload;
|
|
102
|
+
// validation and tool matching remain in canUseTool, which keeps this as a
|
|
103
|
+
// lightweight WebSocket -> SDK relay.
|
|
104
|
+
function resolveToolApproval(requestId, decision) {
|
|
105
|
+
const resolver = pendingToolApprovals.get(requestId);
|
|
106
|
+
if (resolver) {
|
|
107
|
+
resolver(decision);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Match stored permission entries against a tool + input combo.
|
|
112
|
+
// This only supports exact tool names and the Bash(command:*) shorthand
|
|
113
|
+
// used by the UI; it intentionally does not implement full glob semantics,
|
|
114
|
+
// introduced to stay consistent with the UI's "Allow rule" format.
|
|
115
|
+
function matchesToolPermission(entry, toolName, input) {
|
|
116
|
+
if (!entry || !toolName) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (entry === toolName) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const bashMatch = entry.match(/^Bash\((.+):\*\)$/);
|
|
125
|
+
if (toolName === 'Bash' && bashMatch) {
|
|
126
|
+
const allowedPrefix = bashMatch[1];
|
|
127
|
+
let command = '';
|
|
128
|
+
|
|
129
|
+
if (typeof input === 'string') {
|
|
130
|
+
command = input.trim();
|
|
131
|
+
} else if (input && typeof input === 'object' && typeof input.command === 'string') {
|
|
132
|
+
command = input.command.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!command) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return command.startsWith(allowedPrefix);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
23
144
|
|
|
24
145
|
/**
|
|
25
146
|
* Maps CLI options to SDK-compatible options format
|
|
@@ -52,29 +173,28 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
52
173
|
if (settings.skipPermissions && permissionMode !== 'plan') {
|
|
53
174
|
// When skipping permissions, use bypassPermissions mode
|
|
54
175
|
sdkOptions.permissionMode = 'bypassPermissions';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Map allowed tools (always set to avoid implicit "allow all" defaults).
|
|
179
|
+
// This does not grant permissions by itself; it just configures the SDK,
|
|
180
|
+
// introduced because leaving it undefined made the SDK treat it as "all tools allowed."
|
|
181
|
+
let allowedTools = [...(settings.allowedTools || [])];
|
|
182
|
+
|
|
183
|
+
// Add plan mode default tools
|
|
184
|
+
if (permissionMode === 'plan') {
|
|
185
|
+
const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch'];
|
|
186
|
+
for (const tool of planModeTools) {
|
|
187
|
+
if (!allowedTools.includes(tool)) {
|
|
188
|
+
allowedTools.push(tool);
|
|
66
189
|
}
|
|
67
190
|
}
|
|
191
|
+
}
|
|
68
192
|
|
|
69
|
-
|
|
70
|
-
sdkOptions.allowedTools = allowedTools;
|
|
71
|
-
}
|
|
193
|
+
sdkOptions.allowedTools = allowedTools;
|
|
72
194
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
}
|
|
195
|
+
// Map disallowed tools (always set so the SDK doesn't treat "undefined" as permissive).
|
|
196
|
+
// This does not override allowlists; it only feeds the canUseTool gate.
|
|
197
|
+
sdkOptions.disallowedTools = settings.disallowedTools || [];
|
|
78
198
|
|
|
79
199
|
// Map model (default to sonnet)
|
|
80
200
|
// Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
|
|
@@ -370,6 +490,76 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
370
490
|
tempImagePaths = imageResult.tempImagePaths;
|
|
371
491
|
tempDir = imageResult.tempDir;
|
|
372
492
|
|
|
493
|
+
// Gate tool usage with explicit UI approval when not auto-approved.
|
|
494
|
+
// This does not render UI or persist permissions; it only bridges to the UI
|
|
495
|
+
// via WebSocket and waits for the response, introduced so tool calls pause
|
|
496
|
+
// instead of auto-running when the allowlist is empty.
|
|
497
|
+
sdkOptions.canUseTool = async (toolName, input, context) => {
|
|
498
|
+
if (sdkOptions.permissionMode === 'bypassPermissions') {
|
|
499
|
+
return { behavior: 'allow', updatedInput: input };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const isDisallowed = (sdkOptions.disallowedTools || []).some(entry =>
|
|
503
|
+
matchesToolPermission(entry, toolName, input)
|
|
504
|
+
);
|
|
505
|
+
if (isDisallowed) {
|
|
506
|
+
return { behavior: 'deny', message: 'Tool disallowed by settings' };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const isAllowed = (sdkOptions.allowedTools || []).some(entry =>
|
|
510
|
+
matchesToolPermission(entry, toolName, input)
|
|
511
|
+
);
|
|
512
|
+
if (isAllowed) {
|
|
513
|
+
return { behavior: 'allow', updatedInput: input };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const requestId = createRequestId();
|
|
517
|
+
ws.send({
|
|
518
|
+
type: 'claude-permission-request',
|
|
519
|
+
requestId,
|
|
520
|
+
toolName,
|
|
521
|
+
input,
|
|
522
|
+
sessionId: capturedSessionId || sessionId || null
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// Wait for the UI; if the SDK cancels, notify the UI so it can dismiss the banner.
|
|
526
|
+
// This does not retry or resurface the prompt; it just reflects the cancellation.
|
|
527
|
+
const decision = await waitForToolApproval(requestId, {
|
|
528
|
+
signal: context?.signal,
|
|
529
|
+
onCancel: (reason) => {
|
|
530
|
+
ws.send({
|
|
531
|
+
type: 'claude-permission-cancelled',
|
|
532
|
+
requestId,
|
|
533
|
+
reason,
|
|
534
|
+
sessionId: capturedSessionId || sessionId || null
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
if (!decision) {
|
|
539
|
+
return { behavior: 'deny', message: 'Permission request timed out' };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (decision.cancelled) {
|
|
543
|
+
return { behavior: 'deny', message: 'Permission request cancelled' };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (decision.allow) {
|
|
547
|
+
// rememberEntry only updates this run's in-memory allowlist to prevent
|
|
548
|
+
// repeated prompts in the same session; persistence is handled by the UI.
|
|
549
|
+
if (decision.rememberEntry && typeof decision.rememberEntry === 'string') {
|
|
550
|
+
if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) {
|
|
551
|
+
sdkOptions.allowedTools.push(decision.rememberEntry);
|
|
552
|
+
}
|
|
553
|
+
if (Array.isArray(sdkOptions.disallowedTools)) {
|
|
554
|
+
sdkOptions.disallowedTools = sdkOptions.disallowedTools.filter(entry => entry !== decision.rememberEntry);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return { behavior: 'allow', updatedInput: decision.updatedInput ?? input };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
|
|
561
|
+
};
|
|
562
|
+
|
|
373
563
|
// Create SDK query instance
|
|
374
564
|
const queryInstance = query({
|
|
375
565
|
prompt: finalCommand,
|
|
@@ -413,7 +603,8 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
413
603
|
const transformedMessage = transformMessage(message);
|
|
414
604
|
ws.send({
|
|
415
605
|
type: 'claude-response',
|
|
416
|
-
data: transformedMessage
|
|
606
|
+
data: transformedMessage,
|
|
607
|
+
sessionId: capturedSessionId || sessionId || null
|
|
417
608
|
});
|
|
418
609
|
|
|
419
610
|
// Extract and send token budget updates from result messages
|
|
@@ -423,7 +614,8 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
423
614
|
console.log('Token budget from modelUsage:', tokenBudget);
|
|
424
615
|
ws.send({
|
|
425
616
|
type: 'token-budget',
|
|
426
|
-
data: tokenBudget
|
|
617
|
+
data: tokenBudget,
|
|
618
|
+
sessionId: capturedSessionId || sessionId || null
|
|
427
619
|
});
|
|
428
620
|
}
|
|
429
621
|
}
|
|
@@ -461,7 +653,8 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
461
653
|
// Send error to WebSocket
|
|
462
654
|
ws.send({
|
|
463
655
|
type: 'claude-error',
|
|
464
|
-
error: error.message
|
|
656
|
+
error: error.message,
|
|
657
|
+
sessionId: capturedSessionId || sessionId || null
|
|
465
658
|
});
|
|
466
659
|
|
|
467
660
|
throw error;
|
|
@@ -526,5 +719,6 @@ export {
|
|
|
526
719
|
queryClaudeSDK,
|
|
527
720
|
abortClaudeSDKSession,
|
|
528
721
|
isClaudeSDKSessionActive,
|
|
529
|
-
getActiveClaudeSDKSessions
|
|
722
|
+
getActiveClaudeSDKSessions,
|
|
723
|
+
resolveToolApproval
|
|
530
724
|
};
|
package/server/cli.js
CHANGED
|
@@ -151,6 +151,7 @@ Usage:
|
|
|
151
151
|
Commands:
|
|
152
152
|
start Start the Claude Code UI server (default)
|
|
153
153
|
status Show configuration and data locations
|
|
154
|
+
update Update to the latest version
|
|
154
155
|
help Show this help information
|
|
155
156
|
version Show version information
|
|
156
157
|
|
|
@@ -186,8 +187,67 @@ function showVersion() {
|
|
|
186
187
|
console.log(`${packageJson.version}`);
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
// Compare semver versions, returns true if v1 > v2
|
|
191
|
+
function isNewerVersion(v1, v2) {
|
|
192
|
+
const parts1 = v1.split('.').map(Number);
|
|
193
|
+
const parts2 = v2.split('.').map(Number);
|
|
194
|
+
for (let i = 0; i < 3; i++) {
|
|
195
|
+
if (parts1[i] > parts2[i]) return true;
|
|
196
|
+
if (parts1[i] < parts2[i]) return false;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for updates
|
|
202
|
+
async function checkForUpdates(silent = false) {
|
|
203
|
+
try {
|
|
204
|
+
const { execSync } = await import('child_process');
|
|
205
|
+
const latestVersion = execSync('npm show @siteboon/claude-code-ui version', { encoding: 'utf8' }).trim();
|
|
206
|
+
const currentVersion = packageJson.version;
|
|
207
|
+
|
|
208
|
+
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
209
|
+
console.log(`\n${c.warn('[UPDATE]')} New version available: ${c.bright(latestVersion)} (current: ${currentVersion})`);
|
|
210
|
+
console.log(` Run ${c.bright('cloudcli update')} to update\n`);
|
|
211
|
+
return { hasUpdate: true, latestVersion, currentVersion };
|
|
212
|
+
} else if (!silent) {
|
|
213
|
+
console.log(`${c.ok('[OK]')} You are on the latest version (${currentVersion})`);
|
|
214
|
+
}
|
|
215
|
+
return { hasUpdate: false, latestVersion, currentVersion };
|
|
216
|
+
} catch (e) {
|
|
217
|
+
if (!silent) {
|
|
218
|
+
console.log(`${c.warn('[WARN]')} Could not check for updates`);
|
|
219
|
+
}
|
|
220
|
+
return { hasUpdate: false, error: e.message };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Update the package
|
|
225
|
+
async function updatePackage() {
|
|
226
|
+
try {
|
|
227
|
+
const { execSync } = await import('child_process');
|
|
228
|
+
console.log(`${c.info('[INFO]')} Checking for updates...`);
|
|
229
|
+
|
|
230
|
+
const { hasUpdate, latestVersion, currentVersion } = await checkForUpdates(true);
|
|
231
|
+
|
|
232
|
+
if (!hasUpdate) {
|
|
233
|
+
console.log(`${c.ok('[OK]')} Already on the latest version (${currentVersion})`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
|
|
238
|
+
execSync('npm update -g @siteboon/claude-code-ui', { stdio: 'inherit' });
|
|
239
|
+
console.log(`${c.ok('[OK]')} Update complete! Restart cloudcli to use the new version.`);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
|
|
242
|
+
console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @siteboon/claude-code-ui`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
189
246
|
// Start the server
|
|
190
247
|
async function startServer() {
|
|
248
|
+
// Check for updates silently on startup
|
|
249
|
+
checkForUpdates(true);
|
|
250
|
+
|
|
191
251
|
// Import and run the server
|
|
192
252
|
await import('./index.js');
|
|
193
253
|
}
|
|
@@ -250,6 +310,9 @@ async function main() {
|
|
|
250
310
|
case '--version':
|
|
251
311
|
showVersion();
|
|
252
312
|
break;
|
|
313
|
+
case 'update':
|
|
314
|
+
await updatePackage();
|
|
315
|
+
break;
|
|
253
316
|
default:
|
|
254
317
|
console.error(`\n❌ Unknown command: ${command}`);
|
|
255
318
|
console.log(' Run "cloudcli help" for usage information.\n');
|
package/server/cursor-cli.js
CHANGED
|
@@ -114,7 +114,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
114
114
|
// Send system info to frontend
|
|
115
115
|
ws.send({
|
|
116
116
|
type: 'cursor-system',
|
|
117
|
-
data: response
|
|
117
|
+
data: response,
|
|
118
|
+
sessionId: capturedSessionId || sessionId || null
|
|
118
119
|
});
|
|
119
120
|
}
|
|
120
121
|
break;
|
|
@@ -123,7 +124,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
123
124
|
// Forward user message
|
|
124
125
|
ws.send({
|
|
125
126
|
type: 'cursor-user',
|
|
126
|
-
data: response
|
|
127
|
+
data: response,
|
|
128
|
+
sessionId: capturedSessionId || sessionId || null
|
|
127
129
|
});
|
|
128
130
|
break;
|
|
129
131
|
|
|
@@ -142,7 +144,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
142
144
|
type: 'text_delta',
|
|
143
145
|
text: textContent
|
|
144
146
|
}
|
|
145
|
-
}
|
|
147
|
+
},
|
|
148
|
+
sessionId: capturedSessionId || sessionId || null
|
|
146
149
|
});
|
|
147
150
|
}
|
|
148
151
|
break;
|
|
@@ -157,7 +160,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
157
160
|
type: 'claude-response',
|
|
158
161
|
data: {
|
|
159
162
|
type: 'content_block_stop'
|
|
160
|
-
}
|
|
163
|
+
},
|
|
164
|
+
sessionId: capturedSessionId || sessionId || null
|
|
161
165
|
});
|
|
162
166
|
}
|
|
163
167
|
|
|
@@ -174,7 +178,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
174
178
|
// Forward any other message types
|
|
175
179
|
ws.send({
|
|
176
180
|
type: 'cursor-response',
|
|
177
|
-
data: response
|
|
181
|
+
data: response,
|
|
182
|
+
sessionId: capturedSessionId || sessionId || null
|
|
178
183
|
});
|
|
179
184
|
}
|
|
180
185
|
} catch (parseError) {
|
|
@@ -182,7 +187,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
182
187
|
// If not JSON, send as raw text
|
|
183
188
|
ws.send({
|
|
184
189
|
type: 'cursor-output',
|
|
185
|
-
data: line
|
|
190
|
+
data: line,
|
|
191
|
+
sessionId: capturedSessionId || sessionId || null
|
|
186
192
|
});
|
|
187
193
|
}
|
|
188
194
|
}
|
|
@@ -193,7 +199,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
193
199
|
console.error('Cursor CLI stderr:', data.toString());
|
|
194
200
|
ws.send({
|
|
195
201
|
type: 'cursor-error',
|
|
196
|
-
error: data.toString()
|
|
202
|
+
error: data.toString(),
|
|
203
|
+
sessionId: capturedSessionId || sessionId || null
|
|
197
204
|
});
|
|
198
205
|
});
|
|
199
206
|
|
|
@@ -229,7 +236,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
229
236
|
|
|
230
237
|
ws.send({
|
|
231
238
|
type: 'cursor-error',
|
|
232
|
-
error: error.message
|
|
239
|
+
error: error.message,
|
|
240
|
+
sessionId: capturedSessionId || sessionId || null
|
|
233
241
|
});
|
|
234
242
|
|
|
235
243
|
reject(error);
|
|
@@ -264,4 +272,4 @@ export {
|
|
|
264
272
|
abortCursorSession,
|
|
265
273
|
isCursorSessionActive,
|
|
266
274
|
getActiveCursorSessions
|
|
267
|
-
};
|
|
275
|
+
};
|
package/server/index.js
CHANGED
|
@@ -58,7 +58,7 @@ import fetch from 'node-fetch';
|
|
|
58
58
|
import mime from 'mime-types';
|
|
59
59
|
|
|
60
60
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
|
61
|
-
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions } from './claude-sdk.js';
|
|
61
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
|
|
62
62
|
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
|
63
63
|
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
|
|
64
64
|
import gitRoutes from './routes/git.js';
|
|
@@ -80,6 +80,20 @@ import { validateApiKey, authenticateToken, authenticateWebSocket } from './midd
|
|
|
80
80
|
// File system watcher for projects folder
|
|
81
81
|
let projectsWatcher = null;
|
|
82
82
|
const connectedClients = new Set();
|
|
83
|
+
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
84
|
+
|
|
85
|
+
// Broadcast progress to all connected WebSocket clients
|
|
86
|
+
function broadcastProgress(progress) {
|
|
87
|
+
const message = JSON.stringify({
|
|
88
|
+
type: 'loading_progress',
|
|
89
|
+
...progress
|
|
90
|
+
});
|
|
91
|
+
connectedClients.forEach(client => {
|
|
92
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
93
|
+
client.send(message);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
83
97
|
|
|
84
98
|
// Setup file system watcher for Claude projects folder using chokidar
|
|
85
99
|
async function setupProjectsWatcher() {
|
|
@@ -117,13 +131,19 @@ async function setupProjectsWatcher() {
|
|
|
117
131
|
const debouncedUpdate = async (eventType, filePath) => {
|
|
118
132
|
clearTimeout(debounceTimer);
|
|
119
133
|
debounceTimer = setTimeout(async () => {
|
|
134
|
+
// Prevent reentrant calls
|
|
135
|
+
if (isGetProjectsRunning) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
120
139
|
try {
|
|
140
|
+
isGetProjectsRunning = true;
|
|
121
141
|
|
|
122
142
|
// Clear project directory cache when files change
|
|
123
143
|
clearProjectDirectoryCache();
|
|
124
144
|
|
|
125
145
|
// Get updated projects list
|
|
126
|
-
const updatedProjects = await getProjects();
|
|
146
|
+
const updatedProjects = await getProjects(broadcastProgress);
|
|
127
147
|
|
|
128
148
|
// Notify all connected clients about the project changes
|
|
129
149
|
const updateMessage = JSON.stringify({
|
|
@@ -142,6 +162,8 @@ async function setupProjectsWatcher() {
|
|
|
142
162
|
|
|
143
163
|
} catch (error) {
|
|
144
164
|
console.error('[ERROR] Error handling project changes:', error);
|
|
165
|
+
} finally {
|
|
166
|
+
isGetProjectsRunning = false;
|
|
145
167
|
}
|
|
146
168
|
}, 300); // 300ms debounce (slightly faster than before)
|
|
147
169
|
};
|
|
@@ -366,7 +388,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
|
366
388
|
|
|
367
389
|
app.get('/api/projects', authenticateToken, async (req, res) => {
|
|
368
390
|
try {
|
|
369
|
-
const projects = await getProjects();
|
|
391
|
+
const projects = await getProjects(broadcastProgress);
|
|
370
392
|
res.json(projects);
|
|
371
393
|
} catch (error) {
|
|
372
394
|
res.status(500).json({ error: error.message });
|
|
@@ -433,11 +455,12 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
|
|
|
433
455
|
}
|
|
434
456
|
});
|
|
435
457
|
|
|
436
|
-
// Delete project endpoint (
|
|
458
|
+
// Delete project endpoint (force=true to delete with sessions)
|
|
437
459
|
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
438
460
|
try {
|
|
439
461
|
const { projectName } = req.params;
|
|
440
|
-
|
|
462
|
+
const force = req.query.force === 'true';
|
|
463
|
+
await deleteProject(projectName, force);
|
|
441
464
|
res.json({ success: true });
|
|
442
465
|
} catch (error) {
|
|
443
466
|
res.status(500).json({ error: error.message });
|
|
@@ -496,7 +519,13 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
|
|
|
496
519
|
name: item.name,
|
|
497
520
|
type: 'directory'
|
|
498
521
|
}))
|
|
499
|
-
.
|
|
522
|
+
.sort((a, b) => {
|
|
523
|
+
const aHidden = a.name.startsWith('.');
|
|
524
|
+
const bHidden = b.name.startsWith('.');
|
|
525
|
+
if (aHidden && !bHidden) return 1;
|
|
526
|
+
if (!aHidden && bHidden) return -1;
|
|
527
|
+
return a.name.localeCompare(b.name);
|
|
528
|
+
});
|
|
500
529
|
|
|
501
530
|
// Add common directories if browsing home directory
|
|
502
531
|
const suggestions = [];
|
|
@@ -804,6 +833,18 @@ function handleChatConnection(ws) {
|
|
|
804
833
|
provider,
|
|
805
834
|
success
|
|
806
835
|
});
|
|
836
|
+
} else if (data.type === 'claude-permission-response') {
|
|
837
|
+
// Relay UI approval decisions back into the SDK control flow.
|
|
838
|
+
// This does not persist permissions; it only resolves the in-flight request,
|
|
839
|
+
// introduced so the SDK can resume once the user clicks Allow/Deny.
|
|
840
|
+
if (data.requestId) {
|
|
841
|
+
resolveToolApproval(data.requestId, {
|
|
842
|
+
allow: Boolean(data.allow),
|
|
843
|
+
updatedInput: data.updatedInput,
|
|
844
|
+
message: data.message,
|
|
845
|
+
rememberEntry: data.rememberEntry
|
|
846
|
+
});
|
|
847
|
+
}
|
|
807
848
|
} else if (data.type === 'cursor-abort') {
|
|
808
849
|
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
|
|
809
850
|
const success = abortCursorSession(data.sessionId);
|
|
@@ -1625,10 +1666,13 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
|
|
|
1625
1666
|
// Debug: log all entries including hidden files
|
|
1626
1667
|
|
|
1627
1668
|
|
|
1628
|
-
// Skip
|
|
1669
|
+
// Skip heavy build directories and VCS directories
|
|
1629
1670
|
if (entry.name === 'node_modules' ||
|
|
1630
1671
|
entry.name === 'dist' ||
|
|
1631
|
-
entry.name === 'build'
|
|
1672
|
+
entry.name === 'build' ||
|
|
1673
|
+
entry.name === '.git' ||
|
|
1674
|
+
entry.name === '.svn' ||
|
|
1675
|
+
entry.name === '.hg') continue;
|
|
1632
1676
|
|
|
1633
1677
|
const itemPath = path.join(dirPath, entry.name);
|
|
1634
1678
|
const item = {
|
package/server/openai-codex.js
CHANGED
|
@@ -272,7 +272,8 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
272
272
|
data: {
|
|
273
273
|
used: totalTokens,
|
|
274
274
|
total: 200000 // Default context window for Codex models
|
|
275
|
-
}
|
|
275
|
+
},
|
|
276
|
+
sessionId: currentSessionId
|
|
276
277
|
});
|
|
277
278
|
}
|
|
278
279
|
}
|
|
@@ -280,7 +281,8 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
280
281
|
// Send completion event
|
|
281
282
|
sendMessage(ws, {
|
|
282
283
|
type: 'codex-complete',
|
|
283
|
-
sessionId: currentSessionId
|
|
284
|
+
sessionId: currentSessionId,
|
|
285
|
+
actualSessionId: thread.id
|
|
284
286
|
});
|
|
285
287
|
|
|
286
288
|
} catch (error) {
|