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/indexer.js
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
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
|
+
exports.discoverSessionDirs = discoverSessionDirs;
|
|
7
|
+
exports.listJsonlFiles = listJsonlFiles;
|
|
8
|
+
exports.indexFile = indexFile;
|
|
9
|
+
exports.indexCronRunFile = indexCronRunFile;
|
|
10
|
+
exports.indexAll = indexAll;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const db_js_1 = require("./db.js");
|
|
14
|
+
const config_js_1 = require("./config.js");
|
|
15
|
+
const REINDEX = process.argv.includes('--reindex');
|
|
16
|
+
const WATCH = process.argv.includes('--watch');
|
|
17
|
+
function listJsonlFiles(baseDir, recursive = false) {
|
|
18
|
+
if (!node_fs_1.default.existsSync(baseDir))
|
|
19
|
+
return [];
|
|
20
|
+
const out = [];
|
|
21
|
+
function walk(dir) {
|
|
22
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const full = node_path_1.default.join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
if (recursive)
|
|
26
|
+
walk(full);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
30
|
+
out.push(full);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
walk(baseDir);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function discoverSessionDirs(config) {
|
|
37
|
+
const dirs = [];
|
|
38
|
+
const home = process.env.HOME;
|
|
39
|
+
const codexSessionsPath = node_path_1.default.join(home, '.codex/sessions');
|
|
40
|
+
const cronRunsPath = node_path_1.default.join(home, '.openclaw/cron/runs');
|
|
41
|
+
function normalizedPath(p) {
|
|
42
|
+
return node_path_1.default.resolve(p).replace(/[\\\/]+$/, '');
|
|
43
|
+
}
|
|
44
|
+
function hasDir(targetPath, sourceType = 'transcript') {
|
|
45
|
+
const wanted = normalizedPath(targetPath);
|
|
46
|
+
return dirs.some(d => normalizedPath(d.path) === wanted && (d.sourceType || 'transcript') === sourceType);
|
|
47
|
+
}
|
|
48
|
+
function addDir(dir) {
|
|
49
|
+
if (!dir || !dir.path)
|
|
50
|
+
return;
|
|
51
|
+
if (hasDir(dir.path, dir.sourceType || 'transcript'))
|
|
52
|
+
return;
|
|
53
|
+
dirs.push(dir);
|
|
54
|
+
}
|
|
55
|
+
// Expand a single path into session dirs, handling Claude Code's per-project structure
|
|
56
|
+
function expandPath(p) {
|
|
57
|
+
if (!node_fs_1.default.existsSync(p))
|
|
58
|
+
return;
|
|
59
|
+
const stat = node_fs_1.default.statSync(p);
|
|
60
|
+
if (!stat.isDirectory())
|
|
61
|
+
return;
|
|
62
|
+
const normalized = normalizedPath(p);
|
|
63
|
+
const normalizedCodex = normalizedPath(codexSessionsPath);
|
|
64
|
+
// Claude Code: ~/.claude/projects contains per-project subdirs with JSONL files
|
|
65
|
+
if (normalized.endsWith('/.claude/projects')) {
|
|
66
|
+
for (const proj of node_fs_1.default.readdirSync(p)) {
|
|
67
|
+
const projDir = node_path_1.default.join(p, proj);
|
|
68
|
+
if (node_fs_1.default.statSync(projDir).isDirectory()) {
|
|
69
|
+
const hasJsonl = node_fs_1.default.readdirSync(projDir).some(f => f.endsWith('.jsonl'));
|
|
70
|
+
if (hasJsonl)
|
|
71
|
+
addDir({ path: projDir, agent: 'claude-code' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (normalized === normalizedCodex) {
|
|
76
|
+
// Codex CLI stores nested YYYY/MM/DD directories and must be recursive.
|
|
77
|
+
addDir({ path: p, agent: 'codex-cli', recursive: true });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
addDir({ path: p, agent: node_path_1.default.basename(node_path_1.default.dirname(p)) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Config sessionsPath or env var override
|
|
84
|
+
const sessionsOverride = process.env.AGENTACTA_SESSIONS_PATH || (config && config.sessionsPath);
|
|
85
|
+
if (sessionsOverride) {
|
|
86
|
+
const overridePaths = Array.isArray(sessionsOverride)
|
|
87
|
+
? sessionsOverride
|
|
88
|
+
: sessionsOverride.split(':');
|
|
89
|
+
overridePaths.forEach(expandPath);
|
|
90
|
+
}
|
|
91
|
+
// Auto-discover: ~/.openclaw/agents/*/sessions/
|
|
92
|
+
const oclawAgents = node_path_1.default.join(home, '.openclaw/agents');
|
|
93
|
+
if (node_fs_1.default.existsSync(oclawAgents)) {
|
|
94
|
+
for (const agent of node_fs_1.default.readdirSync(oclawAgents)) {
|
|
95
|
+
const sp = node_path_1.default.join(oclawAgents, agent, 'sessions');
|
|
96
|
+
if (node_fs_1.default.existsSync(sp) && node_fs_1.default.statSync(sp).isDirectory()) {
|
|
97
|
+
addDir({ path: sp, agent });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Auto-discover: ~/.claude/projects/
|
|
102
|
+
expandPath(node_path_1.default.join(home, '.claude/projects'));
|
|
103
|
+
// Scan ~/.codex/sessions recursively (Codex CLI stores nested YYYY/MM/DD/*.jsonl)
|
|
104
|
+
const codexSessions = codexSessionsPath;
|
|
105
|
+
if (node_fs_1.default.existsSync(codexSessions) && node_fs_1.default.statSync(codexSessions).isDirectory()) {
|
|
106
|
+
addDir({ path: codexSessions, agent: 'codex-cli', recursive: true });
|
|
107
|
+
}
|
|
108
|
+
// Fallback synthetic source for cron-backed runs that have metadata but no transcript JSONL.
|
|
109
|
+
if (node_fs_1.default.existsSync(cronRunsPath) && node_fs_1.default.statSync(cronRunsPath).isDirectory()) {
|
|
110
|
+
addDir({ path: cronRunsPath, agent: 'cron', sourceType: 'cron-run' });
|
|
111
|
+
}
|
|
112
|
+
if (!dirs.length) {
|
|
113
|
+
// Fallback to hardcoded
|
|
114
|
+
const fallback = node_path_1.default.join(home, '.openclaw/agents/main/sessions');
|
|
115
|
+
if (node_fs_1.default.existsSync(fallback))
|
|
116
|
+
addDir({ path: fallback, agent: 'main' });
|
|
117
|
+
}
|
|
118
|
+
return dirs;
|
|
119
|
+
}
|
|
120
|
+
function isHeartbeat(text) {
|
|
121
|
+
if (!text)
|
|
122
|
+
return false;
|
|
123
|
+
const lower = text.toLowerCase();
|
|
124
|
+
return lower.includes('heartbeat') || lower.includes('heartbeat_ok');
|
|
125
|
+
}
|
|
126
|
+
function isBoilerplatePrompt(text) {
|
|
127
|
+
if (!text)
|
|
128
|
+
return false;
|
|
129
|
+
const lower = text.toLowerCase();
|
|
130
|
+
return lower.includes('<permissions instructions>')
|
|
131
|
+
|| lower.includes('filesystem sandboxing defines which files can be read or written')
|
|
132
|
+
|| lower.includes('# agents.md instructions for ');
|
|
133
|
+
}
|
|
134
|
+
function isSummaryCandidate(text) {
|
|
135
|
+
if (!text || text.trim().length <= 10)
|
|
136
|
+
return false;
|
|
137
|
+
if (isHeartbeat(text))
|
|
138
|
+
return false;
|
|
139
|
+
if (isBoilerplatePrompt(text))
|
|
140
|
+
return false;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
function stripLeadingDatetimePrefix(text) {
|
|
144
|
+
if (!text)
|
|
145
|
+
return text;
|
|
146
|
+
return text
|
|
147
|
+
.replace(/^\[(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}[^\]]*\]\s*/i, '')
|
|
148
|
+
.trim();
|
|
149
|
+
}
|
|
150
|
+
function extractContent(msg) {
|
|
151
|
+
if (!msg || typeof msg !== 'object')
|
|
152
|
+
return '';
|
|
153
|
+
const m = msg;
|
|
154
|
+
if (!m.content)
|
|
155
|
+
return '';
|
|
156
|
+
if (typeof m.content === 'string')
|
|
157
|
+
return m.content;
|
|
158
|
+
if (Array.isArray(m.content)) {
|
|
159
|
+
return m.content.filter(b => b.type === 'text').map(b => b.text || '').join('\n');
|
|
160
|
+
}
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
function extractToolCalls(msg) {
|
|
164
|
+
if (!msg || typeof msg !== 'object')
|
|
165
|
+
return [];
|
|
166
|
+
const m = msg;
|
|
167
|
+
if (!Array.isArray(m.content))
|
|
168
|
+
return [];
|
|
169
|
+
return m.content
|
|
170
|
+
.filter(b => b.type === 'tool_use' || b.type === 'toolCall')
|
|
171
|
+
.map(b => ({
|
|
172
|
+
id: b.id || b.toolCallId || '',
|
|
173
|
+
name: b.name || '',
|
|
174
|
+
args: JSON.stringify(b.input || b.arguments || {})
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
function extractToolResult(msg) {
|
|
178
|
+
if (!msg)
|
|
179
|
+
return null;
|
|
180
|
+
const m = msg;
|
|
181
|
+
if (m.role === 'toolResult' || m.role === 'tool') {
|
|
182
|
+
const content = Array.isArray(m.content)
|
|
183
|
+
? m.content.map(b => b.text || '').join('\n')
|
|
184
|
+
: (typeof m.content === 'string' ? m.content : '');
|
|
185
|
+
return { toolCallId: m.toolCallId || '', toolName: m.toolName || '', content: content.slice(0, 10000) };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
function extractCodexMessageText(content) {
|
|
190
|
+
if (!content)
|
|
191
|
+
return '';
|
|
192
|
+
if (typeof content === 'string')
|
|
193
|
+
return content;
|
|
194
|
+
if (!Array.isArray(content))
|
|
195
|
+
return '';
|
|
196
|
+
return content
|
|
197
|
+
.map((part) => {
|
|
198
|
+
if (!part || typeof part !== 'object')
|
|
199
|
+
return '';
|
|
200
|
+
if (typeof part.text === 'string')
|
|
201
|
+
return part.text;
|
|
202
|
+
if (typeof part.output_text === 'string')
|
|
203
|
+
return part.output_text;
|
|
204
|
+
if (typeof part.input_text === 'string')
|
|
205
|
+
return part.input_text;
|
|
206
|
+
return '';
|
|
207
|
+
})
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.join('\n');
|
|
210
|
+
}
|
|
211
|
+
function extractFilePaths(toolName, toolArgs) {
|
|
212
|
+
const paths = [];
|
|
213
|
+
if (!toolArgs)
|
|
214
|
+
return paths;
|
|
215
|
+
const maybePath = (value) => {
|
|
216
|
+
if (typeof value !== 'string')
|
|
217
|
+
return;
|
|
218
|
+
if (value.startsWith('/') || value.startsWith('~/') || value.startsWith('./') || value.startsWith('../')) {
|
|
219
|
+
paths.push(value);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (value.includes('/') || value.includes('\\'))
|
|
223
|
+
paths.push(value);
|
|
224
|
+
};
|
|
225
|
+
const visit = (obj) => {
|
|
226
|
+
if (!obj || typeof obj !== 'object')
|
|
227
|
+
return;
|
|
228
|
+
if (Array.isArray(obj)) {
|
|
229
|
+
for (const item of obj)
|
|
230
|
+
visit(item);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
234
|
+
if (typeof value === 'string') {
|
|
235
|
+
if (['path', 'file_path', 'filePath', 'file', 'filename', 'cwd', 'workdir', 'directory', 'dir'].includes(key)) {
|
|
236
|
+
maybePath(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (value && typeof value === 'object') {
|
|
240
|
+
visit(value);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
try {
|
|
245
|
+
const args = typeof toolArgs === 'string' ? JSON.parse(toolArgs) : toolArgs;
|
|
246
|
+
visit(args);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// ignore parse errors
|
|
250
|
+
}
|
|
251
|
+
return [...new Set(paths)];
|
|
252
|
+
}
|
|
253
|
+
function aliasProject(project, config) {
|
|
254
|
+
if (!project)
|
|
255
|
+
return project;
|
|
256
|
+
const aliases = (config && config.projectAliases && typeof config.projectAliases === 'object') ? config.projectAliases : {};
|
|
257
|
+
return aliases[project] || project;
|
|
258
|
+
}
|
|
259
|
+
function extractProjectFromPath(filePath, config) {
|
|
260
|
+
if (!filePath || typeof filePath !== 'string')
|
|
261
|
+
return null;
|
|
262
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
263
|
+
// Relative paths are usually from workspace cwd -> treat as workspace activity
|
|
264
|
+
if (!normalized.startsWith('/') && !normalized.startsWith('~'))
|
|
265
|
+
return aliasProject('workspace', config);
|
|
266
|
+
const rel = normalized
|
|
267
|
+
.replace(/^\/home\/[^/]+\//, '')
|
|
268
|
+
.replace(/^\/Users\/[^/]+\//, '')
|
|
269
|
+
.replace(/^~\//, '');
|
|
270
|
+
const parts = rel.split('/').filter(Boolean);
|
|
271
|
+
if (!parts.length)
|
|
272
|
+
return null;
|
|
273
|
+
// Common repo location: ~/Developer/<repo>/...
|
|
274
|
+
if (parts[0] === 'Developer' && parts[1])
|
|
275
|
+
return aliasProject(parts[1], config);
|
|
276
|
+
// Symphony worktrees: ~/symphony-workspaces/<issue>/...
|
|
277
|
+
if (parts[0] === 'symphony-workspaces' && parts[1])
|
|
278
|
+
return aliasProject(parts[1], config);
|
|
279
|
+
// OpenClaw workspace and agent stores
|
|
280
|
+
if (parts[0] === '.openclaw' && parts[1] === 'workspace')
|
|
281
|
+
return aliasProject('workspace', config);
|
|
282
|
+
if (parts[0] === '.openclaw' && parts[1] === 'agents' && parts[2])
|
|
283
|
+
return aliasProject(`agent:${parts[2]}`, config);
|
|
284
|
+
// Claude Code projects
|
|
285
|
+
if (parts[0] === '.claude' && parts[1] === 'projects' && parts[2])
|
|
286
|
+
return aliasProject(`claude:${parts[2]}`, config);
|
|
287
|
+
// Shared files area
|
|
288
|
+
if (parts[0] === 'Shared')
|
|
289
|
+
return aliasProject('shared', config);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
function indexCronRunFile(db, filePath, agentName, stmts) {
|
|
293
|
+
const stat = node_fs_1.default.statSync(filePath);
|
|
294
|
+
const mtime = stat.mtime.toISOString();
|
|
295
|
+
if (!REINDEX) {
|
|
296
|
+
const state = stmts.getState.get(filePath);
|
|
297
|
+
if (state && state.last_modified === mtime)
|
|
298
|
+
return { skipped: true };
|
|
299
|
+
}
|
|
300
|
+
const raw = node_fs_1.default.readFileSync(filePath, 'utf8').trim();
|
|
301
|
+
if (!raw)
|
|
302
|
+
return { skipped: true };
|
|
303
|
+
let meta;
|
|
304
|
+
try {
|
|
305
|
+
meta = JSON.parse(raw.split('\n').find(Boolean));
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return { skipped: true };
|
|
309
|
+
}
|
|
310
|
+
const sessionId = meta.sessionId;
|
|
311
|
+
if (!sessionId)
|
|
312
|
+
return { skipped: true };
|
|
313
|
+
// Guard: don't overwrite a session that was already indexed from a real transcript.
|
|
314
|
+
// Check both event presence AND session_type — a transcript session with zero events
|
|
315
|
+
// (e.g. header-only file) should still win over synthetic cron metadata.
|
|
316
|
+
const existingSession = db.prepare('SELECT session_type FROM sessions WHERE id = ?').get(sessionId);
|
|
317
|
+
if (existingSession && existingSession.session_type !== 'cron') {
|
|
318
|
+
stmts.upsertState.run(filePath, 1, mtime);
|
|
319
|
+
return { skipped: true, preferredTranscript: true, sessionId };
|
|
320
|
+
}
|
|
321
|
+
const existingRealSession = db.prepare('SELECT EXISTS(SELECT 1 FROM events WHERE session_id = ?) AS has_events').get(sessionId);
|
|
322
|
+
if (existingRealSession && existingRealSession.has_events) {
|
|
323
|
+
stmts.upsertState.run(filePath, 1, mtime);
|
|
324
|
+
return { skipped: true, preferredTranscript: true, sessionId };
|
|
325
|
+
}
|
|
326
|
+
const ts = typeof meta.ts === 'number' ? new Date(meta.ts).toISOString() : new Date().toISOString();
|
|
327
|
+
const runAt = typeof meta.runAtMs === 'number' ? new Date(meta.runAtMs).toISOString() : ts;
|
|
328
|
+
const durationMs = typeof meta.durationMs === 'number' ? meta.durationMs : null;
|
|
329
|
+
const endTime = ts;
|
|
330
|
+
const startTime = durationMs ? new Date(new Date(endTime).getTime() - durationMs).toISOString() : runAt;
|
|
331
|
+
const summary = stripLeadingDatetimePrefix(meta.summary || 'Cron run');
|
|
332
|
+
const sessionKey = typeof meta.sessionKey === 'string' ? meta.sessionKey : '';
|
|
333
|
+
const sessionKeyParts = sessionKey.split(':');
|
|
334
|
+
const inferredAgent = sessionKeyParts[0] === 'agent' && sessionKeyParts[1] ? sessionKeyParts[1] : agentName;
|
|
335
|
+
const model = meta.model || meta.provider || null;
|
|
336
|
+
const totalInputTokens = meta.usage && typeof meta.usage.input_tokens === 'number' ? meta.usage.input_tokens : 0;
|
|
337
|
+
const totalOutputTokens = meta.usage && typeof meta.usage.output_tokens === 'number' ? meta.usage.output_tokens : 0;
|
|
338
|
+
const totalTokens = meta.usage && typeof meta.usage.total_tokens === 'number'
|
|
339
|
+
? meta.usage.total_tokens
|
|
340
|
+
: totalInputTokens + totalOutputTokens;
|
|
341
|
+
const commitIndex = db.transaction(() => {
|
|
342
|
+
if (stmts.deleteArchive)
|
|
343
|
+
stmts.deleteArchive.run(sessionId);
|
|
344
|
+
stmts.deleteFileActivity.run(sessionId);
|
|
345
|
+
stmts.deleteEvents.run(sessionId);
|
|
346
|
+
stmts.deleteSession.run(sessionId);
|
|
347
|
+
stmts.upsertSession.run(sessionId, startTime, endTime, 0, 0, model, summary, inferredAgent, 'cron', 0, totalTokens, totalInputTokens, totalOutputTokens, 0, 0, null, null, null, model ? JSON.stringify([model]) : null, null);
|
|
348
|
+
stmts.upsertState.run(filePath, 1, mtime);
|
|
349
|
+
});
|
|
350
|
+
commitIndex();
|
|
351
|
+
return { sessionId, synthetic: true };
|
|
352
|
+
}
|
|
353
|
+
function indexFile(db, filePath, agentName, stmts, archiveMode, config) {
|
|
354
|
+
const stat = node_fs_1.default.statSync(filePath);
|
|
355
|
+
const mtime = stat.mtime.toISOString();
|
|
356
|
+
if (!REINDEX) {
|
|
357
|
+
const state = stmts.getState.get(filePath);
|
|
358
|
+
if (state && state.last_modified === mtime)
|
|
359
|
+
return { skipped: true };
|
|
360
|
+
}
|
|
361
|
+
const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
362
|
+
const lines = raw.trim().split('\n').filter(Boolean);
|
|
363
|
+
if (lines.length === 0)
|
|
364
|
+
return { skipped: true };
|
|
365
|
+
let sessionId = null;
|
|
366
|
+
let sessionStart = null;
|
|
367
|
+
let sessionEnd = null;
|
|
368
|
+
let msgCount = 0;
|
|
369
|
+
let toolCount = 0;
|
|
370
|
+
let model = null;
|
|
371
|
+
const modelsSet = new Set();
|
|
372
|
+
let summary = '';
|
|
373
|
+
let sessionType = null;
|
|
374
|
+
let agent = agentName;
|
|
375
|
+
let totalCost = 0;
|
|
376
|
+
let totalTokens = 0;
|
|
377
|
+
let totalInputTokens = 0;
|
|
378
|
+
let totalOutputTokens = 0;
|
|
379
|
+
let totalCacheReadTokens = 0;
|
|
380
|
+
let totalCacheWriteTokens = 0;
|
|
381
|
+
let initialPrompt = null;
|
|
382
|
+
let firstMessageId = null;
|
|
383
|
+
let firstMessageTimestamp = null;
|
|
384
|
+
let codexProvider = null;
|
|
385
|
+
let codexSource = null;
|
|
386
|
+
let codexOriginator = null;
|
|
387
|
+
let sawSnapshotRecord = false;
|
|
388
|
+
let sawNonSnapshotRecord = false;
|
|
389
|
+
let firstLine;
|
|
390
|
+
try {
|
|
391
|
+
firstLine = JSON.parse(lines[0]);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return { skipped: true };
|
|
395
|
+
}
|
|
396
|
+
let isClaudeCode = false;
|
|
397
|
+
let isCodexCli = false;
|
|
398
|
+
if (firstLine.type === 'session') {
|
|
399
|
+
// OpenClaw format
|
|
400
|
+
sessionId = firstLine.id || null;
|
|
401
|
+
sessionStart = firstLine.timestamp || null;
|
|
402
|
+
if (firstLine.agent)
|
|
403
|
+
agent = firstLine.agent;
|
|
404
|
+
if (firstLine.sessionType)
|
|
405
|
+
sessionType = firstLine.sessionType;
|
|
406
|
+
if (sessionId && sessionId.includes('subagent'))
|
|
407
|
+
sessionType = 'subagent';
|
|
408
|
+
}
|
|
409
|
+
else if (firstLine.type === 'user' || firstLine.type === 'assistant' || firstLine.type === 'file-history-snapshot' || firstLine.type === 'queue-operation') {
|
|
410
|
+
// Claude Code format — no session header, extract from first message or queue-operation line
|
|
411
|
+
isClaudeCode = true;
|
|
412
|
+
for (const line of lines) {
|
|
413
|
+
let obj;
|
|
414
|
+
try {
|
|
415
|
+
obj = JSON.parse(line);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (obj.sessionId && obj.timestamp) {
|
|
421
|
+
sessionId = obj.sessionId;
|
|
422
|
+
sessionStart = obj.timestamp;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if ((obj.type === 'user' || obj.type === 'assistant') && obj.sessionId) {
|
|
426
|
+
sessionId = obj.sessionId;
|
|
427
|
+
sessionStart = obj.timestamp || null;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!sessionId) {
|
|
432
|
+
// Fallback: use filename as session ID
|
|
433
|
+
sessionId = node_path_1.default.basename(filePath, '.jsonl');
|
|
434
|
+
sessionStart = new Date(firstLine.timestamp || Date.now()).toISOString();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (firstLine.type === 'session_meta') {
|
|
438
|
+
// Codex CLI format
|
|
439
|
+
isCodexCli = true;
|
|
440
|
+
const meta = (firstLine.payload || {});
|
|
441
|
+
sessionId = meta.id || node_path_1.default.basename(filePath, '.jsonl');
|
|
442
|
+
sessionStart = meta.timestamp || firstLine.timestamp || new Date().toISOString();
|
|
443
|
+
sessionType = 'codex-direct';
|
|
444
|
+
agent = 'codex-cli';
|
|
445
|
+
if (meta.model) {
|
|
446
|
+
model = meta.model;
|
|
447
|
+
modelsSet.add(meta.model);
|
|
448
|
+
}
|
|
449
|
+
codexProvider = meta.model_provider || null;
|
|
450
|
+
codexSource = meta.source || null;
|
|
451
|
+
codexOriginator = meta.originator || null;
|
|
452
|
+
if (codexOriginator && codexOriginator.includes('symphony'))
|
|
453
|
+
sessionType = 'codex-symphony';
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
return { skipped: true };
|
|
457
|
+
}
|
|
458
|
+
// --- Parse the entire file BEFORE any DB operations ---
|
|
459
|
+
const pendingEvents = [];
|
|
460
|
+
const fileActivities = [];
|
|
461
|
+
const projectCounts = new Map();
|
|
462
|
+
// Seed project from session cwd when available (helps chat-only sessions)
|
|
463
|
+
const sessionCwd = (firstLine && firstLine.cwd) || (firstLine && firstLine.payload && firstLine.payload.cwd);
|
|
464
|
+
if (sessionCwd) {
|
|
465
|
+
const p = extractProjectFromPath(sessionCwd, config);
|
|
466
|
+
if (p)
|
|
467
|
+
projectCounts.set(p, 1);
|
|
468
|
+
}
|
|
469
|
+
for (const line of lines) {
|
|
470
|
+
let obj;
|
|
471
|
+
try {
|
|
472
|
+
obj = JSON.parse(line);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (obj.type === 'file-history-snapshot')
|
|
478
|
+
sawSnapshotRecord = true;
|
|
479
|
+
else
|
|
480
|
+
sawNonSnapshotRecord = true;
|
|
481
|
+
if (isCodexCli) {
|
|
482
|
+
if (obj.type === 'session_meta') {
|
|
483
|
+
const meta = (obj.payload || {});
|
|
484
|
+
if (meta.id)
|
|
485
|
+
sessionId = meta.id;
|
|
486
|
+
if (meta.timestamp && !sessionStart)
|
|
487
|
+
sessionStart = meta.timestamp;
|
|
488
|
+
if (meta.model) {
|
|
489
|
+
if (!model)
|
|
490
|
+
model = meta.model;
|
|
491
|
+
modelsSet.add(meta.model);
|
|
492
|
+
}
|
|
493
|
+
if (meta.model_provider)
|
|
494
|
+
codexProvider = meta.model_provider;
|
|
495
|
+
if (meta.source)
|
|
496
|
+
codexSource = meta.source;
|
|
497
|
+
if (meta.originator)
|
|
498
|
+
codexOriginator = meta.originator;
|
|
499
|
+
if (codexOriginator && codexOriginator.includes('symphony'))
|
|
500
|
+
sessionType = 'codex-symphony';
|
|
501
|
+
if (meta.model_provider && !model)
|
|
502
|
+
model = meta.model_provider;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (obj.type === 'turn_context' && obj.payload) {
|
|
506
|
+
const tc = obj.payload;
|
|
507
|
+
if (tc.model && typeof tc.model === 'string') {
|
|
508
|
+
if (!model || model === codexProvider)
|
|
509
|
+
model = tc.model;
|
|
510
|
+
modelsSet.add(tc.model);
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (obj.type === 'response_item' && obj.payload) {
|
|
515
|
+
const p = obj.payload;
|
|
516
|
+
const ts = obj.timestamp || sessionStart;
|
|
517
|
+
const eventId = `evt-${obj.type}-${Date.parse(ts) || Math.random()}`;
|
|
518
|
+
if (p.type === 'function_call') {
|
|
519
|
+
const toolName = p.name || p.tool_name || '';
|
|
520
|
+
const toolArgs = typeof p.arguments === 'string' ? p.arguments : JSON.stringify(p.arguments || {});
|
|
521
|
+
const callBaseId = p.call_id || p.id || eventId;
|
|
522
|
+
pendingEvents.push([`${callBaseId}:call`, sessionId, ts, 'tool_call', 'assistant', null, toolName, toolArgs, null]);
|
|
523
|
+
toolCount++;
|
|
524
|
+
const fps = extractFilePaths(toolName, toolArgs);
|
|
525
|
+
for (const fp of fps) {
|
|
526
|
+
fileActivities.push([sessionId, fp, 'read', ts]);
|
|
527
|
+
const project = extractProjectFromPath(fp, config);
|
|
528
|
+
if (project)
|
|
529
|
+
projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
|
|
530
|
+
}
|
|
531
|
+
sessionEnd = ts;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (p.type === 'function_call_output') {
|
|
535
|
+
const output = (typeof p.output === 'string' ? p.output : JSON.stringify(p.output || '')).slice(0, 10000);
|
|
536
|
+
const resultBaseId = p.call_id || p.id || eventId;
|
|
537
|
+
pendingEvents.push([`${resultBaseId}:result`, sessionId, ts, 'tool_result', 'tool', output, p.name || p.tool_name || '', null, output]);
|
|
538
|
+
sessionEnd = ts;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (p.type === 'message') {
|
|
542
|
+
const rawRole = p.role || 'assistant';
|
|
543
|
+
const role = rawRole === 'assistant' ? 'assistant' : 'user';
|
|
544
|
+
const content = extractCodexMessageText(p.content);
|
|
545
|
+
if (content) {
|
|
546
|
+
pendingEvents.push([p.id || eventId, sessionId, ts, 'message', role, content, null, null, null]);
|
|
547
|
+
msgCount++;
|
|
548
|
+
if (!summary && role === 'user' && isSummaryCandidate(content))
|
|
549
|
+
summary = content.slice(0, 200);
|
|
550
|
+
if (!initialPrompt && role === 'user' && isSummaryCandidate(content)) {
|
|
551
|
+
initialPrompt = content.slice(0, 500);
|
|
552
|
+
firstMessageId = p.id || eventId;
|
|
553
|
+
firstMessageTimestamp = ts;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
sessionEnd = ts;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (obj.type === 'event_msg' && obj.payload) {
|
|
561
|
+
const p = obj.payload;
|
|
562
|
+
const ts = obj.timestamp || sessionStart;
|
|
563
|
+
const eventId = `evt-${p.type || 'event'}-${Date.parse(ts) || Math.random()}`;
|
|
564
|
+
if (p.type === 'agent_message' && p.message) {
|
|
565
|
+
pendingEvents.push([eventId, sessionId, ts, 'message', 'assistant', p.message, null, null, null]);
|
|
566
|
+
msgCount++;
|
|
567
|
+
sessionEnd = ts;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (p.type === 'user_message' && p.message) {
|
|
571
|
+
pendingEvents.push([eventId, sessionId, ts, 'message', 'user', p.message, null, null, null]);
|
|
572
|
+
msgCount++;
|
|
573
|
+
if (!summary && isSummaryCandidate(p.message))
|
|
574
|
+
summary = p.message.slice(0, 200);
|
|
575
|
+
if (!initialPrompt && isSummaryCandidate(p.message)) {
|
|
576
|
+
initialPrompt = p.message.slice(0, 500);
|
|
577
|
+
firstMessageId = eventId;
|
|
578
|
+
firstMessageTimestamp = ts;
|
|
579
|
+
}
|
|
580
|
+
sessionEnd = ts;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (obj.type === 'session' || obj.type === 'model_change' || obj.type === 'thinking_level_change' || obj.type === 'custom' || obj.type === 'file-history-snapshot') {
|
|
587
|
+
if (obj.type === 'model_change' && obj.modelId) {
|
|
588
|
+
if (!model)
|
|
589
|
+
model = obj.modelId; // First model for backwards compat
|
|
590
|
+
modelsSet.add(obj.modelId); // Collect all unique models
|
|
591
|
+
}
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
// Normalize: Claude Code uses top-level type "user"/"assistant" with message object
|
|
595
|
+
// OpenClaw uses type "message" with message.role
|
|
596
|
+
let msg;
|
|
597
|
+
let ts;
|
|
598
|
+
if (obj.type === 'message' && obj.message) {
|
|
599
|
+
msg = obj.message;
|
|
600
|
+
ts = obj.timestamp;
|
|
601
|
+
}
|
|
602
|
+
else if ((obj.type === 'user' || obj.type === 'assistant') && obj.message) {
|
|
603
|
+
// Claude Code format: wrap into consistent shape
|
|
604
|
+
msg = obj.message;
|
|
605
|
+
if (!msg.role)
|
|
606
|
+
msg.role = obj.type === 'user' ? 'user' : 'assistant';
|
|
607
|
+
ts = obj.timestamp;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (msg) {
|
|
613
|
+
sessionEnd = ts || null;
|
|
614
|
+
// Extract model from assistant messages
|
|
615
|
+
if (msg.role === 'assistant' && msg.model && msg.model !== 'delivery-mirror' && !msg.model.startsWith('<')) {
|
|
616
|
+
if (!model)
|
|
617
|
+
model = msg.model; // Keep first model for backwards compat
|
|
618
|
+
modelsSet.add(msg.model); // Collect all unique models
|
|
619
|
+
}
|
|
620
|
+
// Cost tracking
|
|
621
|
+
const usage = msg.usage;
|
|
622
|
+
if (usage) {
|
|
623
|
+
const cost = usage.cost;
|
|
624
|
+
if (cost && typeof cost.total === 'number') {
|
|
625
|
+
totalCost += cost.total;
|
|
626
|
+
}
|
|
627
|
+
if (typeof usage.totalTokens === 'number') {
|
|
628
|
+
totalTokens += usage.totalTokens;
|
|
629
|
+
}
|
|
630
|
+
// OpenClaw format
|
|
631
|
+
if (typeof usage.input === 'number')
|
|
632
|
+
totalInputTokens += usage.input;
|
|
633
|
+
if (typeof usage.output === 'number')
|
|
634
|
+
totalOutputTokens += usage.output;
|
|
635
|
+
if (typeof usage.cacheRead === 'number')
|
|
636
|
+
totalCacheReadTokens += usage.cacheRead;
|
|
637
|
+
if (typeof usage.cacheWrite === 'number')
|
|
638
|
+
totalCacheWriteTokens += usage.cacheWrite;
|
|
639
|
+
// Claude Code format
|
|
640
|
+
if (typeof usage.input_tokens === 'number')
|
|
641
|
+
totalInputTokens += usage.input_tokens;
|
|
642
|
+
if (typeof usage.output_tokens === 'number')
|
|
643
|
+
totalOutputTokens += usage.output_tokens;
|
|
644
|
+
if (typeof usage.cache_read_input_tokens === 'number')
|
|
645
|
+
totalCacheReadTokens += usage.cache_read_input_tokens;
|
|
646
|
+
if (typeof usage.cache_creation_input_tokens === 'number')
|
|
647
|
+
totalCacheWriteTokens += usage.cache_creation_input_tokens;
|
|
648
|
+
}
|
|
649
|
+
const eventId = obj.id || obj.uuid || `evt-${Date.parse(ts) || Math.random()}`;
|
|
650
|
+
const tr = extractToolResult(msg);
|
|
651
|
+
if (tr) {
|
|
652
|
+
pendingEvents.push([eventId, sessionId, ts, 'tool_result', 'tool', tr.content, tr.toolName, null, tr.content]);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const content = extractContent(msg);
|
|
656
|
+
const role = msg.role || 'unknown';
|
|
657
|
+
if (content) {
|
|
658
|
+
pendingEvents.push([eventId, sessionId, ts, 'message', role, content, null, null, null]);
|
|
659
|
+
msgCount++;
|
|
660
|
+
// Better summary: skip heartbeat/boilerplate messages
|
|
661
|
+
if (!summary && role === 'user' && isSummaryCandidate(content)) {
|
|
662
|
+
summary = content.slice(0, 200);
|
|
663
|
+
}
|
|
664
|
+
// Capture initial prompt from first substantial user message
|
|
665
|
+
if (!initialPrompt && role === 'user' && isSummaryCandidate(content)) {
|
|
666
|
+
initialPrompt = content.slice(0, 500); // Limit to 500 chars
|
|
667
|
+
firstMessageId = eventId;
|
|
668
|
+
firstMessageTimestamp = ts || null;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const tools = extractToolCalls(msg);
|
|
672
|
+
for (const tool of tools) {
|
|
673
|
+
pendingEvents.push([tool.id || `${eventId}-${tool.name}`, sessionId, ts, 'tool_call', role, null, tool.name, tool.args, null]);
|
|
674
|
+
toolCount++;
|
|
675
|
+
// File activity tracking
|
|
676
|
+
const fps = extractFilePaths(tool.name, tool.args);
|
|
677
|
+
for (const fp of fps) {
|
|
678
|
+
const op = tool.name.includes('write') || tool.name === 'Write' ? 'write'
|
|
679
|
+
: tool.name.includes('edit') || tool.name === 'Edit' ? 'edit'
|
|
680
|
+
: 'read';
|
|
681
|
+
fileActivities.push([sessionId, fp, op, ts]);
|
|
682
|
+
const project = extractProjectFromPath(fp, config);
|
|
683
|
+
if (project)
|
|
684
|
+
projectCounts.set(project, (projectCounts.get(project) || 0) + 1);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Classify snapshot-only Claude files explicitly (avoid heartbeat mislabel)
|
|
690
|
+
if (isClaudeCode && sawSnapshotRecord && !sawNonSnapshotRecord) {
|
|
691
|
+
sessionType = 'snapshot';
|
|
692
|
+
if (!summary)
|
|
693
|
+
summary = 'Claude file snapshot';
|
|
694
|
+
}
|
|
695
|
+
// Normalize summary text
|
|
696
|
+
if (summary)
|
|
697
|
+
summary = stripLeadingDatetimePrefix(summary);
|
|
698
|
+
// If no real summary found, set a sensible default
|
|
699
|
+
if (!summary) {
|
|
700
|
+
if (isCodexCli) {
|
|
701
|
+
const parts = ['Codex CLI session'];
|
|
702
|
+
if (codexProvider)
|
|
703
|
+
parts.push(`provider=${codexProvider}`);
|
|
704
|
+
if (codexSource)
|
|
705
|
+
parts.push(`source=${codexSource}`);
|
|
706
|
+
if (codexOriginator)
|
|
707
|
+
parts.push(`originator=${codexOriginator}`);
|
|
708
|
+
summary = parts.join(' · ');
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
summary = 'Heartbeat session';
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// Infer session type from first user message content
|
|
715
|
+
if (!sessionType && initialPrompt) {
|
|
716
|
+
const p = initialPrompt.toLowerCase();
|
|
717
|
+
if (p.includes('[cron:'))
|
|
718
|
+
sessionType = 'cron';
|
|
719
|
+
else if (p.includes('heartbeat') && p.includes('heartbeat_ok'))
|
|
720
|
+
sessionType = 'heartbeat';
|
|
721
|
+
}
|
|
722
|
+
if (!sessionType && !initialPrompt)
|
|
723
|
+
sessionType = 'heartbeat';
|
|
724
|
+
// Detect subagent: task-style prompts injected by sessions_spawn
|
|
725
|
+
if (!sessionType && initialPrompt) {
|
|
726
|
+
const p = initialPrompt.trim();
|
|
727
|
+
if (/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-/.test(p) && !p.includes('[System Message]')) {
|
|
728
|
+
sessionType = 'subagent';
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const modelsJson = modelsSet.size > 0 ? JSON.stringify([...modelsSet]) : null;
|
|
732
|
+
const projects = [...projectCounts.entries()]
|
|
733
|
+
.sort((a, b) => (b[1] - a[1]) || a[0].localeCompare(b[0]))
|
|
734
|
+
.map(([name]) => name);
|
|
735
|
+
const projectsJson = projects.length > 0 ? JSON.stringify(projects) : null;
|
|
736
|
+
// --- All DB operations in a single transaction for atomicity ---
|
|
737
|
+
const commitIndex = db.transaction(() => {
|
|
738
|
+
stmts.deleteEvents.run(sessionId);
|
|
739
|
+
stmts.deleteFileActivity.run(sessionId);
|
|
740
|
+
if (stmts.deleteArchive)
|
|
741
|
+
stmts.deleteArchive.run(sessionId);
|
|
742
|
+
stmts.deleteSession.run(sessionId);
|
|
743
|
+
stmts.upsertSession.run(sessionId, sessionStart, sessionEnd, msgCount, toolCount, model, summary, agent, sessionType, totalCost, totalTokens, totalInputTokens, totalOutputTokens, totalCacheReadTokens, totalCacheWriteTokens, initialPrompt, firstMessageId, firstMessageTimestamp, modelsJson, projectsJson);
|
|
744
|
+
for (const ev of pendingEvents)
|
|
745
|
+
stmts.insertEvent.run(...ev);
|
|
746
|
+
for (const fa of fileActivities)
|
|
747
|
+
stmts.insertFileActivity.run(...fa);
|
|
748
|
+
// Archive mode: store raw JSONL lines
|
|
749
|
+
if (archiveMode && stmts.insertArchive) {
|
|
750
|
+
for (let i = 0; i < lines.length; i++) {
|
|
751
|
+
stmts.insertArchive.run(sessionId, i + 1, lines[i]);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
stmts.upsertState.run(filePath, lines.length, mtime);
|
|
755
|
+
});
|
|
756
|
+
commitIndex();
|
|
757
|
+
return { sessionId: sessionId, msgCount, toolCount };
|
|
758
|
+
}
|
|
759
|
+
function run() {
|
|
760
|
+
const config = (0, config_js_1.loadConfig)();
|
|
761
|
+
(0, db_js_1.init)();
|
|
762
|
+
const db = (0, db_js_1.open)();
|
|
763
|
+
const archiveMode = config.storage === 'archive';
|
|
764
|
+
console.log(`AgentActa indexer running in ${config.storage} mode`);
|
|
765
|
+
const stmts = (0, db_js_1.createStmts)(db);
|
|
766
|
+
const sessionDirs = discoverSessionDirs(config);
|
|
767
|
+
console.log(`Discovered ${sessionDirs.length} session directories:`);
|
|
768
|
+
sessionDirs.forEach(d => console.log(` ${d.agent}: ${d.path}`));
|
|
769
|
+
let allFiles = [];
|
|
770
|
+
for (const dir of sessionDirs) {
|
|
771
|
+
const files = listJsonlFiles(dir.path, !!dir.recursive)
|
|
772
|
+
.map(filePath => ({ path: filePath, agent: dir.agent, sourceType: dir.sourceType || 'transcript' }));
|
|
773
|
+
allFiles.push(...files);
|
|
774
|
+
}
|
|
775
|
+
console.log(`Found ${allFiles.length} session files`);
|
|
776
|
+
const indexMany = db.transaction(() => {
|
|
777
|
+
let indexed = 0;
|
|
778
|
+
for (const f of allFiles) {
|
|
779
|
+
const result = f.sourceType === 'cron-run'
|
|
780
|
+
? indexCronRunFile(db, f.path, f.agent, stmts)
|
|
781
|
+
: indexFile(db, f.path, f.agent, stmts, archiveMode, config);
|
|
782
|
+
if (!result.skipped) {
|
|
783
|
+
indexed++;
|
|
784
|
+
if (indexed % 10 === 0)
|
|
785
|
+
process.stdout.write('.');
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return indexed;
|
|
789
|
+
});
|
|
790
|
+
const count = indexMany();
|
|
791
|
+
console.log(`\nIndexed ${count} sessions`);
|
|
792
|
+
const stats = db.prepare('SELECT COUNT(*) as sessions FROM sessions').get();
|
|
793
|
+
const evStats = db.prepare('SELECT COUNT(*) as events FROM events').get();
|
|
794
|
+
console.log(`Total: ${stats.sessions} sessions, ${evStats.events} events`);
|
|
795
|
+
if (WATCH) {
|
|
796
|
+
console.log('\nWatching for changes...');
|
|
797
|
+
const rescanTimers = new Map();
|
|
798
|
+
for (const dir of sessionDirs) {
|
|
799
|
+
node_fs_1.default.watch(dir.path, { persistent: true }, (_eventType, filename) => {
|
|
800
|
+
// Recursive sources (e.g. ~/.codex/sessions/YYYY/MM/DD/*.jsonl):
|
|
801
|
+
// fs.watch on Linux does not watch nested dirs recursively, so on any root event
|
|
802
|
+
// run a debounced full rescan of known JSONL files under this source.
|
|
803
|
+
if (dir.recursive) {
|
|
804
|
+
const key = dir.path;
|
|
805
|
+
const existing = rescanTimers.get(key);
|
|
806
|
+
if (existing)
|
|
807
|
+
clearTimeout(existing);
|
|
808
|
+
const t = setTimeout(() => {
|
|
809
|
+
try {
|
|
810
|
+
const files = listJsonlFiles(dir.path, true);
|
|
811
|
+
let changed = 0;
|
|
812
|
+
for (const filePath of files) {
|
|
813
|
+
const result = dir.sourceType === 'cron-run'
|
|
814
|
+
? indexCronRunFile(db, filePath, dir.agent, stmts)
|
|
815
|
+
: indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
|
|
816
|
+
if (!result.skipped)
|
|
817
|
+
changed++;
|
|
818
|
+
}
|
|
819
|
+
if (changed > 0)
|
|
820
|
+
console.log(`Re-indexed ${changed} files (${dir.agent})`);
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
console.error(`Error rescanning ${dir.path}:`, err.message);
|
|
824
|
+
}
|
|
825
|
+
}, 500);
|
|
826
|
+
rescanTimers.set(key, t);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (!filename || !filename.endsWith('.jsonl'))
|
|
830
|
+
return;
|
|
831
|
+
const filePath = node_path_1.default.join(dir.path, filename);
|
|
832
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
833
|
+
return;
|
|
834
|
+
setTimeout(() => {
|
|
835
|
+
try {
|
|
836
|
+
const result = dir.sourceType === 'cron-run'
|
|
837
|
+
? indexCronRunFile(db, filePath, dir.agent, stmts)
|
|
838
|
+
: indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
|
|
839
|
+
if (!result.skipped)
|
|
840
|
+
console.log(`Re-indexed: ${filename} (${dir.agent})`);
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
844
|
+
}
|
|
845
|
+
}, 500);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
db.close();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function indexAll(db, config) {
|
|
854
|
+
const sessionDirs = discoverSessionDirs(config);
|
|
855
|
+
const archiveMode = config.storage === 'archive';
|
|
856
|
+
const stmts = (0, db_js_1.createStmts)(db);
|
|
857
|
+
let totalSessions = 0;
|
|
858
|
+
for (const dir of sessionDirs) {
|
|
859
|
+
const files = listJsonlFiles(dir.path, !!dir.recursive);
|
|
860
|
+
for (const filePath of files) {
|
|
861
|
+
try {
|
|
862
|
+
const result = dir.sourceType === 'cron-run'
|
|
863
|
+
? indexCronRunFile(db, filePath, dir.agent, stmts)
|
|
864
|
+
: indexFile(db, filePath, dir.agent, stmts, archiveMode, config);
|
|
865
|
+
if (!result.skipped)
|
|
866
|
+
totalSessions++;
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
console.error(`Error indexing ${node_path_1.default.basename(filePath)}:`, err.message);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const stats = db.prepare('SELECT COUNT(*) as sessions FROM sessions').get();
|
|
874
|
+
const evStats = db.prepare('SELECT COUNT(*) as events FROM events').get();
|
|
875
|
+
return { sessions: stats.sessions, events: evStats.events, newSessions: totalSessions };
|
|
876
|
+
}
|
|
877
|
+
if (require.main === module)
|
|
878
|
+
run();
|
|
879
|
+
//# sourceMappingURL=indexer.js.map
|