- Upgrading to Vite 7

- Refactor to use es modules
- Added permission mode
- Switched to better sqlite3
- several UX enhancements
This commit is contained in:
simos
2025-07-11 10:29:36 +00:00
parent d8bc6348d5
commit fc2a94a2e5
16 changed files with 581 additions and 1465 deletions

View File

@@ -1,10 +1,10 @@
const { spawn } = require('child_process');
import { spawn } from 'child_process';
let activeClaudeProcesses = new Map(); // Track active processes by session ID
async function spawnClaude(command, options = {}, ws) {
return new Promise(async (resolve, reject) => {
const { sessionId, projectPath, cwd, resume, toolsSettings } = options;
const { sessionId, projectPath, cwd, resume, toolsSettings, permissionMode } = options;
let capturedSessionId = sessionId; // Track session ID throughout the process
let sessionCreatedSent = false; // Track if we've already sent session-created event
@@ -36,15 +36,38 @@ async function spawnClaude(command, options = {}, ws) {
args.push('--model', 'sonnet');
}
// Add permission mode if specified (works for both new and resumed sessions)
if (permissionMode && permissionMode !== 'default') {
args.push('--permission-mode', permissionMode);
console.log('🔒 Using permission mode:', permissionMode);
}
// Add tools settings flags
if (settings.skipPermissions) {
// Don't use --dangerously-skip-permissions when in plan mode
if (settings.skipPermissions && permissionMode !== 'plan') {
args.push('--dangerously-skip-permissions');
console.log('⚠️ Using --dangerously-skip-permissions (skipping other tool settings)');
} else {
// Only add allowed/disallowed tools if not skipping permissions
// Collect all allowed tools, including plan mode defaults
let allowedTools = [...(settings.allowedTools || [])];
// Add plan mode specific tools
if (permissionMode === 'plan') {
const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite'];
// Add plan mode tools that aren't already in the allowed list
for (const tool of planModeTools) {
if (!allowedTools.includes(tool)) {
allowedTools.push(tool);
}
}
console.log('📝 Plan mode: Added default allowed tools:', planModeTools);
}
// Add allowed tools
if (settings.allowedTools && settings.allowedTools.length > 0) {
for (const tool of settings.allowedTools) {
if (allowedTools.length > 0) {
for (const tool of allowedTools) {
args.push('--allowedTools', tool);
console.log('✅ Allowing tool:', tool);
}
@@ -57,6 +80,11 @@ async function spawnClaude(command, options = {}, ws) {
console.log('❌ Disallowing tool:', tool);
}
}
// Log when skip permissions is disabled due to plan mode
if (settings.skipPermissions && permissionMode === 'plan') {
console.log('📝 Skip permissions disabled due to plan mode');
}
}
// Use cwd (actual project directory) instead of projectPath (Claude's metadata directory)
@@ -201,7 +229,7 @@ function abortClaudeSession(sessionId) {
return false;
}
module.exports = {
export {
spawnClaude,
abortClaudeSession
};

View File

@@ -1,99 +1,85 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const DB_PATH = path.join(__dirname, 'auth.db');
const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
// Create database connection
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
console.error('Error opening database:', err.message);
} else {
console.log('Connected to SQLite database');
}
});
const db = new Database(DB_PATH);
console.log('Connected to SQLite database');
// Initialize database with schema
const initializeDatabase = async () => {
return new Promise((resolve, reject) => {
try {
const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8');
db.exec(initSQL, (err) => {
if (err) {
console.error('Error initializing database:', err.message);
reject(err);
} else {
console.log('Database initialized successfully');
resolve();
}
});
} catch (error) {
console.error('Error reading init SQL file:', error);
reject(error);
}
});
try {
const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8');
db.exec(initSQL);
console.log('Database initialized successfully');
} catch (error) {
console.error('Error initializing database:', error.message);
throw error;
}
};
// User database operations
const userDb = {
// Check if any users exist
hasUsers: () => {
return new Promise((resolve, reject) => {
db.get('SELECT COUNT(*) as count FROM users', (err, row) => {
if (err) reject(err);
else resolve(row.count > 0);
});
});
try {
const row = db.prepare('SELECT COUNT(*) as count FROM users').get();
return row.count > 0;
} catch (err) {
throw err;
}
},
// Create a new user
createUser: (username, passwordHash) => {
return new Promise((resolve, reject) => {
try {
const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
stmt.run(username, passwordHash, function(err) {
if (err) {
reject(err);
} else {
resolve({ id: this.lastID, username });
}
});
stmt.finalize();
});
const result = stmt.run(username, passwordHash);
return { id: result.lastInsertRowid, username };
} catch (err) {
throw err;
}
},
// Get user by username
getUserByUsername: (username) => {
return new Promise((resolve, reject) => {
db.get('SELECT * FROM users WHERE username = ? AND is_active = 1', [username], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
try {
const row = db.prepare('SELECT * FROM users WHERE username = ? AND is_active = 1').get(username);
return row;
} catch (err) {
throw err;
}
},
// Update last login time
updateLastLogin: (userId) => {
return new Promise((resolve, reject) => {
db.run('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', [userId], (err) => {
if (err) reject(err);
else resolve();
});
});
try {
db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId);
} catch (err) {
throw err;
}
},
// Get user by ID
getUserById: (userId) => {
return new Promise((resolve, reject) => {
db.get('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1', [userId], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
try {
const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId);
return row;
} catch (err) {
throw err;
}
}
};
module.exports = {
export {
db,
initializeDatabase,
userDb

View File

@@ -1,7 +1,13 @@
// Load environment variables from .env file
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
try {
const fs = require('fs');
const path = require('path');
const envPath = path.join(__dirname, '../.env');
const envFile = fs.readFileSync(envPath, 'utf8');
envFile.split('\n').forEach(line => {
@@ -19,31 +25,31 @@ try {
console.log('PORT from env:', process.env.PORT);
const express = require('express');
const { WebSocketServer } = require('ws');
const http = require('http');
const path = require('path');
const cors = require('cors');
const fs = require('fs').promises;
const { spawn } = require('child_process');
const os = require('os');
const pty = require('node-pty');
const fetch = require('node-fetch');
import express from 'express';
import { WebSocketServer } from 'ws';
import http from 'http';
import cors from 'cors';
import { promises as fsPromises } from 'fs';
import { spawn } from 'child_process';
import os from 'os';
import pty from 'node-pty';
import fetch from 'node-fetch';
import mime from 'mime-types';
const { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } = require('./projects');
const { spawnClaude, abortClaudeSession } = require('./claude-cli');
const gitRoutes = require('./routes/git');
const authRoutes = require('./routes/auth');
const { initializeDatabase } = require('./database/db');
const { validateApiKey, authenticateToken, authenticateWebSocket } = require('./middleware/auth');
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
import { spawnClaude, abortClaudeSession } from './claude-cli.js';
import gitRoutes from './routes/git.js';
import authRoutes from './routes/auth.js';
import { initializeDatabase } from './database/db.js';
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
// File system watcher for projects folder
let projectsWatcher = null;
const connectedClients = new Set();
// Setup file system watcher for Claude projects folder using chokidar
function setupProjectsWatcher() {
const chokidar = require('chokidar');
async function setupProjectsWatcher() {
const chokidar = (await import('chokidar')).default;
const claudeProjectsPath = path.join(process.env.HOME, '.claude', 'projects');
if (projectsWatcher) {
@@ -283,14 +289,14 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
console.log('📄 File read request:', projectName, filePath);
const fs = require('fs').promises;
// Using fsPromises from import
// Security check - ensure the path is safe and absolute
if (!filePath || !path.isAbsolute(filePath)) {
return res.status(400).json({ error: 'Invalid file path' });
}
const content = await fs.readFile(filePath, 'utf8');
const content = await fsPromises.readFile(filePath, 'utf8');
res.json({ content, path: filePath });
} catch (error) {
console.error('Error reading file:', error);
@@ -312,8 +318,8 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
console.log('🖼️ Binary file serve request:', projectName, filePath);
const fs = require('fs');
const mime = require('mime-types');
// Using fs from import
// Using mime from import
// Security check - ensure the path is safe and absolute
if (!filePath || !path.isAbsolute(filePath)) {
@@ -322,7 +328,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
// Check if file exists
try {
await fs.promises.access(filePath);
await fsPromises.access(filePath);
} catch (error) {
return res.status(404).json({ error: 'File not found' });
}
@@ -358,7 +364,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
console.log('💾 File save request:', projectName, filePath);
const fs = require('fs').promises;
// Using fsPromises from import
// Security check - ensure the path is safe and absolute
if (!filePath || !path.isAbsolute(filePath)) {
@@ -372,14 +378,14 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
// Create backup of original file
try {
const backupPath = filePath + '.backup.' + Date.now();
await fs.copyFile(filePath, backupPath);
await fsPromises.copyFile(filePath, backupPath);
console.log('📋 Created backup:', backupPath);
} catch (backupError) {
console.warn('Could not create backup:', backupError.message);
}
// Write the new content
await fs.writeFile(filePath, content, 'utf8');
await fsPromises.writeFile(filePath, content, 'utf8');
res.json({
success: true,
@@ -401,7 +407,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
try {
const fs = require('fs').promises;
// Using fsPromises from import
// Use extractProjectDirectory to get the actual project path
let actualPath;
@@ -415,7 +421,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
// Check if path exists
try {
await fs.access(actualPath);
await fsPromises.access(actualPath);
} catch (e) {
return res.status(404).json({ error: `Project path not found: ${actualPath}` });
}
@@ -662,7 +668,7 @@ function handleShellConnection(ws) {
// Audio transcription endpoint
app.post('/api/transcribe', authenticateToken, async (req, res) => {
try {
const multer = require('multer');
const multer = (await import('multer')).default;
const upload = multer({ storage: multer.memoryStorage() });
// Handle multipart form data
@@ -682,7 +688,7 @@ app.post('/api/transcribe', authenticateToken, async (req, res) => {
try {
// Create form data for OpenAI
const FormData = require('form-data');
const FormData = (await import('form-data')).default;
const formData = new FormData();
formData.append('file', req.file.buffer, {
filename: req.file.originalname,
@@ -725,7 +731,7 @@ app.post('/api/transcribe', authenticateToken, async (req, res) => {
// Handle different enhancement modes
try {
const OpenAI = require('openai');
const OpenAI = (await import('openai')).default;
const openai = new OpenAI({ apiKey });
let prompt, systemMessage, temperature = 0.7, maxTokens = 800;
@@ -815,11 +821,11 @@ app.get('*', (req, res) => {
});
async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
const fs = require('fs').promises;
// Using fsPromises from import
const items = [];
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
// Debug: log all entries including hidden files
@@ -840,7 +846,7 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
// Recursively get subdirectories but limit depth
try {
// Check if we can access the directory before trying to read it
await fs.access(item.path, fs.constants.R_OK);
await fsPromises.access(item.path, fs.constants.R_OK);
item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden);
} catch (e) {
// Silently skip directories we can't access (permission denied, etc.)
@@ -872,13 +878,13 @@ async function startServer() {
try {
// Initialize authentication database
await initializeDatabase();
console.log('✅ Database initialized successfully');
console.log('✅ Database initialization skipped (testing)');
server.listen(PORT, '0.0.0.0', () => {
server.listen(PORT, '0.0.0.0', async () => {
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
// Start watching the projects folder for changes
setupProjectsWatcher();
await setupProjectsWatcher(); // Re-enabled with better-sqlite3
});
} catch (error) {
console.error('❌ Failed to start server:', error);

View File

@@ -1,5 +1,5 @@
const jwt = require('jsonwebtoken');
const { userDb } = require('../database/db');
import jwt from 'jsonwebtoken';
import { userDb } from '../database/db.js';
// Get JWT secret from environment or use default (for development)
const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
@@ -31,7 +31,7 @@ const authenticateToken = async (req, res, next) => {
const decoded = jwt.verify(token, JWT_SECRET);
// Verify user still exists and is active
const user = await userDb.getUserById(decoded.userId);
const user = userDb.getUserById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'Invalid token. User not found.' });
}
@@ -71,7 +71,7 @@ const authenticateWebSocket = (token) => {
}
};
module.exports = {
export {
validateApiKey,
authenticateToken,
generateToken,

View File

@@ -1,6 +1,7 @@
const fs = require('fs').promises;
const path = require('path');
const readline = require('readline');
import { promises as fs } from 'fs';
import fsSync from 'fs';
import path from 'path';
import readline from 'readline';
// Cache for extracted project directories
const projectDirectoryCache = new Map();
@@ -91,7 +92,7 @@ async function extractProjectDirectory(projectName) {
// Process all JSONL files to collect cwd values
for (const file of jsonlFiles) {
const jsonlFile = path.join(projectDir, file);
const fileStream = require('fs').createReadStream(jsonlFile);
const fileStream = fsSync.createReadStream(jsonlFile);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
@@ -325,7 +326,7 @@ async function parseJsonlSessions(filePath) {
const sessions = new Map();
try {
const fileStream = require('fs').createReadStream(filePath);
const fileStream = fsSync.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
@@ -409,7 +410,7 @@ async function getSessionMessages(projectName, sessionId) {
// Process all JSONL files to find messages for this session
for (const file of jsonlFiles) {
const jsonlFile = path.join(projectDir, file);
const fileStream = require('fs').createReadStream(jsonlFile);
const fileStream = fsSync.createReadStream(jsonlFile);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
@@ -601,7 +602,7 @@ async function addProjectManually(projectPath, displayName = null) {
}
module.exports = {
export {
getProjects,
getSessions,
getSessionMessages,

View File

@@ -1,7 +1,7 @@
const express = require('express');
const bcrypt = require('bcrypt');
const { userDb } = require('../database/db');
const { generateToken, authenticateToken } = require('../middleware/auth');
import express from 'express';
import bcrypt from 'bcrypt';
import { userDb } from '../database/db.js';
import { generateToken, authenticateToken } from '../middleware/auth.js';
const router = express.Router();
@@ -34,7 +34,7 @@ router.post('/register', async (req, res) => {
}
// Check if users already exist (only allow one user)
const hasUsers = await userDb.hasUsers();
const hasUsers = userDb.hasUsers();
if (hasUsers) {
return res.status(403).json({ error: 'User already exists. This is a single-user system.' });
}
@@ -44,13 +44,13 @@ router.post('/register', async (req, res) => {
const passwordHash = await bcrypt.hash(password, saltRounds);
// Create user
const user = await userDb.createUser(username, passwordHash);
const user = userDb.createUser(username, passwordHash);
// Generate token
const token = generateToken(user);
// Update last login
await userDb.updateLastLogin(user.id);
userDb.updateLastLogin(user.id);
res.json({
success: true,
@@ -79,7 +79,7 @@ router.post('/login', async (req, res) => {
}
// Get user from database
const user = await userDb.getUserByUsername(username);
const user = userDb.getUserByUsername(username);
if (!user) {
return res.status(401).json({ error: 'Invalid username or password' });
}
@@ -94,7 +94,7 @@ router.post('/login', async (req, res) => {
const token = generateToken(user);
// Update last login
await userDb.updateLastLogin(user.id);
userDb.updateLastLogin(user.id);
res.json({
success: true,
@@ -122,4 +122,4 @@ router.post('/logout', authenticateToken, (req, res) => {
res.json({ success: true, message: 'Logged out successfully' });
});
module.exports = router;
export default router;

View File

@@ -1,9 +1,9 @@
const express = require('express');
const { exec } = require('child_process');
const { promisify } = require('util');
const path = require('path');
const fs = require('fs').promises;
const { extractProjectDirectory } = require('../projects');
import express from 'express';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import { promises as fs } from 'fs';
import { extractProjectDirectory } from '../projects.js';
const router = express.Router();
const execAsync = promisify(exec);
@@ -420,4 +420,4 @@ function generateSimpleCommitMessage(files, diff) {
}
}
module.exports = router;
export default router;