@siteboon/claude-code-ui 1.16.3 → 1.16.4
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-D9u4X-u6.js → index-Cep8Annb.js} +203 -203
- package/dist/assets/index-DQad8ylc.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +3 -3
- package/server/constants/config.js +5 -0
- package/server/index.js +109 -50
- package/server/load-env.js +24 -0
- package/server/middleware/auth.js +3 -2
- package/server/routes/agent.js +3 -2
- package/shared/modelConstants.js +1 -1
- package/dist/assets/index-BQGOOBNa.css +0 -32
package/server/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Load environment variables
|
|
2
|
+
// Load environment variables before other imports execute
|
|
3
|
+
import './load-env.js';
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
@@ -28,22 +29,6 @@ const c = {
|
|
|
28
29
|
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
try {
|
|
32
|
-
const envPath = path.join(__dirname, '../.env');
|
|
33
|
-
const envFile = fs.readFileSync(envPath, 'utf8');
|
|
34
|
-
envFile.split('\n').forEach(line => {
|
|
35
|
-
const trimmedLine = line.trim();
|
|
36
|
-
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
|
37
|
-
const [key, ...valueParts] = trimmedLine.split('=');
|
|
38
|
-
if (key && valueParts.length > 0 && !process.env[key]) {
|
|
39
|
-
process.env[key] = valueParts.join('=').trim();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
} catch (e) {
|
|
44
|
-
console.log('No .env file found or error reading it:', e.message);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
32
|
console.log('PORT from env:', process.env.PORT);
|
|
48
33
|
|
|
49
34
|
import express from 'express';
|
|
@@ -76,6 +61,7 @@ import userRoutes from './routes/user.js';
|
|
|
76
61
|
import codexRoutes from './routes/codex.js';
|
|
77
62
|
import { initializeDatabase } from './database/db.js';
|
|
78
63
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
64
|
+
import { IS_PLATFORM } from './constants/config.js';
|
|
79
65
|
|
|
80
66
|
// File system watcher for projects folder
|
|
81
67
|
let projectsWatcher = null;
|
|
@@ -192,6 +178,69 @@ const server = http.createServer(app);
|
|
|
192
178
|
|
|
193
179
|
const ptySessionsMap = new Map();
|
|
194
180
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
181
|
+
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
|
182
|
+
const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
|
|
183
|
+
const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
|
|
184
|
+
|
|
185
|
+
function stripAnsiSequences(value = '') {
|
|
186
|
+
return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeDetectedUrl(url) {
|
|
190
|
+
if (!url || typeof url !== 'string') return null;
|
|
191
|
+
|
|
192
|
+
const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
|
|
193
|
+
if (!cleaned) return null;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const parsed = new URL(cleaned);
|
|
197
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return parsed.toString();
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function extractUrlsFromText(value = '') {
|
|
207
|
+
const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || [];
|
|
208
|
+
|
|
209
|
+
// Handle wrapped terminal URLs split across lines by terminal width.
|
|
210
|
+
const wrappedMatches = [];
|
|
211
|
+
const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
|
|
212
|
+
const lines = value.split(/\r?\n/);
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
const line = lines[i].trim();
|
|
215
|
+
const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
|
|
216
|
+
if (!startMatch) continue;
|
|
217
|
+
|
|
218
|
+
let combined = startMatch[0];
|
|
219
|
+
let j = i + 1;
|
|
220
|
+
while (j < lines.length) {
|
|
221
|
+
const continuation = lines[j].trim();
|
|
222
|
+
if (!continuation) break;
|
|
223
|
+
if (!continuationRegex.test(continuation)) break;
|
|
224
|
+
combined += continuation;
|
|
225
|
+
j++;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
wrappedMatches.push(combined.replace(/\r?\n\s*/g, ''));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Array.from(new Set([...directMatches, ...wrappedMatches]));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function shouldAutoOpenUrlFromOutput(value = '') {
|
|
235
|
+
const normalized = value.toLowerCase();
|
|
236
|
+
return (
|
|
237
|
+
normalized.includes('browser didn\'t open') ||
|
|
238
|
+
normalized.includes('open this url') ||
|
|
239
|
+
normalized.includes('continue in your browser') ||
|
|
240
|
+
normalized.includes('press enter to open') ||
|
|
241
|
+
normalized.includes('open_url:')
|
|
242
|
+
);
|
|
243
|
+
}
|
|
195
244
|
|
|
196
245
|
// Single WebSocket server that handles both paths
|
|
197
246
|
const wss = new WebSocketServer({
|
|
@@ -200,7 +249,7 @@ const wss = new WebSocketServer({
|
|
|
200
249
|
console.log('WebSocket connection attempt to:', info.req.url);
|
|
201
250
|
|
|
202
251
|
// Platform mode: always allow connection
|
|
203
|
-
if (
|
|
252
|
+
if (IS_PLATFORM) {
|
|
204
253
|
const user = authenticateWebSocket(null); // Will return first user
|
|
205
254
|
if (!user) {
|
|
206
255
|
console.log('[WARN] Platform mode: No user found in database');
|
|
@@ -974,7 +1023,8 @@ function handleShellConnection(ws) {
|
|
|
974
1023
|
console.log('🐚 Shell client connected');
|
|
975
1024
|
let shellProcess = null;
|
|
976
1025
|
let ptySessionKey = null;
|
|
977
|
-
let
|
|
1026
|
+
let urlDetectionBuffer = '';
|
|
1027
|
+
const announcedAuthUrls = new Set();
|
|
978
1028
|
|
|
979
1029
|
ws.on('message', async (message) => {
|
|
980
1030
|
try {
|
|
@@ -988,6 +1038,8 @@ function handleShellConnection(ws) {
|
|
|
988
1038
|
const provider = data.provider || 'claude';
|
|
989
1039
|
const initialCommand = data.initialCommand;
|
|
990
1040
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
1041
|
+
urlDetectionBuffer = '';
|
|
1042
|
+
announcedAuthUrls.clear();
|
|
991
1043
|
|
|
992
1044
|
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
|
993
1045
|
const isLoginCommand = initialCommand && (
|
|
@@ -1127,9 +1179,7 @@ function handleShellConnection(ws) {
|
|
|
1127
1179
|
...process.env,
|
|
1128
1180
|
TERM: 'xterm-256color',
|
|
1129
1181
|
COLORTERM: 'truecolor',
|
|
1130
|
-
FORCE_COLOR: '3'
|
|
1131
|
-
// Override browser opening commands to echo URL for detection
|
|
1132
|
-
BROWSER: os.platform() === 'win32' ? 'echo "OPEN_URL:"' : 'echo "OPEN_URL:"'
|
|
1182
|
+
FORCE_COLOR: '3'
|
|
1133
1183
|
}
|
|
1134
1184
|
});
|
|
1135
1185
|
|
|
@@ -1159,38 +1209,47 @@ function handleShellConnection(ws) {
|
|
|
1159
1209
|
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1160
1210
|
let outputData = data;
|
|
1161
1211
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
// BROWSER environment variable override
|
|
1212
|
+
const cleanChunk = stripAnsiSequences(data);
|
|
1213
|
+
urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
|
|
1214
|
+
|
|
1215
|
+
outputData = outputData.replace(
|
|
1167
1216
|
/OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
while ((match = pattern.exec(data)) !== null) {
|
|
1179
|
-
const url = match[1];
|
|
1180
|
-
console.log('[DEBUG] Detected URL for opening:', url);
|
|
1181
|
-
|
|
1182
|
-
// Send URL opening message to client
|
|
1217
|
+
'[INFO] Opening in browser: $1'
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1220
|
+
const emitAuthUrl = (detectedUrl, autoOpen = false) => {
|
|
1221
|
+
const normalizedUrl = normalizeDetectedUrl(detectedUrl);
|
|
1222
|
+
if (!normalizedUrl) return;
|
|
1223
|
+
|
|
1224
|
+
const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
|
|
1225
|
+
if (isNewUrl) {
|
|
1226
|
+
announcedAuthUrls.add(normalizedUrl);
|
|
1183
1227
|
session.ws.send(JSON.stringify({
|
|
1184
|
-
type: '
|
|
1185
|
-
url:
|
|
1228
|
+
type: 'auth_url',
|
|
1229
|
+
url: normalizedUrl,
|
|
1230
|
+
autoOpen
|
|
1186
1231
|
}));
|
|
1187
|
-
|
|
1188
|
-
// Replace the OPEN_URL pattern with a user-friendly message
|
|
1189
|
-
if (pattern.source.includes('OPEN_URL')) {
|
|
1190
|
-
outputData = outputData.replace(match[0], `[INFO] Opening in browser: ${url}`);
|
|
1191
|
-
}
|
|
1192
1232
|
}
|
|
1193
|
-
|
|
1233
|
+
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
|
|
1237
|
+
.map((url) => normalizeDetectedUrl(url))
|
|
1238
|
+
.filter(Boolean);
|
|
1239
|
+
|
|
1240
|
+
// Prefer the most complete URL if shorter prefix variants are also present.
|
|
1241
|
+
const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter((url, _, urls) =>
|
|
1242
|
+
!urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
|
|
1246
|
+
|
|
1247
|
+
if (shouldAutoOpenUrlFromOutput(cleanChunk) && dedupedDetectedUrls.length > 0) {
|
|
1248
|
+
const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
|
|
1249
|
+
current.length > longest.length ? current : longest
|
|
1250
|
+
);
|
|
1251
|
+
emitAuthUrl(bestUrl, true);
|
|
1252
|
+
}
|
|
1194
1253
|
|
|
1195
1254
|
// Send regular output
|
|
1196
1255
|
session.ws.send(JSON.stringify({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Load environment variables from .env before other imports execute.
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const envPath = path.join(__dirname, '../.env');
|
|
12
|
+
const envFile = fs.readFileSync(envPath, 'utf8');
|
|
13
|
+
envFile.split('\n').forEach(line => {
|
|
14
|
+
const trimmedLine = line.trim();
|
|
15
|
+
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
|
16
|
+
const [key, ...valueParts] = trimmedLine.split('=');
|
|
17
|
+
if (key && valueParts.length > 0 && !process.env[key]) {
|
|
18
|
+
process.env[key] = valueParts.join('=').trim();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.log('No .env file found or error reading it:', e.message);
|
|
24
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
2
|
import { userDb } from '../database/db.js';
|
|
3
|
+
import { IS_PLATFORM } from '../constants/config.js';
|
|
3
4
|
|
|
4
5
|
// Get JWT secret from environment or use default (for development)
|
|
5
6
|
const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
|
|
@@ -21,7 +22,7 @@ const validateApiKey = (req, res, next) => {
|
|
|
21
22
|
// JWT authentication middleware
|
|
22
23
|
const authenticateToken = async (req, res, next) => {
|
|
23
24
|
// Platform mode: use single database user
|
|
24
|
-
if (
|
|
25
|
+
if (IS_PLATFORM) {
|
|
25
26
|
try {
|
|
26
27
|
const user = userDb.getFirstUser();
|
|
27
28
|
if (!user) {
|
|
@@ -80,7 +81,7 @@ const generateToken = (user) => {
|
|
|
80
81
|
// WebSocket authentication function
|
|
81
82
|
const authenticateWebSocket = (token) => {
|
|
82
83
|
// Platform mode: bypass token validation, return first user
|
|
83
|
-
if (
|
|
84
|
+
if (IS_PLATFORM) {
|
|
84
85
|
try {
|
|
85
86
|
const user = userDb.getFirstUser();
|
|
86
87
|
if (user) {
|
package/server/routes/agent.js
CHANGED
|
@@ -11,6 +11,7 @@ import { spawnCursor } from '../cursor-cli.js';
|
|
|
11
11
|
import { queryCodex } from '../openai-codex.js';
|
|
12
12
|
import { Octokit } from '@octokit/rest';
|
|
13
13
|
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
|
|
14
|
+
import { IS_PLATFORM } from '../constants/config.js';
|
|
14
15
|
|
|
15
16
|
const router = express.Router();
|
|
16
17
|
|
|
@@ -18,7 +19,7 @@ const router = express.Router();
|
|
|
18
19
|
* Middleware to authenticate agent API requests.
|
|
19
20
|
*
|
|
20
21
|
* Supports two authentication modes:
|
|
21
|
-
* 1. Platform mode (
|
|
22
|
+
* 1. Platform mode (IS_PLATFORM=true): For managed/hosted deployments where
|
|
22
23
|
* authentication is handled by an external proxy. Requests are trusted and
|
|
23
24
|
* the default user context is used.
|
|
24
25
|
*
|
|
@@ -28,7 +29,7 @@ const router = express.Router();
|
|
|
28
29
|
const validateExternalApiKey = (req, res, next) => {
|
|
29
30
|
// Platform mode: Authentication is handled externally (e.g., by a proxy layer).
|
|
30
31
|
// Trust the request and use the default user context.
|
|
31
|
-
if (
|
|
32
|
+
if (IS_PLATFORM) {
|
|
32
33
|
try {
|
|
33
34
|
const user = userDb.getFirstUser();
|
|
34
35
|
if (!user) {
|
package/shared/modelConstants.js
CHANGED