@siteboon/claude-code-ui 1.9.0 → 1.10.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/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-D3NZxyU6.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-DPk7rbtA.js"></script>
29
29
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-7V_UDHjJ.js">
30
- <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-D2k1L1JZ.js">
30
+ <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-B7BYDWj-.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-jI4BCHEb.js">
32
- <link rel="stylesheet" crossorigin href="/assets/index-Bmo7Hu70.css">
32
+ <link rel="stylesheet" crossorigin href="/assets/index-BHQThXog.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.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -46,11 +46,16 @@
46
46
  "@codemirror/lang-json": "^6.0.1",
47
47
  "@codemirror/lang-markdown": "^6.3.3",
48
48
  "@codemirror/lang-python": "^6.2.1",
49
+ "@codemirror/merge": "^6.11.1",
49
50
  "@codemirror/theme-one-dark": "^6.1.2",
51
+ "@octokit/rest": "^22.0.0",
52
+ "@replit/codemirror-minimap": "^0.5.2",
50
53
  "@tailwindcss/typography": "^0.5.16",
51
54
  "@uiw/react-codemirror": "^4.23.13",
52
55
  "@xterm/addon-clipboard": "^0.1.0",
56
+ "@xterm/addon-fit": "^0.10.0",
53
57
  "@xterm/addon-webgl": "^0.18.0",
58
+ "@xterm/xterm": "^5.5.0",
54
59
  "bcrypt": "^6.0.0",
55
60
  "better-sqlite3": "^12.2.0",
56
61
  "chokidar": "^4.0.3",
@@ -72,12 +77,11 @@
72
77
  "react-dropzone": "^14.2.3",
73
78
  "react-markdown": "^10.1.0",
74
79
  "react-router-dom": "^6.8.1",
80
+ "remark-gfm": "^4.0.0",
75
81
  "sqlite": "^5.1.1",
76
82
  "sqlite3": "^5.1.7",
77
83
  "tailwind-merge": "^3.3.1",
78
- "ws": "^8.14.2",
79
- "@xterm/xterm": "^5.5.0",
80
- "@xterm/addon-fit": "^0.10.0"
84
+ "ws": "^8.14.2"
81
85
  },
