opencode-rewind 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.
- package/README.md +244 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/opencode.d.ts +178 -0
- package/dist/providers/opencode.d.ts.map +1 -0
- package/dist/providers/opencode.js +687 -0
- package/dist/providers/opencode.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
function defaultDataDir() {
|
|
11
|
+
const dataHome = process.env["XDG_DATA_HOME"] || join(homedir(), ".local", "share");
|
|
12
|
+
return join(dataHome, "opencode");
|
|
13
|
+
}
|
|
14
|
+
function msToIso(ms) {
|
|
15
|
+
return new Date(ms).toISOString();
|
|
16
|
+
}
|
|
17
|
+
/** Safely read and parse a JSON file. Returns null on any error. */
|
|
18
|
+
async function readJson(path) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readFile(/* turbopackIgnore: true */ path, "utf-8");
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Content conversion
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/** Compute duration from a start/end pair, returning undefined if not available. */
|
|
31
|
+
function partDurationMs(time) {
|
|
32
|
+
if (!time || time.end <= time.start)
|
|
33
|
+
return undefined;
|
|
34
|
+
return time.end - time.start;
|
|
35
|
+
}
|
|
36
|
+
function partsToContent(parts) {
|
|
37
|
+
const content = [];
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
switch (part.type) {
|
|
40
|
+
case "text":
|
|
41
|
+
if (part.text) {
|
|
42
|
+
content.push({ type: "text", text: part.text });
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
case "reasoning": {
|
|
46
|
+
const dur = partDurationMs(part.time);
|
|
47
|
+
content.push({
|
|
48
|
+
type: "thinking",
|
|
49
|
+
text: part.text,
|
|
50
|
+
...(dur != null ? { durationMs: dur } : {}),
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case "tool": {
|
|
55
|
+
// Tool parts represent both the call and result in one.
|
|
56
|
+
// We emit a tool_use and (if output exists) a tool_result.
|
|
57
|
+
// Task tools carry the subagent session ID in metadata.
|
|
58
|
+
const subagentSessionId = part.tool === "task"
|
|
59
|
+
? part.state.metadata?.sessionId
|
|
60
|
+
: undefined;
|
|
61
|
+
const toolDur = partDurationMs(part.state.time);
|
|
62
|
+
content.push({
|
|
63
|
+
type: "tool_use",
|
|
64
|
+
toolName: part.tool,
|
|
65
|
+
toolCallId: part.callID,
|
|
66
|
+
input: part.state.input,
|
|
67
|
+
...(subagentSessionId ? { subagentSessionId } : {}),
|
|
68
|
+
...(toolDur != null ? { durationMs: toolDur } : {}),
|
|
69
|
+
...(part.state.title ? { title: part.state.title } : {}),
|
|
70
|
+
});
|
|
71
|
+
if (part.state.output !== undefined) {
|
|
72
|
+
content.push({
|
|
73
|
+
type: "tool_result",
|
|
74
|
+
toolCallId: part.callID,
|
|
75
|
+
toolName: part.tool,
|
|
76
|
+
output: part.state.output ?? "",
|
|
77
|
+
isError: part.state.status === "error",
|
|
78
|
+
status: part.state.status,
|
|
79
|
+
...(toolDur != null ? { durationMs: toolDur } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "compaction":
|
|
85
|
+
content.push({
|
|
86
|
+
type: "compaction",
|
|
87
|
+
auto: part.auto,
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
case "patch":
|
|
91
|
+
content.push({
|
|
92
|
+
type: "patch",
|
|
93
|
+
hash: part.hash,
|
|
94
|
+
files: part.files,
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
// step-start, step-finish – internal bookkeeping, skip
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (content.length === 0) {
|
|
101
|
+
content.push({ type: "text", text: "" });
|
|
102
|
+
}
|
|
103
|
+
return content;
|
|
104
|
+
}
|
|
105
|
+
function messageTokenUsage(raw) {
|
|
106
|
+
if (!raw.tokens)
|
|
107
|
+
return undefined;
|
|
108
|
+
const t = raw.tokens;
|
|
109
|
+
const cacheRead = t.cache?.read ?? 0;
|
|
110
|
+
const cacheWrite = t.cache?.write ?? 0;
|
|
111
|
+
// inputTokens = total prompt tokens (non-cached + cache read + cache write)
|
|
112
|
+
const inputTokens = t.input + cacheRead + cacheWrite;
|
|
113
|
+
const outputTokens = t.output;
|
|
114
|
+
const reasoningTokens = t.reasoning || 0;
|
|
115
|
+
return {
|
|
116
|
+
inputTokens,
|
|
117
|
+
outputTokens,
|
|
118
|
+
reasoningTokens: reasoningTokens || undefined,
|
|
119
|
+
cacheReadTokens: cacheRead || undefined,
|
|
120
|
+
cacheCreationTokens: cacheWrite || undefined,
|
|
121
|
+
totalTokens: inputTokens + outputTokens + reasoningTokens,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Shared message conversion
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
function resolveModel(raw) {
|
|
128
|
+
if (raw.modelID)
|
|
129
|
+
return raw.modelID;
|
|
130
|
+
if (raw.model?.modelID)
|
|
131
|
+
return raw.model.modelID;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function rawErrorToMessageError(raw) {
|
|
135
|
+
if (!raw.error)
|
|
136
|
+
return undefined;
|
|
137
|
+
return {
|
|
138
|
+
name: raw.error.name,
|
|
139
|
+
message: raw.error.data?.message,
|
|
140
|
+
statusCode: raw.error.data?.statusCode,
|
|
141
|
+
isRetryable: raw.error.data?.isRetryable,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function getPartTime(part) {
|
|
145
|
+
if (part.type === "text" || part.type === "reasoning") {
|
|
146
|
+
return part.time?.start ?? null;
|
|
147
|
+
}
|
|
148
|
+
if (part.type === "tool") {
|
|
149
|
+
return part.state.time?.start ?? null;
|
|
150
|
+
}
|
|
151
|
+
// compaction, step-start, step-finish have no sortable time
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function sortParts(parts) {
|
|
155
|
+
parts.sort((a, b) => {
|
|
156
|
+
const aTime = getPartTime(a);
|
|
157
|
+
const bTime = getPartTime(b);
|
|
158
|
+
if (aTime && bTime)
|
|
159
|
+
return aTime - bTime;
|
|
160
|
+
return a.id.localeCompare(b.id);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async function rawToMessage(raw, loadPartsFn, isCompactSummary) {
|
|
164
|
+
const parts = raw._parts ?? await loadPartsFn(raw.id);
|
|
165
|
+
// Compute timing from time.completed (available on assistant messages)
|
|
166
|
+
const completedAt = raw.time.completed ? msToIso(raw.time.completed) : undefined;
|
|
167
|
+
const durationMs = raw.time.completed && raw.time.created
|
|
168
|
+
? raw.time.completed - raw.time.created
|
|
169
|
+
: undefined;
|
|
170
|
+
const msg = {
|
|
171
|
+
id: raw.id,
|
|
172
|
+
role: raw.role,
|
|
173
|
+
content: partsToContent(parts),
|
|
174
|
+
timestamp: msToIso(raw.time.created),
|
|
175
|
+
...(completedAt ? { completedAt } : {}),
|
|
176
|
+
...(durationMs != null && durationMs > 0 ? { durationMs } : {}),
|
|
177
|
+
model: resolveModel(raw),
|
|
178
|
+
usage: messageTokenUsage(raw),
|
|
179
|
+
parentId: raw.parentID,
|
|
180
|
+
...(isCompactSummary ? { isCompactSummary: true } : {}),
|
|
181
|
+
error: rawErrorToMessageError(raw),
|
|
182
|
+
raw: { ...raw, _parts: parts },
|
|
183
|
+
};
|
|
184
|
+
if (raw.finish)
|
|
185
|
+
msg.stopReason = raw.finish;
|
|
186
|
+
if (raw.mode)
|
|
187
|
+
msg.mode = raw.mode;
|
|
188
|
+
if (raw.cost != null)
|
|
189
|
+
msg.cost = raw.cost;
|
|
190
|
+
if (raw.providerID)
|
|
191
|
+
msg.apiProvider = raw.providerID;
|
|
192
|
+
if (raw.path?.cwd)
|
|
193
|
+
msg.cwd = raw.path.cwd;
|
|
194
|
+
if (raw.summary)
|
|
195
|
+
msg.summary = raw.summary;
|
|
196
|
+
return msg;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Convert a sorted array of raw messages, detecting compaction boundaries.
|
|
200
|
+
* When a user message contains a compaction part, the next assistant message
|
|
201
|
+
* is marked as a compact summary.
|
|
202
|
+
*/
|
|
203
|
+
async function convertMessages(rawMessages, loadPartsFn) {
|
|
204
|
+
const messages = [];
|
|
205
|
+
let nextIsSummary = false;
|
|
206
|
+
for (const raw of rawMessages) {
|
|
207
|
+
const parts = raw._parts ?? [];
|
|
208
|
+
const hasCompaction = parts.some((p) => p.type === "compaction");
|
|
209
|
+
if (hasCompaction) {
|
|
210
|
+
messages.push(await rawToMessage(raw, loadPartsFn));
|
|
211
|
+
nextIsSummary = true;
|
|
212
|
+
}
|
|
213
|
+
else if (nextIsSummary && raw.role === "assistant") {
|
|
214
|
+
messages.push(await rawToMessage(raw, loadPartsFn, true));
|
|
215
|
+
nextIsSummary = false;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
messages.push(await rawToMessage(raw, loadPartsFn));
|
|
219
|
+
nextIsSummary = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return messages;
|
|
223
|
+
}
|
|
224
|
+
function rawSessionToSummary(raw, projectWorktrees, messageCount, aggregatedUsage, aggregatedCost, extras) {
|
|
225
|
+
const worktree = projectWorktrees.get(raw.projectID);
|
|
226
|
+
return {
|
|
227
|
+
id: raw.id,
|
|
228
|
+
provider: "opencode",
|
|
229
|
+
title: raw.title ?? raw.slug,
|
|
230
|
+
slug: raw.slug,
|
|
231
|
+
createdAt: msToIso(raw.time.created),
|
|
232
|
+
updatedAt: msToIso(raw.time.updated),
|
|
233
|
+
messageCount,
|
|
234
|
+
projectPath: raw.directory ?? worktree,
|
|
235
|
+
usage: aggregatedUsage,
|
|
236
|
+
cost: aggregatedCost,
|
|
237
|
+
parentSessionId: raw.parentID,
|
|
238
|
+
cliVersion: raw.version,
|
|
239
|
+
primaryModel: extras?.primaryModel,
|
|
240
|
+
models: extras?.models,
|
|
241
|
+
toolCallCount: extras?.toolCallCount,
|
|
242
|
+
subagentCount: extras?.subagentCount,
|
|
243
|
+
compactionCount: extras?.compactionCount,
|
|
244
|
+
topTools: extras?.topTools,
|
|
245
|
+
codeChanges: raw.summary
|
|
246
|
+
? { additions: raw.summary.additions, deletions: raw.summary.deletions, files: raw.summary.files }
|
|
247
|
+
: undefined,
|
|
248
|
+
permissions: raw.permission?.length
|
|
249
|
+
? raw.permission.map((p) => ({ type: p.type, glob: p.glob, description: p.description }))
|
|
250
|
+
: undefined,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Aggregate token usage and cost from raw messages. */
|
|
254
|
+
function aggregateUsage(rawMessages) {
|
|
255
|
+
let tInput = 0;
|
|
256
|
+
let tOutput = 0;
|
|
257
|
+
let tReasoning = 0;
|
|
258
|
+
let tCacheRead = 0;
|
|
259
|
+
let tCacheWrite = 0;
|
|
260
|
+
let tCost = 0;
|
|
261
|
+
let pInput = 0;
|
|
262
|
+
let hasTokens = false;
|
|
263
|
+
let tDuration = 0;
|
|
264
|
+
let hasDuration = false;
|
|
265
|
+
for (const msg of rawMessages) {
|
|
266
|
+
if (msg.tokens) {
|
|
267
|
+
hasTokens = true;
|
|
268
|
+
const cr = msg.tokens.cache?.read || 0;
|
|
269
|
+
const cw = msg.tokens.cache?.write || 0;
|
|
270
|
+
const turnIn = (msg.tokens.input || 0) + cr + cw;
|
|
271
|
+
tInput += turnIn;
|
|
272
|
+
tOutput += msg.tokens.output || 0;
|
|
273
|
+
tReasoning += msg.tokens.reasoning || 0;
|
|
274
|
+
tCacheRead += cr;
|
|
275
|
+
tCacheWrite += cw;
|
|
276
|
+
if (turnIn > pInput)
|
|
277
|
+
pInput = turnIn;
|
|
278
|
+
}
|
|
279
|
+
if (msg.cost)
|
|
280
|
+
tCost += msg.cost;
|
|
281
|
+
if (msg.time.completed && msg.time.created) {
|
|
282
|
+
const dur = msg.time.completed - msg.time.created;
|
|
283
|
+
if (dur > 0) {
|
|
284
|
+
tDuration += dur;
|
|
285
|
+
hasDuration = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
usage: hasTokens
|
|
291
|
+
? {
|
|
292
|
+
inputTokens: tInput,
|
|
293
|
+
outputTokens: tOutput,
|
|
294
|
+
reasoningTokens: tReasoning || undefined,
|
|
295
|
+
cacheReadTokens: tCacheRead || undefined,
|
|
296
|
+
cacheCreationTokens: tCacheWrite || undefined,
|
|
297
|
+
totalTokens: tInput + tOutput + tReasoning,
|
|
298
|
+
peakInputTokens: pInput || undefined,
|
|
299
|
+
}
|
|
300
|
+
: undefined,
|
|
301
|
+
cost: tCost > 0 ? tCost : undefined,
|
|
302
|
+
totalDurationMs: hasDuration ? tDuration : undefined,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function aggregateSummaryExtras(rawMessages) {
|
|
306
|
+
const modelCounts = new Map();
|
|
307
|
+
const toolCounts = new Map();
|
|
308
|
+
let toolCallCount = 0;
|
|
309
|
+
let subagentCount = 0;
|
|
310
|
+
let compactionCount = 0;
|
|
311
|
+
for (const raw of rawMessages) {
|
|
312
|
+
const model = resolveModel(raw);
|
|
313
|
+
if (model) {
|
|
314
|
+
modelCounts.set(model, (modelCounts.get(model) ?? 0) + 1);
|
|
315
|
+
}
|
|
316
|
+
for (const part of raw._parts ?? []) {
|
|
317
|
+
if (part.type === "tool") {
|
|
318
|
+
toolCounts.set(part.tool, (toolCounts.get(part.tool) ?? 0) + 1);
|
|
319
|
+
const isSubagent = part.tool === "task" && Boolean(part.state.metadata?.sessionId);
|
|
320
|
+
if (isSubagent)
|
|
321
|
+
subagentCount++;
|
|
322
|
+
else
|
|
323
|
+
toolCallCount++;
|
|
324
|
+
}
|
|
325
|
+
if (part.type === "compaction")
|
|
326
|
+
compactionCount++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const models = Array.from(modelCounts.entries())
|
|
330
|
+
.sort((a, b) => b[1] - a[1])
|
|
331
|
+
.map(([model]) => model);
|
|
332
|
+
const topTools = Array.from(toolCounts.entries())
|
|
333
|
+
.sort((a, b) => b[1] - a[1])
|
|
334
|
+
.slice(0, 8)
|
|
335
|
+
.map(([name, count]) => ({ name, count }));
|
|
336
|
+
return {
|
|
337
|
+
primaryModel: models[0],
|
|
338
|
+
...(models.length > 0 ? { models } : {}),
|
|
339
|
+
...(toolCallCount > 0 ? { toolCallCount } : {}),
|
|
340
|
+
...(subagentCount > 0 ? { subagentCount } : {}),
|
|
341
|
+
...(compactionCount > 0 ? { compactionCount } : {}),
|
|
342
|
+
...(topTools.length > 0 ? { topTools } : {}),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function createSqliteBackend(dbPath) {
|
|
346
|
+
const Database = require("better-sqlite3");
|
|
347
|
+
const db = new Database(dbPath, { readonly: true });
|
|
348
|
+
// Prepare statements for reuse
|
|
349
|
+
const stmtProjects = db.prepare("SELECT id, worktree FROM project");
|
|
350
|
+
const stmtSessions = db.prepare(`
|
|
351
|
+
SELECT id, project_id, parent_id, slug, directory, title, version,
|
|
352
|
+
summary_additions, summary_deletions, summary_files,
|
|
353
|
+
permission, time_created, time_updated
|
|
354
|
+
FROM session
|
|
355
|
+
WHERE time_archived IS NULL
|
|
356
|
+
ORDER BY time_updated DESC
|
|
357
|
+
`);
|
|
358
|
+
const stmtMessages = db.prepare(`
|
|
359
|
+
SELECT id, session_id, time_created, time_updated, data
|
|
360
|
+
FROM message
|
|
361
|
+
WHERE session_id = ?
|
|
362
|
+
ORDER BY time_created ASC
|
|
363
|
+
`);
|
|
364
|
+
const stmtParts = db.prepare(`
|
|
365
|
+
SELECT id, message_id, session_id, time_created, time_updated, data
|
|
366
|
+
FROM part
|
|
367
|
+
WHERE message_id = ?
|
|
368
|
+
ORDER BY time_created ASC
|
|
369
|
+
`);
|
|
370
|
+
const stmtPartsBySession = db.prepare(`
|
|
371
|
+
SELECT id, message_id, session_id, time_created, time_updated, data
|
|
372
|
+
FROM part
|
|
373
|
+
WHERE session_id = ?
|
|
374
|
+
ORDER BY time_created ASC
|
|
375
|
+
`);
|
|
376
|
+
const stmtTodos = db.prepare(`
|
|
377
|
+
SELECT session_id, content, status, priority, position, time_created, time_updated
|
|
378
|
+
FROM todo
|
|
379
|
+
WHERE session_id = ?
|
|
380
|
+
ORDER BY position ASC
|
|
381
|
+
`);
|
|
382
|
+
function rowToSession(row) {
|
|
383
|
+
const session = {
|
|
384
|
+
id: row.id,
|
|
385
|
+
projectID: row.project_id,
|
|
386
|
+
slug: row.slug || undefined,
|
|
387
|
+
directory: row.directory || undefined,
|
|
388
|
+
parentID: row.parent_id || undefined,
|
|
389
|
+
title: row.title || undefined,
|
|
390
|
+
version: row.version || undefined,
|
|
391
|
+
time: {
|
|
392
|
+
created: row.time_created,
|
|
393
|
+
updated: row.time_updated,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
// Reconstruct summary if data exists
|
|
397
|
+
if (row.summary_additions != null || row.summary_deletions != null || row.summary_files != null) {
|
|
398
|
+
session.summary = {
|
|
399
|
+
additions: row.summary_additions ?? 0,
|
|
400
|
+
deletions: row.summary_deletions ?? 0,
|
|
401
|
+
files: row.summary_files ?? 0,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
// Parse permission JSON if present
|
|
405
|
+
if (row.permission) {
|
|
406
|
+
try {
|
|
407
|
+
session.permission = JSON.parse(row.permission);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// ignore malformed permission
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return session;
|
|
414
|
+
}
|
|
415
|
+
function rowToRawMessage(row) {
|
|
416
|
+
const id = row.id;
|
|
417
|
+
const sessionID = row.session_id;
|
|
418
|
+
const data = JSON.parse(row.data);
|
|
419
|
+
// Reconstruct OpenCodeRawMessage from the row + embedded data.
|
|
420
|
+
const msg = {
|
|
421
|
+
id,
|
|
422
|
+
sessionID,
|
|
423
|
+
role: data.role,
|
|
424
|
+
time: data.time,
|
|
425
|
+
parentID: data.parentID,
|
|
426
|
+
modelID: data.modelID,
|
|
427
|
+
providerID: data.providerID,
|
|
428
|
+
mode: data.mode,
|
|
429
|
+
agent: data.agent,
|
|
430
|
+
cost: data.cost,
|
|
431
|
+
tokens: data.tokens,
|
|
432
|
+
finish: data.finish,
|
|
433
|
+
model: data.model,
|
|
434
|
+
summary: data.summary,
|
|
435
|
+
path: data.path,
|
|
436
|
+
tools: data.tools,
|
|
437
|
+
error: data.error,
|
|
438
|
+
};
|
|
439
|
+
return msg;
|
|
440
|
+
}
|
|
441
|
+
function rowToPart(row) {
|
|
442
|
+
const id = row.id;
|
|
443
|
+
const messageID = row.message_id;
|
|
444
|
+
const sessionID = row.session_id;
|
|
445
|
+
const data = JSON.parse(row.data);
|
|
446
|
+
// Re-attach row metadata stripped from the JSON payload.
|
|
447
|
+
return { ...data, id, messageID, sessionID };
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
loadProjectWorktrees() {
|
|
451
|
+
const map = new Map();
|
|
452
|
+
const rows = stmtProjects.all();
|
|
453
|
+
for (const row of rows) {
|
|
454
|
+
map.set(row.id, row.worktree);
|
|
455
|
+
}
|
|
456
|
+
return map;
|
|
457
|
+
},
|
|
458
|
+
loadAllSessions() {
|
|
459
|
+
const rows = stmtSessions.all();
|
|
460
|
+
return rows.map(rowToSession);
|
|
461
|
+
},
|
|
462
|
+
loadRawMessages(sessionId) {
|
|
463
|
+
const rows = stmtMessages.all(sessionId);
|
|
464
|
+
return rows.map(rowToRawMessage);
|
|
465
|
+
},
|
|
466
|
+
loadParts(messageId) {
|
|
467
|
+
const rows = stmtParts.all(messageId);
|
|
468
|
+
const parts = rows.map(rowToPart);
|
|
469
|
+
sortParts(parts);
|
|
470
|
+
return parts;
|
|
471
|
+
},
|
|
472
|
+
loadPartsBySession(sessionId) {
|
|
473
|
+
const rows = stmtPartsBySession.all(sessionId);
|
|
474
|
+
const map = new Map();
|
|
475
|
+
for (const row of rows) {
|
|
476
|
+
const part = rowToPart(row);
|
|
477
|
+
const msgId = row.message_id;
|
|
478
|
+
let arr = map.get(msgId);
|
|
479
|
+
if (!arr) {
|
|
480
|
+
arr = [];
|
|
481
|
+
map.set(msgId, arr);
|
|
482
|
+
}
|
|
483
|
+
arr.push(part);
|
|
484
|
+
}
|
|
485
|
+
// Sort parts within each message
|
|
486
|
+
for (const parts of map.values()) {
|
|
487
|
+
sortParts(parts);
|
|
488
|
+
}
|
|
489
|
+
return map;
|
|
490
|
+
},
|
|
491
|
+
loadTodos(sessionId) {
|
|
492
|
+
const rows = stmtTodos.all(sessionId);
|
|
493
|
+
return rows.map((row) => ({
|
|
494
|
+
id: `todo-${row.session_id}-${row.position}`,
|
|
495
|
+
sessionID: row.session_id,
|
|
496
|
+
content: row.content,
|
|
497
|
+
status: row.status,
|
|
498
|
+
priority: row.priority,
|
|
499
|
+
time: {
|
|
500
|
+
created: row.time_created,
|
|
501
|
+
updated: row.time_updated,
|
|
502
|
+
},
|
|
503
|
+
}));
|
|
504
|
+
},
|
|
505
|
+
close() {
|
|
506
|
+
db.close();
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
export function createOpenCodeProvider(config = {}) {
|
|
511
|
+
// Resolve the base data directory and SQLite database path.
|
|
512
|
+
//
|
|
513
|
+
// Path resolution strategy:
|
|
514
|
+
// 1. No config → use default data dir (~/.local/share/opencode)
|
|
515
|
+
// 2. Config basePath provided:
|
|
516
|
+
// a. If <basePath>/opencode.db exists → basePath is the data dir
|
|
517
|
+
// b. If <basePath>/storage/ exists → basePath is the data dir
|
|
518
|
+
// c. Otherwise → treat basePath as the storage dir and use ../opencode.db
|
|
519
|
+
let dataDir;
|
|
520
|
+
let storageDir;
|
|
521
|
+
let dbPath;
|
|
522
|
+
if (config?.basePath) {
|
|
523
|
+
const bp = config.basePath;
|
|
524
|
+
if (existsSync(join(/* turbopackIgnore: true */ bp, "opencode.db"))) {
|
|
525
|
+
dataDir = bp;
|
|
526
|
+
storageDir = join(/* turbopackIgnore: true */ bp, "storage");
|
|
527
|
+
dbPath = join(/* turbopackIgnore: true */ bp, "opencode.db");
|
|
528
|
+
}
|
|
529
|
+
else if (existsSync(join(/* turbopackIgnore: true */ bp, "storage"))) {
|
|
530
|
+
dataDir = bp;
|
|
531
|
+
storageDir = join(/* turbopackIgnore: true */ bp, "storage");
|
|
532
|
+
dbPath = join(/* turbopackIgnore: true */ bp, "opencode.db");
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
dataDir = join(/* turbopackIgnore: true */ bp, "..");
|
|
536
|
+
storageDir = bp;
|
|
537
|
+
dbPath = join(/* turbopackIgnore: true */ dataDir, "opencode.db");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
dataDir = defaultDataDir();
|
|
542
|
+
storageDir = join(/* turbopackIgnore: true */ dataDir, "storage");
|
|
543
|
+
dbPath = join(/* turbopackIgnore: true */ dataDir, "opencode.db");
|
|
544
|
+
}
|
|
545
|
+
const hasSqlite = existsSync(/* turbopackIgnore: true */ dbPath);
|
|
546
|
+
const sessionDiffDir = join(/* turbopackIgnore: true */ storageDir, "session_diff");
|
|
547
|
+
// Lazily initialized SQLite backend
|
|
548
|
+
let _sqlite = null;
|
|
549
|
+
function getSqlite() {
|
|
550
|
+
if (!_sqlite) {
|
|
551
|
+
_sqlite = createSqliteBackend(dbPath);
|
|
552
|
+
}
|
|
553
|
+
return _sqlite;
|
|
554
|
+
}
|
|
555
|
+
// ---- Session diff helper ----
|
|
556
|
+
async function loadSessionDiffsFromStorage(sessionId) {
|
|
557
|
+
const filePath = join(sessionDiffDir, `${sessionId}.json`);
|
|
558
|
+
const data = await readJson(filePath);
|
|
559
|
+
if (!data || !Array.isArray(data))
|
|
560
|
+
return [];
|
|
561
|
+
return data
|
|
562
|
+
.map(({ path: _path, ...diff }) => ({ ...diff, file: diff.file ?? _path }))
|
|
563
|
+
.filter((diff) => Boolean(diff.file));
|
|
564
|
+
}
|
|
565
|
+
// ---- SQLite-backed data access ----
|
|
566
|
+
async function loadProjectWorktrees() {
|
|
567
|
+
return hasSqlite ? getSqlite().loadProjectWorktrees() : new Map();
|
|
568
|
+
}
|
|
569
|
+
async function loadAllSessions() {
|
|
570
|
+
return hasSqlite ? getSqlite().loadAllSessions() : [];
|
|
571
|
+
}
|
|
572
|
+
async function loadRawMessages(sessionId) {
|
|
573
|
+
return hasSqlite ? getSqlite().loadRawMessages(sessionId) : [];
|
|
574
|
+
}
|
|
575
|
+
async function loadParts(messageId) {
|
|
576
|
+
return hasSqlite ? getSqlite().loadParts(messageId) : [];
|
|
577
|
+
}
|
|
578
|
+
async function loadAndAttachParts(rawMessages, sessionId) {
|
|
579
|
+
if (!hasSqlite)
|
|
580
|
+
return;
|
|
581
|
+
const partsByMessage = getSqlite().loadPartsBySession(sessionId);
|
|
582
|
+
for (const msg of rawMessages) {
|
|
583
|
+
msg._parts = partsByMessage.get(msg.id) ?? [];
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function loadSessionDiffs(sessionId) {
|
|
587
|
+
return loadSessionDiffsFromStorage(sessionId);
|
|
588
|
+
}
|
|
589
|
+
async function loadSessionTodos(sessionId) {
|
|
590
|
+
return hasSqlite ? getSqlite().loadTodos(sessionId) : [];
|
|
591
|
+
}
|
|
592
|
+
// ---- Provider API ----
|
|
593
|
+
const reportedBasePath = dataDir;
|
|
594
|
+
return {
|
|
595
|
+
id: "opencode",
|
|
596
|
+
name: "OpenCode",
|
|
597
|
+
basePath: reportedBasePath,
|
|
598
|
+
async listSessions(options) {
|
|
599
|
+
const projectWorktrees = await loadProjectWorktrees();
|
|
600
|
+
let sessions = await loadAllSessions();
|
|
601
|
+
// Filter subagent sessions (excluded by default)
|
|
602
|
+
if (!options?.includeSubagents) {
|
|
603
|
+
sessions = sessions.filter((s) => !s.parentID);
|
|
604
|
+
}
|
|
605
|
+
// Apply filters
|
|
606
|
+
if (options?.projectPath) {
|
|
607
|
+
const pp = options.projectPath;
|
|
608
|
+
sessions = sessions.filter((s) => {
|
|
609
|
+
if (s.directory === pp)
|
|
610
|
+
return true;
|
|
611
|
+
// Also check project worktree
|
|
612
|
+
const worktree = projectWorktrees.get(s.projectID);
|
|
613
|
+
return worktree === pp;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
if (options?.after) {
|
|
617
|
+
const afterMs = new Date(options.after).getTime();
|
|
618
|
+
sessions = sessions.filter((s) => s.time.updated >= afterMs);
|
|
619
|
+
}
|
|
620
|
+
if (options?.before) {
|
|
621
|
+
const beforeMs = new Date(options.before).getTime();
|
|
622
|
+
sessions = sessions.filter((s) => s.time.updated <= beforeMs);
|
|
623
|
+
}
|
|
624
|
+
// Sort by updated DESC
|
|
625
|
+
sessions.sort((a, b) => b.time.updated - a.time.updated);
|
|
626
|
+
// Apply limit
|
|
627
|
+
if (options?.limit && options.limit > 0) {
|
|
628
|
+
sessions = sessions.slice(0, options.limit);
|
|
629
|
+
}
|
|
630
|
+
const results = [];
|
|
631
|
+
for (const raw of sessions) {
|
|
632
|
+
const rawMessages = await loadRawMessages(raw.id);
|
|
633
|
+
await loadAndAttachParts(rawMessages, raw.id);
|
|
634
|
+
const { usage, cost, totalDurationMs } = aggregateUsage(rawMessages);
|
|
635
|
+
const extras = aggregateSummaryExtras(rawMessages);
|
|
636
|
+
const summary = rawSessionToSummary(raw, projectWorktrees, rawMessages.length, usage, cost, extras);
|
|
637
|
+
if (totalDurationMs != null)
|
|
638
|
+
summary.totalDurationMs = totalDurationMs;
|
|
639
|
+
results.push(summary);
|
|
640
|
+
}
|
|
641
|
+
return results;
|
|
642
|
+
},
|
|
643
|
+
async getSession(sessionId) {
|
|
644
|
+
const projectWorktrees = await loadProjectWorktrees();
|
|
645
|
+
const allSessions = await loadAllSessions();
|
|
646
|
+
const rawSession = allSessions.find((s) => s.id === sessionId);
|
|
647
|
+
if (!rawSession)
|
|
648
|
+
return null;
|
|
649
|
+
const rawMessages = await loadRawMessages(sessionId);
|
|
650
|
+
await loadAndAttachParts(rawMessages, sessionId);
|
|
651
|
+
const messages = await convertMessages(rawMessages, loadParts);
|
|
652
|
+
const { usage, cost } = aggregateUsage(rawMessages);
|
|
653
|
+
const summary = rawSessionToSummary(rawSession, projectWorktrees, messages.length, usage, cost);
|
|
654
|
+
// Compute totalDurationMs from per-message timing
|
|
655
|
+
let totalDuration = 0;
|
|
656
|
+
let hasTiming = false;
|
|
657
|
+
for (const m of messages) {
|
|
658
|
+
if (m.durationMs != null) {
|
|
659
|
+
totalDuration += m.durationMs;
|
|
660
|
+
hasTiming = true;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Load session diffs and todos
|
|
664
|
+
const diffs = await loadSessionDiffs(sessionId);
|
|
665
|
+
const todos = await loadSessionTodos(sessionId);
|
|
666
|
+
return {
|
|
667
|
+
...summary,
|
|
668
|
+
...(hasTiming ? { totalDurationMs: totalDuration } : {}),
|
|
669
|
+
messages,
|
|
670
|
+
...(diffs.length > 0 ? { diffs } : {}),
|
|
671
|
+
...(todos.length > 0 ? { todos } : {}),
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
async getMessages(sessionId) {
|
|
675
|
+
const rawMessages = await loadRawMessages(sessionId);
|
|
676
|
+
await loadAndAttachParts(rawMessages, sessionId);
|
|
677
|
+
return await convertMessages(rawMessages, loadParts);
|
|
678
|
+
},
|
|
679
|
+
close() {
|
|
680
|
+
if (_sqlite) {
|
|
681
|
+
_sqlite.close();
|
|
682
|
+
_sqlite = null;
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
//# sourceMappingURL=opencode.js.map
|