@siteboon/claude-code-ui 1.23.2 → 1.25.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/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-C6ZomNnQ.js → index-BmWWsL1A.js} +298 -255
- package/dist/assets/index-CO53aUoS.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/database/db.js +44 -0
- package/server/database/init.sql +8 -1
- package/server/index.js +105 -62
- package/server/middleware/auth.js +25 -10
- package/server/projects.js +684 -5
- package/server/routes/gemini.js +7 -1
- package/server/routes/git.js +127 -71
- package/server/routes/plugins.js +303 -0
- package/server/routes/projects.js +4 -6
- package/server/routes/user.js +22 -5
- package/server/utils/gitConfig.js +15 -5
- 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-BFyod1Qa.css +0 -32
package/server/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import pty from 'node-pty';
|
|
|
44
44
|
import fetch from 'node-fetch';
|
|
45
45
|
import mime from 'mime-types';
|
|
46
46
|
|
|
47
|
-
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
|
47
|
+
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, searchConversations } from './projects.js';
|
|
48
48
|
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js';
|
|
49
49
|
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
|
50
50
|
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
|
|
@@ -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
|
|
|
@@ -608,6 +613,51 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => {
|
|
|
608
613
|
}
|
|
609
614
|
});
|
|
610
615
|
|
|
616
|
+
// Search conversations content (SSE streaming)
|
|
617
|
+
app.get('/api/search/conversations', authenticateToken, async (req, res) => {
|
|
618
|
+
const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
619
|
+
const parsedLimit = Number.parseInt(String(req.query.limit), 10);
|
|
620
|
+
const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 100));
|
|
621
|
+
|
|
622
|
+
if (query.length < 2) {
|
|
623
|
+
return res.status(400).json({ error: 'Query must be at least 2 characters' });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
res.writeHead(200, {
|
|
627
|
+
'Content-Type': 'text/event-stream',
|
|
628
|
+
'Cache-Control': 'no-cache',
|
|
629
|
+
'Connection': 'keep-alive',
|
|
630
|
+
'X-Accel-Buffering': 'no',
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
let closed = false;
|
|
634
|
+
const abortController = new AbortController();
|
|
635
|
+
req.on('close', () => { closed = true; abortController.abort(); });
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
await searchConversations(query, limit, ({ projectResult, totalMatches, scannedProjects, totalProjects }) => {
|
|
639
|
+
if (closed) return;
|
|
640
|
+
if (projectResult) {
|
|
641
|
+
res.write(`event: result\ndata: ${JSON.stringify({ projectResult, totalMatches, scannedProjects, totalProjects })}\n\n`);
|
|
642
|
+
} else {
|
|
643
|
+
res.write(`event: progress\ndata: ${JSON.stringify({ totalMatches, scannedProjects, totalProjects })}\n\n`);
|
|
644
|
+
}
|
|
645
|
+
}, abortController.signal);
|
|
646
|
+
if (!closed) {
|
|
647
|
+
res.write(`event: done\ndata: {}\n\n`);
|
|
648
|
+
}
|
|
649
|
+
} catch (error) {
|
|
650
|
+
console.error('Error searching conversations:', error);
|
|
651
|
+
if (!closed) {
|
|
652
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: 'Search failed' })}\n\n`);
|
|
653
|
+
}
|
|
654
|
+
} finally {
|
|
655
|
+
if (!closed) {
|
|
656
|
+
res.end();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
611
661
|
const expandWorkspacePath = (inputPath) => {
|
|
612
662
|
if (!inputPath) return inputPath;
|
|
613
663
|
if (inputPath === '~') {
|
|
@@ -1649,50 +1699,43 @@ function handleShellConnection(ws) {
|
|
|
1649
1699
|
}));
|
|
1650
1700
|
|
|
1651
1701
|
try {
|
|
1652
|
-
//
|
|
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)
|
|
1653
1722
|
let shellCommand;
|
|
1654
1723
|
if (isPlainShell) {
|
|
1655
|
-
// Plain shell mode -
|
|
1656
|
-
|
|
1657
|
-
shellCommand = `Set-Location -Path "${projectPath}"; ${initialCommand}`;
|
|
1658
|
-
} else {
|
|
1659
|
-
shellCommand = `cd "${projectPath}" && ${initialCommand}`;
|
|
1660
|
-
}
|
|
1724
|
+
// Plain shell mode - run the initial command in the project directory
|
|
1725
|
+
shellCommand = initialCommand;
|
|
1661
1726
|
} else if (provider === 'cursor') {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
if (hasSession && sessionId) {
|
|
1665
|
-
shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent --resume="${sessionId}"`;
|
|
1666
|
-
} else {
|
|
1667
|
-
shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent`;
|
|
1668
|
-
}
|
|
1727
|
+
if (hasSession && sessionId) {
|
|
1728
|
+
shellCommand = `cursor-agent --resume="${sessionId}"`;
|
|
1669
1729
|
} else {
|
|
1670
|
-
|
|
1671
|
-
shellCommand = `cd "${projectPath}" && cursor-agent --resume="${sessionId}"`;
|
|
1672
|
-
} else {
|
|
1673
|
-
shellCommand = `cd "${projectPath}" && cursor-agent`;
|
|
1674
|
-
}
|
|
1730
|
+
shellCommand = 'cursor-agent';
|
|
1675
1731
|
}
|
|
1676
|
-
|
|
1677
1732
|
} else if (provider === 'codex') {
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
if (hasSession && sessionId) {
|
|
1681
|
-
// Try to resume session, but with fallback to a new session if it fails
|
|
1682
|
-
shellCommand = `Set-Location -Path "${projectPath}"; codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
|
|
1683
|
-
} else {
|
|
1684
|
-
shellCommand = `Set-Location -Path "${projectPath}"; codex`;
|
|
1685
|
-
}
|
|
1733
|
+
if (hasSession && sessionId) {
|
|
1734
|
+
shellCommand = `codex resume "${sessionId}" || codex`;
|
|
1686
1735
|
} else {
|
|
1687
|
-
|
|
1688
|
-
// Try to resume session, but with fallback to a new session if it fails
|
|
1689
|
-
shellCommand = `cd "${projectPath}" && codex resume "${sessionId}" || codex`;
|
|
1690
|
-
} else {
|
|
1691
|
-
shellCommand = `cd "${projectPath}" && codex`;
|
|
1692
|
-
}
|
|
1736
|
+
shellCommand = 'codex';
|
|
1693
1737
|
}
|
|
1694
1738
|
} else if (provider === 'gemini') {
|
|
1695
|
-
// Use gemini command
|
|
1696
1739
|
const command = initialCommand || 'gemini';
|
|
1697
1740
|
let resumeId = sessionId;
|
|
1698
1741
|
if (hasSession && sessionId) {
|
|
@@ -1703,41 +1746,28 @@ function handleShellConnection(ws) {
|
|
|
1703
1746
|
const sess = sessionManager.getSession(sessionId);
|
|
1704
1747
|
if (sess && sess.cliSessionId) {
|
|
1705
1748
|
resumeId = sess.cliSessionId;
|
|
1749
|
+
// Validate the looked-up CLI session ID too
|
|
1750
|
+
if (!safeSessionIdPattern.test(resumeId)) {
|
|
1751
|
+
resumeId = null;
|
|
1752
|
+
}
|
|
1706
1753
|
}
|
|
1707
1754
|
} catch (err) {
|
|
1708
1755
|
console.error('Failed to get Gemini CLI session ID:', err);
|
|
1709
1756
|
}
|
|
1710
1757
|
}
|
|
1711
1758
|
|
|
1712
|
-
if (
|
|
1713
|
-
|
|
1714
|
-
shellCommand = `Set-Location -Path "${projectPath}"; ${command} --resume "${resumeId}"`;
|
|
1715
|
-
} else {
|
|
1716
|
-
shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
|
|
1717
|
-
}
|
|
1759
|
+
if (hasSession && resumeId) {
|
|
1760
|
+
shellCommand = `${command} --resume "${resumeId}"`;
|
|
1718
1761
|
} else {
|
|
1719
|
-
|
|
1720
|
-
shellCommand = `cd "${projectPath}" && ${command} --resume "${resumeId}"`;
|
|
1721
|
-
} else {
|
|
1722
|
-
shellCommand = `cd "${projectPath}" && ${command}`;
|
|
1723
|
-
}
|
|
1762
|
+
shellCommand = command;
|
|
1724
1763
|
}
|
|
1725
1764
|
} else {
|
|
1726
|
-
//
|
|
1765
|
+
// Claude (default provider)
|
|
1727
1766
|
const command = initialCommand || 'claude';
|
|
1728
|
-
if (
|
|
1729
|
-
|
|
1730
|
-
// Try to resume session, but with fallback to new session if it fails
|
|
1731
|
-
shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`;
|
|
1732
|
-
} else {
|
|
1733
|
-
shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
|
|
1734
|
-
}
|
|
1767
|
+
if (hasSession && sessionId) {
|
|
1768
|
+
shellCommand = `claude --resume "${sessionId}" || claude`;
|
|
1735
1769
|
} else {
|
|
1736
|
-
|
|
1737
|
-
shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`;
|
|
1738
|
-
} else {
|
|
1739
|
-
shellCommand = `cd "${projectPath}" && ${command}`;
|
|
1740
|
-
}
|
|
1770
|
+
shellCommand = command;
|
|
1741
1771
|
}
|
|
1742
1772
|
}
|
|
1743
1773
|
|
|
@@ -1756,7 +1786,7 @@ function handleShellConnection(ws) {
|
|
|
1756
1786
|
name: 'xterm-256color',
|
|
1757
1787
|
cols: termCols,
|
|
1758
1788
|
rows: termRows,
|
|
1759
|
-
cwd:
|
|
1789
|
+
cwd: resolvedProjectPath,
|
|
1760
1790
|
env: {
|
|
1761
1791
|
...process.env,
|
|
1762
1792
|
TERM: 'xterm-256color',
|
|
@@ -2487,7 +2517,20 @@ async function startServer() {
|
|
|
2487
2517
|
|
|
2488
2518
|
// Start watching the projects folder for changes
|
|
2489
2519
|
await setupProjectsWatcher();
|
|
2520
|
+
|
|
2521
|
+
// Start server-side plugin processes for enabled plugins
|
|
2522
|
+
startEnabledPluginServers().catch(err => {
|
|
2523
|
+
console.error('[Plugins] Error during startup:', err.message);
|
|
2524
|
+
});
|
|
2490
2525
|
});
|
|
2526
|
+
|
|
2527
|
+
// Clean up plugin processes on shutdown
|
|
2528
|
+
const shutdownPlugins = async () => {
|
|
2529
|
+
await stopAllPlugins();
|
|
2530
|
+
process.exit(0);
|
|
2531
|
+
};
|
|
2532
|
+
process.on('SIGTERM', () => void shutdownPlugins());
|
|
2533
|
+
process.on('SIGINT', () => void shutdownPlugins());
|
|
2491
2534
|
} catch (error) {
|
|
2492
2535
|
console.error('[ERROR] Failed to start server:', error);
|
|
2493
2536
|
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;
|