agentacta 1.0.0
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/LICENSE +21 -0
- package/README.md +216 -0
- package/config.js +55 -0
- package/db.js +137 -0
- package/index.js +376 -0
- package/indexer.js +330 -0
- package/package.json +51 -0
- package/public/app.js +658 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +72 -0
- package/public/index.html +50 -0
- package/public/manifest.json +26 -0
- package/public/style.css +562 -0
- package/public/sw.js +42 -0
package/indexer.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { open, init, createStmts } = require('./db');
|
|
4
|
+
const { loadConfig } = require('./config');
|
|
5
|
+
|
|
6
|
+
const REINDEX = process.argv.includes('--reindex');
|
|
7
|
+
const WATCH = process.argv.includes('--watch');
|
|
8
|
+
|
|
9
|
+
function discoverSessionDirs(config) {
|
|
10
|
+
const dirs = [];
|
|
11
|
+
const home = process.env.HOME;
|
|
12
|
+
|
|
13
|
+
// Config sessionsPath or env var override
|
|
14
|
+
const sessionsOverride = process.env.AGENTACTA_SESSIONS_PATH || (config && config.sessionsPath);
|
|
15
|
+
if (sessionsOverride) {
|
|
16
|
+
for (const p of sessionsOverride.split(':')) {
|
|
17
|
+
if (fs.existsSync(p)) dirs.push({ path: p, agent: path.basename(path.dirname(p)) });
|
|
18
|
+
}
|
|
19
|
+
if (dirs.length) return dirs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Scan ~/.openclaw/agents/*/sessions/
|
|
23
|
+
const oclawAgents = path.join(home, '.openclaw/agents');
|
|
24
|
+
if (fs.existsSync(oclawAgents)) {
|
|
25
|
+
for (const agent of fs.readdirSync(oclawAgents)) {
|
|
26
|
+
const sp = path.join(oclawAgents, agent, 'sessions');
|
|
27
|
+
if (fs.existsSync(sp) && fs.statSync(sp).isDirectory()) {
|
|
28
|
+
dirs.push({ path: sp, agent });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Scan ~/.claude/projects/*/sessions/
|
|
34
|
+
const claudeProjects = path.join(home, '.claude/projects');
|
|
35
|
+
if (fs.existsSync(claudeProjects)) {
|
|
36
|
+
for (const proj of fs.readdirSync(claudeProjects)) {
|
|
37
|
+
const sp = path.join(claudeProjects, proj, 'sessions');
|
|
38
|
+
if (fs.existsSync(sp) && fs.statSync(sp).isDirectory()) {
|
|
39
|
+
dirs.push({ path: sp, agent: `claude-${proj}` });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!dirs.length) {
|
|
45
|
+
// Fallback to hardcoded
|
|
46
|
+
const fallback = path.join(home, '.openclaw/agents/main/sessions');
|
|
47
|
+
if (fs.existsSync(fallback)) dirs.push({ path: fallback, agent: 'main' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return dirs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isHeartbeat(text) {
|
|
54
|
+
if (!text) return false;
|
|
55
|
+
const lower = text.toLowerCase();
|
|
56
|
+
return lower.includes('heartbeat') || lower.includes('heartbeat_ok');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractContent(msg) {
|
|
60
|
+
if (!msg || !msg.content) return '';
|
|
61
|
+
if (typeof msg.content === 'string') return msg.content;
|
|
62
|
+
if (Array.isArray(msg.content)) {
|
|
63
|
+
return msg.content.filter(b => b.type === 'text').map(b => b.text || '').join('\n');
|
|
64
|
+
}
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractToolCalls(msg) {
|
|
69
|
+
if (!msg || !Array.isArray(msg.content)) return [];
|
|
70
|
+
return msg.content
|
|
71
|
+
.filter(b => b.type === 'tool_use' || b.type === 'toolCall')
|
|
72
|
+
.map(b => ({
|
|
73
|
+
id: b.id || b.toolCallId || '',
|
|
74
|
+
name: b.name || '',
|
|
75
|
+
args: JSON.stringify(b.input || b.arguments || {})
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractToolResult(msg) {
|
|
80
|
+
if (!msg) return null;
|
|
81
|
+
if (msg.role === 'toolResult' || msg.role === 'tool') {
|
|
82
|
+
const content = Array.isArray(msg.content)
|
|
83
|
+
? msg.content.map(b => b.text || '').join('\n')
|
|
84
|
+
: (typeof msg.content === 'string' ? msg.content : '');
|
|
85
|
+
return { toolCallId: msg.toolCallId || '', toolName: msg.toolName || '', content: content.slice(0, 10000) };
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractFilePaths(toolName, toolArgs) {
|
|
91
|
+
const paths = [];
|
|
92
|
+
if (!toolArgs) return paths;
|
|
93
|
+
try {
|
|
94
|
+
const args = typeof toolArgs === 'string' ? JSON.parse(toolArgs) : toolArgs;
|
|
95
|
+
// Common field names for file paths
|
|
96
|
+
for (const key of ['path', 'file_path', 'filePath', 'file', 'filename']) {
|
|
97
|
+
if (args[key] && typeof args[key] === 'string') paths.push(args[key]);
|
|
98
|
+
}
|
|
99
|
+
} catch {}
|
|
100
|
+
return paths;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function indexFile(db, filePath, agentName, stmts, archiveMode) {
|
|
104
|
+
const stat = fs.statSync(filePath);
|
|
105
|
+
const mtime = stat.mtime.toISOString();
|
|
106
|
+
|
|
107
|
+
if (!REINDEX) {
|
|
108
|
+
const state = stmts.getState.get(filePath);
|
|
109
|
+
if (state && state.last_modified === mtime) return { skipped: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
113
|
+
const lines = raw.trim().split('\n').filter(Boolean);
|
|
114
|
+
if (lines.length === 0) return { skipped: true };
|
|
115
|
+
|
|
116
|
+
let sessionId = null;
|
|
117
|
+
let sessionStart = null;
|
|
118
|
+
let sessionEnd = null;
|
|
119
|
+
let msgCount = 0;
|
|
120
|
+
let toolCount = 0;
|
|
121
|
+
let model = null;
|
|
122
|
+
let summary = '';
|
|
123
|
+
let sessionType = null;
|
|
124
|
+
let agent = agentName;
|
|
125
|
+
let totalCost = 0;
|
|
126
|
+
let totalTokens = 0;
|
|
127
|
+
let totalInputTokens = 0;
|
|
128
|
+
let totalOutputTokens = 0;
|
|
129
|
+
let totalCacheReadTokens = 0;
|
|
130
|
+
let totalCacheWriteTokens = 0;
|
|
131
|
+
let initialPrompt = null;
|
|
132
|
+
let firstMessageId = null;
|
|
133
|
+
let firstMessageTimestamp = null;
|
|
134
|
+
|
|
135
|
+
const firstLine = JSON.parse(lines[0]);
|
|
136
|
+
if (firstLine.type === 'session') {
|
|
137
|
+
sessionId = firstLine.id;
|
|
138
|
+
sessionStart = firstLine.timestamp;
|
|
139
|
+
// Parse agent info from session metadata
|
|
140
|
+
if (firstLine.agent) agent = firstLine.agent;
|
|
141
|
+
if (firstLine.sessionType) sessionType = firstLine.sessionType;
|
|
142
|
+
// Detect sub-agent from ID patterns
|
|
143
|
+
if (sessionId.includes('subagent')) sessionType = 'subagent';
|
|
144
|
+
|
|
145
|
+
stmts.deleteEvents.run(sessionId);
|
|
146
|
+
stmts.deleteSession.run(sessionId);
|
|
147
|
+
stmts.deleteFileActivity.run(sessionId);
|
|
148
|
+
if (stmts.deleteArchive) stmts.deleteArchive.run(sessionId);
|
|
149
|
+
} else {
|
|
150
|
+
return { skipped: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pendingEvents = [];
|
|
154
|
+
const fileActivities = [];
|
|
155
|
+
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
let obj;
|
|
158
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
159
|
+
|
|
160
|
+
if (obj.type === 'session' || obj.type === 'model_change' || obj.type === 'thinking_level_change' || obj.type === 'custom') {
|
|
161
|
+
if (obj.type === 'model_change') model = obj.modelId || model;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (obj.type === 'message' && obj.message) {
|
|
166
|
+
const msg = obj.message;
|
|
167
|
+
const ts = obj.timestamp;
|
|
168
|
+
sessionEnd = ts;
|
|
169
|
+
|
|
170
|
+
// Cost tracking
|
|
171
|
+
if (msg.usage && msg.usage.cost && typeof msg.usage.cost.total === 'number') {
|
|
172
|
+
totalCost += msg.usage.cost.total;
|
|
173
|
+
}
|
|
174
|
+
if (msg.usage && typeof msg.usage.totalTokens === 'number') {
|
|
175
|
+
totalTokens += msg.usage.totalTokens;
|
|
176
|
+
}
|
|
177
|
+
if (msg.usage) {
|
|
178
|
+
if (typeof msg.usage.input === 'number') totalInputTokens += msg.usage.input;
|
|
179
|
+
if (typeof msg.usage.output === 'number') totalOutputTokens += msg.usage.output;
|
|
180
|
+
if (typeof msg.usage.cacheRead === 'number') totalCacheReadTokens += msg.usage.cacheRead;
|
|
181
|
+
if (typeof msg.usage.cacheWrite === 'number') totalCacheWriteTokens += msg.usage.cacheWrite;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const tr = extractToolResult(msg);
|
|
185
|
+
if (tr) {
|
|
186
|
+
pendingEvents.push([obj.id, sessionId, ts, 'tool_result', 'tool', tr.content, tr.toolName, null, tr.content]);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const content = extractContent(msg);
|
|
191
|
+
const role = msg.role || 'unknown';
|
|
192
|
+
|
|
193
|
+
if (content) {
|
|
194
|
+
pendingEvents.push([obj.id, sessionId, ts, 'message', role, content, null, null, null]);
|
|
195
|
+
msgCount++;
|
|
196
|
+
// Better summary: skip heartbeat messages
|
|
197
|
+
if (!summary && role === 'user' && !isHeartbeat(content)) {
|
|
198
|
+
summary = content.slice(0, 200);
|
|
199
|
+
}
|
|
200
|
+
// Capture initial prompt from first substantial user message
|
|
201
|
+
if (!initialPrompt && role === 'user' && content.trim().length > 10 && !isHeartbeat(content)) {
|
|
202
|
+
initialPrompt = content.slice(0, 500); // Limit to 500 chars
|
|
203
|
+
firstMessageId = obj.id;
|
|
204
|
+
firstMessageTimestamp = ts;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const tools = extractToolCalls(msg);
|
|
209
|
+
for (const tool of tools) {
|
|
210
|
+
pendingEvents.push([tool.id || `${obj.id}-${tool.name}`, sessionId, ts, 'tool_call', role, null, tool.name, tool.args, null]);
|
|
211
|
+
toolCount++;
|
|
212
|
+
|
|
213
|
+
// File activity tracking
|
|
214
|
+
const fps = extractFilePaths(tool.name, tool.args);
|
|
215
|
+
for (const fp of fps) {
|
|
216
|
+
const op = tool.name.includes('write') || tool.name === 'Write' ? 'write'
|
|
217
|
+
: tool.name.includes('edit') || tool.name === 'Edit' ? 'edit'
|
|
218
|
+
: 'read';
|
|
219
|
+
fileActivities.push([sessionId, fp, op, ts]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// If no real summary found, check if it's a heartbeat session
|
|
226
|
+
if (!summary) {
|
|
227
|
+
summary = 'Heartbeat session';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
stmts.upsertSession.run(sessionId, sessionStart, sessionEnd, msgCount, toolCount, model, summary, agent, sessionType, totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens, initialPrompt, firstMessageId, firstMessageTimestamp);
|
|
231
|
+
for (const ev of pendingEvents) stmts.insertEvent.run(...ev);
|
|
232
|
+
for (const fa of fileActivities) stmts.insertFileActivity.run(...fa);
|
|
233
|
+
|
|
234
|
+
// Archive mode: store raw JSONL lines
|
|
235
|
+
if (archiveMode && stmts.insertArchive) {
|
|
236
|
+
for (let i = 0; i < lines.length; i++) {
|
|
237
|
+
stmts.insertArchive.run(sessionId, i + 1, lines[i]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
stmts.upsertState.run(filePath, lines.length, mtime);
|
|
242
|
+
|
|
243
|
+
return { sessionId, msgCount, toolCount };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function run() {
|
|
247
|
+
const config = loadConfig();
|
|
248
|
+
init();
|
|
249
|
+
const db = open();
|
|
250
|
+
const archiveMode = config.storage === 'archive';
|
|
251
|
+
|
|
252
|
+
console.log(`AgentActa indexer running in ${config.storage} mode`);
|
|
253
|
+
|
|
254
|
+
const stmts = createStmts(db);
|
|
255
|
+
|
|
256
|
+
const sessionDirs = discoverSessionDirs(config);
|
|
257
|
+
console.log(`Discovered ${sessionDirs.length} session directories:`);
|
|
258
|
+
sessionDirs.forEach(d => console.log(` ${d.agent}: ${d.path}`));
|
|
259
|
+
|
|
260
|
+
let allFiles = [];
|
|
261
|
+
for (const dir of sessionDirs) {
|
|
262
|
+
const files = fs.readdirSync(dir.path)
|
|
263
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
264
|
+
.map(f => ({ path: path.join(dir.path, f), agent: dir.agent }));
|
|
265
|
+
allFiles.push(...files);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(`Found ${allFiles.length} session files`);
|
|
269
|
+
|
|
270
|
+
const indexMany = db.transaction(() => {
|
|
271
|
+
let indexed = 0;
|
|
272
|
+
for (const f of allFiles) {
|
|
273
|
+
const result = indexFile(db, f.path, f.agent, stmts, archiveMode);
|
|
274
|
+
if (!result.skipped) {
|
|
275
|
+
indexed++;
|
|
276
|
+
if (indexed % 10 === 0) process.stdout.write('.');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return indexed;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const count = indexMany();
|
|
283
|
+
console.log(`\nIndexed ${count} sessions`);
|
|
284
|
+
|
|
285
|
+
const stats = db.prepare('SELECT COUNT(*) as sessions FROM sessions').get();
|
|
286
|
+
const evStats = db.prepare('SELECT COUNT(*) as events FROM events').get();
|
|
287
|
+
console.log(`Total: ${stats.sessions} sessions, ${evStats.events} events`);
|
|
288
|
+
|
|
289
|
+
if (WATCH) {
|
|
290
|
+
console.log('\nWatching for changes...');
|
|
291
|
+
for (const dir of sessionDirs) {
|
|
292
|
+
fs.watch(dir.path, { persistent: true }, (eventType, filename) => {
|
|
293
|
+
if (!filename || !filename.endsWith('.jsonl')) return;
|
|
294
|
+
const filePath = path.join(dir.path, filename);
|
|
295
|
+
if (!fs.existsSync(filePath)) return;
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
try {
|
|
298
|
+
const result = indexFile(db, filePath, dir.agent, stmts, archiveMode);
|
|
299
|
+
if (!result.skipped) console.log(`Re-indexed: ${filename} (${dir.agent})`);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
302
|
+
}
|
|
303
|
+
}, 500);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
db.close();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function indexAll(db, config) {
|
|
312
|
+
const sessionDirs = discoverSessionDirs(config);
|
|
313
|
+
const archiveMode = config.storage === 'archive';
|
|
314
|
+
const stmts = createStmts(db);
|
|
315
|
+
let totalSessions = 0;
|
|
316
|
+
for (const dir of sessionDirs) {
|
|
317
|
+
const files = fs.readdirSync(dir.path).filter(f => f.endsWith('.jsonl'));
|
|
318
|
+
for (const file of files) {
|
|
319
|
+
const result = indexFile(db, path.join(dir.path, file), dir.agent, stmts, archiveMode);
|
|
320
|
+
if (!result.skipped) totalSessions++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const stats = db.prepare('SELECT COUNT(*) as sessions FROM sessions').get();
|
|
324
|
+
const evStats = db.prepare('SELECT COUNT(*) as events FROM events').get();
|
|
325
|
+
return { sessions: stats.sessions, events: evStats.events, newSessions: totalSessions };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = { discoverSessionDirs, indexFile, indexAll };
|
|
329
|
+
|
|
330
|
+
if (require.main === module) run();
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentacta",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Audit trail and search engine for AI agent sessions",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentacta": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"indexer.js",
|
|
12
|
+
"db.js",
|
|
13
|
+
"config.js",
|
|
14
|
+
"public/",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node index.js",
|
|
20
|
+
"index": "node indexer.js",
|
|
21
|
+
"test": "node --test tests/*.test.js",
|
|
22
|
+
"demo": "node scripts/seed-demo.js && node index.js --demo",
|
|
23
|
+
"seed-demo": "node scripts/seed-demo.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"ai",
|
|
27
|
+
"agent",
|
|
28
|
+
"audit",
|
|
29
|
+
"search",
|
|
30
|
+
"openclaw",
|
|
31
|
+
"claude",
|
|
32
|
+
"sessions",
|
|
33
|
+
"tool-calls",
|
|
34
|
+
"sqlite",
|
|
35
|
+
"fts5"
|
|
36
|
+
],
|
|
37
|
+
"author": "Miraj Chokshi",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/mirajchokshi/agentacta"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/mirajchokshi/agentacta",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"better-sqlite3": "^11.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"puppeteer-core": "^24.37.3",
|
|
49
|
+
"puppeteer-screen-recorder": "^3.0.6"
|
|
50
|
+
}
|
|
51
|
+
}
|