@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/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +31 -10
- package/README.ru.md +218 -0
- package/README.zh-CN.md +4 -4
- package/dist/assets/index-D6EffcRY.js +1263 -0
- package/dist/assets/index-WNTmA_ug.css +32 -0
- package/dist/index.html +2 -2
- package/dist/screenshots/cli-selection.png +0 -0
- package/dist/screenshots/mobile-chat.png +0 -0
- package/package.json +1 -1
- package/server/cursor-cli.js +189 -115
- package/server/database/db.js +44 -0
- package/server/database/init.sql +8 -1
- package/server/index.js +65 -57
- package/server/middleware/auth.js +25 -10
- package/server/routes/commands.js +4 -4
- package/server/routes/git.js +325 -89
- package/server/routes/plugins.js +303 -0
- package/server/utils/commandParser.js +2 -2
- package/server/utils/frontmatter.js +18 -0
- package/server/utils/plugin-loader.js +408 -0
- package/server/utils/plugin-process-manager.js +184 -0
- package/shared/modelConstants.js +47 -45
- package/dist/assets/index-Cyy9g2W2.js +0 -1204
- package/dist/assets/index-Dlc1jmTz.css +0 -32
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
|
-
//
|
|
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 -
|
|
1701
|
-
|
|
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
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
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 (
|
|
1725
|
-
if (
|
|
1726
|
-
//
|
|
1727
|
-
shellCommand = `
|
|
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 = `
|
|
1739
|
+
shellCommand = `codex resume "${sessionId}" || codex`;
|
|
1730
1740
|
}
|
|
1731
1741
|
} else {
|
|
1732
|
-
|
|
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 (
|
|
1758
|
-
|
|
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
|
-
|
|
1765
|
-
shellCommand = `cd "${projectPath}" && ${command} --resume "${resumeId}"`;
|
|
1766
|
-
} else {
|
|
1767
|
-
shellCommand = `cd "${projectPath}" && ${command}`;
|
|
1768
|
-
}
|
|
1768
|
+
shellCommand = command;
|
|
1769
1769
|
}
|
|
1770
1770
|
} else {
|
|
1771
|
-
//
|
|
1771
|
+
// Claude (default provider)
|
|
1772
1772
|
const command = initialCommand || 'claude';
|
|
1773
|
-
if (
|
|
1774
|
-
if (
|
|
1775
|
-
|
|
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 = `
|
|
1777
|
+
shellCommand = `claude --resume "${sessionId}" || claude`;
|
|
1779
1778
|
}
|
|
1780
1779
|
} else {
|
|
1781
|
-
|
|
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:
|
|
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
|
-
//
|
|
6
|
-
const JWT_SECRET = process.env.JWT_SECRET ||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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
|
|