@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/projects.js
CHANGED
|
@@ -266,8 +266,16 @@ async function extractProjectDirectory(projectName) {
|
|
|
266
266
|
if (projectDirectoryCache.has(projectName)) {
|
|
267
267
|
return projectDirectoryCache.get(projectName);
|
|
268
268
|
}
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
|
|
270
|
+
// Check project config for originalPath (manually added projects via UI or platform)
|
|
271
|
+
// This handles projects with dashes in their directory names correctly
|
|
272
|
+
const config = await loadProjectConfig();
|
|
273
|
+
if (config[projectName]?.originalPath) {
|
|
274
|
+
const originalPath = config[projectName].originalPath;
|
|
275
|
+
projectDirectoryCache.set(projectName, originalPath);
|
|
276
|
+
return originalPath;
|
|
277
|
+
}
|
|
278
|
+
|
|
271
279
|
const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
|
|
272
280
|
const cwdCounts = new Map();
|
|
273
281
|
let latestTimestamp = 0;
|
|
@@ -425,7 +433,15 @@ async function getProjects() {
|
|
|
425
433
|
console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
|
|
426
434
|
project.cursorSessions = [];
|
|
427
435
|
}
|
|
428
|
-
|
|
436
|
+
|
|
437
|
+
// Also fetch Codex sessions for this project
|
|
438
|
+
try {
|
|
439
|
+
project.codexSessions = await getCodexSessions(actualProjectDir);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
console.warn(`Could not load Codex sessions for project ${entry.name}:`, e.message);
|
|
442
|
+
project.codexSessions = [];
|
|
443
|
+
}
|
|
444
|
+
|
|
429
445
|
// Add TaskMaster detection
|
|
430
446
|
try {
|
|
431
447
|
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
@@ -478,16 +494,24 @@ async function getProjects() {
|
|
|
478
494
|
isCustomName: !!projectConfig.displayName,
|
|
479
495
|
isManuallyAdded: true,
|
|
480
496
|
sessions: [],
|
|
481
|
-
cursorSessions: []
|
|
497
|
+
cursorSessions: [],
|
|
498
|
+
codexSessions: []
|
|
482
499
|
};
|
|
483
|
-
|
|
500
|
+
|
|
484
501
|
// Try to fetch Cursor sessions for manual projects too
|
|
485
502
|
try {
|
|
486
503
|
project.cursorSessions = await getCursorSessions(actualProjectDir);
|
|
487
504
|
} catch (e) {
|
|
488
505
|
console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message);
|
|
489
506
|
}
|
|
490
|
-
|
|
507
|
+
|
|
508
|
+
// Try to fetch Codex sessions for manual projects too
|
|
509
|
+
try {
|
|
510
|
+
project.codexSessions = await getCodexSessions(actualProjectDir);
|
|
511
|
+
} catch (e) {
|
|
512
|
+
console.warn(`Could not load Codex sessions for manual project ${projectName}:`, e.message);
|
|
513
|
+
}
|
|
514
|
+
|
|
491
515
|
// Add TaskMaster detection for manual projects
|
|
492
516
|
try {
|
|
493
517
|
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
@@ -1141,6 +1165,420 @@ async function getCursorSessions(projectPath) {
|
|
|
1141
1165
|
}
|
|
1142
1166
|
|
|
1143
1167
|
|
|
1168
|
+
// Fetch Codex sessions for a given project path
|
|
1169
|
+
async function getCodexSessions(projectPath) {
|
|
1170
|
+
try {
|
|
1171
|
+
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1172
|
+
const sessions = [];
|
|
1173
|
+
|
|
1174
|
+
// Check if the directory exists
|
|
1175
|
+
try {
|
|
1176
|
+
await fs.access(codexSessionsDir);
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
// No Codex sessions directory
|
|
1179
|
+
return [];
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Recursively find all .jsonl files in the sessions directory
|
|
1183
|
+
const findJsonlFiles = async (dir) => {
|
|
1184
|
+
const files = [];
|
|
1185
|
+
try {
|
|
1186
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1187
|
+
for (const entry of entries) {
|
|
1188
|
+
const fullPath = path.join(dir, entry.name);
|
|
1189
|
+
if (entry.isDirectory()) {
|
|
1190
|
+
files.push(...await findJsonlFiles(fullPath));
|
|
1191
|
+
} else if (entry.name.endsWith('.jsonl')) {
|
|
1192
|
+
files.push(fullPath);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
// Skip directories we can't read
|
|
1197
|
+
}
|
|
1198
|
+
return files;
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const jsonlFiles = await findJsonlFiles(codexSessionsDir);
|
|
1202
|
+
|
|
1203
|
+
// Process each file to find sessions matching the project path
|
|
1204
|
+
for (const filePath of jsonlFiles) {
|
|
1205
|
+
try {
|
|
1206
|
+
const sessionData = await parseCodexSessionFile(filePath);
|
|
1207
|
+
|
|
1208
|
+
// Check if this session matches the project path
|
|
1209
|
+
if (sessionData && sessionData.cwd === projectPath) {
|
|
1210
|
+
sessions.push({
|
|
1211
|
+
id: sessionData.id,
|
|
1212
|
+
summary: sessionData.summary || 'Codex Session',
|
|
1213
|
+
messageCount: sessionData.messageCount || 0,
|
|
1214
|
+
lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
|
|
1215
|
+
cwd: sessionData.cwd,
|
|
1216
|
+
model: sessionData.model,
|
|
1217
|
+
filePath: filePath,
|
|
1218
|
+
provider: 'codex'
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Sort sessions by last activity (newest first)
|
|
1227
|
+
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1228
|
+
|
|
1229
|
+
// Return only the first 5 sessions for performance
|
|
1230
|
+
return sessions.slice(0, 5);
|
|
1231
|
+
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
console.error('Error fetching Codex sessions:', error);
|
|
1234
|
+
return [];
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Parse a Codex session JSONL file to extract metadata
|
|
1239
|
+
async function parseCodexSessionFile(filePath) {
|
|
1240
|
+
try {
|
|
1241
|
+
const fileStream = fsSync.createReadStream(filePath);
|
|
1242
|
+
const rl = readline.createInterface({
|
|
1243
|
+
input: fileStream,
|
|
1244
|
+
crlfDelay: Infinity
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
let sessionMeta = null;
|
|
1248
|
+
let lastTimestamp = null;
|
|
1249
|
+
let lastUserMessage = null;
|
|
1250
|
+
let messageCount = 0;
|
|
1251
|
+
|
|
1252
|
+
for await (const line of rl) {
|
|
1253
|
+
if (line.trim()) {
|
|
1254
|
+
try {
|
|
1255
|
+
const entry = JSON.parse(line);
|
|
1256
|
+
|
|
1257
|
+
// Track timestamp
|
|
1258
|
+
if (entry.timestamp) {
|
|
1259
|
+
lastTimestamp = entry.timestamp;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Extract session metadata
|
|
1263
|
+
if (entry.type === 'session_meta' && entry.payload) {
|
|
1264
|
+
sessionMeta = {
|
|
1265
|
+
id: entry.payload.id,
|
|
1266
|
+
cwd: entry.payload.cwd,
|
|
1267
|
+
model: entry.payload.model || entry.payload.model_provider,
|
|
1268
|
+
timestamp: entry.timestamp,
|
|
1269
|
+
git: entry.payload.git
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Count messages and extract user messages for summary
|
|
1274
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'user_message') {
|
|
1275
|
+
messageCount++;
|
|
1276
|
+
if (entry.payload.text) {
|
|
1277
|
+
lastUserMessage = entry.payload.text;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'message') {
|
|
1282
|
+
messageCount++;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
} catch (parseError) {
|
|
1286
|
+
// Skip malformed lines
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (sessionMeta) {
|
|
1292
|
+
return {
|
|
1293
|
+
...sessionMeta,
|
|
1294
|
+
timestamp: lastTimestamp || sessionMeta.timestamp,
|
|
1295
|
+
summary: lastUserMessage ?
|
|
1296
|
+
(lastUserMessage.length > 50 ? lastUserMessage.substring(0, 50) + '...' : lastUserMessage) :
|
|
1297
|
+
'Codex Session',
|
|
1298
|
+
messageCount
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return null;
|
|
1303
|
+
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
console.error('Error parsing Codex session file:', error);
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Get messages for a specific Codex session
|
|
1311
|
+
async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
1312
|
+
try {
|
|
1313
|
+
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1314
|
+
|
|
1315
|
+
// Find the session file by searching for the session ID
|
|
1316
|
+
const findSessionFile = async (dir) => {
|
|
1317
|
+
try {
|
|
1318
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1319
|
+
for (const entry of entries) {
|
|
1320
|
+
const fullPath = path.join(dir, entry.name);
|
|
1321
|
+
if (entry.isDirectory()) {
|
|
1322
|
+
const found = await findSessionFile(fullPath);
|
|
1323
|
+
if (found) return found;
|
|
1324
|
+
} else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
|
|
1325
|
+
return fullPath;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
// Skip directories we can't read
|
|
1330
|
+
}
|
|
1331
|
+
return null;
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
const sessionFilePath = await findSessionFile(codexSessionsDir);
|
|
1335
|
+
|
|
1336
|
+
if (!sessionFilePath) {
|
|
1337
|
+
console.warn(`Codex session file not found for session ${sessionId}`);
|
|
1338
|
+
return { messages: [], total: 0, hasMore: false };
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const messages = [];
|
|
1342
|
+
let tokenUsage = null;
|
|
1343
|
+
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
1344
|
+
const rl = readline.createInterface({
|
|
1345
|
+
input: fileStream,
|
|
1346
|
+
crlfDelay: Infinity
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// Helper to extract text from Codex content array
|
|
1350
|
+
const extractText = (content) => {
|
|
1351
|
+
if (!Array.isArray(content)) return content;
|
|
1352
|
+
return content
|
|
1353
|
+
.map(item => {
|
|
1354
|
+
if (item.type === 'input_text' || item.type === 'output_text') {
|
|
1355
|
+
return item.text;
|
|
1356
|
+
}
|
|
1357
|
+
if (item.type === 'text') {
|
|
1358
|
+
return item.text;
|
|
1359
|
+
}
|
|
1360
|
+
return '';
|
|
1361
|
+
})
|
|
1362
|
+
.filter(Boolean)
|
|
1363
|
+
.join('\n');
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
for await (const line of rl) {
|
|
1367
|
+
if (line.trim()) {
|
|
1368
|
+
try {
|
|
1369
|
+
const entry = JSON.parse(line);
|
|
1370
|
+
|
|
1371
|
+
// Extract token usage from token_count events (keep latest)
|
|
1372
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
1373
|
+
const info = entry.payload.info;
|
|
1374
|
+
if (info.total_token_usage) {
|
|
1375
|
+
tokenUsage = {
|
|
1376
|
+
used: info.total_token_usage.total_tokens || 0,
|
|
1377
|
+
total: info.model_context_window || 200000
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Extract messages from response_item
|
|
1383
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'message') {
|
|
1384
|
+
const content = entry.payload.content;
|
|
1385
|
+
const role = entry.payload.role || 'assistant';
|
|
1386
|
+
const textContent = extractText(content);
|
|
1387
|
+
|
|
1388
|
+
// Skip system context messages (environment_context)
|
|
1389
|
+
if (textContent?.includes('<environment_context>')) {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Only add if there's actual content
|
|
1394
|
+
if (textContent?.trim()) {
|
|
1395
|
+
messages.push({
|
|
1396
|
+
type: role === 'user' ? 'user' : 'assistant',
|
|
1397
|
+
timestamp: entry.timestamp,
|
|
1398
|
+
message: {
|
|
1399
|
+
role: role,
|
|
1400
|
+
content: textContent
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'reasoning') {
|
|
1407
|
+
const summaryText = entry.payload.summary
|
|
1408
|
+
?.map(s => s.text)
|
|
1409
|
+
.filter(Boolean)
|
|
1410
|
+
.join('\n');
|
|
1411
|
+
if (summaryText?.trim()) {
|
|
1412
|
+
messages.push({
|
|
1413
|
+
type: 'thinking',
|
|
1414
|
+
timestamp: entry.timestamp,
|
|
1415
|
+
message: {
|
|
1416
|
+
role: 'assistant',
|
|
1417
|
+
content: summaryText
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'function_call') {
|
|
1424
|
+
let toolName = entry.payload.name;
|
|
1425
|
+
let toolInput = entry.payload.arguments;
|
|
1426
|
+
|
|
1427
|
+
// Map Codex tool names to Claude equivalents
|
|
1428
|
+
if (toolName === 'shell_command') {
|
|
1429
|
+
toolName = 'Bash';
|
|
1430
|
+
try {
|
|
1431
|
+
const args = JSON.parse(entry.payload.arguments);
|
|
1432
|
+
toolInput = JSON.stringify({ command: args.command });
|
|
1433
|
+
} catch (e) {
|
|
1434
|
+
// Keep original if parsing fails
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
messages.push({
|
|
1439
|
+
type: 'tool_use',
|
|
1440
|
+
timestamp: entry.timestamp,
|
|
1441
|
+
toolName: toolName,
|
|
1442
|
+
toolInput: toolInput,
|
|
1443
|
+
toolCallId: entry.payload.call_id
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
|
|
1448
|
+
messages.push({
|
|
1449
|
+
type: 'tool_result',
|
|
1450
|
+
timestamp: entry.timestamp,
|
|
1451
|
+
toolCallId: entry.payload.call_id,
|
|
1452
|
+
output: entry.payload.output
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call') {
|
|
1457
|
+
const toolName = entry.payload.name || 'custom_tool';
|
|
1458
|
+
const input = entry.payload.input || '';
|
|
1459
|
+
|
|
1460
|
+
if (toolName === 'apply_patch') {
|
|
1461
|
+
// Parse Codex patch format and convert to Claude Edit format
|
|
1462
|
+
const fileMatch = input.match(/\*\*\* Update File: (.+)/);
|
|
1463
|
+
const filePath = fileMatch ? fileMatch[1].trim() : 'unknown';
|
|
1464
|
+
|
|
1465
|
+
// Extract old and new content from patch
|
|
1466
|
+
const lines = input.split('\n');
|
|
1467
|
+
const oldLines = [];
|
|
1468
|
+
const newLines = [];
|
|
1469
|
+
|
|
1470
|
+
for (const line of lines) {
|
|
1471
|
+
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
1472
|
+
oldLines.push(line.substring(1));
|
|
1473
|
+
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
1474
|
+
newLines.push(line.substring(1));
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
messages.push({
|
|
1479
|
+
type: 'tool_use',
|
|
1480
|
+
timestamp: entry.timestamp,
|
|
1481
|
+
toolName: 'Edit',
|
|
1482
|
+
toolInput: JSON.stringify({
|
|
1483
|
+
file_path: filePath,
|
|
1484
|
+
old_string: oldLines.join('\n'),
|
|
1485
|
+
new_string: newLines.join('\n')
|
|
1486
|
+
}),
|
|
1487
|
+
toolCallId: entry.payload.call_id
|
|
1488
|
+
});
|
|
1489
|
+
} else {
|
|
1490
|
+
messages.push({
|
|
1491
|
+
type: 'tool_use',
|
|
1492
|
+
timestamp: entry.timestamp,
|
|
1493
|
+
toolName: toolName,
|
|
1494
|
+
toolInput: input,
|
|
1495
|
+
toolCallId: entry.payload.call_id
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
|
|
1501
|
+
messages.push({
|
|
1502
|
+
type: 'tool_result',
|
|
1503
|
+
timestamp: entry.timestamp,
|
|
1504
|
+
toolCallId: entry.payload.call_id,
|
|
1505
|
+
output: entry.payload.output || ''
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
} catch (parseError) {
|
|
1510
|
+
// Skip malformed lines
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Sort by timestamp
|
|
1516
|
+
messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
|
|
1517
|
+
|
|
1518
|
+
const total = messages.length;
|
|
1519
|
+
|
|
1520
|
+
// Apply pagination if limit is specified
|
|
1521
|
+
if (limit !== null) {
|
|
1522
|
+
const startIndex = Math.max(0, total - offset - limit);
|
|
1523
|
+
const endIndex = total - offset;
|
|
1524
|
+
const paginatedMessages = messages.slice(startIndex, endIndex);
|
|
1525
|
+
const hasMore = startIndex > 0;
|
|
1526
|
+
|
|
1527
|
+
return {
|
|
1528
|
+
messages: paginatedMessages,
|
|
1529
|
+
total,
|
|
1530
|
+
hasMore,
|
|
1531
|
+
offset,
|
|
1532
|
+
limit,
|
|
1533
|
+
tokenUsage
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
return { messages, tokenUsage };
|
|
1538
|
+
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
console.error(`Error reading Codex session messages for ${sessionId}:`, error);
|
|
1541
|
+
return { messages: [], total: 0, hasMore: false };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
async function deleteCodexSession(sessionId) {
|
|
1546
|
+
try {
|
|
1547
|
+
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1548
|
+
|
|
1549
|
+
const findJsonlFiles = async (dir) => {
|
|
1550
|
+
const files = [];
|
|
1551
|
+
try {
|
|
1552
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1553
|
+
for (const entry of entries) {
|
|
1554
|
+
const fullPath = path.join(dir, entry.name);
|
|
1555
|
+
if (entry.isDirectory()) {
|
|
1556
|
+
files.push(...await findJsonlFiles(fullPath));
|
|
1557
|
+
} else if (entry.name.endsWith('.jsonl')) {
|
|
1558
|
+
files.push(fullPath);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
} catch (error) {}
|
|
1562
|
+
return files;
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
const jsonlFiles = await findJsonlFiles(codexSessionsDir);
|
|
1566
|
+
|
|
1567
|
+
for (const filePath of jsonlFiles) {
|
|
1568
|
+
const sessionData = await parseCodexSessionFile(filePath);
|
|
1569
|
+
if (sessionData && sessionData.id === sessionId) {
|
|
1570
|
+
await fs.unlink(filePath);
|
|
1571
|
+
return true;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
throw new Error(`Codex session file not found for session ${sessionId}`);
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
console.error(`Error deleting Codex session ${sessionId}:`, error);
|
|
1578
|
+
throw error;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1144
1582
|
export {
|
|
1145
1583
|
getProjects,
|
|
1146
1584
|
getSessions,
|
|
@@ -1154,5 +1592,8 @@ export {
|
|
|
1154
1592
|
loadProjectConfig,
|
|
1155
1593
|
saveProjectConfig,
|
|
1156
1594
|
extractProjectDirectory,
|
|
1157
|
-
clearProjectDirectoryCache
|
|
1595
|
+
clearProjectDirectoryCache,
|
|
1596
|
+
getCodexSessions,
|
|
1597
|
+
getCodexSessionMessages,
|
|
1598
|
+
deleteCodexSession
|
|
1158
1599
|
};
|
package/server/routes/agent.js
CHANGED
|
@@ -4,16 +4,44 @@ import path from 'path';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
|
-
import { apiKeysDb, githubTokensDb } from '../database/db.js';
|
|
7
|
+
import { userDb, apiKeysDb, githubTokensDb } from '../database/db.js';
|
|
8
8
|
import { addProjectManually } from '../projects.js';
|
|
9
9
|
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
10
10
|
import { spawnCursor } from '../cursor-cli.js';
|
|
11
|
+
import { queryCodex } from '../openai-codex.js';
|
|
11
12
|
import { Octokit } from '@octokit/rest';
|
|
12
13
|
|
|
13
14
|
const router = express.Router();
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Middleware to authenticate agent API requests.
|
|
18
|
+
*
|
|
19
|
+
* Supports two authentication modes:
|
|
20
|
+
* 1. Platform mode (VITE_IS_PLATFORM=true): For managed/hosted deployments where
|
|
21
|
+
* authentication is handled by an external proxy. Requests are trusted and
|
|
22
|
+
* the default user context is used.
|
|
23
|
+
*
|
|
24
|
+
* 2. API key mode (default): For self-hosted deployments where users authenticate
|
|
25
|
+
* via API keys created in the UI. Keys are validated against the local database.
|
|
26
|
+
*/
|
|
16
27
|
const validateExternalApiKey = (req, res, next) => {
|
|
28
|
+
// Platform mode: Authentication is handled externally (e.g., by a proxy layer).
|
|
29
|
+
// Trust the request and use the default user context.
|
|
30
|
+
if (process.env.VITE_IS_PLATFORM === 'true') {
|
|
31
|
+
try {
|
|
32
|
+
const user = userDb.getFirstUser();
|
|
33
|
+
if (!user) {
|
|
34
|
+
return res.status(500).json({ error: 'Platform mode: No user found in database' });
|
|
35
|
+
}
|
|
36
|
+
req.user = user;
|
|
37
|
+
return next();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Platform mode error:', error);
|
|
40
|
+
return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Self-hosted mode: Validate API key from header or query parameter
|
|
17
45
|
const apiKey = req.headers['x-api-key'] || req.query.apiKey;
|
|
18
46
|
|
|
19
47
|
if (!apiKey) {
|
|
@@ -819,8 +847,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
819
847
|
return res.status(400).json({ error: 'message is required' });
|
|
820
848
|
}
|
|
821
849
|
|
|
822
|
-
if (!['claude', 'cursor'].includes(provider)) {
|
|
823
|
-
return res.status(400).json({ error: 'provider must be "claude" or "
|
|
850
|
+
if (!['claude', 'cursor', 'codex'].includes(provider)) {
|
|
851
|
+
return res.status(400).json({ error: 'provider must be "claude", "cursor", or "codex"' });
|
|
824
852
|
}
|
|
825
853
|
|
|
826
854
|
// Validate GitHub branch/PR creation requirements
|
|
@@ -924,6 +952,16 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
924
952
|
model: model || undefined,
|
|
925
953
|
skipPermissions: true // Bypass permissions for Cursor
|
|
926
954
|
}, writer);
|
|
955
|
+
} else if (provider === 'codex') {
|
|
956
|
+
console.log('🤖 Starting Codex SDK session');
|
|
957
|
+
|
|
958
|
+
await queryCodex(message.trim(), {
|
|
959
|
+
projectPath: finalProjectPath,
|
|
960
|
+
cwd: finalProjectPath,
|
|
961
|
+
sessionId: null,
|
|
962
|
+
model: model || 'gpt-5.2',
|
|
963
|
+
permissionMode: 'bypassPermissions'
|
|
964
|
+
}, writer);
|
|
927
965
|
}
|
|
928
966
|
|
|
929
967
|
// Handle GitHub branch and PR creation after successful agent completion
|