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,86 @@
1
+ "use strict";
2
+ /**
3
+ * One vocabulary for what a tool call does, across Claude Code, Codex, and
4
+ * OpenCode tool names. Browser-safe: no Node imports.
5
+ *
6
+ * @module toolCategory
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.TOOL_CATEGORIES = void 0;
10
+ exports.classifyTool = classifyTool;
11
+ const mcpToolName_1 = require("./parsers/mcpToolName");
12
+ exports.TOOL_CATEGORIES = [
13
+ 'shell',
14
+ 'file_read',
15
+ 'file_edit',
16
+ 'search',
17
+ 'web',
18
+ 'subagent',
19
+ 'planning',
20
+ 'mcp',
21
+ 'other',
22
+ ];
23
+ /** Lower-cased tool names of every provider, by category. */
24
+ const CATEGORY_BY_NAME = new Map([
25
+ [
26
+ 'shell',
27
+ [
28
+ 'bash',
29
+ 'bashoutput',
30
+ 'killbash',
31
+ 'killshell',
32
+ 'exec_command',
33
+ 'shell',
34
+ 'local_shell',
35
+ 'local_shell_call',
36
+ 'container.exec',
37
+ 'write_stdin',
38
+ ],
39
+ ],
40
+ ['file_read', ['read', 'notebookread', 'ls', 'list', 'view_image']],
41
+ [
42
+ 'file_edit',
43
+ ['edit', 'multiedit', 'write', 'notebookedit', 'apply_patch', 'patch', 'patch_applied'],
44
+ ],
45
+ ['search', ['grep', 'glob', 'codesearch', 'toolsearch']],
46
+ ['web', ['webfetch', 'websearch', 'web_search', 'web_search_call', 'fetch']],
47
+ [
48
+ 'subagent',
49
+ [
50
+ 'task',
51
+ 'agent',
52
+ 'spawn_agent',
53
+ 'wait_agent',
54
+ 'send_message',
55
+ 'followup_task',
56
+ 'list_agents',
57
+ 'wait',
58
+ ],
59
+ ],
60
+ [
61
+ 'planning',
62
+ [
63
+ 'todowrite',
64
+ 'todoread',
65
+ 'update_plan',
66
+ 'updateplan',
67
+ 'enterplanmode',
68
+ 'exitplanmode',
69
+ 'taskcreate',
70
+ 'taskupdate',
71
+ 'tasklist',
72
+ 'askuserquestion',
73
+ 'request_user_input',
74
+ ],
75
+ ],
76
+ ].flatMap(([category, names]) => names.map((name) => [name, category])));
77
+ /**
78
+ * Classifies a tool name from any supported provider. `mcp__<server>__<tool>`
79
+ * names and calls with a known `mcpServer` are `mcp`; unknown names are
80
+ * `other`.
81
+ */
82
+ function classifyTool(name, options = {}) {
83
+ if (options.mcpServer || (0, mcpToolName_1.parseMcpToolName)(name))
84
+ return 'mcp';
85
+ return CATEGORY_BY_NAME.get(name.trim().toLowerCase()) ?? 'other';
86
+ }
@@ -291,4 +291,8 @@ export interface CodexDbThread {
291
291
  source?: string;
292
292
  agent_nickname?: string | null;
293
293
  agent_role?: string | null;
294
+ /** 1 once the thread is archived; Codex then moves its rollout to `archived_sessions/`. */
295
+ archived?: number;
296
+ /** Unix seconds when the thread was archived. */
297
+ archived_at?: number | null;
294
298
  }
@@ -50,6 +50,8 @@ export interface UsageEventRecord {
50
50
  };
51
51
  costUsd: number | null;
52
52
  costProvenance: PricingProvenance;
53
+ /** Session file the event came from; tells apart two files with one session id. */
54
+ filePath?: string;
53
55
  }
54
56
  /** One session that contributed events. */
