@statforge/claudestat 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dashboard/dist/assets/AnalyticsView-DDGLDoCN.js +7 -0
- package/dashboard/dist/assets/HistoryView-DkPfrNrv.js +1 -0
- package/dashboard/dist/assets/LineChart-BOWYkkEW.js +2 -0
- package/dashboard/dist/assets/ProjectsView-VRoRiEL4.js +6 -0
- package/dashboard/dist/assets/SystemView-B2zbIxhY.js +1 -0
- package/dashboard/dist/assets/TopView-C2qdsy0Y.js +1 -0
- package/dashboard/dist/assets/index-CMhe3KaT.js +84 -0
- package/dashboard/dist/assets/shared-BbBtsdh1.js +1 -0
- package/dashboard/dist/assets/{vendor-lucide-Cym0q5l_.js → vendor-lucide-ClCW-axQ.js} +79 -64
- package/dashboard/dist/assets/{vendor-react-B_Jzs0gY.js → vendor-react-gHSHIE2L.js} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/dist/daemon.js +58 -2
- package/dist/db.d.ts +76 -2
- package/dist/db.js +295 -65
- package/dist/doctor.js +1 -1
- package/dist/enricher.d.ts +3 -2
- package/dist/enricher.js +12 -5
- package/dist/index.js +12 -1
- package/dist/intelligence.d.ts +55 -0
- package/dist/intelligence.js +163 -1
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +17 -0
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +12 -1
- package/dist/routes/events.js +136 -5
- package/dist/routes/history.js +6 -2
- package/dist/routes/intents.d.ts +1 -0
- package/dist/routes/intents.js +155 -0
- package/dist/routes/misc.js +132 -4
- package/dist/routes/opencode-reader.js +42 -8
- package/dist/routes/projects.js +10 -1
- package/dist/routes/replay.d.ts +1 -0
- package/dist/routes/replay.js +29 -0
- package/dist/routes/reports.js +7 -0
- package/dist/routes/stream.js +1 -1
- package/dist/routes/top.js +8 -1
- package/dist/watchers/adapter.d.ts +1 -0
- package/dist/watchers/claude-code.d.ts +16 -1
- package/dist/watchers/claude-code.js +201 -76
- package/dist/watchers/opencode.d.ts +1 -0
- package/dist/watchers/opencode.js +161 -23
- package/package.json +1 -1
- package/dashboard/dist/assets/AnalyticsView-5bUM3UHp.js +0 -8
- package/dashboard/dist/assets/HistoryView-C-AsEqos.js +0 -1
- package/dashboard/dist/assets/ProjectsView-D9bZBdY2.js +0 -6
- package/dashboard/dist/assets/SystemView-DIYDCCF3.js +0 -1
- package/dashboard/dist/assets/TopView-DhdLpsiA.js +0 -1
- package/dashboard/dist/assets/index-DgbWvj42.js +0 -84
|
@@ -10,7 +10,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.claudeCodeAdapter = void 0;
|
|
13
|
-
exports.
|
|
13
|
+
exports.findJSONLForSession = findJSONLForSession;
|
|
14
|
+
exports.extractSemanticData = extractSemanticData;
|
|
14
15
|
exports.getAllBlockCostsForSession = getAllBlockCostsForSession;
|
|
15
16
|
exports.getSessionPrompts = getSessionPrompts;
|
|
16
17
|
const path_1 = __importDefault(require("path"));
|
|
@@ -20,14 +21,6 @@ const adapter_1 = require("./adapter");
|
|
|
20
21
|
const paths_1 = require("../paths");
|
|
21
22
|
const pricing_1 = require("../pricing");
|
|
22
23
|
function projectsDir() { return path_1.default.join((0, paths_1.getClaudeDir)(), 'projects'); }
|
|
23
|
-
const KNOWN_CONTEXT_WINDOWS = {
|
|
24
|
-
'claude-opus-4-6': 200000,
|
|
25
|
-
'claude-sonnet-4-6': 200000,
|
|
26
|
-
'claude-haiku-4-5': 200000,
|
|
27
|
-
};
|
|
28
|
-
function getContextWindow(model) {
|
|
29
|
-
return KNOWN_CONTEXT_WINDOWS[model] ?? 200000;
|
|
30
|
-
}
|
|
31
24
|
const fileOffsets = new Map();
|
|
32
25
|
const FILE_OFFSET_TTL = 30 * 60000;
|
|
33
26
|
function cleanupStaleOffsets() {
|
|
@@ -38,80 +31,106 @@ function cleanupStaleOffsets() {
|
|
|
38
31
|
}
|
|
39
32
|
}
|
|
40
33
|
async function processJSONL(filePath) {
|
|
41
|
-
let
|
|
34
|
+
let fd;
|
|
42
35
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
36
|
+
fd = await promises_1.default.open(filePath, 'r');
|
|
37
|
+
const currentSize = (await fd.stat()).size;
|
|
38
|
+
// File was truncated (e.g., /compact) — drop cached state and re-read from start
|
|
39
|
+
let state = fileOffsets.get(filePath);
|
|
40
|
+
if (state && currentSize < state.offset) {
|
|
41
|
+
fileOffsets.delete(filePath);
|
|
42
|
+
state = undefined;
|
|
43
|
+
}
|
|
44
|
+
const fromByte = state?.offset ?? 0;
|
|
45
|
+
// Nothing new — return cached totals without lastEntry to avoid duplicate SSE
|
|
46
|
+
if (currentSize === fromByte && state)
|
|
47
|
+
return { ...state.totals, lastEntry: undefined };
|
|
48
|
+
// Read only the new bytes since last processed offset
|
|
49
|
+
const buf = Buffer.alloc(currentSize - fromByte);
|
|
50
|
+
await fd.read(buf, 0, buf.length, fromByte);
|
|
51
|
+
const newContent = buf.toString('utf8');
|
|
52
|
+
// Accumulate on top of previous totals (or start from zero on first read)
|
|
53
|
+
const prevTotals = state?.totals;
|
|
54
|
+
const totals = {
|
|
55
|
+
input_tokens: prevTotals?.input_tokens ?? 0,
|
|
56
|
+
output_tokens: prevTotals?.output_tokens ?? 0,
|
|
57
|
+
cache_read: prevTotals?.cache_read ?? 0,
|
|
58
|
+
cache_creation: prevTotals?.cache_creation ?? 0,
|
|
59
|
+
cost_usd: prevTotals?.cost_usd ?? 0,
|
|
60
|
+
context_used: prevTotals?.context_used ?? 0,
|
|
61
|
+
context_window: prevTotals?.context_window ?? 200000,
|
|
62
|
+
firstTs: prevTotals?.firstTs,
|
|
63
|
+
lastModel: prevTotals?.lastModel,
|
|
64
|
+
};
|
|
65
|
+
let lastInputUsd = 0;
|
|
66
|
+
let lastOutputUsd = 0;
|
|
67
|
+
let lastInputTokens = 0;
|
|
68
|
+
let lastOutputTokens = 0;
|
|
69
|
+
let hasNewAssistant = false;
|
|
70
|
+
for (const raw of newContent.split('\n')) {
|
|
71
|
+
const line = raw.trim();
|
|
72
|
+
if (!line)
|
|
74
73
|
continue;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
try {
|
|
75
|
+
const obj = JSON.parse(line);
|
|
76
|
+
if (obj.type !== 'assistant')
|
|
77
|
+
continue;
|
|
78
|
+
const msg = obj.message;
|
|
79
|
+
if (!msg?.usage)
|
|
80
|
+
continue;
|
|
81
|
+
const usage = msg.usage;
|
|
82
|
+
const model = msg.model ?? 'claude-sonnet-4-6';
|
|
83
|
+
hasNewAssistant = true;
|
|
84
|
+
if (obj.timestamp) {
|
|
85
|
+
try {
|
|
86
|
+
const ts = new Date(obj.timestamp).getTime();
|
|
87
|
+
if (totals.firstTs === undefined)
|
|
88
|
+
totals.firstTs = ts;
|
|
89
|
+
totals.lastTs = ts;
|
|
90
|
+
}
|
|
91
|
+
catch { /* ignore */ }
|
|
80
92
|
}
|
|
81
|
-
|
|
93
|
+
totals.input_tokens += usage.input_tokens ?? 0;
|
|
94
|
+
totals.output_tokens += usage.output_tokens ?? 0;
|
|
95
|
+
totals.cache_read += usage.cache_read_input_tokens ?? 0;
|
|
96
|
+
totals.cache_creation += usage.cache_creation_input_tokens ?? 0;
|
|
97
|
+
totals.cost_usd += (0, pricing_1.calcCost)(model, usage);
|
|
98
|
+
totals.context_used = (usage.input_tokens ?? 0)
|
|
99
|
+
+ (usage.cache_read_input_tokens ?? 0)
|
|
100
|
+
+ (usage.cache_creation_input_tokens ?? 0);
|
|
101
|
+
totals.context_window = (0, pricing_1.getContextWindow)(model);
|
|
102
|
+
const price = pricing_1.PRICING[model] ?? pricing_1.DEFAULT_PRICING;
|
|
103
|
+
const M = 1000000;
|
|
104
|
+
const inp = usage.input_tokens ?? 0;
|
|
105
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
106
|
+
const cacheCreate = usage.cache_creation_input_tokens ?? 0;
|
|
107
|
+
const out = usage.output_tokens ?? 0;
|
|
108
|
+
lastInputUsd = (inp * price.input + cacheRead * price.cacheRead + cacheCreate * price.cacheCreate) / M;
|
|
109
|
+
lastOutputUsd = out * price.output / M;
|
|
110
|
+
lastInputTokens = inp + cacheRead + cacheCreate;
|
|
111
|
+
lastOutputTokens = out;
|
|
112
|
+
totals.lastModel = model;
|
|
82
113
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
totals.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const M = 1000000;
|
|
94
|
-
lastInputUsd = ((usage.input_tokens ?? 0) * price.input +
|
|
95
|
-
(usage.cache_read_input_tokens ?? 0) * price.cacheRead +
|
|
96
|
-
(usage.cache_creation_input_tokens ?? 0) * price.cacheCreate) / M;
|
|
97
|
-
lastOutputUsd = ((usage.output_tokens ?? 0) * price.output) / M;
|
|
98
|
-
lastInputTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
99
|
-
lastOutputTokens = usage.output_tokens ?? 0;
|
|
100
|
-
lastModel = model ?? lastModel;
|
|
114
|
+
catch { /* skip malformed lines */ }
|
|
115
|
+
}
|
|
116
|
+
// lastEntry only when there are new API calls — drives the block_cost SSE event
|
|
117
|
+
if (hasNewAssistant && lastInputUsd + lastOutputUsd > 0) {
|
|
118
|
+
const totalUsd = lastInputUsd + lastOutputUsd;
|
|
119
|
+
totals.lastEntry = {
|
|
120
|
+
inputUsd: lastInputUsd, outputUsd: lastOutputUsd,
|
|
121
|
+
totalUsd,
|
|
122
|
+
inputTokens: lastInputTokens, outputTokens: lastOutputTokens,
|
|
123
|
+
};
|
|
101
124
|
}
|
|
102
|
-
|
|
125
|
+
fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now(), totals });
|
|
126
|
+
return totals;
|
|
103
127
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
};
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await fd?.close();
|
|
110
133
|
}
|
|
111
|
-
totals.lastModel = lastModel;
|
|
112
|
-
totals.firstTs = firstTs;
|
|
113
|
-
fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now() });
|
|
114
|
-
return totals;
|
|
115
134
|
}
|
|
116
135
|
exports.claudeCodeAdapter = {
|
|
117
136
|
name: 'claude-code',
|
|
@@ -152,6 +171,112 @@ exports.claudeCodeAdapter = {
|
|
|
152
171
|
};
|
|
153
172
|
setInterval(cleanupStaleOffsets, 5 * 60000).unref();
|
|
154
173
|
(0, adapter_1.registerAdapter)(exports.claudeCodeAdapter);
|
|
174
|
+
async function findJSONLForSession(sessionId) {
|
|
175
|
+
try {
|
|
176
|
+
if (!fs_1.default.existsSync(projectsDir()))
|
|
177
|
+
return null;
|
|
178
|
+
const dirs = await promises_1.default.readdir(projectsDir());
|
|
179
|
+
for (const dir of dirs) {
|
|
180
|
+
const dirPath = path_1.default.join(projectsDir(), dir);
|
|
181
|
+
try {
|
|
182
|
+
const stat = await promises_1.default.stat(dirPath);
|
|
183
|
+
if (!stat.isDirectory())
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
|
|
190
|
+
try {
|
|
191
|
+
await promises_1.default.access(filePath);
|
|
192
|
+
return filePath;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch { /* ignore */ }
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
async function extractSemanticData(filePath) {
|
|
203
|
+
try {
|
|
204
|
+
const content = await promises_1.default.readFile(filePath, 'utf8');
|
|
205
|
+
const turns = [];
|
|
206
|
+
let pendingTurn = null;
|
|
207
|
+
let totalErrorBlocks = 0;
|
|
208
|
+
let turnIndex = 0;
|
|
209
|
+
for (const raw of content.split('\n')) {
|
|
210
|
+
const line = raw.trim();
|
|
211
|
+
if (!line)
|
|
212
|
+
continue;
|
|
213
|
+
try {
|
|
214
|
+
const obj = JSON.parse(line);
|
|
215
|
+
if (obj.type === 'assistant') {
|
|
216
|
+
if (pendingTurn)
|
|
217
|
+
turns.push(pendingTurn);
|
|
218
|
+
const msgContent = obj.message?.content;
|
|
219
|
+
if (!Array.isArray(msgContent)) {
|
|
220
|
+
pendingTurn = null;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
let outputChars = 0;
|
|
224
|
+
const textParts = [];
|
|
225
|
+
const toolCalls = [];
|
|
226
|
+
for (const block of msgContent) {
|
|
227
|
+
if (block?.type === 'text' && typeof block.text === 'string') {
|
|
228
|
+
outputChars += block.text.length;
|
|
229
|
+
textParts.push(block.text);
|
|
230
|
+
}
|
|
231
|
+
else if (block?.type === 'tool_use' && typeof block.name === 'string') {
|
|
232
|
+
toolCalls.push(block.name);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let ts;
|
|
236
|
+
if (obj.timestamp) {
|
|
237
|
+
try {
|
|
238
|
+
ts = new Date(obj.timestamp).getTime();
|
|
239
|
+
}
|
|
240
|
+
catch { /* ignore */ }
|
|
241
|
+
}
|
|
242
|
+
const usage = obj.message?.usage;
|
|
243
|
+
const contextUsed = usage
|
|
244
|
+
? ((usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0))
|
|
245
|
+
: 0;
|
|
246
|
+
pendingTurn = {
|
|
247
|
+
turn_index: turnIndex++,
|
|
248
|
+
ts,
|
|
249
|
+
text_preview: textParts.join('\n').slice(0, 500),
|
|
250
|
+
tool_calls: toolCalls,
|
|
251
|
+
error_count: 0,
|
|
252
|
+
output_chars: outputChars,
|
|
253
|
+
context_used: contextUsed,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
else if ((obj.type === 'human' || obj.type === 'user') && pendingTurn) {
|
|
257
|
+
const msgContent = obj.message?.content;
|
|
258
|
+
if (!Array.isArray(msgContent))
|
|
259
|
+
continue;
|
|
260
|
+
for (const block of msgContent) {
|
|
261
|
+
if (block?.type === 'tool_result' && block.is_error === true) {
|
|
262
|
+
pendingTurn.error_count++;
|
|
263
|
+
totalErrorBlocks++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch { /* skip malformed lines */ }
|
|
269
|
+
}
|
|
270
|
+
if (pendingTurn)
|
|
271
|
+
turns.push(pendingTurn);
|
|
272
|
+
const totalOutputChars = turns.reduce((sum, t) => sum + t.output_chars, 0);
|
|
273
|
+
const avg_output_chars = turns.length > 0 ? Math.round(totalOutputChars / turns.length) : 0;
|
|
274
|
+
return { turns, avg_output_chars, error_block_count: totalErrorBlocks };
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
155
280
|
// ─── Session-level utilities (used by routes/stream and routes/misc) ───────────
|
|
156
281
|
const blockCostCache = new Map();
|
|
157
282
|
const costCacheLocks = new Map();
|
|
@@ -11,14 +11,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
};
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.opencodeAdapter = void 0;
|
|
14
|
-
|
|
15
|
-
const os_1 = __importDefault(require("os"));
|
|
14
|
+
exports.isSessionArchived = isSessionArchived;
|
|
16
15
|
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const os_1 = __importDefault(require("os"));
|
|
17
17
|
const adapter_1 = require("./adapter");
|
|
18
|
-
const
|
|
18
|
+
const paths_1 = require("../paths");
|
|
19
|
+
const pricing_1 = require("../pricing");
|
|
20
|
+
const db_1 = require("../db");
|
|
21
|
+
const helpers_1 = require("../routes/helpers");
|
|
19
22
|
function openDb() {
|
|
20
23
|
const { DatabaseSync } = require('node:sqlite');
|
|
21
|
-
return new DatabaseSync(
|
|
24
|
+
return new DatabaseSync((0, paths_1.getOpencodeDb)(), { open: true });
|
|
22
25
|
}
|
|
23
26
|
function parseModel(raw) {
|
|
24
27
|
if (!raw)
|
|
@@ -31,13 +34,108 @@ function parseModel(raw) {
|
|
|
31
34
|
return 'unknown';
|
|
32
35
|
}
|
|
33
36
|
}
|
|
37
|
+
const HOME = os_1.default.homedir();
|
|
38
|
+
function inferProjectFromParts(db, sessionId) {
|
|
39
|
+
if (!db)
|
|
40
|
+
return undefined;
|
|
41
|
+
const parts = db.prepare(`
|
|
42
|
+
SELECT p.data FROM part p
|
|
43
|
+
JOIN message m ON p.message_id = m.id
|
|
44
|
+
WHERE m.session_id = ?
|
|
45
|
+
AND json_extract(p.data, '$.type') = 'tool'
|
|
46
|
+
AND json_extract(p.data, '$.tool') IN ('read', 'write', 'edit', 'glob', 'grep')
|
|
47
|
+
ORDER BY p.time_created DESC
|
|
48
|
+
LIMIT 30
|
|
49
|
+
`).all(sessionId);
|
|
50
|
+
const roots = new Map();
|
|
51
|
+
for (const { data } of parts) {
|
|
52
|
+
try {
|
|
53
|
+
const input = JSON.parse(data).state?.input;
|
|
54
|
+
if (!input)
|
|
55
|
+
continue;
|
|
56
|
+
const filePath = input.filePath || input.path || input.file_path;
|
|
57
|
+
if (!filePath || typeof filePath !== 'string' || !filePath.startsWith('/'))
|
|
58
|
+
continue;
|
|
59
|
+
const root = (0, helpers_1.findProjectCwdForFile)(filePath);
|
|
60
|
+
if (root)
|
|
61
|
+
roots.set(root, (roots.get(root) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
if (roots.size === 0)
|
|
66
|
+
return undefined;
|
|
67
|
+
return [...roots.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
68
|
+
}
|
|
69
|
+
function importToolEvents(ocDb, sessionId, targetSessionId) {
|
|
70
|
+
const parts = ocDb.prepare(`
|
|
71
|
+
SELECT p.id, p.data, p.time_created
|
|
72
|
+
FROM part p
|
|
73
|
+
JOIN message m ON p.message_id = m.id
|
|
74
|
+
WHERE m.session_id = ?
|
|
75
|
+
AND json_extract(p.data, '$.type') = 'tool'
|
|
76
|
+
AND json_extract(p.data, '$.state') IS NOT NULL
|
|
77
|
+
`).all(sessionId);
|
|
78
|
+
const destId = targetSessionId ?? sessionId;
|
|
79
|
+
for (const part of parts) {
|
|
80
|
+
try {
|
|
81
|
+
const toolName = JSON.parse(part.data).tool;
|
|
82
|
+
if (!toolName)
|
|
83
|
+
continue;
|
|
84
|
+
db_1.dbOps.insertOcEvent(destId, toolName, part.time_created, part.id);
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ─── Session grouping: merge consecutive OC sessions from same conversation ────
|
|
90
|
+
// OpenCode creates a new session row per prompt, even within the same conversation.
|
|
91
|
+
// We group sessions by directory + close time_created (<60s apart) into one.
|
|
92
|
+
function groupOcSessions(rows) {
|
|
93
|
+
const sorted = [...rows].sort((a, b) => a.time_created - b.time_created);
|
|
94
|
+
const groups = new Map(); // master id → group
|
|
95
|
+
const grouped = new Set();
|
|
96
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
97
|
+
if (grouped.has(sorted[i].id))
|
|
98
|
+
continue;
|
|
99
|
+
const group = [sorted[i]];
|
|
100
|
+
grouped.add(sorted[i].id);
|
|
101
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
102
|
+
const prev = group[group.length - 1];
|
|
103
|
+
const curr = sorted[j];
|
|
104
|
+
if (grouped.has(curr.id))
|
|
105
|
+
continue;
|
|
106
|
+
if (curr.directory !== prev.directory)
|
|
107
|
+
continue;
|
|
108
|
+
const gap = curr.time_created - prev.time_created;
|
|
109
|
+
if (gap > 60000)
|
|
110
|
+
break; // too far apart, stop looking
|
|
111
|
+
group.push(curr);
|
|
112
|
+
grouped.add(curr.id);
|
|
113
|
+
}
|
|
114
|
+
groups.set(group[0].id, group);
|
|
115
|
+
}
|
|
116
|
+
// Flatten: return only master (first) rows, with aggregated cost/tokens
|
|
117
|
+
return [...groups.values()].map(group => {
|
|
118
|
+
const master = { ...group[0] };
|
|
119
|
+
for (let k = 1; k < group.length; k++) {
|
|
120
|
+
const s = group[k];
|
|
121
|
+
master.cost += s.cost;
|
|
122
|
+
master.tokens_input += s.tokens_input;
|
|
123
|
+
master.tokens_output += s.tokens_output;
|
|
124
|
+
master.tokens_cache_read += s.tokens_cache_read;
|
|
125
|
+
master.tokens_cache_write += s.tokens_cache_write;
|
|
126
|
+
if (s.time_updated > master.time_updated)
|
|
127
|
+
master.time_updated = s.time_updated;
|
|
128
|
+
}
|
|
129
|
+
return master;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
34
132
|
exports.opencodeAdapter = {
|
|
35
133
|
name: 'opencode',
|
|
36
134
|
label: 'OpenCode',
|
|
37
135
|
get shortName() { return 'OC'; },
|
|
38
136
|
detect() {
|
|
39
137
|
try {
|
|
40
|
-
return fs_1.default.existsSync(
|
|
138
|
+
return fs_1.default.existsSync((0, paths_1.getOpencodeDb)());
|
|
41
139
|
}
|
|
42
140
|
catch {
|
|
43
141
|
return false;
|
|
@@ -53,31 +151,71 @@ exports.opencodeAdapter = {
|
|
|
53
151
|
return null;
|
|
54
152
|
},
|
|
55
153
|
async pollSessions(since) {
|
|
154
|
+
let db = null;
|
|
56
155
|
try {
|
|
57
|
-
|
|
58
|
-
const
|
|
156
|
+
db = openDb();
|
|
157
|
+
const rows = db.prepare(`SELECT id, directory, model, cost, tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
|
|
158
|
+
time_created, time_updated
|
|
59
159
|
FROM session
|
|
60
160
|
WHERE time_updated >= ?
|
|
61
|
-
AND time_archived IS NULL`);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
161
|
+
AND time_archived IS NULL`).all(since);
|
|
162
|
+
const grouped = groupOcSessions(rows);
|
|
163
|
+
// Build master lookup: original session id → master session id
|
|
164
|
+
const masterMap = new Map();
|
|
165
|
+
for (let i = 0; i < rows.length; i++) {
|
|
166
|
+
const masterId = grouped.find(g => g.id === rows[i].id)?.id
|
|
167
|
+
?? rows[i].id;
|
|
168
|
+
masterMap.set(rows[i].id, masterId);
|
|
169
|
+
}
|
|
170
|
+
// Import tool events into the master session ID
|
|
171
|
+
for (const s of rows) {
|
|
172
|
+
const masterId = masterMap.get(s.id) ?? s.id;
|
|
173
|
+
try {
|
|
174
|
+
importToolEvents(db, s.id, masterId);
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
}
|
|
178
|
+
return grouped.map(row => {
|
|
179
|
+
const modelId = parseModel(row.model);
|
|
180
|
+
const dir = row.directory;
|
|
181
|
+
const shouldInfer = !dir || dir === HOME || dir.split('/').filter(Boolean).length <= 3;
|
|
182
|
+
const inferred = shouldInfer ? inferProjectFromParts(db, row.id) : undefined;
|
|
183
|
+
const projectCwd = inferred ?? dir ?? undefined;
|
|
184
|
+
return {
|
|
185
|
+
sessionId: row.id,
|
|
186
|
+
cwd: projectCwd,
|
|
187
|
+
cost: {
|
|
188
|
+
input_tokens: row.tokens_input,
|
|
189
|
+
output_tokens: row.tokens_output,
|
|
190
|
+
cache_read: row.tokens_cache_read,
|
|
191
|
+
cache_creation: row.tokens_cache_write,
|
|
192
|
+
cost_usd: row.cost,
|
|
193
|
+
context_used: row.tokens_input + row.tokens_cache_read + row.tokens_cache_write,
|
|
194
|
+
context_window: (0, pricing_1.getContextWindow)(modelId),
|
|
195
|
+
lastModel: modelId,
|
|
196
|
+
firstTs: row.time_created,
|
|
197
|
+
lastTs: row.time_updated,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
});
|
|
77
201
|
}
|
|
78
202
|
catch {
|
|
79
203
|
return [];
|
|
80
204
|
}
|
|
205
|
+
finally {
|
|
206
|
+
db?.close();
|
|
207
|
+
}
|
|
81
208
|
},
|
|
82
209
|
};
|
|
210
|
+
function isSessionArchived(sessionId) {
|
|
211
|
+
try {
|
|
212
|
+
const db = openDb();
|
|
213
|
+
const row = db.prepare('SELECT time_archived FROM session WHERE id = ?').get(sessionId);
|
|
214
|
+
db.close();
|
|
215
|
+
return row === undefined || row.time_archived !== null;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
83
221
|
(0, adapter_1.registerAdapter)(exports.opencodeAdapter);
|