agentacta 2026.3.7 → 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.
Files changed (3) hide show
  1. package/README.md +70 -0
  2. package/index.js +185 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -165,6 +165,76 @@ Default config (auto-generated on first run — session directories are detected
165
165
  | `GET /api/health` | Server status, version, uptime, session count |
166
166
  | `GET /api/export/search?q=<query>&format=md` | Export search results |
167
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
+
168
238
  Agent integration example:
169
239
 
170
240
  ```javascript
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentacta",
3
- "version": "2026.3.7",
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": {