55
57
  export interface UsageSessionRecord {
@@ -71,6 +73,18 @@ export interface CollectUsageEventsOptions {
71
73
  until?: Date | string;
72
74
  /** Restrict to one workspace's sessions. */
73
75
  workspacePath?: string;
76
+ /**
77
+ * Restrict to the sessions of several workspaces, listed once each and
78
+ * counted once even when roots overlap. Takes precedence over
79
+ * `workspacePath`. Roots that do not resolve are dropped; when none
80
+ * resolves, no session is read.
81
+ */
82
+ workspacePaths?: string[];
83
+ /**
84
+ * Also list each root's main checkout and git worktree siblings. Default
85
+ * true with `workspacePaths`, false with `workspacePath`.
86
+ */
87
+ includeWorktrees?: boolean;
74
88
  /** Preview-read concurrency (see `listSessionPreviewsAsync`). */
75
89
  concurrency?: number;
76
90
  /** Cache directory (default `<configDir>/usage-cache`). */
@@ -93,6 +107,8 @@ export interface CollectUsageEventsResult {
93
107
  diagnostics: SessionProviderDiagnostic[];
94
108
  cacheHits: number;
95
109
  cacheMisses: number;
110
+ /** Realpaths of the roots listed, after worktree expansion, when the call was scoped. */
111
+ roots?: string[];
96
112
  }
97
113
  export declare function getUsageCacheDir(): string;
98
114
  /** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
@@ -64,6 +64,7 @@ const paths_1 = require("../paths");
64
64
  const sessionPreviews_1 = require("../sessionPreviews");
65
65
  const usageNormalization_1 = require("../usageNormalization");
66
66
  const atomic_1 = require("../writers/atomic");
67
+ const workspaceRoots_1 = require("../workspaceRoots");
67
68
  /**
68
69
  * Bump when the cached record shape or the counting rules change; older files
69
70
  * are re-read. v2: Claude split lines and duplicate Codex `token_count` events
@@ -162,7 +163,7 @@ function cachedEventFromUsage(usage, timestamp, model) {
162
163
  return record;
163
164
  }
164
165
  /** Price a cached event with the current catalog (or its provider-reported cost). */
165
- function priceCachedEvent(cached, provider, sessionId, project) {
166
+ function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
166
167
  const usage = {
167
168
  uncachedInputTokens: cached.i,
168
169
  cacheReadTokens: cached.cr,
@@ -195,6 +196,7 @@ function priceCachedEvent(cached, provider, sessionId, project) {
195
196
  },
196
197
  costUsd: priced.costUsd,
197
198
  costProvenance: priced.source,
199
+ filePath: sessionFile,
198
200
  };
199
201
  }
200
202
  const CLAUDE_AGENT_FILE = /^agent-.+\.jsonl$/;
@@ -292,12 +294,8 @@ async function collectUsageEvents(options) {
292
294
  const useCache = !options.noCache;
293
295
  const signal = options.signal;
294
296
  signal?.throwIfAborted();
295
- const listed = await (0, sessionPreviews_1.listSessionPreviewsAsync)(options.providers, {
296
- since: options.since,
297
- workspacePath: options.workspacePath,
298
- limit: Number.MAX_SAFE_INTEGER,
299
- concurrency: options.concurrency,
300
- });
297
+ const scope = usageScope(options);
298
+ const listed = await listScopedPreviews(options, scope?.listingRoots);
301
299
  signal?.throwIfAborted();
302
300
  const diagnostics = [...listed.diagnostics];
303
301
  for (const diagnostic of listed.diagnostics)
@@ -373,7 +371,7 @@ async function collectUsageEvents(options) {
373
371
  continue;
374
372
  if (untilMs !== null && cached.t > untilMs)
375
373
  continue;
376
- events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project));
374
+ events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project, preview.filePath));
377
375
  first = first === null ? cached.t : Math.min(first, cached.t);
378
376
  last = last === null ? cached.t : Math.max(last, cached.t);
379
377
  count += 1;
