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,257 @@
1
+ import { projectSessionTranscript } from '../transcript';
2
+ import { classifySessionActivity, refreshSessionActivityState, } from '../parsers/sessionActivityDetector';
3
+ import { computeSessionFileStats, providerContextSizeFn } from '../sessionStats';
4
+ import { fingerprintString, sessionFingerprintParts } from '../sessionFingerprint';
5
+ function evidence(provider, sessionId, sourcePath, event, eventIndex) {
6
+ return [
7
+ {
8
+ schemaVersion: 1,
9
+ provider,
10
+ sessionId,
11
+ sourcePath,
12
+ eventIndex,
13
+ eventId: event?.message?.id,
14
+ timestamp: event?.timestamp,
15
+ },
16
+ ];
17
+ }
18
+ function observed(value, provenance, confidence, refs) {
19
+ return { value, provenance, confidence, evidence: refs };
20
+ }
21
+ export function derivePendingUserRequestV1(provider, sessionId, sourcePath, events) {
22
+ for (let index = events.length - 1; index >= 0; index--) {
23
+ const event = events[index];
24
+ // A user/tool-result event after a request means the human has responded.
25
+ if (event.type === 'user')
26
+ return null;
27
+ const request = waitingToolRequest(event);
28
+ if (request) {
29
+ const refs = evidence(provider, sessionId, sourcePath, event, index);
30
+ return {
31
+ schemaVersion: 1,
32
+ id: `${provider}:${sessionId}:${request.id ?? event.message?.id ?? index}`,
33
+ provider,
34
+ sessionId,
35
+ kind: observed(request.kind, 'inferred', request.kind === 'question' ? 0.95 : 0.9, refs),
36
+ requestedAt: observed(event.timestamp, 'reported', 1, refs),
37
+ prompt: observed(request.prompt, request.prompt ? 'reported' : 'inferred', request.prompt ? 0.95 : 0.4, refs),
38
+ };
39
+ }
40
+ // A later assistant turn proves any older request was already resolved.
41
+ if (event.type === 'assistant' || event.type === 'tool_use')
42
+ return null;
43
+ }
44
+ return null;
45
+ }
46
+ function waitingToolRequest(event) {
47
+ const calls = [];
48
+ const message = event.message;
49
+ if (event.type === 'tool_use') {
50
+ calls.push({ id: message?.id, name: event.tool?.name, input: event.tool?.input });
51
+ }
52
+ if (Array.isArray(message?.content)) {
53
+ for (const block of message.content) {
54
+ if (!block || typeof block !== 'object')
55
+ continue;
56
+ const value = block;
57
+ if (value.type !== 'tool_use')
58
+ continue;
59
+ calls.push({
60
+ id: typeof value.id === 'string' ? value.id : undefined,
61
+ name: typeof value.name === 'string' ? value.name : undefined,
62
+ input: value.input,
63
+ });
64
+ }
65
+ }
66
+ for (let index = calls.length - 1; index >= 0; index--) {
67
+ const call = calls[index];
68
+ if (call.name !== 'AskUserQuestion' && call.name !== 'ExitPlanMode')
69
+ continue;
70
+ return {
71
+ id: call.id,
72
+ kind: call.name === 'AskUserQuestion' ? 'question' : 'tool_approval',
73
+ prompt: waitingToolPrompt(call.input, call.name),
74
+ };
75
+ }
76
+ return null;
77
+ }
78
+ function waitingToolPrompt(input, toolName) {
79
+ if (!input || typeof input !== 'object') {
80
+ return toolName === 'ExitPlanMode' ? 'Approve the proposed plan?' : null;
81
+ }
82
+ const record = input;
83
+ if (typeof record.question === 'string')
84
+ return record.question;
85
+ if (typeof record.prompt === 'string')
86
+ return record.prompt;
87
+ if (Array.isArray(record.questions)) {
88
+ const questions = record.questions
89
+ .map((question) => question && typeof question === 'object' && 'question' in question
90
+ ? String(question.question ?? '')
91
+ : '')
92
+ .filter(Boolean);
93
+ if (questions.length > 0)
94
+ return questions.join('\n');
95
+ }
96
+ if (toolName === 'ExitPlanMode')
97
+ return 'Approve the proposed plan?';
98
+ return null;
99
+ }
100
+ function providerCapabilities(provider, observationOnly) {
101
+ const refs = [];
102
+ const known = (value) => observed(value, 'inferred', 1, refs);
103
+ return {
104
+ schemaVersion: 1,
105
+ provider,
106
+ resume: known(!observationOnly),
107
+ forkLineage: known(provider !== 'opencode'),
108
+ quotaSource: known(provider === 'claude-code' ? 'api' : provider === 'codex' ? 'mixed' : 'none'),
109
+ assetExtraction: known(true),
110
+ };
111
+ }
112
+ /**
113
+ * Detector reason behind each observed session's activity, kept off the
114
+ * versioned V1 shape so cache refreshes can tell a grace-period "active" from
115
+ * a genuinely ongoing one.
116
+ */
117
+ const activityReasons = new WeakMap();
118
+ /** The activity detector's reason for a session produced by this adapter, if known. */
119
+ export function getObservedActivityReason(session) {
120
+ return activityReasons.get(session);
121
+ }
122
+ export function createProviderSessionAdapterV1(sessionProvider, options = {}) {
123
+ const pollIntervalMs = options.pollIntervalMs ?? 2_000;
124
+ const adapter = {
125
+ schemaVersion: 1,
126
+ provider: sessionProvider.id,
127
+ capabilities: providerCapabilities(sessionProvider.id, options.observationOnly === true),
128
+ async discover(cwd) {
129
+ return Promise.all(sessionProvider.findAllSessions(cwd).map((sessionPath) => adapter.read(sessionPath, cwd)));
130
+ },
131
+ async read(sessionPath, cwd = '') {
132
+ // One reader pass feeds stats, transcript, activity, and the pending
133
+ // request; the reader is flushed so a trailing partial line is included.
134
+ const sessionId = sessionProvider.getSessionId(sessionPath);
135
+ const reader = sessionProvider.createReader(sessionPath);
136
+ const events = reader.readAll();
137
+ reader.flush();
138
+ const stats = computeSessionFileStats(events, {
139
+ providerId: sessionProvider.id,
140
+ sessionId,
141
+ filePath: sessionPath,
142
+ label: '',
143
+ computeContextSize: providerContextSizeFn(sessionProvider),
144
+ });
145
+ const transcript = projectSessionTranscript(events, {
146
+ provider: sessionProvider.id,
147
+ sessionId,
148
+ sourcePath: sessionPath,
149
+ });
150
+ const refs = evidence(sessionProvider.id, sessionId, sessionPath);
151
+ const model = Object.entries(stats.modelUsage).sort((left, right) => right[1].calls - left[1].calls)[0]?.[0] ?? null;
152
+ const parts = sessionFingerprintParts(sessionProvider, sessionPath);
153
+ const activityResult = classifySessionActivity({ events, mtimeMs: parts?.mtimeMs ?? null });
154
+ const activity = activityResult.state === 'ongoing'
155
+ ? 'active'
156
+ : activityResult.state === 'stale'
157
+ ? 'idle'
158
+ : 'ended';
159
+ const allCostsReported = transcript.usage.costs.length > 0 &&
160
+ transcript.usage.costs.every((cost) => cost.source === 'provider-reported');
161
+ const observedAt = new Date().toISOString();
162
+ const session = {
163
+ schemaVersion: 1,
164
+ identity: {
165
+ provider: sessionProvider.id,
166
+ sessionId,
167
+ sourcePath: sessionPath,
168
+ },
169
+ cwd: observed(cwd, 'inferred', cwd ? 0.95 : 0.4, refs),
170
+ model: observed(model, model ? 'reported' : 'inferred', model ? 1 : 0.2, refs),
171
+ activity: observed(activity, 'inferred', 0.85, refs),
172
+ usage: {
173
+ inputTokens: observed(stats.tokens.input, 'reported', 1, refs),
174
+ outputTokens: observed(stats.tokens.output, 'reported', 1, refs),
175
+ cacheReadTokens: observed(stats.tokens.cacheRead, 'reported', 1, refs),
176
+ cacheWriteTokens: observed(stats.tokens.cacheWrite, 'reported', 1, refs),
177
+ normalized: observed(transcript.usage.totals, 'reported', 1, refs),
178
+ costUsd: observed(transcript.usage.totalCostUsd, allCostsReported
179
+ ? 'reported'
180
+ : transcript.usage.totalCostUsd === null
181
+ ? 'inferred'
182
+ : 'estimated', allCostsReported ? 1 : transcript.usage.totalCostUsd === null ? 0.5 : 0.9, refs),
183
+ },
184
+ pendingUserRequest: derivePendingUserRequestV1(sessionProvider.id, sessionId, sessionPath, events),
185
+ contentObservedAt: observedAt,
186
+ observedAt,
187
+ };
188
+ activityReasons.set(session, activityResult.reason);
189
+ return session;
190
+ },
191
+ watch(sessionPath, listener, cwd) {
192
+ let disposed = false;
193
+ let lastSignature = '';
194
+ let lastFingerprint = null;
195
+ let lastSession = null;
196
+ const emitIfChanged = (session) => {
197
+ const signature = JSON.stringify({
198
+ activity: session.activity,
199
+ model: session.model,
200
+ usage: session.usage,
201
+ pendingUserRequest: session.pendingUserRequest,
202
+ });
203
+ if (signature !== lastSignature) {
204
+ lastSignature = signature;
205
+ listener(session);
206
+ }
207
+ };
208
+ const update = async () => {
209
+ if (disposed)
210
+ return;
211
+ try {
212
+ const parts = sessionFingerprintParts(sessionProvider, sessionPath);
213
+ const fingerprint = parts ? fingerprintString(parts) : null;
214
+ if (lastSession && parts && fingerprint === lastFingerprint) {
215
+ // Unchanged content: only the activity can drift with time, so
216
+ // refresh it from the cached classification instead of re-reading.
217
+ const previousReason = activityReasons.get(lastSession);
218
+ const refreshed = refreshSessionActivityState(lastSession.activity.value, parts.mtimeMs, Date.now(), previousReason);
219
+ if (refreshed !== lastSession.activity.value) {
220
+ const session = {
221
+ ...lastSession,
222
+ activity: { ...lastSession.activity, value: refreshed },
223
+ observedAt: new Date().toISOString(),
224
+ };
225
+ activityReasons.set(session, refreshed === 'idle'
226
+ ? 'mtime-stale'
227
+ : previousReason === 'grace-period'
228
+ ? 'ending-event'
229
+ : 'no-activity-signal');
230
+ lastSession = session;
231
+ emitIfChanged(session);
232
+ }
233
+ return;
234
+ }
235
+ const session = await adapter.read(sessionPath, cwd);
236
+ lastFingerprint = fingerprint;
237
+ lastSession = session;
238
+ emitIfChanged(session);
239
+ }
240
+ catch {
241
+ // Observed sources can disappear between polling iterations.
242
+ }
243
+ };
244
+ void update();
245
+ const timer = setInterval(update, pollIntervalMs);
246
+ timer.unref?.();
247
+ return {
248
+ dispose: () => {
249
+ disposed = true;
250
+ clearInterval(timer);
251
+ },
252
+ };
253
+ },
254
+ dispose: () => sessionProvider.dispose(),
255
+ };
256
+ return adapter;
257
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Type definitions for OpenCode session storage format.
3
+ *
4
+ * OpenCode stores data as individual JSON files in ~/.local/share/opencode/storage/:
5
+ * - Sessions: storage/session/{projectID}/{sessionID}.json
6
+ * - Messages: storage/message/{sessionID}/{messageID}.json
7
+ * - Parts: storage/part/{messageID}/{partID}.json
8
+ * - Projects: storage/project/{projectID}.json
9
+ *
10
+ * @module types/opencode
11
+ */
12
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * On-disk schema types for plan persistence.
3
+ * Plans are extracted from agent sessions and persisted for analytics and history.
4
+ */
5
+ export const PLAN_SCHEMA_VERSION = 1;
6
+ /** Maximum number of plans to keep per project */
7
+ export const MAX_PLANS_PER_PROJECT = 50;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @fileoverview Canonical session event types shared across VS Code extension and CLI.
3
+ *
4
+ * These types define the rich event model used for parsing and monitoring
5
+ * Claude Code, OpenCode, and Codex CLI sessions. SessionEvent is the canonical
6
+ * type; ClaudeSessionEvent is a backward-compatible alias.
7
+ *
8
+ * @module types/sessionEvent
9
+ */
10
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * On-disk schema types for cross-session task persistence.
3
+ * Canonical source: sidekick-vscode/src/types/taskPersistence.ts + claudeSession.ts (TaskStatus)
4
+ */
5
+ export const TASK_PERSISTENCE_SCHEMA_VERSION = 1;
6
+ export function normalizeTaskStatus(status) {
7
+ if (status === 'in_progress')
8
+ return 'in_progress';
9
+ if (status === 'completed')
10
+ return 'completed';
11
+ return 'pending';
12
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Five-hour billing blocks computed from usage events.
3
+ *
4
+ * Mirrors the ccusage convention: a block opens at the first usage event
5
+ * (aligned down to the UTC hour), lasts five hours, and an event that lands
6
+ * after the block's end or more than five hours after the previous event opens
7
+ * a new block. The active block also reports a burn rate and an end-of-block
8
+ * projection. Browser-safe: no Node imports.
9
+ *
10
+ * @module usage/billingBlocks
11
+ */
12
+ import { classifyCostProvenance } from '../aggregation/costProvenance';
13
+ import { BurnRateCalculator } from '../statusline/BurnRateCalculator';
14
+ import { summarizeTokens } from '../tokenSummary';
15
+ /** Length of one billing block. */
16
+ export const BILLING_BLOCK_DURATION_MS = 5 * 3_600_000;
17
+ function toMs(value) {
18
+ if (value instanceof Date)
19
+ return value.getTime();
20
+ if (typeof value === 'number')
21
+ return value;
22
+ return Date.parse(value);
23
+ }
24
+ function normalizeInput(event) {
25
+ const timestamp = toMs(event.timestamp);
26
+ if (!Number.isFinite(timestamp))
27
+ return null;
28
+ const costUsd = typeof event.costUsd === 'number' && Number.isFinite(event.costUsd) ? event.costUsd : null;
29
+ return {
30
+ timestamp,
31
+ tokens: event.tokens,
32
+ costUsd,
33
+ costProvenance: costUsd === null ? 'unpriced' : (event.costProvenance ?? 'model-catalog'),
34
+ model: event.model ?? 'unknown',
35
+ };
36
+ }
37
+ function floorToUtcHour(ms) {
38
+ return ms - (ms % 3_600_000);
39
+ }
40
+ function buildBlock(events, start, durationMs, nowMs) {
41
+ const end = start + durationMs;
42
+ const first = events[0].timestamp;
43
+ const last = events[events.length - 1].timestamp;
44
+ const isActive = nowMs >= start && nowMs < end && nowMs - last < durationMs;
45
+ const tokens = { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 };
46
+ let total = 0;
47
+ let costUsd = 0;
48
+ let reportedCalls = 0;
49
+ let estimatedCalls = 0;
50
+ let unpricedCalls = 0;
51
+ const models = {};
52
+ const burn = new BurnRateCalculator(durationMs / 60_000);
53
+ for (const event of events) {
54
+ const summary = summarizeTokens(event.tokens);
55
+ tokens.inputTokens += summary.input;
56
+ tokens.outputTokens += summary.output;
57
+ tokens.cacheWriteTokens += summary.cacheWrite;
58
+ tokens.cacheReadTokens += summary.cacheRead;
59
+ total += summary.total;
60
+ burn.addEvent(summary.total, new Date(event.timestamp));
61
+ const model = models[event.model] ?? { calls: 0, tokens: 0, costUsd: 0 };
62
+ model.calls += 1;
63
+ model.tokens += summary.total;
64
+ if (event.costUsd !== null) {
65
+ costUsd += event.costUsd;
66
+ model.costUsd += event.costUsd;
67
+ if (event.costProvenance === 'provider-reported')
68
+ reportedCalls += 1;
69
+ else
70
+ estimatedCalls += 1;
71
+ }
72
+ else {
73
+ unpricedCalls += 1;
74
+ }
75
+ models[event.model] = model;
76
+ }
77
+ const rateAt = isActive ? nowMs : last;
78
+ const elapsedMs = Math.max(0, rateAt - first);
79
+ const burnRatePerMinute = burn.calculateBurnRate(new Date(rateAt));
80
+ const remainingMs = isActive ? Math.max(0, end - nowMs) : 0;
81
+ const projectedTokens = isActive
82
+ ? Math.round(total + burnRatePerMinute * (remainingMs / 60_000))
83
+ : total;
84
+ const costPerToken = total > 0 ? costUsd / total : 0;
85
+ const projectedCostUsd = isActive ? costUsd + costPerToken * (projectedTokens - total) : costUsd;
86
+ const startIso = new Date(start).toISOString();
87
+ return {
88
+ id: startIso,
89
+ start: startIso,
90
+ end: new Date(end).toISOString(),
91
+ firstEvent: new Date(first).toISOString(),
92
+ lastEvent: new Date(last).toISOString(),
93
+ isActive,
94
+ calls: events.length,
95
+ tokens: { ...tokens, total },
96
+ costUsd,
97
+ costProvenance: classifyCostProvenance({ reportedCalls, estimatedCalls, unpricedCalls }),
98
+ unpricedCalls,
99
+ models,
100
+ elapsedMs,
101
+ remainingMs,
102
+ burnRatePerMinute,
103
+ projectedTokens,
104
+ projectedCostUsd,
105
+ };
106
+ }
107
+ /**
108
+ * Group usage events into billing blocks, oldest first.
109
+ *
110
+ * Events with an unparseable timestamp are ignored. Events are sorted before
111
+ * grouping, so callers may pass them in any order.
112
+ */
113
+ export function computeBillingBlocks(events, options = {}) {
114
+ const durationMs = options.blockDurationMs ?? BILLING_BLOCK_DURATION_MS;
115
+ const alignToHour = options.alignToHour ?? true;
116
+ const nowMs = options.now === undefined ? Date.now() : toMs(options.now);
117
+ const sorted = events
118
+ .map(normalizeInput)
119
+ .filter((event) => event !== null)
120
+ .sort((a, b) => a.timestamp - b.timestamp);
121
+ const blocks = [];
122
+ let current = [];
123
+ let currentStart = 0;
124
+ for (const event of sorted) {
125
+ const previous = current[current.length - 1];
126
+ const opensNewBlock = !previous ||
127
+ event.timestamp >= currentStart + durationMs ||
128
+ event.timestamp - previous.timestamp > durationMs;
129
+ if (opensNewBlock) {
130
+ if (current.length > 0)
131
+ blocks.push(buildBlock(current, currentStart, durationMs, nowMs));
132
+ current = [];
133
+ currentStart = alignToHour ? floorToUtcHour(event.timestamp) : event.timestamp;
134
+ }
135
+ current.push(event);
136
+ }
137
+ if (current.length > 0)
138
+ blocks.push(buildBlock(current, currentStart, durationMs, nowMs));
139
+ return blocks;
140
+ }
141
+ /** The block that is still open at the calculator's `now`, if any. */
142
+ export function findActiveBillingBlock(blocks) {
143
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
144
+ if (blocks[index].isActive)
145
+ return blocks[index];
146
+ }
147
+ return null;
148
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Claude Code writes one API response as several JSONL lines — one per content
3
+ * block — and every line repeats that response's `message.usage`. Streaming
4
+ * also means early lines can carry a partial `output_tokens` (often 1) that a
5
+ * later line of the same `message.id` supersedes. Summing every line
6
+ * therefore roughly doubles a session's tokens and cost.
7
+ *
8
+ * The correct rule is "the last copy of a message's usage wins", but readers
9
+ * stream events and cannot retract what they already emitted. So the first
10
+ * line carries the usage as a normal call, identical repeats are stripped, and
11
+ * a repeat that grew emits only the growth as a `usageKind: 'correction'`
12
+ * top-up. Consumers add corrections to token and cost totals without counting
13
+ * another call or context sample.
14
+ *
15
+ * Browser-safe: no Node imports.
16
+ *
17
+ * @module usage/claudeUsageDedupe
18
+ */
19
+ import { normalizeProviderUsage } from '../usageNormalization';
20
+ /** Claude Code's placeholder model for locally generated (unbilled) messages. */
21
+ export const SYNTHETIC_MODEL = '<synthetic>';
22
+ const DEFAULT_MAX_KEYS = 4096;
23
+ /** Attach Anthropic-semantics normalized usage and Claude Code provenance to an event. */
24
+ export function normalizeClaudeUsage(event) {
25
+ const message = event.message;
26
+ const usage = message?.usage;
27
+ if (!message || !usage) {
28
+ return {
29
+ ...event,
30
+ providerMetadata: {
31
+ ...event.providerMetadata,
32
+ providerId: 'claude-code',
33
+ source: 'claude-code-jsonl',
34
+ },
35
+ };
36
+ }
37
+ return {
38
+ ...event,
39
+ message: {
40
+ ...message,
41
+ normalizedUsage: normalizeProviderUsage({
42
+ semantics: 'anthropic',
43
+ provider: 'anthropic',
44
+ source: 'claude-code-jsonl',
45
+ model: message.model,
46
+ inputTokens: usage.input_tokens,
47
+ outputTokens: usage.output_tokens,
48
+ cacheReadTokens: usage.cache_read_input_tokens,
49
+ cacheWriteTokens: usage.cache_creation_input_tokens,
50
+ reasoningTokens: usage.reasoning_tokens,
51
+ reasoningIncludedInOutput: false,
52
+ reportedCostUsd: usage.reported_cost,
53
+ }),
54
+ },
55
+ providerMetadata: {
56
+ ...event.providerMetadata,
57
+ providerId: 'claude-code',
58
+ source: 'claude-code-jsonl',
59
+ },
60
+ };
61
+ }
62
+ function bucketsOf(usage) {
63
+ return {
64
+ input: usage.uncachedInputTokens,
65
+ cacheRead: usage.cacheReadTokens,
66
+ cacheWrite: usage.cacheWriteTokens,
67
+ output: usage.outputTokens,
68
+ reasoning: usage.reasoningTokens,
69
+ };
70
+ }
71
+ function stripUsage(event) {
72
+ if (!event.message)
73
+ return event;
74
+ const message = { ...event.message };
75
+ delete message.usage;
76
+ delete message.normalizedUsage;
77
+ return { ...event, message };
78
+ }
79
+ /**
80
+ * Per-reader dedupe state. Split lines of one response are adjacent, so a
81
+ * bounded map (oldest keys evicted first) is enough.
82
+ */
83
+ export class ClaudeUsageDeduper {
84
+ maxKeys;
85
+ seen = new Map();
86
+ constructor(maxKeys = DEFAULT_MAX_KEYS) {
87
+ this.maxKeys = maxKeys;
88
+ }
89
+ /**
90
+ * Returns the event with its usage adjusted: unchanged for the first line of
91
+ * a message, stripped for an identical repeat or a `<synthetic>` message,
92
+ * and replaced by the growth (marked as a correction) for a repeat that grew.
93
+ * Events must already carry `message.normalizedUsage`.
94
+ */
95
+ apply(event) {
96
+ const message = event.message;
97
+ const usage = message?.normalizedUsage;
98
+ if (!message || !usage)
99
+ return event;
100
+ if (message.model === SYNTHETIC_MODEL)
101
+ return stripUsage(event);
102
+ const id = message.id;
103
+ if (!id)
104
+ return event;
105
+ const current = bucketsOf(usage);
106
+ const previous = this.seen.get(id);
107
+ if (!previous) {
108
+ this.remember(id, current);
109
+ return event;
110
+ }
111
+ const delta = {
112
+ input: Math.max(0, current.input - previous.input),
113
+ cacheRead: Math.max(0, current.cacheRead - previous.cacheRead),
114
+ cacheWrite: Math.max(0, current.cacheWrite - previous.cacheWrite),
115
+ output: Math.max(0, current.output - previous.output),
116
+ reasoning: Math.max(0, current.reasoning - previous.reasoning),
117
+ };
118
+ this.remember(id, {
119
+ input: previous.input + delta.input,
120
+ cacheRead: previous.cacheRead + delta.cacheRead,
121
+ cacheWrite: previous.cacheWrite + delta.cacheWrite,
122
+ output: previous.output + delta.output,
123
+ reasoning: previous.reasoning + delta.reasoning,
124
+ });
125
+ if (!delta.input &&
126
+ !delta.cacheRead &&
127
+ !delta.cacheWrite &&
128
+ !delta.output &&
129
+ !delta.reasoning) {
130
+ return stripUsage(event);
131
+ }
132
+ const cacheInclusiveInputTokens = delta.input + delta.cacheRead + delta.cacheWrite;
133
+ const billableOutputTokens = delta.output + (usage.reasoningIncludedInOutput ? 0 : delta.reasoning);
134
+ const normalizedUsage = {
135
+ uncachedInputTokens: delta.input,
136
+ cacheReadTokens: delta.cacheRead,
137
+ cacheWriteTokens: delta.cacheWrite,
138
+ outputTokens: delta.output,
139
+ reasoningTokens: delta.reasoning,
140
+ reasoningIncludedInOutput: usage.reasoningIncludedInOutput,
141
+ cacheInclusiveInputTokens,
142
+ billableOutputTokens,
143
+ totalTokens: cacheInclusiveInputTokens + billableOutputTokens,
144
+ ...(usage.model ? { model: usage.model } : {}),
145
+ provenance: usage.provenance,
146
+ };
147
+ const messageUsage = {
148
+ input_tokens: delta.input,
149
+ output_tokens: delta.output,
150
+ cache_creation_input_tokens: delta.cacheWrite,
151
+ cache_read_input_tokens: delta.cacheRead,
152
+ ...(delta.reasoning ? { reasoning_tokens: delta.reasoning } : {}),
153
+ };
154
+ return {
155
+ ...event,
156
+ message: { ...message, usage: messageUsage, normalizedUsage, usageKind: 'correction' },
157
+ };
158
+ }
159
+ /** Whether a message id has already been counted by this deduper. */
160
+ has(id) {
161
+ return this.seen.has(id);
162
+ }
163
+ reset() {
164
+ this.seen.clear();
165
+ }
166
+ remember(id, buckets) {
167
+ this.seen.delete(id);
168
+ this.seen.set(id, buckets);
169
+ if (this.seen.size > this.maxKeys) {
170
+ const oldest = this.seen.keys().next().value;
171
+ if (oldest !== undefined)
172
+ this.seen.delete(oldest);
173
+ }
174
+ }
175
+ }
176
+ /**
177
+ * Deduplicated normalized usage for one raw Claude Code JSONL assistant line,
178
+ * or null when the line carries none (or only repeats usage already counted).
179
+ */
180
+ export function dedupedRawClaudeUsage(raw, deduper) {
181
+ if (!raw || raw.type !== 'assistant' || !raw.message?.usage)
182
+ return null;
183
+ const event = deduper.apply(normalizeClaudeUsage({
184
+ type: 'assistant',
185
+ timestamp: raw.timestamp ?? '',
186
+ message: raw.message,
187
+ }));
188
+ return event.message?.normalizedUsage ?? null;
189
+ }