@siteboon/claude-code-ui 1.11.0 → 1.13.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.md +19 -16
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-Cc6pl7ji.css +32 -0
- package/dist/assets/index-Zq2roSUR.js +1206 -0
- package/dist/assets/{vendor-codemirror-B7BYDWj-.js → vendor-codemirror-CnTQH7Pk.js} +1 -1
- package/dist/assets/{vendor-react-7V_UDHjJ.js → vendor-react-DVSKlM5e.js} +9 -9
- package/dist/assets/{vendor-xterm-jI4BCHEb.js → vendor-xterm-DfaPXD3y.js} +12 -12
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/index.html +6 -6
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/logo-32.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-64.png +0 -0
- package/dist/logo.svg +17 -9
- package/package.json +7 -1
- package/server/claude-sdk.js +20 -19
- package/server/database/auth.db +0 -0
- package/server/database/db.js +73 -0
- package/server/database/init.sql +4 -1
- package/server/index.js +263 -29
- package/server/middleware/auth.js +34 -3
- package/server/openai-codex.js +387 -0
- package/server/projects.js +448 -7
- package/server/routes/agent.js +42 -4
- package/server/routes/cli-auth.js +263 -0
- package/server/routes/codex.js +310 -0
- package/server/routes/git.js +123 -28
- package/server/routes/projects.js +378 -0
- package/server/routes/taskmaster.js +2 -10
- package/server/routes/user.js +106 -0
- package/server/utils/gitConfig.js +24 -0
- package/dist/assets/index-B4_v-YUz.css +0 -32
- package/dist/assets/index-BZX1vtg9.js +0 -932
package/server/index.js
CHANGED
|
@@ -60,6 +60,7 @@ import mime from 'mime-types';
|
|
|
60
60
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
|
61
61
|
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions } from './claude-sdk.js';
|
|
62
62
|
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
|
63
|
+
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
|
|
63
64
|
import gitRoutes from './routes/git.js';
|
|
64
65
|
import authRoutes from './routes/auth.js';
|
|
65
66
|
import mcpRoutes from './routes/mcp.js';
|
|
@@ -69,6 +70,10 @@ import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
|
69
70
|
import commandsRoutes from './routes/commands.js';
|
|
70
71
|
import settingsRoutes from './routes/settings.js';
|
|
71
72
|
import agentRoutes from './routes/agent.js';
|
|
73
|
+
import projectsRoutes from './routes/projects.js';
|
|
74
|
+
import cliAuthRoutes from './routes/cli-auth.js';
|
|
75
|
+
import userRoutes from './routes/user.js';
|
|
76
|
+
import codexRoutes from './routes/codex.js';
|
|
72
77
|
import { initializeDatabase } from './database/db.js';
|
|
73
78
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
74
79
|
|
|
@@ -163,12 +168,28 @@ async function setupProjectsWatcher() {
|
|
|
163
168
|
const app = express();
|
|
164
169
|
const server = http.createServer(app);
|
|
165
170
|
|
|
171
|
+
const ptySessionsMap = new Map();
|
|
172
|
+
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
173
|
+
|
|
166
174
|
// Single WebSocket server that handles both paths
|
|
167
175
|
const wss = new WebSocketServer({
|
|
168
176
|
server,
|
|
169
177
|
verifyClient: (info) => {
|
|
170
178
|
console.log('WebSocket connection attempt to:', info.req.url);
|
|
171
179
|
|
|
180
|
+
// Platform mode: always allow connection
|
|
181
|
+
if (process.env.VITE_IS_PLATFORM === 'true') {
|
|
182
|
+
const user = authenticateWebSocket(null); // Will return first user
|
|
183
|
+
if (!user) {
|
|
184
|
+
console.log('[WARN] Platform mode: No user found in database');
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
info.req.user = user;
|
|
188
|
+
console.log('[OK] Platform mode WebSocket authenticated for user:', user.username);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Normal mode: verify token
|
|
172
193
|
// Extract token from query parameters or headers
|
|
173
194
|
const url = new URL(info.req.url, 'http://localhost');
|
|
174
195
|
const token = url.searchParams.get('token') ||
|
|
@@ -192,15 +213,36 @@ const wss = new WebSocketServer({
|
|
|
192
213
|
app.locals.wss = wss;
|
|
193
214
|
|
|
194
215
|
app.use(cors());
|
|
195
|
-
app.use(express.json({
|
|
216
|
+
app.use(express.json({
|
|
217
|
+
limit: '50mb',
|
|
218
|
+
type: (req) => {
|
|
219
|
+
// Skip multipart/form-data requests (for file uploads like images)
|
|
220
|
+
const contentType = req.headers['content-type'] || '';
|
|
221
|
+
if (contentType.includes('multipart/form-data')) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return contentType.includes('json');
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
196
227
|
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
|
197
228
|
|
|
229
|
+
// Public health check endpoint (no authentication required)
|
|
230
|
+
app.get('/health', (req, res) => {
|
|
231
|
+
res.json({
|
|
232
|
+
status: 'ok',
|
|
233
|
+
timestamp: new Date().toISOString()
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
198
237
|
// Optional API key validation (if configured)
|
|
199
238
|
app.use('/api', validateApiKey);
|
|
200
239
|
|
|
201
240
|
// Authentication routes (public)
|
|
202
241
|
app.use('/api/auth', authRoutes);
|
|
203
242
|
|
|
243
|
+
// Projects API Routes (protected)
|
|
244
|
+
app.use('/api/projects', authenticateToken, projectsRoutes);
|
|
245
|
+
|
|
204
246
|
// Git API Routes (protected)
|
|
205
247
|
app.use('/api/git', authenticateToken, gitRoutes);
|
|
206
248
|
|
|
@@ -222,6 +264,15 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
|
222
264
|
// Settings API Routes (protected)
|
|
223
265
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
|
224
266
|
|
|
267
|
+
// CLI Authentication API Routes (protected)
|
|
268
|
+
app.use('/api/cli', authenticateToken, cliAuthRoutes);
|
|
269
|
+
|
|
270
|
+
// User API Routes (protected)
|
|
271
|
+
app.use('/api/user', authenticateToken, userRoutes);
|
|
272
|
+
|
|
273
|
+
// Codex API Routes (protected)
|
|
274
|
+
app.use('/api/codex', authenticateToken, codexRoutes);
|
|
275
|
+
|
|
225
276
|
// Agent API Routes (uses API key authentication)
|
|
226
277
|
app.use('/api/agent', agentRoutes);
|
|
227
278
|
|
|
@@ -245,17 +296,8 @@ app.use(express.static(path.join(__dirname, '../dist'), {
|
|
|
245
296
|
}));
|
|
246
297
|
|
|
247
298
|
// API Routes (protected)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const protocol = req.protocol === 'https' || req.get('x-forwarded-proto') === 'https' ? 'wss' : 'ws';
|
|
251
|
-
|
|
252
|
-
console.log('Config API called - Returning host:', host, 'Protocol:', protocol);
|
|
253
|
-
|
|
254
|
-
res.json({
|
|
255
|
-
serverPort: PORT,
|
|
256
|
-
wsUrl: `${protocol}://${host}`
|
|
257
|
-
});
|
|
258
|
-
});
|
|
299
|
+
// /api/config endpoint removed - no longer needed
|
|
300
|
+
// Frontend now uses window.location for WebSocket URLs
|
|
259
301
|
|
|
260
302
|
// System update endpoint
|
|
261
303
|
app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
@@ -381,9 +423,12 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
|
|
|
381
423
|
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
|
|
382
424
|
try {
|
|
383
425
|
const { projectName, sessionId } = req.params;
|
|
426
|
+
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
|
384
427
|
await deleteSession(projectName, sessionId);
|
|
428
|
+
console.log(`[API] Session ${sessionId} deleted successfully`);
|
|
385
429
|
res.json({ success: true });
|
|
386
430
|
} catch (error) {
|
|
431
|
+
console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
|
|
387
432
|
res.status(500).json({ error: error.message });
|
|
388
433
|
}
|
|
389
434
|
});
|
|
@@ -696,6 +741,12 @@ function handleChatConnection(ws) {
|
|
|
696
741
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
697
742
|
console.log('🤖 Model:', data.options?.model || 'default');
|
|
698
743
|
await spawnCursor(data.command, data.options, ws);
|
|
744
|
+
} else if (data.type === 'codex-command') {
|
|
745
|
+
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
|
|
746
|
+
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
747
|
+
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
748
|
+
console.log('🤖 Model:', data.options?.model || 'default');
|
|
749
|
+
await queryCodex(data.command, data.options, ws);
|
|
699
750
|
} else if (data.type === 'cursor-resume') {
|
|
700
751
|
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
701
752
|
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
|
@@ -711,6 +762,8 @@ function handleChatConnection(ws) {
|
|
|
711
762
|
|
|
712
763
|
if (provider === 'cursor') {
|
|
713
764
|
success = abortCursorSession(data.sessionId);
|
|
765
|
+
} else if (provider === 'codex') {
|
|
766
|
+
success = abortCodexSession(data.sessionId);
|
|
714
767
|
} else {
|
|
715
768
|
// Use Claude Agents SDK
|
|
716
769
|
success = await abortClaudeSDKSession(data.sessionId);
|
|
@@ -739,6 +792,8 @@ function handleChatConnection(ws) {
|
|
|
739
792
|
|
|
740
793
|
if (provider === 'cursor') {
|
|
741
794
|
isActive = isCursorSessionActive(sessionId);
|
|
795
|
+
} else if (provider === 'codex') {
|
|
796
|
+
isActive = isCodexSessionActive(sessionId);
|
|
742
797
|
} else {
|
|
743
798
|
// Use Claude Agents SDK
|
|
744
799
|
isActive = isClaudeSDKSessionActive(sessionId);
|
|
@@ -754,7 +809,8 @@ function handleChatConnection(ws) {
|
|
|
754
809
|
// Get all currently active sessions
|
|
755
810
|
const activeSessions = {
|
|
756
811
|
claude: getActiveClaudeSDKSessions(),
|
|
757
|
-
cursor: getActiveCursorSessions()
|
|
812
|
+
cursor: getActiveCursorSessions(),
|
|
813
|
+
codex: getActiveCodexSessions()
|
|
758
814
|
};
|
|
759
815
|
ws.send(JSON.stringify({
|
|
760
816
|
type: 'active-sessions',
|
|
@@ -781,6 +837,8 @@ function handleChatConnection(ws) {
|
|
|
781
837
|
function handleShellConnection(ws) {
|
|
782
838
|
console.log('🐚 Shell client connected');
|
|
783
839
|
let shellProcess = null;
|
|
840
|
+
let ptySessionKey = null;
|
|
841
|
+
let outputBuffer = [];
|
|
784
842
|
|
|
785
843
|
ws.on('message', async (message) => {
|
|
786
844
|
try {
|
|
@@ -788,7 +846,6 @@ function handleShellConnection(ws) {
|
|
|
788
846
|
console.log('📨 Shell message received:', data.type);
|
|
789
847
|
|
|
790
848
|
if (data.type === 'init') {
|
|
791
|
-
// Initialize shell with project path and session info
|
|
792
849
|
const projectPath = data.projectPath || process.cwd();
|
|
793
850
|
const sessionId = data.sessionId;
|
|
794
851
|
const hasSession = data.hasSession;
|
|
@@ -796,6 +853,57 @@ function handleShellConnection(ws) {
|
|
|
796
853
|
const initialCommand = data.initialCommand;
|
|
797
854
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
798
855
|
|
|
856
|
+
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
|
857
|
+
const isLoginCommand = initialCommand && (
|
|
858
|
+
initialCommand.includes('setup-token') ||
|
|
859
|
+
initialCommand.includes('cursor-agent login') ||
|
|
860
|
+
initialCommand.includes('auth login')
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
// Include command hash in session key so different commands get separate sessions
|
|
864
|
+
const commandSuffix = isPlainShell && initialCommand
|
|
865
|
+
? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
|
|
866
|
+
: '';
|
|
867
|
+
ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
|
|
868
|
+
|
|
869
|
+
// Kill any existing login session before starting fresh
|
|
870
|
+
if (isLoginCommand) {
|
|
871
|
+
const oldSession = ptySessionsMap.get(ptySessionKey);
|
|
872
|
+
if (oldSession) {
|
|
873
|
+
console.log('🧹 Cleaning up existing login session:', ptySessionKey);
|
|
874
|
+
if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
|
|
875
|
+
if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
|
|
876
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
|
|
881
|
+
if (existingSession) {
|
|
882
|
+
console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
|
|
883
|
+
shellProcess = existingSession.pty;
|
|
884
|
+
|
|
885
|
+
clearTimeout(existingSession.timeoutId);
|
|
886
|
+
|
|
887
|
+
ws.send(JSON.stringify({
|
|
888
|
+
type: 'output',
|
|
889
|
+
data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
|
|
890
|
+
}));
|
|
891
|
+
|
|
892
|
+
if (existingSession.buffer && existingSession.buffer.length > 0) {
|
|
893
|
+
console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
|
|
894
|
+
existingSession.buffer.forEach(bufferedData => {
|
|
895
|
+
ws.send(JSON.stringify({
|
|
896
|
+
type: 'output',
|
|
897
|
+
data: bufferedData
|
|
898
|
+
}));
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
existingSession.ws = ws;
|
|
903
|
+
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
799
907
|
console.log('[INFO] Starting shell in:', projectPath);
|
|
800
908
|
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
|
|
801
909
|
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
|
|
@@ -869,10 +977,15 @@ function handleShellConnection(ws) {
|
|
|
869
977
|
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
870
978
|
const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
|
871
979
|
|
|
980
|
+
// Use terminal dimensions from client if provided, otherwise use defaults
|
|
981
|
+
const termCols = data.cols || 80;
|
|
982
|
+
const termRows = data.rows || 24;
|
|
983
|
+
console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
|
|
984
|
+
|
|
872
985
|
shellProcess = pty.spawn(shell, shellArgs, {
|
|
873
986
|
name: 'xterm-256color',
|
|
874
|
-
cols:
|
|
875
|
-
rows:
|
|
987
|
+
cols: termCols,
|
|
988
|
+
rows: termRows,
|
|
876
989
|
cwd: process.env.HOME || (os.platform() === 'win32' ? process.env.USERPROFILE : '/'),
|
|
877
990
|
env: {
|
|
878
991
|
...process.env,
|
|
@@ -886,9 +999,28 @@ function handleShellConnection(ws) {
|
|
|
886
999
|
|
|
887
1000
|
console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
|
|
888
1001
|
|
|
1002
|
+
ptySessionsMap.set(ptySessionKey, {
|
|
1003
|
+
pty: shellProcess,
|
|
1004
|
+
ws: ws,
|
|
1005
|
+
buffer: [],
|
|
1006
|
+
timeoutId: null,
|
|
1007
|
+
projectPath,
|
|
1008
|
+
sessionId
|
|
1009
|
+
});
|
|
1010
|
+
|
|
889
1011
|
// Handle data output
|
|
890
1012
|
shellProcess.onData((data) => {
|
|
891
|
-
|
|
1013
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1014
|
+
if (!session) return;
|
|
1015
|
+
|
|
1016
|
+
if (session.buffer.length < 5000) {
|
|
1017
|
+
session.buffer.push(data);
|
|
1018
|
+
} else {
|
|
1019
|
+
session.buffer.shift();
|
|
1020
|
+
session.buffer.push(data);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
892
1024
|
let outputData = data;
|
|
893
1025
|
|
|
894
1026
|
// Check for various URL opening patterns
|
|
@@ -912,7 +1044,7 @@ function handleShellConnection(ws) {
|
|
|
912
1044
|
console.log('[DEBUG] Detected URL for opening:', url);
|
|
913
1045
|
|
|
914
1046
|
// Send URL opening message to client
|
|
915
|
-
ws.send(JSON.stringify({
|
|
1047
|
+
session.ws.send(JSON.stringify({
|
|
916
1048
|
type: 'url_open',
|
|
917
1049
|
url: url
|
|
918
1050
|
}));
|
|
@@ -925,7 +1057,7 @@ function handleShellConnection(ws) {
|
|
|
925
1057
|
});
|
|
926
1058
|
|
|
927
1059
|
// Send regular output
|
|
928
|
-
ws.send(JSON.stringify({
|
|
1060
|
+
session.ws.send(JSON.stringify({
|
|
929
1061
|
type: 'output',
|
|
930
1062
|
data: outputData
|
|
931
1063
|
}));
|
|
@@ -935,12 +1067,17 @@ function handleShellConnection(ws) {
|
|
|
935
1067
|
// Handle process exit
|
|
936
1068
|
shellProcess.onExit((exitCode) => {
|
|
937
1069
|
console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
|
|
938
|
-
|
|
939
|
-
|
|
1070
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1071
|
+
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1072
|
+
session.ws.send(JSON.stringify({
|
|
940
1073
|
type: 'output',
|
|
941
1074
|
data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
|
|
942
1075
|
}));
|
|
943
1076
|
}
|
|
1077
|
+
if (session && session.timeoutId) {
|
|
1078
|
+
clearTimeout(session.timeoutId);
|
|
1079
|
+
}
|
|
1080
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
944
1081
|
shellProcess = null;
|
|
945
1082
|
});
|
|
946
1083
|
|
|
@@ -983,9 +1120,21 @@ function handleShellConnection(ws) {
|
|
|
983
1120
|
|
|
984
1121
|
ws.on('close', () => {
|
|
985
1122
|
console.log('🔌 Shell client disconnected');
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1123
|
+
|
|
1124
|
+
if (ptySessionKey) {
|
|
1125
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1126
|
+
if (session) {
|
|
1127
|
+
console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
|
|
1128
|
+
session.ws = null;
|
|
1129
|
+
|
|
1130
|
+
session.timeoutId = setTimeout(() => {
|
|
1131
|
+
console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
|
|
1132
|
+
if (session.pty && session.pty.kill) {
|
|
1133
|
+
session.pty.kill();
|
|
1134
|
+
}
|
|
1135
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
1136
|
+
}, PTY_SESSION_TIMEOUT);
|
|
1137
|
+
}
|
|
989
1138
|
}
|
|
990
1139
|
});
|
|
991
1140
|
|
|
@@ -1231,8 +1380,98 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|
|
1231
1380
|
app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
|
1232
1381
|
try {
|
|
1233
1382
|
const { projectName, sessionId } = req.params;
|
|
1383
|
+
const { provider = 'claude' } = req.query;
|
|
1234
1384
|
const homeDir = os.homedir();
|
|
1235
1385
|
|
|
1386
|
+
// Allow only safe characters in sessionId
|
|
1387
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1388
|
+
if (!safeSessionId) {
|
|
1389
|
+
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
|
1393
|
+
if (provider === 'cursor') {
|
|
1394
|
+
return res.json({
|
|
1395
|
+
used: 0,
|
|
1396
|
+
total: 0,
|
|
1397
|
+
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
|
1398
|
+
unsupported: true,
|
|
1399
|
+
message: 'Token usage tracking not available for Cursor sessions'
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Handle Codex sessions
|
|
1404
|
+
if (provider === 'codex') {
|
|
1405
|
+
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
1406
|
+
|
|
1407
|
+
// Find the session file by searching for the session ID
|
|
1408
|
+
const findSessionFile = async (dir) => {
|
|
1409
|
+
try {
|
|
1410
|
+
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
1411
|
+
for (const entry of entries) {
|
|
1412
|
+
const fullPath = path.join(dir, entry.name);
|
|
1413
|
+
if (entry.isDirectory()) {
|
|
1414
|
+
const found = await findSessionFile(fullPath);
|
|
1415
|
+
if (found) return found;
|
|
1416
|
+
} else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
|
|
1417
|
+
return fullPath;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
// Skip directories we can't read
|
|
1422
|
+
}
|
|
1423
|
+
return null;
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
const sessionFilePath = await findSessionFile(codexSessionsDir);
|
|
1427
|
+
|
|
1428
|
+
if (!sessionFilePath) {
|
|
1429
|
+
return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Read and parse the Codex JSONL file
|
|
1433
|
+
let fileContent;
|
|
1434
|
+
try {
|
|
1435
|
+
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
if (error.code === 'ENOENT') {
|
|
1438
|
+
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
1439
|
+
}
|
|
1440
|
+
throw error;
|
|
1441
|
+
}
|
|
1442
|
+
const lines = fileContent.trim().split('\n');
|
|
1443
|
+
let totalTokens = 0;
|
|
1444
|
+
let contextWindow = 200000; // Default for Codex/OpenAI
|
|
1445
|
+
|
|
1446
|
+
// Find the latest token_count event with info (scan from end)
|
|
1447
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1448
|
+
try {
|
|
1449
|
+
const entry = JSON.parse(lines[i]);
|
|
1450
|
+
|
|
1451
|
+
// Codex stores token info in event_msg with type: "token_count"
|
|
1452
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
1453
|
+
const tokenInfo = entry.payload.info;
|
|
1454
|
+
if (tokenInfo.total_token_usage) {
|
|
1455
|
+
totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
|
|
1456
|
+
}
|
|
1457
|
+
if (tokenInfo.model_context_window) {
|
|
1458
|
+
contextWindow = tokenInfo.model_context_window;
|
|
1459
|
+
}
|
|
1460
|
+
break; // Stop after finding the latest token count
|
|
1461
|
+
}
|
|
1462
|
+
} catch (parseError) {
|
|
1463
|
+
// Skip lines that can't be parsed
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
return res.json({
|
|
1469
|
+
used: totalTokens,
|
|
1470
|
+
total: contextWindow
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Handle Claude sessions (default)
|
|
1236
1475
|
// Extract actual project path
|
|
1237
1476
|
let projectPath;
|
|
1238
1477
|
try {
|
|
@@ -1248,11 +1487,6 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
1248
1487
|
const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-');
|
|
1249
1488
|
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
1250
1489
|
|
|
1251
|
-
// Allow only safe characters in sessionId
|
|
1252
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1253
|
-
if (!safeSessionId) {
|
|
1254
|
-
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
1255
|
-
}
|
|
1256
1490
|
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
|
1257
1491
|
|
|
1258
1492
|
// Constrain to projectDir
|
|
@@ -20,6 +20,22 @@ const validateApiKey = (req, res, next) => {
|
|
|
20
20
|
|
|
21
21
|
// JWT authentication middleware
|
|
22
22
|
const authenticateToken = async (req, res, next) => {
|
|
23
|
+
// Platform mode: use single database user
|
|
24
|
+
if (process.env.VITE_IS_PLATFORM === 'true') {
|
|
25
|
+
try {
|
|
26
|
+
const user = userDb.getFirstUser();
|
|
27
|
+
if (!user) {
|
|
28
|
+
return res.status(500).json({ error: 'Platform mode: No user found in database' });
|
|
29
|
+
}
|
|
30
|
+
req.user = user;
|
|
31
|
+
return next();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Platform mode error:', error);
|
|
34
|
+
return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Normal OSS JWT validation
|
|
23
39
|
const authHeader = req.headers['authorization'];
|
|
24
40
|
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
|
25
41
|
|
|
@@ -29,13 +45,13 @@ const authenticateToken = async (req, res, next) => {
|
|
|
29
45
|
|
|
30
46
|
try {
|
|
31
47
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
32
|
-
|
|
48
|
+
|
|
33
49
|
// Verify user still exists and is active
|
|
34
50
|
const user = userDb.getUserById(decoded.userId);
|
|
35
51
|
if (!user) {
|
|
36
52
|
return res.status(401).json({ error: 'Invalid token. User not found.' });
|
|
37
53
|
}
|
|
38
|
-
|
|
54
|
+
|
|
39
55
|
req.user = user;
|
|
40
56
|
next();
|
|
41
57
|
} catch (error) {
|
|
@@ -58,10 +74,25 @@ const generateToken = (user) => {
|
|
|
58
74
|
|
|
59
75
|
// WebSocket authentication function
|
|
60
76
|
const authenticateWebSocket = (token) => {
|
|
77
|
+
// Platform mode: bypass token validation, return first user
|
|
78
|
+
if (process.env.VITE_IS_PLATFORM === 'true') {
|
|
79
|
+
try {
|
|
80
|
+
const user = userDb.getFirstUser();
|
|
81
|
+
if (user) {
|
|
82
|
+
return { userId: user.id, username: user.username };
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Platform mode WebSocket error:', error);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Normal OSS JWT validation
|
|
61
92
|
if (!token) {
|
|
62
93
|
return null;
|
|
63
94
|
}
|
|
64
|
-
|
|
95
|
+
|
|
65
96
|
try {
|
|
66
97
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
67
98
|
return decoded;
|