@axhub/genie 0.2.8 → 0.2.9

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 (83) hide show
  1. package/LICENSE +21 -675
  2. package/dist/api-docs.html +2 -2
  3. package/dist/assets/App-GBcTeeUS.js +460 -0
  4. package/dist/assets/ReviewApp-C9K--AQE.js +1 -0
  5. package/dist/assets/{_basePickBy-CqJbRZ9y.js → _basePickBy-DR_8uFCo.js} +1 -1
  6. package/dist/assets/{_baseUniq-BS8YH8jO.js → _baseUniq-D0njlQ_7.js} +1 -1
  7. package/dist/assets/{arc-BBmKEN-S.js → arc-CKlr_Rec.js} +1 -1
  8. package/dist/assets/{architectureDiagram-2XIMDMQ5-N5lcb82R.js → architectureDiagram-2XIMDMQ5-BmO_uLUH.js} +1 -1
  9. package/dist/assets/{blockDiagram-WCTKOSBZ-DTMwHuLn.js → blockDiagram-WCTKOSBZ-DhAeO-56.js} +1 -1
  10. package/dist/assets/{c4Diagram-IC4MRINW-BTKlkXI9.js → c4Diagram-IC4MRINW-C67kFoXx.js} +1 -1
  11. package/dist/assets/channel-V3MBjKys.js +1 -0
  12. package/dist/assets/{chunk-4BX2VUAB-DUdoTxAc.js → chunk-4BX2VUAB-mLLagvJi.js} +1 -1
  13. package/dist/assets/{chunk-55IACEB6-Bm_92xe4.js → chunk-55IACEB6-Lx-hOjlM.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-CGW0g62g.js → chunk-FMBD7UC4-Bt-XmVUV.js} +1 -1
  15. package/dist/assets/{chunk-JSJVCQXG-DYkTH3w1.js → chunk-JSJVCQXG-Cya6gaDV.js} +1 -1
  16. package/dist/assets/{chunk-KX2RTZJC-C9oTlISU.js → chunk-KX2RTZJC-Bd7Ig6tF.js} +1 -1
  17. package/dist/assets/{chunk-NQ4KR5QH-CM50ygWP.js → chunk-NQ4KR5QH-5UAE0Vg-.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-7dzpYeNJ.js → chunk-QZHKN3VN-BAxZ8m7w.js} +1 -1
  19. package/dist/assets/{chunk-WL4C6EOR-Cm9nQrsr.js → chunk-WL4C6EOR-DjDPvUUP.js} +1 -1
  20. package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +1 -0
  21. package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +1 -0
  22. package/dist/assets/clone-BbMGfZwt.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-Ccp_p0JZ.js → cose-bilkent-S5V4N54A-D-60XrkJ.js} +1 -1
  24. package/dist/assets/{dagre-KLK3FWXG-fBwTLUp9.js → dagre-KLK3FWXG-bqu3ZS4K.js} +1 -1
  25. package/dist/assets/{diagram-E7M64L7V-CeNVmFUp.js → diagram-E7M64L7V-BueeqoYm.js} +1 -1
  26. package/dist/assets/{diagram-IFDJBPK2-CtavyLGa.js → diagram-IFDJBPK2-D4fDv2E7.js} +1 -1
  27. package/dist/assets/{diagram-P4PSJMXO-CpQTjQwc.js → diagram-P4PSJMXO-WqipY3fN.js} +1 -1
  28. package/dist/assets/{erDiagram-INFDFZHY-B8R5vwhd.js → erDiagram-INFDFZHY-D0oVnO-x.js} +1 -1
  29. package/dist/assets/{flowDiagram-PKNHOUZH-BvkVVwIQ.js → flowDiagram-PKNHOUZH-DzbGyxrr.js} +1 -1
  30. package/dist/assets/{ganttDiagram-A5KZAMGK-DOu3hSNa.js → ganttDiagram-A5KZAMGK-BwhbbgCP.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-C7zT67YE.js → gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js} +1 -1
  32. package/dist/assets/{graph-D11wiwHo.js → graph-DzKos-N0.js} +1 -1
  33. package/dist/assets/{highlighted-body-TPN3WLV5-Babpthg-.js → highlighted-body-TPN3WLV5-CKDMgz3X.js} +1 -1
  34. package/dist/assets/{index-DFxzgWoO.js → index-DiQlHzGj.js} +2 -2
  35. package/dist/assets/index-Drat2nB9.css +1 -0
  36. package/dist/assets/{infoDiagram-LFFYTUFH-BmA7IpQG.js → infoDiagram-LFFYTUFH-BFicZbTf.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-PHBUUO56-BEquZd3E.js → ishikawaDiagram-PHBUUO56-CtihxDxl.js} +1 -1
  38. package/dist/assets/{journeyDiagram-4ABVD52K-BfemGz7f.js → journeyDiagram-4ABVD52K-Du00J8_d.js} +1 -1
  39. package/dist/assets/{kanban-definition-K7BYSVSG-CWja3mln.js → kanban-definition-K7BYSVSG-BJi9S0iQ.js} +1 -1
  40. package/dist/assets/{layout-BLUNf-PJ.js → layout-B80Sityu.js} +1 -1
  41. package/dist/assets/{linear-DukIV_Xv.js → linear-sRQLOf5H.js} +1 -1
  42. package/dist/assets/{mermaid-O7DHMXV3-SgtM28qI.js → mermaid-O7DHMXV3-CBuVs4eJ.js} +6 -6
  43. package/dist/assets/{mindmap-definition-YRQLILUH-4UjqXITU.js → mindmap-definition-YRQLILUH-C5IL_xi-.js} +1 -1
  44. package/dist/assets/{pieDiagram-SKSYHLDU-8AxqJd0M.js → pieDiagram-SKSYHLDU-CeTwlJ8z.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-337W2JSQ-D60m8V8r.js → quadrantDiagram-337W2JSQ-COfUcLWt.js} +1 -1
  46. package/dist/assets/{requirementDiagram-Z7DCOOCP-zqh9jBVf.js → requirementDiagram-Z7DCOOCP-DSb-CJ5B.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-WA2Y5GQK-CDZILTLI.js → sankeyDiagram-WA2Y5GQK-8jtuVb45.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-2WXFIKYE-7BReFd0L.js → sequenceDiagram-2WXFIKYE-C2VpkMwA.js} +1 -1
  49. package/dist/assets/{stateDiagram-RAJIS63D-HPTVdIG4.js → stateDiagram-RAJIS63D-fmwMqxxc.js} +1 -1
  50. package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +1 -0
  51. package/dist/assets/{timeline-definition-YZTLITO2-CTVllFgr.js → timeline-definition-YZTLITO2-Dx1hP5lg.js} +1 -1
  52. package/dist/assets/{treemap-KZPCXAKY-BtyxboJZ.js → treemap-KZPCXAKY-CkLOdYCZ.js} +1 -1
  53. package/dist/assets/{vendor-codemirror-Dz7_EqNA.js → vendor-codemirror-BxPY6emf.js} +1 -1
  54. package/dist/assets/{vendor-react-Cpt6D04s.js → vendor-react-xmA_f8ig.js} +1 -1
  55. package/dist/assets/{vennDiagram-LZ73GAT5-D96ZI6Mg.js → vennDiagram-LZ73GAT5-D6KWcnln.js} +1 -1
  56. package/dist/assets/{xychartDiagram-JWTSCODW-eRk-39YO.js → xychartDiagram-JWTSCODW-6fh6qmzN.js} +1 -1
  57. package/dist/index.html +5 -5
  58. package/package.json +3 -3
  59. package/server/acp-runtime/client.js +9 -2
  60. package/server/acp-runtime/session-store.js +4 -4
  61. package/server/cli.js +23 -2
  62. package/server/external-agent/service.js +24 -6
  63. package/server/external-agent/ws.js +63 -3
  64. package/server/index.js +34 -5
  65. package/server/projects.js +536 -161
  66. package/server/routes/session-core.js +149 -86
  67. package/server/session-core/eventStore.js +45 -18
  68. package/server/session-core/providerAdapters.js +50 -13
  69. package/server/session-core/runtimeState.js +8 -0
  70. package/shared/conversationEvents.js +78 -14
  71. package/dist/assets/App-CTKZtqB1.js +0 -460
  72. package/dist/assets/ReviewApp-DM6BNAzR.js +0 -1
  73. package/dist/assets/channel-1oJBvF-0.js +0 -1
  74. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +0 -1
  75. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +0 -1
  76. package/dist/assets/clone-CinxIlEu.js +0 -1
  77. package/dist/assets/index-YCFGDVKw.css +0 -1
  78. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +0 -1
  79. package/server/_legacy-providers/README.md +0 -30
  80. package/server/_legacy-providers/claude-sdk.js +0 -956
  81. package/server/_legacy-providers/gemini-cli.js +0 -368
  82. package/server/_legacy-providers/openai-codex.js +0 -705
  83. package/server/_legacy-providers/opencode-cli.js +0 -674
