devday 0.1.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 (47) hide show
  1. package/README.md +58 -0
  2. package/bin/devday.js +2 -0
  3. package/dist/config.d.ts +14 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +89 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/cost.d.ts +12 -0
  8. package/dist/cost.d.ts.map +1 -0
  9. package/dist/cost.js +47 -0
  10. package/dist/cost.js.map +1 -0
  11. package/dist/git.d.ts +6 -0
  12. package/dist/git.d.ts.map +1 -0
  13. package/dist/git.js +95 -0
  14. package/dist/git.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +231 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/merge.d.ts +6 -0
  20. package/dist/merge.d.ts.map +1 -0
  21. package/dist/merge.js +80 -0
  22. package/dist/merge.js.map +1 -0
  23. package/dist/parsers/claude-code.d.ts +18 -0
  24. package/dist/parsers/claude-code.d.ts.map +1 -0
  25. package/dist/parsers/claude-code.js +385 -0
  26. package/dist/parsers/claude-code.js.map +1 -0
  27. package/dist/parsers/cursor.d.ts +15 -0
  28. package/dist/parsers/cursor.d.ts.map +1 -0
  29. package/dist/parsers/cursor.js +302 -0
  30. package/dist/parsers/cursor.js.map +1 -0
  31. package/dist/parsers/opencode.d.ts +21 -0
  32. package/dist/parsers/opencode.d.ts.map +1 -0
  33. package/dist/parsers/opencode.js +301 -0
  34. package/dist/parsers/opencode.js.map +1 -0
  35. package/dist/render.d.ts +6 -0
  36. package/dist/render.d.ts.map +1 -0
  37. package/dist/render.js +161 -0
  38. package/dist/render.js.map +1 -0
  39. package/dist/summarize.d.ts +8 -0
  40. package/dist/summarize.d.ts.map +1 -0
  41. package/dist/summarize.js +157 -0
  42. package/dist/summarize.js.map +1 -0
  43. package/dist/types.d.ts +100 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +23 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +54 -0