@@ -393,5 +391,51 @@ async function collectUsageEvents(options) {
393
391
  if (wroteCache)
394
392
  pruneUsageCache(exports.MAX_USAGE_CACHE_FILES, cacheDir);
395
393
  events.sort((a, b) => a.timestamp - b.timestamp);
396
- return { events, sessions, diagnostics, cacheHits, cacheMisses };
394
+ return {
395
+ events,
396
+ sessions,
397
+ diagnostics,
398
+ cacheHits,
399
+ cacheMisses,
400
+ ...(scope ? { roots: scope.roots } : {}),
401
+ };
402
+ }
403
+ /** The roots to list, or undefined for an unscoped (or legacy single-root) call. */
404
+ function usageScope(options) {
405
+ if (options.workspacePaths) {
406
+ return (0, workspaceRoots_1.resolveWorkspaceRoots)(options.workspacePaths, {
407
+ includeWorktrees: options.includeWorktrees !== false,
408
+ });
409
+ }
410
+ if (options.workspacePath && options.includeWorktrees === true) {
411
+ return (0, workspaceRoots_1.resolveWorkspaceRoots)([options.workspacePath], { includeWorktrees: true });
412
+ }
413
+ return undefined;
414
+ }
415
+ /** Lists previews once per root and keeps each session file once. */
416
+ async function listScopedPreviews(options, listingRoots) {
417
+ const list = (workspacePath) => (0, sessionPreviews_1.listSessionPreviewsAsync)(options.providers, {
418
+ since: options.since,
419
+ workspacePath,
420
+ limit: Number.MAX_SAFE_INTEGER,
421
+ concurrency: options.concurrency,
422
+ });
423
+ if (!listingRoots)
424
+ return list(options.workspacePath);
425
+ const byFile = new Map();
426
+ const diagnostics = new Map();
427
+ for (const root of listingRoots) {
428
+ options.signal?.throwIfAborted();
429
+ const listed = await list(root);
430
+ // Each root lists every provider again; report each problem once.
431
+ for (const diagnostic of listed.diagnostics) {
432
+ diagnostics.set(JSON.stringify(diagnostic), diagnostic);
433
+ }
434
+ for (const preview of listed.previews) {
435
+ if (!byFile.has(preview.filePath))
436
+ byFile.set(preview.filePath, preview);
437
+ }
438
+ }
439
+ const previews = [...byFile.values()].sort((left, right) => Date.parse(right.modifiedAt) - Date.parse(left.modifiedAt));
440
+ return { previews, diagnostics: [...diagnostics.values()] };
397
441
  }
@@ -3,11 +3,30 @@
3
3
  * their own aggregation lifecycle.
4
4
  */
5
5
  import type { ZodType } from 'zod';
6
+ /** Default bytes read from the file per chunk. */
7
+ export declare const DEFAULT_JSONL_TAIL_READ_BYTES: number;
8
+ /** Default longest line parsed; longer lines are skipped and reported. */
9
+ export declare const DEFAULT_JSONL_TAIL_MAX_LINE_BYTES: number;
6
10
  export interface JsonlTailBatch {
7
11
  bytesRead: number;
8
12
  eventsRead: number;
9
13
  offset: number;
10
14
  }
15
+ /** Where an event's line sits in the file. */
16
+ export interface JsonlTailLine {
17
+ /** Byte offset where the line starts. */
18
+ offset: number;
19
+ /** Line length in bytes, without the newline. */
20
+ length: number;
21
+ }
22
+ export type JsonlTailResetReason = 'truncated' | 'rewritten' | 'replaced';
23
+ /** The tail started over from the beginning of the file. */
24
+ export interface JsonlTailReset {
25
+ /** `truncated`: the file shrank; `rewritten`: its first bytes or the resume point changed; `replaced`: another file now has the path. */
26
+ reason: JsonlTailResetReason;
27
+ /** The committed offset before the reset. */
28
+ previousOffset: number;
29
+ }
11
30
  export interface JsonlTailOptions<T> {
12
31
  path: string;
13
32
  schema?: ZodType<T>;
@@ -15,9 +34,15 @@ export interface JsonlTailOptions<T> {
15
34
  startAtEnd?: boolean;
16
35
  debounceMs?: number;
17
36
  catchupIntervalMs?: number;
18
- onEvent: (event: T) => void;
37
+ onEvent: (event: T, line: JsonlTailLine) => void;
19
38
  onBatchComplete?: (batch: JsonlTailBatch) => void;
20
39
  onError?: (error: Error, line?: string) => void;
40
+ /** Called after the tail resets to offset 0; events from the start follow. */
41
+ onReset?: (reset: JsonlTailReset) => void;
42
+ /** Bytes read per chunk (default 1 MiB); one poll reads chunk by chunk to the end. */
43
+ maxReadBytes?: number;
44
+ /** Longest line parsed (default 16 MiB); a longer line is skipped and reported to `onError`. */
45
+ maxLineBytes?: number;
21
46
  }
22
47
  export interface JsonlTail {
23
48
  readonly isActive: boolean;
@@ -37,11 +37,17 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  };
38
38
  })();
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = exports.DEFAULT_JSONL_TAIL_READ_BYTES = void 0;
40
41
  exports.createJsonlTail = createJsonlTail;