@@ -51,13 +51,62 @@ import os from 'os';
51
51
  import path from 'path';
52
52
  import readline from 'readline';
53
53
  import crypto from 'crypto';
54
+ import { fileURLToPath } from 'url';
54
55
  import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
55
56
  import { CODEX_MODELS } from '../shared/modelConstants.js';
56
57
 
58
+ const __filename = fileURLToPath(import.meta.url);
59
+ const __dirname = path.dirname(__filename);
60
+
57
61
  const KNOWN_CODEX_MODELS = new Set(
58
62
  (CODEX_MODELS?.OPTIONS || []).map((option) => String(option?.value || '').trim().toLowerCase()).filter(Boolean)
59
63
  );
60
64
 
65
+ const PROJECT_CONFIG_PERMISSION_ERROR_CODES = new Set(['EACCES', 'EPERM', 'EROFS']);
66
+
67
+ function getPrimaryProjectConfigPath() {
68
+ return path.join(os.homedir(), '.claude', 'project-config.json');
69
+ }
70
+
71
+ function getFallbackProjectConfigPath() {
72
+ const dataFilePath = process.env.DATA_FILE_PATH || path.join(__dirname, 'database', 'state.json');
73
+ return path.join(path.dirname(dataFilePath), 'project-config.json');
74
+ }
75
+
76
+ function getProjectConfigPaths() {
77
+ return [...new Set([
78
+ getPrimaryProjectConfigPath(),
79
+ getFallbackProjectConfigPath()
80
+ ])];
81
+ }
82
+
83
+ function isProjectConfigPermissionError(error) {
84
+ return Boolean(error && PROJECT_CONFIG_PERMISSION_ERROR_CODES.has(error.code));
85
+ }
86
+
87
+ function normalizeProjectConfig(rawConfig) {
88
+ if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
89
+ return {};
90
+ }
91
+
92
+ return rawConfig;
93
+ }
94
+
95
+ async function writeProjectConfigFile(configPath, config) {
96
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
97
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
98
+ }
99
+
100
+ async function removeProjectConfigFile(configPath) {
101
+ try {
102
+ await fs.rm(configPath, { force: true });
103
+ } catch (error) {
104
+ if (error.code !== 'ENOENT') {
105
+ throw error;
106
+ }
107
+ }
108
+ }
109
+
61
110
  function isRecognizedCodexModel(value) {
62
111
  if (typeof value !== 'string') return false;
63
112
  const normalized = value.trim().toLowerCase();
@@ -97,7 +146,12 @@ function extractVisibleUserMessage(value) {
97
146
  return '';
98
147
  }
99
148
 
100
- if (text.startsWith('# AGENTS.md instructions for ') || text.includes('<environment_context>')) {
149
+ if (
150
+ text.startsWith('# AGENTS.md instructions for ') ||
151
+ text.includes('<environment_context>') ||
152
+ text.startsWith('<subagent_notification>') ||
153
+ text.startsWith('</subagent_notification>')
154
+ ) {
101
155
  return '';
102
156
  }
103
157
 
@@ -128,12 +182,16 @@ function isInjectedContextContent(value) {
128
182
 
129
183
  if (
130
184
  text.startsWith('# AGENTS.md instructions for ') ||
131
- text.startsWith('[DYNAMIC CONTEXT V1]')
185
+ text.startsWith('[DYNAMIC CONTEXT V1]') ||
186
+ text.startsWith('<subagent_notification>') ||
187
+ text.startsWith('</subagent_notification>')
132
188
  ) {
133
189
  return true;
134
190
  }
135
191
 
136
- return /^<dynamic_context(?:\s|>)/i.test(text) || text.includes('<environment_context>');
192
+ return /^<dynamic_context(?:\s|>)/i.test(text)
193
+ || text.includes('<environment_context>')
194
+ || /<subagent_notification(?:\s|>)/i.test(text);
137
195
  }
138
196
 
139
197
  // Import TaskMaster detection functions
@@ -266,7 +324,14 @@ async function detectTaskMasterFolder(projectPath) {
266
324
 
267
325
  // Cache for extracted project directories
268
326
  const projectDirectoryCache = new Map();
327
+ const PROJECT_LIST_CACHE_TTL_MS = 15000;
269
328
  const PROVIDER_SESSION_LOOKUP_CACHE_TTL_MS = 5000;
329
+ const projectListCache = {
330
+ data: null,
331
+ promise: null,
332
+ expiresAt: 0
333
+ };
334
+ const codexSessionFileCache = new Map();
270
335
  const providerSessionLookupCache = {
271
336
  codex: { key: null, data: null, promise: null, expiresAt: 0 },
272
337
  gemini: { key: null, data: null, promise: null, expiresAt: 0 },
@@ -343,9 +408,81 @@ async function findFilesRecursively(rootDir, matcher) {
343
408
  return discoveredFiles;
344
409
  }
345
410
 
411
+ function getCodexSessionsDir() {
412
+ return path.join(os.homedir(), '.codex', 'sessions');
413
+ }
414
+
415
+ function cacheCodexSessionFilePath(sessionId, filePath) {
416
+ if (!sessionId || !filePath) {
417
+ return;
418
+ }
419
+
420
+ codexSessionFileCache.set(sessionId, filePath);
421
+ }
422
+
423
+ function findCodexSessionFilePathBySessionIdHint(sessionId, filePaths = []) {
424
+ const normalizedSessionId = String(sessionId || '').trim();
425
+ if (!normalizedSessionId || !Array.isArray(filePaths) || filePaths.length === 0) {
426
+ return null;
427
+ }
428
+
429
+ return filePaths.find((filePath) => {
430
+ const basename = path.basename(filePath, '.jsonl');
431
+ return basename === normalizedSessionId || basename.includes(normalizedSessionId);
432
+ }) || null;
433
+ }
434
+
435
+ async function resolveCodexSessionFile(sessionId) {
436
+ if (!sessionId) {
437
+ return null;
438
+ }
439
+
440
+ const cachedPath = codexSessionFileCache.get(sessionId);
441
+ if (cachedPath) {
442
+ try {
443
+ await fs.access(cachedPath);
444
+ return cachedPath;
445
+ } catch (_) {
446
+ codexSessionFileCache.delete(sessionId);
447
+ }
448
+ }
449
+
450
+ const jsonlFiles = await findFilesRecursively(
451
+ getCodexSessionsDir(),
452
+ (entryName) => entryName.endsWith('.jsonl')
453
+ );
454
+
455
+ const hintedFilePath = findCodexSessionFilePathBySessionIdHint(sessionId, jsonlFiles);
456
+ if (hintedFilePath) {
457
+ cacheCodexSessionFilePath(sessionId, hintedFilePath);
458
+ return hintedFilePath;
459
+ }
460
+
461
+ for (const filePath of jsonlFiles) {
462
+ try {
463
+ const sessionData = await parseCodexSessionFile(filePath);
464
+ if (sessionData?.id === sessionId) {
465
+ cacheCodexSessionFilePath(sessionId, filePath);
466
+ return filePath;
467
+ }
468
+ } catch (error) {
469
+ console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
470
+ }
471
+ }
472
+
473
+ return null;
474
+ }
475
+
476
+ function clearProjectListCache() {
477
+ projectListCache.data = null;
478
+ projectListCache.promise = null;
479
+ projectListCache.expiresAt = 0;
480
+ }
481
+
346
482
  // Clear cache when needed (called when project files change)
347
483
  function clearProjectDirectoryCache() {
348
484
  projectDirectoryCache.clear();
485
+ clearProjectListCache();
349
486
  }
350
487
 
351
488
  function clearProviderSessionLookupCaches() {
@@ -357,6 +494,27 @@ function clearProviderSessionLookupCaches() {
357
494
  }
358
495
  }
359
496
 
497
+ function appendBoundedMessage(buffer, message, maxSize = null) {
498
+ if (!message) {
499
+ return;
500
+ }
501
+
502
+ if (maxSize === null) {
503
+ buffer.push(message);
504
+ return;
505
+ }
506
+
507
+ if (maxSize <= 0) {
508
+ return;
509
+ }
510
+
511
+ if (buffer.length === maxSize) {
512
+ buffer.shift();
513
+ }
514
+
515
+ buffer.push(message);
516
+ }
517
+
360
518
  async function getCachedProviderSessionLookup(providerName, cacheKey, buildLookup) {
361
519
  const cacheEntry = providerSessionLookupCache[providerName];
362
520
 
@@ -393,31 +551,54 @@ async function getCachedProviderSessionLookup(providerName, cacheKey, buildLooku
393
551
 
394
552
  // Load project configuration file
395
553
  async function loadProjectConfig() {
396
- const configPath = path.join(os.homedir(), '.claude', 'project-config.json');
397
- try {
398
- const configData = await fs.readFile(configPath, 'utf8');
399
- return JSON.parse(configData);
400
- } catch (error) {
401
- // Return empty config if file doesn't exist
402
- return {};
554
+ const mergedConfig = {};
555
+
556
+ for (const configPath of getProjectConfigPaths()) {
557
+ try {
558
+ const configData = await fs.readFile(configPath, 'utf8');
559
+ Object.assign(mergedConfig, normalizeProjectConfig(JSON.parse(configData)));
560
+ } catch (error) {
561
+ // Return merged config from any readable location.
562
+ }
403
563
  }
564
+
565
+ return mergedConfig;
404
566
  }
405
567
 
406
568
  // Save project configuration file
407
569
  async function saveProjectConfig(config) {
408
- const claudeDir = path.join(os.homedir(), '.claude');
409
- const configPath = path.join(claudeDir, 'project-config.json');
410
-
411
- // Ensure the .claude directory exists
412
- try {
413
- await fs.mkdir(claudeDir, { recursive: true });
414
- } catch (error) {
415
- if (error.code !== 'EEXIST') {
570
+ const normalizedConfig = normalizeProjectConfig(config);
571
+ const [primaryConfigPath, ...fallbackConfigPaths] = getProjectConfigPaths();
572
+ let lastPermissionError = null;
573
+
574
+ for (const configPath of [primaryConfigPath, ...fallbackConfigPaths]) {
575
+ try {
576
+ await writeProjectConfigFile(configPath, normalizedConfig);
577
+
578
+ if (configPath === primaryConfigPath) {
579
+ for (const fallbackConfigPath of fallbackConfigPaths) {
580
+ await removeProjectConfigFile(fallbackConfigPath);
581
+ }
582
+ }
583
+
584
+ clearProjectDirectoryCache();
585
+ return;
586
+ } catch (error) {
587
+ if (isProjectConfigPermissionError(error)) {
588
+ lastPermissionError = error;
589
+ continue;
590
+ }
591
+
416
592
  throw error;
417
593
  }
418
594
  }
419
-
420
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
595
+
596
+ if (lastPermissionError) {
597
+ const saveError = new Error('Unable to save the project list because both Claude config storage and the app data directory are not writable.');
598
+ saveError.code = lastPermissionError.code;
599
+ saveError.cause = lastPermissionError;
600
+ throw saveError;
601
+ }
421
602
  }
422
603
 
423
604
  // Generate better display name from path
@@ -568,15 +749,17 @@ async function extractProjectDirectory(projectName) {
568
749
  }
569
750
  }
570
751
 
571
- async function getProjects(progressCallback = null) {
752
+ function cloneProjectList(projects = []) {
753
+ return projects.map((project) => ({ ...project }));
754
+ }
755
+
756
+ async function collectProjectDefinitions(progressCallback = null) {
572
757
  const claudeDir = path.join(os.homedir(), '.claude', 'projects');
573
758
  const config = await loadProjectConfig();
574
- const projects = [];
575
759
  const existingProjects = new Set();
576
760
  const projectDefinitions = [];
577
761
  let totalProjects = 0;
578
762
  let processedProjects = 0;
579
- let directories = [];
580
763
 
581
764
  try {
582
765
  // Check if the .claude/projects directory exists
@@ -584,7 +767,7 @@ async function getProjects(progressCallback = null) {
584
767
 
585
768
  // First, get existing Claude projects from the file system
586
769
  const entries = await fs.readdir(claudeDir, { withFileTypes: true });
587
- directories = entries.filter(e => e.isDirectory());
770
+ const directories = entries.filter(e => e.isDirectory());
588
771
 
589
772
  // Build set of existing project names for later
590
773
  directories.forEach(e => existingProjects.add(e.name));
@@ -675,69 +858,173 @@ async function getProjects(progressCallback = null) {
675
858
  }
676
859
  }
677
860
 
678
- const uniqueProjectPaths = Array.from(new Set(
679
- projectDefinitions
680
- .map((definition) => normalizeComparableProjectPath(definition.fullPath))
681
- .filter(Boolean)
682
- ));
861
+ return {
862
+ projectDefinitions,
863
+ totalProjects
864
+ };
865
+ }
683
866
 
684
- const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath] = await Promise.all([
685
- buildCodexSessionsLookup(uniqueProjectPaths, { limit: 5 }),
686
- buildGeminiSessionsLookup(uniqueProjectPaths, { limit: 5 }),
687
- buildOpencodeSessionsLookup(uniqueProjectPaths, { limit: 5 })
688
- ]);
867
+ async function getProjectsList(progressCallback = null) {
868
+ if (
869
+ projectListCache.data &&
870
+ projectListCache.expiresAt > Date.now()
871
+ ) {
872
+ const cachedProjects = cloneProjectList(projectListCache.data);
873
+ if (progressCallback) {
874
+ progressCallback({
875
+ phase: 'complete',
876
+ current: cachedProjects.length,
877
+ total: cachedProjects.length
878
+ });
879
+ }
880
+ return cachedProjects;
881
+ }
689
882
 
690
- for (const definition of projectDefinitions) {
691
- const normalizedProjectPath = normalizeComparableProjectPath(definition.fullPath);
692
- const project = {
883
+ if (!progressCallback && projectListCache.promise) {
884
+ return cloneProjectList(await projectListCache.promise);
885
+ }
886
+
887
+ const loadProjectsList = async () => {
888
+ const { projectDefinitions, totalProjects } = await collectProjectDefinitions(progressCallback);
889
+ const lightweightProjects = projectDefinitions.map((definition) => ({
693
890
  name: definition.name,
694
891
  path: definition.path,
695
892
  displayName: definition.displayName,
696
893
  fullPath: definition.fullPath,
697
894
  isCustomName: definition.isCustomName,
698
- isManuallyAdded: !!definition.isManuallyAdded,
699
- sessions: [],
700
- codexSessions: codexSessionsByProjectPath.get(normalizedProjectPath) || [],
701
- opencodeSessions: opencodeSessionsByProjectPath.get(normalizedProjectPath) || [],
702
- geminiSessions: geminiSessionsByProjectPath.get(normalizedProjectPath) || []
703
- };
895
+ isManuallyAdded: !!definition.isManuallyAdded
896
+ }));
704
897
 
705
- if (!definition.isManuallyAdded) {
706
- try {
707
- const sessionResult = await getSessions(definition.name, 5, 0);
708
- project.sessions = sessionResult.sessions || [];
709
- project.sessionMeta = {
710
- hasMore: sessionResult.hasMore,
711
- total: sessionResult.total
712
- };
713
- } catch (e) {
714
- console.warn(`Could not load sessions for project ${definition.name}:`, e.message);
715
- }
898
+ projectListCache.data = lightweightProjects;
899
+ projectListCache.expiresAt = Date.now() + PROJECT_LIST_CACHE_TTL_MS;
900
+
901
+ if (progressCallback) {
902
+ progressCallback({
903
+ phase: 'complete',
904
+ current: totalProjects,
905
+ total: totalProjects
906
+ });
716
907
  }
717
908
 
718
- try {
719
- const taskMasterResult = await detectTaskMasterFolder(definition.fullPath);
720
- const taskMasterStatus = definition.isManuallyAdded
721
- ? (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'taskmaster-only' : 'not-configured')
722
- : (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured');
723
-
724
- project.taskmaster = {
725
- status: taskMasterStatus,
726
- hasTaskmaster: taskMasterResult.hasTaskmaster,
727
- hasEssentialFiles: taskMasterResult.hasEssentialFiles,
728
- metadata: taskMasterResult.metadata
729
- };
730
- } catch (error) {
731
- console.warn(`TaskMaster detection failed for project ${definition.name}:`, error.message);
732
- project.taskmaster = {
733
- status: 'error',
734
- hasTaskmaster: false,
735
- hasEssentialFiles: false,
736
- error: error.message
909
+ return lightweightProjects;
910
+ };
911
+
912
+ if (progressCallback) {
913
+ return loadProjectsList();
914
+ }
915
+
916
+ projectListCache.promise = loadProjectsList().finally(() => {
917
+ projectListCache.promise = null;
918
+ });
919
+
920
+ return cloneProjectList(await projectListCache.promise);
921
+ }
922
+
923
+ async function buildProjectFromDefinition(definition, providerLookups = null) {
924
+ const normalizedProjectPath = normalizeComparableProjectPath(definition.fullPath);
925
+ const project = {
926
+ name: definition.name,
927
+ path: definition.path,
928
+ displayName: definition.displayName,
929
+ fullPath: definition.fullPath,
930
+ isCustomName: definition.isCustomName,
931
+ isManuallyAdded: !!definition.isManuallyAdded,
932
+ sessions: [],
933
+ codexSessions: [],
934
+ opencodeSessions: [],
935
+ geminiSessions: [],
936
+ sessionMeta: {
937
+ hasMore: false,
938
+ total: 0
939
+ }
940
+ };
941
+
942
+ try {
943
+ if (providerLookups) {
944
+ project.codexSessions = providerLookups.codexSessionsByProjectPath?.get(normalizedProjectPath) || [];
945
+ project.opencodeSessions = providerLookups.opencodeSessionsByProjectPath?.get(normalizedProjectPath) || [];
946
+ project.geminiSessions = providerLookups.geminiSessionsByProjectPath?.get(normalizedProjectPath) || [];
947
+ } else {
948
+ const [codexSessions, opencodeSessions, geminiSessions] = await Promise.all([
949
+ getCodexSessions(definition.fullPath, { limit: 5 }),
950
+ getOpencodeSessions(definition.fullPath, { limit: 5 }),
951
+ getGeminiSessions(definition.fullPath, { limit: 5 })
952
+ ]);
953
+ project.codexSessions = codexSessions;
954
+ project.opencodeSessions = opencodeSessions;
955
+ project.geminiSessions = geminiSessions;
956
+ }
957
+
958
+ if (!definition.isManuallyAdded) {
959
+ const sessionResult = await getSessions(definition.name, 5, 0);
960
+ project.sessions = sessionResult.sessions || [];
961
+ project.sessionMeta = {
962
+ hasMore: sessionResult.hasMore,
963
+ total: sessionResult.total
737
964
  };
738
965
  }
966
+ } catch (error) {
967
+ console.warn(`Could not load session details for project ${definition.name}:`, error.message);
968
+ }
969
+
970
+ try {
971
+ const taskMasterResult = await detectTaskMasterFolder(definition.fullPath);
972
+ const taskMasterStatus = definition.isManuallyAdded
973
+ ? (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'taskmaster-only' : 'not-configured')
974
+ : (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured');
975
+
976
+ project.taskmaster = {
977
+ status: taskMasterStatus,
978
+ hasTaskmaster: taskMasterResult.hasTaskmaster,
979
+ hasEssentialFiles: taskMasterResult.hasEssentialFiles,
980
+ metadata: taskMasterResult.metadata
981
+ };
982
+ } catch (error) {
983
+ console.warn(`TaskMaster detection failed for project ${definition.name}:`, error.message);
984
+ project.taskmaster = {
985
+ status: 'error',
986
+ hasTaskmaster: false,
987
+ hasEssentialFiles: false,
988
+ error: error.message
989
+ };
990
+ }
991
+
992
+ return project;
993
+ }
994
+
995
+ async function getProjectDetails(projectName) {
996
+ const projectList = await getProjectsList();
997
+ const definition = projectList.find((project) => project.name === projectName);
998
+
999
+ if (!definition) {
1000
+ throw new Error(`Project not found: ${projectName}`);
1001
+ }
1002
+
1003
+ return buildProjectFromDefinition(definition);
1004
+ }
1005
+
1006
+ async function getProjects(progressCallback = null) {
1007
+ const projects = [];
1008
+ const { projectDefinitions, totalProjects } = await collectProjectDefinitions(progressCallback);
1009
+
1010
+ const uniqueProjectPaths = Array.from(new Set(
1011
+ projectDefinitions
1012
+ .map((definition) => normalizeComparableProjectPath(definition.fullPath))
1013
+ .filter(Boolean)
1014
+ ));
739
1015
 
740
- projects.push(project);
1016
+ const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath] = await Promise.all([
1017
+ buildCodexSessionsLookup(uniqueProjectPaths, { limit: 5 }),
1018
+ buildGeminiSessionsLookup(uniqueProjectPaths, { limit: 5 }),
1019
+ buildOpencodeSessionsLookup(uniqueProjectPaths, { limit: 5 })
1020
+ ]);
1021
+
1022
+ for (const definition of projectDefinitions) {
1023
+ projects.push(await buildProjectFromDefinition(definition, {
1024
+ codexSessionsByProjectPath,
1025
+ opencodeSessionsByProjectPath,
1026
+ geminiSessionsByProjectPath
1027
+ }));
741
1028
  }
742
1029
 
743
1030
  // Emit completion after all projects (including manual) are processed
@@ -880,6 +1167,43 @@ async function getSessions(projectName, limit = 5, offset = 0) {
880
1167
  }
881
1168
  }
882
1169
 
1170
+ async function getClaudeSessionMetadata(sessionId) {
1171
+ try {
1172
+ const normalizedSessionId = String(sessionId || '').trim();
1173
+ if (!normalizedSessionId) {
1174
+ return null;
1175
+ }
1176
+
1177
+ const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
1178
+ const jsonlFiles = await findFilesRecursively(
1179
+ claudeProjectsDir,
1180
+ (entryName) => entryName.endsWith('.jsonl') && !entryName.startsWith('agent-')
1181
+ );
1182
+
1183
+ for (const filePath of jsonlFiles) {
1184
+ try {
1185
+ const result = await parseJsonlSessions(filePath);
1186
+ const matchedSession = (result?.sessions || []).find((session) => session?.id === normalizedSessionId);
1187
+ if (matchedSession) {
1188
+ return {
1189
+ ...matchedSession,
1190
+ filePath,
1191
+ projectName: path.basename(path.dirname(filePath)),
1192
+ provider: 'claude'
1193
+ };
1194
+ }
1195
+ } catch {}
1196
+ }
1197
+
1198
+ return null;
1199
+ } catch (error) {
1200
+ if (error?.code !== 'ENOENT') {
1201
+ console.error(`Error reading Claude session metadata for ${sessionId}:`, error);
1202
+ }
1203
+ return null;
1204
+ }
1205
+ }
1206
+
883
1207
  async function parseJsonlSessions(filePath) {
884
1208
  const sessions = new Map();
885
1209
  const entries = [];
@@ -1336,7 +1660,7 @@ async function getCodexSessions(projectPath, options = {}) {
1336
1660
  const sessions = [];
1337
1661
  const normalizedProjectPath = normalizeComparableProjectPath(projectPath);
1338
1662
  const jsonlFiles = await findFilesRecursively(
1339
- path.join(os.homedir(), '.codex', 'sessions'),
1663
+ getCodexSessionsDir(),
1340
1664
  (entryName) => entryName.endsWith('.jsonl')
1341
1665
  );
1342
1666
 
@@ -1346,6 +1670,7 @@ async function getCodexSessions(projectPath, options = {}) {
1346
1670
  const sessionData = await parseCodexSessionFile(filePath);
1347
1671
 
1348
1672
  if (sessionData && normalizeComparableProjectPath(sessionData.cwd) === normalizedProjectPath) {
1673
+ cacheCodexSessionFilePath(sessionData.id, filePath);
1349
1674
  sessions.push({
1350
1675
  id: sessionData.id,
1351
1676
  summary: sessionData.summary || 'Codex Session',
@@ -1374,15 +1699,17 @@ async function getCodexSessions(projectPath, options = {}) {
1374
1699
  }
1375
1700
  }
1376
1701
 
1377
- const OPENCODE_SESSION_DIR_CANDIDATES = [
1378
- path.join(os.homedir(), '.opencode', 'sessions'),
1379
- path.join(os.homedir(), '.config', 'opencode', 'sessions')
1380
- ];
1702
+ function getOpencodeSessionDirCandidates() {
1703
+ return [
1704
+ path.join(os.homedir(), '.opencode', 'sessions'),
1705
+ path.join(os.homedir(), '.config', 'opencode', 'sessions')
1706
+ ];
1707
+ }
1381
1708
 
1382
1709
  async function findOpencodeSessionFiles() {
1383
1710
  const files = [];
1384
1711
 
1385
- for (const baseDir of OPENCODE_SESSION_DIR_CANDIDATES) {
1712
+ for (const baseDir of getOpencodeSessionDirCandidates()) {
1386
1713
  const discovered = await findFilesRecursively(
1387
1714
  baseDir,
1388
1715
  (entryName, _fullPath, entry) => entry.isFile() && entryName.endsWith('.jsonl')
@@ -1647,6 +1974,34 @@ async function findOpencodeSessionFileById(sessionId) {
1647
1974
  return null;
1648
1975
  }
1649
1976
 
1977
+ async function getOpencodeSessionMetadata(sessionId) {
1978
+ try {
1979
+ const normalizedSessionId = String(sessionId || '').trim();
1980
+ if (!normalizedSessionId) {
1981
+ return null;
1982
+ }
1983
+
1984
+ const sessionFilePath = await findOpencodeSessionFileById(normalizedSessionId);
1985
+ if (!sessionFilePath) {
1986
+ return null;
1987
+ }
1988
+
1989
+ const sessionData = await parseOpencodeSessionFile(sessionFilePath);
1990
+ if (!sessionData) {
1991
+ return null;
1992
+ }
1993
+
1994
+ return {
1995
+ ...sessionData,
1996
+ filePath: sessionFilePath,
1997
+ provider: 'opencode'
1998
+ };
1999
+ } catch (error) {
2000
+ console.error(`Error reading OpenCode session metadata for ${sessionId}:`, error);
2001
+ return null;
2002
+ }
2003
+ }
2004
+
1650
2005
  async function getOpencodeSessionMessages(sessionId, limit = null, offset = 0) {
1651
2006
  try {
1652
2007
  const sessionFilePath = await findOpencodeSessionFileById(sessionId);
@@ -1857,6 +2212,30 @@ async function parseGeminiSessionFile(filePath) {
1857
2212
  };
1858
2213
  }
1859
2214
 
2215
+ async function getGeminiSessionMetadata(sessionId) {
2216
+ try {
2217
+ const normalizedSessionId = String(sessionId || '').trim();
2218
+ if (!normalizedSessionId) {
2219
+ return null;
2220
+ }
2221
+
2222
+ const sessionFiles = await findGeminiSessionFiles();
2223
+ for (const filePath of sessionFiles) {
2224
+ try {
2225
+ const sessionData = await parseGeminiSessionFile(filePath);
2226
+ if (sessionData?.id === normalizedSessionId) {
2227
+ return sessionData;
2228
+ }
2229
+ } catch {}
2230
+ }
2231
+
2232
+ return null;
2233
+ } catch (error) {
2234
+ console.error(`Error reading Gemini session metadata for ${sessionId}:`, error);
2235
+ return null;
2236
+ }
2237
+ }
2238
+
1860
2239
  async function buildCodexSessionsLookup(projectPaths, options = {}) {
1861
2240
  const { limit = 5 } = options;
1862
2241
  const normalizedProjectPaths = Array.from(new Set(
@@ -1870,7 +2249,7 @@ async function buildCodexSessionsLookup(projectPaths, options = {}) {
1870
2249
  const cacheKey = JSON.stringify({ projectPaths: normalizedProjectPaths, limit });
1871
2250
  return getCachedProviderSessionLookup('codex', cacheKey, async () => {
1872
2251
  const jsonlFiles = await findFilesRecursively(
1873
- path.join(os.homedir(), '.codex', 'sessions'),
2252
+ getCodexSessionsDir(),
1874
2253
  (entryName) => entryName.endsWith('.jsonl')
1875
2254
  );
1876
2255
 
@@ -1879,6 +2258,7 @@ async function buildCodexSessionsLookup(projectPaths, options = {}) {
1879
2258
  try {
1880
2259
  const sessionData = await parseCodexSessionFile(filePath);
1881
2260
  if (sessionData?.id) {
2261
+ cacheCodexSessionFilePath(sessionData.id, filePath);
1882
2262
  sessions.push({
1883
2263
  id: sessionData.id,
1884
2264
  summary: sessionData.summary || 'Codex Session',
@@ -1980,19 +2360,8 @@ async function buildGeminiSessionsLookup(projectPaths, options = {}) {
1980
2360
 
1981
2361
  async function getGeminiSessionMessages(sessionId, limit = null, offset = 0) {
1982
2362
  try {
1983
- const sessionFiles = await findGeminiSessionFiles();
1984
- let sessionFilePath = null;
1985
-
1986
- for (const filePath of sessionFiles) {
1987
- try {
1988
- const raw = await fs.readFile(filePath, 'utf8');
1989
- const parsed = JSON.parse(raw);
1990
- if (parsed?.sessionId === sessionId) {
1991
- sessionFilePath = filePath;
1992
- break;
1993
- }
1994
- } catch {}
1995
- }
2363
+ const sessionMetadata = await getGeminiSessionMetadata(sessionId);
2364
+ const sessionFilePath = sessionMetadata?.filePath || null;
1996
2365
 
1997
2366
  if (!sessionFilePath) {
1998
2367
  return { messages: [], total: 0, hasMore: false };
@@ -2143,6 +2512,7 @@ async function parseCodexSessionFile(filePath) {
2143
2512
  }
2144
2513
 
2145
2514
  if (sessionMeta) {
2515
+ cacheCodexSessionFilePath(sessionMeta.id, filePath);
2146
2516
  return {
2147
2517
  ...sessionMeta,
2148
2518
  model: lastResolvedModel || sessionMeta.model || null,
@@ -2165,35 +2535,18 @@ async function parseCodexSessionFile(filePath) {
2165
2535
  // Get messages for a specific Codex session
2166
2536
  async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2167
2537
  try {
2168
- const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
2169
-
2170
- // Find the session file by searching for the session ID
2171
- const findSessionFile = async (dir) => {
2172
- try {
2173
- const entries = await fs.readdir(dir, { withFileTypes: true });
2174
- for (const entry of entries) {
2175
- const fullPath = path.join(dir, entry.name);
2176
- if (entry.isDirectory()) {
2177
- const found = await findSessionFile(fullPath);
2178
- if (found) return found;
2179
- } else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
2180
- return fullPath;
2181
- }
2182
- }
2183
- } catch (error) {
2184
- // Skip directories we can't read
2185
- }
2186
- return null;
2187
- };
2188
-
2189
- const sessionFilePath = await findSessionFile(codexSessionsDir);
2538
+ const sessionFilePath = await resolveCodexSessionFile(sessionId);
2190
2539
 
2191
2540
  if (!sessionFilePath) {
2192
2541
  console.warn(`Codex session file not found for session ${sessionId}`);
2193
2542
  return { messages: [], total: 0, hasMore: false };
2194
2543
  }
2195
2544
 
2545
+ const normalizedLimit = limit === null ? null : Math.max(0, Number(limit) || 0);
2546
+ const normalizedOffset = Math.max(0, Number(offset) || 0);
2547
+ const maxBufferedMessages = normalizedLimit === null ? null : normalizedLimit + normalizedOffset;
2196
2548
  const messages = [];
2549
+ let total = 0;
2197
2550
  let tokenUsage = null;
2198
2551
  const fileStream = fsSync.createReadStream(sessionFilePath);
2199
2552
  const rl = readline.createInterface({
@@ -2218,6 +2571,11 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2218
2571
  .join('\n');
2219
2572
  };
2220
2573
 
2574
+ const appendMessage = (message) => {
2575
+ total += 1;
2576
+ appendBoundedMessage(messages, message, maxBufferedMessages);
2577
+ };
2578
+
2221
2579
  for await (const line of rl) {
2222
2580
  if (line.trim()) {
2223
2581
  try {
@@ -2243,7 +2601,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2243
2601
 
2244
2602
  // Only add if there's actual content
2245
2603
  if (visibleTextContent?.trim()) {
2246
- messages.push({
2604
+ appendMessage({
2247
2605
  type: role === 'user' ? 'user' : 'assistant',
2248
2606
  timestamp: entry.timestamp,
2249
2607
  message: {
@@ -2260,7 +2618,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2260
2618
  .filter(Boolean)
2261
2619
  .join('\n');
2262
2620
  if (summaryText?.trim()) {
2263
- messages.push({
2621
+ appendMessage({
2264
2622
  type: 'thinking',
2265
2623
  timestamp: entry.timestamp,
2266
2624
  message: {
@@ -2286,7 +2644,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2286
2644
  }
2287
2645
  }
2288
2646
 
2289
- messages.push({
2647
+ appendMessage({
2290
2648
  type: 'tool_use',
2291
2649
  timestamp: entry.timestamp,
2292
2650
  toolName: toolName,
@@ -2296,7 +2654,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2296
2654
  }
2297
2655
 
2298
2656
  if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
2299
- messages.push({
2657
+ appendMessage({
2300
2658
  type: 'tool_result',
2301
2659
  timestamp: entry.timestamp,
2302
2660
  toolCallId: entry.payload.call_id,
@@ -2326,7 +2684,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2326
2684
  }
2327
2685
  }
2328
2686
 
2329
- messages.push({
2687
+ appendMessage({
2330
2688
  type: 'tool_use',
2331
2689
  timestamp: entry.timestamp,
2332
2690
  toolName: 'Edit',
@@ -2338,7 +2696,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2338
2696
  toolCallId: entry.payload.call_id
2339
2697
  });
2340
2698
  } else {
2341
- messages.push({
2699
+ appendMessage({
2342
2700
  type: 'tool_use',
2343
2701
  timestamp: entry.timestamp,
2344
2702
  toolName: toolName,
@@ -2349,7 +2707,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2349
2707
  }
2350
2708
 
2351
2709
  if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
2352
- messages.push({
2710
+ appendMessage({
2353
2711
  type: 'tool_result',
2354
2712
  timestamp: entry.timestamp,
2355
2713
  toolCallId: entry.payload.call_id,
@@ -2363,29 +2721,31 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2363
2721
  }
2364
2722
  }
2365
2723
 
2366
- // Sort by timestamp
2367
- messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
2368
-
2369
- const total = messages.length;
2370
-
2371
2724
  // Apply pagination if limit is specified
2372
- if (limit !== null) {
2373
- const startIndex = Math.max(0, total - offset - limit);
2374
- const endIndex = total - offset;
2725
+ if (normalizedLimit !== null) {
2726
+ const endIndex = Math.max(0, messages.length - normalizedOffset);
2727
+ const startIndex = Math.max(0, endIndex - normalizedLimit);
2375
2728
  const paginatedMessages = messages.slice(startIndex, endIndex);
2376
- const hasMore = startIndex > 0;
2729
+ const hasMore = total > normalizedOffset + paginatedMessages.length;
2377
2730
 
2378
2731
  return {
2379
2732
  messages: paginatedMessages,
2380
2733
  total,
2381
2734
  hasMore,
2382
- offset,
2383
- limit,
2735
+ offset: normalizedOffset,
2736
+ limit: normalizedLimit,
2384
2737
  tokenUsage
2385
2738
  };
2386
2739
  }
2387
2740
 
2388
- return { messages, tokenUsage };
2741
+ return {
2742
+ messages,
2743
+ total,
2744
+ hasMore: false,
2745
+ offset: normalizedOffset,
2746
+ limit: normalizedLimit,
2747
+ tokenUsage
2748
+ };
2389
2749
 
2390
2750
  } catch (error) {
2391
2751
  console.error(`Error reading Codex session messages for ${sessionId}:`, error);
@@ -2393,37 +2753,45 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2393
2753
  }
2394
2754
  }
2395
2755
 
2396
- async function deleteCodexSession(sessionId) {
2397
- try {
2398
- const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
2756
+ async function getCodexSessionMetadata(sessionId) {
2757
+ if (!sessionId) {
2758
+ return null;
2759
+ }
2399
2760
 
2400
- const findJsonlFiles = async (dir) => {
2401
- const files = [];
2402
- try {
2403
- const entries = await fs.readdir(dir, { withFileTypes: true });
2404
- for (const entry of entries) {
2405
- const fullPath = path.join(dir, entry.name);
2406
- if (entry.isDirectory()) {
2407
- files.push(...await findJsonlFiles(fullPath));
2408
- } else if (entry.name.endsWith('.jsonl')) {
2409
- files.push(fullPath);
2410
- }
2411
- }
2412
- } catch (error) {}
2413
- return files;
2414
- };
2761
+ const sessionFilePath = await resolveCodexSessionFile(sessionId);
2762
+ if (!sessionFilePath) {
2763
+ return null;
2764
+ }
2415
2765
 
2416
- const jsonlFiles = await findJsonlFiles(codexSessionsDir);
2766
+ const sessionData = await parseCodexSessionFile(sessionFilePath);
2767
+ if (!sessionData) {
2768
+ return null;
2769
+ }
2417
2770
 
2418
- for (const filePath of jsonlFiles) {
2419
- const sessionData = await parseCodexSessionFile(filePath);
2420
- if (sessionData && sessionData.id === sessionId) {
2421
- await fs.unlink(filePath);
2422
- return true;
2423
- }
2771
+ const metadata = {
2772
+ ...sessionData,
2773
+ filePath: sessionFilePath,
2774
+ provider: 'codex'
2775
+ };
2776
+
2777
+ if (metadata.git === undefined) {
2778
+ delete metadata.git;
2779
+ }
2780
+
2781
+ return metadata;
2782
+ }
2783
+
2784
+ async function deleteCodexSession(sessionId) {
2785
+ try {
2786
+ const sessionFilePath = await resolveCodexSessionFile(sessionId);
2787
+
2788
+ if (!sessionFilePath) {
2789
+ throw new Error(`Codex session file not found for session ${sessionId}`);
2424
2790
  }
2425
2791
 
2426
- throw new Error(`Codex session file not found for session ${sessionId}`);
2792
+ await fs.unlink(sessionFilePath);
2793
+ codexSessionFileCache.delete(sessionId);
2794
+ return true;
2427
2795
  } catch (error) {
2428
2796
  console.error(`Error deleting Codex session ${sessionId}:`, error);
2429
2797
  throw error;
@@ -2432,7 +2800,10 @@ async function deleteCodexSession(sessionId) {
2432
2800
 
2433
2801
  export {
2434
2802
  getProjects,
2803
+ getProjectsList,
2804
+ getProjectDetails,
2435
2805
  getSessions,
2806
+ getClaudeSessionMetadata,
2436
2807
  getSessionMessages,
2437
2808
  parseJsonlSessions,
2438
2809
  renameProject,
@@ -2445,11 +2816,15 @@ export {
2445
2816
  extractProjectDirectory,
2446
2817
  clearProjectDirectoryCache,
2447
2818
  clearProviderSessionLookupCaches,
2819
+ findCodexSessionFilePathBySessionIdHint,
2448
2820
  getCodexSessions,
2449
2821
  getCodexSessionMessages,
2822
+ getCodexSessionMetadata,
2450
2823
  getOpencodeSessions,
2824
+ getOpencodeSessionMetadata,
2451
2825
  getOpencodeSessionMessages,
2452
2826
  getGeminiSessions,
2827
+ getGeminiSessionMetadata,
2453
2828
  getGeminiSessionMessages,
2454
2829
  deleteCodexSession,
2455
2830
  deleteOpencodeSession