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 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": null,
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 in XDG location
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(DEFAULTS, null, 2) + '\n');
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
- for (const p of sessionsOverride.split(':')) {
36
- if (fs.existsSync(p)) dirs.push({ path: p, agent: path.basename(path.dirname(p)) });
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
- // Scan ~/.openclaw/agents/*/sessions/
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
- // Scan ~/.claude/projects/*/ (Claude Code stores JSONL directly in project dirs)
53
- const claudeProjects = path.join(home, '.claude/projects');
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentacta",
3
- "version": "2026.3.6",
3
+ "version": "2026.3.12",
4
4
  "description": "Audit trail and search engine for AI agent sessions",
5
5
  "main": "index.js",
6
6
  "bin": {