41
42
  const fs = __importStar(require("fs"));
43
+ const fileHead_1 = require("../fileHead");
42
44
  const jsonl_1 = require("../parsers/jsonl");
43
45
  const DEFAULT_DEBOUNCE_MS = 100;
44
46
  const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
47
+ /** Default bytes read from the file per chunk. */
48
+ exports.DEFAULT_JSONL_TAIL_READ_BYTES = 1024 * 1024;
49
+ /** Default longest line parsed; longer lines are skipped and reported. */
50
+ exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = 16 * 1024 * 1024;
45
51
  /** Skip old complete events while retaining an in-progress final line. */
46
52
  function lastCompleteLineOffset(filePath) {
47
53
  const fd = fs.openSync(filePath, 'r');
@@ -73,6 +79,15 @@ class JsonlTailReader {
73
79
  /** Resume-safe byte position ending immediately after a newline. */
74
80
  committedOffset;
75
81
  pendingBytes = Buffer.alloc(0);
82
+ /** Inside a line over `maxLineBytes`: drop bytes until its newline. */
83
+ skippingLine = false;
84
+ currentLine = { offset: 0, length: 0 };
85
+ /** Identity and prefix of the file the offsets refer to, once read. */
86
+ identity = null;
87
+ head = null;
88
+ lastMtimeMs = -1;
89
+ maxReadBytes;
90
+ maxLineBytes;
76
91
  fsWatcher = null;
77
92
  debounceTimer = null;
78
93
  catchupTimer = null;
@@ -82,10 +97,12 @@ class JsonlTailReader {
82
97
  this.options = options;
83
98
  this.readOffset = options.startOffset ?? 0;
84
99
  this.committedOffset = this.readOffset;
100
+ this.maxReadBytes = positiveInteger(options.maxReadBytes, exports.DEFAULT_JSONL_TAIL_READ_BYTES);
101
+ this.maxLineBytes = positiveInteger(options.maxLineBytes, exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES);
85
102
  this.parser = new jsonl_1.JsonlParser({
86
103
  onEvent: (event) => {
87
104
  this.eventsInCurrentBatch += 1;
88
- this.options.onEvent(event);
105
+ this.options.onEvent(event, this.currentLine);
89
106
  },
90
107
  onError: (error, line) => this.options.onError?.(error, line),
91
108
  }, { schema: options.schema });
@@ -137,36 +154,25 @@ class JsonlTailReader {
137
154
  readNow() {
138
155
  let fd = null;
139
156
  try {
140
- const stat = fs.statSync(this.options.path);
141
- if (stat.size < this.readOffset) {
142
- this.readOffset = 0;
143
- this.committedOffset = 0;
144
- this.pendingBytes = Buffer.alloc(0);
145
- this.parser.reset();
146
- }
147
- if (stat.size <= this.readOffset)
148
- return;
149
- const bytesToRead = stat.size - this.readOffset;
150
- const buffer = Buffer.alloc(bytesToRead);
151
157
  fd = fs.openSync(this.options.path, 'r');
152
- const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, this.readOffset);
153
- fs.closeSync(fd);
154
- fd = null;
155
- if (bytesRead <= 0)
158
+ const stat = fs.fstatSync(fd);
159
+ this.checkContinuity(fd, stat);
160
+ if (stat.size <= this.readOffset)
156
161
  return;
157
- this.readOffset += bytesRead;
158
162
  this.eventsInCurrentBatch = 0;
159
- const combined = Buffer.concat([this.pendingBytes, buffer.subarray(0, bytesRead)]);
160
- const lastNewline = combined.lastIndexOf(0x0a);
161
- if (lastNewline >= 0) {
162
- const completeLines = combined.subarray(0, lastNewline + 1);
163
- this.pendingBytes = Buffer.from(combined.subarray(lastNewline + 1));
164
- this.parser.processChunk(completeLines.toString('utf8'));
165
- }
166
- else {
167
- this.pendingBytes = combined;
163
+ let bytesRead = 0;
164
+ const chunk = Buffer.alloc(Math.min(this.maxReadBytes, stat.size - this.readOffset));
165
+ while (this.readOffset < stat.size) {
166
+ const want = Math.min(chunk.length, stat.size - this.readOffset);
167
+ const count = fs.readSync(fd, chunk, 0, want, this.readOffset);
168
+ if (count <= 0)
169
+ break;
170
+ this.consume(chunk.subarray(0, count), this.readOffset);
171
+ this.readOffset += count;
172
+ bytesRead += count;
168
173
  }
169
- this.committedOffset = this.readOffset - this.pendingBytes.length;
174
+ if (bytesRead <= 0)
175
+ return;
170
176
  this.options.onBatchComplete?.({
171
177
  bytesRead,
172
178
  eventsRead: this.eventsInCurrentBatch,
@@ -174,6 +180,9 @@ class JsonlTailReader {
174
180
  });
175
181
  }
176
182
  catch (error) {
183
+ this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
184
+ }
185
+ finally {
177
186
  if (fd !== null) {
178
187
  try {
179
188
  fs.closeSync(fd);
@@ -182,9 +191,97 @@ class JsonlTailReader {
182
191
  /* ignore close errors */
183
192
  }
184
193
  }
185
- this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
186
194
  }
187
195
  }
196
+ /**
197
+ * Starts over when the path now names another file, the file shrank, its
198
+ * first bytes changed, or the resume point no longer follows a newline.
199
+ */
200
+ checkContinuity(fd, stat) {
201
+ let reason = null;
202
+ if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
203
+ reason = 'replaced';
204
+ }
205
+ else if (stat.size < this.readOffset) {
206
+ reason = 'truncated';
207
+ }
208
+ if (reason === null && this.identity && stat.mtimeMs !== this.lastMtimeMs) {
209
+ const prefix = readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size));
210
+ const before = this.committedOffset > 0 ? readAt(fd, this.committedOffset - 1, 1) : null;
211
+ if ((this.head && !(0, fileHead_1.headMatches)(this.head, prefix)) || (before && before[0] !== 0x0a)) {
212
+ reason = 'rewritten';
213
+ }
214
+ }
215
+ if (reason !== null)
216
+ this.reset(reason);
217
+ this.identity = { dev: stat.dev, ino: stat.ino };
218
+ this.lastMtimeMs = stat.mtimeMs;
219
+ if (!this.head || this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size)) {
220
+ this.head = (0, fileHead_1.fileHeadOf)(readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size)));
221
+ }
222
+ }
223
+ reset(reason) {
224
+ const previousOffset = this.committedOffset;
225
+ this.readOffset = 0;
226
+ this.committedOffset = 0;
227
+ this.pendingBytes = Buffer.alloc(0);
228
+ this.skippingLine = false;
229
+ this.head = null;
230
+ this.parser.reset();
231
+ if (reason === 'replaced' && this.active) {
232
+ // fs.watch follows the old inode; watch the file now at the path.
233
+ this.fsWatcher?.close();
234
+ this.fsWatcher = null;
235
+ this.watchFile();
236
+ }
237
+ this.options.onReset?.({ reason, previousOffset });
238
+ }
239
+ /** Parses the complete lines in `bytes`, which start at file offset `position`. */
240
+ consume(bytes, position) {
241
+ let start = 0;
242
+ if (this.skippingLine) {
243
+ const newline = bytes.indexOf(0x0a);
244
+ if (newline < 0)
245
+ return;
246
+ this.skippingLine = false;
247
+ start = newline + 1;
248
+ this.committedOffset = position + start;
249
+ }
250
+ let lineStart = this.committedOffset;
251
+ let cursor = start;
252
+ for (;;) {
253
+ const newline = bytes.indexOf(0x0a, cursor);
254
+ if (newline < 0)
255
+ break;
256
+ const piece = bytes.subarray(cursor, newline);
257
+ const line = this.pendingBytes.length > 0 ? Buffer.concat([this.pendingBytes, piece]) : piece;
258
+ this.pendingBytes = Buffer.alloc(0);
259
+ const lineEnd = position + newline + 1;
260
+ if (line.length > this.maxLineBytes) {
261
+ this.reportOversized(lineStart, line.length);
262
+ }
263
+ else {
264
+ this.currentLine = { offset: lineStart, length: line.length };
265
+ this.parser.processChunk(line.toString('utf8') + '\n');
266
+ }
267
+ this.committedOffset = lineEnd;
268
+ lineStart = lineEnd;
269
+ cursor = newline + 1;
270
+ }
271
+ const rest = bytes.subarray(cursor);
272
+ if (rest.length === 0)
273
+ return;
274
+ if (this.pendingBytes.length + rest.length > this.maxLineBytes) {
275
+ this.reportOversized(lineStart, this.pendingBytes.length + rest.length);
276
+ this.pendingBytes = Buffer.alloc(0);
277
+ this.skippingLine = true;
278
+ return;
279
+ }
280
+ this.pendingBytes = Buffer.concat([this.pendingBytes, rest]);
281
+ }
282
+ reportOversized(offset, atLeast) {
283
+ this.options.onError?.(new Error(`JSONL line at byte ${offset} is over ${this.maxLineBytes} bytes (${atLeast} so far); skipped.`));
284
+ }
188
285
  getOffset() {
189
286
  return this.committedOffset;
190
287
  }
