agentacta 2026.4.8 → 2026.4.10
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 +13 -13
- package/dist/config.d.ts +4 -0
- package/dist/config.js +91 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +5 -0
- package/dist/db.js +163 -0
- package/dist/db.js.map +1 -0
- package/dist/delta-attribution-context.d.ts +4 -0
- package/dist/delta-attribution-context.js +50 -0
- package/dist/delta-attribution-context.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +843 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.d.ts +8 -0
- package/dist/indexer.js +879 -0
- package/dist/indexer.js.map +1 -0
- package/dist/insights.d.ts +5 -0
- package/dist/insights.js +224 -0
- package/dist/insights.js.map +1 -0
- package/dist/project-attribution.d.ts +6 -0
- package/dist/project-attribution.js +424 -0
- package/dist/project-attribution.js.map +1 -0
- package/dist/types.d.ts +361 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/index.js +1 -853
- package/package.json +18 -14
- package/config.js +0 -85
- package/db.js +0 -152
- package/delta-attribution-context.js +0 -57
- package/indexer.js +0 -865
- package/insights.js +0 -260
- package/project-attribution.js +0 -443
package/dist/index.js
ADDED
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_events_1 = require("node:events");
|
|
10
|
+
const config_js_1 = require("./config.js");
|
|
11
|
+
const db_js_1 = require("./db.js");
|
|
12
|
+
const indexer_js_1 = require("./indexer.js");
|
|
13
|
+
const project_attribution_js_1 = require("./project-attribution.js");
|
|
14
|
+
const delta_attribution_context_js_1 = require("./delta-attribution-context.js");
|
|
15
|
+
const insights_js_1 = require("./insights.js");
|
|
16
|
+
// --version / -v flag: print version and exit
|
|
17
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
18
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
19
|
+
console.log(`${pkg.name} v${pkg.version}`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
// --demo flag: use demo session data (must run before config load)
|
|
23
|
+
if (process.argv.includes('--demo')) {
|
|
24
|
+
const demoDir = node_path_1.default.join(__dirname, '..', 'demo');
|
|
25
|
+
if (!node_fs_1.default.existsSync(demoDir) || node_fs_1.default.readdirSync(demoDir).filter((f) => f.endsWith('.jsonl')).length === 0) {
|
|
26
|
+
console.error('Demo data not found. Run: node scripts/seed-demo.js');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
process.env.AGENTACTA_SESSIONS_PATH = demoDir;
|
|
30
|
+
process.env.AGENTACTA_DB_PATH = node_path_1.default.join(demoDir, 'demo.db');
|
|
31
|
+
process.env.AGENTACTA_DEMO_MODE = '1'; // signal to config.js to skip file-based sessionsPath
|
|
32
|
+
console.log(`Demo mode: using sessions from ${demoDir}`);
|
|
33
|
+
}
|
|
34
|
+
const config = (0, config_js_1.loadConfig)();
|
|
35
|
+
const PORT = config.port;
|
|
36
|
+
const ARCHIVE_MODE = config.storage === 'archive';
|
|
37
|
+
console.log(`AgentActa running in ${config.storage} mode`);
|
|
38
|
+
const PUBLIC = node_path_1.default.join(__dirname, '..', 'public');
|
|
39
|
+
const MIME = {
|
|
40
|
+
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
|
|
41
|
+
'.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml',
|
|
42
|
+
'.ico': 'image/x-icon', '.webmanifest': 'application/manifest+json'
|
|
43
|
+
};
|
|
44
|
+
function json(res, data, status = 200) {
|
|
45
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
46
|
+
res.end(JSON.stringify(data));
|
|
47
|
+
}
|
|
48
|
+
function download(res, data, filename, contentType) {
|
|
49
|
+
res.writeHead(200, {
|
|
50
|
+
'Content-Type': contentType,
|
|
51
|
+
'Content-Disposition': `attachment; filename="${filename}"`
|
|
52
|
+
});
|
|
53
|
+
res.end(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
|
|
54
|
+
}
|
|
55
|
+
function serveStatic(req, res) {
|
|
56
|
+
const reqUrl = req.url || '/';
|
|
57
|
+
let fp = node_path_1.default.join(PUBLIC, reqUrl.split('?')[0] === '/' ? 'index.html' : reqUrl.split('?')[0]);
|
|
58
|
+
fp = node_path_1.default.normalize(fp);
|
|
59
|
+
if (!fp.startsWith(PUBLIC)) {
|
|
60
|
+
res.writeHead(403);
|
|
61
|
+
res.end();
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (node_fs_1.default.existsSync(fp) && node_fs_1.default.statSync(fp).isFile()) {
|
|
65
|
+
const ext = node_path_1.default.extname(fp);
|
|
66
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
|
67
|
+
node_fs_1.default.createReadStream(fp).pipe(res);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function parseQuery(url) {
|
|
73
|
+
const u = new URL(url, 'http://localhost');
|
|
74
|
+
const o = {};
|
|
75
|
+
u.searchParams.forEach((v, k) => { o[k] = v; });
|
|
76
|
+
return { pathname: u.pathname, query: o };
|
|
77
|
+
}
|
|
78
|
+
function sessionToMarkdown(session, events) {
|
|
79
|
+
let md = `# Session: ${session.id}\n`;
|
|
80
|
+
md += `- **Start:** ${session.start_time}\n`;
|
|
81
|
+
md += `- **End:** ${session.end_time || 'N/A'}\n`;
|
|
82
|
+
md += `- **Model:** ${session.model || 'N/A'}\n`;
|
|
83
|
+
md += `- **Agent:** ${session.agent || 'main'}\n`;
|
|
84
|
+
md += `- **Messages:** ${session.message_count} | **Tools:** ${session.tool_count}\n`;
|
|
85
|
+
md += `- **Cost:** $${(session.total_cost || 0).toFixed(4)} | **Tokens:** ${(session.total_tokens || 0).toLocaleString()}\n\n`;
|
|
86
|
+
md += `## Summary\n${session.summary || 'No summary'}\n\n## Events\n\n`;
|
|
87
|
+
for (const e of events) {
|
|
88
|
+
const time = e.timestamp ? new Date(e.timestamp).toISOString() : '';
|
|
89
|
+
if (e.type === 'tool_call') {
|
|
90
|
+
md += `### [${time}] Tool: ${e.tool_name}\n\`\`\`json\n${e.tool_args || ''}\n\`\`\`\n\n`;
|
|
91
|
+
}
|
|
92
|
+
else if (e.type === 'tool_result') {
|
|
93
|
+
md += `### [${time}] Result: ${e.tool_name}\n\`\`\`\n${(e.content || '').slice(0, 2000)}\n\`\`\`\n\n`;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
md += `### [${time}] ${e.role || e.type}\n${e.content || ''}\n\n`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return md;
|
|
100
|
+
}
|
|
101
|
+
function getDbSize() {
|
|
102
|
+
try {
|
|
103
|
+
const stat = node_fs_1.default.statSync(config.dbPath);
|
|
104
|
+
const mb = stat.size / (1024 * 1024);
|
|
105
|
+
return { bytes: stat.size, display: mb >= 1 ? `${mb.toFixed(1)} MB` : `${(stat.size / 1024).toFixed(1)} KB` };
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { bytes: 0, display: 'N/A' };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function relativeTime(ts) {
|
|
112
|
+
if (!ts)
|
|
113
|
+
return null;
|
|
114
|
+
const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
|
|
115
|
+
if (diff < 60)
|
|
116
|
+
return 'just now';
|
|
117
|
+
if (diff < 3600)
|
|
118
|
+
return `${Math.floor(diff / 60)}m ago`;
|
|
119
|
+
if (diff < 86400)
|
|
120
|
+
return `${Math.floor(diff / 3600)}h ago`;
|
|
121
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
122
|
+
}
|
|
123
|
+
function normalizeAgentLabel(agent) {
|
|
124
|
+
if (!agent)
|
|
125
|
+
return agent;
|
|
126
|
+
if (agent === 'main')
|
|
127
|
+
return 'openclaw-main';
|
|
128
|
+
if (agent.startsWith('claude-') || agent.startsWith('claude--'))
|
|
129
|
+
return 'claude-code';
|
|
130
|
+
return agent;
|
|
131
|
+
}
|
|
132
|
+
function looksLikeSessionId(q) {
|
|
133
|
+
const s = (q || '').trim();
|
|
134
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
|
135
|
+
}
|
|
136
|
+
function toFtsQuery(q) {
|
|
137
|
+
const s = (q || '').trim();
|
|
138
|
+
if (!s)
|
|
139
|
+
return '';
|
|
140
|
+
// Quote each token so dashes and punctuation don't break FTS parsing.
|
|
141
|
+
// Example: abc-def -> "abc-def"
|
|
142
|
+
const tokens = s.match(/"[^"]+"|\S+/g) || [];
|
|
143
|
+
return tokens
|
|
144
|
+
.map((t) => t.replace(/^"|"$/g, '').replace(/"/g, '""'))
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.map((t) => `"${t}"`)
|
|
147
|
+
.join(' AND ');
|
|
148
|
+
}
|
|
149
|
+
// Init DB and start watcher
|
|
150
|
+
(0, db_js_1.init)();
|
|
151
|
+
const db = (0, db_js_1.open)();
|
|
152
|
+
// Live re-indexing setup
|
|
153
|
+
const stmts = (0, db_js_1.createStmts)(db);
|
|
154
|
+
// SSE emitter: notifies connected clients when a session is re-indexed
|
|
155
|
+
const sseEmitter = new node_events_1.EventEmitter();
|
|
156
|
+
sseEmitter.setMaxListeners(100);
|
|
157
|
+
const sessionDirs = (0, indexer_js_1.discoverSessionDirs)(config);
|
|
158
|
+
// Initial indexing pass
|
|
159
|
+
for (const dir of sessionDirs) {
|
|
160
|
+
const files = (0, indexer_js_1.listJsonlFiles)(dir.path, !!dir.recursive);
|
|
161
|
+
for (const filePath of files) {
|
|
162
|
+
try {
|
|
163
|
+
const result = (0, indexer_js_1.indexFile)(db, filePath, dir.agent, stmts, ARCHIVE_MODE, config);
|
|
164
|
+
if (!result.skipped)
|
|
165
|
+
console.log(`Indexed: ${node_path_1.default.basename(filePath)} (${dir.agent})`);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
console.error(`Error indexing ${node_path_1.default.basename(filePath)}:`, err.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Compute insights for all indexed sessions
|
|
173
|
+
try {
|
|
174
|
+
(0, insights_js_1.analyzeAll)(db);
|
|
175
|
+
console.log('Insights computed for all sessions');
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.error('Error computing insights:', err.message);
|
|
179
|
+
}
|
|
180
|
+
console.log(`Watching ${sessionDirs.length} session directories`);
|
|
181
|
+
// Debounce map: filePath -> timeout handle
|
|
182
|
+
const _reindexTimers = new Map();
|
|
183
|
+
const REINDEX_DEBOUNCE_MS = 2000;
|
|
184
|
+
const RECURSIVE_RESCAN_MS = 15000;
|
|
185
|
+
function reindexRecursiveDir(dir) {
|
|
186
|
+
try {
|
|
187
|
+
const files = (0, indexer_js_1.listJsonlFiles)(dir.path, true);
|
|
188
|
+
let changed = 0;
|
|
189
|
+
const upsert = db.prepare('INSERT OR REPLACE INTO session_insights (session_id, signals, confusion_score, flagged, computed_at) VALUES (?, ?, ?, ?, ?)');
|
|
190
|
+
for (const filePath of files) {
|
|
191
|
+
const result = (0, indexer_js_1.indexFile)(db, filePath, dir.agent, stmts, ARCHIVE_MODE, config);
|
|
192
|
+
if (!result.skipped) {
|
|
193
|
+
changed++;
|
|
194
|
+
if (result.sessionId) {
|
|
195
|
+
try {
|
|
196
|
+
const insight = (0, insights_js_1.analyzeSession)(db, result.sessionId);
|
|
197
|
+
if (insight)
|
|
198
|
+
upsert.run(insight.session_id, JSON.stringify(insight.signals), insight.confusion_score, insight.flagged ? 1 : 0, insight.computed_at);
|
|
199
|
+
}
|
|
200
|
+
catch { /* ignore */ }
|
|
201
|
+
sseEmitter.emit('session-update', result.sessionId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (changed > 0)
|
|
206
|
+
console.log(`Live re-indexed ${changed} files (${dir.agent})`);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
console.error(`Error rescanning ${dir.path}:`, err.message);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const dir of sessionDirs) {
|
|
213
|
+
try {
|
|
214
|
+
node_fs_1.default.watch(dir.path, { persistent: false }, (_eventType, filename) => {
|
|
215
|
+
if (dir.recursive) {
|
|
216
|
+
if (_reindexTimers.has(dir.path))
|
|
217
|
+
clearTimeout(_reindexTimers.get(dir.path));
|
|
218
|
+
_reindexTimers.set(dir.path, setTimeout(() => {
|
|
219
|
+
_reindexTimers.delete(dir.path);
|
|
220
|
+
reindexRecursiveDir(dir);
|
|
221
|
+
}, REINDEX_DEBOUNCE_MS));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!filename || !filename.endsWith('.jsonl'))
|
|
225
|
+
return;
|
|
226
|
+
const filePath = node_path_1.default.join(dir.path, filename);
|
|
227
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
228
|
+
return;
|
|
229
|
+
// Debounce: cancel pending re-index for this file, schedule a new one
|
|
230
|
+
if (_reindexTimers.has(filePath))
|
|
231
|
+
clearTimeout(_reindexTimers.get(filePath));
|
|
232
|
+
_reindexTimers.set(filePath, setTimeout(() => {
|
|
233
|
+
_reindexTimers.delete(filePath);
|
|
234
|
+
try {
|
|
235
|
+
const result = (0, indexer_js_1.indexFile)(db, filePath, dir.agent, stmts, ARCHIVE_MODE, config);
|
|
236
|
+
if (!result.skipped) {
|
|
237
|
+
console.log(`Live re-indexed: ${filename} (${dir.agent})`);
|
|
238
|
+
if (result.sessionId) {
|
|
239
|
+
try {
|
|
240
|
+
const upsert = db.prepare('INSERT OR REPLACE INTO session_insights (session_id, signals, confusion_score, flagged, computed_at) VALUES (?, ?, ?, ?, ?)');
|
|
241
|
+
const insight = (0, insights_js_1.analyzeSession)(db, result.sessionId);
|
|
242
|
+
if (insight)
|
|
243
|
+
upsert.run(insight.session_id, JSON.stringify(insight.signals), insight.confusion_score, insight.flagged ? 1 : 0, insight.computed_at);
|
|
244
|
+
}
|
|
245
|
+
catch { /* ignore */ }
|
|
246
|
+
sseEmitter.emit('session-update', result.sessionId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
252
|
+
}
|
|
253
|
+
}, REINDEX_DEBOUNCE_MS));
|
|
254
|
+
});
|
|
255
|
+
console.log(` Watching: ${dir.path}`);
|
|
256
|
+
if (dir.recursive) {
|
|
257
|
+
const timer = setInterval(() => reindexRecursiveDir(dir), RECURSIVE_RESCAN_MS);
|
|
258
|
+
timer.unref?.();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
console.error(` Failed to watch ${dir.path}:`, err.message);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const server = node_http_1.default.createServer((req, res) => {
|
|
266
|
+
const { pathname, query } = parseQuery(req.url || '/');
|
|
267
|
+
try {
|
|
268
|
+
if (pathname === '/api/reindex') {
|
|
269
|
+
const result = (0, indexer_js_1.indexAll)(db, config);
|
|
270
|
+
try {
|
|
271
|
+
(0, insights_js_1.analyzeAll)(db);
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
console.error('Insights recompute error:', e.message);
|
|
275
|
+
}
|
|
276
|
+
return json(res, { ok: true, sessions: result.sessions, events: result.events });
|
|
277
|
+
}
|
|
278
|
+
else if (pathname === '/api/health') {
|
|
279
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
280
|
+
const sessions = db.prepare('SELECT COUNT(*) as c FROM sessions').get().c;
|
|
281
|
+
const dbSize = getDbSize();
|
|
282
|
+
return json(res, {
|
|
283
|
+
status: 'ok',
|
|
284
|
+
version: pkg.version,
|
|
285
|
+
uptime: Math.round(process.uptime()),
|
|
286
|
+
sessions,
|
|
287
|
+
dbSizeBytes: dbSize.bytes,
|
|
288
|
+
node: process.version
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else if (pathname === '/api/config') {
|
|
292
|
+
const dbSize = getDbSize();
|
|
293
|
+
const archiveCount = db.prepare('SELECT COUNT(*) as c FROM archive').get().c;
|
|
294
|
+
json(res, {
|
|
295
|
+
storage: config.storage,
|
|
296
|
+
port: config.port,
|
|
297
|
+
dbPath: config.dbPath,
|
|
298
|
+
dbSize: dbSize,
|
|
299
|
+
sessionsPath: config.sessionsPath,
|
|
300
|
+
sessionDirs: sessionDirs.map((d) => ({ path: d.path, agent: d.agent })),
|
|
301
|
+
archiveEnabled: ARCHIVE_MODE,
|
|
302
|
+
archiveRows: archiveCount
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
else if (pathname === '/api/suggestions') {
|
|
306
|
+
// Top tool names (most used)
|
|
307
|
+
const tools = db.prepare("SELECT tool_name, COUNT(*) as c FROM events WHERE tool_name IS NOT NULL GROUP BY tool_name ORDER BY c DESC LIMIT 5").all().map((r) => r.tool_name);
|
|
308
|
+
// Most touched files (short basenames)
|
|
309
|
+
const files = db.prepare("SELECT file_path, COUNT(*) as c FROM file_activity GROUP BY file_path ORDER BY c DESC LIMIT 5").all().map((r) => {
|
|
310
|
+
const parts = r.file_path.split('/');
|
|
311
|
+
return parts[parts.length - 1];
|
|
312
|
+
}).filter((f) => f.length <= 25);
|
|
313
|
+
// Recent session summary words (crude topic extraction)
|
|
314
|
+
const summaries = db.prepare("SELECT summary FROM sessions WHERE summary IS NOT NULL ORDER BY start_time DESC LIMIT 20").all().map((r) => r.summary).join(' ');
|
|
315
|
+
const wordFreq = {};
|
|
316
|
+
const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'is', 'it', 'that', 'this', 'with', 'was', 'are', 'be', 'has', 'had', 'not', 'no', 'from', 'by', 'as', 'do', 'if', 'so', 'up', 'out', 'then', 'than', 'into', 'its', 'my', 'we', 'he', 'she', 'they', 'you', 'i', 'me', 'all', 'just', 'can', 'will', 'about', 'been', 'have', 'some', 'when', 'would', 'there', 'what', 'which', 'who', 'how', 'each', 'other', 'new', 'old', 'also', 'back', 'after', 'use', 'two', 'way', 'could', 'make', 'like', 'time', 'very', 'your', 'did', 'get', 'made', 'find', 'here', 'thing', 'many', 'well', 'only', 'any', 'those', 'over', 'such', 'our', 'them', 'his', 'her', 'one', 'file', 'files', 'session', 'sessions', 'agent', 'tool', 'message', 'messages', 'run', 'work', 'set', 'used', 'added', 'updated', 'using', 'based', 'check', 'cst', 'est', 'pst', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'mon', 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec', 'via', 'per', 'yet', 'ago', 'etc', 'got']);
|
|
317
|
+
summaries.toLowerCase().replace(/[^a-z\s-]/g, '').split(/\s+/).filter((w) => w.length > 3 && w.length < 20 && !stopWords.has(w) && !/id$/.test(w)).forEach((w) => { wordFreq[w] = (wordFreq[w] || 0) + 1; });
|
|
318
|
+
const topics = Object.entries(wordFreq).sort((a, b) => b[1] - a[1]).slice(0, 5).map((e) => e[0]);
|
|
319
|
+
// Deduplicate and pick up to 8
|
|
320
|
+
const seen = new Set();
|
|
321
|
+
const suggestions = [];
|
|
322
|
+
for (const s of [...tools, ...topics, ...files]) {
|
|
323
|
+
const key = s.toLowerCase();
|
|
324
|
+
if (!seen.has(key) && suggestions.length < 8) {
|
|
325
|
+
seen.add(key);
|
|
326
|
+
suggestions.push(s);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
json(res, { suggestions });
|
|
330
|
+
}
|
|
331
|
+
else if (pathname === '/api/stats') {
|
|
332
|
+
const sessions = db.prepare('SELECT COUNT(*) as c FROM sessions').get().c;
|
|
333
|
+
const events = db.prepare('SELECT COUNT(*) as c FROM events').get().c;
|
|
334
|
+
const messages = db.prepare("SELECT COUNT(*) as c FROM events WHERE type='message'").get().c;
|
|
335
|
+
const toolCalls = db.prepare("SELECT COUNT(*) as c FROM events WHERE type='tool_call'").get().c;
|
|
336
|
+
const tools = db.prepare("SELECT DISTINCT tool_name FROM events WHERE tool_name IS NOT NULL").all().map((r) => r.tool_name);
|
|
337
|
+
const dateRange = db.prepare('SELECT MIN(start_time) as earliest, MAX(start_time) as latest FROM sessions').get();
|
|
338
|
+
const costData = db.prepare('SELECT SUM(total_cost) as cost, SUM(total_tokens) as tokens FROM sessions').get();
|
|
339
|
+
const agents = [...new Set(db.prepare('SELECT DISTINCT agent FROM sessions WHERE agent IS NOT NULL').all()
|
|
340
|
+
.map((r) => normalizeAgentLabel(r.agent))
|
|
341
|
+
.filter((a) => a !== null))];
|
|
342
|
+
const dbSize = getDbSize();
|
|
343
|
+
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 })) });
|
|
344
|
+
}
|
|
345
|
+
else if (pathname === '/api/sessions') {
|
|
346
|
+
const limit = parseInt(query.limit) || 50;
|
|
347
|
+
const offset = parseInt(query.offset) || 0;
|
|
348
|
+
const agent = query.agent || '';
|
|
349
|
+
let sql = 'SELECT * FROM sessions';
|
|
350
|
+
const params = [];
|
|
351
|
+
if (agent) {
|
|
352
|
+
sql += ' WHERE agent = ?';
|
|
353
|
+
params.push(agent);
|
|
354
|
+
}
|
|
355
|
+
sql += ' ORDER BY COALESCE(end_time, start_time) DESC LIMIT ? OFFSET ?';
|
|
356
|
+
params.push(limit, offset);
|
|
357
|
+
const rows = db.prepare(sql).all(...params);
|
|
358
|
+
const countSql = agent ? 'SELECT COUNT(*) as c FROM sessions WHERE agent = ?' : 'SELECT COUNT(*) as c FROM sessions';
|
|
359
|
+
const total = agent ? db.prepare(countSql).get(agent).c : db.prepare(countSql).get().c;
|
|
360
|
+
json(res, { sessions: rows, total, limit, offset });
|
|
361
|
+
}
|
|
362
|
+
else if (pathname.match(/^\/api\/sessions\/[^/]+\/events$/)) {
|
|
363
|
+
const id = pathname.split('/')[3];
|
|
364
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
365
|
+
if (!session)
|
|
366
|
+
return json(res, { error: 'Not found' }, 404);
|
|
367
|
+
const after = query.after || '1970-01-01T00:00:00.000Z';
|
|
368
|
+
const afterId = query.afterId || '';
|
|
369
|
+
const limit = Math.min(parseInt(query.limit || '50', 10) || 50, 200);
|
|
370
|
+
const rows = db.prepare(`SELECT * FROM events
|
|
371
|
+
WHERE session_id = ?
|
|
372
|
+
AND (timestamp > ? OR (timestamp = ? AND id > ?))
|
|
373
|
+
ORDER BY timestamp ASC, id ASC
|
|
374
|
+
LIMIT ?`).all(id, after, after, afterId, limit);
|
|
375
|
+
const contextRows = (0, delta_attribution_context_js_1.loadDeltaAttributionContext)(db, id, rows);
|
|
376
|
+
const events = (0, project_attribution_js_1.attributeEventDelta)(session, rows, contextRows);
|
|
377
|
+
json(res, { events, after, afterId, count: events.length });
|
|
378
|
+
}
|
|
379
|
+
else if (pathname.match(/^\/api\/sessions\/[^/]+\/stream$/)) {
|
|
380
|
+
const id = pathname.split('/')[3];
|
|
381
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
382
|
+
if (!session)
|
|
383
|
+
return json(res, { error: 'Not found' }, 404);
|
|
384
|
+
res.writeHead(200, {
|
|
385
|
+
'Content-Type': 'text/event-stream',
|
|
386
|
+
'Cache-Control': 'no-cache',
|
|
387
|
+
'Connection': 'keep-alive',
|
|
388
|
+
'X-Accel-Buffering': 'no'
|
|
389
|
+
});
|
|
390
|
+
res.write(': connected\n\n');
|
|
391
|
+
let lastTs = req.headers['last-event-id'] || query.after || new Date().toISOString();
|
|
392
|
+
const onUpdate = (sessionId) => {
|
|
393
|
+
if (sessionId !== id)
|
|
394
|
+
return;
|
|
395
|
+
try {
|
|
396
|
+
const rows = db.prepare('SELECT * FROM events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp ASC').all(id, lastTs);
|
|
397
|
+
if (rows.length) {
|
|
398
|
+
const contextRows = (0, delta_attribution_context_js_1.loadDeltaAttributionContext)(db, id, rows);
|
|
399
|
+
const attributedRows = (0, project_attribution_js_1.attributeEventDelta)(session, rows, contextRows);
|
|
400
|
+
lastTs = rows[rows.length - 1].timestamp;
|
|
401
|
+
res.write(`id: ${lastTs}\ndata: ${JSON.stringify(attributedRows)}\n\n`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
console.error('SSE query error:', err.message);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
sseEmitter.on('session-update', onUpdate);
|
|
409
|
+
const ping = setInterval(() => {
|
|
410
|
+
try {
|
|
411
|
+
res.write(': ping\n\n');
|
|
412
|
+
}
|
|
413
|
+
catch { /* ignore */ }
|
|
414
|
+
}, 30000);
|
|
415
|
+
req.on('close', () => {
|
|
416
|
+
sseEmitter.off('session-update', onUpdate);
|
|
417
|
+
clearInterval(ping);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
else if (pathname.match(/^\/api\/sessions\/[^/]+$/) && !pathname.includes('export')) {
|
|
421
|
+
const id = pathname.split('/')[3];
|
|
422
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
423
|
+
if (!session) {
|
|
424
|
+
json(res, { error: 'Not found' }, 404);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
const events = db.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY timestamp DESC').all(id);
|
|
428
|
+
const attributed = (0, project_attribution_js_1.attributeSessionEvents)(session, events);
|
|
429
|
+
const hasArchive = ARCHIVE_MODE && db.prepare('SELECT COUNT(*) as c FROM archive WHERE session_id = ?').get(id).c > 0;
|
|
430
|
+
json(res, { session, events: attributed.events, projectFilters: attributed.projectFilters, hasArchive });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (pathname.match(/^\/api\/archive\/session\/[^/]+$/)) {
|
|
434
|
+
const id = pathname.split('/')[4];
|
|
435
|
+
const rows = db.prepare('SELECT * FROM archive WHERE session_id = ? ORDER BY line_number ASC').all(id);
|
|
436
|
+
if (!rows.length) {
|
|
437
|
+
json(res, { error: 'No archive data for this session' }, 404);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
json(res, { session_id: id, lines: rows.map((r) => ({ line_number: r.line_number, data: JSON.parse(r.raw_json) })) });
|
|
441
|
+
}
|
|
442
|
+
else if (pathname.match(/^\/api\/archive\/export\/[^/]+$/)) {
|
|
443
|
+
const id = pathname.split('/')[4];
|
|
444
|
+
const rows = db.prepare('SELECT raw_json FROM archive WHERE session_id = ? ORDER BY line_number ASC').all(id);
|
|
445
|
+
if (!rows.length) {
|
|
446
|
+
json(res, { error: 'No archive data for this session' }, 404);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const jsonl = rows.map((r) => r.raw_json).join('\n') + '\n';
|
|
450
|
+
download(res, jsonl, `session-${id.slice(0, 8)}.jsonl`, 'application/x-ndjson');
|
|
451
|
+
}
|
|
452
|
+
else if (pathname.match(/^\/api\/export\/session\/[^/]+$/)) {
|
|
453
|
+
const id = pathname.split('/')[4];
|
|
454
|
+
const format = query.format || 'json';
|
|
455
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
456
|
+
if (!session) {
|
|
457
|
+
json(res, { error: 'Not found' }, 404);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const events = db.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC').all(id);
|
|
461
|
+
if (format === 'md') {
|
|
462
|
+
download(res, sessionToMarkdown(session, events), `session-${id.slice(0, 8)}.md`, 'text/markdown');
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
download(res, { session, events }, `session-${id.slice(0, 8)}.json`, 'application/json');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (pathname === '/api/export/search') {
|
|
469
|
+
const q = query.q || '';
|
|
470
|
+
const format = query.format || 'json';
|
|
471
|
+
if (!q) {
|
|
472
|
+
json(res, { error: 'No query' }, 400);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
let results;
|
|
476
|
+
try {
|
|
477
|
+
if (looksLikeSessionId(q)) {
|
|
478
|
+
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());
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
const ftsQuery = toFtsQuery(q);
|
|
482
|
+
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);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
json(res, { error: 'Invalid search query' }, 400);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (format === 'md') {
|
|
490
|
+
let md = `# Search Results: "${q}"\n\n${results.length} results\n\n`;
|
|
491
|
+
for (const r of results) {
|
|
492
|
+
md += `## [${r.timestamp}] ${r.type} (${r.role || ''})\n`;
|
|
493
|
+
md += `Session: ${r.session_id}\n\n`;
|
|
494
|
+
md += `${r.content || r.tool_args || r.tool_result || ''}\n\n---\n\n`;
|
|
495
|
+
}
|
|
496
|
+
download(res, md, `search-${q.slice(0, 20)}.md`, 'text/markdown');
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
download(res, { query: q, results }, `search-${q.slice(0, 20)}.json`, 'application/json');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else if (pathname === '/api/search') {
|
|
503
|
+
const q = query.q || '';
|
|
504
|
+
const type = query.type || '';
|
|
505
|
+
const role = query.role || '';
|
|
506
|
+
const from = query.from || '';
|
|
507
|
+
const to = query.to || '';
|
|
508
|
+
const limit = Math.min(parseInt(query.limit) || 50, 200);
|
|
509
|
+
if (!q) {
|
|
510
|
+
json(res, { results: [], total: 0 });
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const isSessionLookup = looksLikeSessionId(q);
|
|
514
|
+
let sql;
|
|
515
|
+
const params = [];
|
|
516
|
+
if (isSessionLookup) {
|
|
517
|
+
sql = `SELECT e.*, s.start_time as session_start, s.summary as session_summary
|
|
518
|
+
FROM events e
|
|
519
|
+
JOIN sessions s ON s.id = e.session_id
|
|
520
|
+
WHERE e.session_id = ?`;
|
|
521
|
+
params.push(q.trim());
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
sql = `SELECT e.*, s.start_time as session_start, s.summary as session_summary
|
|
525
|
+
FROM events_fts fts
|
|
526
|
+
JOIN events e ON e.rowid = fts.rowid
|
|
527
|
+
JOIN sessions s ON s.id = e.session_id
|
|
528
|
+
WHERE events_fts MATCH ?`;
|
|
529
|
+
params.push(toFtsQuery(q));
|
|
530
|
+
}
|
|
531
|
+
if (type) {
|
|
532
|
+
sql += ` AND e.type = ?`;
|
|
533
|
+
params.push(type);
|
|
534
|
+
}
|
|
535
|
+
if (role) {
|
|
536
|
+
sql += ` AND e.role = ?`;
|
|
537
|
+
params.push(role);
|
|
538
|
+
}
|
|
539
|
+
if (from) {
|
|
540
|
+
sql += ` AND e.timestamp >= ?`;
|
|
541
|
+
params.push(from);
|
|
542
|
+
}
|
|
543
|
+
if (to) {
|
|
544
|
+
sql += ` AND e.timestamp <= ?`;
|
|
545
|
+
params.push(to);
|
|
546
|
+
}
|
|
547
|
+
sql += ` ORDER BY e.timestamp DESC LIMIT ?`;
|
|
548
|
+
params.push(limit);
|
|
549
|
+
try {
|
|
550
|
+
const results = db.prepare(sql).all(...params);
|
|
551
|
+
json(res, { results, total: results.length });
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
json(res, { error: err.message, results: [], total: 0 }, 400);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else if (pathname === '/api/timeline') {
|
|
559
|
+
const date = query.date || (() => { const n = new Date(); return `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, '0')}-${String(n.getDate()).padStart(2, '0')}`; })();
|
|
560
|
+
const from = new Date(date + 'T00:00:00').toISOString();
|
|
561
|
+
const to = new Date(date + 'T23:59:59.999').toISOString();
|
|
562
|
+
const limit = Math.min(parseInt(query.limit || '100', 10) || 100, 500);
|
|
563
|
+
const offset = Math.max(parseInt(query.offset || '0', 10) || 0, 0);
|
|
564
|
+
const events = db.prepare(`SELECT e.*, s.summary as session_summary FROM events e
|
|
565
|
+
JOIN sessions s ON s.id = e.session_id
|
|
566
|
+
WHERE e.timestamp >= ? AND e.timestamp <= ?
|
|
567
|
+
ORDER BY e.timestamp DESC
|
|
568
|
+
LIMIT ? OFFSET ?`).all(from, to, limit, offset);
|
|
569
|
+
const total = db.prepare(`SELECT COUNT(*) as c FROM events e
|
|
570
|
+
WHERE e.timestamp >= ? AND e.timestamp <= ?`).get(from, to).c;
|
|
571
|
+
json(res, { date, events, total, limit, offset, hasMore: offset + events.length < total });
|
|
572
|
+
}
|
|
573
|
+
else if (pathname === '/api/timeline/stream') {
|
|
574
|
+
res.writeHead(200, {
|
|
575
|
+
'Content-Type': 'text/event-stream',
|
|
576
|
+
'Cache-Control': 'no-cache',
|
|
577
|
+
'Connection': 'keep-alive',
|
|
578
|
+
'X-Accel-Buffering': 'no'
|
|
579
|
+
});
|
|
580
|
+
res.write(': connected\n\n');
|
|
581
|
+
let lastTs = query.after || new Date().toISOString();
|
|
582
|
+
let lastId = query.afterId || '';
|
|
583
|
+
const onUpdate = () => {
|
|
584
|
+
try {
|
|
585
|
+
const rows = db.prepare(`SELECT e.*, s.summary as session_summary FROM events e
|
|
586
|
+
JOIN sessions s ON s.id = e.session_id
|
|
587
|
+
WHERE (e.timestamp > ?) OR (e.timestamp = ? AND e.id > ?)
|
|
588
|
+
ORDER BY e.timestamp ASC, e.id ASC`).all(lastTs, lastTs, lastId);
|
|
589
|
+
if (rows.length) {
|
|
590
|
+
const tail = rows[rows.length - 1];
|
|
591
|
+
lastTs = tail.timestamp || lastTs;
|
|
592
|
+
lastId = tail.id || lastId;
|
|
593
|
+
res.write(`id: ${lastTs}:${lastId}\ndata: ${JSON.stringify(rows)}\n\n`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
console.error('Timeline SSE error:', err.message);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
sseEmitter.on('session-update', onUpdate);
|
|
601
|
+
const ping = setInterval(() => {
|
|
602
|
+
try {
|
|
603
|
+
res.write(': ping\n\n');
|
|
604
|
+
}
|
|
605
|
+
catch { /* ignore */ }
|
|
606
|
+
}, 30000);
|
|
607
|
+
req.on('close', () => {
|
|
608
|
+
sseEmitter.off('session-update', onUpdate);
|
|
609
|
+
clearInterval(ping);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
else if (pathname === '/api/maintenance') {
|
|
613
|
+
if (req.method !== 'POST')
|
|
614
|
+
return json(res, { error: 'Method not allowed' }, 405);
|
|
615
|
+
const sizeBefore = getDbSize();
|
|
616
|
+
db.pragma('wal_checkpoint(TRUNCATE)');
|
|
617
|
+
db.exec('VACUUM');
|
|
618
|
+
const sizeAfter = getDbSize();
|
|
619
|
+
json(res, { ok: true, sizeBefore, sizeAfter });
|
|
620
|
+
}
|
|
621
|
+
// --- Context API ---
|
|
622
|
+
else if (pathname === '/api/context/file') {
|
|
623
|
+
const fp = query.path || '';
|
|
624
|
+
if (!fp)
|
|
625
|
+
return json(res, { error: 'path parameter is required' }, 400);
|
|
626
|
+
const sessionCount = db.prepare('SELECT COUNT(DISTINCT session_id) as c FROM file_activity WHERE file_path = ?').get(fp).c;
|
|
627
|
+
if (sessionCount === 0) {
|
|
628
|
+
return json(res, { file: fp, sessionCount: 0, lastModified: null, recentChanges: [], operations: {}, relatedFiles: [], recentErrors: [] });
|
|
629
|
+
}
|
|
630
|
+
const lastTouched = db.prepare('SELECT MAX(timestamp) as t FROM file_activity WHERE file_path = ?').get(fp).t;
|
|
631
|
+
const recentChanges = db.prepare(`SELECT DISTINCT s.summary FROM file_activity fa
|
|
632
|
+
JOIN sessions s ON s.id = fa.session_id
|
|
633
|
+
WHERE fa.file_path = ? AND s.summary IS NOT NULL
|
|
634
|
+
ORDER BY s.start_time DESC LIMIT 5`).all(fp).map((r) => r.summary);
|
|
635
|
+
const opsRows = db.prepare('SELECT operation, COUNT(*) as c FROM file_activity WHERE file_path = ? GROUP BY operation').all(fp);
|
|
636
|
+
const operations = {};
|
|
637
|
+
for (const r of opsRows)
|
|
638
|
+
operations[r.operation] = r.c;
|
|
639
|
+
const relatedFiles = db.prepare(`SELECT fa2.file_path, COUNT(DISTINCT fa1.session_id) as c
|
|
640
|
+
FROM file_activity fa1
|
|
641
|
+
JOIN file_activity fa2 ON fa1.session_id = fa2.session_id
|
|
642
|
+
WHERE fa1.file_path = ? AND fa2.file_path != ?
|
|
643
|
+
GROUP BY fa2.file_path
|
|
644
|
+
ORDER BY c DESC LIMIT 5`).all(fp, fp).map((r) => ({ path: r.file_path, count: r.c }));
|
|
645
|
+
const sessionIds = db.prepare('SELECT DISTINCT session_id FROM file_activity WHERE file_path = ?').all(fp).map((r) => r.session_id);
|
|
646
|
+
let recentErrors = [];
|
|
647
|
+
if (sessionIds.length) {
|
|
648
|
+
const placeholders = sessionIds.map(() => '?').join(',');
|
|
649
|
+
recentErrors = db.prepare(`SELECT tool_result FROM events
|
|
650
|
+
WHERE session_id IN (${placeholders})
|
|
651
|
+
AND tool_result IS NOT NULL
|
|
652
|
+
AND (tool_result LIKE '%error%' OR tool_result LIKE '%Error%' OR tool_result LIKE '%ERROR%')
|
|
653
|
+
ORDER BY timestamp DESC LIMIT 3`).all(...sessionIds).map((r) => r.tool_result.slice(0, 200));
|
|
654
|
+
}
|
|
655
|
+
return json(res, {
|
|
656
|
+
file: fp, sessionCount,
|
|
657
|
+
lastModified: relativeTime(lastTouched),
|
|
658
|
+
recentChanges, operations, relatedFiles, recentErrors
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
else if (pathname === '/api/context/repo') {
|
|
662
|
+
const repoPath = query.path || '';
|
|
663
|
+
if (!repoPath)
|
|
664
|
+
return json(res, { error: 'path parameter is required' }, 400);
|
|
665
|
+
// Find sessions matching the repo path via file_activity or initial_prompt
|
|
666
|
+
const sessionIds = db.prepare(`SELECT DISTINCT session_id FROM file_activity WHERE file_path = ? OR file_path LIKE ?`).all(repoPath, repoPath + '/%').map((r) => r.session_id);
|
|
667
|
+
const promptSessions = db.prepare(`SELECT id FROM sessions WHERE initial_prompt LIKE ?`).all('%' + repoPath + '%').map((r) => r.id);
|
|
668
|
+
const allIds = [...new Set([...sessionIds, ...promptSessions])];
|
|
669
|
+
if (allIds.length === 0) {
|
|
670
|
+
return json(res, { repo: repoPath, sessionCount: 0, totalCost: 0, totalTokens: 0, agents: [], topFiles: [], recentSessions: [], commonTools: [], commonErrors: [] });
|
|
671
|
+
}
|
|
672
|
+
const ph = allIds.map(() => '?').join(',');
|
|
673
|
+
const agg = db.prepare(`SELECT COUNT(*) as c, SUM(total_cost) as cost, SUM(total_tokens) as tokens
|
|
674
|
+
FROM sessions WHERE id IN (${ph})`).get(...allIds);
|
|
675
|
+
const agents = [...new Set(db.prepare(`SELECT DISTINCT agent FROM sessions WHERE id IN (${ph}) AND agent IS NOT NULL`).all(...allIds)
|
|
676
|
+
.map((r) => normalizeAgentLabel(r.agent)).filter((a) => a !== null))];
|
|
677
|
+
const topFiles = db.prepare(`SELECT file_path, COUNT(*) as c FROM file_activity
|
|
678
|
+
WHERE session_id IN (${ph})
|
|
679
|
+
GROUP BY file_path ORDER BY c DESC LIMIT 10`).all(...allIds).map((r) => ({ path: r.file_path, count: r.c }));
|
|
680
|
+
const recentSessions = db.prepare(`SELECT id, summary, agent, start_time, end_time FROM sessions
|
|
681
|
+
WHERE id IN (${ph})
|
|
682
|
+
ORDER BY start_time DESC LIMIT 5`).all(...allIds).map((r) => ({
|
|
683
|
+
id: r.id, summary: r.summary, agent: normalizeAgentLabel(r.agent),
|
|
684
|
+
timestamp: r.start_time, status: r.end_time ? 'completed' : 'in-progress'
|
|
685
|
+
}));
|
|
686
|
+
const commonTools = db.prepare(`SELECT tool_name, COUNT(*) as c FROM events
|
|
687
|
+
WHERE session_id IN (${ph}) AND tool_name IS NOT NULL
|
|
688
|
+
GROUP BY tool_name ORDER BY c DESC LIMIT 10`).all(...allIds).map((r) => ({ tool: r.tool_name, count: r.c }));
|
|
689
|
+
const commonErrors = db.prepare(`SELECT DISTINCT SUBSTR(tool_result, 1, 200) as err FROM events
|
|
690
|
+
WHERE session_id IN (${ph})
|
|
691
|
+
AND tool_result IS NOT NULL
|
|
692
|
+
AND (tool_result LIKE '%error%' OR tool_result LIKE '%Error%' OR tool_result LIKE '%ERROR%')
|
|
693
|
+
ORDER BY timestamp DESC LIMIT 5`).all(...allIds).map((r) => r.err);
|
|
694
|
+
return json(res, {
|
|
695
|
+
repo: repoPath, sessionCount: allIds.length,
|
|
696
|
+
totalCost: agg.cost || 0, totalTokens: agg.tokens || 0,
|
|
697
|
+
agents, topFiles, recentSessions, commonTools, commonErrors
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
else if (pathname === '/api/context/agent') {
|
|
701
|
+
const name = query.name || '';
|
|
702
|
+
if (!name)
|
|
703
|
+
return json(res, { error: 'name parameter is required' }, 400);
|
|
704
|
+
// Try exact match first, then check all sessions with normalized label match
|
|
705
|
+
let sessions = db.prepare('SELECT * FROM sessions WHERE agent = ?').all(name);
|
|
706
|
+
if (sessions.length === 0) {
|
|
707
|
+
sessions = db.prepare('SELECT * FROM sessions WHERE agent IS NOT NULL').all()
|
|
708
|
+
.filter((s) => normalizeAgentLabel(s.agent) === name);
|
|
709
|
+
}
|
|
710
|
+
if (sessions.length === 0) {
|
|
711
|
+
return json(res, { agent: name, sessionCount: 0, totalCost: 0, avgDuration: 0, topTools: [], recentSessions: [], successRate: 0 });
|
|
712
|
+
}
|
|
713
|
+
const totalCost = sessions.reduce((sum, r) => sum + (r.total_cost || 0), 0);
|
|
714
|
+
let totalDuration = 0;
|
|
715
|
+
let durationCount = 0;
|
|
716
|
+
for (const s of sessions) {
|
|
717
|
+
if (s.start_time && s.end_time) {
|
|
718
|
+
totalDuration += (new Date(s.end_time).getTime() - new Date(s.start_time).getTime()) / 1000;
|
|
719
|
+
durationCount++;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const avgDuration = durationCount > 0 ? Math.round(totalDuration / durationCount) : 0;
|
|
723
|
+
const withSummary = sessions.filter((s) => s.summary).length;
|
|
724
|
+
const successRate = Math.round((withSummary / sessions.length) * 100);
|
|
725
|
+
const ids = sessions.map((s) => s.id);
|
|
726
|
+
const ph = ids.map(() => '?').join(',');
|
|
727
|
+
const topTools = db.prepare(`SELECT tool_name, COUNT(*) as c FROM events
|
|
728
|
+
WHERE session_id IN (${ph}) AND tool_name IS NOT NULL
|
|
729
|
+
GROUP BY tool_name ORDER BY c DESC LIMIT 10`).all(...ids).map((r) => ({ tool: r.tool_name, count: r.c }));
|
|
730
|
+
const recentSessions = sessions
|
|
731
|
+
.sort((a, b) => (b.start_time || '').localeCompare(a.start_time || ''))
|
|
732
|
+
.slice(0, 5)
|
|
733
|
+
.map((s) => ({ id: s.id, summary: s.summary, timestamp: s.start_time }));
|
|
734
|
+
return json(res, {
|
|
735
|
+
agent: name, sessionCount: sessions.length,
|
|
736
|
+
totalCost, avgDuration, topTools, recentSessions, successRate
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
else if (pathname === '/api/files') {
|
|
740
|
+
const limit = parseInt(query.limit) || 100;
|
|
741
|
+
const offset = parseInt(query.offset) || 0;
|
|
742
|
+
const rows = db.prepare(`
|
|
743
|
+
SELECT file_path, COUNT(*) as touch_count, COUNT(DISTINCT session_id) as session_count,
|
|
744
|
+
MAX(timestamp) as last_touched,
|
|
745
|
+
GROUP_CONCAT(DISTINCT operation) as operations
|
|
746
|
+
FROM file_activity
|
|
747
|
+
GROUP BY file_path
|
|
748
|
+
ORDER BY touch_count DESC
|
|
749
|
+
LIMIT ? OFFSET ?
|
|
750
|
+
`).all(limit, offset);
|
|
751
|
+
const total = db.prepare('SELECT COUNT(DISTINCT file_path) as c FROM file_activity').get().c;
|
|
752
|
+
json(res, { files: rows, total, limit, offset });
|
|
753
|
+
}
|
|
754
|
+
else if (pathname === '/api/files/sessions') {
|
|
755
|
+
const fp = query.path || '';
|
|
756
|
+
if (!fp) {
|
|
757
|
+
json(res, { sessions: [] });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const rows = db.prepare(`
|
|
761
|
+
SELECT DISTINCT s.*, fa.operation, fa.timestamp as touch_time
|
|
762
|
+
FROM file_activity fa
|
|
763
|
+
JOIN sessions s ON s.id = fa.session_id
|
|
764
|
+
WHERE fa.file_path = ?
|
|
765
|
+
ORDER BY fa.timestamp DESC
|
|
766
|
+
`).all(fp);
|
|
767
|
+
json(res, { file: fp, sessions: rows });
|
|
768
|
+
}
|
|
769
|
+
else if (pathname === '/api/insights') {
|
|
770
|
+
const summary = (0, insights_js_1.getInsightsSummary)(db);
|
|
771
|
+
return json(res, summary);
|
|
772
|
+
}
|
|
773
|
+
else if (pathname.match(/^\/api\/insights\/session\/[^/]+$/)) {
|
|
774
|
+
const id = pathname.split('/')[4];
|
|
775
|
+
const row = db.prepare('SELECT * FROM session_insights WHERE session_id = ?').get(id);
|
|
776
|
+
if (!row) {
|
|
777
|
+
// Compute on-the-fly if not yet analyzed
|
|
778
|
+
const result = (0, insights_js_1.analyzeSession)(db, id);
|
|
779
|
+
if (!result)
|
|
780
|
+
return json(res, { error: 'Session not found' }, 404);
|
|
781
|
+
return json(res, result);
|
|
782
|
+
}
|
|
783
|
+
return json(res, {
|
|
784
|
+
session_id: row.session_id,
|
|
785
|
+
signals: JSON.parse(row.signals || '[]'),
|
|
786
|
+
confusion_score: row.confusion_score,
|
|
787
|
+
flagged: !!row.flagged,
|
|
788
|
+
computed_at: row.computed_at
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
else if (!serveStatic(req, res)) {
|
|
792
|
+
const index = node_path_1.default.join(PUBLIC, 'index.html');
|
|
793
|
+
if (node_fs_1.default.existsSync(index)) {
|
|
794
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
795
|
+
node_fs_1.default.createReadStream(index).pipe(res);
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
json(res, { error: 'Not found' }, 404);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
console.error(err);
|
|
804
|
+
json(res, { error: err.message }, 500);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
const HOST = process.env.AGENTACTA_HOST || '127.0.0.1';
|
|
808
|
+
server.on('error', (err) => {
|
|
809
|
+
if (err.code === 'EADDRINUSE') {
|
|
810
|
+
console.error(`Port ${PORT} in use, retrying in 2s...`);
|
|
811
|
+
setTimeout(() => {
|
|
812
|
+
server.close();
|
|
813
|
+
server.listen(PORT, HOST);
|
|
814
|
+
}, 2000);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
server.listen(PORT, HOST, () => console.log(`AgentActa running on http://${HOST}:${PORT}`));
|
|
821
|
+
// Graceful shutdown
|
|
822
|
+
function shutdown(signal) {
|
|
823
|
+
console.log(`\n${signal} received, shutting down...`);
|
|
824
|
+
server.close(() => {
|
|
825
|
+
try {
|
|
826
|
+
db.close();
|
|
827
|
+
}
|
|
828
|
+
catch { /* ignore */ }
|
|
829
|
+
console.log('AgentActa stopped.');
|
|
830
|
+
process.exit(0);
|
|
831
|
+
});
|
|
832
|
+
// Force exit after 5s if server doesn't close
|
|
833
|
+
setTimeout(() => {
|
|
834
|
+
try {
|
|
835
|
+
db.close();
|
|
836
|
+
}
|
|
837
|
+
catch { /* ignore */ }
|
|
838
|
+
process.exit(1);
|
|
839
|
+
}, 5000);
|
|
840
|
+
}
|
|
841
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
842
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
843
|
+
//# sourceMappingURL=index.js.map
|