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.
- package/README.md +70 -0
- package/index.js +185 -0
- 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;
|