@axhub/genie 0.2.8 → 0.2.10
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/LICENSE +21 -675
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-CYCCsgwf.js +264 -0
- package/dist/assets/ReviewApp-0srHIXwb.js +1 -0
- package/dist/assets/{_basePickBy-CqJbRZ9y.js → _basePickBy-DVVb07UV.js} +1 -1
- package/dist/assets/{_baseUniq-BS8YH8jO.js → _baseUniq-BtbziL5G.js} +1 -1
- package/dist/assets/{arc-BBmKEN-S.js → arc-BsCC8yBD.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-N5lcb82R.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-DTMwHuLn.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-BTKlkXI9.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
- package/dist/assets/channel-BMhScXFe.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DUdoTxAc.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Bm_92xe4.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CGW0g62g.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-DYkTH3w1.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-C9oTlISU.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-CM50ygWP.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-7dzpYeNJ.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-Cm9nQrsr.js → chunk-WL4C6EOR-DhzTthv6.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +1 -0
- package/dist/assets/clone-BPqOt4r3.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Ccp_p0JZ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-fBwTLUp9.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-CeNVmFUp.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2-CtavyLGa.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-CpQTjQwc.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-B8R5vwhd.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-BvkVVwIQ.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK-DOu3hSNa.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-C7zT67YE.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
- package/dist/assets/{graph-D11wiwHo.js → graph-CeJCMjan.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-Babpthg-.js → highlighted-body-TPN3WLV5-B_novwSz.js} +1 -1
- package/dist/assets/index-C514cLyb.js +2 -0
- package/dist/assets/index-h1DBl_g3.css +1 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-BmA7IpQG.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-BEquZd3E.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-BfemGz7f.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-CWja3mln.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
- package/dist/assets/{layout-BLUNf-PJ.js → layout-CI2RM-v6.js} +1 -1
- package/dist/assets/{linear-DukIV_Xv.js → linear-DE7bISck.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-SgtM28qI.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
- package/dist/assets/{mindmap-definition-YRQLILUH-4UjqXITU.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-8AxqJd0M.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-D60m8V8r.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-zqh9jBVf.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-CDZILTLI.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-7BReFd0L.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-HPTVdIG4.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-CTVllFgr.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-BtyxboJZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
- package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
- package/dist/assets/{vendor-react-Cpt6D04s.js → vendor-react-xmA_f8ig.js} +1 -1
- package/dist/assets/{vennDiagram-LZ73GAT5-D96ZI6Mg.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-eRk-39YO.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
- package/dist/index.html +5 -5
- package/package.json +8 -7
- package/server/acp-runtime/client.js +129 -16
- package/server/acp-runtime/index.js +54 -0
- package/server/acp-runtime/registry.js +2 -2
- package/server/acp-runtime/session-store.js +79 -5
- package/server/cli.js +55 -10
- package/server/database/db.js +20 -0
- package/server/external-agent/service.js +24 -6
- package/server/external-agent/ws.js +540 -27
- package/server/index.js +112 -151
- package/server/lan-access/core.js +79 -0
- package/server/lan-access/state.js +102 -0
- package/server/middleware/auth.js +57 -14
- package/server/projects.js +930 -667
- package/server/routes/auth.js +24 -4
- package/server/routes/cli-auth.js +21 -25
- package/server/routes/codex.js +84 -298
- package/server/routes/commands.js +322 -407
- package/server/routes/lan-access.js +231 -0
- package/server/routes/projects.js +154 -158
- package/server/routes/session-core.js +160 -91
- package/server/routes/settings.js +113 -99
- package/server/session-core/eventStore.js +60 -20
- package/server/session-core/providerAdapters.js +75 -38
- package/server/session-core/runtimeState.js +8 -0
- package/server/session-core/sessionListMerge.js +47 -0
- package/shared/conversationEvents.js +174 -15
- package/shared/modelConstants.js +79 -99
- package/dist/assets/App-CTKZtqB1.js +0 -460
- package/dist/assets/ReviewApp-DM6BNAzR.js +0 -1
- package/dist/assets/channel-1oJBvF-0.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +0 -1
- package/dist/assets/clone-CinxIlEu.js +0 -1
- package/dist/assets/index-DFxzgWoO.js +0 -2
- package/dist/assets/index-YCFGDVKw.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +0 -1
- package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
- package/server/_legacy-providers/README.md +0 -30
- package/server/_legacy-providers/claude-sdk.js +0 -956
- package/server/_legacy-providers/gemini-cli.js +0 -368
- package/server/_legacy-providers/openai-codex.js +0 -705
- package/server/_legacy-providers/opencode-cli.js +0 -674
- package/server/routes/git.js +0 -1110
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -536
- package/server/routes/taskmaster.js +0 -1963
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
package/server/projects.js
CHANGED
|
@@ -51,13 +51,68 @@ 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';
|
|
57
|
+
import { listAcpSessions } from './acp-runtime/session-store.js';
|
|
58
|
+
import { mergeSessionLists } from './session-core/sessionListMerge.js';
|
|
59
|
+
|
|
60
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
61
|
+
const __dirname = path.dirname(__filename);
|
|
56
62
|
|
|
57
63
|
const KNOWN_CODEX_MODELS = new Set(
|
|
58
64
|
(CODEX_MODELS?.OPTIONS || []).map((option) => String(option?.value || '').trim().toLowerCase()).filter(Boolean)
|
|
59
65
|
);
|
|
60
66
|
|
|
67
|
+
const PROJECT_CONFIG_PERMISSION_ERROR_CODES = new Set(['EACCES', 'EPERM', 'EROFS']);
|
|
68
|
+
|
|
69
|
+
function trimText(value) {
|
|
70
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getPrimaryProjectConfigPath() {
|
|
74
|
+
return path.join(os.homedir(), '.claude', 'project-config.json');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getFallbackProjectConfigPath() {
|
|
78
|
+
const dataFilePath = process.env.DATA_FILE_PATH || path.join(__dirname, 'database', 'state.json');
|
|
79
|
+
return path.join(path.dirname(dataFilePath), 'project-config.json');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getProjectConfigPaths() {
|
|
83
|
+
return [...new Set([
|
|
84
|
+
getPrimaryProjectConfigPath(),
|
|
85
|
+
getFallbackProjectConfigPath()
|
|
86
|
+
])];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isProjectConfigPermissionError(error) {
|
|
90
|
+
return Boolean(error && PROJECT_CONFIG_PERMISSION_ERROR_CODES.has(error.code));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeProjectConfig(rawConfig) {
|
|
94
|
+
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return rawConfig;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function writeProjectConfigFile(configPath, config) {
|
|
102
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
103
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function removeProjectConfigFile(configPath) {
|
|
107
|
+
try {
|
|
108
|
+
await fs.rm(configPath, { force: true });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== 'ENOENT') {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
61
116
|
function isRecognizedCodexModel(value) {
|
|
62
117
|
if (typeof value !== 'string') return false;
|
|
63
118
|
const normalized = value.trim().toLowerCase();
|
|
@@ -97,7 +152,12 @@ function extractVisibleUserMessage(value) {
|
|
|
97
152
|
return '';
|
|
98
153
|
}
|
|
99
154
|
|
|
100
|
-
if (
|
|
155
|
+
if (
|
|
156
|
+
text.startsWith('# AGENTS.md instructions for ') ||
|
|
157
|
+
text.includes('<environment_context>') ||
|
|
158
|
+
text.startsWith('<subagent_notification>') ||
|
|
159
|
+
text.startsWith('</subagent_notification>')
|
|
160
|
+
) {
|
|
101
161
|
return '';
|
|
102
162
|
}
|
|
103
163
|
|
|
@@ -120,6 +180,40 @@ function extractVisibleUserMessage(value) {
|
|
|
120
180
|
return text;
|
|
121
181
|
}
|
|
122
182
|
|
|
183
|
+
function extractCodexMessageText(content) {
|
|
184
|
+
if (!Array.isArray(content)) {
|
|
185
|
+
return typeof content === 'string' ? content : '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return content
|
|
189
|
+
.map((item) => {
|
|
190
|
+
if (item?.type === 'input_text' || item?.type === 'output_text' || item?.type === 'text') {
|
|
191
|
+
return item.text;
|
|
192
|
+
}
|
|
193
|
+
return '';
|
|
194
|
+
})
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function shouldIncludeCodexTranscriptMessage(entry) {
|
|
200
|
+
const payload = entry?.payload;
|
|
201
|
+
if (entry?.type !== 'response_item' || payload?.type !== 'message') {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const role = String(payload.role || '').trim().toLowerCase();
|
|
206
|
+
if (role !== 'user' && role !== 'assistant') {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (role === 'assistant' && String(payload.phase || '').trim().toLowerCase() === 'commentary') {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
123
217
|
function isInjectedContextContent(value) {
|
|
124
218
|
const text = String(value || '').trim();
|
|
125
219
|
if (!text) {
|
|
@@ -128,145 +222,28 @@ function isInjectedContextContent(value) {
|
|
|
128
222
|
|
|
129
223
|
if (
|
|
130
224
|
text.startsWith('# AGENTS.md instructions for ') ||
|
|
131
|
-
text.startsWith('[DYNAMIC CONTEXT V1]')
|
|
225
|
+
text.startsWith('[DYNAMIC CONTEXT V1]') ||
|
|
226
|
+
text.startsWith('<subagent_notification>') ||
|
|
227
|
+
text.startsWith('</subagent_notification>')
|
|
132
228
|
) {
|
|
133
229
|
return true;
|
|
134
230
|
}
|
|
135
231
|
|
|
136
|
-
return /^<dynamic_context(?:\s|>)/i.test(text)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Import TaskMaster detection functions
|
|
140
|
-
async function detectTaskMasterFolder(projectPath) {
|
|
141
|
-
try {
|
|
142
|
-
const taskMasterPath = path.join(projectPath, '.taskmaster');
|
|
143
|
-
|
|
144
|
-
// Check if .taskmaster directory exists
|
|
145
|
-
try {
|
|
146
|
-
const stats = await fs.stat(taskMasterPath);
|
|
147
|
-
if (!stats.isDirectory()) {
|
|
148
|
-
return {
|
|
149
|
-
hasTaskmaster: false,
|
|
150
|
-
reason: '.taskmaster exists but is not a directory'
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
} catch (error) {
|
|
154
|
-
if (error.code === 'ENOENT') {
|
|
155
|
-
return {
|
|
156
|
-
hasTaskmaster: false,
|
|
157
|
-
reason: '.taskmaster directory not found'
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
throw error;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Check for key TaskMaster files
|
|
164
|
-
const keyFiles = [
|
|
165
|
-
'tasks/tasks.json',
|
|
166
|
-
'config.json'
|
|
167
|
-
];
|
|
168
|
-
|
|
169
|
-
const fileStatus = {};
|
|
170
|
-
let hasEssentialFiles = true;
|
|
171
|
-
|
|
172
|
-
for (const file of keyFiles) {
|
|
173
|
-
const filePath = path.join(taskMasterPath, file);
|
|
174
|
-
try {
|
|
175
|
-
await fs.access(filePath);
|
|
176
|
-
fileStatus[file] = true;
|
|
177
|
-
} catch (error) {
|
|
178
|
-
fileStatus[file] = false;
|
|
179
|
-
if (file === 'tasks/tasks.json') {
|
|
180
|
-
hasEssentialFiles = false;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Parse tasks.json if it exists for metadata
|
|
186
|
-
let taskMetadata = null;
|
|
187
|
-
if (fileStatus['tasks/tasks.json']) {
|
|
188
|
-
try {
|
|
189
|
-
const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
|
|
190
|
-
const tasksContent = await fs.readFile(tasksPath, 'utf8');
|
|
191
|
-
const tasksData = JSON.parse(tasksContent);
|
|
192
|
-
|
|
193
|
-
// Handle both tagged and legacy formats
|
|
194
|
-
let tasks = [];
|
|
195
|
-
if (tasksData.tasks) {
|
|
196
|
-
// Legacy format
|
|
197
|
-
tasks = tasksData.tasks;
|
|
198
|
-
} else {
|
|
199
|
-
// Tagged format - get tasks from all tags
|
|
200
|
-
Object.values(tasksData).forEach(tagData => {
|
|
201
|
-
if (tagData.tasks) {
|
|
202
|
-
tasks = tasks.concat(tagData.tasks);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Calculate task statistics
|
|
208
|
-
const stats = tasks.reduce((acc, task) => {
|
|
209
|
-
acc.total++;
|
|
210
|
-
acc[task.status] = (acc[task.status] || 0) + 1;
|
|
211
|
-
|
|
212
|
-
// Count subtasks
|
|
213
|
-
if (task.subtasks) {
|
|
214
|
-
task.subtasks.forEach(subtask => {
|
|
215
|
-
acc.subtotalTasks++;
|
|
216
|
-
acc.subtasks = acc.subtasks || {};
|
|
217
|
-
acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return acc;
|
|
222
|
-
}, {
|
|
223
|
-
total: 0,
|
|
224
|
-
subtotalTasks: 0,
|
|
225
|
-
pending: 0,
|
|
226
|
-
'in-progress': 0,
|
|
227
|
-
done: 0,
|
|
228
|
-
review: 0,
|
|
229
|
-
deferred: 0,
|
|
230
|
-
cancelled: 0,
|
|
231
|
-
subtasks: {}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
taskMetadata = {
|
|
235
|
-
taskCount: stats.total,
|
|
236
|
-
subtaskCount: stats.subtotalTasks,
|
|
237
|
-
completed: stats.done || 0,
|
|
238
|
-
pending: stats.pending || 0,
|
|
239
|
-
inProgress: stats['in-progress'] || 0,
|
|
240
|
-
review: stats.review || 0,
|
|
241
|
-
completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
|
|
242
|
-
lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
|
|
243
|
-
};
|
|
244
|
-
} catch (parseError) {
|
|
245
|
-
console.warn('Failed to parse tasks.json:', parseError.message);
|
|
246
|
-
taskMetadata = { error: 'Failed to parse tasks.json' };
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
hasTaskmaster: true,
|
|
252
|
-
hasEssentialFiles,
|
|
253
|
-
files: fileStatus,
|
|
254
|
-
metadata: taskMetadata,
|
|
255
|
-
path: taskMasterPath
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
} catch (error) {
|
|
259
|
-
console.error('Error detecting TaskMaster folder:', error);
|
|
260
|
-
return {
|
|
261
|
-
hasTaskmaster: false,
|
|
262
|
-
reason: `Error checking directory: ${error.message}`
|
|
263
|
-
};
|
|
264
|
-
}
|
|
232
|
+
return /^<dynamic_context(?:\s|>)/i.test(text)
|
|
233
|
+
|| text.includes('<environment_context>')
|
|
234
|
+
|| /<subagent_notification(?:\s|>)/i.test(text);
|
|
265
235
|
}
|
|
266
236
|
|
|
267
237
|
// Cache for extracted project directories
|
|
268
238
|
const projectDirectoryCache = new Map();
|
|
239
|
+
const PROJECT_LIST_CACHE_TTL_MS = 15000;
|
|
269
240
|
const PROVIDER_SESSION_LOOKUP_CACHE_TTL_MS = 5000;
|
|
241
|
+
const projectListCache = {
|
|
242
|
+
data: null,
|
|
243
|
+
promise: null,
|
|
244
|
+
expiresAt: 0
|
|
245
|
+
};
|
|
246
|
+
const codexSessionFileCache = new Map();
|
|
270
247
|
const providerSessionLookupCache = {
|
|
271
248
|
codex: { key: null, data: null, promise: null, expiresAt: 0 },
|
|
272
249
|
gemini: { key: null, data: null, promise: null, expiresAt: 0 },
|
|
@@ -343,9 +320,81 @@ async function findFilesRecursively(rootDir, matcher) {
|
|
|
343
320
|
return discoveredFiles;
|
|
344
321
|
}
|
|
345
322
|
|
|
323
|
+
function getCodexSessionsDir() {
|
|
324
|
+
return path.join(os.homedir(), '.codex', 'sessions');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function cacheCodexSessionFilePath(sessionId, filePath) {
|
|
328
|
+
if (!sessionId || !filePath) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
codexSessionFileCache.set(sessionId, filePath);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function findCodexSessionFilePathBySessionIdHint(sessionId, filePaths = []) {
|
|
336
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
337
|
+
if (!normalizedSessionId || !Array.isArray(filePaths) || filePaths.length === 0) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return filePaths.find((filePath) => {
|
|
342
|
+
const basename = path.basename(filePath, '.jsonl');
|
|
343
|
+
return basename === normalizedSessionId || basename.includes(normalizedSessionId);
|
|
344
|
+
}) || null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function resolveCodexSessionFile(sessionId) {
|
|
348
|
+
if (!sessionId) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const cachedPath = codexSessionFileCache.get(sessionId);
|
|
353
|
+
if (cachedPath) {
|
|
354
|
+
try {
|
|
355
|
+
await fs.access(cachedPath);
|
|
356
|
+
return cachedPath;
|
|
357
|
+
} catch (_) {
|
|
358
|
+
codexSessionFileCache.delete(sessionId);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const jsonlFiles = await findFilesRecursively(
|
|
363
|
+
getCodexSessionsDir(),
|
|
364
|
+
(entryName) => entryName.endsWith('.jsonl')
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const hintedFilePath = findCodexSessionFilePathBySessionIdHint(sessionId, jsonlFiles);
|
|
368
|
+
if (hintedFilePath) {
|
|
369
|
+
cacheCodexSessionFilePath(sessionId, hintedFilePath);
|
|
370
|
+
return hintedFilePath;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const filePath of jsonlFiles) {
|
|
374
|
+
try {
|
|
375
|
+
const sessionData = await parseCodexSessionFile(filePath);
|
|
376
|
+
if (sessionData?.id === sessionId) {
|
|
377
|
+
cacheCodexSessionFilePath(sessionId, filePath);
|
|
378
|
+
return filePath;
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function clearProjectListCache() {
|
|
389
|
+
projectListCache.data = null;
|
|
390
|
+
projectListCache.promise = null;
|
|
391
|
+
projectListCache.expiresAt = 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
346
394
|
// Clear cache when needed (called when project files change)
|
|
347
395
|
function clearProjectDirectoryCache() {
|
|
348
396
|
projectDirectoryCache.clear();
|
|
397
|
+
clearProjectListCache();
|
|
349
398
|
}
|
|
350
399
|
|
|
351
400
|
function clearProviderSessionLookupCaches() {
|
|
@@ -357,6 +406,27 @@ function clearProviderSessionLookupCaches() {
|
|
|
357
406
|
}
|
|
358
407
|
}
|
|
359
408
|
|
|
409
|
+
function appendBoundedMessage(buffer, message, maxSize = null) {
|
|
410
|
+
if (!message) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (maxSize === null) {
|
|
415
|
+
buffer.push(message);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (maxSize <= 0) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (buffer.length === maxSize) {
|
|
424
|
+
buffer.shift();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
buffer.push(message);
|
|
428
|
+
}
|
|
429
|
+
|
|
360
430
|
async function getCachedProviderSessionLookup(providerName, cacheKey, buildLookup) {
|
|
361
431
|
const cacheEntry = providerSessionLookupCache[providerName];
|
|
362
432
|
|
|
@@ -393,190 +463,182 @@ async function getCachedProviderSessionLookup(providerName, cacheKey, buildLooku
|
|
|
393
463
|
|
|
394
464
|
// Load project configuration file
|
|
395
465
|
async function loadProjectConfig() {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
466
|
+
const mergedConfig = {};
|
|
467
|
+
|
|
468
|
+
for (const configPath of getProjectConfigPaths()) {
|
|
469
|
+
try {
|
|
470
|
+
const configData = await fs.readFile(configPath, 'utf8');
|
|
471
|
+
Object.assign(mergedConfig, normalizeProjectConfig(JSON.parse(configData)));
|
|
472
|
+
} catch (error) {
|
|
473
|
+
// Return merged config from any readable location.
|
|
474
|
+
}
|
|
403
475
|
}
|
|
476
|
+
|
|
477
|
+
return mergedConfig;
|
|
404
478
|
}
|
|
405
479
|
|
|
406
480
|
// Save project configuration file
|
|
407
481
|
async function saveProjectConfig(config) {
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
482
|
+
const normalizedConfig = normalizeProjectConfig(config);
|
|
483
|
+
const [primaryConfigPath, ...fallbackConfigPaths] = getProjectConfigPaths();
|
|
484
|
+
let lastPermissionError = null;
|
|
485
|
+
|
|
486
|
+
for (const configPath of [primaryConfigPath, ...fallbackConfigPaths]) {
|
|
487
|
+
try {
|
|
488
|
+
await writeProjectConfigFile(configPath, normalizedConfig);
|
|
489
|
+
|
|
490
|
+
if (configPath === primaryConfigPath) {
|
|
491
|
+
for (const fallbackConfigPath of fallbackConfigPaths) {
|
|
492
|
+
await removeProjectConfigFile(fallbackConfigPath);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
clearProjectDirectoryCache();
|
|
497
|
+
return;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (isProjectConfigPermissionError(error)) {
|
|
500
|
+
lastPermissionError = error;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
416
504
|
throw error;
|
|
417
505
|
}
|
|
418
506
|
}
|
|
419
|
-
|
|
420
|
-
|
|
507
|
+
|
|
508
|
+
if (lastPermissionError) {
|
|
509
|
+
const saveError = new Error('Unable to save the project list because both Claude config storage and the app data directory are not writable.');
|
|
510
|
+
saveError.code = lastPermissionError.code;
|
|
511
|
+
saveError.cause = lastPermissionError;
|
|
512
|
+
throw saveError;
|
|
513
|
+
}
|
|
421
514
|
}
|
|
422
515
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// Try to read package.json from the project path
|
|
516
|
+
function decodeProjectNameFallback(projectName) {
|
|
517
|
+
return String(projectName || '').replace(/-/g, '/');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function readPackageDisplayName(projectPath) {
|
|
429
521
|
try {
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
522
|
+
const raw = await fs.readFile(path.join(projectPath, 'package.json'), 'utf8');
|
|
523
|
+
const parsed = JSON.parse(raw);
|
|
524
|
+
return trimText(parsed?.name) || null;
|
|
525
|
+
} catch {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function lastPathSegment(projectPath) {
|
|
531
|
+
const normalized = path.normalize(projectPath);
|
|
532
|
+
const segment = path.basename(normalized);
|
|
533
|
+
return segment || normalized;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function generateDisplayName(projectName, actualProjectDir = null) {
|
|
537
|
+
const preferredPath = actualProjectDir || decodeProjectNameFallback(projectName);
|
|
538
|
+
return (await readPackageDisplayName(preferredPath)) || lastPathSegment(preferredPath);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function summarizeProjectDirectoryHistory(projectDirectoryPath) {
|
|
542
|
+
const cwdCounts = new Map();
|
|
543
|
+
let latestSeen = { cwd: null, timestamp: 0 };
|
|
544
|
+
const files = (await fs.readdir(projectDirectoryPath))
|
|
545
|
+
.filter((fileName) => fileName.endsWith('.jsonl') && !fileName.startsWith('agent-'));
|
|
546
|
+
|
|
547
|
+
for (const fileName of files) {
|
|
548
|
+
const stream = fsSync.createReadStream(path.join(projectDirectoryPath, fileName));
|
|
549
|
+
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
550
|
+
|
|
551
|
+
for await (const line of lines) {
|
|
552
|
+
if (!line.trim()) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const entry = JSON.parse(line);
|
|
558
|
+
const cwd = trimText(entry?.cwd);
|
|
559
|
+
if (!cwd) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
cwdCounts.set(cwd, (cwdCounts.get(cwd) || 0) + 1);
|
|
564
|
+
const timestamp = Date.parse(entry?.timestamp || '') || 0;
|
|
565
|
+
if (timestamp >= latestSeen.timestamp) {
|
|
566
|
+
latestSeen = { cwd, timestamp };
|
|
567
|
+
}
|
|
568
|
+
} catch {
|
|
569
|
+
}
|
|
437
570
|
}
|
|
438
|
-
} catch (error) {
|
|
439
|
-
// Fall back to path-based naming if package.json doesn't exist or can't be read
|
|
440
571
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
572
|
+
|
|
573
|
+
return { cwdCounts, latestSeen };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function chooseProjectDirectory(projectName, cwdCounts, latestSeen) {
|
|
577
|
+
if (cwdCounts.size === 0) {
|
|
578
|
+
return decodeProjectNameFallback(projectName);
|
|
447
579
|
}
|
|
448
|
-
|
|
449
|
-
|
|
580
|
+
|
|
581
|
+
if (cwdCounts.size === 1) {
|
|
582
|
+
return Array.from(cwdCounts.keys())[0];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let topPath = null;
|
|
586
|
+
let topCount = -1;
|
|
587
|
+
for (const [cwd, count] of cwdCounts.entries()) {
|
|
588
|
+
if (count > topCount) {
|
|
589
|
+
topPath = cwd;
|
|
590
|
+
topCount = count;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (latestSeen.cwd && (cwdCounts.get(latestSeen.cwd) || 0) >= Math.max(1, Math.floor(topCount / 4))) {
|
|
595
|
+
return latestSeen.cwd;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return topPath || decodeProjectNameFallback(projectName);
|
|
450
599
|
}
|
|
451
600
|
|
|
452
|
-
// Extract the actual project directory from JSONL sessions (with caching)
|
|
453
601
|
async function extractProjectDirectory(projectName) {
|
|
454
|
-
// Check cache first
|
|
455
602
|
if (projectDirectoryCache.has(projectName)) {
|
|
456
603
|
return projectDirectoryCache.get(projectName);
|
|
457
604
|
}
|
|
458
605
|
|
|
459
|
-
// Check project config for originalPath (manually added projects via UI or platform)
|
|
460
|
-
// This handles projects with dashes in their directory names correctly
|
|
461
606
|
const config = await loadProjectConfig();
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
projectDirectoryCache.set(projectName,
|
|
465
|
-
return
|
|
607
|
+
const configuredPath = trimText(config?.[projectName]?.originalPath);
|
|
608
|
+
if (configuredPath) {
|
|
609
|
+
projectDirectoryCache.set(projectName, configuredPath);
|
|
610
|
+
return configuredPath;
|
|
466
611
|
}
|
|
467
612
|
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
let latestTimestamp = 0;
|
|
471
|
-
let latestCwd = null;
|
|
472
|
-
let extractedPath;
|
|
473
|
-
|
|
613
|
+
const projectDirectoryPath = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
614
|
+
|
|
474
615
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
|
|
480
|
-
|
|
481
|
-
if (jsonlFiles.length === 0) {
|
|
482
|
-
// Fall back to decoded project name if no sessions
|
|
483
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
484
|
-
} else {
|
|
485
|
-
// Process all JSONL files to collect cwd values
|
|
486
|
-
for (const file of jsonlFiles) {
|
|
487
|
-
const jsonlFile = path.join(projectDir, file);
|
|
488
|
-
const fileStream = fsSync.createReadStream(jsonlFile);
|
|
489
|
-
const rl = readline.createInterface({
|
|
490
|
-
input: fileStream,
|
|
491
|
-
crlfDelay: Infinity
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
for await (const line of rl) {
|
|
495
|
-
if (line.trim()) {
|
|
496
|
-
try {
|
|
497
|
-
const entry = JSON.parse(line);
|
|
498
|
-
|
|
499
|
-
if (entry.cwd) {
|
|
500
|
-
// Count occurrences of each cwd
|
|
501
|
-
cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
|
|
502
|
-
|
|
503
|
-
// Track the most recent cwd
|
|
504
|
-
const timestamp = new Date(entry.timestamp || 0).getTime();
|
|
505
|
-
if (timestamp > latestTimestamp) {
|
|
506
|
-
latestTimestamp = timestamp;
|
|
507
|
-
latestCwd = entry.cwd;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
} catch (parseError) {
|
|
511
|
-
// Skip malformed lines
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Determine the best cwd to use
|
|
518
|
-
if (cwdCounts.size === 0) {
|
|
519
|
-
// No cwd found, fall back to decoded project name
|
|
520
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
521
|
-
} else if (cwdCounts.size === 1) {
|
|
522
|
-
// Only one cwd, use it
|
|
523
|
-
extractedPath = Array.from(cwdCounts.keys())[0];
|
|
524
|
-
} else {
|
|
525
|
-
// Multiple cwd values - prefer the most recent one if it has reasonable usage
|
|
526
|
-
const mostRecentCount = cwdCounts.get(latestCwd) || 0;
|
|
527
|
-
const maxCount = Math.max(...cwdCounts.values());
|
|
528
|
-
|
|
529
|
-
// Use most recent if it has at least 25% of the max count
|
|
530
|
-
if (mostRecentCount >= maxCount * 0.25) {
|
|
531
|
-
extractedPath = latestCwd;
|
|
532
|
-
} else {
|
|
533
|
-
// Otherwise use the most frequently used cwd
|
|
534
|
-
for (const [cwd, count] of cwdCounts.entries()) {
|
|
535
|
-
if (count === maxCount) {
|
|
536
|
-
extractedPath = cwd;
|
|
537
|
-
break;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Fallback (shouldn't reach here)
|
|
543
|
-
if (!extractedPath) {
|
|
544
|
-
extractedPath = latestCwd || projectName.replace(/-/g, '/');
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Cache the result
|
|
550
|
-
projectDirectoryCache.set(projectName, extractedPath);
|
|
551
|
-
|
|
552
|
-
return extractedPath;
|
|
553
|
-
|
|
616
|
+
const { cwdCounts, latestSeen } = await summarizeProjectDirectoryHistory(projectDirectoryPath);
|
|
617
|
+
const resolvedPath = chooseProjectDirectory(projectName, cwdCounts, latestSeen);
|
|
618
|
+
projectDirectoryCache.set(projectName, resolvedPath);
|
|
619
|
+
return resolvedPath;
|
|
554
620
|
} catch (error) {
|
|
555
|
-
|
|
556
|
-
if (error.code === 'ENOENT') {
|
|
557
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
558
|
-
} else {
|
|
621
|
+
if (error.code !== 'ENOENT') {
|
|
559
622
|
console.error(`Error extracting project directory for ${projectName}:`, error);
|
|
560
|
-
// Fall back to decoded project name for other errors
|
|
561
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
562
623
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
projectDirectoryCache.set(projectName,
|
|
566
|
-
|
|
567
|
-
return extractedPath;
|
|
624
|
+
|
|
625
|
+
const fallbackPath = decodeProjectNameFallback(projectName);
|
|
626
|
+
projectDirectoryCache.set(projectName, fallbackPath);
|
|
627
|
+
return fallbackPath;
|
|
568
628
|
}
|
|
569
629
|
}
|
|
570
630
|
|
|
571
|
-
|
|
631
|
+
function cloneProjectList(projects = []) {
|
|
632
|
+
return projects.map((project) => ({ ...project }));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function collectProjectDefinitions(progressCallback = null) {
|
|
572
636
|
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
573
637
|
const config = await loadProjectConfig();
|
|
574
|
-
const projects = [];
|
|
575
638
|
const existingProjects = new Set();
|
|
576
639
|
const projectDefinitions = [];
|
|
577
640
|
let totalProjects = 0;
|
|
578
641
|
let processedProjects = 0;
|
|
579
|
-
let directories = [];
|
|
580
642
|
|
|
581
643
|
try {
|
|
582
644
|
// Check if the .claude/projects directory exists
|
|
@@ -584,7 +646,7 @@ async function getProjects(progressCallback = null) {
|
|
|
584
646
|
|
|
585
647
|
// First, get existing Claude projects from the file system
|
|
586
648
|
const entries = await fs.readdir(claudeDir, { withFileTypes: true });
|
|
587
|
-
directories = entries.filter(e => e.isDirectory());
|
|
649
|
+
const directories = entries.filter(e => e.isDirectory());
|
|
588
650
|
|
|
589
651
|
// Build set of existing project names for later
|
|
590
652
|
directories.forEach(e => existingProjects.add(e.name));
|
|
@@ -675,367 +737,507 @@ async function getProjects(progressCallback = null) {
|
|
|
675
737
|
}
|
|
676
738
|
}
|
|
677
739
|
|
|
740
|
+
return {
|
|
741
|
+
projectDefinitions,
|
|
742
|
+
totalProjects
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function getProjectsList(progressCallback = null) {
|
|
747
|
+
if (
|
|
748
|
+
projectListCache.data &&
|
|
749
|
+
projectListCache.expiresAt > Date.now()
|
|
750
|
+
) {
|
|
751
|
+
const cachedProjects = cloneProjectList(projectListCache.data);
|
|
752
|
+
if (progressCallback) {
|
|
753
|
+
progressCallback({
|
|
754
|
+
phase: 'complete',
|
|
755
|
+
current: cachedProjects.length,
|
|
756
|
+
total: cachedProjects.length
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
return cachedProjects;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!progressCallback && projectListCache.promise) {
|
|
763
|
+
return cloneProjectList(await projectListCache.promise);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const loadProjectsList = async () => {
|
|
767
|
+
const { projectDefinitions, totalProjects } = await collectProjectDefinitions(progressCallback);
|
|
768
|
+
const lightweightProjects = projectDefinitions.map((definition) => ({
|
|
769
|
+
name: definition.name,
|
|
770
|
+
path: definition.path,
|
|
771
|
+
displayName: definition.displayName,
|
|
772
|
+
fullPath: definition.fullPath,
|
|
773
|
+
isCustomName: definition.isCustomName,
|
|
774
|
+
isManuallyAdded: !!definition.isManuallyAdded
|
|
775
|
+
}));
|
|
776
|
+
|
|
777
|
+
projectListCache.data = lightweightProjects;
|
|
778
|
+
projectListCache.expiresAt = Date.now() + PROJECT_LIST_CACHE_TTL_MS;
|
|
779
|
+
|
|
780
|
+
if (progressCallback) {
|
|
781
|
+
progressCallback({
|
|
782
|
+
phase: 'complete',
|
|
783
|
+
current: totalProjects,
|
|
784
|
+
total: totalProjects
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return lightweightProjects;
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
if (progressCallback) {
|
|
792
|
+
return loadProjectsList();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
projectListCache.promise = loadProjectsList().finally(() => {
|
|
796
|
+
projectListCache.promise = null;
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
return cloneProjectList(await projectListCache.promise);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function buildProjectFromDefinition(definition, providerLookups = null) {
|
|
803
|
+
const normalizedProjectPath = normalizeComparableProjectPath(definition.fullPath);
|
|
804
|
+
const project = {
|
|
805
|
+
name: definition.name,
|
|
806
|
+
path: definition.path,
|
|
807
|
+
displayName: definition.displayName,
|
|
808
|
+
fullPath: definition.fullPath,
|
|
809
|
+
isCustomName: definition.isCustomName,
|
|
810
|
+
isManuallyAdded: !!definition.isManuallyAdded,
|
|
811
|
+
sessions: [],
|
|
812
|
+
codexSessions: [],
|
|
813
|
+
opencodeSessions: [],
|
|
814
|
+
geminiSessions: [],
|
|
815
|
+
sessionMeta: {
|
|
816
|
+
hasMore: false,
|
|
817
|
+
total: 0
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
let claudeAcpSessions = [];
|
|
823
|
+
let codexAcpSessions = [];
|
|
824
|
+
let opencodeAcpSessions = [];
|
|
825
|
+
let geminiAcpSessions = [];
|
|
826
|
+
|
|
827
|
+
if (providerLookups) {
|
|
828
|
+
claudeAcpSessions = providerLookups.claudeAcpSessionsByProjectPath?.get(normalizedProjectPath) || [];
|
|
829
|
+
codexAcpSessions = providerLookups.codexAcpSessionsByProjectPath?.get(normalizedProjectPath) || [];
|
|
830
|
+
opencodeAcpSessions = providerLookups.opencodeAcpSessionsByProjectPath?.get(normalizedProjectPath) || [];
|
|
831
|
+
geminiAcpSessions = providerLookups.geminiAcpSessionsByProjectPath?.get(normalizedProjectPath) || [];
|
|
832
|
+
|
|
833
|
+
project.codexSessions = mergeSessionLists(
|
|
834
|
+
providerLookups.codexSessionsByProjectPath?.get(normalizedProjectPath) || [],
|
|
835
|
+
codexAcpSessions,
|
|
836
|
+
{ fallbackProvider: 'codex' }
|
|
837
|
+
);
|
|
838
|
+
project.opencodeSessions = mergeSessionLists(
|
|
839
|
+
providerLookups.opencodeSessionsByProjectPath?.get(normalizedProjectPath) || [],
|
|
840
|
+
opencodeAcpSessions,
|
|
841
|
+
{ fallbackProvider: 'opencode' }
|
|
842
|
+
);
|
|
843
|
+
project.geminiSessions = mergeSessionLists(
|
|
844
|
+
providerLookups.geminiSessionsByProjectPath?.get(normalizedProjectPath) || [],
|
|
845
|
+
geminiAcpSessions,
|
|
846
|
+
{ fallbackProvider: 'gemini' }
|
|
847
|
+
);
|
|
848
|
+
} else {
|
|
849
|
+
const [codexSessions, opencodeSessions, geminiSessions, acpSessions] = await Promise.all([
|
|
850
|
+
getCodexSessions(definition.fullPath, { limit: 5 }),
|
|
851
|
+
getOpencodeSessions(definition.fullPath, { limit: 5 }),
|
|
852
|
+
getGeminiSessions(definition.fullPath, { limit: 5 }),
|
|
853
|
+
listAcpSessions({ projectPath: definition.fullPath })
|
|
854
|
+
]);
|
|
855
|
+
claudeAcpSessions = acpSessions.filter((session) => session.provider === 'claude');
|
|
856
|
+
codexAcpSessions = acpSessions.filter((session) => session.provider === 'codex');
|
|
857
|
+
opencodeAcpSessions = acpSessions.filter((session) => session.provider === 'opencode');
|
|
858
|
+
geminiAcpSessions = acpSessions.filter((session) => session.provider === 'gemini');
|
|
859
|
+
project.codexSessions = mergeSessionLists(codexSessions, codexAcpSessions, { fallbackProvider: 'codex' });
|
|
860
|
+
project.opencodeSessions = mergeSessionLists(opencodeSessions, opencodeAcpSessions, { fallbackProvider: 'opencode' });
|
|
861
|
+
project.geminiSessions = mergeSessionLists(geminiSessions, geminiAcpSessions, { fallbackProvider: 'gemini' });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (!definition.isManuallyAdded) {
|
|
865
|
+
const sessionResult = await getSessions(definition.name, 5, 0);
|
|
866
|
+
project.sessions = mergeSessionLists(
|
|
867
|
+
(sessionResult.sessions || []).map((session) => ({
|
|
868
|
+
...session,
|
|
869
|
+
provider: 'claude',
|
|
870
|
+
source: session?.source || 'legacy'
|
|
871
|
+
})),
|
|
872
|
+
claudeAcpSessions,
|
|
873
|
+
{ fallbackProvider: 'claude' }
|
|
874
|
+
);
|
|
875
|
+
project.sessionMeta = {
|
|
876
|
+
hasMore: sessionResult.hasMore,
|
|
877
|
+
total: sessionResult.total
|
|
878
|
+
};
|
|
879
|
+
} else {
|
|
880
|
+
project.sessions = mergeSessionLists([], claudeAcpSessions, { fallbackProvider: 'claude' });
|
|
881
|
+
}
|
|
882
|
+
} catch (error) {
|
|
883
|
+
console.warn(`Could not load session details for project ${definition.name}:`, error.message);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return project;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function getProjectDetails(projectName) {
|
|
890
|
+
const projectList = await getProjectsList();
|
|
891
|
+
const definition = projectList.find((project) => project.name === projectName);
|
|
892
|
+
|
|
893
|
+
if (!definition) {
|
|
894
|
+
throw new Error(`Project not found: ${projectName}`);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return buildProjectFromDefinition(definition);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function getProjects(progressCallback = null) {
|
|
901
|
+
const projects = [];
|
|
902
|
+
const { projectDefinitions, totalProjects } = await collectProjectDefinitions(progressCallback);
|
|
903
|
+
|
|
678
904
|
const uniqueProjectPaths = Array.from(new Set(
|
|
679
905
|
projectDefinitions
|
|
680
906
|
.map((definition) => normalizeComparableProjectPath(definition.fullPath))
|
|
681
907
|
.filter(Boolean)
|
|
682
908
|
));
|
|
683
909
|
|
|
684
|
-
const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath] = await Promise.all([
|
|
910
|
+
const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath, claudeAcpSessionsByProjectPath, codexAcpSessionsByProjectPath, geminiAcpSessionsByProjectPath, opencodeAcpSessionsByProjectPath] = await Promise.all([
|
|
685
911
|
buildCodexSessionsLookup(uniqueProjectPaths, { limit: 5 }),
|
|
686
912
|
buildGeminiSessionsLookup(uniqueProjectPaths, { limit: 5 }),
|
|
687
|
-
buildOpencodeSessionsLookup(uniqueProjectPaths, { limit: 5 })
|
|
913
|
+
buildOpencodeSessionsLookup(uniqueProjectPaths, { limit: 5 }),
|
|
914
|
+
buildAcpProviderSessionsLookup('claude', uniqueProjectPaths, { limit: 5 }),
|
|
915
|
+
buildAcpProviderSessionsLookup('codex', uniqueProjectPaths, { limit: 5 }),
|
|
916
|
+
buildAcpProviderSessionsLookup('gemini', uniqueProjectPaths, { limit: 5 }),
|
|
917
|
+
buildAcpProviderSessionsLookup('opencode', uniqueProjectPaths, { limit: 5 })
|
|
688
918
|
]);
|
|
689
919
|
|
|
690
920
|
for (const definition of projectDefinitions) {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
opencodeSessions: opencodeSessionsByProjectPath.get(normalizedProjectPath) || [],
|
|
702
|
-
geminiSessions: geminiSessionsByProjectPath.get(normalizedProjectPath) || []
|
|
703
|
-
};
|
|
921
|
+
projects.push(await buildProjectFromDefinition(definition, {
|
|
922
|
+
claudeAcpSessionsByProjectPath,
|
|
923
|
+
codexSessionsByProjectPath,
|
|
924
|
+
codexAcpSessionsByProjectPath,
|
|
925
|
+
geminiSessionsByProjectPath,
|
|
926
|
+
geminiAcpSessionsByProjectPath,
|
|
927
|
+
opencodeSessionsByProjectPath,
|
|
928
|
+
opencodeAcpSessionsByProjectPath
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
704
931
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
932
|
+
// Emit completion after all projects (including manual) are processed
|
|
933
|
+
if (progressCallback) {
|
|
934
|
+
progressCallback({
|
|
935
|
+
phase: 'complete',
|
|
936
|
+
current: totalProjects,
|
|
937
|
+
total: totalProjects
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return projects;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function createClaudeSessionRecord(sessionId) {
|
|
945
|
+
return {
|
|
946
|
+
id: sessionId,
|
|
947
|
+
summary: 'New Session',
|
|
948
|
+
messageCount: 0,
|
|
949
|
+
lastActivity: new Date(0),
|
|
950
|
+
cwd: '',
|
|
951
|
+
lastUserMessage: null,
|
|
952
|
+
lastAssistantMessage: null,
|
|
953
|
+
rootMessageId: null
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function truncateSummary(text, maxLength = 50) {
|
|
958
|
+
const normalized = trimText(text);
|
|
959
|
+
if (!normalized) {
|
|
960
|
+
return 'New Session';
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function extractClaudeTextContent(content) {
|
|
967
|
+
if (typeof content === 'string') {
|
|
968
|
+
return content;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (Array.isArray(content)) {
|
|
972
|
+
return content
|
|
973
|
+
.filter((part) => part?.type === 'text' && typeof part?.text === 'string')
|
|
974
|
+
.map((part) => part.text)
|
|
975
|
+
.join('\n')
|
|
976
|
+
.trim();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return '';
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function isIgnoredClaudePrompt(text) {
|
|
983
|
+
return text.startsWith('<command-name>')
|
|
984
|
+
|| text.startsWith('<command-message>')
|
|
985
|
+
|| text.startsWith('<command-args>')
|
|
986
|
+
|| text.startsWith('<local-command-stdout>')
|
|
987
|
+
|| text.startsWith('<system-reminder>')
|
|
988
|
+
|| text.startsWith('Caveat:')
|
|
989
|
+
|| text.startsWith('This session is being continued from a previous')
|
|
990
|
+
|| text.startsWith('Invalid API key')
|
|
991
|
+
|| text.includes('{"subtasks":')
|
|
992
|
+
|| text.includes('CRITICAL: You MUST respond with ONLY a JSON')
|
|
993
|
+
|| text === 'Warmup';
|
|
994
|
+
}
|
|
717
995
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
|
737
|
-
};
|
|
738
|
-
}
|
|
996
|
+
function mergeClaudeSession(target, incoming) {
|
|
997
|
+
if (incoming.messageCount > target.messageCount) {
|
|
998
|
+
target.messageCount = incoming.messageCount;
|
|
999
|
+
}
|
|
739
1000
|
|
|
740
|
-
|
|
1001
|
+
if (incoming.summary !== 'New Session' && (target.summary === 'New Session' || incoming.lastActivity > target.lastActivity)) {
|
|
1002
|
+
target.summary = incoming.summary;
|
|
741
1003
|
}
|
|
742
1004
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
current: totalProjects,
|
|
748
|
-
total: totalProjects
|
|
749
|
-
});
|
|
1005
|
+
if (incoming.lastActivity > target.lastActivity) {
|
|
1006
|
+
target.lastActivity = incoming.lastActivity;
|
|
1007
|
+
target.lastUserMessage = incoming.lastUserMessage || target.lastUserMessage;
|
|
1008
|
+
target.lastAssistantMessage = incoming.lastAssistantMessage || target.lastAssistantMessage;
|
|
750
1009
|
}
|
|
751
1010
|
|
|
752
|
-
|
|
1011
|
+
if (!target.cwd && incoming.cwd) {
|
|
1012
|
+
target.cwd = incoming.cwd;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (!target.rootMessageId && incoming.rootMessageId) {
|
|
1016
|
+
target.rootMessageId = incoming.rootMessageId;
|
|
1017
|
+
}
|
|
753
1018
|
}
|
|
754
1019
|
|
|
755
1020
|
async function getSessions(projectName, limit = 5, offset = 0) {
|
|
756
1021
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
757
1022
|
|
|
758
1023
|
try {
|
|
759
|
-
const files = await fs.readdir(projectDir)
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (jsonlFiles.length === 0) {
|
|
765
|
-
return { sessions: [], hasMore: false, total: 0 };
|
|
1024
|
+
const files = (await fs.readdir(projectDir))
|
|
1025
|
+
.filter((fileName) => fileName.endsWith('.jsonl') && !fileName.startsWith('agent-'));
|
|
1026
|
+
|
|
1027
|
+
if (files.length === 0) {
|
|
1028
|
+
return { sessions: [], hasMore: false, total: 0, offset, limit };
|
|
766
1029
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const jsonlFile = path.join(projectDir, file);
|
|
785
|
-
const result = await parseJsonlSessions(jsonlFile);
|
|
786
|
-
|
|
787
|
-
result.sessions.forEach(session => {
|
|
788
|
-
if (!allSessions.has(session.id)) {
|
|
789
|
-
allSessions.set(session.id, session);
|
|
1030
|
+
|
|
1031
|
+
const filesByRecency = await Promise.all(files.map(async (fileName) => {
|
|
1032
|
+
const filePath = path.join(projectDir, fileName);
|
|
1033
|
+
const stat = await fs.stat(filePath);
|
|
1034
|
+
return { filePath, modifiedAt: stat.mtime.getTime() };
|
|
1035
|
+
}));
|
|
1036
|
+
filesByRecency.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
1037
|
+
|
|
1038
|
+
const sessionsById = new Map();
|
|
1039
|
+
|
|
1040
|
+
for (const { filePath } of filesByRecency) {
|
|
1041
|
+
const parsed = await parseJsonlSessions(filePath);
|
|
1042
|
+
for (const session of parsed.sessions) {
|
|
1043
|
+
const existing = sessionsById.get(session.id);
|
|
1044
|
+
if (!existing) {
|
|
1045
|
+
sessionsById.set(session.id, session);
|
|
1046
|
+
continue;
|
|
790
1047
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
allEntries.push(...result.entries);
|
|
794
|
-
|
|
795
|
-
// Early exit optimization for large projects
|
|
796
|
-
if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
|
|
797
|
-
break;
|
|
1048
|
+
mergeClaudeSession(existing, session);
|
|
798
1049
|
}
|
|
799
1050
|
}
|
|
800
|
-
|
|
801
|
-
// Build UUID-to-session mapping for timeline detection
|
|
802
|
-
allEntries.forEach(entry => {
|
|
803
|
-
if (entry.uuid && entry.sessionId) {
|
|
804
|
-
uuidToSessionMap.set(entry.uuid, entry.sessionId);
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
// Group sessions by first user message ID
|
|
809
|
-
const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
|
|
810
|
-
const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
|
|
811
|
-
|
|
812
|
-
// Find the first user message for each session
|
|
813
|
-
allEntries.forEach(entry => {
|
|
814
|
-
if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
|
|
815
|
-
// This is a first user message in a session (parentUuid is null)
|
|
816
|
-
const firstUserMsgId = entry.uuid;
|
|
817
|
-
|
|
818
|
-
if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
|
|
819
|
-
sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
|
|
820
|
-
|
|
821
|
-
const session = allSessions.get(entry.sessionId);
|
|
822
|
-
if (session) {
|
|
823
|
-
if (!sessionGroups.has(firstUserMsgId)) {
|
|
824
|
-
sessionGroups.set(firstUserMsgId, {
|
|
825
|
-
latestSession: session,
|
|
826
|
-
allSessions: [session]
|
|
827
|
-
});
|
|
828
|
-
} else {
|
|
829
|
-
const group = sessionGroups.get(firstUserMsgId);
|
|
830
|
-
group.allSessions.push(session);
|
|
831
1051
|
|
|
832
|
-
|
|
833
|
-
if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
|
|
834
|
-
group.latestSession = session;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
});
|
|
1052
|
+
const groupedSessions = new Map();
|
|
841
1053
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1054
|
+
for (const session of sessionsById.values()) {
|
|
1055
|
+
const groupKey = session.rootMessageId || `session:${session.id}`;
|
|
1056
|
+
const currentGroup = groupedSessions.get(groupKey) || [];
|
|
1057
|
+
currentGroup.push(session);
|
|
1058
|
+
groupedSessions.set(groupKey, currentGroup);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const visibleSessions = Array.from(groupedSessions.values()).map((group) => {
|
|
1062
|
+
const ordered = [...group].sort((left, right) => right.lastActivity - left.lastActivity);
|
|
1063
|
+
const primary = { ...ordered[0] };
|
|
847
1064
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
|
|
853
|
-
const session = { ...group.latestSession };
|
|
854
|
-
// Add metadata about grouping
|
|
855
|
-
if (group.allSessions.length > 1) {
|
|
856
|
-
session.isGrouped = true;
|
|
857
|
-
session.groupSize = group.allSessions.length;
|
|
858
|
-
session.groupSessions = group.allSessions.map(s => s.id);
|
|
1065
|
+
if (ordered.length > 1) {
|
|
1066
|
+
primary.isGrouped = true;
|
|
1067
|
+
primary.groupSize = ordered.length;
|
|
1068
|
+
primary.groupSessions = ordered.map((session) => session.id);
|
|
859
1069
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
.filter(session => !session.summary.startsWith('{ "'))
|
|
864
|
-
.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1070
|
+
|
|
1071
|
+
return primary;
|
|
1072
|
+
}).sort((left, right) => right.lastActivity - left.lastActivity);
|
|
865
1073
|
|
|
866
1074
|
const total = visibleSessions.length;
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
1075
|
+
const sessions = visibleSessions.slice(offset, offset + limit);
|
|
1076
|
+
|
|
870
1077
|
return {
|
|
871
|
-
sessions
|
|
872
|
-
hasMore,
|
|
1078
|
+
sessions,
|
|
1079
|
+
hasMore: offset + limit < total,
|
|
873
1080
|
total,
|
|
874
1081
|
offset,
|
|
875
1082
|
limit
|
|
876
1083
|
};
|
|
877
1084
|
} catch (error) {
|
|
878
1085
|
console.error(`Error reading sessions for project ${projectName}:`, error);
|
|
879
|
-
return { sessions: [], hasMore: false, total: 0 };
|
|
1086
|
+
return { sessions: [], hasMore: false, total: 0, offset, limit };
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function getClaudeSessionMetadata(sessionId) {
|
|
1091
|
+
try {
|
|
1092
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
1093
|
+
if (!normalizedSessionId) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
1098
|
+
const jsonlFiles = await findFilesRecursively(
|
|
1099
|
+
claudeProjectsDir,
|
|
1100
|
+
(entryName) => entryName.endsWith('.jsonl') && !entryName.startsWith('agent-')
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
for (const filePath of jsonlFiles) {
|
|
1104
|
+
try {
|
|
1105
|
+
const result = await parseJsonlSessions(filePath);
|
|
1106
|
+
const matchedSession = (result?.sessions || []).find((session) => session?.id === normalizedSessionId);
|
|
1107
|
+
if (matchedSession) {
|
|
1108
|
+
return {
|
|
1109
|
+
...matchedSession,
|
|
1110
|
+
filePath,
|
|
1111
|
+
projectName: path.basename(path.dirname(filePath)),
|
|
1112
|
+
provider: 'claude'
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
} catch {}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
return null;
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (error?.code !== 'ENOENT') {
|
|
1121
|
+
console.error(`Error reading Claude session metadata for ${sessionId}:`, error);
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
880
1124
|
}
|
|
881
1125
|
}
|
|
882
1126
|
|
|
883
1127
|
async function parseJsonlSessions(filePath) {
|
|
884
1128
|
const sessions = new Map();
|
|
885
1129
|
const entries = [];
|
|
886
|
-
const pendingSummaries = new Map();
|
|
1130
|
+
const pendingSummaries = new Map();
|
|
887
1131
|
|
|
888
1132
|
try {
|
|
889
|
-
const
|
|
890
|
-
const
|
|
891
|
-
input: fileStream,
|
|
892
|
-
crlfDelay: Infinity
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
for await (const line of rl) {
|
|
896
|
-
if (line.trim()) {
|
|
897
|
-
try {
|
|
898
|
-
const entry = JSON.parse(line);
|
|
899
|
-
entries.push(entry);
|
|
1133
|
+
const stream = fsSync.createReadStream(filePath);
|
|
1134
|
+
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
900
1135
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (entry.sessionId) {
|
|
907
|
-
if (!sessions.has(entry.sessionId)) {
|
|
908
|
-
sessions.set(entry.sessionId, {
|
|
909
|
-
id: entry.sessionId,
|
|
910
|
-
summary: 'New Session',
|
|
911
|
-
messageCount: 0,
|
|
912
|
-
lastActivity: new Date(),
|
|
913
|
-
cwd: entry.cwd || '',
|
|
914
|
-
lastUserMessage: null,
|
|
915
|
-
lastAssistantMessage: null
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
const session = sessions.get(entry.sessionId);
|
|
1136
|
+
for await (const line of lines) {
|
|
1137
|
+
if (!line.trim()) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
920
1140
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
1141
|
+
try {
|
|
1142
|
+
const entry = JSON.parse(line);
|
|
1143
|
+
entries.push(entry);
|
|
925
1144
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
1145
|
+
if (entry.type === 'summary' && trimText(entry.summary) && !entry.sessionId && trimText(entry.leafUuid)) {
|
|
1146
|
+
pendingSummaries.set(entry.leafUuid, trimText(entry.summary));
|
|
1147
|
+
}
|
|
930
1148
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1149
|
+
const sessionId = trimText(entry.sessionId);
|
|
1150
|
+
if (!sessionId) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
934
1153
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
|
|
938
|
-
textContent = content[0].text;
|
|
939
|
-
}
|
|
1154
|
+
const session = sessions.get(sessionId) || createClaudeSessionRecord(sessionId);
|
|
1155
|
+
sessions.set(sessionId, session);
|
|
940
1156
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
textContent.startsWith('<command-args>') ||
|
|
945
|
-
textContent.startsWith('<local-command-stdout>') ||
|
|
946
|
-
textContent.startsWith('<system-reminder>') ||
|
|
947
|
-
textContent.startsWith('Caveat:') ||
|
|
948
|
-
textContent.startsWith('This session is being continued from a previous') ||
|
|
949
|
-
textContent.startsWith('Invalid API key') ||
|
|
950
|
-
textContent.includes('{"subtasks":') || // Filter Task Master prompts
|
|
951
|
-
textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
|
|
952
|
-
textContent === 'Warmup' // Explicitly filter out "Warmup"
|
|
953
|
-
);
|
|
954
|
-
|
|
955
|
-
if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
|
|
956
|
-
session.lastUserMessage = textContent;
|
|
957
|
-
}
|
|
958
|
-
} else if (entry.message?.role === 'assistant' && entry.message?.content) {
|
|
959
|
-
// Skip API error messages using the isApiErrorMessage flag
|
|
960
|
-
if (entry.isApiErrorMessage === true) {
|
|
961
|
-
// Skip this message entirely
|
|
962
|
-
} else {
|
|
963
|
-
// Track last assistant text message
|
|
964
|
-
let assistantText = null;
|
|
965
|
-
|
|
966
|
-
if (Array.isArray(entry.message.content)) {
|
|
967
|
-
for (const part of entry.message.content) {
|
|
968
|
-
if (part.type === 'text' && part.text) {
|
|
969
|
-
assistantText = part.text;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
} else if (typeof entry.message.content === 'string') {
|
|
973
|
-
assistantText = entry.message.content;
|
|
974
|
-
}
|
|
1157
|
+
if (!session.cwd && trimText(entry.cwd)) {
|
|
1158
|
+
session.cwd = trimText(entry.cwd);
|
|
1159
|
+
}
|
|
975
1160
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
assistantText.includes('{"subtasks":') ||
|
|
980
|
-
assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
|
|
981
|
-
);
|
|
1161
|
+
if (trimText(entry.timestamp)) {
|
|
1162
|
+
session.lastActivity = new Date(entry.timestamp);
|
|
1163
|
+
}
|
|
982
1164
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1165
|
+
if (entry.type === 'summary' && trimText(entry.summary)) {
|
|
1166
|
+
session.summary = trimText(entry.summary);
|
|
1167
|
+
} else if (session.summary === 'New Session' && trimText(entry.parentUuid) && pendingSummaries.has(entry.parentUuid)) {
|
|
1168
|
+
session.summary = pendingSummaries.get(entry.parentUuid);
|
|
1169
|
+
}
|
|
988
1170
|
|
|
989
|
-
|
|
1171
|
+
if (!session.rootMessageId && entry.type === 'user' && entry.parentUuid === null && trimText(entry.uuid)) {
|
|
1172
|
+
session.rootMessageId = trimText(entry.uuid);
|
|
1173
|
+
}
|
|
990
1174
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1175
|
+
if (entry.message?.role === 'user') {
|
|
1176
|
+
const text = extractClaudeTextContent(entry.message.content);
|
|
1177
|
+
if (text && !isIgnoredClaudePrompt(text)) {
|
|
1178
|
+
session.lastUserMessage = text;
|
|
1179
|
+
}
|
|
1180
|
+
} else if (entry.message?.role === 'assistant' && entry.isApiErrorMessage !== true) {
|
|
1181
|
+
const text = extractClaudeTextContent(entry.message.content);
|
|
1182
|
+
if (text && !isIgnoredClaudePrompt(text)) {
|
|
1183
|
+
session.lastAssistantMessage = text;
|
|
994
1184
|
}
|
|
995
|
-
} catch (parseError) {
|
|
996
|
-
// Skip malformed lines silently
|
|
997
1185
|
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
1186
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (session.summary === 'New Session') {
|
|
1004
|
-
// Prefer last user message, fall back to last assistant message
|
|
1005
|
-
const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
|
|
1006
|
-
if (lastMessage) {
|
|
1007
|
-
session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
|
|
1008
|
-
}
|
|
1187
|
+
session.messageCount += 1;
|
|
1188
|
+
} catch {
|
|
1009
1189
|
}
|
|
1010
1190
|
}
|
|
1011
1191
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
if (Math.random() < 0.01) { // Log 1% of sessions
|
|
1020
|
-
}
|
|
1021
|
-
return !shouldFilter;
|
|
1022
|
-
});
|
|
1192
|
+
const normalizedSessions = Array.from(sessions.values())
|
|
1193
|
+
.map((session) => {
|
|
1194
|
+
if (session.summary === 'New Session') {
|
|
1195
|
+
session.summary = truncateSummary(session.lastUserMessage || session.lastAssistantMessage || 'New Session');
|
|
1196
|
+
} else {
|
|
1197
|
+
session.summary = truncateSummary(session.summary, 80);
|
|
1198
|
+
}
|
|
1023
1199
|
|
|
1200
|
+
return session;
|
|
1201
|
+
})
|
|
1202
|
+
.filter((session) => !session.summary.startsWith('{ "'));
|
|
1024
1203
|
|
|
1025
1204
|
return {
|
|
1026
|
-
sessions:
|
|
1027
|
-
entries
|
|
1205
|
+
sessions: normalizedSessions,
|
|
1206
|
+
entries
|
|
1028
1207
|
};
|
|
1029
|
-
|
|
1030
1208
|
} catch (error) {
|
|
1031
1209
|
console.error('Error reading JSONL file:', error);
|
|
1032
1210
|
return { sessions: [], entries: [] };
|
|
1033
1211
|
}
|
|
1034
1212
|
}
|
|
1035
1213
|
|
|
1214
|
+
async function resolveClaudeProjectDir(projectName, sessionId = null) {
|
|
1215
|
+
const defaultProjectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
1216
|
+
|
|
1217
|
+
try {
|
|
1218
|
+
await fs.access(defaultProjectDir);
|
|
1219
|
+
return defaultProjectDir;
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
if (error?.code !== 'ENOENT') {
|
|
1222
|
+
throw error;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (!sessionId) {
|
|
1227
|
+
return defaultProjectDir;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const sessionMetadata = await getClaudeSessionMetadata(sessionId);
|
|
1231
|
+
if (sessionMetadata?.filePath) {
|
|
1232
|
+
return path.dirname(sessionMetadata.filePath);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return defaultProjectDir;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1036
1238
|
// Get messages for a specific session with pagination support
|
|
1037
1239
|
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
|
|
1038
|
-
const projectDir =
|
|
1240
|
+
const projectDir = await resolveClaudeProjectDir(projectName, sessionId);
|
|
1039
1241
|
|
|
1040
1242
|
try {
|
|
1041
1243
|
const files = await fs.readdir(projectDir);
|
|
@@ -1336,7 +1538,7 @@ async function getCodexSessions(projectPath, options = {}) {
|
|
|
1336
1538
|
const sessions = [];
|
|
1337
1539
|
const normalizedProjectPath = normalizeComparableProjectPath(projectPath);
|
|
1338
1540
|
const jsonlFiles = await findFilesRecursively(
|
|
1339
|
-
|
|
1541
|
+
getCodexSessionsDir(),
|
|
1340
1542
|
(entryName) => entryName.endsWith('.jsonl')
|
|
1341
1543
|
);
|
|
1342
1544
|
|
|
@@ -1346,6 +1548,7 @@ async function getCodexSessions(projectPath, options = {}) {
|
|
|
1346
1548
|
const sessionData = await parseCodexSessionFile(filePath);
|
|
1347
1549
|
|
|
1348
1550
|
if (sessionData && normalizeComparableProjectPath(sessionData.cwd) === normalizedProjectPath) {
|
|
1551
|
+
cacheCodexSessionFilePath(sessionData.id, filePath);
|
|
1349
1552
|
sessions.push({
|
|
1350
1553
|
id: sessionData.id,
|
|
1351
1554
|
summary: sessionData.summary || 'Codex Session',
|
|
@@ -1374,15 +1577,17 @@ async function getCodexSessions(projectPath, options = {}) {
|
|
|
1374
1577
|
}
|
|
1375
1578
|
}
|
|
1376
1579
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1580
|
+
function getOpencodeSessionDirCandidates() {
|
|
1581
|
+
return [
|
|
1582
|
+
path.join(os.homedir(), '.opencode', 'sessions'),
|
|
1583
|
+
path.join(os.homedir(), '.config', 'opencode', 'sessions')
|
|
1584
|
+
];
|
|
1585
|
+
}
|
|
1381
1586
|
|
|
1382
1587
|
async function findOpencodeSessionFiles() {
|
|
1383
1588
|
const files = [];
|
|
1384
1589
|
|
|
1385
|
-
for (const baseDir of
|
|
1590
|
+
for (const baseDir of getOpencodeSessionDirCandidates()) {
|
|
1386
1591
|
const discovered = await findFilesRecursively(
|
|
1387
1592
|
baseDir,
|
|
1388
1593
|
(entryName, _fullPath, entry) => entry.isFile() && entryName.endsWith('.jsonl')
|
|
@@ -1647,6 +1852,34 @@ async function findOpencodeSessionFileById(sessionId) {
|
|
|
1647
1852
|
return null;
|
|
1648
1853
|
}
|
|
1649
1854
|
|
|
1855
|
+
async function getOpencodeSessionMetadata(sessionId) {
|
|
1856
|
+
try {
|
|
1857
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
1858
|
+
if (!normalizedSessionId) {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
const sessionFilePath = await findOpencodeSessionFileById(normalizedSessionId);
|
|
1863
|
+
if (!sessionFilePath) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const sessionData = await parseOpencodeSessionFile(sessionFilePath);
|
|
1868
|
+
if (!sessionData) {
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
return {
|
|
1873
|
+
...sessionData,
|
|
1874
|
+
filePath: sessionFilePath,
|
|
1875
|
+
provider: 'opencode'
|
|
1876
|
+
};
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
console.error(`Error reading OpenCode session metadata for ${sessionId}:`, error);
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1650
1883
|
async function getOpencodeSessionMessages(sessionId, limit = null, offset = 0) {
|
|
1651
1884
|
try {
|
|
1652
1885
|
const sessionFilePath = await findOpencodeSessionFileById(sessionId);
|
|
@@ -1857,6 +2090,30 @@ async function parseGeminiSessionFile(filePath) {
|
|
|
1857
2090
|
};
|
|
1858
2091
|
}
|
|
1859
2092
|
|
|
2093
|
+
async function getGeminiSessionMetadata(sessionId) {
|
|
2094
|
+
try {
|
|
2095
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
2096
|
+
if (!normalizedSessionId) {
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
const sessionFiles = await findGeminiSessionFiles();
|
|
2101
|
+
for (const filePath of sessionFiles) {
|
|
2102
|
+
try {
|
|
2103
|
+
const sessionData = await parseGeminiSessionFile(filePath);
|
|
2104
|
+
if (sessionData?.id === normalizedSessionId) {
|
|
2105
|
+
return sessionData;
|
|
2106
|
+
}
|
|
2107
|
+
} catch {}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
return null;
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
console.error(`Error reading Gemini session metadata for ${sessionId}:`, error);
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
1860
2117
|
async function buildCodexSessionsLookup(projectPaths, options = {}) {
|
|
1861
2118
|
const { limit = 5 } = options;
|
|
1862
2119
|
const normalizedProjectPaths = Array.from(new Set(
|
|
@@ -1870,7 +2127,7 @@ async function buildCodexSessionsLookup(projectPaths, options = {}) {
|
|
|
1870
2127
|
const cacheKey = JSON.stringify({ projectPaths: normalizedProjectPaths, limit });
|
|
1871
2128
|
return getCachedProviderSessionLookup('codex', cacheKey, async () => {
|
|
1872
2129
|
const jsonlFiles = await findFilesRecursively(
|
|
1873
|
-
|
|
2130
|
+
getCodexSessionsDir(),
|
|
1874
2131
|
(entryName) => entryName.endsWith('.jsonl')
|
|
1875
2132
|
);
|
|
1876
2133
|
|
|
@@ -1879,6 +2136,7 @@ async function buildCodexSessionsLookup(projectPaths, options = {}) {
|
|
|
1879
2136
|
try {
|
|
1880
2137
|
const sessionData = await parseCodexSessionFile(filePath);
|
|
1881
2138
|
if (sessionData?.id) {
|
|
2139
|
+
cacheCodexSessionFilePath(sessionData.id, filePath);
|
|
1882
2140
|
sessions.push({
|
|
1883
2141
|
id: sessionData.id,
|
|
1884
2142
|
summary: sessionData.summary || 'Codex Session',
|
|
@@ -1899,6 +2157,24 @@ async function buildCodexSessionsLookup(projectPaths, options = {}) {
|
|
|
1899
2157
|
});
|
|
1900
2158
|
}
|
|
1901
2159
|
|
|
2160
|
+
async function buildAcpProviderSessionsLookup(provider, projectPaths, options = {}) {
|
|
2161
|
+
const { limit = 5 } = options;
|
|
2162
|
+
const normalizedProjectPaths = Array.from(new Set(
|
|
2163
|
+
(projectPaths || []).map(normalizeComparableProjectPath).filter(Boolean)
|
|
2164
|
+
));
|
|
2165
|
+
|
|
2166
|
+
if (normalizedProjectPaths.length === 0) {
|
|
2167
|
+
return new Map();
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const sessions = await listAcpSessions({ provider });
|
|
2171
|
+
return groupSessionsByNormalizedProjectPath(
|
|
2172
|
+
normalizedProjectPaths,
|
|
2173
|
+
sessions.map((session) => ({ ...session, provider, source: 'acp' })),
|
|
2174
|
+
limit
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
|
|
1902
2178
|
async function buildOpencodeSessionsLookup(projectPaths, options = {}) {
|
|
1903
2179
|
const { limit = 5 } = options;
|
|
1904
2180
|
const normalizedProjectPaths = Array.from(new Set(
|
|
@@ -1980,19 +2256,8 @@ async function buildGeminiSessionsLookup(projectPaths, options = {}) {
|
|
|
1980
2256
|
|
|
1981
2257
|
async function getGeminiSessionMessages(sessionId, limit = null, offset = 0) {
|
|
1982
2258
|
try {
|
|
1983
|
-
const
|
|
1984
|
-
|
|
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
|
-
}
|
|
2259
|
+
const sessionMetadata = await getGeminiSessionMetadata(sessionId);
|
|
2260
|
+
const sessionFilePath = sessionMetadata?.filePath || null;
|
|
1996
2261
|
|
|
1997
2262
|
if (!sessionFilePath) {
|
|
1998
2263
|
return { messages: [], total: 0, hasMore: false };
|
|
@@ -2132,6 +2397,15 @@ async function parseCodexSessionFile(filePath) {
|
|
|
2132
2397
|
}
|
|
2133
2398
|
}
|
|
2134
2399
|
|
|
2400
|
+
if (shouldIncludeCodexTranscriptMessage(entry) && String(entry.payload.role).trim().toLowerCase() === 'user') {
|
|
2401
|
+
const visibleUserMessage = extractVisibleUserMessage(extractCodexMessageText(entry.payload.content));
|
|
2402
|
+
if (!visibleUserMessage) {
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
messageCount++;
|
|
2406
|
+
lastUserMessage = visibleUserMessage;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2135
2409
|
if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
|
|
2136
2410
|
messageCount++;
|
|
2137
2411
|
}
|
|
@@ -2143,6 +2417,7 @@ async function parseCodexSessionFile(filePath) {
|
|
|
2143
2417
|
}
|
|
2144
2418
|
|
|
2145
2419
|
if (sessionMeta) {
|
|
2420
|
+
cacheCodexSessionFilePath(sessionMeta.id, filePath);
|
|
2146
2421
|
return {
|
|
2147
2422
|
...sessionMeta,
|
|
2148
2423
|
model: lastResolvedModel || sessionMeta.model || null,
|
|
@@ -2165,35 +2440,18 @@ async function parseCodexSessionFile(filePath) {
|
|
|
2165
2440
|
// Get messages for a specific Codex session
|
|
2166
2441
|
async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
2167
2442
|
try {
|
|
2168
|
-
const
|
|
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);
|
|
2443
|
+
const sessionFilePath = await resolveCodexSessionFile(sessionId);
|
|
2190
2444
|
|
|
2191
2445
|
if (!sessionFilePath) {
|
|
2192
2446
|
console.warn(`Codex session file not found for session ${sessionId}`);
|
|
2193
2447
|
return { messages: [], total: 0, hasMore: false };
|
|
2194
2448
|
}
|
|
2195
2449
|
|
|
2450
|
+
const normalizedLimit = limit === null ? null : Math.max(0, Number(limit) || 0);
|
|
2451
|
+
const normalizedOffset = Math.max(0, Number(offset) || 0);
|
|
2452
|
+
const maxBufferedMessages = normalizedLimit === null ? null : normalizedLimit + normalizedOffset;
|
|
2196
2453
|
const messages = [];
|
|
2454
|
+
let total = 0;
|
|
2197
2455
|
let tokenUsage = null;
|
|
2198
2456
|
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
2199
2457
|
const rl = readline.createInterface({
|
|
@@ -2201,21 +2459,9 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2201
2459
|
crlfDelay: Infinity
|
|
2202
2460
|
});
|
|
2203
2461
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
return content
|
|
2208
|
-
.map(item => {
|
|
2209
|
-
if (item.type === 'input_text' || item.type === 'output_text') {
|
|
2210
|
-
return item.text;
|
|
2211
|
-
}
|
|
2212
|
-
if (item.type === 'text') {
|
|
2213
|
-
return item.text;
|
|
2214
|
-
}
|
|
2215
|
-
return '';
|
|
2216
|
-
})
|
|
2217
|
-
.filter(Boolean)
|
|
2218
|
-
.join('\n');
|
|
2462
|
+
const appendMessage = (message) => {
|
|
2463
|
+
total += 1;
|
|
2464
|
+
appendBoundedMessage(messages, message, maxBufferedMessages);
|
|
2219
2465
|
};
|
|
2220
2466
|
|
|
2221
2467
|
for await (const line of rl) {
|
|
@@ -2229,10 +2475,10 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2229
2475
|
}
|
|
2230
2476
|
|
|
2231
2477
|
// Extract messages from response_item
|
|
2232
|
-
if (entry
|
|
2478
|
+
if (shouldIncludeCodexTranscriptMessage(entry)) {
|
|
2233
2479
|
const content = entry.payload.content;
|
|
2234
2480
|
const role = entry.payload.role || 'assistant';
|
|
2235
|
-
const textContent =
|
|
2481
|
+
const textContent = extractCodexMessageText(content);
|
|
2236
2482
|
const visibleTextContent = role === 'user'
|
|
2237
2483
|
? extractVisibleUserMessage(textContent)
|
|
2238
2484
|
: textContent;
|
|
@@ -2243,7 +2489,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2243
2489
|
|
|
2244
2490
|
// Only add if there's actual content
|
|
2245
2491
|
if (visibleTextContent?.trim()) {
|
|
2246
|
-
|
|
2492
|
+
appendMessage({
|
|
2247
2493
|
type: role === 'user' ? 'user' : 'assistant',
|
|
2248
2494
|
timestamp: entry.timestamp,
|
|
2249
2495
|
message: {
|
|
@@ -2260,7 +2506,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2260
2506
|
.filter(Boolean)
|
|
2261
2507
|
.join('\n');
|
|
2262
2508
|
if (summaryText?.trim()) {
|
|
2263
|
-
|
|
2509
|
+
appendMessage({
|
|
2264
2510
|
type: 'thinking',
|
|
2265
2511
|
timestamp: entry.timestamp,
|
|
2266
2512
|
message: {
|
|
@@ -2286,7 +2532,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2286
2532
|
}
|
|
2287
2533
|
}
|
|
2288
2534
|
|
|
2289
|
-
|
|
2535
|
+
appendMessage({
|
|
2290
2536
|
type: 'tool_use',
|
|
2291
2537
|
timestamp: entry.timestamp,
|
|
2292
2538
|
toolName: toolName,
|
|
@@ -2296,7 +2542,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2296
2542
|
}
|
|
2297
2543
|
|
|
2298
2544
|
if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
|
|
2299
|
-
|
|
2545
|
+
appendMessage({
|
|
2300
2546
|
type: 'tool_result',
|
|
2301
2547
|
timestamp: entry.timestamp,
|
|
2302
2548
|
toolCallId: entry.payload.call_id,
|
|
@@ -2326,7 +2572,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2326
2572
|
}
|
|
2327
2573
|
}
|
|
2328
2574
|
|
|
2329
|
-
|
|
2575
|
+
appendMessage({
|
|
2330
2576
|
type: 'tool_use',
|
|
2331
2577
|
timestamp: entry.timestamp,
|
|
2332
2578
|
toolName: 'Edit',
|
|
@@ -2338,7 +2584,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2338
2584
|
toolCallId: entry.payload.call_id
|
|
2339
2585
|
});
|
|
2340
2586
|
} else {
|
|
2341
|
-
|
|
2587
|
+
appendMessage({
|
|
2342
2588
|
type: 'tool_use',
|
|
2343
2589
|
timestamp: entry.timestamp,
|
|
2344
2590
|
toolName: toolName,
|
|
@@ -2349,7 +2595,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2349
2595
|
}
|
|
2350
2596
|
|
|
2351
2597
|
if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
|
|
2352
|
-
|
|
2598
|
+
appendMessage({
|
|
2353
2599
|
type: 'tool_result',
|
|
2354
2600
|
timestamp: entry.timestamp,
|
|
2355
2601
|
toolCallId: entry.payload.call_id,
|
|
@@ -2363,29 +2609,31 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2363
2609
|
}
|
|
2364
2610
|
}
|
|
2365
2611
|
|
|
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
2612
|
// Apply pagination if limit is specified
|
|
2372
|
-
if (
|
|
2373
|
-
const
|
|
2374
|
-
const
|
|
2613
|
+
if (normalizedLimit !== null) {
|
|
2614
|
+
const endIndex = Math.max(0, messages.length - normalizedOffset);
|
|
2615
|
+
const startIndex = Math.max(0, endIndex - normalizedLimit);
|
|
2375
2616
|
const paginatedMessages = messages.slice(startIndex, endIndex);
|
|
2376
|
-
const hasMore =
|
|
2617
|
+
const hasMore = total > normalizedOffset + paginatedMessages.length;
|
|
2377
2618
|
|
|
2378
2619
|
return {
|
|
2379
2620
|
messages: paginatedMessages,
|
|
2380
2621
|
total,
|
|
2381
2622
|
hasMore,
|
|
2382
|
-
offset,
|
|
2383
|
-
limit,
|
|
2623
|
+
offset: normalizedOffset,
|
|
2624
|
+
limit: normalizedLimit,
|
|
2384
2625
|
tokenUsage
|
|
2385
2626
|
};
|
|
2386
2627
|
}
|
|
2387
2628
|
|
|
2388
|
-
return {
|
|
2629
|
+
return {
|
|
2630
|
+
messages,
|
|
2631
|
+
total,
|
|
2632
|
+
hasMore: false,
|
|
2633
|
+
offset: normalizedOffset,
|
|
2634
|
+
limit: normalizedLimit,
|
|
2635
|
+
tokenUsage
|
|
2636
|
+
};
|
|
2389
2637
|
|
|
2390
2638
|
} catch (error) {
|
|
2391
2639
|
console.error(`Error reading Codex session messages for ${sessionId}:`, error);
|
|
@@ -2393,37 +2641,45 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2393
2641
|
}
|
|
2394
2642
|
}
|
|
2395
2643
|
|
|
2396
|
-
async function
|
|
2397
|
-
|
|
2398
|
-
|
|
2644
|
+
async function getCodexSessionMetadata(sessionId) {
|
|
2645
|
+
if (!sessionId) {
|
|
2646
|
+
return null;
|
|
2647
|
+
}
|
|
2399
2648
|
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
files.push(fullPath);
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
} catch (error) {}
|
|
2413
|
-
return files;
|
|
2414
|
-
};
|
|
2649
|
+
const sessionFilePath = await resolveCodexSessionFile(sessionId);
|
|
2650
|
+
if (!sessionFilePath) {
|
|
2651
|
+
return null;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
const sessionData = await parseCodexSessionFile(sessionFilePath);
|
|
2655
|
+
if (!sessionData) {
|
|
2656
|
+
return null;
|
|
2657
|
+
}
|
|
2415
2658
|
|
|
2416
|
-
|
|
2659
|
+
const metadata = {
|
|
2660
|
+
...sessionData,
|
|
2661
|
+
filePath: sessionFilePath,
|
|
2662
|
+
provider: 'codex'
|
|
2663
|
+
};
|
|
2417
2664
|
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2665
|
+
if (metadata.git === undefined) {
|
|
2666
|
+
delete metadata.git;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
return metadata;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
async function deleteCodexSession(sessionId) {
|
|
2673
|
+
try {
|
|
2674
|
+
const sessionFilePath = await resolveCodexSessionFile(sessionId);
|
|
2675
|
+
|
|
2676
|
+
if (!sessionFilePath) {
|
|
2677
|
+
throw new Error(`Codex session file not found for session ${sessionId}`);
|
|
2424
2678
|
}
|
|
2425
2679
|
|
|
2426
|
-
|
|
2680
|
+
await fs.unlink(sessionFilePath);
|
|
2681
|
+
codexSessionFileCache.delete(sessionId);
|
|
2682
|
+
return true;
|
|
2427
2683
|
} catch (error) {
|
|
2428
2684
|
console.error(`Error deleting Codex session ${sessionId}:`, error);
|
|
2429
2685
|
throw error;
|
|
@@ -2432,7 +2688,10 @@ async function deleteCodexSession(sessionId) {
|
|
|
2432
2688
|
|
|
2433
2689
|
export {
|
|
2434
2690
|
getProjects,
|
|
2691
|
+
getProjectsList,
|
|
2692
|
+
getProjectDetails,
|
|
2435
2693
|
getSessions,
|
|
2694
|
+
getClaudeSessionMetadata,
|
|
2436
2695
|
getSessionMessages,
|
|
2437
2696
|
parseJsonlSessions,
|
|
2438
2697
|
renameProject,
|
|
@@ -2445,11 +2704,15 @@ export {
|
|
|
2445
2704
|
extractProjectDirectory,
|
|
2446
2705
|
clearProjectDirectoryCache,
|
|
2447
2706
|
clearProviderSessionLookupCaches,
|
|
2707
|
+
findCodexSessionFilePathBySessionIdHint,
|
|
2448
2708
|
getCodexSessions,
|
|
2449
2709
|
getCodexSessionMessages,
|
|
2710
|
+
getCodexSessionMetadata,
|
|
2450
2711
|
getOpencodeSessions,
|
|
2712
|
+
getOpencodeSessionMetadata,
|
|
2451
2713
|
getOpencodeSessionMessages,
|
|
2452
2714
|
getGeminiSessions,
|
|
2715
|
+
getGeminiSessionMetadata,
|
|
2453
2716
|
getGeminiSessionMessages,
|
|
2454
2717
|
deleteCodexSession,
|
|
2455
2718
|
deleteOpencodeSession
|