@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.
Files changed (37) hide show
  1. package/dist/api-docs.html +351 -909
  2. package/dist/assets/index-CVjMty4a.js +902 -0
  3. package/dist/assets/index-eo5scY_Z.css +32 -0
  4. package/dist/index.html +5 -5
  5. package/dist/manifest.json +2 -2
  6. package/package.json +8 -2
  7. package/server/channels/core/ChannelManager.js +399 -0
  8. package/server/channels/core/PluginManager.js +59 -0
  9. package/server/channels/index.js +3 -0
  10. package/server/channels/plugins/BasePlugin.js +46 -0
  11. package/server/channels/plugins/dingtalk/DingTalkAdapter.js +156 -0
  12. package/server/channels/plugins/dingtalk/DingTalkPlugin.js +592 -0
  13. package/server/channels/plugins/dingtalk/index.js +2 -0
  14. package/server/channels/plugins/lark/LarkAdapter.js +100 -0
  15. package/server/channels/plugins/lark/LarkCards.js +43 -0
  16. package/server/channels/plugins/lark/LarkPlugin.js +260 -0
  17. package/server/channels/runtime/AgentRuntimeAdapter.js +179 -0
  18. package/server/channels/runtime/DingTalkStreamWriter.js +105 -0
  19. package/server/channels/runtime/LarkStreamWriter.js +99 -0
  20. package/server/channels/store/ChannelStore.js +236 -0
  21. package/server/database/db.js +109 -1
  22. package/server/database/init.sql +47 -1
  23. package/server/gemini-cli.js +280 -0
  24. package/server/index.js +230 -11
  25. package/server/openai-codex.js +104 -8
  26. package/server/opencode-cli.js +673 -0
  27. package/server/projects.js +645 -5
  28. package/server/routes/agent.js +40 -12
  29. package/server/routes/channels.js +221 -0
  30. package/server/routes/cli-auth.js +317 -0
  31. package/server/routes/commands.js +29 -3
  32. package/server/routes/git.js +15 -5
  33. package/server/routes/opencode.js +72 -0
  34. package/shared/modelConstants.js +62 -17
  35. package/dist/assets/index-CtRxrKDm.css +0 -32
  36. package/dist/assets/index-OENtErNy.js +0 -1249
  37. package/server/database/auth.db +0 -0
@@ -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
- const project = {
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
- deleteCodexSession
2314
+ getOpencodeSessions,
2315
+ getOpencodeSessionMessages,
2316
+ getGeminiSessionMessages,
2317
+ deleteCodexSession,
2318
+ deleteOpencodeSession
1679
2319
  };