spiracha 1.4.0 → 1.5.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 (159) hide show
  1. package/AGENTS.md +48 -5
  2. package/README.md +40 -5
  3. package/apps/ui/AGENTS.md +23 -4
  4. package/apps/ui/README.md +49 -2
  5. package/apps/ui/dist/client/assets/analytics-DOy1b6Sh.js +1 -0
  6. package/apps/ui/dist/client/assets/{antigravity-conversations._conversationId-DmD0sN8X.js → antigravity-conversations._conversationId-DqySVO2K.js} +2 -2
  7. package/apps/ui/dist/client/assets/antigravity-keychain-panel-B77qJz6f.js +1 -0
  8. package/apps/ui/dist/client/assets/antigravity._workspaceKey-B3NM6gRH.js +1 -0
  9. package/apps/ui/dist/client/assets/{antigravity.index-DEEkjlZa.js → antigravity.index-DP9JC2P9.js} +1 -1
  10. package/apps/ui/dist/client/assets/{badge-DfWvPfKF.js → badge-C3i4OKs8.js} +1 -1
  11. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-8pQ55ci2.js +1 -0
  12. package/apps/ui/dist/client/assets/claude-code-sessions._sessionId-DVz5UmtU.js +1 -0
  13. package/apps/ui/dist/client/assets/claude-code._workspaceKey-BBKskSum.js +1 -0
  14. package/apps/ui/dist/client/assets/claude-code._workspaceKey-OkikMUFs.js +1 -0
  15. package/apps/ui/dist/client/assets/claude-code.index-Cwu76dbb.js +1 -0
  16. package/apps/ui/dist/client/assets/claude-code.index-D6eMd08t.js +1 -0
  17. package/apps/ui/dist/client/assets/createServerFn-B-KkwmHU.js +3 -0
  18. package/apps/ui/dist/client/assets/cursor-threads._composerId-hBWlP1D-.js +1 -0
  19. package/apps/ui/dist/client/assets/cursor._workspaceKey-BFPkIRi-.js +1 -0
  20. package/apps/ui/dist/client/assets/cursor.index-8y4o20kb.js +2 -0
  21. package/apps/ui/dist/client/assets/delete-confirm-dialog-Bxs07u-j.js +7 -0
  22. package/apps/ui/dist/client/assets/dist-BrJhN2A0.js +5 -0
  23. package/apps/ui/dist/client/assets/{dist-BhZOxAPP.js → dist-Cmd3AIfD.js} +1 -1
  24. package/apps/ui/dist/client/assets/dropdown-menu-D5cvPaZt.js +1 -0
  25. package/apps/ui/dist/client/assets/{es2015-BkCWttyM.js → es2015-ad6040dw.js} +1 -1
  26. package/apps/ui/dist/client/assets/export-dialog-Dr8d2MqD.js +1 -0
  27. package/apps/ui/dist/client/assets/{formatters-BM9kB7ed.js → formatters-BtqyrX_c.js} +1 -1
  28. package/apps/ui/dist/client/assets/index-C-STgIH4.js +166 -0
  29. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-CKgfJ7GR.js +1 -0
  30. package/apps/ui/dist/client/assets/kiro-sessions._sessionId-zvIJHtQF.js +1 -0
  31. package/apps/ui/dist/client/assets/kiro._workspaceKey-DhVuJuZB.js +1 -0
  32. package/apps/ui/dist/client/assets/kiro._workspaceKey-QArsjkTY.js +1 -0
  33. package/apps/ui/dist/client/assets/kiro.index-C-fr4m7i.js +1 -0
  34. package/apps/ui/dist/client/assets/kiro.index-OYgAdtZd.js +1 -0
  35. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-DLjiTst-.js +1 -0
  36. package/apps/ui/dist/client/assets/opencode-sessions._sessionId-biBwrRbt.js +8 -0
  37. package/apps/ui/dist/client/assets/opencode._workspaceKey-C7OIzMOJ.js +1 -0
  38. package/apps/ui/dist/client/assets/opencode._workspaceKey-sBakX0oI.js +1 -0
  39. package/apps/ui/dist/client/assets/opencode.index-Bl_kNs9N.js +1 -0
  40. package/apps/ui/dist/client/assets/opencode.index-CN53bVmb.js +1 -0
  41. package/apps/ui/dist/client/assets/preload-helper-B8fFrdxC.js +1 -0
  42. package/apps/ui/dist/client/assets/projects._project-D8WloxQN.js +1 -0
  43. package/apps/ui/dist/client/assets/projects.index-5xRtuJdX.js +3 -0
  44. package/apps/ui/dist/client/assets/routes-DnzF_9CX.js +1 -0
  45. package/apps/ui/dist/client/assets/select-CYF6pF37.js +1 -0
  46. package/apps/ui/dist/client/assets/settings-Dx70jgP_.js +1 -0
  47. package/apps/ui/dist/client/assets/styles-B7Zi9EFA.css +1 -0
  48. package/apps/ui/dist/client/assets/tabs-CmuhkT-c.js +7 -0
  49. package/apps/ui/dist/client/assets/thread-transcript-stats-BUNz6u7F.js +1 -0
  50. package/apps/ui/dist/client/assets/threads._threadId-BMRSFow3.js +1 -0
  51. package/apps/ui/dist/client/assets/useMutation-BSFult2i.js +1 -0
  52. package/apps/ui/dist/client/assets/useQuery-CjINKZf1.js +1 -0
  53. package/apps/ui/dist/client/icon.svg +2 -1
  54. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CQ8Jjs3B.js +387 -0
  55. package/apps/ui/dist/server/assets/{antigravity-conversations._conversationId-BtexWY-K.js → antigravity-conversations._conversationId-BKOyL2T6.js} +2 -2
  56. package/apps/ui/dist/server/assets/{antigravity-conversations._conversationId-zMrR6v6R.js → antigravity-conversations._conversationId-eS87Dx7g.js} +1 -1
  57. package/apps/ui/dist/server/assets/claude-code-db-Ik3VStKZ.js +361 -0
  58. package/apps/ui/dist/server/assets/claude-code-queries-Cgzi4K-e.js +37 -0
  59. package/apps/ui/dist/server/assets/claude-code-server-PfraClXS.js +72 -0
  60. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-C55oe6zj.js +18 -0
  61. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-D1RYYi3j.js +473 -0
  62. package/apps/ui/dist/server/assets/claude-code-sessions._sessionId-DSvPLEUL.js +11 -0
  63. package/apps/ui/dist/server/assets/claude-code-transcript-C-bxh3cS.js +139 -0
  64. package/apps/ui/dist/server/assets/claude-code-transcript-phase-DmUMc8VU.js +10 -0
  65. package/apps/ui/dist/server/assets/claude-code._workspaceKey-B5Xg_-tE.js +28 -0
  66. package/apps/ui/dist/server/assets/claude-code._workspaceKey-BPlzyXPa.js +11 -0
  67. package/apps/ui/dist/server/assets/claude-code._workspaceKey-DqDve2wl.js +191 -0
  68. package/apps/ui/dist/server/assets/claude-code.index-B7yYR7dD.js +92 -0
  69. package/apps/ui/dist/server/assets/claude-code.index-BUHThG1W.js +11 -0
  70. package/apps/ui/dist/server/assets/{codex-server-CNXSJuc2.js → codex-server-CGXU4H8-.js} +319 -29
  71. package/apps/ui/dist/server/assets/{cursor-db-cYZEU3WQ.js → cursor-db-CZnYy7aT.js} +3 -3
  72. package/apps/ui/dist/server/assets/{cursor-recovery-nq-kR62j.js → cursor-recovery-CXpO8KcD.js} +2 -2
  73. package/apps/ui/dist/server/assets/{cursor-server-C3q7hrp-.js → cursor-server-DNu52A3K.js} +13 -13
  74. package/apps/ui/dist/server/assets/{cursor-threads._composerId-BkMMTQ3v.js → cursor-threads._composerId-Ckn6ItHI.js} +3 -3
  75. package/apps/ui/dist/server/assets/{cursor-threads._composerId-BPJFWfJj.js → cursor-threads._composerId-p9-ARAzc.js} +1 -1
  76. package/apps/ui/dist/server/assets/{cursor-transcript-2iL3KFSK.js → cursor-transcript-B7tGnqtn.js} +1 -1
  77. package/apps/ui/dist/server/assets/{cursor._workspaceKey-C5PdTaKB.js → cursor._workspaceKey-CZuohIUL.js} +1 -1
  78. package/apps/ui/dist/server/assets/{cursor._workspaceKey-Ckj7apKI.js → cursor._workspaceKey-DPp5JSjy.js} +2 -2
  79. package/apps/ui/dist/server/assets/{export-dialog-BItjWgkZ.js → export-dialog-D88ze9Gy.js} +2 -2
  80. package/apps/ui/dist/server/assets/kiro-db--fXrED_-.js +552 -0
  81. package/apps/ui/dist/server/assets/kiro-queries-CzNlM2ok.js +37 -0
  82. package/apps/ui/dist/server/assets/kiro-server-9WRxg2Au.js +72 -0
  83. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-CRY2c7YO.js +11 -0
  84. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-F1UlwxmR.js +449 -0
  85. package/apps/ui/dist/server/assets/kiro-sessions._sessionId-bvTulT7C.js +18 -0
  86. package/apps/ui/dist/server/assets/kiro-transcript-DoekQBaH.js +112 -0
  87. package/apps/ui/dist/server/assets/kiro-transcript-phase-CJG40nQg.js +27 -0
  88. package/apps/ui/dist/server/assets/kiro._workspaceKey-BDe9sQwo.js +11 -0
  89. package/apps/ui/dist/server/assets/kiro._workspaceKey-CJsjgNwO.js +28 -0
  90. package/apps/ui/dist/server/assets/kiro._workspaceKey-D_-4Qb7y.js +193 -0
  91. package/apps/ui/dist/server/assets/kiro.index-C__-UROk.js +11 -0
  92. package/apps/ui/dist/server/assets/kiro.index-CmTbqeYj.js +93 -0
  93. package/apps/ui/dist/server/assets/opencode-db-BM7KjOzc.js +397 -0
  94. package/apps/ui/dist/server/assets/opencode-queries-BeNYXnQv.js +50 -0
  95. package/apps/ui/dist/server/assets/opencode-server-BpQ_kRze.js +72 -0
  96. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-CGQ-t0vD.js +518 -0
  97. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-D8Q7in_Z.js +18 -0
  98. package/apps/ui/dist/server/assets/opencode-sessions._sessionId-DKOvi0lV.js +11 -0
  99. package/apps/ui/dist/server/assets/opencode-think-tags-CAoD-EcZ.js +87 -0
  100. package/apps/ui/dist/server/assets/opencode-transcript-BNTcGNym.js +141 -0
  101. package/apps/ui/dist/server/assets/opencode-transcript-phase-OqLkiQD0.js +32 -0
  102. package/apps/ui/dist/server/assets/opencode._workspaceKey-BdqcWHw1.js +11 -0
  103. package/apps/ui/dist/server/assets/opencode._workspaceKey-CfH96HTT.js +28 -0
  104. package/apps/ui/dist/server/assets/opencode._workspaceKey-DdbHPDrO.js +207 -0
  105. package/apps/ui/dist/server/assets/opencode.index-BbVzkJXP.js +11 -0
  106. package/apps/ui/dist/server/assets/opencode.index-Vh_Gq_1L.js +93 -0
  107. package/apps/ui/dist/server/assets/{projects._project-Bpbc3C-L.js → projects._project-4io5LO0E.js} +1 -1
  108. package/apps/ui/dist/server/assets/{projects._project-CvZho6EQ.js → projects._project-D6ZoGhJ8.js} +2 -2
  109. package/apps/ui/dist/server/assets/{router-6m-ihwqA.js → router-Sac2DGk6.js} +157 -49
  110. package/apps/ui/dist/server/assets/{shared-CPRNYIql.js → shared-DyhChtHf.js} +4 -1
  111. package/apps/ui/dist/server/assets/source-session-export-server-DNutCawb.js +38 -0
  112. package/apps/ui/dist/server/assets/{start-CI_0bSiY.js → start-B_HFwkkk.js} +1 -1
  113. package/apps/ui/dist/server/assets/{tabs-CGA13IZM.js → tabs-DkaChNVg.js} +0 -1
  114. package/apps/ui/dist/server/assets/thread-transcript-stats-Dh34mt2u.js +45 -0
  115. package/apps/ui/dist/server/assets/{threads._threadId-D2GYkVn6.js → threads._threadId-BeXEc4Lo.js} +15 -6
  116. package/apps/ui/dist/server/assets/{threads._threadId-Bs0yuqO9.js → threads._threadId-Ci0S4mKU.js} +1 -1
  117. package/apps/ui/dist/server/server.js +72 -24
  118. package/package.json +21 -4
  119. package/src/lib/claude-code-db.ts +655 -0
  120. package/src/lib/claude-code-exporter-types.ts +110 -0
  121. package/src/lib/claude-code-transcript-phase.ts +25 -0
  122. package/src/lib/claude-code-transcript.ts +180 -0
  123. package/src/lib/codex-browser-db.ts +490 -31
  124. package/src/lib/codex-browser-export.ts +18 -2
  125. package/src/lib/codex-thread-cache.ts +17 -1
  126. package/src/lib/concurrency.ts +25 -0
  127. package/src/lib/kiro-db.ts +949 -0
  128. package/src/lib/kiro-exporter-types.ts +100 -0
  129. package/src/lib/kiro-transcript-phase.ts +41 -0
  130. package/src/lib/kiro-transcript.ts +115 -0
  131. package/src/lib/opencode-db.ts +610 -0
  132. package/src/lib/opencode-exporter-types.ts +129 -0
  133. package/src/lib/opencode-think-tags.ts +126 -0
  134. package/src/lib/opencode-transcript-phase.ts +50 -0
  135. package/src/lib/opencode-transcript.ts +194 -0
  136. package/src/lib/shared.ts +25 -0
  137. package/src/lib/ui-export-archive.ts +61 -0
  138. package/apps/ui/dist/client/assets/analytics-DK2jdvmI.js +0 -1
  139. package/apps/ui/dist/client/assets/antigravity-keychain-panel-BRHVjFAM.js +0 -1
  140. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CGV7amas.js +0 -1
  141. package/apps/ui/dist/client/assets/createServerFn-CgRRVpBH.js +0 -3
  142. package/apps/ui/dist/client/assets/cursor-threads._composerId-Ti2gTm63.js +0 -1
  143. package/apps/ui/dist/client/assets/cursor._workspaceKey-DzD_c6j9.js +0 -1
  144. package/apps/ui/dist/client/assets/cursor.index-PkCFVcl4.js +0 -2
  145. package/apps/ui/dist/client/assets/delete-confirm-dialog-BHRmyLHr.js +0 -11
  146. package/apps/ui/dist/client/assets/dropdown-menu-Dy5cLzTy.js +0 -1
  147. package/apps/ui/dist/client/assets/export-dialog-C86SYHJn.js +0 -1
  148. package/apps/ui/dist/client/assets/index-CPvAP-jk.js +0 -149
  149. package/apps/ui/dist/client/assets/projects._project-Ckf6muWZ.js +0 -1
  150. package/apps/ui/dist/client/assets/projects.index-V5ibNZII.js +0 -3
  151. package/apps/ui/dist/client/assets/routes-Df2tgpyd.js +0 -1
  152. package/apps/ui/dist/client/assets/select-Bf2GwfPR.js +0 -1
  153. package/apps/ui/dist/client/assets/settings-5VQIzME0.js +0 -1
  154. package/apps/ui/dist/client/assets/styles-BhRkXgwB.css +0 -1
  155. package/apps/ui/dist/client/assets/tabs-OO4VU5KR.js +0 -7
  156. package/apps/ui/dist/client/assets/threads._threadId-BgDhFj2I.js +0 -1
  157. package/apps/ui/dist/client/assets/useMutation-CC_B7uy5.js +0 -1
  158. package/apps/ui/dist/client/assets/useQuery-C5By6WKU.js +0 -1
  159. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CRW2kqj0.js +0 -232
