sidekick-shared 0.27.6 → 0.28.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 (269) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +58 -18
  3. package/dist/browser.d.ts +13 -2
  4. package/dist/browser.js +30 -2
  5. package/dist/claudePaths.d.ts +11 -0
  6. package/dist/claudePaths.js +61 -0
  7. package/dist/claudeProfiles.d.ts +2 -9
  8. package/dist/claudeProfiles.js +10 -25
  9. package/dist/codexPaths.d.ts +22 -0
  10. package/dist/codexPaths.js +67 -0
  11. package/dist/codexProfiles.d.ts +1 -4
  12. package/dist/codexProfiles.js +5 -43
  13. package/dist/doctor.js +2 -2
  14. package/dist/esm/accountChangeSignal.js +15 -0
  15. package/dist/esm/accountChanges.js +152 -0
  16. package/dist/esm/accountHealth.js +288 -0
  17. package/dist/esm/accountLaunch.js +145 -0
  18. package/dist/esm/accountLease.js +60 -0
  19. package/dist/esm/accountManager.js +606 -0
  20. package/dist/esm/accountRegistry.js +214 -0
  21. package/dist/esm/accountStatus.js +51 -0
  22. package/dist/esm/accountSwitch.js +74 -0
  23. package/dist/esm/accountSync.js +156 -0
  24. package/dist/esm/accountSyncTypes.js +4 -0
  25. package/dist/esm/accounts.js +1291 -0
  26. package/dist/esm/aggregation/EventAggregator.js +2034 -0
  27. package/dist/esm/aggregation/FrequencyTracker.js +69 -0
  28. package/dist/esm/aggregation/HeatmapTracker.js +89 -0
  29. package/dist/esm/aggregation/PatternExtractor.js +168 -0
  30. package/dist/esm/aggregation/costProvenance.js +31 -0
  31. package/dist/esm/aggregation/snapshot.js +161 -0
  32. package/dist/esm/aggregation/types.js +5 -0
  33. package/dist/esm/analytics/codeImpact.js +18 -0
  34. package/dist/esm/analytics/compactionLedger.js +37 -0
  35. package/dist/esm/analytics/qualityScore.js +112 -0
  36. package/dist/esm/autoSwitch.js +185 -0
  37. package/dist/esm/browser.js +34 -0
  38. package/dist/esm/claudeCredentialDoctor.js +239 -0
  39. package/dist/esm/claudeCredentialLedger.js +91 -0
  40. package/dist/esm/claudeCredentials.js +111 -0
  41. package/dist/esm/claudeLiveGuard.js +43 -0
  42. package/dist/esm/claudePaths.js +22 -0
  43. package/dist/esm/claudeProfiles.js +142 -0
  44. package/dist/esm/claudeRefreshLock.js +106 -0
  45. package/dist/esm/codexAuth.js +101 -0
  46. package/dist/esm/codexCredentialDoctor.js +265 -0
  47. package/dist/esm/codexCredentialStore.js +164 -0
  48. package/dist/esm/codexKeepAlive.js +112 -0
  49. package/dist/esm/codexPaths.js +172 -0
  50. package/dist/esm/codexProfiles.js +1251 -0
  51. package/dist/esm/codexQuota.js +564 -0
  52. package/dist/esm/codexQuotaWatcher.js +363 -0
  53. package/dist/esm/context/composer.js +71 -0
  54. package/dist/esm/context/sessionContext.js +546 -0
  55. package/dist/esm/credentialIO.js +283 -0
  56. package/dist/esm/credentials.js +49 -0
  57. package/dist/esm/doctor.js +305 -0
  58. package/dist/esm/ensureDefaultAccounts.js +62 -0
  59. package/dist/esm/errorHistory.js +56 -0
  60. package/dist/esm/extractors/errorTaxonomy.js +43 -0
  61. package/dist/esm/extractors/gatherAssets.js +32 -0
  62. package/dist/esm/extractors/sessionAssets.js +176 -0
  63. package/dist/esm/extractors/sources/claudeAssets.js +158 -0
  64. package/dist/esm/extractors/sources/codexAssets.js +234 -0
  65. package/dist/esm/extractors/tokenUsage.js +41 -0
  66. package/dist/esm/extractors/toolCall.js +142 -0
  67. package/dist/esm/failingTools.js +65 -0
  68. package/dist/esm/fileHead.js +27 -0
  69. package/dist/esm/formatters/eventHighlighter.js +332 -0
  70. package/dist/esm/formatters/noiseClassifier.js +209 -0
  71. package/dist/esm/formatters/sessionDump.js +328 -0
  72. package/dist/esm/formatters/toolSummary.js +226 -0
  73. package/dist/esm/formatting.js +82 -0
  74. package/dist/esm/handoffUrl.js +23 -0
  75. package/dist/esm/historicalStore.js +305 -0
  76. package/dist/esm/humanPrompt.js +88 -0
  77. package/dist/esm/index.js +213 -0
  78. package/dist/esm/keepAlive.js +189 -0
  79. package/dist/esm/liveCredentialGuard.js +33 -0
  80. package/dist/esm/modelAliases.js +51 -0
  81. package/dist/esm/modelCatalog.js +139 -0
  82. package/dist/esm/modelContext.js +280 -0
  83. package/dist/esm/modelInfo.js +832 -0
  84. package/dist/esm/multiProviderQuotaService.js +450 -0
  85. package/dist/esm/node.js +46 -0
  86. package/dist/esm/observedContextWindows.js +165 -0
  87. package/dist/esm/observedSessionCollector.js +939 -0
  88. package/dist/esm/parsers/changelogParser.js +71 -0
  89. package/dist/esm/parsers/codexHistory.js +89 -0
  90. package/dist/esm/parsers/codexParser.js +906 -0
  91. package/dist/esm/parsers/debugLogParser.js +126 -0
  92. package/dist/esm/parsers/jsonl.js +66 -0
  93. package/dist/esm/parsers/mcpToolName.js +28 -0
  94. package/dist/esm/parsers/openCodeParser.js +625 -0
  95. package/dist/esm/parsers/planExtractor.js +335 -0
  96. package/dist/esm/parsers/sessionActivityDetector.js +246 -0
  97. package/dist/esm/parsers/sessionPathResolver.js +713 -0
  98. package/dist/esm/parsers/subagentScanner.js +360 -0
  99. package/dist/esm/parsers/subagentStatsCache.js +45 -0
  100. package/dist/esm/parsers/subagentTraceParser.js +325 -0
  101. package/dist/esm/paths.js +134 -0
  102. package/dist/esm/peakHours.js +194 -0
  103. package/dist/esm/phrases.js +1330 -0
  104. package/dist/esm/pricingCatalog.js +247 -0
  105. package/dist/esm/processDetection.js +200 -0
  106. package/dist/esm/projectMigration.js +59 -0
  107. package/dist/esm/promptHistory.js +1221 -0
  108. package/dist/esm/providerFailure.js +362 -0
  109. package/dist/esm/providerIds.js +6 -0
  110. package/dist/esm/providerQuota.js +1 -0
  111. package/dist/esm/providerServiceStatus.js +145 -0
  112. package/dist/esm/providerServiceStatusTypes.js +1 -0
  113. package/dist/esm/providerStatus.js +117 -0
  114. package/dist/esm/providers/claudeCode.js +575 -0
  115. package/dist/esm/providers/claudeListing.js +121 -0
  116. package/dist/esm/providers/codex.js +1034 -0
  117. package/dist/esm/providers/codexDatabase.js +297 -0
  118. package/dist/esm/providers/codexRollouts.js +215 -0
  119. package/dist/esm/providers/detect.js +185 -0
  120. package/dist/esm/providers/diagnostics.js +83 -0
  121. package/dist/esm/providers/directoryListingCache.js +206 -0
  122. package/dist/esm/providers/factory.js +54 -0
  123. package/dist/esm/providers/openCode.js +2008 -0
  124. package/dist/esm/providers/openCodeDatabase.js +419 -0
  125. package/dist/esm/providers/rolloutWalker.js +252 -0
  126. package/dist/esm/providers/types.js +12 -0
  127. package/dist/esm/quota.js +182 -0
  128. package/dist/esm/quotaHistory.js +364 -0
  129. package/dist/esm/quotaPoller.js +230 -0
  130. package/dist/esm/quotaPresentation.js +97 -0
  131. package/dist/esm/quotaResolver.js +231 -0
  132. package/dist/esm/quotaSnapshots.js +130 -0
  133. package/dist/esm/quotaThresholds.js +85 -0
  134. package/dist/esm/readers/decisions.js +24 -0
  135. package/dist/esm/readers/handoff.js +20 -0
  136. package/dist/esm/readers/helpers.js +27 -0
  137. package/dist/esm/readers/history.js +9 -0
  138. package/dist/esm/readers/notes.js +48 -0
  139. package/dist/esm/readers/plans.js +225 -0
  140. package/dist/esm/readers/tasks.js +19 -0
  141. package/dist/esm/report/htmlHelpers.js +280 -0
  142. package/dist/esm/report/htmlReportGenerator.js +704 -0
  143. package/dist/esm/report/index.js +7 -0
  144. package/dist/esm/report/logo.js +2 -0
  145. package/dist/esm/report/openBrowser.js +19 -0
  146. package/dist/esm/report/sessionReportInputs.js +33 -0
  147. package/dist/esm/report/transcriptParser.js +83 -0
  148. package/dist/esm/report/types.js +4 -0
  149. package/dist/esm/schemas/accountManager.js +204 -0
  150. package/dist/esm/schemas/accountStatus.js +25 -0
  151. package/dist/esm/schemas/assistantTurn.js +74 -0
  152. package/dist/esm/schemas/index.js +25 -0
  153. package/dist/esm/schemas/observedSessionV1.js +77 -0
  154. package/dist/esm/schemas/providerFailure.js +37 -0
  155. package/dist/esm/schemas/quota.js +132 -0
  156. package/dist/esm/schemas/quotaHistory.js +40 -0
  157. package/dist/esm/schemas/sessionEvent.js +216 -0
  158. package/dist/esm/schemas/stateFile.js +75 -0
  159. package/dist/esm/schemas/tokenEstimation.js +7 -0
  160. package/dist/esm/schemas/transcript.js +86 -0
  161. package/dist/esm/schemas/usageNormalization.js +38 -0
  162. package/dist/esm/search/advancedFilter.js +197 -0
  163. package/dist/esm/search/sessionSearch.js +66 -0
  164. package/dist/esm/sessionFingerprint.js +40 -0
  165. package/dist/esm/sessionMonitor.js +198 -0
  166. package/dist/esm/sessionPreviews.js +522 -0
  167. package/dist/esm/sessionPromptHistory.js +254 -0
  168. package/dist/esm/sessionPromptSignals.js +364 -0
  169. package/dist/esm/sessionStats.js +160 -0
  170. package/dist/esm/sessionTokenTotals.js +98 -0
  171. package/dist/esm/sessionTranscripts.js +49 -0
  172. package/dist/esm/stateFile.js +119 -0
  173. package/dist/esm/statusline/BurnRateCalculator.js +40 -0
  174. package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
  175. package/dist/esm/statusline/formatter.js +122 -0
  176. package/dist/esm/statusline/index.js +8 -0
  177. package/dist/esm/terminalSync.js +95 -0
  178. package/dist/esm/testing/credentialGuard.js +197 -0
  179. package/dist/esm/testing/index.js +12 -0
  180. package/dist/esm/testing/sessionLogFixtures.js +128 -0
  181. package/dist/esm/tokenEstimation.js +67 -0
  182. package/dist/esm/tokenSummary.js +113 -0
  183. package/dist/esm/toolCategory.js +82 -0
  184. package/dist/esm/transcript.js +257 -0
  185. package/dist/esm/turns/assistantTurn.js +373 -0
  186. package/dist/esm/types/codex.js +23 -0
  187. package/dist/esm/types/decisionLog.js +5 -0
  188. package/dist/esm/types/historicalData.js +47 -0
  189. package/dist/esm/types/knowledgeNote.js +15 -0
  190. package/dist/esm/types/observedSessionV1.js +257 -0
  191. package/dist/esm/types/opencode.js +12 -0
  192. package/dist/esm/types/plan.js +7 -0
  193. package/dist/esm/types/sessionEvent.js +10 -0
  194. package/dist/esm/types/taskPersistence.js +12 -0
  195. package/dist/esm/usage/billingBlocks.js +148 -0
  196. package/dist/esm/usage/claudeUsageDedupe.js +189 -0
  197. package/dist/esm/usage/importSessionHistory.js +84 -0
  198. package/dist/esm/usage/subagentUsage.js +18 -0
  199. package/dist/esm/usage/usageEvents.js +402 -0
  200. package/dist/esm/usage/usageReports.js +202 -0
  201. package/dist/esm/usageNormalization.js +95 -0
  202. package/dist/esm/watchers/eventBridge.js +219 -0
  203. package/dist/esm/watchers/factory.js +63 -0
  204. package/dist/esm/watchers/jsonlTail.js +289 -0
  205. package/dist/esm/watchers/jsonlWatcher.js +435 -0
  206. package/dist/esm/watchers/providerReaderWatcher.js +118 -0
  207. package/dist/esm/watchers/sqliteWatcher.js +285 -0
  208. package/dist/esm/watchers/types.js +4 -0
  209. package/dist/esm/workspaceRoots.js +56 -0
  210. package/dist/esm/writers/atomic.js +375 -0
  211. package/dist/esm/writers/decisions.js +51 -0
  212. package/dist/esm/writers/notes.js +46 -0
  213. package/dist/esm/writers/tasks.js +63 -0
  214. package/dist/esm/zaiQuota.js +323 -0
  215. package/dist/esm/zaiQuotaApi.js +271 -0
  216. package/dist/esm/zaiQuotaWatcher.js +265 -0
  217. package/dist/fileHead.d.ts +22 -0
  218. package/dist/fileHead.js +33 -0
  219. package/dist/index.d.ts +16 -7
  220. package/dist/index.js +49 -15
  221. package/dist/node.d.ts +13 -1
  222. package/dist/node.js +43 -2
  223. package/dist/parsers/codexHistory.js +2 -2
  224. package/dist/parsers/codexParser.d.ts +12 -0
  225. package/dist/parsers/codexParser.js +34 -12
  226. package/dist/parsers/debugLogParser.js +2 -2
  227. package/dist/parsers/sessionPathResolver.js +5 -4
  228. package/dist/promptHistory.d.ts +32 -0
  229. package/dist/promptHistory.js +115 -59
  230. package/dist/providerFailure.d.ts +12 -6
  231. package/dist/providerFailure.js +45 -0
  232. package/dist/providers/claudeCode.js +7 -99
  233. package/dist/providers/claudeListing.d.ts +22 -0
  234. package/dist/providers/claudeListing.js +158 -0
  235. package/dist/providers/codex.d.ts +3 -1
  236. package/dist/providers/codex.js +26 -227
  237. package/dist/providers/codexRollouts.d.ts +44 -0
  238. package/dist/providers/codexRollouts.js +261 -0
  239. package/dist/providers/detect.js +4 -4
  240. package/dist/readers/plans.js +3 -3
  241. package/dist/schemas/index.d.ts +2 -0
  242. package/dist/schemas/index.js +7 -2
  243. package/dist/schemas/providerFailure.d.ts +154 -0
  244. package/dist/schemas/providerFailure.js +40 -0
  245. package/dist/sessionPromptHistory.d.ts +12 -7
  246. package/dist/sessionPromptHistory.js +15 -3
  247. package/dist/sessionPromptSignals.d.ts +9 -0
  248. package/dist/sessionPromptSignals.js +13 -0
  249. package/dist/testing/credentialGuard.d.ts +23 -0
  250. package/dist/testing/credentialGuard.js +233 -0
  251. package/dist/testing/index.d.ts +14 -0
  252. package/dist/testing/index.js +22 -0
  253. package/dist/testing/sessionLogFixtures.d.ts +28 -0
  254. package/dist/testing/sessionLogFixtures.js +169 -0
  255. package/dist/tokenEstimation.d.ts +2 -0
  256. package/dist/tokenEstimation.js +21 -11
  257. package/dist/toolCategory.d.ts +18 -0
  258. package/dist/toolCategory.js +86 -0
  259. package/dist/types/codex.d.ts +4 -0
  260. package/dist/usage/usageEvents.d.ts +16 -0
  261. package/dist/usage/usageEvents.js +53 -9
  262. package/dist/watchers/jsonlTail.d.ts +26 -1
  263. package/dist/watchers/jsonlTail.js +136 -28
  264. package/dist/watchers/jsonlWatcher.js +7 -0
  265. package/dist/watchers/providerReaderWatcher.js +1 -0
  266. package/dist/watchers/types.d.ts +6 -0
  267. package/dist/workspaceRoots.d.ts +23 -0
  268. package/dist/workspaceRoots.js +93 -0
  269. package/package.json +22 -2
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Read-only SQLite wrapper for OpenCode's database.
3
+ * Ported from sidekick-vscode/src/services/providers/OpenCodeDatabase.ts
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { execFile, execFileSync } from 'child_process';
8
+ /**
9
+ * Ids per inlined `IN (…)` query. Values are passed inside a single argv
10
+ * element to sqlite3, so an unbounded list would hit E2BIG on large stores.
11
+ */
12
+ const SQL_ID_CHUNK_SIZE = 400;
13
+ export class OpenCodeDatabase {
14
+ dbPath;
15
+ runtimeStatus = null;
16
+ constructor(dataDir) {
17
+ this.dbPath = path.join(dataDir, 'opencode.db');
18
+ }
19
+ isAvailable() {
20
+ return fs.existsSync(this.dbPath);
21
+ }
22
+ getRuntimeStatus() {
23
+ if (this.runtimeStatus)
24
+ return this.runtimeStatus;
25
+ if (!this.isAvailable()) {
26
+ this.runtimeStatus = { available: false, kind: 'db_missing' };
27
+ return this.runtimeStatus;
28
+ }
29
+ return {
30
+ available: false,
31
+ kind: 'query_failed',
32
+ message: 'OpenCode database has not been initialized yet.',
33
+ };
34
+ }
35
+ open() {
36
+ if (this.runtimeStatus?.kind === 'available')
37
+ return true;
38
+ if (this.runtimeStatus &&
39
+ ['db_missing', 'sqlite_missing', 'sqlite_blocked'].includes(this.runtimeStatus.kind)) {
40
+ return false;
41
+ }
42
+ if (!this.isAvailable()) {
43
+ this.runtimeStatus = { available: false, kind: 'db_missing' };
44
+ return false;
45
+ }
46
+ try {
47
+ execFileSync('sqlite3', ['--version'], {
48
+ encoding: 'utf-8',
49
+ timeout: 4000,
50
+ killSignal: 'SIGKILL',
51
+ stdio: ['pipe', 'pipe', 'pipe'],
52
+ });
53
+ this.runtimeStatus = { available: true, kind: 'available' };
54
+ return true;
55
+ }
56
+ catch (error) {
57
+ this.runtimeStatus = toRuntimeStatus(error);
58
+ return false;
59
+ }
60
+ }
61
+ close() { }
62
+ query(sql, params = []) {
63
+ if (!this.open())
64
+ return [];
65
+ let paramIndex = 0;
66
+ const query = sql.replace(/\?/g, () => {
67
+ if (paramIndex >= params.length)
68
+ return '?';
69
+ const param = params[paramIndex++];
70
+ if (typeof param === 'number')
71
+ return Number.isFinite(param) ? String(param) : '0';
72
+ const escaped = String(param).replace(/'/g, "''");
73
+ return `'${escaped}'`;
74
+ });
75
+ try {
76
+ const result = execFileSync('sqlite3', ['-json', '-readonly', this.dbPath, query], {
77
+ encoding: 'utf-8',
78
+ timeout: 4000,
79
+ killSignal: 'SIGKILL',
80
+ stdio: ['pipe', 'pipe', 'pipe'],
81
+ maxBuffer: 50 * 1024 * 1024,
82
+ });
83
+ this.runtimeStatus = { available: true, kind: 'available' };
84
+ const trimmed = result.trim();
85
+ if (!trimmed)
86
+ return [];
87
+ return JSON.parse(trimmed);
88
+ }
89
+ catch (error) {
90
+ this.runtimeStatus = {
91
+ ...toRuntimeStatus(error, 'query_failed'),
92
+ // The database and sqlite binary were successfully opened. A failed
93
+ // query is transient and must not gate the next attempt.
94
+ available: true,
95
+ };
96
+ return [];
97
+ }
98
+ }
99
+ async queryAsync(sql, params = []) {
100
+ if (!this.isAvailable()) {
101
+ this.runtimeStatus = { available: false, kind: 'db_missing' };
102
+ return [];
103
+ }
104
+ const query = bindSqlParams(sql, params);
105
+ try {
106
+ const result = await execSqlite(this.dbPath, query);
107
+ this.runtimeStatus = { available: true, kind: 'available' };
108
+ const trimmed = result.trim();
109
+ return trimmed ? JSON.parse(trimmed) : [];
110
+ }
111
+ catch (error) {
112
+ this.runtimeStatus = toRuntimeStatus(error, 'query_failed');
113
+ return [];
114
+ }
115
+ }
116
+ queryOne(sql, params = []) {
117
+ const results = this.query(sql, params);
118
+ return results[0] ?? null;
119
+ }
120
+ findProjectByWorktree(workspacePath) {
121
+ const normalized = normalizePath(workspacePath);
122
+ const all = this.getAllProjects();
123
+ let best = null;
124
+ let bestScore = -1;
125
+ for (const project of all) {
126
+ const worktreeScore = pathMatchScore(project.worktree, normalized);
127
+ if (worktreeScore > bestScore) {
128
+ best = project;
129
+ bestScore = worktreeScore;
130
+ }
131
+ for (const sandbox of project.sandboxes) {
132
+ const sandboxScore = pathMatchScore(sandbox, normalized);
133
+ if (sandboxScore > bestScore) {
134
+ best = project;
135
+ bestScore = sandboxScore;
136
+ }
137
+ }
138
+ }
139
+ return bestScore >= 0 ? best : null;
140
+ }
141
+ findProjectBySessionDirectory(workspacePath) {
142
+ const normalized = normalizePath(workspacePath);
143
+ const sessions = this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
144
+ let bestProjectId = null;
145
+ let bestScore = -1;
146
+ for (const session of sessions) {
147
+ const score = pathMatchScore(session.directory, normalized);
148
+ if (score > bestScore) {
149
+ bestProjectId = session.project_id;
150
+ bestScore = score;
151
+ }
152
+ }
153
+ if (!bestProjectId)
154
+ return null;
155
+ return this.getAllProjects().find((project) => project.id === bestProjectId) ?? null;
156
+ }
157
+ getAllProjects() {
158
+ return this.query('SELECT id, worktree, name, sandboxes, time_created, time_updated FROM project').map((project) => ({
159
+ ...project,
160
+ sandboxes: parseStringArray(project.sandboxes),
161
+ }));
162
+ }
163
+ hasProject(projectId) {
164
+ return (this.queryOne('SELECT id FROM project WHERE id = ? LIMIT 1', [projectId]) !==
165
+ null);
166
+ }
167
+ getSessionsForProject(projectId) {
168
+ return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC', [projectId]);
169
+ }
170
+ /** Get every root session in one query for global preview enumeration. */
171
+ getAllSessions() {
172
+ return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
173
+ }
174
+ /** Enumerate root sessions with one non-blocking sqlite3 subprocess. */
175
+ getAllSessionsAsync() {
176
+ return this.queryAsync('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
177
+ }
178
+ /**
179
+ * Resolve many sessions with non-blocking sqlite3 subprocesses — one per
180
+ * chunk of ids, so the inlined query stays clear of argv length limits.
181
+ */
182
+ async getSessionsByIdsAsync(sessionIds) {
183
+ const results = [];
184
+ for (let i = 0; i < sessionIds.length; i += SQL_ID_CHUNK_SIZE) {
185
+ const chunk = sessionIds.slice(i, i + SQL_ID_CHUNK_SIZE);
186
+ const placeholders = chunk.map(() => '?').join(', ');
187
+ results.push(...(await this.queryAsync(`SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE id IN (${placeholders})`, [...chunk])));
188
+ }
189
+ return results;
190
+ }
191
+ /** Get the most recently updated session for a project (excludes subagent child sessions). */
192
+ getMostRecentSession(projectId) {
193
+ return this.queryOne('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC LIMIT 1', [projectId]);
194
+ }
195
+ /** Get child sessions (subagents) whose parent_id matches the given session ID. */
196
+ getChildSessions(parentSessionId) {
197
+ return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id = ? ORDER BY time_created ASC', [parentSessionId]);
198
+ }
199
+ getSession(sessionId) {
200
+ return this.queryOne('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE id = ?', [sessionId]);
201
+ }
202
+ getMessagesForSession(sessionId) {
203
+ return this.query('SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ? ORDER BY time_created ASC', [sessionId]);
204
+ }
205
+ /** Get specific messages for a session by message IDs. */
206
+ getMessagesByIds(sessionId, messageIds) {
207
+ if (messageIds.length === 0)
208
+ return [];
209
+ const placeholders = messageIds.map(() => '?').join(', ');
210
+ return this.query(`SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ? AND id IN (${placeholders}) ORDER BY time_created ASC`, [sessionId, ...messageIds]);
211
+ }
212
+ /** Get messages newer than a given time_updated timestamp (ms epoch). */
213
+ getMessagesNewerThan(sessionId, afterTimeUpdated) {
214
+ return this.query('SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ? AND time_updated > ? ORDER BY time_created ASC', [sessionId, afterTimeUpdated]);
215
+ }
216
+ /** Get the latest message time_updated for a session. */
217
+ getLatestMessageTimeUpdated(sessionId) {
218
+ const row = this.queryOne('SELECT COALESCE(MAX(time_updated), 0) AS maxTimeUpdated FROM message WHERE session_id = ?', [sessionId]);
219
+ return row?.maxTimeUpdated ?? 0;
220
+ }
221
+ /** Get user message IDs that already have an assistant child message. */
222
+ getProcessedUserMessageIds(sessionId, userMessageIds) {
223
+ if (userMessageIds.length === 0)
224
+ return [];
225
+ const placeholders = userMessageIds.map(() => '?').join(', ');
226
+ const rows = this.query(`SELECT DISTINCT json_extract(data, '$.parentID') AS parentId
227
+ FROM message
228
+ WHERE session_id = ?
229
+ AND json_extract(data, '$.role') = 'assistant'
230
+ AND json_extract(data, '$.parentID') IN (${placeholders})`, [sessionId, ...userMessageIds]);
231
+ return rows
232
+ .map((r) => r.parentId)
233
+ .filter((id) => typeof id === 'string' && id.length > 0);
234
+ }
235
+ /** Get the latest assistant token usage row with non-zero context signal. */
236
+ getLatestAssistantContextUsage(sessionId) {
237
+ return this.queryOne(`SELECT
238
+ time_created AS timeCreated,
239
+ COALESCE(json_extract(data, '$.modelID'), 'unknown') AS modelId,
240
+ COALESCE(json_extract(data, '$.tokens.input'), 0) AS inputTokens,
241
+ COALESCE(json_extract(data, '$.tokens.output'), 0) AS outputTokens,
242
+ COALESCE(json_extract(data, '$.tokens.cache.read'), 0) AS cacheReadTokens,
243
+ COALESCE(json_extract(data, '$.tokens.cache.write'), 0) AS cacheWriteTokens,
244
+ COALESCE(json_extract(data, '$.tokens.reasoning'), 0) AS reasoningTokens
245
+ FROM message
246
+ WHERE session_id = ?
247
+ AND json_extract(data, '$.role') = 'assistant'
248
+ AND (
249
+ COALESCE(json_extract(data, '$.tokens.input'), 0) > 0
250
+ OR COALESCE(json_extract(data, '$.tokens.output'), 0) > 0
251
+ OR COALESCE(json_extract(data, '$.tokens.cache.read'), 0) > 0
252
+ OR COALESCE(json_extract(data, '$.tokens.cache.write'), 0) > 0
253
+ OR COALESCE(json_extract(data, '$.tokens.reasoning'), 0) > 0
254
+ )
255
+ ORDER BY time_created DESC
256
+ LIMIT 1`, [sessionId]);
257
+ }
258
+ /** Get all parts for a message. */
259
+ getPartsForMessage(messageId) {
260
+ return this.query('SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE message_id = ? ORDER BY time_created ASC', [messageId]);
261
+ }
262
+ /** Get all parts for a set of messages in one query. */
263
+ getPartsForMessages(sessionId, messageIds) {
264
+ if (messageIds.length === 0)
265
+ return [];
266
+ const placeholders = messageIds.map(() => '?').join(', ');
267
+ return this.query(`SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ? AND message_id IN (${placeholders}) ORDER BY time_created ASC`, [sessionId, ...messageIds]);
268
+ }
269
+ getPartsForSession(sessionId) {
270
+ return this.query('SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ? ORDER BY time_created ASC', [sessionId]);
271
+ }
272
+ /** Get parts newer than a given time_updated timestamp (ms epoch). */
273
+ getPartsNewerThan(sessionId, afterTimeUpdated) {
274
+ return this.query('SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ? AND time_updated > ? ORDER BY time_created ASC', [sessionId, afterTimeUpdated]);
275
+ }
276
+ /** Get the latest part time_updated for a session. */
277
+ getLatestPartTimeUpdated(sessionId) {
278
+ const row = this.queryOne('SELECT COALESCE(MAX(time_updated), 0) AS maxTimeUpdated FROM part WHERE session_id = ?', [sessionId]);
279
+ return row?.maxTimeUpdated ?? 0;
280
+ }
281
+ getProjectSessionStats() {
282
+ return this.query('SELECT project_id AS projectId, COUNT(*) AS sessionCount, MAX(time_updated) AS maxTimeUpdated FROM session GROUP BY project_id');
283
+ }
284
+ /**
285
+ * Returns assistant messages tagged with the given providerID(s), completed
286
+ * within `sinceMs`. Used by the z.ai quota accumulator to walk OpenCode's
287
+ * per-turn token records without re-deriving the session filter.
288
+ *
289
+ * Results are ordered oldest-first by `time_created`.
290
+ */
291
+ getAssistantMessagesByProviderId(providerIds, sinceMs) {
292
+ if (providerIds.length === 0)
293
+ return [];
294
+ const providerClauses = providerIds
295
+ .map(() => "json_extract(data, '$.providerID') = ?")
296
+ .join(' OR ');
297
+ return this.query(`SELECT
298
+ time_created AS timeCreated,
299
+ COALESCE(json_extract(data, '$.modelID'), 'unknown') AS modelId,
300
+ COALESCE(json_extract(data, '$.providerID'), '') AS providerId,
301
+ COALESCE(json_extract(data, '$.tokens.input'), 0) AS inputTokens,
302
+ COALESCE(json_extract(data, '$.tokens.output'), 0) AS outputTokens,
303
+ COALESCE(json_extract(data, '$.tokens.cache.read'), 0) AS cacheReadTokens,
304
+ COALESCE(json_extract(data, '$.tokens.cache.write'), 0) AS cacheWriteTokens,
305
+ COALESCE(json_extract(data, '$.tokens.reasoning'), 0) AS reasoningTokens,
306
+ json_extract(data, '$.error.message') AS errorMessage,
307
+ json_extract(data, '$.error.code') AS errorCode
308
+ FROM message
309
+ WHERE time_created >= ?
310
+ AND json_extract(data, '$.role') = 'assistant'
311
+ AND (${providerClauses})
312
+ ORDER BY time_created ASC`, [sinceMs, ...providerIds]);
313
+ }
314
+ /** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
315
+ getDbMtime() {
316
+ try {
317
+ return fs.statSync(this.dbPath).mtime.getTime();
318
+ }
319
+ catch {
320
+ return 0;
321
+ }
322
+ }
323
+ }
324
+ function bindSqlParams(sql, params) {
325
+ let paramIndex = 0;
326
+ return sql.replace(/\?/g, () => {
327
+ if (paramIndex >= params.length)
328
+ return '?';
329
+ const param = params[paramIndex++];
330
+ if (typeof param === 'number')
331
+ return Number.isFinite(param) ? String(param) : '0';
332
+ return `'${String(param).replace(/'/g, "''")}'`;
333
+ });
334
+ }
335
+ function execSqlite(dbPath, query) {
336
+ return new Promise((resolve, reject) => {
337
+ execFile('sqlite3', ['-json', '-readonly', dbPath, query], {
338
+ encoding: 'utf8',
339
+ timeout: 4_000,
340
+ killSignal: 'SIGKILL',
341
+ maxBuffer: 50 * 1024 * 1024,
342
+ }, (error, stdout) => {
343
+ if (error)
344
+ reject(error);
345
+ else
346
+ resolve(stdout);
347
+ });
348
+ });
349
+ }
350
+ function normalizePath(input) {
351
+ try {
352
+ return fs.realpathSync(input);
353
+ }
354
+ catch {
355
+ return path.resolve(input);
356
+ }
357
+ }
358
+ function parseStringArray(value) {
359
+ if (!value)
360
+ return [];
361
+ try {
362
+ const parsed = JSON.parse(value);
363
+ return Array.isArray(parsed)
364
+ ? parsed
365
+ .filter((entry) => typeof entry === 'string' && entry.length > 0)
366
+ .map(normalizePath)
367
+ : [];
368
+ }
369
+ catch {
370
+ return [];
371
+ }
372
+ }
373
+ function normalizeForCompare(input) {
374
+ const normalized = normalizePath(input);
375
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
376
+ }
377
+ function pathMatchScore(candidate, workspacePath) {
378
+ if (!candidate)
379
+ return -1;
380
+ const normalizedCandidate = normalizeForCompare(candidate);
381
+ const normalizedWorkspace = normalizeForCompare(workspacePath);
382
+ if (normalizedCandidate === normalizedWorkspace)
383
+ return 10_000 + normalizedCandidate.length;
384
+ const candidatePrefix = normalizedCandidate.endsWith(path.sep)
385
+ ? normalizedCandidate
386
+ : normalizedCandidate + path.sep;
387
+ const workspacePrefix = normalizedWorkspace.endsWith(path.sep)
388
+ ? normalizedWorkspace
389
+ : normalizedWorkspace + path.sep;
390
+ if (normalizedWorkspace.startsWith(candidatePrefix))
391
+ return 5_000 + normalizedCandidate.length;
392
+ if (normalizedCandidate.startsWith(workspacePrefix))
393
+ return 1_000 + normalizedCandidate.length;
394
+ return -1;
395
+ }
396
+ function toRuntimeStatus(error, fallback = 'sqlite_missing') {
397
+ if (isErrno(error, 'ENOENT')) {
398
+ return {
399
+ available: false,
400
+ kind: 'sqlite_missing',
401
+ message: 'sqlite3 executable not found in PATH.',
402
+ };
403
+ }
404
+ if (isErrno(error, 'EPERM') || isErrno(error, 'EACCES')) {
405
+ return {
406
+ available: false,
407
+ kind: 'sqlite_blocked',
408
+ message: 'sqlite3 exists but could not be executed.',
409
+ };
410
+ }
411
+ const message = error instanceof Error ? error.message : String(error);
412
+ return { available: false, kind: fallback, message };
413
+ }
414
+ function isErrno(error, code) {
415
+ return (typeof error === 'object' &&
416
+ error !== null &&
417
+ 'code' in error &&
418
+ error.code === code);
419
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * One capped walker for Codex rollout files.
3
+ *
4
+ * Codex writes `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`.
5
+ * Six recursive, uncapped walkers used to exist across the provider and the
6
+ * quota resolver; this module replaces them with a single walk that caps
7
+ * depth and file count, visits the newest-dated directories first so a
8
+ * `limit` can stop early, stats each file exactly once, and returns results
9
+ * newest first with the size and session id already attached.
10
+ *
11
+ * With `limit` or `since`, the early exit is by directory date: a rollout
12
+ * that was created days ago but is still being written lands in its creation
13
+ * day's directory, so a very small limit (or a recent `since`) can miss it in
14
+ * favour of newer-dated, older-modified files. Callers that need exact mtime
15
+ * order over the whole history omit both (the `maxFiles` cap still applies);
16
+ * a live session missed this way is picked up by its next watch event.
17
+ *
18
+ * The async walker accepts a {@link DirectoryListingCache}: unchanged
19
+ * directories are then one stat each instead of a readdir plus a stat per
20
+ * file, which is what makes repeated discovery over a large history cheap.
21
+ *
22
+ * @module providers/rolloutWalker
23
+ */
24
+ import * as fs from 'fs';
25
+ import * as path from 'path';
26
+ export const DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
27
+ export const DEFAULT_ROLLOUT_WALK_MAX_FILES = 20_000;
28
+ /** Test if a filename is a Codex rollout file. */
29
+ export function isRolloutFile(filename) {
30
+ return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
31
+ }
32
+ /**
33
+ * Extract the session UUID from a rollout filename.
34
+ * Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
35
+ */
36
+ export function extractRolloutSessionId(filename) {
37
+ const base = path.basename(filename, '.jsonl');
38
+ // rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
39
+ const parts = base.split('-');
40
+ // The UUID is typically the last 5 segments (8-4-4-4-12)
41
+ if (parts.length >= 6) {
42
+ const possibleUuid = parts.slice(-5).join('-');
43
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(possibleUuid)) {
44
+ return possibleUuid;
45
+ }
46
+ }
47
+ // Fallback: use everything after "rollout-"
48
+ return base.replace(/^rollout-/, '');
49
+ }
50
+ function walkState(options) {
51
+ const maxFiles = Math.max(0, options.maxFiles ?? DEFAULT_ROLLOUT_WALK_MAX_FILES);
52
+ const limit = options.limit === undefined ? Infinity : Math.max(0, options.limit);
53
+ return {
54
+ results: [],
55
+ seen: new Set(),
56
+ cap: Math.min(maxFiles, limit),
57
+ maxDepth: options.maxDepth ?? DEFAULT_ROLLOUT_WALK_MAX_DEPTH,
58
+ since: options.since !== undefined && Number.isFinite(options.since) ? options.since : null,
59
+ sessionId: options.sessionId ? options.sessionId.trim().toLowerCase() : null,
60
+ includeEmpty: options.includeEmpty ?? false,
61
+ cache: options.cache ?? null,
62
+ revalidateRecent: options.revalidateRecent ?? false,
63
+ stats: options.stats,
64
+ };
65
+ }
66
+ /** Newest-dated first: directory and file names sort descending. */
67
+ function byNameDescending(a, b) {
68
+ return b.name.localeCompare(a.name);
69
+ }
70
+ function matchesFilter(state, name) {
71
+ if (!isRolloutFile(name))
72
+ return false;
73
+ return (state.sessionId === null || extractRolloutSessionId(name).toLowerCase() === state.sessionId);
74
+ }
75
+ function record(state, fullPath, name, stat) {
76
+ if (!state.includeEmpty && stat.size <= 0)
77
+ return;
78
+ if (state.since !== null && stat.mtimeMs < state.since)
79
+ return;
80
+ if (state.seen.has(fullPath))
81
+ return;
82
+ state.seen.add(fullPath);
83
+ state.results.push({
84
+ path: fullPath,
85
+ mtime: new Date(stat.mtimeMs),
86
+ sizeBytes: stat.size,
87
+ sessionId: extractRolloutSessionId(name),
88
+ });
89
+ }
90
+ function finish(state) {
91
+ return state.results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
92
+ }
93
+ /** Slack added to a dated directory's range so a timezone skew cannot prune a live day. */
94
+ const DATE_DIR_SLACK_MS = 24 * 60 * 60_000;
95
+ /**
96
+ * True when `segments` (the directory names below the root, e.g. `['2026',
97
+ * '09', '04']`) are all numeric and describe a year, month, or day that ended
98
+ * more than a day before `since`. Non-numeric names are never pruned.
99
+ */
100
+ export function isDatedDirectoryBefore(segments, since) {
101
+ if (segments.length === 0 || segments.length > 3)
102
+ return false;
103
+ const numbers = segments.map((segment) => (/^\d+$/.test(segment) ? Number(segment) : NaN));
104
+ if (numbers.some((value) => !Number.isFinite(value)))
105
+ return false;
106
+ const [year, month, day] = numbers;
107
+ if (year < 1970 || year > 9999)
108
+ return false;
109
+ if (segments.length >= 2 && (month < 1 || month > 12))
110
+ return false;
111
+ if (segments.length === 3 && (day < 1 || day > 31))
112
+ return false;
113
+ const rangeEnd = segments.length === 1
114
+ ? new Date(year + 1, 0, 1).getTime()
115
+ : segments.length === 2
116
+ ? new Date(year, month, 1).getTime()
117
+ : new Date(year, month - 1, day + 1).getTime();
118
+ return rangeEnd + DATE_DIR_SLACK_MS < since;
119
+ }
120
+ function visitSync(state, dir, depth, segments) {
121
+ if (depth > state.maxDepth || state.results.length >= state.cap)
122
+ return;
123
+ if (state.since !== null && isDatedDirectoryBefore(segments, state.since))
124
+ return;
125
+ let entries;
126
+ try {
127
+ entries = fs.readdirSync(dir, { withFileTypes: true });
128
+ }
129
+ catch {
130
+ return;
131
+ }
132
+ entries.sort(byNameDescending);
133
+ for (const entry of entries) {
134
+ if (state.results.length >= state.cap)
135
+ return;
136
+ if (!entry.isFile() || !matchesFilter(state, entry.name))
137
+ continue;
138
+ const fullPath = path.join(dir, entry.name);
139
+ try {
140
+ record(state, fullPath, entry.name, fs.statSync(fullPath));
141
+ }
142
+ catch {
143
+ // Skip files that vanish between readdir and stat.
144
+ }
145
+ }
146
+ for (const entry of entries) {
147
+ if (state.results.length >= state.cap)
148
+ return;
149
+ if (entry.isDirectory()) {
150
+ visitSync(state, path.join(dir, entry.name), depth + 1, [...segments, entry.name]);
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Walk one or more `sessions` roots for rollout files, newest first.
156
+ * Missing or unreadable directories are skipped; paths are deduplicated.
157
+ */
158
+ export function walkRolloutFiles(roots, options = {}) {
159
+ const state = walkState(options);
160
+ for (const root of roots)
161
+ visitSync(state, root, 0, []);
162
+ return finish(state);
163
+ }
164
+ function yieldToEventLoop() {
165
+ return new Promise((resolve) => setImmediate(resolve));
166
+ }
167
+ function byStringDescending(a, b) {
168
+ return b.localeCompare(a);
169
+ }
170
+ async function visitCached(state, cache, dir, depth, segments) {
171
+ if (depth > state.maxDepth || state.results.length >= state.cap)
172
+ return;
173
+ if (state.since !== null && isDatedDirectoryBefore(segments, state.since))
174
+ return;
175
+ const listing = await cache.listDirectory(dir, (name) => matchesFilter(state, name), {
176
+ revalidateRecent: state.revalidateRecent,
177
+ stats: state.stats,
178
+ });
179
+ if (!listing)
180
+ return;
181
+ const names = [...listing.files.keys()].sort(byStringDescending);
182
+ for (const name of names) {
183
+ if (state.results.length >= state.cap)
184
+ return;
185
+ const stat = listing.files.get(name);
186
+ if (!stat)
187
+ continue;
188
+ record(state, path.join(dir, name), name, { size: stat.sizeBytes, mtimeMs: stat.mtimeMs });
189
+ }
190
+ await yieldToEventLoop();
191
+ for (const name of [...listing.subdirectories].sort(byStringDescending)) {
192
+ if (state.results.length >= state.cap)
193
+ return;
194
+ await visitCached(state, cache, path.join(dir, name), depth + 1, [...segments, name]);
195
+ }
196
+ }
197
+ async function visitAsync(state, dir, depth, segments) {
198
+ if (depth > state.maxDepth || state.results.length >= state.cap)
199
+ return;
200
+ if (state.since !== null && isDatedDirectoryBefore(segments, state.since))
201
+ return;
202
+ let entries;
203
+ try {
204
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
205
+ if (state.stats)
206
+ state.stats.directoriesListed = (state.stats.directoriesListed ?? 0) + 1;
207
+ }
208
+ catch {
209
+ return;
210
+ }
211
+ entries.sort(byNameDescending);
212
+ let sinceYield = 0;
213
+ for (const entry of entries) {
214
+ if (state.results.length >= state.cap)
215
+ return;
216
+ if (!entry.isFile() || !matchesFilter(state, entry.name))
217
+ continue;
218
+ const fullPath = path.join(dir, entry.name);
219
+ try {
220
+ const stat = await fs.promises.stat(fullPath);
221
+ if (state.stats)
222
+ state.stats.filesStatted = (state.stats.filesStatted ?? 0) + 1;
223
+ record(state, fullPath, entry.name, stat);
224
+ }
225
+ catch {
226
+ // Skip files that vanish between readdir and stat.
227
+ }
228
+ if (++sinceYield >= 200) {
229
+ sinceYield = 0;
230
+ await yieldToEventLoop();
231
+ }
232
+ }
233
+ await yieldToEventLoop();
234
+ for (const entry of entries) {
235
+ if (state.results.length >= state.cap)
236
+ return;
237
+ if (entry.isDirectory()) {
238
+ await visitAsync(state, path.join(dir, entry.name), depth + 1, [...segments, entry.name]);
239
+ }
240
+ }
241
+ }
242
+ /** Async twin of {@link walkRolloutFiles} that yields between directories. */
243
+ export async function walkRolloutFilesAsync(roots, options = {}) {
244
+ const state = walkState(options);
245
+ for (const root of roots) {
246
+ if (state.cache)
247
+ await visitCached(state, state.cache, root, 0, []);
248
+ else
249
+ await visitAsync(state, root, 0, []);
250
+ }
251
+ return finish(state);
252
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared provider interface and types for session providers.
3
+ *
4
+ * SessionProviderBase is the expanded interface that both the VS Code extension
5
+ * and CLI can use. The VS Code extension extends it with vscode.Disposable and
6
+ * optional VS Code-specific methods.
7
+ *
8
+ * SessionProvider is the original simplified interface maintained for backward
9
+ * compatibility with existing CLI code.
10
+ */
11
+ /** Stable runtime list for consumers that must enumerate session providers. */
12
+ export { SESSION_PROVIDER_IDS } from '../providerIds';