@statforge/claudestat 1.6.1 → 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.
Files changed (48) hide show
  1. package/README.md +3 -1
  2. package/dashboard/dist/assets/AnalyticsView-DDGLDoCN.js +7 -0
  3. package/dashboard/dist/assets/HistoryView-DkPfrNrv.js +1 -0
  4. package/dashboard/dist/assets/LineChart-BOWYkkEW.js +2 -0
  5. package/dashboard/dist/assets/ProjectsView-VRoRiEL4.js +6 -0
  6. package/dashboard/dist/assets/SystemView-B2zbIxhY.js +1 -0
  7. package/dashboard/dist/assets/TopView-C2qdsy0Y.js +1 -0
  8. package/dashboard/dist/assets/index-CMhe3KaT.js +84 -0
  9. package/dashboard/dist/assets/shared-BbBtsdh1.js +1 -0
  10. package/dashboard/dist/assets/{vendor-lucide-Cym0q5l_.js → vendor-lucide-ClCW-axQ.js} +79 -64
  11. package/dashboard/dist/assets/{vendor-react-B_Jzs0gY.js → vendor-react-gHSHIE2L.js} +1 -1
  12. package/dashboard/dist/index.html +3 -3
  13. package/dist/daemon.js +57 -1
  14. package/dist/db.d.ts +76 -2
  15. package/dist/db.js +295 -65
  16. package/dist/doctor.js +1 -1
  17. package/dist/enricher.d.ts +3 -2
  18. package/dist/enricher.js +10 -5
  19. package/dist/index.js +12 -1
  20. package/dist/intelligence.d.ts +55 -0
  21. package/dist/intelligence.js +163 -1
  22. package/dist/paths.d.ts +5 -0
  23. package/dist/paths.js +8 -0
  24. package/dist/pricing.d.ts +2 -0
  25. package/dist/pricing.js +12 -1
  26. package/dist/routes/events.js +136 -5
  27. package/dist/routes/history.js +6 -2
  28. package/dist/routes/intents.d.ts +1 -0
  29. package/dist/routes/intents.js +155 -0
  30. package/dist/routes/misc.js +131 -3
  31. package/dist/routes/opencode-reader.js +39 -3
  32. package/dist/routes/projects.js +10 -1
  33. package/dist/routes/replay.d.ts +1 -0
  34. package/dist/routes/replay.js +29 -0
  35. package/dist/routes/reports.js +7 -0
  36. package/dist/routes/top.js +8 -1
  37. package/dist/watchers/adapter.d.ts +1 -0
  38. package/dist/watchers/claude-code.d.ts +16 -1
  39. package/dist/watchers/claude-code.js +201 -76
  40. package/dist/watchers/opencode.d.ts +1 -0
  41. package/dist/watchers/opencode.js +152 -14
  42. package/package.json +1 -1
  43. package/dashboard/dist/assets/AnalyticsView-5bUM3UHp.js +0 -8
  44. package/dashboard/dist/assets/HistoryView-C-AsEqos.js +0 -1
  45. package/dashboard/dist/assets/ProjectsView-D9bZBdY2.js +0 -6
  46. package/dashboard/dist/assets/SystemView-DIYDCCF3.js +0 -1
  47. package/dashboard/dist/assets/TopView-DhdLpsiA.js +0 -1
  48. 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.getContextWindow = getContextWindow;
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 fileContent;
34
+ let fd;
42
35
  try {
43
- fileContent = await promises_1.default.readFile(filePath, 'utf8');
44
- }
45
- catch {
46
- return null;
47
- }
48
- const currentSize = Buffer.byteLength(fileContent, 'utf8');
49
- const knownEntry = fileOffsets.get(filePath);
50
- const knownOffset = knownEntry?.offset ?? 0;
51
- if (currentSize < knownOffset)
52
- fileOffsets.set(filePath, { offset: 0, lastAccess: Date.now() });
53
- const totals = {
54
- input_tokens: 0, output_tokens: 0,
55
- cache_read: 0, cache_creation: 0, cost_usd: 0,
56
- context_used: 0, context_window: 200000
57
- };
58
- let lastInputUsd = 0;
59
- let lastOutputUsd = 0;
60
- let lastInputTokens = 0;
61
- let lastOutputTokens = 0;
62
- let lastModel;
63
- let firstTs;
64
- for (const raw of fileContent.split('\n')) {
65
- const line = raw.trim();
66
- if (!line)
67
- continue;
68
- try {
69
- const obj = JSON.parse(line);
70
- if (obj.type !== 'assistant')
71
- continue;
72
- const msg = obj.message;
73
- if (!msg?.usage)
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
- const usage = msg.usage;
76
- const model = msg.model ?? 'claude-sonnet-4-6';
77
- if (firstTs === undefined && obj.timestamp) {
78
- try {
79
- firstTs = new Date(obj.timestamp).getTime();
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
- catch { /* ignore */ }
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
- totals.input_tokens += usage.input_tokens ?? 0;
84
- totals.output_tokens += usage.output_tokens ?? 0;
85
- totals.cache_read += usage.cache_read_input_tokens ?? 0;
86
- totals.cache_creation += usage.cache_creation_input_tokens ?? 0;
87
- totals.cost_usd += (0, pricing_1.calcCost)(model, usage);
88
- totals.context_used = (usage.input_tokens ?? 0)
89
- + (usage.cache_read_input_tokens ?? 0)
90
- + (usage.cache_creation_input_tokens ?? 0);
91
- totals.context_window = getContextWindow(model);
92
- const price = pricing_1.PRICING[model] ?? pricing_1.DEFAULT_PRICING;
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
- catch { /* skip malformed lines */ }
125
+ fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now(), totals });
126
+ return totals;
103
127
  }
104
- if (lastInputUsd + lastOutputUsd > 0) {
105
- totals.lastEntry = {
106
- inputUsd: lastInputUsd, outputUsd: lastOutputUsd,
107
- totalUsd: lastInputUsd + lastOutputUsd,
108
- inputTokens: lastInputTokens, outputTokens: lastOutputTokens,
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();
@@ -7,3 +7,4 @@
7
7
  */
8
8
  import { type PollableAdapter } from './adapter';
9
9
  export declare const opencodeAdapter: PollableAdapter;
10
+ export declare function isSessionArchived(sessionId: string): boolean;
@@ -11,9 +11,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.opencodeAdapter = void 0;
14
+ exports.isSessionArchived = isSessionArchived;
14
15
  const fs_1 = __importDefault(require("fs"));
16
+ const os_1 = __importDefault(require("os"));
15
17
  const adapter_1 = require("./adapter");
16
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");
17
22
  function openDb() {
18
23
  const { DatabaseSync } = require('node:sqlite');
19
24
  return new DatabaseSync((0, paths_1.getOpencodeDb)(), { open: true });
@@ -29,6 +34,101 @@ function parseModel(raw) {
29
34
  return 'unknown';
30
35
  }
31
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
+ }
32
132
  exports.opencodeAdapter = {
33
133
  name: 'opencode',
34
134
  label: 'OpenCode',
@@ -54,23 +154,50 @@ exports.opencodeAdapter = {
54
154
  let db = null;
55
155
  try {
56
156
  db = openDb();
57
- const rows = db.prepare(`SELECT id, model, cost, tokens_input, tokens_output, tokens_cache_read, tokens_cache_write, time_updated
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
58
159
  FROM session
59
160
  WHERE time_updated >= ?
60
161
  AND time_archived IS NULL`).all(since);
61
- return rows.map(row => ({
62
- sessionId: row.id,
63
- cost: {
64
- input_tokens: row.tokens_input,
65
- output_tokens: row.tokens_output,
66
- cache_read: row.tokens_cache_read,
67
- cache_creation: row.tokens_cache_write,
68
- cost_usd: row.cost,
69
- context_used: 0,
70
- context_window: 200000,
71
- lastModel: parseModel(row.model),
72
- },
73
- }));
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
+ });
74
201
  }
75
202
  catch {
76
203
  return [];
@@ -80,4 +207,15 @@ exports.opencodeAdapter = {
80
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statforge/claudestat",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"