@@ -192,6 +289,7 @@ class JsonlTailReader {
192
289
  this.readOffset = Math.max(0, offset);
193
290
  this.committedOffset = this.readOffset;
194
291
  this.pendingBytes = Buffer.alloc(0);
292
+ this.skippingLine = false;
195
293
  this.parser.reset();
196
294
  }
197
295
  watchFile() {
@@ -216,3 +314,13 @@ class JsonlTailReader {
216
314
  this.debounceTimer = setTimeout(() => this.readNow(), debounceMs);
217
315
  }
218
316
  }
317
+ function positiveInteger(value, fallback) {
318
+ return typeof value === 'number' && Number.isFinite(value) && value >= 1
319
+ ? Math.floor(value)
320
+ : fallback;
321
+ }
322
+ function readAt(fd, position, length) {
323
+ const buffer = Buffer.alloc(length);
324
+ const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
325
+ return buffer.subarray(0, count);
326
+ }
@@ -240,6 +240,13 @@ class JsonlSessionWatcher {
240
240
  onEvent: (event) => this.handleRawEvent(event),
241
241
  onBatchComplete: () => this.callbacks.onBatchComplete?.(),
242
242
  onError: (error) => this.callbacks.onError?.(error),
243
+ // The file starts over, so per-file state from the old content must go too.
244
+ onReset: () => {
245
+ this.codexInPlanMode = false;
246
+ this.claudeUsageDeduper.reset();
247
+ this.codexPrevTotal = undefined;
248
+ this.callbacks.onReset?.();
249
+ },
243
250
  });