@@ -0,0 +1,302 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import Database from 'better-sqlite3';
5
+ import { estimateCost, emptyTokenUsage } from '../cost.js';
6
+ // ── Cursor model name → known model ID mapping ──────────────────
7
+ const CURSOR_MODEL_MAP = {
8
+ 'composer-1': 'gpt-4o', // Cursor's default agent model
9
+ 'cheetah': 'gpt-4o-mini', // Cursor's fast model
10
+ 'default': 'gpt-4o',
11
+ 'gpt-5': 'gpt-4o', // closest proxy
12
+ 'gpt-5-codex': 'gpt-4o',
13
+ 'claude-4.5-sonnet-thinking': 'claude-3-5-sonnet-20241022',
14
+ 'claude-4-sonnet-thinking': 'claude-3-5-sonnet-20241022',
15
+ 'claude-4.5-opus-high-thinking': 'claude-3-opus-20240229',
16
+ };
17
+ function mapCursorModel(cursorModel) {
18
+ return CURSOR_MODEL_MAP[cursorModel] ?? cursorModel;
19
+ }
20
+ // ── Parser ───────────────────────────────────────────────────────
21
+ export class CursorParser {
22
+ name = 'cursor';
23
+ dbPath;
24
+ constructor(dbPath) {
25
+ this.dbPath = dbPath;
26
+ }
27
+ async isAvailable() {
28
+ return existsSync(this.dbPath);
29
+ }
30
+ async getSessions(date) {
31
+ // Build day boundaries in local time
32
+ const [year, month, day] = date.split('-').map(Number);
33
+ const dayStart = new Date(year, month - 1, day, 0, 0, 0, 0);
34
+ const dayEnd = new Date(year, month - 1, day, 23, 59, 59, 999);
35
+ const dayStartMs = dayStart.getTime();
36
+ const dayEndMs = dayEnd.getTime();
37
+ let db;
38
+ try {
39
+ db = new Database(this.dbPath, { readonly: true });
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ const sessions = [];
45
+ try {
46
+ // 1. Load all composerData entries
47
+ const rows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all();
48
+ for (const row of rows) {
49
+ let composer;
50
+ try {
51
+ const val = typeof row.value === 'string' ? row.value : row.value.toString('utf-8');
52
+ composer = JSON.parse(val);
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ if (!composer.composerId)
58
+ continue;
59
+ // 2. Date filter: session must overlap with target day
60
+ const createdMs = composer.createdAt ?? 0;
61
+ const updatedMs = composer.lastUpdatedAt ?? createdMs;
62
+ const overlapsDay = (createdMs >= dayStartMs && createdMs <= dayEndMs) ||
63
+ (updatedMs >= dayStartMs && updatedMs <= dayEndMs) ||
64
+ (createdMs <= dayStartMs && updatedMs >= dayEndMs);
65
+ if (!overlapsDay)
66
+ continue;
67
+ // 3. Load bubbles for this session
68
+ const bubbles = this.loadBubbles(db, composer, dayStartMs, dayEndMs);
69
+ if (bubbles.length === 0)
70
+ continue;
71
+ // 4. Build session
72
+ const session = this.buildSession(composer, bubbles, dayStartMs, dayEndMs);
73
+ if (session)
74
+ sessions.push(session);
75
+ }
76
+ }
77
+ finally {
78
+ db.close();
79
+ }
80
+ return sessions;
81
+ }
82
+ // ── Load bubbles ────────────────────────────────────────────────
83
+ loadBubbles(db, composer, dayStartMs, dayEndMs) {
84
+ const bubbles = [];
85
+ // Strategy 1: v1 inline conversation array
86
+ if (composer.conversation && Array.isArray(composer.conversation)) {
87
+ for (const bubble of composer.conversation) {
88
+ if (this.bubbleInDay(bubble, composer, dayStartMs, dayEndMs)) {
89
+ bubbles.push(bubble);
90
+ }
91
+ }
92
+ if (bubbles.length > 0)
93
+ return bubbles;
94
+ }
95
+ // Strategy 2: v3+ — load from separate KV entries
96
+ const headers = composer.fullConversationHeadersOnly;
97
+ if (headers && headers.length > 0) {
98
+ // Batch query: get all bubbles for this composer at once
99
+ const composerId = composer.composerId;
100
+ const rows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE ?").all(`bubbleId:${composerId}:%`);
101
+ const bubbleMap = new Map();
102
+ for (const row of rows) {
103
+ try {
104
+ const val = typeof row.value === 'string' ? row.value : row.value.toString('utf-8');
105
+ const bubble = JSON.parse(val);
106
+ // Extract bubbleId from key: "bubbleId:{composerId}:{bubbleId}"
107
+ const parts = row.key.split(':');
108
+ const bubbleId = parts.slice(2).join(':'); // rejoin in case bubbleId contains ':'
109
+ if (bubble && bubbleId) {
110
+ bubble.bubbleId = bubble.bubbleId ?? bubbleId;
111
+ bubbleMap.set(bubbleId, bubble);
112
+ }
113
+ }
114
+ catch {
115
+ // skip
116
+ }
117
+ }
118
+ // Also check conversationMap (sometimes data is inline)
119
+ if (composer.conversationMap) {
120
+ for (const [id, bubble] of Object.entries(composer.conversationMap)) {
121
+ if (bubble && typeof bubble === 'object' && !bubbleMap.has(id)) {
122
+ bubble.bubbleId = bubble.bubbleId ?? id;
123
+ bubbleMap.set(id, bubble);
124
+ }
125
+ }
126
+ }
127
+ // Return in conversation order, filtered by day
128
+ for (const header of headers) {
129
+ const bubble = bubbleMap.get(header.bubbleId);
130
+ if (bubble && bubble.type && this.bubbleInDay(bubble, composer, dayStartMs, dayEndMs)) {
131
+ bubbles.push(bubble);
132
+ }
133
+ }
134
+ }
135
+ return bubbles;
136
+ }
137
+ /** Check if a bubble's timestamp falls within the target day */
138
+ bubbleInDay(bubble, composer, dayStartMs, dayEndMs) {
139
+ const ts = this.getBubbleTimestamp(bubble, composer);
140
+ if (ts === null)
141
+ return true; // no timestamp — include by default (session already filtered)
142
+ return ts >= dayStartMs && ts <= dayEndMs;
143
+ }
144
+ /** Extract the best available timestamp from a bubble (ms epoch) */
145
+ getBubbleTimestamp(bubble, composer) {
146
+ // Prefer timingInfo (most reliable Unix ms timestamp)
147
+ if (bubble.timingInfo?.clientRpcSendTime) {
148
+ return bubble.timingInfo.clientRpcSendTime;
149
+ }
150
+ // v3 createdAt is ISO string
151
+ if (bubble.createdAt) {
152
+ const ts = new Date(bubble.createdAt).getTime();
153
+ if (!isNaN(ts))
154
+ return ts;
155
+ }
156
+ // Fall back to composer-level timestamps
157
+ return null;
158
+ }
159
+ // ── Build session from bubbles ──────────────────────────────────
160
+ buildSession(composer, bubbles, dayStartMs, dayEndMs) {
161
+ const MAX_MSG_DURATION_MS = 5 * 60 * 1000;
162
+ const home = homedir();
163
+ const userBubbles = bubbles.filter((b) => b.type === 1);
164
+ const aiBubbles = bubbles.filter((b) => b.type === 2);
165
+ if (userBubbles.length === 0 && aiBubbles.length === 0)
166
+ return null;
167
+ // ── Model detection ───────────────────────────────────────
168
+ const models = new Set();
169
+ for (const b of aiBubbles) {
170
+ const raw = b.modelInfo?.modelName ?? composer.modelConfig?.modelName;
171
+ if (raw)
172
+ models.add(raw);
173
+ }
174
+ if (models.size === 0 && composer.modelConfig?.modelName) {
175
+ models.add(composer.modelConfig.modelName);
176
+ }
177
+ // ── Token aggregation ─────────────────────────────────────
178
+ const totalTokens = emptyTokenUsage();
179
+ for (const b of aiBubbles) {
180
+ if (b.tokenCount) {
181
+ totalTokens.input += b.tokenCount.inputTokens ?? 0;
182
+ totalTokens.output += b.tokenCount.outputTokens ?? 0;
183
+ }
184
+ }
185
+ totalTokens.total = totalTokens.input + totalTokens.output;
186
+ // ── Cost estimation ───────────────────────────────────────
187
+ let totalCost = 0;
188
+ if (totalTokens.total > 0 && models.size > 0) {
189
+ const mappedModel = mapCursorModel([...models][0]);
190
+ totalCost = estimateCost(mappedModel, totalTokens);
191
+ }
192
+ // ── Duration ──────────────────────────────────────────────
193
+ let totalDurationMs = 0;
194
+ for (const b of aiBubbles) {
195
+ const ti = b.timingInfo;
196
+ if (ti?.clientRpcSendTime && ti?.clientEndTime) {
197
+ const dur = ti.clientEndTime - ti.clientRpcSendTime;
198
+ if (dur > 0) {
199
+ totalDurationMs += Math.min(dur, MAX_MSG_DURATION_MS);
200
+ }
201
+ }
202
+ }
203
+ // Fallback: if no timingInfo, estimate from timestamp gaps
204
+ if (totalDurationMs === 0 && bubbles.length > 1) {
205
+ const timestamps = bubbles
206
+ .map((b) => this.getBubbleTimestamp(b, composer))
207
+ .filter((t) => t !== null)
208
+ .sort((a, b) => a - b);
209
+ for (let i = 1; i < timestamps.length; i++) {
210
+ const gap = timestamps[i] - timestamps[i - 1];
211
+ if (gap > 0) {
212
+ totalDurationMs += Math.min(gap, MAX_MSG_DURATION_MS);
213
+ }
214
+ }
215
+ }
216
+ // ── Project path ──────────────────────────────────────────
217
+ // Extract from first bubble's cwd, or from any bubble that has it
218
+ let projectPath = null;
219
+ for (const b of bubbles) {
220
+ if (b.cwd) {
221
+ projectPath = b.cwd;
222
+ break;
223
+ }
224
+ }
225
+ // ── Timestamps ────────────────────────────────────────────
226
+ const allTimestamps = bubbles
227
+ .map((b) => this.getBubbleTimestamp(b, composer))
228
+ .filter((t) => t !== null);
229
+ const earliest = allTimestamps.length > 0
230
+ ? Math.max(dayStartMs, Math.min(...allTimestamps))
231
+ : composer.createdAt ?? dayStartMs;
232
+ const latest = allTimestamps.length > 0
233
+ ? Math.min(dayEndMs, Math.max(...allTimestamps))
234
+ : composer.lastUpdatedAt ?? dayEndMs;
235
+ // ── Content extraction ────────────────────────────────────
236
+ const digestParts = [];
237
+ const toolSummaries = [];
238
+ const files = new Set();
239
+ for (const b of bubbles) {
240
+ if (!b.text?.trim())
241
+ continue;
242
+ if (b.type === 1) {
243
+ const truncated = b.text.length > 500 ? b.text.slice(0, 500) + '...' : b.text;
244
+ digestParts.push(`[User]: ${truncated}`);
245
+ }
246
+ else if (b.type === 2) {
247
+ const truncated = b.text.length > 500 ? b.text.slice(0, 500) + '...' : b.text;
248
+ digestParts.push(`[Assistant]: ${truncated}`);
249
+ }
250
+ // Extract files from code blocks
251
+ if (b.codeBlocks) {
252
+ for (const cb of b.codeBlocks) {
253
+ const filePath = cb.uri?.fsPath ?? cb.uri?.path;
254
+ if (filePath) {
255
+ files.add(filePath);
256
+ const short = filePath.startsWith(home) ? '~' + filePath.slice(home.length) : filePath;
257
+ toolSummaries.push(`edit ${short}`);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ // Cap digest
263
+ let conversationDigest = digestParts.join('\n\n');
264
+ if (conversationDigest.length > 4000) {
265
+ conversationDigest = conversationDigest.slice(0, 4000) + '\n\n[...truncated]';
266
+ }
267
+ // Dedup tool summaries
268
+ const uniqueToolSummaries = [...new Set(toolSummaries)];
269
+ // ── Title ─────────────────────────────────────────────────
270
+ const title = composer.name ?? null;
271
+ // ── Topics ────────────────────────────────────────────────
272
+ const topics = [];
273
+ if (composer.name)
274
+ topics.push(composer.name);
275
+ if (composer.subtitle)
276
+ topics.push(composer.subtitle);
277
+ // Map model names for display
278
+ const displayModels = [...models].map(mapCursorModel);
279
+ return {
280
+ id: composer.composerId,
281
+ tool: 'cursor',
282
+ projectPath,
283
+ projectName: projectPath ? basename(projectPath) : null,
284
+ title,
285
+ startedAt: new Date(earliest),
286
+ endedAt: new Date(latest),
287
+ durationMs: totalDurationMs,
288
+ messageCount: bubbles.length,
289
+ userMessageCount: userBubbles.length,
290
+ assistantMessageCount: aiBubbles.length,
291
+ summary: composer.name ?? null,
292
+ topics,
293
+ tokens: totalTokens,
294
+ costUsd: totalCost,
295
+ models: displayModels,
296
+ filesTouched: [...files],
297
+ conversationDigest,
298
+ toolCallSummaries: uniqueToolSummaries,
299
+ };
300
+ }
301
+ }
302
+ //# sourceMappingURL=cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/parsers/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAa,MAAM,YAAY,CAAC;AAEtE,mEAAmE;AAEnE,MAAM,gBAAgB,GAA2B;IAC/C,YAAY,EAAE,QAAQ,EAAc,+BAA+B;IACnE,SAAS,EAAE,aAAa,EAAY,sBAAsB;IAC1D,SAAS,EAAE,QAAQ;IACnB,OAAO,EAAE,QAAQ,EAAmB,gBAAgB;IACpD,aAAa,EAAE,QAAQ;IACvB,4BAA4B,EAAE,4BAA4B;IAC1D,0BAA0B,EAAE,4BAA4B;IACxD,+BAA+B,EAAE,wBAAwB;CAC1D,CAAC;AAEF,SAAS,cAAc,CAAC,WAAmB;IACzC,OAAO,gBAAgB,CAAC,WAAW,CAAC,IAAI,WAAW,CAAC;AACtD,CAAC;AAkDD,oEAAoE;AAEpE,MAAM,OAAO,YAAY;IACd,IAAI,GAAG,QAAiB,CAAC;IAC1B,MAAM,CAAS;IAEvB,YAAY,MAAc;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,WAAW;QACf,OAAO,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAY;QAC5B,qCAAqC;QACrC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvD,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QAC/D,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;QAElC,IAAI,EAAiC,CAAC;QACtC,IAAI,CAAC;YACH,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,IAAI,CAAC;YACH,mCAAmC;YACnC,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CACrB,qEAAqE,CACtE,CAAC,GAAG,EAAoD,CAAC;YAE1D,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,IAAI,QAA4B,CAAC;gBACjC,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;oBACpF,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAuB,CAAC;gBACnD,CAAC;gBAAC,MAAM,CAAC;oBACP,SAAS;gBACX,CAAC;gBAED,IAAI,CAAC,QAAQ,CAAC,UAAU;oBAAE,SAAS;gBAEnC,uDAAuD;gBACvD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,IAAI,CAAC,CAAC;gBAC1C,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,IAAI,SAAS,CAAC;gBAEtD,MAAM,WAAW,GACf,CAAC,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,QAAQ,CAAC;oBAClD,CAAC,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,QAAQ,CAAC;oBAClD,CAAC,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,QAAQ,CAAC,CAAC;gBAErD,IAAI,CAAC,WAAW;oBAAE,SAAS;gBAE3B,mCAAmC;gBACnC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;gBACrE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;oBAAE,SAAS;gBAEnC,mBAAmB;gBACnB,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;gBAC3E,IAAI,OAAO;oBAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,EAAE,CAAC,KAAK,EAAE,CAAC;QACb,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,mEAAmE;IAE3D,WAAW,CACjB,EAAiC,EACjC,QAA4B,EAC5B,UAAkB,EAClB,QAAgB;QAEhB,MAAM,OAAO,GAAmB,EAAE,CAAC;QAEnC,2CAA2C;QAC3C,IAAI,QAAQ,CAAC,YAAY,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YAClE,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;gBAC3C,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;oBAC7D,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC;YACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,OAAO,CAAC;QACzC,CAAC;QAED,kDAAkD;QAClD,MAAM,OAAO,GAAG,QAAQ,CAAC,2BAA2B,CAAC;QACrD,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,yDAAyD;YACzD,MAAM,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC;YACvC,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CACrB,sDAAsD,CACvD,CAAC,GAAG,CAAC,YAAY,UAAU,IAAI,CAAmD,CAAC;YAEpF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAC;YAClD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;oBACpF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAC;oBAC/C,gEAAgE;oBAChE,MAAM,KAAK,GAAI,GAAG,CAAC,GAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;oBAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,uCAAuC;oBAClF,IAAI,MAAM,IAAI,QAAQ,EAAE,CAAC;wBACvB,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,QAAQ,CAAC;wBAC9C,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;oBAClC,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO;gBACT,CAAC;YACH,CAAC;YAED,wDAAwD;YACxD,IAAI,QAAQ,CAAC,eAAe,EAAE,CAAC;gBAC7B,KAAK,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;oBACpE,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;wBAC/D,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;wBACxC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;oBAC5B,CAAC;gBACH,CAAC;YACH,CAAC;YAED,gDAAgD;YAChD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAC9C,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;oBACtF,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,gEAAgE;IACxD,WAAW,CACjB,MAAoB,EACpB,QAA4B,EAC5B,UAAkB,EAClB,QAAgB;QAEhB,MAAM,EAAE,GAAG,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACrD,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,CAAC,+DAA+D;QAC7F,OAAO,EAAE,IAAI,UAAU,IAAI,EAAE,IAAI,QAAQ,CAAC;IAC5C,CAAC;IAED,oEAAoE;IAC5D,kBAAkB,CAAC,MAAoB,EAAE,QAA4B;QAC3E,sDAAsD;QACtD,IAAI,MAAM,CAAC,UAAU,EAAE,iBAAiB,EAAE,CAAC;YACzC,OAAO,MAAM,CAAC,UAAU,CAAC,iBAAiB,CAAC;QAC7C,CAAC;QACD,6BAA6B;QAC7B,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;YAChD,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAAE,OAAO,EAAE,CAAC;QAC5B,CAAC;QACD,yCAAyC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,mEAAmE;IAE3D,YAAY,CAClB,QAA4B,EAC5B,OAAuB,EACvB,UAAkB,EAClB,QAAgB;QAEhB,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QAC1C,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;QAEvB,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC;QACxD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC;QAEtD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEpE,6DAA6D;QAC7D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;QACjC,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,EAAE,SAAS,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC;YACtE,IAAI,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,QAAQ,CAAC,WAAW,EAAE,SAAS,EAAE,CAAC;YACzD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAC7C,CAAC;QAED,6DAA6D;QAC7D,MAAM,WAAW,GAAG,eAAe,EAAE,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YAC1B,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;gBACjB,WAAW,CAAC,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC,WAAW,IAAI,CAAC,CAAC;gBACnD,WAAW,CAAC,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,YAAY,IAAI,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;QACD,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC;QAE3D,6DAA6D;QAC7D,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,IAAI,WAAW,CAAC,KAAK,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC7C,MAAM,WAAW,GAAG,cAAc,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACnD,SAAS,GAAG,YAAY,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;QACrD,CAAC;QAED,6DAA6D;QAC7D,IAAI,eAAe,GAAG,CAAC,CAAC;QACxB,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YAC1B,MAAM,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC;YACxB,IAAI,EAAE,EAAE,iBAAiB,IAAI,EAAE,EAAE,aAAa,EAAE,CAAC;gBAC/C,MAAM,GAAG,GAAG,EAAE,CAAC,aAAa,GAAG,EAAE,CAAC,iBAAiB,CAAC;gBACpD,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;oBACZ,eAAe,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;gBACxD,CAAC;YACH,CAAC;QACH,CAAC;QAED,2DAA2D;QAC3D,IAAI,eAAe,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChD,MAAM,UAAU,GAAG,OAAO;iBACvB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;iBAChD,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;iBACtC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAEzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC3C,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC9C,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;oBACZ,eAAe,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;gBACxD,CAAC;YACH,CAAC;QACH,CAAC;QAED,6DAA6D;QAC7D,kEAAkE;QAClE,IAAI,WAAW,GAAkB,IAAI,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;gBACV,WAAW,GAAG,CAAC,CAAC,GAAG,CAAC;gBACpB,MAAM;YACR,CAAC;QACH,CAAC;QAED,6DAA6D;QAC7D,MAAM,aAAa,GAAG,OAAO;aAC1B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;aAChD,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QAE1C,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC;YACvC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC;YAClD,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,UAAU,CAAC;QACrC,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC;YACrC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC;YAChD,CAAC,CAAC,QAAQ,CAAC,aAAa,IAAI,QAAQ,CAAC;QAEvC,6DAA6D;QAC7D,MAAM,WAAW,GAAa,EAAE,CAAC;QACjC,MAAM,aAAa,GAAa,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;QAEhC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE;gBAAE,SAAS;YAE9B,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACjB,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC9E,WAAW,CAAC,IAAI,CAAC,WAAW,SAAS,EAAE,CAAC,CAAC;YAC3C,CAAC;iBAAM,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACxB,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC9E,WAAW,CAAC,IAAI,CAAC,gBAAgB,SAAS,EAAE,CAAC,CAAC;YAChD,CAAC;YAED,iCAAiC;YACjC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;gBACjB,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;oBAC9B,MAAM,QAAQ,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC;oBAChD,IAAI,QAAQ,EAAE,CAAC;wBACb,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;wBACpB,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;wBACvF,aAAa,CAAC,IAAI,CAAC,QAAQ,KAAK,EAAE,CAAC,CAAC;oBACtC,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,aAAa;QACb,IAAI,kBAAkB,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClD,IAAI,kBAAkB,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;YACrC,kBAAkB,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,oBAAoB,CAAC;QAChF,CAAC;QAED,uBAAuB;QACvB,MAAM,mBAAmB,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC;QAExD,6DAA6D;QAC7D,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,IAAI,IAAI,CAAC;QAEpC,6DAA6D;QAC7D,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,QAAQ,CAAC,IAAI;YAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,QAAQ,CAAC,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAEtD,8BAA8B;QAC9B,MAAM,aAAa,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAEtD,OAAO;YACL,EAAE,EAAE,QAAQ,CAAC,UAAU;YACvB,IAAI,EAAE,QAAQ;YACd,WAAW;YACX,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI;YACvD,KAAK;YACL,SAAS,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC;YAC7B,OAAO,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC;YACzB,UAAU,EAAE,eAAe;YAC3B,YAAY,EAAE,OAAO,CAAC,MAAM;YAC5B,gBAAgB,EAAE,WAAW,CAAC,MAAM;YACpC,qBAAqB,EAAE,SAAS,CAAC,MAAM;YACvC,OAAO,EAAE,QAAQ,CAAC,IAAI,IAAI,IAAI;YAC9B,MAAM;YACN,MAAM,EAAE,WAAW;YACnB,OAAO,EAAE,SAAS;YAClB,MAAM,EAAE,aAAa;YACrB,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC;YACxB,kBAAkB;YAClB,iBAAiB,EAAE,mBAAmB;SACvC,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,21 @@
1
+ import type { Parser, Session } from '../types.js';
2
+ export declare class OpenCodeParser implements Parser {
3
+ readonly name: "opencode";
4
+ private storagePath;
5
+ constructor(storagePath: string);
6
+ isAvailable(): Promise<boolean>;
7
+ getSessions(date: string): Promise<Session[]>;
8
+ private loadProjects;
9
+ private loadSessionsForProject;
10
+ private loadMessagesForSession;
11
+ private loadPartsForMessage;
12
+ /**
13
+ * Single-pass extraction of all session content:
14
+ * - conversationDigest: user prompts + assistant text (for LLM summarization)
15
+ * - toolCallSummaries: human-readable tool call descriptions
16
+ * - filesTouched: unique file paths from tool calls and patches
17
+ */
18
+ private extractSessionContent;
19
+ private messageToTokens;
20
+ }
21
+ //# sourceMappingURL=opencode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../src/parsers/opencode.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAc,MAAM,aAAa,CAAC;AA6E/D,qBAAa,cAAe,YAAW,MAAM;IAC3C,QAAQ,CAAC,IAAI,EAAG,UAAU,CAAU;IACpC,OAAO,CAAC,WAAW,CAAS;gBAEhB,WAAW,EAAE,MAAM;IAIzB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IA2InD,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,mBAAmB;IAgB3B;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAyF7B,OAAO,CAAC,eAAe;CAkBxB"}
@@ -0,0 +1,301 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join, basename } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { estimateCost, emptyTokenUsage, sumTokens } from '../cost.js';
5
+ // ── Parser ───────────────────────────────────────────────────────
6
+ export class OpenCodeParser {
7
+ name = 'opencode';
8
+ storagePath;
9
+ constructor(storagePath) {
10
+ this.storagePath = storagePath;
11
+ }
12
+ async isAvailable() {
13
+ return existsSync(this.storagePath) && existsSync(join(this.storagePath, 'project'));
14
+ }
15
+ async getSessions(date) {
16
+ // Parse YYYY-MM-DD and build day boundaries in local time manually.
17
+ // Avoids date-fns startOfDay/endOfDay which can mishandle timezones.
18
+ const [year, month, day] = date.split('-').map(Number);
19
+ const dayStart = new Date(year, month - 1, day, 0, 0, 0, 0);
20
+ const dayEnd = new Date(year, month - 1, day, 23, 59, 59, 999);
21
+ const dayStartMs = dayStart.getTime();
22
+ const dayEndMs = dayEnd.getTime();
23
+ // 1. Load all projects
24
+ const projects = this.loadProjects();
25
+ const sessions = [];
26
+ for (const project of projects) {
27
+ const isGlobal = project.worktree === '/';
28
+ // 2. Load sessions for this project
29
+ const ocSessions = this.loadSessionsForProject(project.id);
30
+ for (const ocSession of ocSessions) {
31
+ // Filter by date: session must overlap with the target day
32
+ const sessionCreated = new Date(ocSession.time.created);
33
+ const sessionUpdated = new Date(ocSession.time.updated);
34
+ const overlapsDay = (sessionCreated >= dayStart && sessionCreated <= dayEnd) ||
35
+ (sessionUpdated >= dayStart && sessionUpdated <= dayEnd) ||
36
+ (sessionCreated <= dayStart && sessionUpdated >= dayEnd);
37
+ if (!overlapsDay)
38
+ continue;
39
+ // Skip sub-agent sessions (they have a parentID, their data is attributed to the parent)
40
+ if (ocSession.parentID)
41
+ continue;
42
+ // For global project sessions, use the session's directory as the project path.
43
+ // Skip if the directory is also "/" or missing (truly global, no project context).
44
+ if (isGlobal && (!ocSession.directory || ocSession.directory === '/'))
45
+ continue;
46
+ // 3. Load messages for this session (including sub-agent sessions)
47
+ const childSessionIds = ocSessions
48
+ .filter((s) => s.parentID === ocSession.id)
49
+ .map((s) => s.id);
50
+ const allSessionIds = [ocSession.id, ...childSessionIds];
51
+ const messages = allSessionIds.flatMap((sid) => this.loadMessagesForSession(sid));
52
+ // Filter messages to only those within the target day
53
+ const dayMessages = messages.filter((m) => {
54
+ const createdMs = m.time.created;
55
+ return createdMs >= dayStartMs && createdMs <= dayEndMs;
56
+ });
57
+ if (dayMessages.length === 0)
58
+ continue;
59
+ // 4. Compute aggregates from messages
60
+ const userMessages = dayMessages.filter((m) => m.role === 'user');
61
+ const assistantMessages = dayMessages.filter((m) => m.role === 'assistant');
62
+ const models = [...new Set(assistantMessages.map((m) => m.modelID).filter(Boolean))];
63
+ // Token aggregation
64
+ const tokenUsages = dayMessages.map((m) => this.messageToTokens(m));
65
+ const totalTokens = sumTokens(...tokenUsages);
66
+ // Cost estimation (OpenCode cost field is always 0)
67
+ let totalCost = 0;
68
+ for (const msg of assistantMessages) {
69
+ if (msg.modelID && msg.tokens) {
70
+ totalCost += estimateCost(msg.modelID, this.messageToTokens(msg));
71
+ }
72
+ }
73
+ // 5. Extract content: files touched, conversation text, tool call summaries
74
+ const contentExtraction = this.extractSessionContent(dayMessages);
75
+ // 6. Duration — sum actual message processing times, not wall-clock span
76
+ // Each message has created → completed timestamps representing active time.
77
+ // Clamp to target day boundaries to avoid cross-day inflation.
78
+ // Cap individual message duration at 5 minutes — no single LLM response
79
+ // should take longer; OpenCode sometimes writes bogus completed timestamps
80
+ // (e.g. reconciliation when session is loaded days later).
81
+ const MAX_MSG_DURATION_MS = 5 * 60 * 1000; // 5 minutes
82
+ let durationMs = 0;
83
+ let earliestTs = Infinity;
84
+ let latestTs = -Infinity;
85
+ for (const msg of dayMessages) {
86
+ const start = Math.max(dayStartMs, msg.time.created);
87
+ const end = msg.time.completed
88
+ ? Math.min(dayEndMs, msg.time.completed)
89
+ : start; // no completion = instant
90
+ if (end > start) {
91
+ durationMs += Math.min(end - start, MAX_MSG_DURATION_MS);
92
+ }
93
+ if (msg.time.created < earliestTs)
94
+ earliestTs = msg.time.created;
95
+ const msgEnd = msg.time.completed ?? msg.time.created;
96
+ if (msgEnd > latestTs)
97
+ latestTs = msgEnd;
98
+ }
99
+ // Clamp start/end for display purposes
100
+ const earliest = Math.max(dayStartMs, earliestTs);
101
+ const latest = Math.min(dayEndMs, latestTs === -Infinity ? earliest : latestTs);
102
+ // 7. Build topics from session title + summary
103
+ const topics = [];
104
+ if (ocSession.title)
105
+ topics.push(ocSession.title);
106
+ const sessionProjectPath = isGlobal ? ocSession.directory : project.worktree;
107
+ sessions.push({
108
+ id: ocSession.id,
109
+ tool: 'opencode',
110
+ projectPath: sessionProjectPath,
111
+ projectName: basename(sessionProjectPath),
112
+ title: ocSession.title ?? ocSession.slug ?? null,
113
+ startedAt: new Date(earliest),
114
+ endedAt: new Date(latest),
115
+ durationMs,
116
+ messageCount: dayMessages.length,
117
+ userMessageCount: userMessages.length,
118
+ assistantMessageCount: assistantMessages.length,
119
+ summary: ocSession.title ?? null,
120
+ topics,
121
+ tokens: totalTokens,
122
+ costUsd: totalCost,
123
+ models,
124
+ filesTouched: contentExtraction.filesTouched,
125
+ conversationDigest: contentExtraction.conversationDigest,
126
+ toolCallSummaries: contentExtraction.toolCallSummaries,
127
+ });
128
+ }
129
+ }
130
+ return sessions;
131
+ }
132
+ // ── Internal helpers ─────────────────────────────────────────
133
+ loadProjects() {
134
+ const dir = join(this.storagePath, 'project');
135
+ if (!existsSync(dir))
136
+ return [];
137
+ return readdirSync(dir)
138
+ .filter((f) => f.endsWith('.json'))
139
+ .map((f) => {
140
+ try {
141
+ return JSON.parse(readFileSync(join(dir, f), 'utf-8'));
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ })
147
+ .filter((p) => p !== null);
148
+ }
149
+ loadSessionsForProject(projectId) {
150
+ const dir = join(this.storagePath, 'session', projectId);
151
+ if (!existsSync(dir))
152
+ return [];
153
+ return readdirSync(dir)
154
+ .filter((f) => f.endsWith('.json'))
155
+ .map((f) => {
156
+ try {
157
+ return JSON.parse(readFileSync(join(dir, f), 'utf-8'));
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ })
163
+ .filter((s) => s !== null);
164
+ }
165
+ loadMessagesForSession(sessionId) {
166
+ const dir = join(this.storagePath, 'message', sessionId);
167
+ if (!existsSync(dir))
168
+ return [];
169
+ return readdirSync(dir)
170
+ .filter((f) => f.endsWith('.json'))
171
+ .map((f) => {
172
+ try {
173
+ return JSON.parse(readFileSync(join(dir, f), 'utf-8'));
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ })
179
+ .filter((m) => m !== null);
180
+ }
181
+ loadPartsForMessage(messageId) {
182
+ const dir = join(this.storagePath, 'part', messageId);
183
+ if (!existsSync(dir))
184
+ return [];
185
+ return readdirSync(dir)
186
+ .filter((f) => f.endsWith('.json'))
187
+ .map((f) => {
188
+ try {
189
+ return JSON.parse(readFileSync(join(dir, f), 'utf-8'));
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ })
195
+ .filter((p) => p !== null);
196
+ }
197
+ /**
198
+ * Single-pass extraction of all session content:
199
+ * - conversationDigest: user prompts + assistant text (for LLM summarization)
200
+ * - toolCallSummaries: human-readable tool call descriptions
201
+ * - filesTouched: unique file paths from tool calls and patches
202
+ */
203
+ extractSessionContent(messages) {
204
+ const digestParts = [];
205
+ const toolSummaries = [];
206
+ const files = new Set();
207
+ // Sort messages by creation time
208
+ const sorted = [...messages].sort((a, b) => a.time.created - b.time.created);
209
+ for (const msg of sorted) {
210
+ const parts = this.loadPartsForMessage(msg.id);
211
+ // Sort parts by their time if available
212
+ const textParts = [];
213
+ for (const part of parts) {
214
+ if (part.type === 'text' && part.text) {
215
+ textParts.push(part.text);
216
+ }
217
+ if (part.type === 'tool') {
218
+ const toolPart = part;
219
+ const toolName = toolPart.tool;
220
+ const input = toolPart.state?.input;
221
+ const title = toolPart.state?.title;
222
+ // Build a human-readable tool call summary
223
+ let summary = toolName;
224
+ if (title) {
225
+ summary = `${toolName}: ${title}`;
226
+ }
227
+ else if (input) {
228
+ const filePath = (input.filePath ?? input.path ?? input.file);
229
+ const command = input.command;
230
+ const pattern = input.pattern;
231
+ if (filePath) {
232
+ // Shorten to relative path (cross-platform)
233
+ const home = homedir();
234
+ const short = filePath.startsWith(home) ? '~' + filePath.slice(home.length) : filePath;
235
+ summary = `${toolName} ${short}`;
236
+ }
237
+ else if (command) {
238
+ summary = `bash: ${String(command).slice(0, 80)}`;
239
+ }
240
+ else if (pattern) {
241
+ summary = `${toolName}: ${pattern}`;
242
+ }
243
+ }
244
+ toolSummaries.push(summary);
245
+ // Extract file paths
246
+ if (input) {
247
+ if (typeof input.filePath === 'string')
248
+ files.add(input.filePath);
249
+ if (typeof input.path === 'string')
250
+ files.add(input.path);
251
+ if (typeof input.file === 'string')
252
+ files.add(input.file);
253
+ }
254
+ }
255
+ if (part.type === 'patch') {
256
+ const patchPart = part;
257
+ if (patchPart.path) {
258
+ files.add(patchPart.path);
259
+ toolSummaries.push(`patch: ${patchPart.path}`);
260
+ }
261
+ }
262
+ }
263
+ // Add to conversation digest
264
+ if (textParts.length > 0) {
265
+ const role = msg.role === 'user' ? 'User' : 'Assistant';
266
+ const text = textParts.join('\n');
267
+ // Truncate individual messages to keep digest manageable
268
+ const truncated = text.length > 500 ? text.slice(0, 500) + '...' : text;
269
+ digestParts.push(`[${role}]: ${truncated}`);
270
+ }
271
+ }
272
+ // Cap total digest at ~4000 chars (fits in an LLM context easily)
273
+ let conversationDigest = digestParts.join('\n\n');
274
+ if (conversationDigest.length > 4000) {
275
+ conversationDigest = conversationDigest.slice(0, 4000) + '\n\n[...truncated]';
276
+ }
277
+ return {
278
+ conversationDigest,
279
+ toolCallSummaries: toolSummaries,
280
+ filesTouched: [...files],
281
+ };
282
+ }
283
+ messageToTokens(msg) {
284
+ if (!msg.tokens)
285
+ return emptyTokenUsage();
286
+ const input = msg.tokens.input ?? 0;
287
+ const output = msg.tokens.output ?? 0;
288
+ const reasoning = msg.tokens.reasoning ?? 0;
289
+ const cacheRead = msg.tokens.cache?.read ?? 0;
290
+ const cacheWrite = msg.tokens.cache?.write ?? 0;
291
+ return {
292
+ input,
293
+ output,
294
+ reasoning,
295
+ cacheRead,
296
+ cacheWrite,
297
+ total: input + output + reasoning + cacheRead + cacheWrite,
298
+ };
299
+ }
300
+ }
301
+ //# sourceMappingURL=opencode.js.map