@siteboon/claude-code-ui 1.12.0 → 1.13.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.md +19 -16
- package/dist/api-docs.html +30 -8
- package/dist/assets/index-BL1HpeHJ.js +1206 -0
- package/dist/assets/index-Cc6pl7ji.css +32 -0
- 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 +4 -4
- 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 +4 -1
- package/server/claude-sdk.js +32 -30
- package/server/cursor-cli.js +24 -24
- package/server/database/db.js +64 -0
- package/server/database/init.sql +4 -1
- package/server/index.js +278 -31
- package/server/openai-codex.js +388 -0
- package/server/projects.js +448 -7
- package/server/routes/agent.js +54 -8
- package/server/routes/cli-auth.js +263 -0
- package/server/routes/codex.js +310 -0
- package/server/routes/commands.js +6 -57
- package/server/routes/cursor.js +2 -1
- package/server/routes/git.js +123 -28
- 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-DXtzL-q9.css +0 -32
- package/dist/assets/index-Do2w3FiK.js +0 -1189
- package/server/database/auth.db +0 -0
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';
|
|
@@ -70,6 +71,9 @@ import commandsRoutes from './routes/commands.js';
|
|
|
70
71
|
import settingsRoutes from './routes/settings.js';
|
|
71
72
|
import agentRoutes from './routes/agent.js';
|
|
72
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';
|
|
73
77
|
import { initializeDatabase } from './database/db.js';
|
|
74
78
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
75
79
|
|
|
@@ -164,6 +168,9 @@ async function setupProjectsWatcher() {
|
|
|
164
168
|
const app = express();
|
|
165
169
|
const server = http.createServer(app);
|
|
166
170
|
|
|
171
|
+
const ptySessionsMap = new Map();
|
|
172
|
+
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
173
|
+
|
|
167
174
|
// Single WebSocket server that handles both paths
|
|
168
175
|
const wss = new WebSocketServer({
|
|
169
176
|
server,
|
|
@@ -206,7 +213,17 @@ const wss = new WebSocketServer({
|
|
|
206
213
|
app.locals.wss = wss;
|
|
207
214
|
|
|
208
215
|
app.use(cors());
|
|
209
|
-
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
|
+
}));
|
|
210
227
|
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
|
211
228
|
|
|
212
229
|
// Public health check endpoint (no authentication required)
|
|
@@ -247,6 +264,15 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
|
247
264
|
// Settings API Routes (protected)
|
|
248
265
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
|
249
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
|
+
|
|
250
276
|
// Agent API Routes (uses API key authentication)
|
|
251
277
|
app.use('/api/agent', agentRoutes);
|
|
252
278
|
|
|
@@ -397,9 +423,12 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
|
|
|
397
423
|
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
|
|
398
424
|
try {
|
|
399
425
|
const { projectName, sessionId } = req.params;
|
|
426
|
+
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
|
400
427
|
await deleteSession(projectName, sessionId);
|
|
428
|
+
console.log(`[API] Session ${sessionId} deleted successfully`);
|
|
401
429
|
res.json({ success: true });
|
|
402
430
|
} catch (error) {
|
|
431
|
+
console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
|
|
403
432
|
res.status(500).json({ error: error.message });
|
|
404
433
|
}
|
|
405
434
|
});
|
|
@@ -688,6 +717,32 @@ wss.on('connection', (ws, request) => {
|
|
|
688
717
|
}
|
|
689
718
|
});
|
|
690
719
|
|
|
720
|
+
/**
|
|
721
|
+
* WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
|
|
722
|
+
*/
|
|
723
|
+
class WebSocketWriter {
|
|
724
|
+
constructor(ws) {
|
|
725
|
+
this.ws = ws;
|
|
726
|
+
this.sessionId = null;
|
|
727
|
+
this.isWebSocketWriter = true; // Marker for transport detection
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
send(data) {
|
|
731
|
+
if (this.ws.readyState === 1) { // WebSocket.OPEN
|
|
732
|
+
// Providers send raw objects, we stringify for WebSocket
|
|
733
|
+
this.ws.send(JSON.stringify(data));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
setSessionId(sessionId) {
|
|
738
|
+
this.sessionId = sessionId;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
getSessionId() {
|
|
742
|
+
return this.sessionId;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
691
746
|
// Handle chat WebSocket connections
|
|
692
747
|
function handleChatConnection(ws) {
|
|
693
748
|
console.log('[INFO] Chat WebSocket connected');
|
|
@@ -695,6 +750,9 @@ function handleChatConnection(ws) {
|
|
|
695
750
|
// Add to connected clients for project updates
|
|
696
751
|
connectedClients.add(ws);
|
|
697
752
|
|
|
753
|
+
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
|
|
754
|
+
const writer = new WebSocketWriter(ws);
|
|
755
|
+
|
|
698
756
|
ws.on('message', async (message) => {
|
|
699
757
|
try {
|
|
700
758
|
const data = JSON.parse(message);
|
|
@@ -705,13 +763,19 @@ function handleChatConnection(ws) {
|
|
|
705
763
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
706
764
|
|
|
707
765
|
// Use Claude Agents SDK
|
|
708
|
-
await queryClaudeSDK(data.command, data.options,
|
|
766
|
+
await queryClaudeSDK(data.command, data.options, writer);
|
|
709
767
|
} else if (data.type === 'cursor-command') {
|
|
710
768
|
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
|
|
711
769
|
console.log('📁 Project:', data.options?.cwd || 'Unknown');
|
|
712
770
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
713
771
|
console.log('🤖 Model:', data.options?.model || 'default');
|
|
714
|
-
await spawnCursor(data.command, data.options,
|
|
772
|
+
await spawnCursor(data.command, data.options, writer);
|
|
773
|
+
} else if (data.type === 'codex-command') {
|
|
774
|
+
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
|
|
775
|
+
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
776
|
+
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
777
|
+
console.log('🤖 Model:', data.options?.model || 'default');
|
|
778
|
+
await queryCodex(data.command, data.options, writer);
|
|
715
779
|
} else if (data.type === 'cursor-resume') {
|
|
716
780
|
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
717
781
|
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
|
@@ -719,7 +783,7 @@ function handleChatConnection(ws) {
|
|
|
719
783
|
sessionId: data.sessionId,
|
|
720
784
|
resume: true,
|
|
721
785
|
cwd: data.options?.cwd
|
|
722
|
-
},
|
|
786
|
+
}, writer);
|
|
723
787
|
} else if (data.type === 'abort-session') {
|
|
724
788
|
console.log('[DEBUG] Abort session request:', data.sessionId);
|
|
725
789
|
const provider = data.provider || 'claude';
|
|
@@ -727,26 +791,28 @@ function handleChatConnection(ws) {
|
|
|
727
791
|
|
|
728
792
|
if (provider === 'cursor') {
|
|
729
793
|
success = abortCursorSession(data.sessionId);
|
|
794
|
+
} else if (provider === 'codex') {
|
|
795
|
+
success = abortCodexSession(data.sessionId);
|
|
730
796
|
} else {
|
|
731
797
|
// Use Claude Agents SDK
|
|
732
798
|
success = await abortClaudeSDKSession(data.sessionId);
|
|
733
799
|
}
|
|
734
800
|
|
|
735
|
-
|
|
801
|
+
writer.send({
|
|
736
802
|
type: 'session-aborted',
|
|
737
803
|
sessionId: data.sessionId,
|
|
738
804
|
provider,
|
|
739
805
|
success
|
|
740
|
-
})
|
|
806
|
+
});
|
|
741
807
|
} else if (data.type === 'cursor-abort') {
|
|
742
808
|
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
|
|
743
809
|
const success = abortCursorSession(data.sessionId);
|
|
744
|
-
|
|
810
|
+
writer.send({
|
|
745
811
|
type: 'session-aborted',
|
|
746
812
|
sessionId: data.sessionId,
|
|
747
813
|
provider: 'cursor',
|
|
748
814
|
success
|
|
749
|
-
})
|
|
815
|
+
});
|
|
750
816
|
} else if (data.type === 'check-session-status') {
|
|
751
817
|
// Check if a specific session is currently processing
|
|
752
818
|
const provider = data.provider || 'claude';
|
|
@@ -755,34 +821,37 @@ function handleChatConnection(ws) {
|
|
|
755
821
|
|
|
756
822
|
if (provider === 'cursor') {
|
|
757
823
|
isActive = isCursorSessionActive(sessionId);
|
|
824
|
+
} else if (provider === 'codex') {
|
|
825
|
+
isActive = isCodexSessionActive(sessionId);
|
|
758
826
|
} else {
|
|
759
827
|
// Use Claude Agents SDK
|
|
760
828
|
isActive = isClaudeSDKSessionActive(sessionId);
|
|
761
829
|
}
|
|
762
830
|
|
|
763
|
-
|
|
831
|
+
writer.send({
|
|
764
832
|
type: 'session-status',
|
|
765
833
|
sessionId,
|
|
766
834
|
provider,
|
|
767
835
|
isProcessing: isActive
|
|
768
|
-
})
|
|
836
|
+
});
|
|
769
837
|
} else if (data.type === 'get-active-sessions') {
|
|
770
838
|
// Get all currently active sessions
|
|
771
839
|
const activeSessions = {
|
|
772
840
|
claude: getActiveClaudeSDKSessions(),
|
|
773
|
-
cursor: getActiveCursorSessions()
|
|
841
|
+
cursor: getActiveCursorSessions(),
|
|
842
|
+
codex: getActiveCodexSessions()
|
|
774
843
|
};
|
|
775
|
-
|
|
844
|
+
writer.send({
|
|
776
845
|
type: 'active-sessions',
|
|
777
846
|
sessions: activeSessions
|
|
778
|
-
})
|
|
847
|
+
});
|
|
779
848
|
}
|
|
780
849
|
} catch (error) {
|
|
781
850
|
console.error('[ERROR] Chat WebSocket error:', error.message);
|
|
782
|
-
|
|
851
|
+
writer.send({
|
|
783
852
|
type: 'error',
|
|
784
853
|
error: error.message
|
|
785
|
-
})
|
|
854
|
+
});
|
|
786
855
|
}
|
|
787
856
|
});
|
|
788
857
|
|
|
@@ -797,6 +866,8 @@ function handleChatConnection(ws) {
|
|
|
797
866
|
function handleShellConnection(ws) {
|
|
798
867
|
console.log('🐚 Shell client connected');
|
|
799
868
|
let shellProcess = null;
|
|
869
|
+
let ptySessionKey = null;
|
|
870
|
+
let outputBuffer = [];
|
|
800
871
|
|
|
801
872
|
ws.on('message', async (message) => {
|
|
802
873
|
try {
|
|
@@ -804,7 +875,6 @@ function handleShellConnection(ws) {
|
|
|
804
875
|
console.log('📨 Shell message received:', data.type);
|
|
805
876
|
|
|
806
877
|
if (data.type === 'init') {
|
|
807
|
-
// Initialize shell with project path and session info
|
|
808
878
|
const projectPath = data.projectPath || process.cwd();
|
|
809
879
|
const sessionId = data.sessionId;
|
|
810
880
|
const hasSession = data.hasSession;
|
|
@@ -812,6 +882,57 @@ function handleShellConnection(ws) {
|
|
|
812
882
|
const initialCommand = data.initialCommand;
|
|
813
883
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
814
884
|
|
|
885
|
+
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
|
886
|
+
const isLoginCommand = initialCommand && (
|
|
887
|
+
initialCommand.includes('setup-token') ||
|
|
888
|
+
initialCommand.includes('cursor-agent login') ||
|
|
889
|
+
initialCommand.includes('auth login')
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
// Include command hash in session key so different commands get separate sessions
|
|
893
|
+
const commandSuffix = isPlainShell && initialCommand
|
|
894
|
+
? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
|
|
895
|
+
: '';
|
|
896
|
+
ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
|
|
897
|
+
|
|
898
|
+
// Kill any existing login session before starting fresh
|
|
899
|
+
if (isLoginCommand) {
|
|
900
|
+
const oldSession = ptySessionsMap.get(ptySessionKey);
|
|
901
|
+
if (oldSession) {
|
|
902
|
+
console.log('🧹 Cleaning up existing login session:', ptySessionKey);
|
|
903
|
+
if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
|
|
904
|
+
if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
|
|
905
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
|
|
910
|
+
if (existingSession) {
|
|
911
|
+
console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
|
|
912
|
+
shellProcess = existingSession.pty;
|
|
913
|
+
|
|
914
|
+
clearTimeout(existingSession.timeoutId);
|
|
915
|
+
|
|
916
|
+
ws.send(JSON.stringify({
|
|
917
|
+
type: 'output',
|
|
918
|
+
data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
|
|
919
|
+
}));
|
|
920
|
+
|
|
921
|
+
if (existingSession.buffer && existingSession.buffer.length > 0) {
|
|
922
|
+
console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
|
|
923
|
+
existingSession.buffer.forEach(bufferedData => {
|
|
924
|
+
ws.send(JSON.stringify({
|
|
925
|
+
type: 'output',
|
|
926
|
+
data: bufferedData
|
|
927
|
+
}));
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
existingSession.ws = ws;
|
|
932
|
+
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
815
936
|
console.log('[INFO] Starting shell in:', projectPath);
|
|
816
937
|
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
|
|
817
938
|
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
|
|
@@ -885,10 +1006,15 @@ function handleShellConnection(ws) {
|
|
|
885
1006
|
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
886
1007
|
const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
|
887
1008
|
|
|
1009
|
+
// Use terminal dimensions from client if provided, otherwise use defaults
|
|
1010
|
+
const termCols = data.cols || 80;
|
|
1011
|
+
const termRows = data.rows || 24;
|
|
1012
|
+
console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
|
|
1013
|
+
|
|
888
1014
|
shellProcess = pty.spawn(shell, shellArgs, {
|
|
889
1015
|
name: 'xterm-256color',
|
|
890
|
-
cols:
|
|
891
|
-
rows:
|
|
1016
|
+
cols: termCols,
|
|
1017
|
+
rows: termRows,
|
|
892
1018
|
cwd: process.env.HOME || (os.platform() === 'win32' ? process.env.USERPROFILE : '/'),
|
|
893
1019
|
env: {
|
|
894
1020
|
...process.env,
|
|
@@ -902,9 +1028,28 @@ function handleShellConnection(ws) {
|
|
|
902
1028
|
|
|
903
1029
|
console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
|
|
904
1030
|
|
|
1031
|
+
ptySessionsMap.set(ptySessionKey, {
|
|
1032
|
+
pty: shellProcess,
|
|
1033
|
+
ws: ws,
|
|
1034
|
+
buffer: [],
|
|
1035
|
+
timeoutId: null,
|
|
1036
|
+
projectPath,
|
|
1037
|
+
sessionId
|
|
1038
|
+
});
|
|
1039
|
+
|
|
905
1040
|
// Handle data output
|
|
906
1041
|
shellProcess.onData((data) => {
|
|
907
|
-
|
|
1042
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1043
|
+
if (!session) return;
|
|
1044
|
+
|
|
1045
|
+
if (session.buffer.length < 5000) {
|
|
1046
|
+
session.buffer.push(data);
|
|
1047
|
+
} else {
|
|
1048
|
+
session.buffer.shift();
|
|
1049
|
+
session.buffer.push(data);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
908
1053
|
let outputData = data;
|
|
909
1054
|
|
|
910
1055
|
// Check for various URL opening patterns
|
|
@@ -928,7 +1073,7 @@ function handleShellConnection(ws) {
|
|
|
928
1073
|
console.log('[DEBUG] Detected URL for opening:', url);
|
|
929
1074
|
|
|
930
1075
|
// Send URL opening message to client
|
|
931
|
-
ws.send(JSON.stringify({
|
|
1076
|
+
session.ws.send(JSON.stringify({
|
|
932
1077
|
type: 'url_open',
|
|
933
1078
|
url: url
|
|
934
1079
|
}));
|
|
@@ -941,7 +1086,7 @@ function handleShellConnection(ws) {
|
|
|
941
1086
|
});
|
|
942
1087
|
|
|
943
1088
|
// Send regular output
|
|
944
|
-
ws.send(JSON.stringify({
|
|
1089
|
+
session.ws.send(JSON.stringify({
|
|
945
1090
|
type: 'output',
|
|
946
1091
|
data: outputData
|
|
947
1092
|
}));
|
|
@@ -951,12 +1096,17 @@ function handleShellConnection(ws) {
|
|
|
951
1096
|
// Handle process exit
|
|
952
1097
|
shellProcess.onExit((exitCode) => {
|
|
953
1098
|
console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
|
|
954
|
-
|
|
955
|
-
|
|
1099
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1100
|
+
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1101
|
+
session.ws.send(JSON.stringify({
|
|
956
1102
|
type: 'output',
|
|
957
1103
|
data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
|
|
958
1104
|
}));
|
|
959
1105
|
}
|
|
1106
|
+
if (session && session.timeoutId) {
|
|
1107
|
+
clearTimeout(session.timeoutId);
|
|
1108
|
+
}
|
|
1109
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
960
1110
|
shellProcess = null;
|
|
961
1111
|
});
|
|
962
1112
|
|
|
@@ -999,9 +1149,21 @@ function handleShellConnection(ws) {
|
|
|
999
1149
|
|
|
1000
1150
|
ws.on('close', () => {
|
|
1001
1151
|
console.log('🔌 Shell client disconnected');
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1152
|
+
|
|
1153
|
+
if (ptySessionKey) {
|
|
1154
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1155
|
+
if (session) {
|
|
1156
|
+
console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
|
|
1157
|
+
session.ws = null;
|
|
1158
|
+
|
|
1159
|
+
session.timeoutId = setTimeout(() => {
|
|
1160
|
+
console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
|
|
1161
|
+
if (session.pty && session.pty.kill) {
|
|
1162
|
+
session.pty.kill();
|
|
1163
|
+
}
|
|
1164
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
1165
|
+
}, PTY_SESSION_TIMEOUT);
|
|
1166
|
+
}
|
|
1005
1167
|
}
|
|
1006
1168
|
});
|
|
1007
1169
|
|
|
@@ -1247,8 +1409,98 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|
|
1247
1409
|
app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
|
1248
1410
|
try {
|
|
1249
1411
|
const { projectName, sessionId } = req.params;
|
|
1412
|
+
const { provider = 'claude' } = req.query;
|
|
1250
1413
|
const homeDir = os.homedir();
|
|
1251
1414
|
|
|
1415
|
+
// Allow only safe characters in sessionId
|
|
1416
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1417
|
+
if (!safeSessionId) {
|
|
1418
|
+
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
|
1422
|
+
if (provider === 'cursor') {
|
|
1423
|
+
return res.json({
|
|
1424
|
+
used: 0,
|
|
1425
|
+
total: 0,
|
|
1426
|
+
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
|
1427
|
+
unsupported: true,
|
|
1428
|
+
message: 'Token usage tracking not available for Cursor sessions'
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Handle Codex sessions
|
|
1433
|
+
if (provider === 'codex') {
|
|
1434
|
+
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
1435
|
+
|
|
1436
|
+
// Find the session file by searching for the session ID
|
|
1437
|
+
const findSessionFile = async (dir) => {
|
|
1438
|
+
try {
|
|
1439
|
+
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
1440
|
+
for (const entry of entries) {
|
|
1441
|
+
const fullPath = path.join(dir, entry.name);
|
|
1442
|
+
if (entry.isDirectory()) {
|
|
1443
|
+
const found = await findSessionFile(fullPath);
|
|
1444
|
+
if (found) return found;
|
|
1445
|
+
} else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
|
|
1446
|
+
return fullPath;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
// Skip directories we can't read
|
|
1451
|
+
}
|
|
1452
|
+
return null;
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
const sessionFilePath = await findSessionFile(codexSessionsDir);
|
|
1456
|
+
|
|
1457
|
+
if (!sessionFilePath) {
|
|
1458
|
+
return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Read and parse the Codex JSONL file
|
|
1462
|
+
let fileContent;
|
|
1463
|
+
try {
|
|
1464
|
+
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
if (error.code === 'ENOENT') {
|
|
1467
|
+
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
1468
|
+
}
|
|
1469
|
+
throw error;
|
|
1470
|
+
}
|
|
1471
|
+
const lines = fileContent.trim().split('\n');
|
|
1472
|
+
let totalTokens = 0;
|
|
1473
|
+
let contextWindow = 200000; // Default for Codex/OpenAI
|
|
1474
|
+
|
|
1475
|
+
// Find the latest token_count event with info (scan from end)
|
|
1476
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1477
|
+
try {
|
|
1478
|
+
const entry = JSON.parse(lines[i]);
|
|
1479
|
+
|
|
1480
|
+
// Codex stores token info in event_msg with type: "token_count"
|
|
1481
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
1482
|
+
const tokenInfo = entry.payload.info;
|
|
1483
|
+
if (tokenInfo.total_token_usage) {
|
|
1484
|
+
totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
|
|
1485
|
+
}
|
|
1486
|
+
if (tokenInfo.model_context_window) {
|
|
1487
|
+
contextWindow = tokenInfo.model_context_window;
|
|
1488
|
+
}
|
|
1489
|
+
break; // Stop after finding the latest token count
|
|
1490
|
+
}
|
|
1491
|
+
} catch (parseError) {
|
|
1492
|
+
// Skip lines that can't be parsed
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
return res.json({
|
|
1498
|
+
used: totalTokens,
|
|
1499
|
+
total: contextWindow
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Handle Claude sessions (default)
|
|
1252
1504
|
// Extract actual project path
|
|
1253
1505
|
let projectPath;
|
|
1254
1506
|
try {
|
|
@@ -1264,11 +1516,6 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
1264
1516
|
const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-');
|
|
1265
1517
|
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
1266
1518
|
|
|
1267
|
-
// Allow only safe characters in sessionId
|
|
1268
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1269
|
-
if (!safeSessionId) {
|
|
1270
|
-
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
1271
|
-
}
|
|
1272
1519
|
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
|
1273
1520
|
|
|
1274
1521
|
// Constrain to projectDir
|