agentacta 1.1.4 → 1.2.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/README.md +3 -1
- package/config.js +10 -1
- package/index.js +56 -8
- package/indexer.js +20 -14
- package/package.json +1 -1
- package/public/app.js +212 -99
- package/public/index.html +27 -20
- package/public/style.css +1057 -269
package/README.md
CHANGED
|
@@ -108,7 +108,8 @@ On first run, AgentActa creates a config file with sensible defaults at `~/.conf
|
|
|
108
108
|
"port": 4003,
|
|
109
109
|
"storage": "reference",
|
|
110
110
|
"sessionsPath": null,
|
|
111
|
-
"dbPath": "./agentacta.db"
|
|
111
|
+
"dbPath": "./agentacta.db",
|
|
112
|
+
"projectAliases": {}
|
|
112
113
|
}
|
|
113
114
|
```
|
|
114
115
|
|
|
@@ -126,6 +127,7 @@ On first run, AgentActa creates a config file with sensible defaults at `~/.conf
|
|
|
126
127
|
| `AGENTACTA_SESSIONS_PATH` | Auto-detected | Custom sessions directory |
|
|
127
128
|
| `AGENTACTA_DB_PATH` | `./agentacta.db` | Database file location |
|
|
128
129
|
| `AGENTACTA_STORAGE` | `reference` | Storage mode (`reference` or `archive`) |
|
|
130
|
+
| `AGENTACTA_PROJECT_ALIASES_JSON` | unset | JSON object mapping inferred project names (e.g. `{"old-name":"new-name"}`) |
|
|
129
131
|
|
|
130
132
|
## API
|
|
131
133
|
|
package/config.js
CHANGED
|
@@ -18,7 +18,8 @@ const DEFAULTS = {
|
|
|
18
18
|
port: 4003,
|
|
19
19
|
storage: 'reference',
|
|
20
20
|
sessionsPath: null,
|
|
21
|
-
dbPath: './agentacta.db'
|
|
21
|
+
dbPath: './agentacta.db',
|
|
22
|
+
projectAliases: {}
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
function loadConfig() {
|
|
@@ -45,9 +46,17 @@ function loadConfig() {
|
|
|
45
46
|
if (process.env.AGENTACTA_STORAGE) config.storage = process.env.AGENTACTA_STORAGE;
|
|
46
47
|
if (process.env.AGENTACTA_SESSIONS_PATH) config.sessionsPath = process.env.AGENTACTA_SESSIONS_PATH;
|
|
47
48
|
if (process.env.AGENTACTA_DB_PATH) config.dbPath = process.env.AGENTACTA_DB_PATH;
|
|
49
|
+
if (process.env.AGENTACTA_PROJECT_ALIASES_JSON) {
|
|
50
|
+
try {
|
|
51
|
+
config.projectAliases = JSON.parse(process.env.AGENTACTA_PROJECT_ALIASES_JSON);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Warning: Could not parse AGENTACTA_PROJECT_ALIASES_JSON:', err.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
48
56
|
|
|
49
57
|
// Resolve dbPath relative to cwd
|
|
50
58
|
config.dbPath = path.resolve(config.dbPath);
|
|
59
|
+
if (!config.projectAliases || typeof config.projectAliases !== 'object') config.projectAliases = {};
|
|
51
60
|
|
|
52
61
|
return config;
|
|
53
62
|
}
|
package/index.js
CHANGED
|
@@ -97,6 +97,31 @@ function getDbSize() {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function normalizeAgentLabel(agent) {
|
|
101
|
+
if (!agent) return agent;
|
|
102
|
+
if (agent === 'main') return 'openclaw-main';
|
|
103
|
+
if (agent.startsWith('claude-') || agent.startsWith('claude--')) return 'claude-code';
|
|
104
|
+
return agent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function looksLikeSessionId(q) {
|
|
108
|
+
const s = (q || '').trim();
|
|
109
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toFtsQuery(q) {
|
|
113
|
+
const s = (q || '').trim();
|
|
114
|
+
if (!s) return '';
|
|
115
|
+
// Quote each token so dashes and punctuation don't break FTS parsing.
|
|
116
|
+
// Example: abc-def -> "abc-def"
|
|
117
|
+
const tokens = s.match(/"[^"]+"|\S+/g) || [];
|
|
118
|
+
return tokens
|
|
119
|
+
.map(t => t.replace(/^"|"$/g, '').replace(/"/g, '""'))
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.map(t => `"${t}"`)
|
|
122
|
+
.join(' AND ');
|
|
123
|
+
}
|
|
124
|
+
|
|
100
125
|
// Init DB and start watcher
|
|
101
126
|
init();
|
|
102
127
|
const db = open();
|
|
@@ -197,7 +222,11 @@ const server = http.createServer((req, res) => {
|
|
|
197
222
|
const tools = db.prepare("SELECT DISTINCT tool_name FROM events WHERE tool_name IS NOT NULL").all().map(r => r.tool_name);
|
|
198
223
|
const dateRange = db.prepare('SELECT MIN(start_time) as earliest, MAX(start_time) as latest FROM sessions').get();
|
|
199
224
|
const costData = db.prepare('SELECT SUM(total_cost) as cost, SUM(total_tokens) as tokens FROM sessions').get();
|
|
200
|
-
const agents =
|
|
225
|
+
const agents = [...new Set(
|
|
226
|
+
db.prepare('SELECT DISTINCT agent FROM sessions WHERE agent IS NOT NULL').all()
|
|
227
|
+
.map(r => normalizeAgentLabel(r.agent))
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
)];
|
|
201
230
|
const dbSize = getDbSize();
|
|
202
231
|
json(res, { sessions, events, messages, toolCalls, uniqueTools: tools.length, tools, dateRange, totalCost: costData.cost || 0, totalTokens: costData.tokens || 0, agents, storageMode: config.storage, dbSize, sessionDirs: sessionDirs.map(d => ({ path: d.path, agent: d.agent })) });
|
|
203
232
|
}
|
|
@@ -256,7 +285,12 @@ const server = http.createServer((req, res) => {
|
|
|
256
285
|
if (!q) { json(res, { error: 'No query' }, 400); return; }
|
|
257
286
|
let results;
|
|
258
287
|
try {
|
|
259
|
-
|
|
288
|
+
if (looksLikeSessionId(q)) {
|
|
289
|
+
results = db.prepare(`SELECT e.*, s.start_time as session_start, s.summary as session_summary FROM events e JOIN sessions s ON s.id = e.session_id WHERE e.session_id = ? ORDER BY e.timestamp DESC LIMIT 200`).all(q.trim());
|
|
290
|
+
} else {
|
|
291
|
+
const ftsQuery = toFtsQuery(q);
|
|
292
|
+
results = db.prepare(`SELECT e.*, s.start_time as session_start, s.summary as session_summary FROM events_fts fts JOIN events e ON e.rowid = fts.rowid JOIN sessions s ON s.id = e.session_id WHERE events_fts MATCH ? ORDER BY e.timestamp DESC LIMIT 200`).all(ftsQuery);
|
|
293
|
+
}
|
|
260
294
|
} catch (err) { json(res, { error: 'Invalid search query' }, 400); return; }
|
|
261
295
|
if (format === 'md') {
|
|
262
296
|
let md = `# Search Results: "${q}"\n\n${results.length} results\n\n`;
|
|
@@ -280,18 +314,32 @@ const server = http.createServer((req, res) => {
|
|
|
280
314
|
|
|
281
315
|
if (!q) { json(res, { results: [], total: 0 }); }
|
|
282
316
|
else {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
const isSessionLookup = looksLikeSessionId(q);
|
|
318
|
+
let sql;
|
|
319
|
+
const params = [];
|
|
320
|
+
|
|
321
|
+
if (isSessionLookup) {
|
|
322
|
+
sql = `SELECT e.*, s.start_time as session_start, s.summary as session_summary
|
|
323
|
+
FROM events e
|
|
324
|
+
JOIN sessions s ON s.id = e.session_id
|
|
325
|
+
WHERE e.session_id = ?`;
|
|
326
|
+
params.push(q.trim());
|
|
327
|
+
} else {
|
|
328
|
+
sql = `SELECT e.*, s.start_time as session_start, s.summary as session_summary
|
|
329
|
+
FROM events_fts fts
|
|
330
|
+
JOIN events e ON e.rowid = fts.rowid
|
|
331
|
+
JOIN sessions s ON s.id = e.session_id
|
|
332
|
+
WHERE events_fts MATCH ?`;
|
|
333
|
+
params.push(toFtsQuery(q));
|
|
334
|
+
}
|
|
335
|
+
|
|
289
336
|
if (type) { sql += ` AND e.type = ?`; params.push(type); }
|
|
290
337
|
if (role) { sql += ` AND e.role = ?`; params.push(role); }
|
|
291
338
|
if (from) { sql += ` AND e.timestamp >= ?`; params.push(from); }
|
|
292
339
|
if (to) { sql += ` AND e.timestamp <= ?`; params.push(to); }
|
|
293
340
|
sql += ` ORDER BY e.timestamp DESC LIMIT ?`;
|
|
294
341
|
params.push(limit);
|
|
342
|
+
|
|
295
343
|
try {
|
|
296
344
|
const results = db.prepare(sql).all(...params);
|
|
297
345
|
json(res, { results, total: results.length });
|
package/indexer.js
CHANGED
|
@@ -38,12 +38,12 @@ function discoverSessionDirs(config) {
|
|
|
38
38
|
// Claude Code: JSONL files directly in project dir
|
|
39
39
|
if (fs.existsSync(projDir) && fs.statSync(projDir).isDirectory()) {
|
|
40
40
|
const hasJsonl = fs.readdirSync(projDir).some(f => f.endsWith('.jsonl'));
|
|
41
|
-
if (hasJsonl) dirs.push({ path: projDir, agent:
|
|
41
|
+
if (hasJsonl) dirs.push({ path: projDir, agent: 'claude-code' });
|
|
42
42
|
}
|
|
43
43
|
// Also check sessions/ subdirectory (future-proofing)
|
|
44
44
|
const sp = path.join(projDir, 'sessions');
|
|
45
45
|
if (fs.existsSync(sp) && fs.statSync(sp).isDirectory()) {
|
|
46
|
-
dirs.push({ path: sp, agent:
|
|
46
|
+
dirs.push({ path: sp, agent: 'claude-code' });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -107,13 +107,19 @@ function extractFilePaths(toolName, toolArgs) {
|
|
|
107
107
|
return paths;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function
|
|
110
|
+
function aliasProject(project, config) {
|
|
111
|
+
if (!project) return project;
|
|
112
|
+
const aliases = (config && config.projectAliases && typeof config.projectAliases === 'object') ? config.projectAliases : {};
|
|
113
|
+
return aliases[project] || project;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractProjectFromPath(filePath, config) {
|
|
111
117
|
if (!filePath || typeof filePath !== 'string') return null;
|
|
112
118
|
|
|
113
119
|
const normalized = filePath.replace(/\\/g, '/');
|
|
114
120
|
|
|
115
121
|
// Relative paths are usually from workspace cwd -> treat as workspace activity
|
|
116
|
-
if (!normalized.startsWith('/') && !normalized.startsWith('~')) return 'workspace';
|
|
122
|
+
if (!normalized.startsWith('/') && !normalized.startsWith('~')) return aliasProject('workspace', config);
|
|
117
123
|
|
|
118
124
|
let rel = normalized
|
|
119
125
|
.replace(/^\/home\/[^/]+\//, '')
|
|
@@ -124,22 +130,22 @@ function extractProjectFromPath(filePath) {
|
|
|
124
130
|
if (!parts.length) return null;
|
|
125
131
|
|
|
126
132
|
// Common repo location: ~/Developer/<repo>/...
|
|
127
|
-
if (parts[0] === 'Developer' && parts[1]) return parts[1];
|
|
133
|
+
if (parts[0] === 'Developer' && parts[1]) return aliasProject(parts[1], config);
|
|
128
134
|
|
|
129
135
|
// OpenClaw workspace and agent stores
|
|
130
|
-
if (parts[0] === '.openclaw' && parts[1] === 'workspace') return 'workspace';
|
|
131
|
-
if (parts[0] === '.openclaw' && parts[1] === 'agents' && parts[2]) return `agent:${parts[2]}
|
|
136
|
+
if (parts[0] === '.openclaw' && parts[1] === 'workspace') return aliasProject('workspace', config);
|
|
137
|
+
if (parts[0] === '.openclaw' && parts[1] === 'agents' && parts[2]) return aliasProject(`agent:${parts[2]}`, config);
|
|
132
138
|
|
|
133
139
|
// Claude Code projects
|
|
134
|
-
if (parts[0] === '.claude' && parts[1] === 'projects' && parts[2]) return `claude:${parts[2]}
|
|
140
|
+
if (parts[0] === '.claude' && parts[1] === 'projects' && parts[2]) return aliasProject(`claude:${parts[2]}`, config);
|
|
135
141
|
|
|
136
142
|
// Shared files area
|
|
137
|
-
if (parts[0] === 'Shared') return 'shared';
|
|
143
|
+
if (parts[0] === 'Shared') return aliasProject('shared', config);
|
|
138
144
|
|
|
139
145
|
return null;
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
function indexFile(db, filePath, agentName, stmts, archiveMode) {
|
|
148
|
+
function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
143
149
|
const stat = fs.statSync(filePath);
|
|
144
150
|
const mtime = stat.mtime.toISOString();
|
|
145
151
|
|
|
@@ -312,7 +318,7 @@ function indexFile(db, filePath, agentName, stmts, archiveMode) {
|
|
|
312
318
|
: 'read';
|
|
313
319
|
fileActivities.push([sessionId, fp, op, ts]);
|
|
314
320
|
|
|
315
|
-
const project = extractProjectFromPath(fp);
|
|
321
|
+
const project = extractProjectFromPath(fp, config);
|
|
316
322
|
if (project) projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
|
|
317
323
|
}
|
|
318
324
|
}
|
|
@@ -391,7 +397,7 @@ function run() {
|
|
|
391
397
|
const indexMany = db.transaction(() => {
|
|
392
398
|
let indexed = 0;
|
|
393
399
|
for (const f of allFiles) {
|
|
394
|
-
const result = indexFile(db, f.path, f.agent, stmts, archiveMode);
|
|
400
|
+
const result = indexFile(db, f.path, f.agent, stmts, archiveMode, config);
|
|
395
401
|
if (!result.skipped) {
|
|
396
402
|
indexed++;
|
|
397
403
|
if (indexed % 10 === 0) process.stdout.write('.');
|
|
@@ -416,7 +422,7 @@ function run() {
|
|
|
416
422
|
if (!fs.existsSync(filePath)) return;
|
|
417
423
|
setTimeout(() => {
|
|
418
424
|
try {
|
|
419
|
-
const result = indexFile(db, filePath, dir.agent, stmts, archiveMode);
|
|
425
|
+
const result = indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
|
|
420
426
|
if (!result.skipped) console.log(`Re-indexed: ${filename} (${dir.agent})`);
|
|
421
427
|
} catch (err) {
|
|
422
428
|
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
@@ -437,7 +443,7 @@ function indexAll(db, config) {
|
|
|
437
443
|
for (const dir of sessionDirs) {
|
|
438
444
|
const files = fs.readdirSync(dir.path).filter(f => f.endsWith('.jsonl'));
|
|
439
445
|
for (const file of files) {
|
|
440
|
-
const result = indexFile(db, path.join(dir.path, file), dir.agent, stmts, archiveMode);
|
|
446
|
+
const result = indexFile(db, path.join(dir.path, file), dir.agent, stmts, archiveMode, config);
|
|
441
447
|
if (!result.skipped) totalSessions++;
|
|
442
448
|
}
|
|
443
449
|
}
|