@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/server/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // Load environment variables from .env file
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 (process.env.VITE_IS_PLATFORM === 'true') {
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 outputBuffer = [];
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
- // Check for various URL opening patterns
1163
- const patterns = [
1164
- // Direct browser opening commands
1165
- /(?:xdg-open|open|start)\s+(https?:\/\/[^\s\x1b\x07]+)/g,
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
- // Git and other tools opening URLs
1169
- /Opening\s+(https?:\/\/[^\s\x1b\x07]+)/gi,
1170
- // General URL patterns that might be opened
1171
- /Visit:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
1172
- /View at:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
1173
- /Browse to:\s*(https?:\/\/[^\s\x1b\x07]+)/gi
1174
- ];
1175
-
1176
- patterns.forEach(pattern => {
1177
- let match;
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: 'url_open',
1185
- url: 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 (process.env.VITE_IS_PLATFORM === 'true') {
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 (process.env.VITE_IS_PLATFORM === 'true') {
84
+ if (IS_PLATFORM) {
84
85
  try {
85
86
  const user = userDb.getFirstUser();
86
87
  if (user) {
@@ -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 (VITE_IS_PLATFORM=true): For managed/hosted deployments where
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 (process.env.VITE_IS_PLATFORM === 'true') {
32
+ if (IS_PLATFORM) {
32
33
  try {
33
34
  const user = userDb.getFirstUser();
34
35
  if (!user) {
@@ -62,4 +62,4 @@ export const CODEX_MODELS = {
62
62
  ],
63
63
 
64
64
  DEFAULT: 'gpt-5.2'
65
- };
65
+ };