@axhub/genie 0.1.6 → 0.1.8
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/dist/api-docs.html +351 -909
- package/dist/assets/index-CVjMty4a.js +902 -0
- package/dist/assets/index-eo5scY_Z.css +32 -0
- package/dist/index.html +5 -5
- package/dist/manifest.json +2 -2
- package/package.json +8 -2
- package/server/channels/core/ChannelManager.js +399 -0
- package/server/channels/core/PluginManager.js +59 -0
- package/server/channels/index.js +3 -0
- package/server/channels/plugins/BasePlugin.js +46 -0
- package/server/channels/plugins/dingtalk/DingTalkAdapter.js +156 -0
- package/server/channels/plugins/dingtalk/DingTalkPlugin.js +592 -0
- package/server/channels/plugins/dingtalk/index.js +2 -0
- package/server/channels/plugins/lark/LarkAdapter.js +100 -0
- package/server/channels/plugins/lark/LarkCards.js +43 -0
- package/server/channels/plugins/lark/LarkPlugin.js +260 -0
- package/server/channels/runtime/AgentRuntimeAdapter.js +179 -0
- package/server/channels/runtime/DingTalkStreamWriter.js +105 -0
- package/server/channels/runtime/LarkStreamWriter.js +99 -0
- package/server/channels/store/ChannelStore.js +236 -0
- package/server/database/db.js +109 -1
- package/server/database/init.sql +47 -1
- package/server/gemini-cli.js +280 -0
- package/server/index.js +230 -11
- package/server/openai-codex.js +104 -8
- package/server/opencode-cli.js +673 -0
- package/server/projects.js +645 -5
- package/server/routes/agent.js +40 -12
- package/server/routes/channels.js +221 -0
- package/server/routes/cli-auth.js +317 -0
- package/server/routes/commands.js +29 -3
- package/server/routes/git.js +15 -5
- package/server/routes/opencode.js +72 -0
- package/shared/modelConstants.js +62 -17
- package/dist/assets/index-CtRxrKDm.css +0 -32
- package/dist/assets/index-OENtErNy.js +0 -1249
- package/server/database/auth.db +0 -0
package/server/projects.js
CHANGED
|
@@ -436,7 +436,11 @@ async function getProjects(progressCallback = null) {
|
|
|
436
436
|
displayName: customName || autoDisplayName,
|
|
437
437
|
fullPath: fullPath,
|
|
438
438
|
isCustomName: !!customName,
|
|
439
|
-
sessions: []
|
|
439
|
+
sessions: [],
|
|
440
|
+
cursorSessions: [],
|
|
441
|
+
codexSessions: [],
|
|
442
|
+
opencodeSessions: [],
|
|
443
|
+
geminiSessions: []
|
|
440
444
|
};
|
|
441
445
|
|
|
442
446
|
// Try to get sessions for this project (just first 5 for performance)
|
|
@@ -467,6 +471,20 @@ async function getProjects(progressCallback = null) {
|
|
|
467
471
|
project.codexSessions = [];
|
|
468
472
|
}
|
|
469
473
|
|
|
474
|
+
try {
|
|
475
|
+
project.geminiSessions = await getGeminiSessions(actualProjectDir);
|
|
476
|
+
} catch (e) {
|
|
477
|
+
console.warn(`Could not load Gemini sessions for project ${entry.name}:`, e.message);
|
|
478
|
+
project.geminiSessions = [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
project.opencodeSessions = await getOpencodeSessions(actualProjectDir);
|
|
483
|
+
} catch (e) {
|
|
484
|
+
console.warn(`Could not load OpenCode sessions for project ${entry.name}:`, e.message);
|
|
485
|
+
project.opencodeSessions = [];
|
|
486
|
+
}
|
|
487
|
+
|
|
470
488
|
// Add TaskMaster detection
|
|
471
489
|
try {
|
|
472
490
|
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
@@ -526,7 +544,7 @@ async function getProjects(progressCallback = null) {
|
|
|
526
544
|
}
|
|
527
545
|
}
|
|
528
546
|
|
|
529
|
-
|
|
547
|
+
const project = {
|
|
530
548
|
name: projectName,
|
|
531
549
|
path: actualProjectDir,
|
|
532
550
|
displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
|
|
@@ -535,7 +553,9 @@ async function getProjects(progressCallback = null) {
|
|
|
535
553
|
isManuallyAdded: true,
|
|
536
554
|
sessions: [],
|
|
537
555
|
cursorSessions: [],
|
|
538
|
-
codexSessions: []
|
|
556
|
+
codexSessions: [],
|
|
557
|
+
opencodeSessions: [],
|
|
558
|
+
geminiSessions: []
|
|
539
559
|
};
|
|
540
560
|
|
|
541
561
|
// Try to fetch Cursor sessions for manual projects too
|
|
@@ -552,6 +572,18 @@ async function getProjects(progressCallback = null) {
|
|
|
552
572
|
console.warn(`Could not load Codex sessions for manual project ${projectName}:`, e.message);
|
|
553
573
|
}
|
|
554
574
|
|
|
575
|
+
try {
|
|
576
|
+
project.geminiSessions = await getGeminiSessions(actualProjectDir);
|
|
577
|
+
} catch (e) {
|
|
578
|
+
console.warn(`Could not load Gemini sessions for manual project ${projectName}:`, e.message);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
project.opencodeSessions = await getOpencodeSessions(actualProjectDir);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
console.warn(`Could not load OpenCode sessions for manual project ${projectName}:`, e.message);
|
|
585
|
+
}
|
|
586
|
+
|
|
555
587
|
// Add TaskMaster detection for manual projects
|
|
556
588
|
try {
|
|
557
589
|
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
@@ -1129,7 +1161,10 @@ async function addProjectManually(projectPath, displayName = null) {
|
|
|
1129
1161
|
displayName: displayName || await generateDisplayName(projectName, absolutePath),
|
|
1130
1162
|
isManuallyAdded: true,
|
|
1131
1163
|
sessions: [],
|
|
1132
|
-
cursorSessions: []
|
|
1164
|
+
cursorSessions: [],
|
|
1165
|
+
codexSessions: [],
|
|
1166
|
+
opencodeSessions: [],
|
|
1167
|
+
geminiSessions: []
|
|
1133
1168
|
};
|
|
1134
1169
|
}
|
|
1135
1170
|
|
|
@@ -1321,6 +1356,607 @@ async function getCodexSessions(projectPath, options = {}) {
|
|
|
1321
1356
|
}
|
|
1322
1357
|
}
|
|
1323
1358
|
|
|
1359
|
+
const OPENCODE_SESSION_DIR_CANDIDATES = [
|
|
1360
|
+
path.join(os.homedir(), '.opencode', 'sessions'),
|
|
1361
|
+
path.join(os.homedir(), '.config', 'opencode', 'sessions')
|
|
1362
|
+
];
|
|
1363
|
+
|
|
1364
|
+
async function findOpencodeSessionFiles() {
|
|
1365
|
+
const files = [];
|
|
1366
|
+
|
|
1367
|
+
const walk = async (dirPath) => {
|
|
1368
|
+
try {
|
|
1369
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
1370
|
+
for (const entry of entries) {
|
|
1371
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
1372
|
+
if (entry.isDirectory()) {
|
|
1373
|
+
await walk(fullPath);
|
|
1374
|
+
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
1375
|
+
files.push(fullPath);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
} catch (_) {}
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
for (const baseDir of OPENCODE_SESSION_DIR_CANDIDATES) {
|
|
1382
|
+
try {
|
|
1383
|
+
await fs.access(baseDir);
|
|
1384
|
+
await walk(baseDir);
|
|
1385
|
+
} catch (_) {}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return Array.from(new Set(files));
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function normalizeOpencodePath(pathValue) {
|
|
1392
|
+
if (typeof pathValue !== 'string') {
|
|
1393
|
+
return '';
|
|
1394
|
+
}
|
|
1395
|
+
return pathValue.startsWith('\\\\?\\') ? pathValue.slice(4) : pathValue;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function extractOpencodeText(value) {
|
|
1399
|
+
if (typeof value === 'string') {
|
|
1400
|
+
return value;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (Array.isArray(value)) {
|
|
1404
|
+
return value
|
|
1405
|
+
.map((part) => {
|
|
1406
|
+
if (typeof part === 'string') {
|
|
1407
|
+
return part;
|
|
1408
|
+
}
|
|
1409
|
+
if (part && typeof part === 'object') {
|
|
1410
|
+
if (typeof part.text === 'string') {
|
|
1411
|
+
return part.text;
|
|
1412
|
+
}
|
|
1413
|
+
if (typeof part.content === 'string') {
|
|
1414
|
+
return part.content;
|
|
1415
|
+
}
|
|
1416
|
+
if (typeof part.value === 'string') {
|
|
1417
|
+
return part.value;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return '';
|
|
1421
|
+
})
|
|
1422
|
+
.filter(Boolean)
|
|
1423
|
+
.join('\n');
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (value && typeof value === 'object') {
|
|
1427
|
+
if (typeof value.text === 'string') {
|
|
1428
|
+
return value.text;
|
|
1429
|
+
}
|
|
1430
|
+
if (typeof value.content === 'string') {
|
|
1431
|
+
return value.content;
|
|
1432
|
+
}
|
|
1433
|
+
if (typeof value.value === 'string') {
|
|
1434
|
+
return value.value;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
return '';
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function extractOpencodeRole(entry) {
|
|
1442
|
+
return (
|
|
1443
|
+
entry?.role ||
|
|
1444
|
+
entry?.message?.role ||
|
|
1445
|
+
entry?.payload?.role ||
|
|
1446
|
+
entry?.payload?.message?.role ||
|
|
1447
|
+
null
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function extractOpencodeContent(entry) {
|
|
1452
|
+
return (
|
|
1453
|
+
entry?.content ??
|
|
1454
|
+
entry?.message?.content ??
|
|
1455
|
+
entry?.payload?.content ??
|
|
1456
|
+
entry?.payload?.message?.content ??
|
|
1457
|
+
entry?.payload?.text ??
|
|
1458
|
+
''
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function inferOpencodeMessageType(entry, role) {
|
|
1463
|
+
const explicitType = entry?.type || entry?.payload?.type;
|
|
1464
|
+
const normalizedType = typeof explicitType === 'string' ? explicitType.toLowerCase() : '';
|
|
1465
|
+
const normalizedRole = typeof role === 'string' ? role.toLowerCase() : '';
|
|
1466
|
+
|
|
1467
|
+
if (normalizedType.includes('thinking') || normalizedType === 'reasoning') {
|
|
1468
|
+
return 'thinking';
|
|
1469
|
+
}
|
|
1470
|
+
if (normalizedType.includes('tool_call') || normalizedType === 'function_call') {
|
|
1471
|
+
return 'tool_use';
|
|
1472
|
+
}
|
|
1473
|
+
if (normalizedType.includes('tool_result') || normalizedType === 'function_call_output') {
|
|
1474
|
+
return 'tool_result';
|
|
1475
|
+
}
|
|
1476
|
+
if (normalizedRole === 'user') {
|
|
1477
|
+
return 'user';
|
|
1478
|
+
}
|
|
1479
|
+
if (normalizedRole === 'assistant') {
|
|
1480
|
+
return 'assistant';
|
|
1481
|
+
}
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async function parseOpencodeSessionFile(filePath) {
|
|
1486
|
+
const session = {
|
|
1487
|
+
id: null,
|
|
1488
|
+
summary: 'OpenCode Session',
|
|
1489
|
+
messageCount: 0,
|
|
1490
|
+
lastActivity: new Date().toISOString(),
|
|
1491
|
+
cwd: '',
|
|
1492
|
+
filePath,
|
|
1493
|
+
provider: 'opencode'
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
let lastUserMessage = null;
|
|
1497
|
+
let latestTimestamp = null;
|
|
1498
|
+
|
|
1499
|
+
try {
|
|
1500
|
+
const fileStream = fsSync.createReadStream(filePath);
|
|
1501
|
+
const rl = readline.createInterface({
|
|
1502
|
+
input: fileStream,
|
|
1503
|
+
crlfDelay: Infinity
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
for await (const line of rl) {
|
|
1507
|
+
if (!line.trim()) {
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
let entry;
|
|
1512
|
+
try {
|
|
1513
|
+
entry = JSON.parse(line);
|
|
1514
|
+
} catch (_) {
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (!session.id) {
|
|
1519
|
+
session.id =
|
|
1520
|
+
entry?.sessionId ||
|
|
1521
|
+
entry?.session_id ||
|
|
1522
|
+
entry?.id ||
|
|
1523
|
+
entry?.payload?.sessionId ||
|
|
1524
|
+
entry?.payload?.session_id ||
|
|
1525
|
+
entry?.payload?.id ||
|
|
1526
|
+
null;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (!session.cwd) {
|
|
1530
|
+
session.cwd =
|
|
1531
|
+
entry?.cwd ||
|
|
1532
|
+
entry?.payload?.cwd ||
|
|
1533
|
+
entry?.metadata?.cwd ||
|
|
1534
|
+
entry?.session?.cwd ||
|
|
1535
|
+
'';
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (typeof entry?.summary === 'string' && entry.summary.trim()) {
|
|
1539
|
+
session.summary = entry.summary.trim();
|
|
1540
|
+
} else if (typeof entry?.payload?.summary === 'string' && entry.payload.summary.trim()) {
|
|
1541
|
+
session.summary = entry.payload.summary.trim();
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const timestampCandidate =
|
|
1545
|
+
entry?.timestamp ||
|
|
1546
|
+
entry?.createdAt ||
|
|
1547
|
+
entry?.updatedAt ||
|
|
1548
|
+
entry?.payload?.timestamp ||
|
|
1549
|
+
entry?.payload?.createdAt ||
|
|
1550
|
+
null;
|
|
1551
|
+
|
|
1552
|
+
if (timestampCandidate) {
|
|
1553
|
+
latestTimestamp = timestampCandidate;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
const role = extractOpencodeRole(entry);
|
|
1557
|
+
const messageType = inferOpencodeMessageType(entry, role);
|
|
1558
|
+
const contentText = extractOpencodeText(extractOpencodeContent(entry));
|
|
1559
|
+
|
|
1560
|
+
if (messageType === 'user' || messageType === 'assistant') {
|
|
1561
|
+
session.messageCount += 1;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (messageType === 'user' && contentText.trim()) {
|
|
1565
|
+
lastUserMessage = contentText;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (!session.id) {
|
|
1570
|
+
session.id = path.basename(filePath, '.jsonl');
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (session.summary === 'OpenCode Session' && lastUserMessage) {
|
|
1574
|
+
session.summary = lastUserMessage.length > 50
|
|
1575
|
+
? `${lastUserMessage.substring(0, 50)}...`
|
|
1576
|
+
: lastUserMessage;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
if (latestTimestamp) {
|
|
1580
|
+
session.lastActivity = new Date(latestTimestamp).toISOString();
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
return session;
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
console.warn(`Could not parse OpenCode session file ${filePath}:`, error.message);
|
|
1586
|
+
return null;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
async function getOpencodeSessions(projectPath, options = {}) {
|
|
1591
|
+
const { limit = 5 } = options;
|
|
1592
|
+
|
|
1593
|
+
try {
|
|
1594
|
+
const sessionFiles = await findOpencodeSessionFiles();
|
|
1595
|
+
if (sessionFiles.length === 0) {
|
|
1596
|
+
return [];
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const sessions = [];
|
|
1600
|
+
const cleanProjectPath = normalizeOpencodePath(projectPath);
|
|
1601
|
+
|
|
1602
|
+
for (const filePath of sessionFiles) {
|
|
1603
|
+
const sessionData = await parseOpencodeSessionFile(filePath);
|
|
1604
|
+
if (!sessionData) {
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const sessionCwd = normalizeOpencodePath(sessionData.cwd);
|
|
1609
|
+
if (!sessionCwd) {
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
if (
|
|
1614
|
+
sessionCwd === cleanProjectPath ||
|
|
1615
|
+
path.relative(sessionCwd, cleanProjectPath) === ''
|
|
1616
|
+
) {
|
|
1617
|
+
sessions.push(sessionData);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1622
|
+
return limit > 0 ? sessions.slice(0, limit) : sessions;
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
console.error('Error fetching OpenCode sessions:', error);
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
async function findOpencodeSessionFileById(sessionId) {
|
|
1630
|
+
const sessionFiles = await findOpencodeSessionFiles();
|
|
1631
|
+
for (const filePath of sessionFiles) {
|
|
1632
|
+
if (path.basename(filePath, '.jsonl') === sessionId) {
|
|
1633
|
+
return filePath;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const sessionData = await parseOpencodeSessionFile(filePath);
|
|
1637
|
+
if (sessionData?.id === sessionId) {
|
|
1638
|
+
return filePath;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
async function getOpencodeSessionMessages(sessionId, limit = null, offset = 0) {
|
|
1646
|
+
try {
|
|
1647
|
+
const sessionFilePath = await findOpencodeSessionFileById(sessionId);
|
|
1648
|
+
if (!sessionFilePath) {
|
|
1649
|
+
return { messages: [], total: 0, hasMore: false };
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const messages = [];
|
|
1653
|
+
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
1654
|
+
const rl = readline.createInterface({
|
|
1655
|
+
input: fileStream,
|
|
1656
|
+
crlfDelay: Infinity
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
for await (const line of rl) {
|
|
1660
|
+
if (!line.trim()) {
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
let entry;
|
|
1665
|
+
try {
|
|
1666
|
+
entry = JSON.parse(line);
|
|
1667
|
+
} catch (_) {
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
const role = extractOpencodeRole(entry);
|
|
1672
|
+
const messageType = inferOpencodeMessageType(entry, role);
|
|
1673
|
+
const timestamp =
|
|
1674
|
+
entry?.timestamp ||
|
|
1675
|
+
entry?.createdAt ||
|
|
1676
|
+
entry?.updatedAt ||
|
|
1677
|
+
entry?.payload?.timestamp ||
|
|
1678
|
+
null;
|
|
1679
|
+
|
|
1680
|
+
if (messageType === 'tool_use') {
|
|
1681
|
+
messages.push({
|
|
1682
|
+
type: 'tool_use',
|
|
1683
|
+
timestamp,
|
|
1684
|
+
toolName: entry?.name || entry?.payload?.name || 'tool',
|
|
1685
|
+
toolInput: entry?.arguments || entry?.payload?.arguments || entry?.input || entry?.payload?.input || '',
|
|
1686
|
+
toolCallId: entry?.call_id || entry?.payload?.call_id || null
|
|
1687
|
+
});
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
if (messageType === 'tool_result') {
|
|
1692
|
+
messages.push({
|
|
1693
|
+
type: 'tool_result',
|
|
1694
|
+
timestamp,
|
|
1695
|
+
toolCallId: entry?.call_id || entry?.payload?.call_id || null,
|
|
1696
|
+
output: entry?.output || entry?.payload?.output || ''
|
|
1697
|
+
});
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
if (messageType === 'thinking') {
|
|
1702
|
+
const thinkingText = extractOpencodeText(
|
|
1703
|
+
entry?.summary || entry?.payload?.summary || entry?.content || entry?.payload?.content
|
|
1704
|
+
);
|
|
1705
|
+
if (thinkingText.trim()) {
|
|
1706
|
+
messages.push({
|
|
1707
|
+
type: 'thinking',
|
|
1708
|
+
timestamp,
|
|
1709
|
+
message: {
|
|
1710
|
+
role: 'assistant',
|
|
1711
|
+
content: thinkingText
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
if (messageType !== 'user' && messageType !== 'assistant') {
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const contentText = extractOpencodeText(extractOpencodeContent(entry));
|
|
1723
|
+
if (!contentText.trim()) {
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
messages.push({
|
|
1728
|
+
type: messageType,
|
|
1729
|
+
timestamp,
|
|
1730
|
+
message: {
|
|
1731
|
+
role: messageType === 'user' ? 'user' : 'assistant',
|
|
1732
|
+
content: contentText
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
|
|
1738
|
+
|
|
1739
|
+
const total = messages.length;
|
|
1740
|
+
if (limit !== null) {
|
|
1741
|
+
const startIndex = Math.max(0, total - offset - limit);
|
|
1742
|
+
const endIndex = total - offset;
|
|
1743
|
+
const paginatedMessages = messages.slice(startIndex, endIndex);
|
|
1744
|
+
return {
|
|
1745
|
+
messages: paginatedMessages,
|
|
1746
|
+
total,
|
|
1747
|
+
hasMore: startIndex > 0,
|
|
1748
|
+
offset,
|
|
1749
|
+
limit
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
return { messages, total, hasMore: false };
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
console.error(`Error reading OpenCode session messages for ${sessionId}:`, error);
|
|
1756
|
+
return { messages: [], total: 0, hasMore: false };
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
async function deleteOpencodeSession(sessionId) {
|
|
1761
|
+
try {
|
|
1762
|
+
const sessionFilePath = await findOpencodeSessionFileById(sessionId);
|
|
1763
|
+
if (!sessionFilePath) {
|
|
1764
|
+
throw new Error(`OpenCode session file not found for session ${sessionId}`);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
await fs.unlink(sessionFilePath);
|
|
1768
|
+
return true;
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
console.error(`Error deleting OpenCode session ${sessionId}:`, error);
|
|
1771
|
+
throw error;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
async function getGeminiSessions(projectPath, options = {}) {
|
|
1776
|
+
const { limit = 5 } = options;
|
|
1777
|
+
try {
|
|
1778
|
+
const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
|
|
1779
|
+
try {
|
|
1780
|
+
await fs.access(geminiTmpDir);
|
|
1781
|
+
} catch {
|
|
1782
|
+
return [];
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const projectHash = crypto.createHash('sha256').update(projectPath).digest('hex');
|
|
1786
|
+
const chatsDir = path.join(geminiTmpDir, projectHash, 'chats');
|
|
1787
|
+
|
|
1788
|
+
try {
|
|
1789
|
+
await fs.access(chatsDir);
|
|
1790
|
+
} catch {
|
|
1791
|
+
return [];
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const entries = await fs.readdir(chatsDir, { withFileTypes: true });
|
|
1795
|
+
const sessionFiles = entries
|
|
1796
|
+
.filter(e => e.isFile() && e.name.endsWith('.json'))
|
|
1797
|
+
.map(e => path.join(chatsDir, e.name));
|
|
1798
|
+
|
|
1799
|
+
const sessions = [];
|
|
1800
|
+
for (const filePath of sessionFiles) {
|
|
1801
|
+
try {
|
|
1802
|
+
const sessionData = await parseGeminiSessionFile(filePath);
|
|
1803
|
+
if (!sessionData?.id) continue;
|
|
1804
|
+
sessions.push(sessionData);
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
console.warn(`Could not parse Gemini session file ${filePath}:`, error.message);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1811
|
+
return limit > 0 ? sessions.slice(0, limit) : sessions;
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
console.error('Error fetching Gemini sessions:', error);
|
|
1814
|
+
return [];
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
async function parseGeminiSessionFile(filePath) {
|
|
1819
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
1820
|
+
const data = JSON.parse(raw);
|
|
1821
|
+
const messages = Array.isArray(data?.messages) ? data.messages : [];
|
|
1822
|
+
|
|
1823
|
+
let summary = 'Gemini Session';
|
|
1824
|
+
let messageCount = 0;
|
|
1825
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1826
|
+
const msg = messages[i];
|
|
1827
|
+
if (msg?.type === 'user') {
|
|
1828
|
+
const userText = Array.isArray(msg?.content)
|
|
1829
|
+
? msg.content.map(p => p?.text).filter(Boolean).join('\n')
|
|
1830
|
+
: typeof msg?.content === 'string'
|
|
1831
|
+
? msg.content
|
|
1832
|
+
: '';
|
|
1833
|
+
if (userText.trim()) {
|
|
1834
|
+
summary = userText.length > 50 ? `${userText.substring(0, 50)}...` : userText;
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
messageCount = messages.filter(m => m?.type === 'user' || m?.type === 'gemini').length;
|
|
1841
|
+
|
|
1842
|
+
const lastActivity = data?.lastUpdated || data?.startTime || new Date().toISOString();
|
|
1843
|
+
return {
|
|
1844
|
+
id: data?.sessionId,
|
|
1845
|
+
summary,
|
|
1846
|
+
messageCount,
|
|
1847
|
+
lastActivity,
|
|
1848
|
+
createdAt: data?.startTime || lastActivity,
|
|
1849
|
+
model: messages.filter(m => m?.model).slice(-1)[0]?.model || null,
|
|
1850
|
+
provider: 'gemini',
|
|
1851
|
+
filePath
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
async function getGeminiSessionMessages(sessionId, limit = null, offset = 0) {
|
|
1856
|
+
try {
|
|
1857
|
+
const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
|
|
1858
|
+
const findSessionFile = async (dir) => {
|
|
1859
|
+
try {
|
|
1860
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1861
|
+
for (const entry of entries) {
|
|
1862
|
+
const fullPath = path.join(dir, entry.name);
|
|
1863
|
+
if (entry.isDirectory()) {
|
|
1864
|
+
const found = await findSessionFile(fullPath);
|
|
1865
|
+
if (found) return found;
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
1869
|
+
try {
|
|
1870
|
+
const raw = await fs.readFile(fullPath, 'utf8');
|
|
1871
|
+
const parsed = JSON.parse(raw);
|
|
1872
|
+
if (parsed?.sessionId === sessionId) {
|
|
1873
|
+
return fullPath;
|
|
1874
|
+
}
|
|
1875
|
+
} catch {}
|
|
1876
|
+
}
|
|
1877
|
+
} catch {}
|
|
1878
|
+
return null;
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
const sessionFilePath = await findSessionFile(geminiTmpDir);
|
|
1882
|
+
if (!sessionFilePath) {
|
|
1883
|
+
return { messages: [], total: 0, hasMore: false };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const raw = await fs.readFile(sessionFilePath, 'utf8');
|
|
1887
|
+
const parsed = JSON.parse(raw);
|
|
1888
|
+
const sourceMessages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1889
|
+
const messages = [];
|
|
1890
|
+
|
|
1891
|
+
for (const msg of sourceMessages) {
|
|
1892
|
+
if (msg?.type === 'user') {
|
|
1893
|
+
const content = Array.isArray(msg?.content)
|
|
1894
|
+
? msg.content.map(p => p?.text).filter(Boolean).join('\n')
|
|
1895
|
+
: typeof msg?.content === 'string'
|
|
1896
|
+
? msg.content
|
|
1897
|
+
: '';
|
|
1898
|
+
if (content.trim()) {
|
|
1899
|
+
messages.push({
|
|
1900
|
+
type: 'user',
|
|
1901
|
+
timestamp: msg.timestamp,
|
|
1902
|
+
message: {
|
|
1903
|
+
role: 'user',
|
|
1904
|
+
content
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
} else if (msg?.type === 'gemini' && typeof msg?.content === 'string' && msg.content.trim()) {
|
|
1909
|
+
messages.push({
|
|
1910
|
+
type: 'assistant',
|
|
1911
|
+
timestamp: msg.timestamp,
|
|
1912
|
+
message: {
|
|
1913
|
+
role: 'assistant',
|
|
1914
|
+
content: msg.content
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
if (Array.isArray(msg.thoughts) && msg.thoughts.length > 0) {
|
|
1919
|
+
const thoughtText = msg.thoughts
|
|
1920
|
+
.map(t => [t?.subject, t?.description].filter(Boolean).join(': '))
|
|
1921
|
+
.filter(Boolean)
|
|
1922
|
+
.join('\n');
|
|
1923
|
+
if (thoughtText) {
|
|
1924
|
+
messages.push({
|
|
1925
|
+
type: 'thinking',
|
|
1926
|
+
timestamp: msg.timestamp,
|
|
1927
|
+
message: {
|
|
1928
|
+
role: 'assistant',
|
|
1929
|
+
content: thoughtText
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
|
|
1938
|
+
|
|
1939
|
+
const total = messages.length;
|
|
1940
|
+
if (limit !== null) {
|
|
1941
|
+
const startIndex = Math.max(0, total - offset - limit);
|
|
1942
|
+
const endIndex = total - offset;
|
|
1943
|
+
const paginatedMessages = messages.slice(startIndex, endIndex);
|
|
1944
|
+
return {
|
|
1945
|
+
messages: paginatedMessages,
|
|
1946
|
+
total,
|
|
1947
|
+
hasMore: startIndex > 0,
|
|
1948
|
+
offset,
|
|
1949
|
+
limit
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
return { messages, total, hasMore: false };
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
console.error(`Error reading Gemini session messages for ${sessionId}:`, error);
|
|
1956
|
+
return { messages: [], total: 0, hasMore: false };
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1324
1960
|
// Parse a Codex session JSONL file to extract metadata
|
|
1325
1961
|
async function parseCodexSessionFile(filePath) {
|
|
1326
1962
|
try {
|
|
@@ -1675,5 +2311,9 @@ export {
|
|
|
1675
2311
|
clearProjectDirectoryCache,
|
|
1676
2312
|
getCodexSessions,
|
|
1677
2313
|
getCodexSessionMessages,
|
|
1678
|
-
|
|
2314
|
+
getOpencodeSessions,
|
|
2315
|
+
getOpencodeSessionMessages,
|
|
2316
|
+
getGeminiSessionMessages,
|
|
2317
|
+
deleteCodexSession,
|
|
2318
|
+
deleteOpencodeSession
|
|
1679
2319
|
};
|