agentacta 1.3.3 → 1.4.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 +102 -80
- package/db.js +1 -0
- package/index.js +57 -3
- package/indexer.js +281 -20
- package/package.json +1 -1
- package/public/app.js +231 -37
- package/public/index.html +4 -0
- package/public/style.css +144 -4
package/indexer.js
CHANGED
|
@@ -6,6 +6,25 @@ const { loadConfig } = require('./config');
|
|
|
6
6
|
const REINDEX = process.argv.includes('--reindex');
|
|
7
7
|
const WATCH = process.argv.includes('--watch');
|
|
8
8
|
|
|
9
|
+
function listJsonlFiles(baseDir, recursive = false) {
|
|
10
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
11
|
+
const out = [];
|
|
12
|
+
|
|
13
|
+
function walk(dir) {
|
|
14
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
15
|
+
const full = path.join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
if (recursive) walk(full);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
walk(baseDir);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
function discoverSessionDirs(config) {
|
|
10
29
|
const dirs = [];
|
|
11
30
|
const home = process.env.HOME;
|
|
@@ -48,6 +67,12 @@ function discoverSessionDirs(config) {
|
|
|
48
67
|
}
|
|
49
68
|
}
|
|
50
69
|
|
|
70
|
+
// Scan ~/.codex/sessions recursively (Codex CLI stores nested YYYY/MM/DD/*.jsonl)
|
|
71
|
+
const codexSessions = path.join(home, '.codex/sessions');
|
|
72
|
+
if (fs.existsSync(codexSessions) && fs.statSync(codexSessions).isDirectory()) {
|
|
73
|
+
dirs.push({ path: codexSessions, agent: 'codex-cli', recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
if (!dirs.length) {
|
|
52
77
|
// Fallback to hardcoded
|
|
53
78
|
const fallback = path.join(home, '.openclaw/agents/main/sessions');
|
|
@@ -63,6 +88,28 @@ function isHeartbeat(text) {
|
|
|
63
88
|
return lower.includes('heartbeat') || lower.includes('heartbeat_ok');
|
|
64
89
|
}
|
|
65
90
|
|
|
91
|
+
function isBoilerplatePrompt(text) {
|
|
92
|
+
if (!text) return false;
|
|
93
|
+
const lower = text.toLowerCase();
|
|
94
|
+
return lower.includes('<permissions instructions>')
|
|
95
|
+
|| lower.includes('filesystem sandboxing defines which files can be read or written')
|
|
96
|
+
|| lower.includes('# agents.md instructions for ');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isSummaryCandidate(text) {
|
|
100
|
+
if (!text || text.trim().length <= 10) return false;
|
|
101
|
+
if (isHeartbeat(text)) return false;
|
|
102
|
+
if (isBoilerplatePrompt(text)) return false;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stripLeadingDatetimePrefix(text) {
|
|
107
|
+
if (!text) return text;
|
|
108
|
+
return text
|
|
109
|
+
.replace(/^\[(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}[^\]]*\]\s*/i, '')
|
|
110
|
+
.trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
66
113
|
function extractContent(msg) {
|
|
67
114
|
if (!msg || !msg.content) return '';
|
|
68
115
|
if (typeof msg.content === 'string') return msg.content;
|
|
@@ -94,17 +141,60 @@ function extractToolResult(msg) {
|
|
|
94
141
|
return null;
|
|
95
142
|
}
|
|
96
143
|
|
|
144
|
+
function extractCodexMessageText(content) {
|
|
145
|
+
if (!content) return '';
|
|
146
|
+
if (typeof content === 'string') return content;
|
|
147
|
+
if (!Array.isArray(content)) return '';
|
|
148
|
+
|
|
149
|
+
return content
|
|
150
|
+
.map((part) => {
|
|
151
|
+
if (!part || typeof part !== 'object') return '';
|
|
152
|
+
if (typeof part.text === 'string') return part.text;
|
|
153
|
+
if (typeof part.output_text === 'string') return part.output_text;
|
|
154
|
+
if (typeof part.input_text === 'string') return part.input_text;
|
|
155
|
+
return '';
|
|
156
|
+
})
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
97
161
|
function extractFilePaths(toolName, toolArgs) {
|
|
98
162
|
const paths = [];
|
|
99
163
|
if (!toolArgs) return paths;
|
|
164
|
+
|
|
165
|
+
const maybePath = (value) => {
|
|
166
|
+
if (typeof value !== 'string') return;
|
|
167
|
+
if (value.startsWith('/') || value.startsWith('~/') || value.startsWith('./') || value.startsWith('../')) {
|
|
168
|
+
paths.push(value);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (value.includes('/') || value.includes('\\')) paths.push(value);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const visit = (obj) => {
|
|
175
|
+
if (!obj || typeof obj !== 'object') return;
|
|
176
|
+
if (Array.isArray(obj)) {
|
|
177
|
+
for (const item of obj) visit(item);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
182
|
+
if (typeof value === 'string') {
|
|
183
|
+
if (['path', 'file_path', 'filePath', 'file', 'filename', 'cwd', 'workdir', 'directory', 'dir'].includes(key)) {
|
|
184
|
+
maybePath(value);
|
|
185
|
+
}
|
|
186
|
+
} else if (value && typeof value === 'object') {
|
|
187
|
+
visit(value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
100
192
|
try {
|
|
101
193
|
const args = typeof toolArgs === 'string' ? JSON.parse(toolArgs) : toolArgs;
|
|
102
|
-
|
|
103
|
-
for (const key of ['path', 'file_path', 'filePath', 'file', 'filename']) {
|
|
104
|
-
if (args[key] && typeof args[key] === 'string') paths.push(args[key]);
|
|
105
|
-
}
|
|
194
|
+
visit(args);
|
|
106
195
|
} catch {}
|
|
107
|
-
|
|
196
|
+
|
|
197
|
+
return [...new Set(paths)];
|
|
108
198
|
}
|
|
109
199
|
|
|
110
200
|
function aliasProject(project, config) {
|
|
@@ -177,9 +267,20 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
177
267
|
let initialPrompt = null;
|
|
178
268
|
let firstMessageId = null;
|
|
179
269
|
let firstMessageTimestamp = null;
|
|
270
|
+
let codexProvider = null;
|
|
271
|
+
let codexSource = null;
|
|
272
|
+
let sawSnapshotRecord = false;
|
|
273
|
+
let sawNonSnapshotRecord = false;
|
|
274
|
+
|
|
275
|
+
let firstLine;
|
|
276
|
+
try {
|
|
277
|
+
firstLine = JSON.parse(lines[0]);
|
|
278
|
+
} catch {
|
|
279
|
+
return { skipped: true };
|
|
280
|
+
}
|
|
180
281
|
|
|
181
|
-
const firstLine = JSON.parse(lines[0]);
|
|
182
282
|
let isClaudeCode = false;
|
|
283
|
+
let isCodexCli = false;
|
|
183
284
|
|
|
184
285
|
if (firstLine.type === 'session') {
|
|
185
286
|
// OpenClaw format
|
|
@@ -204,6 +305,20 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
204
305
|
sessionId = path.basename(filePath, '.jsonl');
|
|
205
306
|
sessionStart = new Date(firstLine.timestamp || Date.now()).toISOString();
|
|
206
307
|
}
|
|
308
|
+
} else if (firstLine.type === 'session_meta') {
|
|
309
|
+
// Codex CLI format
|
|
310
|
+
isCodexCli = true;
|
|
311
|
+
const meta = firstLine.payload || {};
|
|
312
|
+
sessionId = meta.id || path.basename(filePath, '.jsonl');
|
|
313
|
+
sessionStart = meta.timestamp || firstLine.timestamp || new Date().toISOString();
|
|
314
|
+
sessionType = 'codex-cli';
|
|
315
|
+
agent = 'codex-cli';
|
|
316
|
+
if (meta.model) {
|
|
317
|
+
model = meta.model;
|
|
318
|
+
modelsSet.add(meta.model);
|
|
319
|
+
}
|
|
320
|
+
codexProvider = meta.model_provider || null;
|
|
321
|
+
codexSource = meta.source || null;
|
|
207
322
|
} else {
|
|
208
323
|
return { skipped: true };
|
|
209
324
|
}
|
|
@@ -214,8 +329,9 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
214
329
|
const projectCounts = new Map();
|
|
215
330
|
|
|
216
331
|
// Seed project from session cwd when available (helps chat-only sessions)
|
|
217
|
-
|
|
218
|
-
|
|
332
|
+
const sessionCwd = (firstLine && firstLine.cwd) || (firstLine && firstLine.payload && firstLine.payload.cwd);
|
|
333
|
+
if (sessionCwd) {
|
|
334
|
+
const p = extractProjectFromPath(sessionCwd, config);
|
|
219
335
|
if (p) projectCounts.set(p, 1);
|
|
220
336
|
}
|
|
221
337
|
|
|
@@ -223,6 +339,111 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
223
339
|
let obj;
|
|
224
340
|
try { obj = JSON.parse(line); } catch { continue; }
|
|
225
341
|
|
|
342
|
+
if (obj.type === 'file-history-snapshot') sawSnapshotRecord = true;
|
|
343
|
+
else sawNonSnapshotRecord = true;
|
|
344
|
+
|
|
345
|
+
if (isCodexCli) {
|
|
346
|
+
if (obj.type === 'session_meta') {
|
|
347
|
+
const meta = obj.payload || {};
|
|
348
|
+
if (meta.id) sessionId = meta.id;
|
|
349
|
+
if (meta.timestamp && !sessionStart) sessionStart = meta.timestamp;
|
|
350
|
+
if (meta.model) {
|
|
351
|
+
if (!model) model = meta.model;
|
|
352
|
+
modelsSet.add(meta.model);
|
|
353
|
+
}
|
|
354
|
+
if (meta.model_provider) codexProvider = meta.model_provider;
|
|
355
|
+
if (meta.source) codexSource = meta.source;
|
|
356
|
+
if (meta.model_provider && !model) model = meta.model_provider;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (obj.type === 'turn_context' && obj.payload) {
|
|
361
|
+
const tc = obj.payload;
|
|
362
|
+
if (tc.model && typeof tc.model === 'string') {
|
|
363
|
+
if (!model || model === codexProvider) model = tc.model;
|
|
364
|
+
modelsSet.add(tc.model);
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (obj.type === 'response_item' && obj.payload) {
|
|
370
|
+
const p = obj.payload;
|
|
371
|
+
const ts = obj.timestamp || sessionStart;
|
|
372
|
+
const eventId = `evt-${obj.type}-${Date.parse(ts) || Math.random()}`;
|
|
373
|
+
|
|
374
|
+
if (p.type === 'function_call') {
|
|
375
|
+
const toolName = p.name || p.tool_name || '';
|
|
376
|
+
const toolArgs = typeof p.arguments === 'string' ? p.arguments : JSON.stringify(p.arguments || {});
|
|
377
|
+
const callBaseId = p.call_id || p.id || eventId;
|
|
378
|
+
pendingEvents.push([`${callBaseId}:call`, sessionId, ts, 'tool_call', 'assistant', null, toolName, toolArgs, null]);
|
|
379
|
+
toolCount++;
|
|
380
|
+
|
|
381
|
+
const fps = extractFilePaths(toolName, toolArgs);
|
|
382
|
+
for (const fp of fps) {
|
|
383
|
+
fileActivities.push([sessionId, fp, 'read', ts]);
|
|
384
|
+
const project = extractProjectFromPath(fp, config);
|
|
385
|
+
if (project) projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
|
|
386
|
+
}
|
|
387
|
+
sessionEnd = ts;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (p.type === 'function_call_output') {
|
|
392
|
+
const output = (typeof p.output === 'string' ? p.output : JSON.stringify(p.output || '')).slice(0, 10000);
|
|
393
|
+
const resultBaseId = p.call_id || p.id || eventId;
|
|
394
|
+
pendingEvents.push([`${resultBaseId}:result`, sessionId, ts, 'tool_result', 'tool', output, p.name || p.tool_name || '', null, output]);
|
|
395
|
+
sessionEnd = ts;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (p.type === 'message') {
|
|
400
|
+
const rawRole = p.role || 'assistant';
|
|
401
|
+
const role = rawRole === 'assistant' ? 'assistant' : 'user';
|
|
402
|
+
const content = extractCodexMessageText(p.content);
|
|
403
|
+
if (content) {
|
|
404
|
+
pendingEvents.push([p.id || eventId, sessionId, ts, 'message', role, content, null, null, null]);
|
|
405
|
+
msgCount++;
|
|
406
|
+
if (!summary && role === 'user' && isSummaryCandidate(content)) summary = content.slice(0, 200);
|
|
407
|
+
if (!initialPrompt && role === 'user' && isSummaryCandidate(content)) {
|
|
408
|
+
initialPrompt = content.slice(0, 500);
|
|
409
|
+
firstMessageId = p.id || eventId;
|
|
410
|
+
firstMessageTimestamp = ts;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
sessionEnd = ts;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (obj.type === 'event_msg' && obj.payload) {
|
|
419
|
+
const p = obj.payload;
|
|
420
|
+
const ts = obj.timestamp || sessionStart;
|
|
421
|
+
const eventId = `evt-${p.type || 'event'}-${Date.parse(ts) || Math.random()}`;
|
|
422
|
+
|
|
423
|
+
if (p.type === 'agent_message' && p.message) {
|
|
424
|
+
pendingEvents.push([eventId, sessionId, ts, 'message', 'assistant', p.message, null, null, null]);
|
|
425
|
+
msgCount++;
|
|
426
|
+
sessionEnd = ts;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (p.type === 'user_message' && p.message) {
|
|
431
|
+
pendingEvents.push([eventId, sessionId, ts, 'message', 'user', p.message, null, null, null]);
|
|
432
|
+
msgCount++;
|
|
433
|
+
if (!summary && isSummaryCandidate(p.message)) summary = p.message.slice(0, 200);
|
|
434
|
+
if (!initialPrompt && isSummaryCandidate(p.message)) {
|
|
435
|
+
initialPrompt = p.message.slice(0, 500);
|
|
436
|
+
firstMessageId = eventId;
|
|
437
|
+
firstMessageTimestamp = ts;
|
|
438
|
+
}
|
|
439
|
+
sessionEnd = ts;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
226
447
|
if (obj.type === 'session' || obj.type === 'model_change' || obj.type === 'thinking_level_change' || obj.type === 'custom' || obj.type === 'file-history-snapshot') {
|
|
227
448
|
if (obj.type === 'model_change' && obj.modelId) {
|
|
228
449
|
if (!model) model = obj.modelId; // First model for backwards compat
|
|
@@ -289,12 +510,12 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
289
510
|
if (content) {
|
|
290
511
|
pendingEvents.push([eventId, sessionId, ts, 'message', role, content, null, null, null]);
|
|
291
512
|
msgCount++;
|
|
292
|
-
// Better summary: skip heartbeat messages
|
|
293
|
-
if (!summary && role === 'user' &&
|
|
513
|
+
// Better summary: skip heartbeat/boilerplate messages
|
|
514
|
+
if (!summary && role === 'user' && isSummaryCandidate(content)) {
|
|
294
515
|
summary = content.slice(0, 200);
|
|
295
516
|
}
|
|
296
517
|
// Capture initial prompt from first substantial user message
|
|
297
|
-
if (!initialPrompt && role === 'user' &&
|
|
518
|
+
if (!initialPrompt && role === 'user' && isSummaryCandidate(content)) {
|
|
298
519
|
initialPrompt = content.slice(0, 500); // Limit to 500 chars
|
|
299
520
|
firstMessageId = eventId;
|
|
300
521
|
firstMessageTimestamp = ts;
|
|
@@ -321,9 +542,25 @@ function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
|
321
542
|
}
|
|
322
543
|
}
|
|
323
544
|
|
|
324
|
-
//
|
|
545
|
+
// Classify snapshot-only Claude files explicitly (avoid heartbeat mislabel)
|
|
546
|
+
if (isClaudeCode && sawSnapshotRecord && !sawNonSnapshotRecord) {
|
|
547
|
+
sessionType = 'snapshot';
|
|
548
|
+
if (!summary) summary = 'Claude file snapshot';
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Normalize summary text
|
|
552
|
+
if (summary) summary = stripLeadingDatetimePrefix(summary);
|
|
553
|
+
|
|
554
|
+
// If no real summary found, set a sensible default
|
|
325
555
|
if (!summary) {
|
|
326
|
-
|
|
556
|
+
if (isCodexCli) {
|
|
557
|
+
const parts = ['Codex CLI session'];
|
|
558
|
+
if (codexProvider) parts.push(`provider=${codexProvider}`);
|
|
559
|
+
if (codexSource) parts.push(`source=${codexSource}`);
|
|
560
|
+
summary = parts.join(' · ');
|
|
561
|
+
} else {
|
|
562
|
+
summary = 'Heartbeat session';
|
|
563
|
+
}
|
|
327
564
|
}
|
|
328
565
|
|
|
329
566
|
// Infer session type from first user message content
|
|
@@ -389,9 +626,8 @@ function run() {
|
|
|
389
626
|
|
|
390
627
|
let allFiles = [];
|
|
391
628
|
for (const dir of sessionDirs) {
|
|
392
|
-
const files =
|
|
393
|
-
.
|
|
394
|
-
.map(f => ({ path: path.join(dir.path, f), agent: dir.agent }));
|
|
629
|
+
const files = listJsonlFiles(dir.path, !!dir.recursive)
|
|
630
|
+
.map(filePath => ({ path: filePath, agent: dir.agent }));
|
|
395
631
|
allFiles.push(...files);
|
|
396
632
|
}
|
|
397
633
|
|
|
@@ -418,8 +654,33 @@ function run() {
|
|
|
418
654
|
|
|
419
655
|
if (WATCH) {
|
|
420
656
|
console.log('\nWatching for changes...');
|
|
657
|
+
const rescanTimers = new Map();
|
|
658
|
+
|
|
421
659
|
for (const dir of sessionDirs) {
|
|
422
660
|
fs.watch(dir.path, { persistent: true }, (eventType, filename) => {
|
|
661
|
+
// Recursive sources (e.g. ~/.codex/sessions/YYYY/MM/DD/*.jsonl):
|
|
662
|
+
// fs.watch on Linux does not watch nested dirs recursively, so on any root event
|
|
663
|
+
// run a debounced full rescan of known JSONL files under this source.
|
|
664
|
+
if (dir.recursive) {
|
|
665
|
+
const key = dir.path;
|
|
666
|
+
if (rescanTimers.get(key)) clearTimeout(rescanTimers.get(key));
|
|
667
|
+
const t = setTimeout(() => {
|
|
668
|
+
try {
|
|
669
|
+
const files = listJsonlFiles(dir.path, true);
|
|
670
|
+
let changed = 0;
|
|
671
|
+
for (const filePath of files) {
|
|
672
|
+
const result = indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
|
|
673
|
+
if (!result.skipped) changed++;
|
|
674
|
+
}
|
|
675
|
+
if (changed > 0) console.log(`Re-indexed ${changed} files (${dir.agent})`);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
console.error(`Error rescanning ${dir.path}:`, err.message);
|
|
678
|
+
}
|
|
679
|
+
}, 500);
|
|
680
|
+
rescanTimers.set(key, t);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
423
684
|
if (!filename || !filename.endsWith('.jsonl')) return;
|
|
424
685
|
const filePath = path.join(dir.path, filename);
|
|
425
686
|
if (!fs.existsSync(filePath)) return;
|
|
@@ -444,13 +705,13 @@ function indexAll(db, config) {
|
|
|
444
705
|
const stmts = createStmts(db);
|
|
445
706
|
let totalSessions = 0;
|
|
446
707
|
for (const dir of sessionDirs) {
|
|
447
|
-
const files =
|
|
448
|
-
for (const
|
|
708
|
+
const files = listJsonlFiles(dir.path, !!dir.recursive);
|
|
709
|
+
for (const filePath of files) {
|
|
449
710
|
try {
|
|
450
|
-
const result = indexFile(db,
|
|
711
|
+
const result = indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
|
|
451
712
|
if (!result.skipped) totalSessions++;
|
|
452
713
|
} catch (err) {
|
|
453
|
-
console.error(`Error indexing ${
|
|
714
|
+
console.error(`Error indexing ${path.basename(filePath)}:`, err.message);
|
|
454
715
|
}
|
|
455
716
|
}
|
|
456
717
|
}
|