agentacta 2026.3.6 → 2026.3.12
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/README.md +74 -2
- package/config.js +20 -2
- package/index.js +185 -0
- package/indexer.js +33 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,18 +118,20 @@ On first run, AgentActa creates:
|
|
|
118
118
|
- `~/.config/agentacta/config.json`
|
|
119
119
|
- or `agentacta.config.json` in current directory (if present)
|
|
120
120
|
|
|
121
|
-
Default config:
|
|
121
|
+
Default config (auto-generated on first run — session directories are detected automatically):
|
|
122
122
|
|
|
123
123
|
```json
|
|
124
124
|
{
|
|
125
125
|
"port": 4003,
|
|
126
126
|
"storage": "reference",
|
|
127
|
-
"sessionsPath":
|
|
127
|
+
"sessionsPath": ["~/.claude/projects", "~/.openclaw/sessions"],
|
|
128
128
|
"dbPath": "./agentacta.db",
|
|
129
129
|
"projectAliases": {}
|
|
130
130
|
}
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
`sessionsPath` accepts a string, a colon-delimited string, or a JSON array.
|
|
134
|
+
|
|
133
135
|
### Storage modes
|
|
134
136
|
|
|
135
137
|
- `reference` (default): index parsed events in SQLite, keep source JSONL on disk. Lightweight.
|
|
@@ -163,6 +165,76 @@ Default config:
|
|
|
163
165
|
| `GET /api/health` | Server status, version, uptime, session count |
|
|
164
166
|
| `GET /api/export/search?q=<query>&format=md` | Export search results |
|
|
165
167
|
|
|
168
|
+
### Context API
|
|
169
|
+
|
|
170
|
+
The Context API gives agents historical context before they start working. Instead of exploring a codebase from scratch, an agent can query what's happened before.
|
|
171
|
+
|
|
172
|
+
| Endpoint | Description |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `GET /api/context/file?path=<filepath>` | History for a specific file |
|
|
175
|
+
| `GET /api/context/repo?path=<repo-path>` | Aggregates for a repo/project |
|
|
176
|
+
| `GET /api/context/agent?name=<agent-name>` | Stats for a specific agent |
|
|
177
|
+
|
|
178
|
+
**File context** — how many sessions touched this file, when it was last modified, recent change summaries, operation breakdown (reads vs edits), related files, and recent errors:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
curl http://localhost:4003/api/context/file?path=/home/user/project/server.js
|
|
182
|
+
```
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"file": "/home/user/project/server.js",
|
|
186
|
+
"sessionCount": 34,
|
|
187
|
+
"lastModified": "3h ago",
|
|
188
|
+
"recentChanges": ["Added OAuth state validation", "Fixed password masking"],
|
|
189
|
+
"operations": { "edit": 105, "read": 56 },
|
|
190
|
+
"relatedFiles": [{ "path": "public/app.js", "count": 28 }],
|
|
191
|
+
"recentErrors": []
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Agent context** — total sessions, cost, average duration, most-used tools, recent work:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
curl http://localhost:4003/api/context/agent?name=claude-code
|
|
199
|
+
```
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"agent": "claude-code",
|
|
203
|
+
"sessionCount": 60,
|
|
204
|
+
"totalCost": 18.83,
|
|
205
|
+
"avgDuration": 288,
|
|
206
|
+
"topTools": [{ "tool": "edit", "count": 190 }, { "tool": "exec", "count": 560 }],
|
|
207
|
+
"recentSessions": [{ "id": "...", "summary": "Added context API...", "timestamp": "..." }],
|
|
208
|
+
"successRate": 100
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Repo context** — aggregate cost, tokens, distinct agents, most-touched files, common tools:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
curl http://localhost:4003/api/context/repo?path=agentacta
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Using the Context API with agents
|
|
219
|
+
|
|
220
|
+
Inject context into agent prompts so new sessions start informed:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Fetch context before starting Claude Code
|
|
224
|
+
CONTEXT=$(curl -s http://localhost:4003/api/context/file?path=$(pwd)/server.js)
|
|
225
|
+
claude --print "Context from previous sessions: $CONTEXT
|
|
226
|
+
|
|
227
|
+
Your task: refactor the auth module"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Or add it to a CLAUDE.md / AGENTS.md:
|
|
231
|
+
|
|
232
|
+
```markdown
|
|
233
|
+
## Project Context API
|
|
234
|
+
Before modifying key files, query AgentActa for history:
|
|
235
|
+
curl http://localhost:4003/api/context/file?path={filepath}
|
|
236
|
+
```
|
|
237
|
+
|
|
166
238
|
Agent integration example:
|
|
167
239
|
|
|
168
240
|
```javascript
|
package/config.js
CHANGED
|
@@ -14,6 +14,12 @@ function resolveConfigFile() {
|
|
|
14
14
|
|
|
15
15
|
const CONFIG_FILE = resolveConfigFile();
|
|
16
16
|
|
|
17
|
+
const KNOWN_SESSION_DIRS = [
|
|
18
|
+
path.join(os.homedir(), '.claude', 'projects'), // Claude Code
|
|
19
|
+
path.join(os.homedir(), '.codex', 'sessions'), // Codex CLI
|
|
20
|
+
path.join(os.homedir(), '.openclaw', 'sessions'), // OpenClaw
|
|
21
|
+
];
|
|
22
|
+
|
|
17
23
|
const DEFAULTS = {
|
|
18
24
|
port: 4003,
|
|
19
25
|
storage: 'reference',
|
|
@@ -22,6 +28,11 @@ const DEFAULTS = {
|
|
|
22
28
|
projectAliases: {}
|
|
23
29
|
};
|
|
24
30
|
|
|
31
|
+
function detectSessionDirs() {
|
|
32
|
+
const found = KNOWN_SESSION_DIRS.filter(d => fs.existsSync(d));
|
|
33
|
+
return found.length > 0 ? found : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
function loadConfig() {
|
|
26
37
|
let fileConfig = {};
|
|
27
38
|
|
|
@@ -32,11 +43,18 @@ function loadConfig() {
|
|
|
32
43
|
console.error(`Warning: Could not parse ${CONFIG_FILE}:`, err.message);
|
|
33
44
|
}
|
|
34
45
|
} else {
|
|
35
|
-
// First-run: create default config
|
|
46
|
+
// First-run: create default config with auto-detected session dirs
|
|
47
|
+
const detected = detectSessionDirs();
|
|
48
|
+
const firstRunDefaults = { ...DEFAULTS, sessionsPath: detected };
|
|
36
49
|
const dir = path.dirname(CONFIG_FILE);
|
|
37
50
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
38
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(
|
|
51
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(firstRunDefaults, null, 2) + '\n');
|
|
52
|
+
// Apply to in-memory config so this run also benefits
|
|
53
|
+
fileConfig = firstRunDefaults;
|
|
39
54
|
console.log(`Created default config: ${CONFIG_FILE}`);
|
|
55
|
+
if (detected) {
|
|
56
|
+
console.log(`Auto-detected session directories:\n${detected.map(d => ` - ${d}`).join('\n')}`);
|
|
57
|
+
}
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
const config = { ...DEFAULTS, ...fileConfig };
|
package/index.js
CHANGED
|
@@ -105,6 +105,15 @@ function getDbSize() {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function relativeTime(ts) {
|
|
109
|
+
if (!ts) return null;
|
|
110
|
+
const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
|
|
111
|
+
if (diff < 60) return 'just now';
|
|
112
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
113
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
114
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
function normalizeAgentLabel(agent) {
|
|
109
118
|
if (!agent) return agent;
|
|
110
119
|
if (agent === 'main') return 'openclaw-main';
|
|
@@ -514,6 +523,182 @@ const server = http.createServer((req, res) => {
|
|
|
514
523
|
const sizeAfter = getDbSize();
|
|
515
524
|
json(res, { ok: true, sizeBefore, sizeAfter });
|
|
516
525
|
}
|
|
526
|
+
// --- Context API ---
|
|
527
|
+
else if (pathname === '/api/context/file') {
|
|
528
|
+
const fp = query.path || '';
|
|
529
|
+
if (!fp) return json(res, { error: 'path parameter is required' }, 400);
|
|
530
|
+
|
|
531
|
+
const sessionCount = db.prepare(
|
|
532
|
+
'SELECT COUNT(DISTINCT session_id) as c FROM file_activity WHERE file_path = ?'
|
|
533
|
+
).get(fp).c;
|
|
534
|
+
|
|
535
|
+
if (sessionCount === 0) {
|
|
536
|
+
return json(res, { file: fp, sessionCount: 0, lastModified: null, recentChanges: [], operations: {}, relatedFiles: [], recentErrors: [] });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const lastTouched = db.prepare(
|
|
540
|
+
'SELECT MAX(timestamp) as t FROM file_activity WHERE file_path = ?'
|
|
541
|
+
).get(fp).t;
|
|
542
|
+
|
|
543
|
+
const recentChanges = db.prepare(
|
|
544
|
+
`SELECT DISTINCT s.summary FROM file_activity fa
|
|
545
|
+
JOIN sessions s ON s.id = fa.session_id
|
|
546
|
+
WHERE fa.file_path = ? AND s.summary IS NOT NULL
|
|
547
|
+
ORDER BY s.start_time DESC LIMIT 5`
|
|
548
|
+
).all(fp).map(r => r.summary);
|
|
549
|
+
|
|
550
|
+
const opsRows = db.prepare(
|
|
551
|
+
'SELECT operation, COUNT(*) as c FROM file_activity WHERE file_path = ? GROUP BY operation'
|
|
552
|
+
).all(fp);
|
|
553
|
+
const operations = {};
|
|
554
|
+
for (const r of opsRows) operations[r.operation] = r.c;
|
|
555
|
+
|
|
556
|
+
const relatedFiles = db.prepare(
|
|
557
|
+
`SELECT fa2.file_path, COUNT(DISTINCT fa1.session_id) as c
|
|
558
|
+
FROM file_activity fa1
|
|
559
|
+
JOIN file_activity fa2 ON fa1.session_id = fa2.session_id
|
|
560
|
+
WHERE fa1.file_path = ? AND fa2.file_path != ?
|
|
561
|
+
GROUP BY fa2.file_path
|
|
562
|
+
ORDER BY c DESC LIMIT 5`
|
|
563
|
+
).all(fp, fp).map(r => ({ path: r.file_path, count: r.c }));
|
|
564
|
+
|
|
565
|
+
const sessionIds = db.prepare(
|
|
566
|
+
'SELECT DISTINCT session_id FROM file_activity WHERE file_path = ?'
|
|
567
|
+
).all(fp).map(r => r.session_id);
|
|
568
|
+
|
|
569
|
+
let recentErrors = [];
|
|
570
|
+
if (sessionIds.length) {
|
|
571
|
+
const placeholders = sessionIds.map(() => '?').join(',');
|
|
572
|
+
recentErrors = db.prepare(
|
|
573
|
+
`SELECT tool_result FROM events
|
|
574
|
+
WHERE session_id IN (${placeholders})
|
|
575
|
+
AND tool_result IS NOT NULL
|
|
576
|
+
AND (tool_result LIKE '%error%' OR tool_result LIKE '%Error%' OR tool_result LIKE '%ERROR%')
|
|
577
|
+
ORDER BY timestamp DESC LIMIT 3`
|
|
578
|
+
).all(...sessionIds).map(r => r.tool_result.slice(0, 200));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return json(res, {
|
|
582
|
+
file: fp, sessionCount,
|
|
583
|
+
lastModified: relativeTime(lastTouched),
|
|
584
|
+
recentChanges, operations, relatedFiles, recentErrors
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
else if (pathname === '/api/context/repo') {
|
|
588
|
+
const repoPath = query.path || '';
|
|
589
|
+
if (!repoPath) return json(res, { error: 'path parameter is required' }, 400);
|
|
590
|
+
|
|
591
|
+
// Find sessions matching the repo path via file_activity or initial_prompt
|
|
592
|
+
const sessionIds = db.prepare(
|
|
593
|
+
`SELECT DISTINCT session_id FROM file_activity WHERE file_path = ? OR file_path LIKE ?`
|
|
594
|
+
).all(repoPath, repoPath + '/%').map(r => r.session_id);
|
|
595
|
+
|
|
596
|
+
const promptSessions = db.prepare(
|
|
597
|
+
`SELECT id FROM sessions WHERE initial_prompt LIKE ?`
|
|
598
|
+
).all('%' + repoPath + '%').map(r => r.id);
|
|
599
|
+
|
|
600
|
+
const allIds = [...new Set([...sessionIds, ...promptSessions])];
|
|
601
|
+
|
|
602
|
+
if (allIds.length === 0) {
|
|
603
|
+
return json(res, { repo: repoPath, sessionCount: 0, totalCost: 0, totalTokens: 0, agents: [], topFiles: [], recentSessions: [], commonTools: [], commonErrors: [] });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const ph = allIds.map(() => '?').join(',');
|
|
607
|
+
|
|
608
|
+
const agg = db.prepare(
|
|
609
|
+
`SELECT COUNT(*) as c, SUM(total_cost) as cost, SUM(total_tokens) as tokens
|
|
610
|
+
FROM sessions WHERE id IN (${ph})`
|
|
611
|
+
).get(...allIds);
|
|
612
|
+
|
|
613
|
+
const agents = [...new Set(
|
|
614
|
+
db.prepare(`SELECT DISTINCT agent FROM sessions WHERE id IN (${ph}) AND agent IS NOT NULL`).all(...allIds)
|
|
615
|
+
.map(r => normalizeAgentLabel(r.agent)).filter(Boolean)
|
|
616
|
+
)];
|
|
617
|
+
|
|
618
|
+
const topFiles = db.prepare(
|
|
619
|
+
`SELECT file_path, COUNT(*) as c FROM file_activity
|
|
620
|
+
WHERE session_id IN (${ph})
|
|
621
|
+
GROUP BY file_path ORDER BY c DESC LIMIT 10`
|
|
622
|
+
).all(...allIds).map(r => ({ path: r.file_path, count: r.c }));
|
|
623
|
+
|
|
624
|
+
const recentSessions = db.prepare(
|
|
625
|
+
`SELECT id, summary, agent, start_time, end_time FROM sessions
|
|
626
|
+
WHERE id IN (${ph})
|
|
627
|
+
ORDER BY start_time DESC LIMIT 5`
|
|
628
|
+
).all(...allIds).map(r => ({
|
|
629
|
+
id: r.id, summary: r.summary, agent: normalizeAgentLabel(r.agent),
|
|
630
|
+
timestamp: r.start_time, status: r.end_time ? 'completed' : 'in-progress'
|
|
631
|
+
}));
|
|
632
|
+
|
|
633
|
+
const commonTools = db.prepare(
|
|
634
|
+
`SELECT tool_name, COUNT(*) as c FROM events
|
|
635
|
+
WHERE session_id IN (${ph}) AND tool_name IS NOT NULL
|
|
636
|
+
GROUP BY tool_name ORDER BY c DESC LIMIT 10`
|
|
637
|
+
).all(...allIds).map(r => ({ tool: r.tool_name, count: r.c }));
|
|
638
|
+
|
|
639
|
+
const commonErrors = db.prepare(
|
|
640
|
+
`SELECT DISTINCT SUBSTR(tool_result, 1, 200) as err FROM events
|
|
641
|
+
WHERE session_id IN (${ph})
|
|
642
|
+
AND tool_result IS NOT NULL
|
|
643
|
+
AND (tool_result LIKE '%error%' OR tool_result LIKE '%Error%' OR tool_result LIKE '%ERROR%')
|
|
644
|
+
ORDER BY timestamp DESC LIMIT 5`
|
|
645
|
+
).all(...allIds).map(r => r.err);
|
|
646
|
+
|
|
647
|
+
return json(res, {
|
|
648
|
+
repo: repoPath, sessionCount: allIds.length,
|
|
649
|
+
totalCost: agg.cost || 0, totalTokens: agg.tokens || 0,
|
|
650
|
+
agents, topFiles, recentSessions, commonTools, commonErrors
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
else if (pathname === '/api/context/agent') {
|
|
654
|
+
const name = query.name || '';
|
|
655
|
+
if (!name) return json(res, { error: 'name parameter is required' }, 400);
|
|
656
|
+
|
|
657
|
+
// Try exact match first, then check all sessions with normalized label match
|
|
658
|
+
let sessions = db.prepare(
|
|
659
|
+
'SELECT * FROM sessions WHERE agent = ?'
|
|
660
|
+
).all(name);
|
|
661
|
+
if (sessions.length === 0) {
|
|
662
|
+
sessions = db.prepare('SELECT * FROM sessions WHERE agent IS NOT NULL').all()
|
|
663
|
+
.filter(s => normalizeAgentLabel(s.agent) === name);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (sessions.length === 0) {
|
|
667
|
+
return json(res, { agent: name, sessionCount: 0, totalCost: 0, avgDuration: 0, topTools: [], recentSessions: [], successRate: 0 });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const totalCost = sessions.reduce((s, r) => s + (r.total_cost || 0), 0);
|
|
671
|
+
let totalDuration = 0;
|
|
672
|
+
let durationCount = 0;
|
|
673
|
+
for (const s of sessions) {
|
|
674
|
+
if (s.start_time && s.end_time) {
|
|
675
|
+
totalDuration += (new Date(s.end_time) - new Date(s.start_time)) / 1000;
|
|
676
|
+
durationCount++;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const avgDuration = durationCount > 0 ? Math.round(totalDuration / durationCount) : 0;
|
|
680
|
+
|
|
681
|
+
const withSummary = sessions.filter(s => s.summary).length;
|
|
682
|
+
const successRate = Math.round((withSummary / sessions.length) * 100);
|
|
683
|
+
|
|
684
|
+
const ids = sessions.map(s => s.id);
|
|
685
|
+
const ph = ids.map(() => '?').join(',');
|
|
686
|
+
const topTools = db.prepare(
|
|
687
|
+
`SELECT tool_name, COUNT(*) as c FROM events
|
|
688
|
+
WHERE session_id IN (${ph}) AND tool_name IS NOT NULL
|
|
689
|
+
GROUP BY tool_name ORDER BY c DESC LIMIT 10`
|
|
690
|
+
).all(...ids).map(r => ({ tool: r.tool_name, count: r.c }));
|
|
691
|
+
|
|
692
|
+
const recentSessions = sessions
|
|
693
|
+
.sort((a, b) => (b.start_time || '').localeCompare(a.start_time || ''))
|
|
694
|
+
.slice(0, 5)
|
|
695
|
+
.map(s => ({ id: s.id, summary: s.summary, timestamp: s.start_time }));
|
|
696
|
+
|
|
697
|
+
return json(res, {
|
|
698
|
+
agent: name, sessionCount: sessions.length,
|
|
699
|
+
totalCost, avgDuration, topTools, recentSessions, successRate
|
|
700
|
+
});
|
|
701
|
+
}
|
|
517
702
|
else if (pathname === '/api/files') {
|
|
518
703
|
const limit = parseInt(query.limit) || 100;
|
|
519
704
|
const offset = parseInt(query.offset) || 0;
|
package/indexer.js
CHANGED
|
@@ -29,16 +29,36 @@ function discoverSessionDirs(config) {
|
|
|
29
29
|
const dirs = [];
|
|
30
30
|
const home = process.env.HOME;
|
|
31
31
|
|
|
32
|
+
// Expand a single path into session dirs, handling Claude Code's per-project structure
|
|
33
|
+
function expandPath(p) {
|
|
34
|
+
if (!fs.existsSync(p)) return;
|
|
35
|
+
const stat = fs.statSync(p);
|
|
36
|
+
if (!stat.isDirectory()) return;
|
|
37
|
+
// Claude Code: ~/.claude/projects contains per-project subdirs with JSONL files
|
|
38
|
+
if (p.replace(/\/$/, '').endsWith('/.claude/projects')) {
|
|
39
|
+
for (const proj of fs.readdirSync(p)) {
|
|
40
|
+
const projDir = path.join(p, proj);
|
|
41
|
+
if (fs.statSync(projDir).isDirectory()) {
|
|
42
|
+
const hasJsonl = fs.readdirSync(projDir).some(f => f.endsWith('.jsonl'));
|
|
43
|
+
if (hasJsonl) dirs.push({ path: projDir, agent: 'claude-code' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
dirs.push({ path: p, agent: path.basename(path.dirname(p)) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
// Config sessionsPath or env var override
|
|
33
52
|
const sessionsOverride = process.env.AGENTACTA_SESSIONS_PATH || (config && config.sessionsPath);
|
|
34
53
|
if (sessionsOverride) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
const overridePaths = Array.isArray(sessionsOverride)
|
|
55
|
+
? sessionsOverride
|
|
56
|
+
: sessionsOverride.split(':');
|
|
57
|
+
overridePaths.forEach(expandPath);
|
|
38
58
|
if (dirs.length) return dirs;
|
|
39
59
|
}
|
|
40
60
|
|
|
41
|
-
//
|
|
61
|
+
// Auto-discover: ~/.openclaw/agents/*/sessions/
|
|
42
62
|
const oclawAgents = path.join(home, '.openclaw/agents');
|
|
43
63
|
if (fs.existsSync(oclawAgents)) {
|
|
44
64
|
for (const agent of fs.readdirSync(oclawAgents)) {
|
|
@@ -49,23 +69,8 @@ function discoverSessionDirs(config) {
|
|
|
49
69
|
}
|
|
50
70
|
}
|
|
51
71
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
if (fs.existsSync(claudeProjects)) {
|
|
55
|
-
for (const proj of fs.readdirSync(claudeProjects)) {
|
|
56
|
-
const projDir = path.join(claudeProjects, proj);
|
|
57
|
-
// Claude Code: JSONL files directly in project dir
|
|
58
|
-
if (fs.existsSync(projDir) && fs.statSync(projDir).isDirectory()) {
|
|
59
|
-
const hasJsonl = fs.readdirSync(projDir).some(f => f.endsWith('.jsonl'));
|
|
60
|
-
if (hasJsonl) dirs.push({ path: projDir, agent: 'claude-code' });
|
|
61
|
-
}
|
|
62
|
-
// Also check sessions/ subdirectory (future-proofing)
|
|
63
|
-
const sp = path.join(projDir, 'sessions');
|
|
64
|
-
if (fs.existsSync(sp) && fs.statSync(sp).isDirectory()) {
|
|
65
|
-
dirs.push({ path: sp, agent: 'claude-code' });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
72
|
+
// Auto-discover: ~/.claude/projects/
|
|
73
|
+
expandPath(path.join(home, '.claude/projects'));
|
|
69
74
|
|
|
70
75
|
// Scan ~/.codex/sessions recursively (Codex CLI stores nested YYYY/MM/DD/*.jsonl)
|
|
71
76
|
const codexSessions = path.join(home, '.codex/sessions');
|
|
@@ -289,11 +294,16 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
289
294
|
if (firstLine.agent) agent = firstLine.agent;
|
|
290
295
|
if (firstLine.sessionType) sessionType = firstLine.sessionType;
|
|
291
296
|
if (sessionId.includes('subagent')) sessionType = 'subagent';
|
|
292
|
-
} else if (firstLine.type === 'user' || firstLine.type === 'assistant' || firstLine.type === 'file-history-snapshot') {
|
|
293
|
-
// Claude Code format — no session header, extract from first message line
|
|
297
|
+
} else if (firstLine.type === 'user' || firstLine.type === 'assistant' || firstLine.type === 'file-history-snapshot' || firstLine.type === 'queue-operation') {
|
|
298
|
+
// Claude Code format — no session header, extract from first message or queue-operation line
|
|
294
299
|
isClaudeCode = true;
|
|
295
300
|
for (const line of lines) {
|
|
296
301
|
let obj; try { obj = JSON.parse(line); } catch { continue; }
|
|
302
|
+
if (obj.sessionId && obj.timestamp) {
|
|
303
|
+
sessionId = obj.sessionId;
|
|
304
|
+
sessionStart = obj.timestamp;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
297
307
|
if ((obj.type === 'user' || obj.type === 'assistant') && obj.sessionId) {
|
|
298
308
|
sessionId = obj.sessionId;
|
|
299
309
|
sessionStart = obj.timestamp;
|