@@ -0,0 +1,610 @@
1
+ import { constants, Database } from 'bun:sqlite';
2
+ import { stat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import {
6
+ getDefaultOpenCodeDataDir,
7
+ type OpenCodeModelInfo,
8
+ type OpenCodePartType,
9
+ type OpenCodeSessionSummary,
10
+ type OpenCodeSessionTranscript,
11
+ type OpenCodeStepTokens,
12
+ type OpenCodeTranscriptMessage,
13
+ type OpenCodeTranscriptPart,
14
+ type OpenCodeWorkspaceGroup,
15
+ resolveOpenCodeDbPath,
16
+ } from './opencode-exporter-types';
17
+ import { splitOpenCodeThinkTaggedText } from './opencode-think-tags';
18
+ import {
19
+ asNumber,
20
+ asObject,
21
+ asString,
22
+ isWorkspacePathQuery,
23
+ type JsonValue,
24
+ workspacePathMatchesQuery,
25
+ } from './shared';
26
+
27
+ export { getDefaultOpenCodeDataDir, resolveOpenCodeDbPath };
28
+
29
+ export const OPENCODE_READONLY_DB_OPEN_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
30
+
31
+ type WorkspaceRow = {
32
+ archivedSessionCount: number;
33
+ lastActiveMs: number;
34
+ messageCount: number;
35
+ name: string | null;
36
+ partCount: number;
37
+ projectId: string;
38
+ sessionCount: number;
39
+ worktree: string;
40
+ };
41
+
42
+ type SessionRow = {
43
+ agent: string | null;
44
+ archivedAtMs: number | null;
45
+ cost: number;
46
+ createdAtMs: number;
47
+ directory: string;
48
+ lastUpdatedAtMs: number;
49
+ messageCount: number;
50
+ model: string | null;
51
+ partCount: number;
52
+ path: string | null;
53
+ permission: string | null;
54
+ projectId: string;
55
+ projectName: string | null;
56
+ renderablePartCount: number;
57
+ sessionId: string;
58
+ slug: string;
59
+ summaryAdditions: number | null;
60
+ summaryDeletions: number | null;
61
+ summaryFiles: number | null;
62
+ textPartCount: number;
63
+ title: string;
64
+ tokensCacheRead: number;
65
+ tokensCacheWrite: number;
66
+ tokensInput: number;
67
+ tokensOutput: number;
68
+ tokensReasoning: number;
69
+ toolPartCount: number;
70
+ worktree: string;
71
+ };
72
+
73
+ type MessageRow = {
74
+ data: string;
75
+ messageId: string;
76
+ timeCreated: number;
77
+ timeUpdated: number;
78
+ };
79
+
80
+ type PartRow = {
81
+ data: string;
82
+ messageId: string;
83
+ partId: string;
84
+ timeCreated: number;
85
+ timeUpdated: number;
86
+ };
87
+
88
+ const MAIN_SESSION_FILTER = 's.parent_id IS NULL';
89
+
90
+ const pathExists = async (target: string): Promise<boolean> => {
91
+ return await stat(target)
92
+ .then(() => true)
93
+ .catch(() => false);
94
+ };
95
+
96
+ export const getOpenCodeReadonlyDbUri = (dbPath: string): string => {
97
+ const url = pathToFileURL(dbPath);
98
+ url.searchParams.set('mode', 'ro');
99
+ return url.href;
100
+ };
101
+
102
+ export const openOpenCodeReadonlyDb = (dbPath: string): Database => {
103
+ return new Database(getOpenCodeReadonlyDbUri(dbPath), OPENCODE_READONLY_DB_OPEN_FLAGS);
104
+ };
105
+
106
+ const parseJsonValue = (value: string | null): JsonValue | string | null => {
107
+ if (!value) {
108
+ return null;
109
+ }
110
+
111
+ try {
112
+ return JSON.parse(value) as JsonValue;
113
+ } catch {
114
+ return value;
115
+ }
116
+ };
117
+
118
+ const parseJsonObject = (value: string): Record<string, JsonValue> => {
119
+ const parsed = parseJsonValue(value);
120
+ return asObject(parsed) ?? {};
121
+ };
122
+
123
+ const cleanLabel = (value: string | null | undefined): string | null => {
124
+ const cleaned = value?.replace(/\s+/g, ' ').trim();
125
+ return cleaned ? cleaned : null;
126
+ };
127
+
128
+ const getWorkspaceLabel = (name: string | null, worktree: string): string => {
129
+ const named = cleanLabel(name);
130
+ if (named) {
131
+ return named;
132
+ }
133
+
134
+ if (worktree === '/') {
135
+ return '(global)';
136
+ }
137
+
138
+ return path.basename(worktree.replace(/\/+$/u, '')) || worktree;
139
+ };
140
+
141
+ const getWorkspaceKey = (projectId: string): string => `project:${projectId}`;
142
+
143
+ const getProjectIdFromWorkspaceKey = (workspaceKey: string): string | null => {
144
+ return workspaceKey.startsWith('project:') ? workspaceKey.slice('project:'.length) : null;
145
+ };
146
+
147
+ const parseModelInfo = (value: string | null): OpenCodeModelInfo => {
148
+ const parsed = parseJsonValue(value);
149
+ if (typeof parsed === 'string') {
150
+ return {
151
+ id: parsed,
152
+ providerID: null,
153
+ raw: parsed,
154
+ variant: null,
155
+ };
156
+ }
157
+
158
+ const model = asObject(parsed);
159
+ return {
160
+ id: asString(model?.id ?? null),
161
+ providerID: asString(model?.providerID ?? null),
162
+ raw: parsed,
163
+ variant: asString(model?.variant ?? null),
164
+ };
165
+ };
166
+
167
+ const formatModelLabel = (model: OpenCodeModelInfo): string | null => {
168
+ if (typeof model.raw === 'string') {
169
+ return cleanLabel(model.raw);
170
+ }
171
+
172
+ const id = cleanLabel(model.id);
173
+ const variant = cleanLabel(model.variant);
174
+ if (!id) {
175
+ return null;
176
+ }
177
+
178
+ return variant ? `${id} ${variant}` : id;
179
+ };
180
+
181
+ const toWorkspaceGroup = (row: WorkspaceRow): OpenCodeWorkspaceGroup => {
182
+ const label = getWorkspaceLabel(row.name, row.worktree);
183
+ return {
184
+ archivedSessionCount: row.archivedSessionCount,
185
+ key: getWorkspaceKey(row.projectId),
186
+ label,
187
+ lastActiveMs: row.lastActiveMs,
188
+ messageCount: row.messageCount,
189
+ partCount: row.partCount,
190
+ projectId: row.projectId,
191
+ sessionCount: row.sessionCount,
192
+ uri: pathToFileURL(row.worktree).href,
193
+ worktree: row.worktree,
194
+ };
195
+ };
196
+
197
+ const toSessionSummary = (row: SessionRow): OpenCodeSessionSummary => {
198
+ const label = getWorkspaceLabel(row.projectName, row.worktree);
199
+ const model = parseModelInfo(row.model);
200
+ return {
201
+ agent: row.agent,
202
+ archivedAtMs: row.archivedAtMs,
203
+ cost: row.cost,
204
+ createdAtMs: row.createdAtMs,
205
+ directory: row.directory,
206
+ lastUpdatedAtMs: row.lastUpdatedAtMs,
207
+ messageCount: row.messageCount,
208
+ model,
209
+ modelLabel: formatModelLabel(model),
210
+ partCount: row.partCount,
211
+ path: row.path,
212
+ permission: row.permission,
213
+ projectId: row.projectId,
214
+ renderablePartCount: row.renderablePartCount,
215
+ sessionId: row.sessionId,
216
+ slug: row.slug,
217
+ summaryAdditions: row.summaryAdditions,
218
+ summaryDeletions: row.summaryDeletions,
219
+ summaryFiles: row.summaryFiles,
220
+ textPartCount: row.textPartCount,
221
+ title: cleanLabel(row.title) ?? row.sessionId,
222
+ tokensCacheRead: row.tokensCacheRead,
223
+ tokensCacheWrite: row.tokensCacheWrite,
224
+ tokensInput: row.tokensInput,
225
+ tokensOutput: row.tokensOutput,
226
+ tokensReasoning: row.tokensReasoning,
227
+ toolPartCount: row.toolPartCount,
228
+ totalTokens:
229
+ row.tokensInput + row.tokensOutput + row.tokensReasoning + row.tokensCacheRead + row.tokensCacheWrite,
230
+ workspaceKey: getWorkspaceKey(row.projectId),
231
+ workspaceLabel: label,
232
+ worktree: row.worktree,
233
+ };
234
+ };
235
+
236
+ const workspaceRowsQuery = `
237
+ SELECT
238
+ p.id AS projectId,
239
+ p.name AS name,
240
+ p.worktree AS worktree,
241
+ (
242
+ SELECT COUNT(*)
243
+ FROM session s
244
+ WHERE s.project_id = p.id AND ${MAIN_SESSION_FILTER}
245
+ ) AS sessionCount,
246
+ (
247
+ SELECT COUNT(*)
248
+ FROM session s
249
+ WHERE s.project_id = p.id AND ${MAIN_SESSION_FILTER} AND s.time_archived IS NOT NULL
250
+ ) AS archivedSessionCount,
251
+ (
252
+ SELECT COUNT(*)
253
+ FROM message m
254
+ JOIN session s ON s.id = m.session_id
255
+ WHERE s.project_id = p.id AND ${MAIN_SESSION_FILTER}
256
+ ) AS messageCount,
257
+ (
258
+ SELECT COUNT(*)
259
+ FROM part prt
260
+ JOIN session s ON s.id = prt.session_id
261
+ WHERE s.project_id = p.id AND ${MAIN_SESSION_FILTER}
262
+ ) AS partCount,
263
+ COALESCE((
264
+ SELECT MAX(s.time_updated)
265
+ FROM session s
266
+ WHERE s.project_id = p.id AND ${MAIN_SESSION_FILTER}
267
+ ), p.time_updated) AS lastActiveMs
268
+ FROM project p
269
+ `;
270
+
271
+ const sessionSelectQuery = `
272
+ SELECT
273
+ s.id AS sessionId,
274
+ s.project_id AS projectId,
275
+ p.name AS projectName,
276
+ p.worktree AS worktree,
277
+ s.slug AS slug,
278
+ s.directory AS directory,
279
+ s.title AS title,
280
+ s.permission AS permission,
281
+ s.time_created AS createdAtMs,
282
+ s.time_updated AS lastUpdatedAtMs,
283
+ s.time_archived AS archivedAtMs,
284
+ s.path AS path,
285
+ s.agent AS agent,
286
+ s.model AS model,
287
+ s.cost AS cost,
288
+ s.tokens_input AS tokensInput,
289
+ s.tokens_output AS tokensOutput,
290
+ s.tokens_reasoning AS tokensReasoning,
291
+ s.tokens_cache_read AS tokensCacheRead,
292
+ s.tokens_cache_write AS tokensCacheWrite,
293
+ s.summary_files AS summaryFiles,
294
+ s.summary_additions AS summaryAdditions,
295
+ s.summary_deletions AS summaryDeletions,
296
+ (
297
+ SELECT COUNT(*)
298
+ FROM message m
299
+ WHERE m.session_id = s.id
300
+ ) AS messageCount,
301
+ (
302
+ SELECT COUNT(*)
303
+ FROM part prt
304
+ WHERE prt.session_id = s.id
305
+ ) AS partCount,
306
+ (
307
+ SELECT COUNT(*)
308
+ FROM part prt
309
+ WHERE prt.session_id = s.id AND json_extract(prt.data, '$.type') = 'text'
310
+ ) AS textPartCount,
311
+ (
312
+ SELECT COUNT(*)
313
+ FROM part prt
314
+ WHERE prt.session_id = s.id AND json_extract(prt.data, '$.type') = 'tool'
315
+ ) AS toolPartCount,
316
+ (
317
+ SELECT COUNT(*)
318
+ FROM part prt
319
+ WHERE prt.session_id = s.id
320
+ AND (
321
+ (
322
+ json_extract(prt.data, '$.type') IN ('text', 'reasoning')
323
+ AND trim(COALESCE(json_extract(prt.data, '$.text'), '')) <> ''
324
+ )
325
+ OR json_extract(prt.data, '$.type') = 'tool'
326
+ )
327
+ ) AS renderablePartCount
328
+ FROM session s
329
+ JOIN project p ON p.id = s.project_id
330
+ `;
331
+
332
+ export const listOpenCodeWorkspaceGroups = async (
333
+ dbPath = resolveOpenCodeDbPath(),
334
+ ): Promise<OpenCodeWorkspaceGroup[]> => {
335
+ if (!(await pathExists(dbPath))) {
336
+ return [];
337
+ }
338
+
339
+ const db = openOpenCodeReadonlyDb(dbPath);
340
+ try {
341
+ const rows = db
342
+ .query(`${workspaceRowsQuery} ORDER BY lastActiveMs DESC, p.worktree ASC`)
343
+ .all() as WorkspaceRow[];
344
+ return rows.map(toWorkspaceGroup);
345
+ } finally {
346
+ db.close();
347
+ }
348
+ };
349
+
350
+ const openCodeWorkspaceMatchesQuery = (workspace: OpenCodeWorkspaceGroup, query: string): boolean => {
351
+ const raw = query.trim();
352
+ if (!raw) {
353
+ return true;
354
+ }
355
+
356
+ const lowered = raw.toLowerCase();
357
+ if (
358
+ workspace.key.toLowerCase() === lowered ||
359
+ workspace.projectId.toLowerCase() === lowered ||
360
+ workspace.label.toLowerCase() === lowered
361
+ ) {
362
+ return true;
363
+ }
364
+
365
+ if (isWorkspacePathQuery(raw)) {
366
+ return workspacePathMatchesQuery(workspace.worktree, raw);
367
+ }
368
+
369
+ return path.basename(workspace.worktree).toLowerCase() === lowered;
370
+ };
371
+
372
+ export const findOpenCodeWorkspaceGroups = (
373
+ groups: OpenCodeWorkspaceGroup[],
374
+ query: string,
375
+ ): OpenCodeWorkspaceGroup[] => {
376
+ return groups.filter((group) => openCodeWorkspaceMatchesQuery(group, query));
377
+ };
378
+
379
+ const readSessionSummaries = (db: Database, whereSql: string, params: string[]): OpenCodeSessionSummary[] => {
380
+ const rows = db
381
+ .query(`${sessionSelectQuery} WHERE ${whereSql} ORDER BY s.time_updated DESC, s.title ASC`)
382
+ .all(...params) as SessionRow[];
383
+ return rows.map(toSessionSummary);
384
+ };
385
+
386
+ export const listOpenCodeSessionsForGroup = async (
387
+ workspaceKey: string,
388
+ dbPath = resolveOpenCodeDbPath(),
389
+ ): Promise<OpenCodeSessionSummary[]> => {
390
+ const projectId = getProjectIdFromWorkspaceKey(workspaceKey);
391
+ if (!projectId || !(await pathExists(dbPath))) {
392
+ return [];
393
+ }
394
+
395
+ const db = openOpenCodeReadonlyDb(dbPath);
396
+ try {
397
+ return readSessionSummaries(db, `s.project_id = ? AND ${MAIN_SESSION_FILTER}`, [projectId]);
398
+ } finally {
399
+ db.close();
400
+ }
401
+ };
402
+
403
+ const readOpenCodeSessionSummary = (db: Database, sessionId: string): OpenCodeSessionSummary | null => {
404
+ return readSessionSummaries(db, `s.id = ? AND ${MAIN_SESSION_FILTER}`, [sessionId])[0] ?? null;
405
+ };
406
+
407
+ const getMessageRole = (raw: Record<string, JsonValue>): string => asString(raw.role ?? null) ?? 'unknown';
408
+
409
+ const toPartType = (value: JsonValue | undefined): OpenCodePartType => {
410
+ switch (value) {
411
+ case 'reasoning':
412
+ case 'step-finish':
413
+ case 'step-start':
414
+ case 'text':
415
+ case 'tool':
416
+ return value;
417
+ default:
418
+ return 'unknown';
419
+ }
420
+ };
421
+
422
+ const formatJsonLike = (value: JsonValue | undefined): string | null => {
423
+ if (value === undefined || value === null) {
424
+ return null;
425
+ }
426
+
427
+ if (typeof value === 'string') {
428
+ return value;
429
+ }
430
+
431
+ return JSON.stringify(value, null, 2);
432
+ };
433
+
434
+ const parseTimeMs = (raw: Record<string, JsonValue>, key: 'end' | 'start'): number | null => {
435
+ const time = asObject(raw.time ?? null);
436
+ return asNumber(time?.[key] ?? null);
437
+ };
438
+
439
+ const parseToolTimeMs = (raw: Record<string, JsonValue>, key: 'end' | 'start'): number | null => {
440
+ const state = asObject(raw.state ?? null);
441
+ const time = asObject(state?.time ?? null) ?? asObject(raw.time ?? null);
442
+ return asNumber(time?.[key] ?? null);
443
+ };
444
+
445
+ const parseStepTokens = (value: JsonValue | undefined): OpenCodeStepTokens | null => {
446
+ const tokens = asObject(value ?? null);
447
+ if (!tokens) {
448
+ return null;
449
+ }
450
+
451
+ const cache = asObject(tokens.cache ?? null);
452
+ return {
453
+ cacheRead: asNumber(cache?.read ?? null) ?? 0,
454
+ cacheWrite: asNumber(cache?.write ?? null) ?? 0,
455
+ input: asNumber(tokens.input ?? null) ?? 0,
456
+ output: asNumber(tokens.output ?? null) ?? 0,
457
+ reasoning: asNumber(tokens.reasoning ?? null) ?? 0,
458
+ total: asNumber(tokens.total ?? null) ?? 0,
459
+ };
460
+ };
461
+
462
+ type BaseOpenCodePart = Omit<OpenCodeTranscriptPart, 'type'> & { type: OpenCodePartType };
463
+
464
+ const parseTextOpenCodePart = (base: BaseOpenCodePart): OpenCodeTranscriptPart => ({
465
+ ...base,
466
+ endTimeMs: parseTimeMs(base.raw, 'end'),
467
+ startTimeMs: parseTimeMs(base.raw, 'start'),
468
+ text: asString(base.raw.text ?? null) ?? '',
469
+ });
470
+
471
+ const parseToolOpenCodePart = (base: BaseOpenCodePart): OpenCodeTranscriptPart => {
472
+ const state = asObject(base.raw.state ?? null);
473
+ return {
474
+ ...base,
475
+ argumentsText: formatJsonLike(state?.input),
476
+ callId: asString(base.raw.callID ?? null) ?? asString(base.raw.callId ?? null),
477
+ endTimeMs: parseToolTimeMs(base.raw, 'end'),
478
+ outputText: formatJsonLike(state?.output),
479
+ startTimeMs: parseToolTimeMs(base.raw, 'start'),
480
+ status: asString(state?.status ?? null),
481
+ title: asString(state?.title ?? null),
482
+ toolName: asString(base.raw.tool ?? null) ?? 'unknown',
483
+ };
484
+ };
485
+
486
+ const parseStepFinishOpenCodePart = (base: BaseOpenCodePart): OpenCodeTranscriptPart => ({
487
+ ...base,
488
+ reason: asString(base.raw.reason ?? null),
489
+ snapshot: asString(base.raw.snapshot ?? null),
490
+ tokens: parseStepTokens(base.raw.tokens),
491
+ });
492
+
493
+ const parseStepStartOpenCodePart = (base: BaseOpenCodePart): OpenCodeTranscriptPart => ({
494
+ ...base,
495
+ snapshot: asString(base.raw.snapshot ?? null),
496
+ });
497
+
498
+ const parseOpenCodePartByType = (base: BaseOpenCodePart): OpenCodeTranscriptPart => {
499
+ switch (base.type) {
500
+ case 'reasoning':
501
+ case 'text':
502
+ return parseTextOpenCodePart(base);
503
+ case 'tool':
504
+ return parseToolOpenCodePart(base);
505
+ case 'step-finish':
506
+ return parseStepFinishOpenCodePart(base);
507
+ case 'step-start':
508
+ return parseStepStartOpenCodePart(base);
509
+ case 'unknown':
510
+ return base;
511
+ }
512
+ };
513
+
514
+ const parseOpenCodePart = (row: PartRow, role: string): OpenCodeTranscriptPart => {
515
+ const raw = parseJsonObject(row.data);
516
+ return parseOpenCodePartByType({
517
+ createdAtMs: row.timeCreated,
518
+ messageId: row.messageId,
519
+ partId: row.partId,
520
+ raw,
521
+ role,
522
+ type: toPartType(raw.type),
523
+ updatedAtMs: row.timeUpdated,
524
+ });
525
+ };
526
+
527
+ const isRenderablePart = (part: OpenCodeTranscriptPart): boolean => {
528
+ if (part.type === 'text' || part.type === 'reasoning') {
529
+ const { reasoningBlocks, visibleText } = splitOpenCodeThinkTaggedText(part.text ?? '');
530
+ return Boolean(visibleText.trim() || reasoningBlocks.length > 0);
531
+ }
532
+
533
+ if (part.type === 'tool') {
534
+ return Boolean(part.toolName || part.outputText?.trim() || part.argumentsText?.trim());
535
+ }
536
+
537
+ return false;
538
+ };
539
+
540
+ const readMessages = (db: Database, sessionId: string): OpenCodeTranscriptMessage[] => {
541
+ const messageRows = db
542
+ .query(
543
+ `SELECT id AS messageId, time_created AS timeCreated, time_updated AS timeUpdated, data
544
+ FROM message
545
+ WHERE session_id = ?
546
+ ORDER BY time_created ASC, id ASC`,
547
+ )
548
+ .all(sessionId) as MessageRow[];
549
+ const partRows = db
550
+ .query(
551
+ `SELECT id AS partId, message_id AS messageId, time_created AS timeCreated, time_updated AS timeUpdated, data
552
+ FROM part
553
+ WHERE session_id = ?
554
+ ORDER BY time_created ASC, id ASC`,
555
+ )
556
+ .all(sessionId) as PartRow[];
557
+ const partsByMessageId = new Map<string, PartRow[]>();
558
+ for (const part of partRows) {
559
+ const parts = partsByMessageId.get(part.messageId) ?? [];
560
+ parts.push(part);
561
+ partsByMessageId.set(part.messageId, parts);
562
+ }
563
+
564
+ return messageRows.map((message): OpenCodeTranscriptMessage => {
565
+ const raw = parseJsonObject(message.data);
566
+ const role = getMessageRole(raw);
567
+ const parts = (partsByMessageId.get(message.messageId) ?? []).map((part) => parseOpenCodePart(part, role));
568
+ return {
569
+ createdAtMs: message.timeCreated,
570
+ messageId: message.messageId,
571
+ parts,
572
+ raw,
573
+ role,
574
+ updatedAtMs: message.timeUpdated,
575
+ };
576
+ });
577
+ };
578
+
579
+ export const readOpenCodeSessionTranscript = async (
580
+ dbPath: string,
581
+ sessionId: string,
582
+ ): Promise<OpenCodeSessionTranscript | null> => {
583
+ if (!(await pathExists(dbPath))) {
584
+ return null;
585
+ }
586
+
587
+ const db = openOpenCodeReadonlyDb(dbPath);
588
+ try {
589
+ const session = readOpenCodeSessionSummary(db, sessionId);
590
+ if (!session) {
591
+ return null;
592
+ }
593
+
594
+ const messages = readMessages(db, sessionId);
595
+ const partCount = messages.reduce((total, message) => total + message.parts.length, 0);
596
+ const renderablePartCount = messages.reduce(
597
+ (total, message) => total + message.parts.filter(isRenderablePart).length,
598
+ 0,
599
+ );
600
+
601
+ return {
602
+ messages,
603
+ partCount,
604
+ renderablePartCount,
605
+ session,
606
+ };
607
+ } finally {
608
+ db.close();
609
+ }
610
+ };