244
251
  this.tail.start();
245
252
  }
@@ -137,6 +137,7 @@ class ProviderReaderSessionWatcher {
137
137
  try {
138
138
  if (this.reader.wasTruncated()) {
139
139
  this.reader.reset();
140
+ this.callbacks.onReset?.();
140
141
  }
141
142
  const events = this.reader.readNew();
142
143
  for (const event of events) {
@@ -54,6 +54,12 @@ export interface SessionWatcherCallbacks {
54
54
  /** All events in this read have been delivered; the cursor can now be checkpointed. */
55
55
  onBatchComplete?: () => void;
56
56
  onError?: (error: Error) => void;
57
+ /**
58
+ * The session file was truncated, rewritten, or replaced, and its events
59
+ * are being delivered again from the beginning: discard anything built
60
+ * from earlier events.
61
+ */
62
+ onReset?: () => void;
57
63
  }
58
64
  export interface SessionWatcher {
59
65
  /** Start watching. If replay=true, emit existing events before streaming. */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Workspace-root expansion shared by the session readers: realpath each root
3
+ * and, optionally, add its git worktree siblings.
4
+ *
5
+ * @module workspaceRoots
6
+ */
7
+ export interface ResolveWorkspaceRootsOptions {
8
+ /** Add the main checkout and every git worktree of each root. Default true. */
9
+ includeWorktrees?: boolean;
10
+ }
11
+ export interface ResolvedWorkspaceRoots {
12
+ /** Realpaths of the roots, after worktree expansion; scope checks use these. */
13
+ roots: string[];
14
+ /** The roots plus their unresolved spellings, for listing session files. */
15
+ listingRoots: string[];
16
+ }
17
+ /** Realpath of an absolute path, or null when it does not resolve. */
18
+ export declare function realpathOrNull(input: string, cache?: Map<string, string | null>): string | null;
19
+ /**
20
+ * Realpaths each absolute root and, by default, adds its main checkout and
21
+ * git worktree siblings. Relative and unresolvable roots are dropped.
22
+ */
23
+ export declare function resolveWorkspaceRoots(workspacePaths: string[], options?: ResolveWorkspaceRootsOptions, realpaths?: Map<string, string | null>): ResolvedWorkspaceRoots;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace-root expansion shared by the session readers: realpath each root
4
+ * and, optionally, add its git worktree siblings.
5
+ *
6
+ * @module workspaceRoots
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.realpathOrNull = realpathOrNull;
43
+ exports.resolveWorkspaceRoots = resolveWorkspaceRoots;
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const sessionPathResolver_1 = require("./parsers/sessionPathResolver");
47
+ /** Realpath of an absolute path, or null when it does not resolve. */
48
+ function realpathOrNull(input, cache = new Map()) {
49
+ const cached = cache.get(input);
50
+ if (cached !== undefined)
51
+ return cached;
52
+ let resolved = null;
53
+ try {
54
+ resolved = path.isAbsolute(input) ? fs.realpathSync.native(input) : null;
55
+ }
56
+ catch {
57
+ resolved = null;
58
+ }
59
+ cache.set(input, resolved);
60
+ return resolved;
61
+ }
62
+ /**
63
+ * Realpaths each absolute root and, by default, adds its main checkout and
64
+ * git worktree siblings. Relative and unresolvable roots are dropped.
65
+ */
66
+ function resolveWorkspaceRoots(workspacePaths, options = {}, realpaths = new Map()) {
67
+ const includeWorktrees = options.includeWorktrees !== false;
68
+ const roots = new Set();
69
+ const listingRoots = new Set();
70
+ const add = (candidate) => {
71
+ const real = realpathOrNull(candidate, realpaths);
72
+ if (!real)
73
+ return;
74
+ roots.add(real);
75
+ listingRoots.add(real);
76
+ listingRoots.add(path.resolve(candidate));
77
+ };
78
+ for (const workspacePath of workspacePaths) {
79
+ if (typeof workspacePath !== 'string' || !path.isAbsolute(workspacePath))
80
+ continue;
81
+ add(workspacePath);
82
+ if (!includeWorktrees)
83
+ continue;
84
+ const real = realpathOrNull(workspacePath, realpaths);
85
+ if (!real)
86
+ continue;
87
+ const mainRepo = (0, sessionPathResolver_1.resolveWorktreeMainRepo)(real) ?? real;
88
+ add(mainRepo);
89
+ for (const sibling of (0, sessionPathResolver_1.discoverWorktreeSiblings)(mainRepo))
90
+ add(sibling);
91
+ }
92
+ return { roots: [...roots], listingRoots: [...listingRoots] };
93
+ }