82
86
  "devDependencies": {
83
87
  "@types/react": "^18.2.43",
@@ -378,6 +378,11 @@ async function queryClaudeSDK(command, options = {}, ws) {
378
378
  capturedSessionId = message.session_id;
379
379
  addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
380
380
 
381
+ // Set session ID on writer
382
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
383
+ ws.setSessionId(capturedSessionId);
384
+ }
385
+
381
386
  // Send session-created event only once for new sessions
382
387
  if (!sessionId && !sessionCreatedSent) {
383
388
  sessionCreatedSent = true;
@@ -94,6 +94,11 @@ async function spawnCursor(command, options = {}, ws) {
94
94
  activeCursorProcesses.set(capturedSessionId, cursorProcess);
95
95
  }
96
96
 
97
+ // Set session ID on writer (for API endpoint compatibility)
98
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
99
+ ws.setSessionId(capturedSessionId);
100
+ }
101
+
97
102
  // Send session-created event only once for new sessions
98
103
  if (!sessionId && !sessionCreatedSent) {
99
104
  sessionCreatedSent = true;
@@ -1,18 +1,34 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
+ import crypto from 'crypto';
4
5
  import { fileURLToPath } from 'url';
5
6
  import { dirname } from 'path';
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = dirname(__filename);
9
10
 
10
- const DB_PATH = path.join(__dirname, 'auth.db');
11
+ // Use DATABASE_PATH environment variable if set, otherwise use default location
12
+ const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db');
11
13
  const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
12
14
 
15
+ // Ensure database directory exists if custom path is provided
16
+ if (process.env.DATABASE_PATH) {
17
+ const dbDir = path.dirname(DB_PATH);
18
+ try {
19
+ if (!fs.existsSync(dbDir)) {
20
+ fs.mkdirSync(dbDir, { recursive: true });
21
+ console.log(`Created database directory: ${dbDir}`);
22
+ }
23
+ } catch (error) {
24
+ console.error(`Failed to create database directory ${dbDir}:`, error.message);
25
+ throw error;
26
+ }
27
+ }
28
+
13
29
  // Create database connection
14
30
  const db = new Database(DB_PATH);
15
- console.log('Connected to SQLite database');
31
+ console.log(`Connected to SQLite database at: ${DB_PATH}`);
16
32
 
17
33
  // Initialize database with schema
18
34
  const initializeDatabase = async () => {
@@ -79,8 +95,169 @@ const userDb = {
79
95
  }
80
96
  };
81
97
 
98
+ // API Keys database operations
99
+ const apiKeysDb = {
100
+ // Generate a new API key
101
+ generateApiKey: () => {
102
+ return 'ck_' + crypto.randomBytes(32).toString('hex');
103
+ },
104
+
105
+ // Create a new API key
106
+ createApiKey: (userId, keyName) => {
107
+ try {
108
+ const apiKey = apiKeysDb.generateApiKey();
109
+ const stmt = db.prepare('INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)');
110
+ const result = stmt.run(userId, keyName, apiKey);
111
+ return { id: result.lastInsertRowid, keyName, apiKey };
112
+ } catch (err) {
113
+ throw err;
114
+ }
115
+ },
116
+
117
+ // Get all API keys for a user
118
+ getApiKeys: (userId) => {
119
+ try {
120
+ const rows = db.prepare('SELECT id, key_name, api_key, created_at, last_used, is_active FROM api_keys WHERE user_id = ? ORDER BY created_at DESC').all(userId);
121
+ return rows;
122
+ } catch (err) {
123
+ throw err;
124
+ }
125
+ },
126
+
127
+ // Validate API key and get user
128
+ validateApiKey: (apiKey) => {
129
+ try {
130
+ const row = db.prepare(`
131
+ SELECT u.id, u.username, ak.id as api_key_id
132
+ FROM api_keys ak
133
+ JOIN users u ON ak.user_id = u.id
134
+ WHERE ak.api_key = ? AND ak.is_active = 1 AND u.is_active = 1
135
+ `).get(apiKey);
136
+
137
+ if (row) {
138
+ // Update last_used timestamp
139
+ db.prepare('UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?').run(row.api_key_id);
140
+ }
141
+
142
+ return row;
143
+ } catch (err) {
144
+ throw err;
145
+ }
146
+ },
147
+
148
+ // Delete an API key
149
+ deleteApiKey: (userId, apiKeyId) => {
150
+ try {
151
+ const stmt = db.prepare('DELETE FROM api_keys WHERE id = ? AND user_id = ?');
152
+ const result = stmt.run(apiKeyId, userId);
153
+ return result.changes > 0;
154
+ } catch (err) {
155
+ throw err;
156
+ }
157
+ },
158
+
159
+ // Toggle API key active status
160
+ toggleApiKey: (userId, apiKeyId, isActive) => {
161
+ try {
162
+ const stmt = db.prepare('UPDATE api_keys SET is_active = ? WHERE id = ? AND user_id = ?');
163
+ const result = stmt.run(isActive ? 1 : 0, apiKeyId, userId);
164
+ return result.changes > 0;
165
+ } catch (err) {
166
+ throw err;
167
+ }
168
+ }
169
+ };
170
+
171
+ // User credentials database operations (for GitHub tokens, GitLab tokens, etc.)
172
+ const credentialsDb = {
173
+ // Create a new credential
174
+ createCredential: (userId, credentialName, credentialType, credentialValue, description = null) => {
175
+ try {
176
+ const stmt = db.prepare('INSERT INTO user_credentials (user_id, credential_name, credential_type, credential_value, description) VALUES (?, ?, ?, ?, ?)');
177
+ const result = stmt.run(userId, credentialName, credentialType, credentialValue, description);
178
+ return { id: result.lastInsertRowid, credentialName, credentialType };
179
+ } catch (err) {
180
+ throw err;
181
+ }
182
+ },
183
+
184
+ // Get all credentials for a user, optionally filtered by type
185
+ getCredentials: (userId, credentialType = null) => {
186
+ try {
187
+ let query = 'SELECT id, credential_name, credential_type, description, created_at, is_active FROM user_credentials WHERE user_id = ?';
188
+ const params = [userId];
189
+
190
+ if (credentialType) {
191
+ query += ' AND credential_type = ?';
192
+ params.push(credentialType);
193
+ }
194
+
195
+ query += ' ORDER BY created_at DESC';
196
+
197
+ const rows = db.prepare(query).all(...params);
198
+ return rows;
199
+ } catch (err) {
200
+ throw err;
201
+ }
202
+ },
203
+
204
+ // Get active credential value for a user by type (returns most recent active)
205
+ getActiveCredential: (userId, credentialType) => {
206
+ try {
207
+ const row = db.prepare('SELECT credential_value FROM user_credentials WHERE user_id = ? AND credential_type = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1').get(userId, credentialType);
208
+ return row?.credential_value || null;
209
+ } catch (err) {
210
+ throw err;
211
+ }
212
+ },
213
+
214
+ // Delete a credential
215
+ deleteCredential: (userId, credentialId) => {
216
+ try {
217
+ const stmt = db.prepare('DELETE FROM user_credentials WHERE id = ? AND user_id = ?');
218
+ const result = stmt.run(credentialId, userId);
219
+ return result.changes > 0;
220
+ } catch (err) {
221
+ throw err;
222
+ }
223
+ },
224
+
225
+ // Toggle credential active status
226
+ toggleCredential: (userId, credentialId, isActive) => {
227
+ try {
228
+ const stmt = db.prepare('UPDATE user_credentials SET is_active = ? WHERE id = ? AND user_id = ?');
229
+ const result = stmt.run(isActive ? 1 : 0, credentialId, userId);
230
+ return result.changes > 0;
231
+ } catch (err) {
232
+ throw err;
233
+ }
234
+ }
235
+ };
236
+
237
+ // Backward compatibility - keep old names pointing to new system
238
+ const githubTokensDb = {
239
+ createGithubToken: (userId, tokenName, githubToken, description = null) => {
240
+ return credentialsDb.createCredential(userId, tokenName, 'github_token', githubToken, description);
241
+ },
242
+ getGithubTokens: (userId) => {
243
+ return credentialsDb.getCredentials(userId, 'github_token');
244
+ },
245
+ getActiveGithubToken: (userId) => {
246
+ return credentialsDb.getActiveCredential(userId, 'github_token');
247
+ },
248
+ deleteGithubToken: (userId, tokenId) => {
249
+ return credentialsDb.deleteCredential(userId, tokenId);
250
+ },
251
+ toggleGithubToken: (userId, tokenId, isActive) => {
252
+ return credentialsDb.toggleCredential(userId, tokenId, isActive);
253
+ }
254
+ };
255
+
82
256
  export {
83
257
  db,
84
258
  initializeDatabase,
85
- userDb
259
+ userDb,
260
+ apiKeysDb,
261
+ credentialsDb,
262
+ githubTokensDb // Backward compatibility
86
263
  };
@@ -13,4 +13,37 @@ CREATE TABLE IF NOT EXISTS users (
13
13
 
14
14
  -- Indexes for performance
15
15
  CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
16
- CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
16
+ CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
17
+
18
+ -- API Keys table for external API access
19
+ CREATE TABLE IF NOT EXISTS api_keys (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ user_id INTEGER NOT NULL,
22
+ key_name TEXT NOT NULL,
23
+ api_key TEXT UNIQUE NOT NULL,
24
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
25
+ last_used DATETIME,
26
+ is_active BOOLEAN DEFAULT 1,
27
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
28
+ );
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(api_key);
31
+ CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
32
+ CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active);
33
+
34
+ -- User credentials table for storing various tokens/credentials (GitHub, GitLab, etc.)
35
+ CREATE TABLE IF NOT EXISTS user_credentials (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ user_id INTEGER NOT NULL,
38
+ credential_name TEXT NOT NULL,
39
+ credential_type TEXT NOT NULL, -- 'github_token', 'gitlab_token', 'bitbucket_token', etc.
40
+ credential_value TEXT NOT NULL,
41
+ description TEXT,
42
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
43
+ is_active BOOLEAN DEFAULT 1,
44
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
48
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
49
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
package/server/index.js CHANGED
@@ -47,6 +47,8 @@ import cursorRoutes from './routes/cursor.js';
47
47
  import taskmasterRoutes from './routes/taskmaster.js';
48
48
  import mcpUtilsRoutes from './routes/mcp-utils.js';
49
49
  import commandsRoutes from './routes/commands.js';
50
+ import settingsRoutes from './routes/settings.js';
51
+ import agentRoutes from './routes/agent.js';
50
52
  import { initializeDatabase } from './database/db.js';
51
53
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
52
54
 
@@ -170,7 +172,8 @@ const wss = new WebSocketServer({
170
172
  app.locals.wss = wss;
171
173
 
172
174
  app.use(cors());
173
- app.use(express.json());
175
+ app.use(express.json({ limit: '50mb' }));
176
+ app.use(express.urlencoded({ limit: '50mb', extended: true }));
174
177
 
175
178
  // Optional API key validation (if configured)
176
179
  app.use('/api', validateApiKey);
@@ -196,6 +199,15 @@ app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
196
199
  // Commands API Routes (protected)
197
200
  app.use('/api/commands', authenticateToken, commandsRoutes);
198
201
 
202
+ // Settings API Routes (protected)
203
+ app.use('/api/settings', authenticateToken, settingsRoutes);
204
+
205
+ // Agent API Routes (uses API key authentication)
206
+ app.use('/api/agent', agentRoutes);
207
+
208
+ // Serve public files (like api-docs.html)
209
+ app.use(express.static(path.join(__dirname, '../public')));
210
+
199
211
  // Static files served after API routes
200
212
  // Add cache control: HTML files should not be cached, but assets can be cached
201
213
  app.use(express.static(path.join(__dirname, '../dist'), {
@@ -397,7 +409,10 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
397
409
  return res.status(404).json({ error: 'Project not found' });
398
410
  }
399
411
 
400
- const resolved = path.resolve(filePath);
412
+ // Handle both absolute and relative paths
413
+ const resolved = path.isAbsolute(filePath)
414
+ ? path.resolve(filePath)
415
+ : path.resolve(projectRoot, filePath);
401
416
  const normalizedRoot = path.resolve(projectRoot) + path.sep;
402
417
  if (!resolved.startsWith(normalizedRoot)) {
403
418
  return res.status(403).json({ error: 'Path must be under project root' });
@@ -493,21 +508,15 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
493
508
  return res.status(404).json({ error: 'Project not found' });
494
509
  }
495
510
 
496
- const resolved = path.resolve(filePath);
511
+ // Handle both absolute and relative paths
512
+ const resolved = path.isAbsolute(filePath)
513
+ ? path.resolve(filePath)
514
+ : path.resolve(projectRoot, filePath);
497
515
  const normalizedRoot = path.resolve(projectRoot) + path.sep;
498
516
  if (!resolved.startsWith(normalizedRoot)) {
499
517
  return res.status(403).json({ error: 'Path must be under project root' });
500
518
  }
501
519
 
502
- // Create backup of original file
503
- try {
504
- const backupPath = resolved + '.backup.' + Date.now();
505
- await fsPromises.copyFile(resolved, backupPath);
506
- console.log('📋 Created backup:', backupPath);
507
- } catch (backupError) {
508
- console.warn('Could not create backup:', backupError.message);
509
- }
510
-
511
520
  // Write the new content
512
521
  await fsPromises.writeFile(resolved, content, 'utf8');
513
522
 
@@ -1234,14 +1243,17 @@ app.get('*', (req, res) => {
1234
1243
 
1235
1244
  // Only serve index.html for HTML routes, not for static assets
1236
1245
  // Static assets should already be handled by express.static middleware above
1237
- if (process.env.NODE_ENV === 'production') {
1246
+ const indexPath = path.join(__dirname, '../dist/index.html');
1247
+
1248
+ // Check if dist/index.html exists (production build available)
1249
+ if (fs.existsSync(indexPath)) {
1238
1250
  // Set no-cache headers for HTML to prevent service worker issues
1239
1251
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1240
1252
  res.setHeader('Pragma', 'no-cache');
1241
1253
  res.setHeader('Expires', '0');
1242
- res.sendFile(path.join(__dirname, '../dist/index.html'));
1254
+ res.sendFile(indexPath);
1243
1255
  } else {
1244
- // In development, redirect to Vite dev server
1256
+ // In development, redirect to Vite dev server only if dist doesn't exist
1245
1257
  res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`);
1246
1258
  }
1247
1259
  });
@@ -1336,8 +1348,17 @@ async function startServer() {
1336
1348
  await initializeDatabase();
1337
1349
  console.log('✅ Database initialization skipped (testing)');
1338
1350
 
1351
+ // Check if running in production mode (dist folder exists)
1352
+ const distIndexPath = path.join(__dirname, '../dist/index.html');
1353
+ const isProduction = fs.existsSync(distIndexPath);
1354
+
1339
1355
  // Log Claude implementation mode
1340
1356
  console.log('🚀 Using Claude Agents SDK for Claude integration');
1357
+ console.log(`📦 Running in ${isProduction ? 'PRODUCTION' : 'DEVELOPMENT'} mode`);
1358
+
1359
+ if (!isProduction) {
1360
+ console.log(`⚠️ Note: Requests will be proxied to Vite dev server at http://localhost:${process.env.VITE_PORT || 5173}`);
1361
+ }
1341
1362
 
1342
1363
  server.listen(PORT, '0.0.0.0', async () => {
1343
1364
  console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
@@ -523,10 +523,12 @@ async function getProjects() {
523
523
 
524
524
  async function getSessions(projectName, limit = 5, offset = 0) {
525
525
  const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
526
-
526
+
527
527
  try {
528
528
  const files = await fs.readdir(projectDir);
529
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
529
+ // agent-*.jsonl files contain session start data at this point. This needs to be revisited
530
+ // periodically to make sure only accurate data is there and no new functionality is added there
531
+ const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
530
532
 
531
533
  if (jsonlFiles.length === 0) {
532
534
  return { sessions: [], hasMore: false, total: 0 };
@@ -803,10 +805,12 @@ async function parseJsonlSessions(filePath) {
803
805
  // Get messages for a specific session with pagination support
804
806
  async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
805
807
  const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
806
-
808
+
807
809
  try {
808
810
  const files = await fs.readdir(projectDir);
809
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
811
+ // agent-*.jsonl files contain session start data at this point. This needs to be revisited
812
+ // periodically to make sure only accurate data is there and no new functionality is added there
813
+ const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
810
814
 
811
815
  if (jsonlFiles.length === 0) {
812
816
  return { messages: [], total: 0, hasMore: false };