@axhub/genie 0.2.9 → 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/dist/api-docs.html +2 -2
- package/dist/assets/App-CYCCsgwf.js +264 -0
- package/dist/assets/{ReviewApp-C9K--AQE.js → ReviewApp-0srHIXwb.js} +1 -1
- package/dist/assets/{_basePickBy-DR_8uFCo.js → _basePickBy-DVVb07UV.js} +1 -1
- package/dist/assets/{_baseUniq-D0njlQ_7.js → _baseUniq-BtbziL5G.js} +1 -1
- package/dist/assets/{arc-CKlr_Rec.js → arc-BsCC8yBD.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-BmO_uLUH.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-DhAeO-56.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-C67kFoXx.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
- package/dist/assets/channel-BMhScXFe.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-mLLagvJi.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Lx-hOjlM.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Bt-XmVUV.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-Cya6gaDV.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-Bd7Ig6tF.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-5UAE0Vg-.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-BAxZ8m7w.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-DjDPvUUP.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-D-60XrkJ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-bqu3ZS4K.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-BueeqoYm.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2-D4fDv2E7.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-WqipY3fN.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-D0oVnO-x.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-DzbGyxrr.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK-BwhbbgCP.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
- package/dist/assets/{graph-DzKos-N0.js → graph-CeJCMjan.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-CKDMgz3X.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-BFicZbTf.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-CtihxDxl.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-Du00J8_d.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-BJi9S0iQ.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
- package/dist/assets/{layout-B80Sityu.js → layout-CI2RM-v6.js} +1 -1
- package/dist/assets/{linear-sRQLOf5H.js → linear-DE7bISck.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-CBuVs4eJ.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
- package/dist/assets/{mindmap-definition-YRQLILUH-C5IL_xi-.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-CeTwlJ8z.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-COfUcLWt.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-DSb-CJ5B.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-8jtuVb45.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-C2VpkMwA.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-fmwMqxxc.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-Dx1hP5lg.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-CkLOdYCZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
- package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
- package/dist/assets/{vennDiagram-LZ73GAT5-D6KWcnln.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-6fh6qmzN.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +6 -5
- package/server/acp-runtime/client.js +120 -14
- package/server/acp-runtime/index.js +54 -0
- package/server/acp-runtime/registry.js +2 -2
- package/server/acp-runtime/session-store.js +75 -1
- package/server/cli.js +32 -8
- package/server/database/db.js +20 -0
- package/server/external-agent/ws.js +477 -24
- package/server/index.js +78 -146
- 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 +423 -535
- 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 +13 -7
- package/server/routes/settings.js +113 -99
- package/server/session-core/eventStore.js +15 -2
- package/server/session-core/providerAdapters.js +28 -28
- package/server/session-core/sessionListMerge.js +47 -0
- package/shared/conversationEvents.js +96 -1
- package/shared/modelConstants.js +79 -99
- package/dist/assets/App-GBcTeeUS.js +0 -460
- package/dist/assets/channel-V3MBjKys.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +0 -1
- package/dist/assets/clone-BbMGfZwt.js +0 -1
- package/dist/assets/index-DiQlHzGj.js +0 -2
- package/dist/assets/index-Drat2nB9.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +0 -1
- package/dist/assets/vendor-codemirror-BxPY6emf.js +0 -39
- 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
|
@@ -54,6 +54,8 @@ import crypto from 'crypto';
|
|
|
54
54
|
import { fileURLToPath } from 'url';
|
|
55
55
|
import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
|
|
56
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';
|
|
57
59
|
|
|
58
60
|
const __filename = fileURLToPath(import.meta.url);
|
|
59
61
|
const __dirname = path.dirname(__filename);
|
|
@@ -64,6 +66,10 @@ const KNOWN_CODEX_MODELS = new Set(
|
|
|
64
66
|
|
|
65
67
|
const PROJECT_CONFIG_PERMISSION_ERROR_CODES = new Set(['EACCES', 'EPERM', 'EROFS']);
|
|
66
68
|
|
|
69
|
+
function trimText(value) {
|
|
70
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
function getPrimaryProjectConfigPath() {
|
|
68
74
|
return path.join(os.homedir(), '.claude', 'project-config.json');
|
|
69
75
|
}
|
|
@@ -174,6 +180,40 @@ function extractVisibleUserMessage(value) {
|
|
|
174
180
|
return text;
|
|
175
181
|
}
|
|
176
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
|
+
|
|
177
217
|
function isInjectedContextContent(value) {
|
|
178
218
|
const text = String(value || '').trim();
|
|
179
219
|
if (!text) {
|
|
@@ -194,134 +234,6 @@ function isInjectedContextContent(value) {
|
|
|
194
234
|
|| /<subagent_notification(?:\s|>)/i.test(text);
|
|
195
235
|
}
|
|
196
236
|
|
|
197
|
-
// Import TaskMaster detection functions
|
|
198
|
-
async function detectTaskMasterFolder(projectPath) {
|
|
199
|
-
try {
|
|
200
|
-
const taskMasterPath = path.join(projectPath, '.taskmaster');
|
|
201
|
-
|
|
202
|
-
// Check if .taskmaster directory exists
|
|
203
|
-
try {
|
|
204
|
-
const stats = await fs.stat(taskMasterPath);
|
|
205
|
-
if (!stats.isDirectory()) {
|
|
206
|
-
return {
|
|
207
|
-
hasTaskmaster: false,
|
|
208
|
-
reason: '.taskmaster exists but is not a directory'
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
} catch (error) {
|
|
212
|
-
if (error.code === 'ENOENT') {
|
|
213
|
-
return {
|
|
214
|
-
hasTaskmaster: false,
|
|
215
|
-
reason: '.taskmaster directory not found'
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Check for key TaskMaster files
|
|
222
|
-
const keyFiles = [
|
|
223
|
-
'tasks/tasks.json',
|
|
224
|
-
'config.json'
|
|
225
|
-
];
|
|
226
|
-
|
|
227
|
-
const fileStatus = {};
|
|
228
|
-
let hasEssentialFiles = true;
|
|
229
|
-
|
|
230
|
-
for (const file of keyFiles) {
|
|
231
|
-
const filePath = path.join(taskMasterPath, file);
|
|
232
|
-
try {
|
|
233
|
-
await fs.access(filePath);
|
|
234
|
-
fileStatus[file] = true;
|
|
235
|
-
} catch (error) {
|
|
236
|
-
fileStatus[file] = false;
|
|
237
|
-
if (file === 'tasks/tasks.json') {
|
|
238
|
-
hasEssentialFiles = false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Parse tasks.json if it exists for metadata
|
|
244
|
-
let taskMetadata = null;
|
|
245
|
-
if (fileStatus['tasks/tasks.json']) {
|
|
246
|
-
try {
|
|
247
|
-
const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
|
|
248
|
-
const tasksContent = await fs.readFile(tasksPath, 'utf8');
|
|
249
|
-
const tasksData = JSON.parse(tasksContent);
|
|
250
|
-
|
|
251
|
-
// Handle both tagged and legacy formats
|
|
252
|
-
let tasks = [];
|
|
253
|
-
if (tasksData.tasks) {
|
|
254
|
-
// Legacy format
|
|
255
|
-
tasks = tasksData.tasks;
|
|
256
|
-
} else {
|
|
257
|
-
// Tagged format - get tasks from all tags
|
|
258
|
-
Object.values(tasksData).forEach(tagData => {
|
|
259
|
-
if (tagData.tasks) {
|
|
260
|
-
tasks = tasks.concat(tagData.tasks);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Calculate task statistics
|
|
266
|
-
const stats = tasks.reduce((acc, task) => {
|
|
267
|
-
acc.total++;
|
|
268
|
-
acc[task.status] = (acc[task.status] || 0) + 1;
|
|
269
|
-
|
|
270
|
-
// Count subtasks
|
|
271
|
-
if (task.subtasks) {
|
|
272
|
-
task.subtasks.forEach(subtask => {
|
|
273
|
-
acc.subtotalTasks++;
|
|
274
|
-
acc.subtasks = acc.subtasks || {};
|
|
275
|
-
acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return acc;
|
|
280
|
-
}, {
|
|
281
|
-
total: 0,
|
|
282
|
-
subtotalTasks: 0,
|
|
283
|
-
pending: 0,
|
|
284
|
-
'in-progress': 0,
|
|
285
|
-
done: 0,
|
|
286
|
-
review: 0,
|
|
287
|
-
deferred: 0,
|
|
288
|
-
cancelled: 0,
|
|
289
|
-
subtasks: {}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
taskMetadata = {
|
|
293
|
-
taskCount: stats.total,
|
|
294
|
-
subtaskCount: stats.subtotalTasks,
|
|
295
|
-
completed: stats.done || 0,
|
|
296
|
-
pending: stats.pending || 0,
|
|
297
|
-
inProgress: stats['in-progress'] || 0,
|
|
298
|
-
review: stats.review || 0,
|
|
299
|
-
completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
|
|
300
|
-
lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
|
|
301
|
-
};
|
|
302
|
-
} catch (parseError) {
|
|
303
|
-
console.warn('Failed to parse tasks.json:', parseError.message);
|
|
304
|
-
taskMetadata = { error: 'Failed to parse tasks.json' };
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
hasTaskmaster: true,
|
|
310
|
-
hasEssentialFiles,
|
|
311
|
-
files: fileStatus,
|
|
312
|
-
metadata: taskMetadata,
|
|
313
|
-
path: taskMasterPath
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
} catch (error) {
|
|
317
|
-
console.error('Error detecting TaskMaster folder:', error);
|
|
318
|
-
return {
|
|
319
|
-
hasTaskmaster: false,
|
|
320
|
-
reason: `Error checking directory: ${error.message}`
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
237
|
// Cache for extracted project directories
|
|
326
238
|
const projectDirectoryCache = new Map();
|
|
327
239
|
const PROJECT_LIST_CACHE_TTL_MS = 15000;
|
|
@@ -601,151 +513,118 @@ async function saveProjectConfig(config) {
|
|
|
601
513
|
}
|
|
602
514
|
}
|
|
603
515
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
// 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) {
|
|
610
521
|
try {
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
+
}
|
|
618
570
|
}
|
|
619
|
-
} catch (error) {
|
|
620
|
-
// Fall back to path-based naming if package.json doesn't exist or can't be read
|
|
621
571
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
572
|
+
|
|
573
|
+
return { cwdCounts, latestSeen };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function chooseProjectDirectory(projectName, cwdCounts, latestSeen) {
|
|
577
|
+
if (cwdCounts.size === 0) {
|
|
578
|
+
return decodeProjectNameFallback(projectName);
|
|
628
579
|
}
|
|
629
|
-
|
|
630
|
-
|
|
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);
|
|
631
599
|
}
|
|
632
600
|
|
|
633
|
-
// Extract the actual project directory from JSONL sessions (with caching)
|
|
634
601
|
async function extractProjectDirectory(projectName) {
|
|
635
|
-
// Check cache first
|
|
636
602
|
if (projectDirectoryCache.has(projectName)) {
|
|
637
603
|
return projectDirectoryCache.get(projectName);
|
|
638
604
|
}
|
|
639
605
|
|
|
640
|
-
// Check project config for originalPath (manually added projects via UI or platform)
|
|
641
|
-
// This handles projects with dashes in their directory names correctly
|
|
642
606
|
const config = await loadProjectConfig();
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
projectDirectoryCache.set(projectName,
|
|
646
|
-
return
|
|
607
|
+
const configuredPath = trimText(config?.[projectName]?.originalPath);
|
|
608
|
+
if (configuredPath) {
|
|
609
|
+
projectDirectoryCache.set(projectName, configuredPath);
|
|
610
|
+
return configuredPath;
|
|
647
611
|
}
|
|
648
612
|
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
let latestTimestamp = 0;
|
|
652
|
-
let latestCwd = null;
|
|
653
|
-
let extractedPath;
|
|
654
|
-
|
|
613
|
+
const projectDirectoryPath = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
614
|
+
|
|
655
615
|
try {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
|
|
661
|
-
|
|
662
|
-
if (jsonlFiles.length === 0) {
|
|
663
|
-
// Fall back to decoded project name if no sessions
|
|
664
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
665
|
-
} else {
|
|
666
|
-
// Process all JSONL files to collect cwd values
|
|
667
|
-
for (const file of jsonlFiles) {
|
|
668
|
-
const jsonlFile = path.join(projectDir, file);
|
|
669
|
-
const fileStream = fsSync.createReadStream(jsonlFile);
|
|
670
|
-
const rl = readline.createInterface({
|
|
671
|
-
input: fileStream,
|
|
672
|
-
crlfDelay: Infinity
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
for await (const line of rl) {
|
|
676
|
-
if (line.trim()) {
|
|
677
|
-
try {
|
|
678
|
-
const entry = JSON.parse(line);
|
|
679
|
-
|
|
680
|
-
if (entry.cwd) {
|
|
681
|
-
// Count occurrences of each cwd
|
|
682
|
-
cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
|
|
683
|
-
|
|
684
|
-
// Track the most recent cwd
|
|
685
|
-
const timestamp = new Date(entry.timestamp || 0).getTime();
|
|
686
|
-
if (timestamp > latestTimestamp) {
|
|
687
|
-
latestTimestamp = timestamp;
|
|
688
|
-
latestCwd = entry.cwd;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
} catch (parseError) {
|
|
692
|
-
// Skip malformed lines
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Determine the best cwd to use
|
|
699
|
-
if (cwdCounts.size === 0) {
|
|
700
|
-
// No cwd found, fall back to decoded project name
|
|
701
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
702
|
-
} else if (cwdCounts.size === 1) {
|
|
703
|
-
// Only one cwd, use it
|
|
704
|
-
extractedPath = Array.from(cwdCounts.keys())[0];
|
|
705
|
-
} else {
|
|
706
|
-
// Multiple cwd values - prefer the most recent one if it has reasonable usage
|
|
707
|
-
const mostRecentCount = cwdCounts.get(latestCwd) || 0;
|
|
708
|
-
const maxCount = Math.max(...cwdCounts.values());
|
|
709
|
-
|
|
710
|
-
// Use most recent if it has at least 25% of the max count
|
|
711
|
-
if (mostRecentCount >= maxCount * 0.25) {
|
|
712
|
-
extractedPath = latestCwd;
|
|
713
|
-
} else {
|
|
714
|
-
// Otherwise use the most frequently used cwd
|
|
715
|
-
for (const [cwd, count] of cwdCounts.entries()) {
|
|
716
|
-
if (count === maxCount) {
|
|
717
|
-
extractedPath = cwd;
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Fallback (shouldn't reach here)
|
|
724
|
-
if (!extractedPath) {
|
|
725
|
-
extractedPath = latestCwd || projectName.replace(/-/g, '/');
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Cache the result
|
|
731
|
-
projectDirectoryCache.set(projectName, extractedPath);
|
|
732
|
-
|
|
733
|
-
return extractedPath;
|
|
734
|
-
|
|
616
|
+
const { cwdCounts, latestSeen } = await summarizeProjectDirectoryHistory(projectDirectoryPath);
|
|
617
|
+
const resolvedPath = chooseProjectDirectory(projectName, cwdCounts, latestSeen);
|
|
618
|
+
projectDirectoryCache.set(projectName, resolvedPath);
|
|
619
|
+
return resolvedPath;
|
|
735
620
|
} catch (error) {
|
|
736
|
-
|
|
737
|
-
if (error.code === 'ENOENT') {
|
|
738
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
739
|
-
} else {
|
|
621
|
+
if (error.code !== 'ENOENT') {
|
|
740
622
|
console.error(`Error extracting project directory for ${projectName}:`, error);
|
|
741
|
-
// Fall back to decoded project name for other errors
|
|
742
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
743
623
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
projectDirectoryCache.set(projectName,
|
|
747
|
-
|
|
748
|
-
return extractedPath;
|
|
624
|
+
|
|
625
|
+
const fallbackPath = decodeProjectNameFallback(projectName);
|
|
626
|
+
projectDirectoryCache.set(projectName, fallbackPath);
|
|
627
|
+
return fallbackPath;
|
|
749
628
|
}
|
|
750
629
|
}
|
|
751
630
|
|
|
@@ -940,55 +819,70 @@ async function buildProjectFromDefinition(definition, providerLookups = null) {
|
|
|
940
819
|
};
|
|
941
820
|
|
|
942
821
|
try {
|
|
822
|
+
let claudeAcpSessions = [];
|
|
823
|
+
let codexAcpSessions = [];
|
|
824
|
+
let opencodeAcpSessions = [];
|
|
825
|
+
let geminiAcpSessions = [];
|
|
826
|
+
|
|
943
827
|
if (providerLookups) {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
+
);
|
|
947
848
|
} else {
|
|
948
|
-
const [codexSessions, opencodeSessions, geminiSessions] = await Promise.all([
|
|
849
|
+
const [codexSessions, opencodeSessions, geminiSessions, acpSessions] = await Promise.all([
|
|
949
850
|
getCodexSessions(definition.fullPath, { limit: 5 }),
|
|
950
851
|
getOpencodeSessions(definition.fullPath, { limit: 5 }),
|
|
951
|
-
getGeminiSessions(definition.fullPath, { limit: 5 })
|
|
852
|
+
getGeminiSessions(definition.fullPath, { limit: 5 }),
|
|
853
|
+
listAcpSessions({ projectPath: definition.fullPath })
|
|
952
854
|
]);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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' });
|
|
956
862
|
}
|
|
957
863
|
|
|
958
864
|
if (!definition.isManuallyAdded) {
|
|
959
865
|
const sessionResult = await getSessions(definition.name, 5, 0);
|
|
960
|
-
project.sessions =
|
|
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
|
+
);
|
|
961
875
|
project.sessionMeta = {
|
|
962
876
|
hasMore: sessionResult.hasMore,
|
|
963
877
|
total: sessionResult.total
|
|
964
878
|
};
|
|
879
|
+
} else {
|
|
880
|
+
project.sessions = mergeSessionLists([], claudeAcpSessions, { fallbackProvider: 'claude' });
|
|
965
881
|
}
|
|
966
882
|
} catch (error) {
|
|
967
883
|
console.warn(`Could not load session details for project ${definition.name}:`, error.message);
|
|
968
884
|
}
|
|
969
885
|
|
|
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
886
|
return project;
|
|
993
887
|
}
|
|
994
888
|
|
|
@@ -1013,17 +907,25 @@ async function getProjects(progressCallback = null) {
|
|
|
1013
907
|
.filter(Boolean)
|
|
1014
908
|
));
|
|
1015
909
|
|
|
1016
|
-
const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath] = await Promise.all([
|
|
910
|
+
const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath, claudeAcpSessionsByProjectPath, codexAcpSessionsByProjectPath, geminiAcpSessionsByProjectPath, opencodeAcpSessionsByProjectPath] = await Promise.all([
|
|
1017
911
|
buildCodexSessionsLookup(uniqueProjectPaths, { limit: 5 }),
|
|
1018
912
|
buildGeminiSessionsLookup(uniqueProjectPaths, { limit: 5 }),
|
|
1019
|
-
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 })
|
|
1020
918
|
]);
|
|
1021
919
|
|
|
1022
920
|
for (const definition of projectDefinitions) {
|
|
1023
921
|
projects.push(await buildProjectFromDefinition(definition, {
|
|
922
|
+
claudeAcpSessionsByProjectPath,
|
|
1024
923
|
codexSessionsByProjectPath,
|
|
924
|
+
codexAcpSessionsByProjectPath,
|
|
925
|
+
geminiSessionsByProjectPath,
|
|
926
|
+
geminiAcpSessionsByProjectPath,
|
|
1025
927
|
opencodeSessionsByProjectPath,
|
|
1026
|
-
|
|
928
|
+
opencodeAcpSessionsByProjectPath
|
|
1027
929
|
}));
|
|
1028
930
|
}
|
|
1029
931
|
|
|
@@ -1039,131 +941,149 @@ async function getProjects(progressCallback = null) {
|
|
|
1039
941
|
return projects;
|
|
1040
942
|
}
|
|
1041
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
|
+
}
|
|
995
|
+
|
|
996
|
+
function mergeClaudeSession(target, incoming) {
|
|
997
|
+
if (incoming.messageCount > target.messageCount) {
|
|
998
|
+
target.messageCount = incoming.messageCount;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (incoming.summary !== 'New Session' && (target.summary === 'New Session' || incoming.lastActivity > target.lastActivity)) {
|
|
1002
|
+
target.summary = incoming.summary;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
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;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
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
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1042
1020
|
async function getSessions(projectName, limit = 5, offset = 0) {
|
|
1043
1021
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
1044
1022
|
|
|
1045
1023
|
try {
|
|
1046
|
-
const files = await fs.readdir(projectDir)
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if (jsonlFiles.length === 0) {
|
|
1052
|
-
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 };
|
|
1053
1029
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const jsonlFile = path.join(projectDir, file);
|
|
1072
|
-
const result = await parseJsonlSessions(jsonlFile);
|
|
1073
|
-
|
|
1074
|
-
result.sessions.forEach(session => {
|
|
1075
|
-
if (!allSessions.has(session.id)) {
|
|
1076
|
-
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;
|
|
1077
1047
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
allEntries.push(...result.entries);
|
|
1081
|
-
|
|
1082
|
-
// Early exit optimization for large projects
|
|
1083
|
-
if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
|
|
1084
|
-
break;
|
|
1048
|
+
mergeClaudeSession(existing, session);
|
|
1085
1049
|
}
|
|
1086
1050
|
}
|
|
1087
|
-
|
|
1088
|
-
// Build UUID-to-session mapping for timeline detection
|
|
1089
|
-
allEntries.forEach(entry => {
|
|
1090
|
-
if (entry.uuid && entry.sessionId) {
|
|
1091
|
-
uuidToSessionMap.set(entry.uuid, entry.sessionId);
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
// Group sessions by first user message ID
|
|
1096
|
-
const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
|
|
1097
|
-
const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
|
|
1098
|
-
|
|
1099
|
-
// Find the first user message for each session
|
|
1100
|
-
allEntries.forEach(entry => {
|
|
1101
|
-
if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
|
|
1102
|
-
// This is a first user message in a session (parentUuid is null)
|
|
1103
|
-
const firstUserMsgId = entry.uuid;
|
|
1104
|
-
|
|
1105
|
-
if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
|
|
1106
|
-
sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
|
|
1107
|
-
|
|
1108
|
-
const session = allSessions.get(entry.sessionId);
|
|
1109
|
-
if (session) {
|
|
1110
|
-
if (!sessionGroups.has(firstUserMsgId)) {
|
|
1111
|
-
sessionGroups.set(firstUserMsgId, {
|
|
1112
|
-
latestSession: session,
|
|
1113
|
-
allSessions: [session]
|
|
1114
|
-
});
|
|
1115
|
-
} else {
|
|
1116
|
-
const group = sessionGroups.get(firstUserMsgId);
|
|
1117
|
-
group.allSessions.push(session);
|
|
1118
1051
|
|
|
1119
|
-
|
|
1120
|
-
if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
|
|
1121
|
-
group.latestSession = session;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
});
|
|
1052
|
+
const groupedSessions = new Map();
|
|
1128
1053
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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] };
|
|
1134
1064
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
|
|
1140
|
-
const session = { ...group.latestSession };
|
|
1141
|
-
// Add metadata about grouping
|
|
1142
|
-
if (group.allSessions.length > 1) {
|
|
1143
|
-
session.isGrouped = true;
|
|
1144
|
-
session.groupSize = group.allSessions.length;
|
|
1145
|
-
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);
|
|
1146
1069
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
.filter(session => !session.summary.startsWith('{ "'))
|
|
1151
|
-
.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1070
|
+
|
|
1071
|
+
return primary;
|
|
1072
|
+
}).sort((left, right) => right.lastActivity - left.lastActivity);
|
|
1152
1073
|
|
|
1153
1074
|
const total = visibleSessions.length;
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1075
|
+
const sessions = visibleSessions.slice(offset, offset + limit);
|
|
1076
|
+
|
|
1157
1077
|
return {
|
|
1158
|
-
sessions
|
|
1159
|
-
hasMore,
|
|
1078
|
+
sessions,
|
|
1079
|
+
hasMore: offset + limit < total,
|
|
1160
1080
|
total,
|
|
1161
1081
|
offset,
|
|
1162
1082
|
limit
|
|
1163
1083
|
};
|
|
1164
1084
|
} catch (error) {
|
|
1165
1085
|
console.error(`Error reading sessions for project ${projectName}:`, error);
|
|
1166
|
-
return { sessions: [], hasMore: false, total: 0 };
|
|
1086
|
+
return { sessions: [], hasMore: false, total: 0, offset, limit };
|
|
1167
1087
|
}
|
|
1168
1088
|
}
|
|
1169
1089
|
|
|
@@ -1207,159 +1127,117 @@ async function getClaudeSessionMetadata(sessionId) {
|
|
|
1207
1127
|
async function parseJsonlSessions(filePath) {
|
|
1208
1128
|
const sessions = new Map();
|
|
1209
1129
|
const entries = [];
|
|
1210
|
-
const pendingSummaries = new Map();
|
|
1130
|
+
const pendingSummaries = new Map();
|
|
1211
1131
|
|
|
1212
1132
|
try {
|
|
1213
|
-
const
|
|
1214
|
-
const
|
|
1215
|
-
input: fileStream,
|
|
1216
|
-
crlfDelay: Infinity
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
for await (const line of rl) {
|
|
1220
|
-
if (line.trim()) {
|
|
1221
|
-
try {
|
|
1222
|
-
const entry = JSON.parse(line);
|
|
1223
|
-
entries.push(entry);
|
|
1224
|
-
|
|
1225
|
-
// Handle summary entries that don't have sessionId yet
|
|
1226
|
-
if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
|
|
1227
|
-
pendingSummaries.set(entry.leafUuid, entry.summary);
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
if (entry.sessionId) {
|
|
1231
|
-
if (!sessions.has(entry.sessionId)) {
|
|
1232
|
-
sessions.set(entry.sessionId, {
|
|
1233
|
-
id: entry.sessionId,
|
|
1234
|
-
summary: 'New Session',
|
|
1235
|
-
messageCount: 0,
|
|
1236
|
-
lastActivity: new Date(),
|
|
1237
|
-
cwd: entry.cwd || '',
|
|
1238
|
-
lastUserMessage: null,
|
|
1239
|
-
lastAssistantMessage: null
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1133
|
+
const stream = fsSync.createReadStream(filePath);
|
|
1134
|
+
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1242
1135
|
|
|
1243
|
-
|
|
1136
|
+
for await (const line of lines) {
|
|
1137
|
+
if (!line.trim()) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1244
1140
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
}
|
|
1141
|
+
try {
|
|
1142
|
+
const entry = JSON.parse(line);
|
|
1143
|
+
entries.push(entry);
|
|
1249
1144
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1145
|
+
if (entry.type === 'summary' && trimText(entry.summary) && !entry.sessionId && trimText(entry.leafUuid)) {
|
|
1146
|
+
pendingSummaries.set(entry.leafUuid, trimText(entry.summary));
|
|
1147
|
+
}
|
|
1254
1148
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1149
|
+
const sessionId = trimText(entry.sessionId);
|
|
1150
|
+
if (!sessionId) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1258
1153
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
|
|
1262
|
-
textContent = content[0].text;
|
|
1263
|
-
}
|
|
1154
|
+
const session = sessions.get(sessionId) || createClaudeSessionRecord(sessionId);
|
|
1155
|
+
sessions.set(sessionId, session);
|
|
1264
1156
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
textContent.startsWith('<command-args>') ||
|
|
1269
|
-
textContent.startsWith('<local-command-stdout>') ||
|
|
1270
|
-
textContent.startsWith('<system-reminder>') ||
|
|
1271
|
-
textContent.startsWith('Caveat:') ||
|
|
1272
|
-
textContent.startsWith('This session is being continued from a previous') ||
|
|
1273
|
-
textContent.startsWith('Invalid API key') ||
|
|
1274
|
-
textContent.includes('{"subtasks":') || // Filter Task Master prompts
|
|
1275
|
-
textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
|
|
1276
|
-
textContent === 'Warmup' // Explicitly filter out "Warmup"
|
|
1277
|
-
);
|
|
1278
|
-
|
|
1279
|
-
if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
|
|
1280
|
-
session.lastUserMessage = textContent;
|
|
1281
|
-
}
|
|
1282
|
-
} else if (entry.message?.role === 'assistant' && entry.message?.content) {
|
|
1283
|
-
// Skip API error messages using the isApiErrorMessage flag
|
|
1284
|
-
if (entry.isApiErrorMessage === true) {
|
|
1285
|
-
// Skip this message entirely
|
|
1286
|
-
} else {
|
|
1287
|
-
// Track last assistant text message
|
|
1288
|
-
let assistantText = null;
|
|
1289
|
-
|
|
1290
|
-
if (Array.isArray(entry.message.content)) {
|
|
1291
|
-
for (const part of entry.message.content) {
|
|
1292
|
-
if (part.type === 'text' && part.text) {
|
|
1293
|
-
assistantText = part.text;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
} else if (typeof entry.message.content === 'string') {
|
|
1297
|
-
assistantText = entry.message.content;
|
|
1298
|
-
}
|
|
1157
|
+
if (!session.cwd && trimText(entry.cwd)) {
|
|
1158
|
+
session.cwd = trimText(entry.cwd);
|
|
1159
|
+
}
|
|
1299
1160
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
assistantText.includes('{"subtasks":') ||
|
|
1304
|
-
assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
|
|
1305
|
-
);
|
|
1161
|
+
if (trimText(entry.timestamp)) {
|
|
1162
|
+
session.lastActivity = new Date(entry.timestamp);
|
|
1163
|
+
}
|
|
1306
1164
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
+
}
|
|
1312
1170
|
|
|
1313
|
-
|
|
1171
|
+
if (!session.rootMessageId && entry.type === 'user' && entry.parentUuid === null && trimText(entry.uuid)) {
|
|
1172
|
+
session.rootMessageId = trimText(entry.uuid);
|
|
1173
|
+
}
|
|
1314
1174
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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;
|
|
1318
1184
|
}
|
|
1319
|
-
} catch (parseError) {
|
|
1320
|
-
// Skip malformed lines silently
|
|
1321
1185
|
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
1186
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
if (session.summary === 'New Session') {
|
|
1328
|
-
// Prefer last user message, fall back to last assistant message
|
|
1329
|
-
const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
|
|
1330
|
-
if (lastMessage) {
|
|
1331
|
-
session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
|
|
1332
|
-
}
|
|
1187
|
+
session.messageCount += 1;
|
|
1188
|
+
} catch {
|
|
1333
1189
|
}
|
|
1334
1190
|
}
|
|
1335
1191
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (Math.random() < 0.01) { // Log 1% of sessions
|
|
1344
|
-
}
|
|
1345
|
-
return !shouldFilter;
|
|
1346
|
-
});
|
|
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
|
+
}
|
|
1347
1199
|
|
|
1200
|
+
return session;
|
|
1201
|
+
})
|
|
1202
|
+
.filter((session) => !session.summary.startsWith('{ "'));
|
|
1348
1203
|
|
|
1349
1204
|
return {
|
|
1350
|
-
sessions:
|
|
1351
|
-
entries
|
|
1205
|
+
sessions: normalizedSessions,
|
|
1206
|
+
entries
|
|
1352
1207
|
};
|
|
1353
|
-
|
|
1354
1208
|
} catch (error) {
|
|
1355
1209
|
console.error('Error reading JSONL file:', error);
|
|
1356
1210
|
return { sessions: [], entries: [] };
|
|
1357
1211
|
}
|
|
1358
1212
|
}
|
|
1359
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
|
+
|
|
1360
1238
|
// Get messages for a specific session with pagination support
|
|
1361
1239
|
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
|
|
1362
|
-
const projectDir =
|
|
1240
|
+
const projectDir = await resolveClaudeProjectDir(projectName, sessionId);
|
|
1363
1241
|
|
|
1364
1242
|
try {
|
|
1365
1243
|
const files = await fs.readdir(projectDir);
|
|
@@ -2279,6 +2157,24 @@ async function buildCodexSessionsLookup(projectPaths, options = {}) {
|
|
|
2279
2157
|
});
|
|
2280
2158
|
}
|
|
2281
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
|
+
|
|
2282
2178
|
async function buildOpencodeSessionsLookup(projectPaths, options = {}) {
|
|
2283
2179
|
const { limit = 5 } = options;
|
|
2284
2180
|
const normalizedProjectPaths = Array.from(new Set(
|
|
@@ -2501,6 +2397,15 @@ async function parseCodexSessionFile(filePath) {
|
|
|
2501
2397
|
}
|
|
2502
2398
|
}
|
|
2503
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
|
+
|
|
2504
2409
|
if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
|
|
2505
2410
|
messageCount++;
|
|
2506
2411
|
}
|
|
@@ -2554,23 +2459,6 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2554
2459
|
crlfDelay: Infinity
|
|
2555
2460
|
});
|
|
2556
2461
|
|
|
2557
|
-
// Helper to extract text from Codex content array
|
|
2558
|
-
const extractText = (content) => {
|
|
2559
|
-
if (!Array.isArray(content)) return content;
|
|
2560
|
-
return content
|
|
2561
|
-
.map(item => {
|
|
2562
|
-
if (item.type === 'input_text' || item.type === 'output_text') {
|
|
2563
|
-
return item.text;
|
|
2564
|
-
}
|
|
2565
|
-
if (item.type === 'text') {
|
|
2566
|
-
return item.text;
|
|
2567
|
-
}
|
|
2568
|
-
return '';
|
|
2569
|
-
})
|
|
2570
|
-
.filter(Boolean)
|
|
2571
|
-
.join('\n');
|
|
2572
|
-
};
|
|
2573
|
-
|
|
2574
2462
|
const appendMessage = (message) => {
|
|
2575
2463
|
total += 1;
|
|
2576
2464
|
appendBoundedMessage(messages, message, maxBufferedMessages);
|
|
@@ -2587,10 +2475,10 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
2587
2475
|
}
|
|
2588
2476
|
|
|
2589
2477
|
// Extract messages from response_item
|
|
2590
|
-
if (entry
|
|
2478
|
+
if (shouldIncludeCodexTranscriptMessage(entry)) {
|
|
2591
2479
|
const content = entry.payload.content;
|
|
2592
2480
|
const role = entry.payload.role || 'assistant';
|
|
2593
|
-
const textContent =
|
|
2481
|
+
const textContent = extractCodexMessageText(content);
|
|
2594
2482
|
const visibleTextContent = role === 'user'
|
|
2595
2483
|
? extractVisibleUserMessage(textContent)
|
|
2596
2484
|
: textContent;
|