@siteboon/claude-code-ui 1.24.0 → 1.25.1

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
@@ -64,6 +64,8 @@ import cliAuthRoutes from './routes/cli-auth.js';
64
64
  import userRoutes from './routes/user.js';
65
65
  import codexRoutes from './routes/codex.js';
66
66
  import geminiRoutes from './routes/gemini.js';
67
+ import pluginsRoutes from './routes/plugins.js';
68
+ import { startEnabledPluginServers, stopAllPlugins } from './utils/plugin-process-manager.js';
67
69
  import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
68
70
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
69
71
  import { IS_PLATFORM } from './constants/config.js';
@@ -324,7 +326,7 @@ const wss = new WebSocketServer({
324
326
  // Make WebSocket server available to routes
325
327
  app.locals.wss = wss;
326
328
 
327
- app.use(cors());
329
+ app.use(cors({ exposedHeaders: ['X-Refreshed-Token'] }));
328
330
  app.use(express.json({
329
331
  limit: '50mb',
330
332
  type: (req) => {
@@ -389,6 +391,9 @@ app.use('/api/codex', authenticateToken, codexRoutes);
389
391
  // Gemini API Routes (protected)
390
392
  app.use('/api/gemini', authenticateToken, geminiRoutes);
391
393
 
394
+ // Plugins API Routes (protected)
395
+ app.use('/api/plugins', authenticateToken, pluginsRoutes);
396
+
392
397
  // Agent API Routes (uses API key authentication)
393
398
  app.use('/api/agent', agentRoutes);
394
399
 
@@ -1694,50 +1699,49 @@ function handleShellConnection(ws) {
1694
1699
  }));
1695
1700
 
1696
1701
  try {
1697
- // Prepare the shell command adapted to the platform and provider
1702
+ // Validate projectPath resolve to absolute and verify it exists
1703
+ const resolvedProjectPath = path.resolve(projectPath);
1704
+ try {
1705
+ const stats = fs.statSync(resolvedProjectPath);
1706
+ if (!stats.isDirectory()) {
1707
+ throw new Error('Not a directory');
1708
+ }
1709
+ } catch (pathErr) {
1710
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid project path' }));
1711
+ return;
1712
+ }
1713
+
1714
+ // Validate sessionId — only allow safe characters
1715
+ const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
1716
+ if (sessionId && !safeSessionIdPattern.test(sessionId)) {
1717
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid session ID' }));
1718
+ return;
1719
+ }
1720
+
1721
+ // Build shell command — use cwd for project path (never interpolate into shell string)
1698
1722
  let shellCommand;
1699
1723
  if (isPlainShell) {
1700
- // Plain shell mode - just run the initial command in the project directory
1701
- if (os.platform() === 'win32') {
1702
- shellCommand = `Set-Location -Path "${projectPath}"; ${initialCommand}`;
1703
- } else {
1704
- shellCommand = `cd "${projectPath}" && ${initialCommand}`;
1705
- }
1724
+ // Plain shell mode - run the initial command in the project directory
1725
+ shellCommand = initialCommand;
1706
1726
  } else if (provider === 'cursor') {
1707
- // Use cursor-agent command
1708
- if (os.platform() === 'win32') {
1709
- if (hasSession && sessionId) {
1710
- shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent --resume="${sessionId}"`;
1711
- } else {
1712
- shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent`;
1713
- }
1727
+ if (hasSession && sessionId) {
1728
+ shellCommand = `cursor-agent --resume="${sessionId}"`;
1714
1729
  } else {
1715
- if (hasSession && sessionId) {
1716
- shellCommand = `cd "${projectPath}" && cursor-agent --resume="${sessionId}"`;
1717
- } else {
1718
- shellCommand = `cd "${projectPath}" && cursor-agent`;
1719
- }
1730
+ shellCommand = 'cursor-agent';
1720
1731
  }
1721
-
1722
1732
  } else if (provider === 'codex') {
1723
- // Use codex command
1724
- if (os.platform() === 'win32') {
1725
- if (hasSession && sessionId) {
1726
- // Try to resume session, but with fallback to a new session if it fails
1727
- shellCommand = `Set-Location -Path "${projectPath}"; codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
1733
+ // Use codex command; attempt to resume and fall back to a new session when the resume fails.
1734
+ if (hasSession && sessionId) {
1735
+ if (os.platform() === 'win32') {
1736
+ // PowerShell syntax for fallback
1737
+ shellCommand = `codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
1728
1738
  } else {
1729
- shellCommand = `Set-Location -Path "${projectPath}"; codex`;
1739
+ shellCommand = `codex resume "${sessionId}" || codex`;
1730
1740
  }
1731
1741
  } else {
1732
- if (hasSession && sessionId) {
1733
- // Try to resume session, but with fallback to a new session if it fails
1734
- shellCommand = `cd "${projectPath}" && codex resume "${sessionId}" || codex`;
1735
- } else {
1736
- shellCommand = `cd "${projectPath}" && codex`;
1737
- }
1742
+ shellCommand = 'codex';
1738
1743
  }
1739
1744
  } else if (provider === 'gemini') {
1740
- // Use gemini command
1741
1745
  const command = initialCommand || 'gemini';
1742
1746
  let resumeId = sessionId;
1743
1747
  if (hasSession && sessionId) {
@@ -1748,41 +1752,32 @@ function handleShellConnection(ws) {
1748
1752
  const sess = sessionManager.getSession(sessionId);
1749
1753
  if (sess && sess.cliSessionId) {
1750
1754
  resumeId = sess.cliSessionId;
1755
+ // Validate the looked-up CLI session ID too
1756
+ if (!safeSessionIdPattern.test(resumeId)) {
1757
+ resumeId = null;
1758
+ }
1751
1759
  }
1752
1760
  } catch (err) {
1753
1761
  console.error('Failed to get Gemini CLI session ID:', err);
1754
1762
  }
1755
1763
  }
1756
1764
 
1757
- if (os.platform() === 'win32') {
1758
- if (hasSession && resumeId) {
1759
- shellCommand = `Set-Location -Path "${projectPath}"; ${command} --resume "${resumeId}"`;
1760
- } else {
1761
- shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
1762
- }
1765
+ if (hasSession && resumeId) {
1766
+ shellCommand = `${command} --resume "${resumeId}"`;
1763
1767
  } else {
1764
- if (hasSession && resumeId) {
1765
- shellCommand = `cd "${projectPath}" && ${command} --resume "${resumeId}"`;
1766
- } else {
1767
- shellCommand = `cd "${projectPath}" && ${command}`;
1768
- }
1768
+ shellCommand = command;
1769
1769
  }
1770
1770
  } else {
1771
- // Use claude command (default) or initialCommand if provided
1771
+ // Claude (default provider)
1772
1772
  const command = initialCommand || 'claude';
1773
- if (os.platform() === 'win32') {
1774
- if (hasSession && sessionId) {
1775
- // Try to resume session, but with fallback to new session if it fails
1776
- shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`;
1773
+ if (hasSession && sessionId) {
1774
+ if (os.platform() === 'win32') {
1775
+ shellCommand = `claude --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
1777
1776
  } else {
1778
- shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
1777
+ shellCommand = `claude --resume "${sessionId}" || claude`;
1779
1778
  }
1780
1779
  } else {
1781
- if (hasSession && sessionId) {
1782
- shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`;
1783
- } else {
1784
- shellCommand = `cd "${projectPath}" && ${command}`;
1785
- }
1780
+ shellCommand = command;
1786
1781
  }
1787
1782
  }
1788
1783
 
@@ -1801,7 +1796,7 @@ function handleShellConnection(ws) {
1801
1796
  name: 'xterm-256color',
1802
1797
  cols: termCols,
1803
1798
  rows: termRows,
1804
- cwd: os.homedir(),
1799
+ cwd: resolvedProjectPath,
1805
1800
  env: {
1806
1801
  ...process.env,
1807
1802
  TERM: 'xterm-256color',
@@ -2532,7 +2527,20 @@ async function startServer() {
2532
2527
 
2533
2528
  // Start watching the projects folder for changes
2534
2529
  await setupProjectsWatcher();
2530
+
2531
+ // Start server-side plugin processes for enabled plugins
2532
+ startEnabledPluginServers().catch(err => {
2533
+ console.error('[Plugins] Error during startup:', err.message);
2534
+ });
2535
2535
  });
2536
+
2537
+ // Clean up plugin processes on shutdown
2538
+ const shutdownPlugins = async () => {
2539
+ await stopAllPlugins();
2540
+ process.exit(0);
2541
+ };
2542
+ process.on('SIGTERM', () => void shutdownPlugins());
2543
+ process.on('SIGINT', () => void shutdownPlugins());
2536
2544
  } catch (error) {
2537
2545
  console.error('[ERROR] Failed to start server:', error);
2538
2546
  process.exit(1);
@@ -1,9 +1,9 @@
1
1
  import jwt from 'jsonwebtoken';
2
- import { userDb } from '../database/db.js';
2
+ import { userDb, appConfigDb } from '../database/db.js';
3
3
  import { IS_PLATFORM } from '../constants/config.js';
4
4
 
5
- // Get JWT secret from environment or use default (for development)
6
- const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
5
+ // Use env var if set, otherwise auto-generate a unique secret per installation
6
+ const JWT_SECRET = process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
7
7
 
8
8
  // Optional API key middleware
9
9
  const validateApiKey = (req, res, next) => {
@@ -58,6 +58,16 @@ const authenticateToken = async (req, res, next) => {
58
58
  return res.status(401).json({ error: 'Invalid token. User not found.' });
59
59
  }
60
60
 
61
+ // Auto-refresh: if token is past halfway through its lifetime, issue a new one
62
+ if (decoded.exp && decoded.iat) {
63
+ const now = Math.floor(Date.now() / 1000);
64
+ const halfLife = (decoded.exp - decoded.iat) / 2;
65
+ if (now > decoded.iat + halfLife) {
66
+ const newToken = generateToken(user);
67
+ res.setHeader('X-Refreshed-Token', newToken);
68
+ }
69
+ }
70
+
61
71
  req.user = user;
62
72
  next();
63
73
  } catch (error) {
@@ -66,15 +76,15 @@ const authenticateToken = async (req, res, next) => {
66
76
  }
67
77
  };
68
78
 
69
- // Generate JWT token (never expires)
79
+ // Generate JWT token
70
80
  const generateToken = (user) => {
71
81
  return jwt.sign(
72
- {
73
- userId: user.id,
74
- username: user.username
82
+ {
83
+ userId: user.id,
84
+ username: user.username
75
85
  },
76
- JWT_SECRET
77
- // No expiration - token lasts forever
86
+ JWT_SECRET,
87
+ { expiresIn: '7d' }
78
88
  );
79
89
  };
80
90
 
@@ -101,7 +111,12 @@ const authenticateWebSocket = (token) => {
101
111
 
102
112
  try {
103
113
  const decoded = jwt.verify(token, JWT_SECRET);
104
- return decoded;
114
+ // Verify user actually exists in database (matches REST authenticateToken behavior)
115
+ const user = userDb.getUserById(decoded.userId);
116
+ if (!user) {
117
+ return null;
118
+ }
119
+ return { userId: user.id, username: user.username };
105
120
  } catch (error) {
106
121
  console.error('WebSocket token verification error:', error);
107
122
  return null;
@@ -3,8 +3,8 @@ import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import os from 'os';
6
- import matter from 'gray-matter';
7
6
  import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
7
+ import { parseFrontmatter } from '../utils/frontmatter.js';
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
@@ -38,7 +38,7 @@ async function scanCommandsDirectory(dir, baseDir, namespace) {
38
38
  // Parse markdown file for metadata
39
39
  try {
40
40
  const content = await fs.readFile(fullPath, 'utf8');
41
- const { data: frontmatter, content: commandContent } = matter(content);
41
+ const { data: frontmatter, content: commandContent } = parseFrontmatter(content);
42
42
 
43
43
  // Calculate relative path from baseDir for command name
44
44
  const relativePath = path.relative(baseDir, fullPath);
@@ -475,7 +475,7 @@ router.post('/load', async (req, res) => {
475
475
 
476
476
  // Read and parse the command file
477
477
  const content = await fs.readFile(commandPath, 'utf8');
478
- const { data: metadata, content: commandContent } = matter(content);
478
+ const { data: metadata, content: commandContent } = parseFrontmatter(content);
479
479
 
480
480
  res.json({
481
481
  path: commandPath,
@@ -560,7 +560,7 @@ router.post('/execute', async (req, res) => {
560
560
  }
561
561
  }
562
562
  const content = await fs.readFile(commandPath, 'utf8');
563
- const { data: metadata, content: commandContent } = matter(content);
563
+ const { data: metadata, content: commandContent } = parseFrontmatter(content);
564
564
  // Basic argument replacement (will be enhanced in command parser utility)
565
565
  let processedContent = commandContent;
566
566