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,435 @@
1
+ /**
2
+ * JSONL session watcher for Claude Code and Codex.
3
+ * Uses byte-offset tracking + fs.watch with debounce + catch-up polling.
4
+ */
5
+ import { createJsonlTail } from './jsonlTail';
6
+ import { normalizeCodexToolName, resolveCodexCallUsage } from '../parsers/codexParser';
7
+ import { formatToolSummary } from '../formatters/toolSummary';
8
+ import { ClaudeUsageDeduper, normalizeClaudeUsage } from '../usage/claudeUsageDedupe';
9
+ // ── Normalizers ──
10
+ /**
11
+ * Split lines of one Claude response repeat its usage; run the raw usage
12
+ * through the shared deduper so each response is counted once (last copy wins).
13
+ */
14
+ function dedupedClaudeUsage(raw, deduper) {
15
+ if (!raw.message?.usage)
16
+ return { usage: undefined, usageKind: undefined };
17
+ const event = deduper.apply(normalizeClaudeUsage({
18
+ type: 'assistant',
19
+ timestamp: raw.timestamp || '',
20
+ message: raw.message,
21
+ }));
22
+ return { usage: event.message?.usage, usageKind: event.message?.usageKind };
23
+ }
24
+ function normalizeClaudeCodeEvent(raw, deduper) {
25
+ const events = [];
26
+ const ts = raw.timestamp || new Date().toISOString();
27
+ const permissionMode = raw.permissionMode;
28
+ const { usage, usageKind } = raw.type === 'assistant'
29
+ ? dedupedClaudeUsage(raw, deduper)
30
+ : { usage: undefined, usageKind: undefined };
31
+ const tokens = usage
32
+ ? { input: usage.input_tokens || 0, output: usage.output_tokens || 0 }
33
+ : undefined;
34
+ const cacheTokens = usage && (usage.cache_read_input_tokens || usage.cache_creation_input_tokens)
35
+ ? { read: usage.cache_read_input_tokens || 0, write: usage.cache_creation_input_tokens || 0 }
36
+ : undefined;
37
+ const cost = usage?.reported_cost;
38
+ const model = raw.message?.model;
39
+ if (raw.type === 'user') {
40
+ const content = raw.message?.content;
41
+ // Extract tool_result blocks (these carry TaskCreate results, etc.)
42
+ if (Array.isArray(content)) {
43
+ for (const block of content) {
44
+ if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') {
45
+ const resultText = typeof block.content === 'string' ? truncate(block.content, 120) : '';
46
+ events.push({
47
+ providerId: 'claude-code',
48
+ type: 'tool_result',
49
+ timestamp: ts,
50
+ summary: resultText || '(tool result)',
51
+ raw: block,
52
+ });
53
+ }
54
+ }
55
+ }
56
+ const text = extractTextContent(content);
57
+ if (text || events.length === 0) {
58
+ events.push({
59
+ providerId: 'claude-code',
60
+ type: 'user',
61
+ timestamp: ts,
62
+ summary: text || '(user message)',
63
+ model,
64
+ raw,
65
+ });
66
+ }
67
+ }
68
+ else if (raw.type === 'assistant') {
69
+ const content = raw.message?.content;
70
+ // Extract tool_use blocks first
71
+ if (Array.isArray(content)) {
72
+ for (const block of content) {
73
+ if (block.type === 'tool_use' && typeof block.name === 'string') {
74
+ const input = block.input
75
+ ? summarizeToolInput(block.name, block.input)
76
+ : '';
77
+ events.push({
78
+ providerId: 'claude-code',
79
+ type: 'tool_use',
80
+ timestamp: ts,
81
+ summary: input ? `${block.name} ${input}` : block.name,
82
+ toolName: block.name,
83
+ toolInput: input,
84
+ model,
85
+ raw: block,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ // Emit the assistant text (if any)
91
+ const text = extractTextContent(content);
92
+ if (text || events.length === 0) {
93
+ events.push({
94
+ providerId: 'claude-code',
95
+ type: 'assistant',
96
+ timestamp: ts,
97
+ summary: text || '(thinking...)',
98
+ tokens,
99
+ cacheTokens,
100
+ ...(usageKind ? { usageKind } : {}),
101
+ cost,
102
+ model,
103
+ raw,
104
+ });
105
+ }
106
+ else if (tokens) {
107
+ // Attach tokens to the last tool_use event if no separate text
108
+ const last = events[events.length - 1];
109
+ last.tokens = tokens;
110
+ last.cacheTokens = cacheTokens;
111
+ if (usageKind)
112
+ last.usageKind = usageKind;
113
+ last.cost = cost;
114
+ }
115
+ }
116
+ else if (raw.type === 'summary') {
117
+ events.push({
118
+ providerId: 'claude-code',
119
+ type: 'summary',
120
+ timestamp: ts,
121
+ summary: 'Context compacted',
122
+ raw,
123
+ });
124
+ }
125
+ else {
126
+ // system / result events
127
+ if (raw.type === 'result') {
128
+ events.push({
129
+ providerId: 'claude-code',
130
+ type: 'system',
131
+ timestamp: ts,
132
+ summary: 'Session ended',
133
+ raw,
134
+ });
135
+ }
136
+ }
137
+ // Propagate permission mode to all generated events
138
+ if (permissionMode) {
139
+ for (const e of events) {
140
+ e.permissionMode = permissionMode;
141
+ }
142
+ }
143
+ return events;
144
+ }
145
+ // normalizeCodexEvent is now a method on JsonlSessionWatcher for plan mode state tracking
146
+ // ── Helpers ──
147
+ function extractTextContent(content) {
148
+ if (!content)
149
+ return '';
150
+ if (typeof content === 'string')
151
+ return truncate(content, 200);
152
+ if (Array.isArray(content)) {
153
+ for (const block of content) {
154
+ if (block.type === 'text' && typeof block.text === 'string') {
155
+ return truncate(block.text, 200);
156
+ }
157
+ }
158
+ }
159
+ return '';
160
+ }
161
+ function extractPayloadContent(payload) {
162
+ const content = payload.content;
163
+ if (typeof content === 'string')
164
+ return truncate(content, 200);
165
+ if (Array.isArray(content)) {
166
+ for (const part of content) {
167
+ if (typeof part.text === 'string')
168
+ return truncate(part.text, 200);
169
+ }
170
+ }
171
+ return '';
172
+ }
173
+ function summarizeToolInput(toolName, input) {
174
+ return formatToolSummary(toolName, input);
175
+ }
176
+ function extractRateLimits(rl) {
177
+ const primary = rl.primary;
178
+ const secondary = rl.secondary;
179
+ if (!primary && !secondary)
180
+ return undefined;
181
+ return {
182
+ primary: primary
183
+ ? {
184
+ usedPercent: primary.used_percent || 0,
185
+ windowMinutes: primary.window_minutes || 0,
186
+ resetsAt: primary.resets_at || 0,
187
+ }
188
+ : undefined,
189
+ secondary: secondary
190
+ ? {
191
+ usedPercent: secondary.used_percent || 0,
192
+ windowMinutes: secondary.window_minutes || 0,
193
+ resetsAt: secondary.resets_at || 0,
194
+ }
195
+ : undefined,
196
+ };
197
+ }
198
+ function truncate(text, maxLen) {
199
+ const clean = text.replace(/\s+/g, ' ').trim();
200
+ return clean.length > maxLen ? clean.substring(0, maxLen - 3) + '...' : clean;
201
+ }
202
+ // ── Watcher ──
203
+ export class JsonlSessionWatcher {
204
+ providerId;
205
+ sessionPath;
206
+ callbacks;
207
+ tail;
208
+ startOffset;
209
+ codexInPlanMode = false;
210
+ claudeUsageDeduper = new ClaudeUsageDeduper();
211
+ /** Last Codex cumulative usage seen; repeated `token_count` events carry the same value. */
212
+ codexPrevTotal;
213
+ constructor(providerId, sessionPath, callbacks) {
214
+ this.providerId = providerId;
215
+ this.sessionPath = sessionPath;
216
+ this.callbacks = callbacks;
217
+ }
218
+ get isActive() {
219
+ return this.tail?.isActive ?? false;
220
+ }
221
+ /** Seek to a committed byte offset before starting replay. */
222
+ seekTo(position) {
223
+ this.startOffset = Math.max(0, position);
224
+ this.tail?.seekTo(this.startOffset);
225
+ }
226
+ /** Position after the last complete JSONL line. */
227
+ getPosition() {
228
+ return this.tail?.getOffset() ?? this.startOffset ?? 0;
229
+ }
230
+ start(replay) {
231
+ if (this.isActive)
232
+ return;
233
+ this.tail = createJsonlTail({
234
+ path: this.sessionPath,
235
+ startOffset: replay ? this.getPosition() : undefined,
236
+ startAtEnd: !replay,
237
+ onEvent: (event) => this.handleRawEvent(event),
238
+ onBatchComplete: () => this.callbacks.onBatchComplete?.(),
239
+ onError: (error) => this.callbacks.onError?.(error),
240
+ // The file starts over, so per-file state from the old content must go too.
241
+ onReset: () => {
242
+ this.codexInPlanMode = false;
243
+ this.claudeUsageDeduper.reset();
244
+ this.codexPrevTotal = undefined;
245
+ this.callbacks.onReset?.();
246
+ },
247
+ });
248
+ this.tail.start();
249
+ }
250
+ stop() {
251
+ this.tail?.stop();
252
+ this.codexInPlanMode = false;
253
+ this.claudeUsageDeduper.reset();
254
+ this.codexPrevTotal = undefined;
255
+ }
256
+ handleRawEvent(event) {
257
+ // For codex, raw events don't match RawSessionEvent shape exactly, but
258
+ // JsonlParser parses any JSON object. Cast to Record for codex normalizer.
259
+ const followEvents = this.providerId === 'codex'
260
+ ? this.normalizeCodexEvent(event)
261
+ : normalizeClaudeCodeEvent(event, this.claudeUsageDeduper);
262
+ for (const fe of followEvents) {
263
+ this.callbacks.onEvent(fe);
264
+ }
265
+ }
266
+ /** One call's usage from a token_count `info`, skipping duplicate events. */
267
+ codexCallUsage(info) {
268
+ const resolved = resolveCodexCallUsage(info, this.codexPrevTotal ?? null);
269
+ this.codexPrevTotal = resolved.total ?? undefined;
270
+ return resolved.usage;
271
+ }
272
+ normalizeCodexEvent(raw) {
273
+ const events = [];
274
+ const ts = raw.timestamp || new Date().toISOString();
275
+ const type = raw.type;
276
+ if (type === 'session_meta') {
277
+ events.push({
278
+ providerId: 'codex',
279
+ type: 'system',
280
+ timestamp: ts,
281
+ summary: `Session started in ${raw.payload?.cwd || '?'}`,
282
+ raw,
283
+ });
284
+ }
285
+ else if (type === 'turn_context') {
286
+ const payload = raw.payload;
287
+ if (payload?.model) {
288
+ events.push({
289
+ providerId: 'codex',
290
+ type: 'system',
291
+ timestamp: ts,
292
+ summary: `Model: ${payload.model}`,
293
+ model: payload.model,
294
+ raw,
295
+ });
296
+ }
297
+ }
298
+ else if (type === 'response_item') {
299
+ const p = raw.payload;
300
+ if (!p)
301
+ return events;
302
+ if (p.role === 'user') {
303
+ const text = extractPayloadContent(p);
304
+ events.push({
305
+ providerId: 'codex',
306
+ type: 'user',
307
+ timestamp: ts,
308
+ summary: text || '(user message)',
309
+ raw,
310
+ });
311
+ }
312
+ else if (p.role === 'assistant' || p.type === 'message') {
313
+ const text = extractPayloadContent(p);
314
+ events.push({
315
+ providerId: 'codex',
316
+ type: 'assistant',
317
+ timestamp: ts,
318
+ summary: text || '(thinking...)',
319
+ raw,
320
+ });
321
+ }
322
+ else if (p.type === 'function_call' || p.type === 'custom_tool_call') {
323
+ const rawName = p.name || 'unknown';
324
+ const name = normalizeCodexToolName(rawName);
325
+ let parsedArgs = {};
326
+ try {
327
+ parsedArgs = JSON.parse(p.arguments);
328
+ }
329
+ catch {
330
+ /* keep empty */
331
+ }
332
+ const args = typeof p.arguments === 'string' ? truncate(p.arguments, 80) : '';
333
+ events.push({
334
+ providerId: 'codex',
335
+ type: 'tool_use',
336
+ timestamp: ts,
337
+ summary: args ? `${name} ${args}` : name,
338
+ toolName: name,
339
+ toolInput: args,
340
+ raw: { input: parsedArgs },
341
+ });
342
+ }
343
+ else if (p.type === 'local_shell_call') {
344
+ const cmd = truncate(JSON.stringify(p.command ?? p.arguments ?? ''), 80);
345
+ events.push({
346
+ providerId: 'codex',
347
+ type: 'tool_use',
348
+ timestamp: ts,
349
+ summary: `Bash ${cmd}`,
350
+ toolName: 'Bash',
351
+ toolInput: cmd,
352
+ raw,
353
+ });
354
+ }
355
+ else if (p.type === 'function_call_output') {
356
+ events.push({
357
+ providerId: 'codex',
358
+ type: 'tool_result',
359
+ timestamp: ts,
360
+ summary: truncate(String(p.output ?? ''), 120),
361
+ raw,
362
+ });
363
+ }
364
+ }
365
+ else if (type === 'event_msg') {
366
+ const payload = raw.payload;
367
+ const evtType = payload?.type;
368
+ if (evtType === 'token_count') {
369
+ const info = payload?.info;
370
+ const usage = this.codexCallUsage(info);
371
+ const rl = payload?.rate_limits;
372
+ const rateLimits = rl ? extractRateLimits(rl) : undefined;
373
+ if (usage || rateLimits) {
374
+ events.push({
375
+ providerId: 'codex',
376
+ type: 'system',
377
+ timestamp: ts,
378
+ summary: usage
379
+ ? `Tokens: ${usage.input_tokens ?? 0} in / ${usage.output_tokens ?? 0} out`
380
+ : 'Rate limits updated',
381
+ // Codex input includes cached input; FollowEvent buckets are disjoint.
382
+ tokens: usage
383
+ ? {
384
+ input: Math.max(0, (usage.input_tokens || 0) - (usage.cached_input_tokens || 0)),
385
+ output: usage.output_tokens || 0,
386
+ }
387
+ : undefined,
388
+ cacheTokens: usage?.cached_input_tokens
389
+ ? { read: usage.cached_input_tokens, write: 0 }
390
+ : undefined,
391
+ rateLimits,
392
+ raw,
393
+ });
394
+ }
395
+ }
396
+ else if (evtType === 'task_started') {
397
+ const collaboration = payload?.collaboration_mode_kind;
398
+ if (collaboration === 'plan' && !this.codexInPlanMode) {
399
+ this.codexInPlanMode = true;
400
+ events.push({
401
+ providerId: 'codex',
402
+ type: 'tool_use',
403
+ timestamp: ts,
404
+ summary: 'EnterPlanMode',
405
+ toolName: 'EnterPlanMode',
406
+ raw: { input: { source: 'codex_task_started' } },
407
+ });
408
+ }
409
+ }
410
+ else if (evtType === 'task_complete') {
411
+ if (this.codexInPlanMode) {
412
+ this.codexInPlanMode = false;
413
+ events.push({
414
+ providerId: 'codex',
415
+ type: 'tool_use',
416
+ timestamp: ts,
417
+ summary: 'ExitPlanMode',
418
+ toolName: 'ExitPlanMode',
419
+ raw: { input: { source: 'codex_task_complete' } },
420
+ });
421
+ }
422
+ }
423
+ }
424
+ else if (type === 'compacted') {
425
+ events.push({
426
+ providerId: 'codex',
427
+ type: 'summary',
428
+ timestamp: ts,
429
+ summary: 'Context compacted',
430
+ raw,
431
+ });
432
+ }
433
+ return events;
434
+ }
435
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Session watcher backed by a provider SessionReader.
3
+ *
4
+ * This keeps live/replay FollowEvent consumers on the same canonical parser
5
+ * path used by provider-aware VS Code monitoring.
6
+ */
7
+ import * as fs from 'fs';
8
+ import { toFollowEvents } from './eventBridge';
9
+ const DEBOUNCE_MS = 100;
10
+ const CATCHUP_INTERVAL_MS = 30_000;
11
+ export class ProviderReaderSessionWatcher {
12
+ provider;
13
+ sessionPath;
14
+ callbacks;
15
+ _isActive = false;
16
+ fsWatcher = null;
17
+ catchupTimer = null;
18
+ debounceTimer = null;
19
+ reader;
20
+ constructor(provider, sessionPath, callbacks) {
21
+ this.provider = provider;
22
+ this.sessionPath = sessionPath;
23
+ this.callbacks = callbacks;
24
+ this.reader = provider.createReader(sessionPath);
25
+ }
26
+ get isActive() {
27
+ return this._isActive;
28
+ }
29
+ seekTo(position) {
30
+ this.reader.seekTo(position);
31
+ }
32
+ getPosition() {
33
+ return this.reader.getPosition();
34
+ }
35
+ start(replay) {
36
+ if (this._isActive)
37
+ return;
38
+ this._isActive = true;
39
+ if (!replay) {
40
+ this.seekToEnd();
41
+ }
42
+ else {
43
+ this.readNewEvents();
44
+ }
45
+ try {
46
+ this.fsWatcher = fs.watch(this.sessionPath, { persistent: false }, () => {
47
+ this.debouncedRead();
48
+ });
49
+ this.fsWatcher.on('error', (error) => {
50
+ this.fsWatcher?.close();
51
+ this.fsWatcher = null;
52
+ this.callbacks.onError?.(error);
53
+ });
54
+ }
55
+ catch {
56
+ // fs.watch may be unavailable; polling still catches up.
57
+ }
58
+ this.catchupTimer = setInterval(() => {
59
+ if (this._isActive)
60
+ this.readNewEvents();
61
+ }, CATCHUP_INTERVAL_MS);
62
+ }
63
+ stop() {
64
+ if (!this._isActive)
65
+ return;
66
+ this._isActive = false;
67
+ if (this.debounceTimer) {
68
+ clearTimeout(this.debounceTimer);
69
+ this.debounceTimer = null;
70
+ }
71
+ if (this.fsWatcher) {
72
+ this.fsWatcher.close();
73
+ this.fsWatcher = null;
74
+ }
75
+ if (this.catchupTimer) {
76
+ clearInterval(this.catchupTimer);
77
+ this.catchupTimer = null;
78
+ }
79
+ // Keep unfinished input buffered so a later start can resume losslessly.
80
+ }
81
+ seekToEnd() {
82
+ try {
83
+ // Consume existing complete records to retain parser context and any
84
+ // unfinished line, without emitting historical events.
85
+ this.reader.readNew();
86
+ }
87
+ catch {
88
+ this.reader.reset();
89
+ }
90
+ }
91
+ debouncedRead() {
92
+ if (this.debounceTimer)
93
+ clearTimeout(this.debounceTimer);
94
+ this.debounceTimer = setTimeout(() => {
95
+ this.readNewEvents();
96
+ }, DEBOUNCE_MS);
97
+ }
98
+ readNewEvents() {
99
+ if (!this._isActive)
100
+ return;
101
+ try {
102
+ if (this.reader.wasTruncated()) {
103
+ this.reader.reset();
104
+ this.callbacks.onReset?.();
105
+ }
106
+ const events = this.reader.readNew();
107
+ for (const event of events) {
108
+ for (const followEvent of toFollowEvents(event, this.provider.id)) {
109
+ this.callbacks.onEvent(followEvent);
110
+ }
111
+ }
112
+ this.callbacks.onBatchComplete?.();
113
+ }
114
+ catch (err) {
115
+ this.callbacks.onError?.(err instanceof Error ? err : new Error(String(err)));
116
+ }
117
+ }
118
+ }