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,197 @@
1
+ /**
2
+ * Multi-mode filter engine for session events.
3
+ *
4
+ * Supports 4 filter modes: substring, fuzzy, regex, and date range.
5
+ * Provides both filtering and match highlighting for CLI (blessed) and VS Code (HTML).
6
+ *
7
+ * @module search/advancedFilter
8
+ */
9
+ export class FilterEngine {
10
+ mode = 'substring';
11
+ query = '';
12
+ since;
13
+ until;
14
+ // Cached compiled regex
15
+ compiledRegex = null;
16
+ regexError = null;
17
+ // Cached fuzzy words
18
+ fuzzyWords = [];
19
+ /** Set the active filter mode. */
20
+ setMode(mode) {
21
+ this.mode = mode;
22
+ this.recompile();
23
+ }
24
+ /** Set the query string (used by substring, fuzzy, regex modes). */
25
+ setQuery(text) {
26
+ this.query = text;
27
+ this.recompile();
28
+ }
29
+ /** Set date range bounds (used by date mode). */
30
+ setDateRange(since, until) {
31
+ this.since = since;
32
+ this.until = until;
33
+ }
34
+ /** Get the current filter state. */
35
+ getState() {
36
+ return { mode: this.mode, query: this.query, since: this.since, until: this.until };
37
+ }
38
+ /** Whether the current regex is valid (always true for non-regex modes). */
39
+ get isValid() {
40
+ if (this.mode !== 'regex')
41
+ return true;
42
+ return this.regexError === null;
43
+ }
44
+ /** Regex compilation error, or null if valid. */
45
+ get error() {
46
+ return this.mode === 'regex' ? this.regexError : null;
47
+ }
48
+ /** Whether the filter has an active query. */
49
+ get isActive() {
50
+ if (this.mode === 'date')
51
+ return !!(this.since || this.until);
52
+ return this.query.length > 0;
53
+ }
54
+ /** Test whether a FollowEvent matches the current filter. */
55
+ matches(event) {
56
+ if (!this.isActive)
57
+ return true;
58
+ switch (this.mode) {
59
+ case 'substring':
60
+ return this.matchSubstring(event.summary);
61
+ case 'fuzzy':
62
+ return this.matchFuzzy(event.summary);
63
+ case 'regex':
64
+ return this.matchRegex(event.summary);
65
+ case 'date':
66
+ return this.matchDate(event.timestamp);
67
+ default:
68
+ return true;
69
+ }
70
+ }
71
+ /** Test whether arbitrary text matches the current filter (for PanelItem labels etc). */
72
+ matchesText(text) {
73
+ if (!this.isActive)
74
+ return true;
75
+ switch (this.mode) {
76
+ case 'substring':
77
+ return this.matchSubstring(text);
78
+ case 'fuzzy':
79
+ return this.matchFuzzy(text);
80
+ case 'regex':
81
+ return this.matchRegex(text);
82
+ case 'date':
83
+ return true; // date mode doesn't filter text
84
+ default:
85
+ return true;
86
+ }
87
+ }
88
+ /**
89
+ * Highlight matched portions of text in the given format.
90
+ * Returns the text with matched substrings wrapped in highlight tags.
91
+ */
92
+ highlightMatches(text, format) {
93
+ if (!this.isActive || !text)
94
+ return text;
95
+ switch (this.mode) {
96
+ case 'substring':
97
+ return this.highlightSubstring(text, format);
98
+ case 'fuzzy':
99
+ return this.highlightFuzzy(text, format);
100
+ case 'regex':
101
+ return this.highlightRegex(text, format);
102
+ case 'date':
103
+ return text; // no highlighting for date mode
104
+ default:
105
+ return text;
106
+ }
107
+ }
108
+ // ── Private: matching ──
109
+ matchSubstring(text) {
110
+ return text.includes(this.query);
111
+ }
112
+ matchFuzzy(text) {
113
+ const lower = text.toLowerCase();
114
+ return this.fuzzyWords.every((w) => lower.includes(w));
115
+ }
116
+ matchRegex(text) {
117
+ if (!this.compiledRegex)
118
+ return false;
119
+ this.compiledRegex.lastIndex = 0;
120
+ return this.compiledRegex.test(text);
121
+ }
122
+ matchDate(timestamp) {
123
+ if (this.since && timestamp < this.since)
124
+ return false;
125
+ if (this.until && timestamp > this.until)
126
+ return false;
127
+ return true;
128
+ }
129
+ // ── Private: highlighting ──
130
+ highlightSubstring(text, format) {
131
+ if (!this.query)
132
+ return text;
133
+ return this.wrapMatches(text, this.escapeRegex(this.query), format);
134
+ }
135
+ highlightFuzzy(text, format) {
136
+ if (this.fuzzyWords.length === 0)
137
+ return text;
138
+ // Highlight each fuzzy word independently
139
+ const pattern = this.fuzzyWords.map((w) => this.escapeRegex(w)).join('|');
140
+ return this.wrapMatches(text, pattern, format, 'gi');
141
+ }
142
+ highlightRegex(text, format) {
143
+ if (!this.compiledRegex)
144
+ return text;
145
+ return this.wrapMatches(text, this.query, format, 'g');
146
+ }
147
+ wrapMatches(text, pattern, format, flags = 'g') {
148
+ let regex;
149
+ try {
150
+ regex = new RegExp(`(${pattern})`, flags);
151
+ }
152
+ catch {
153
+ return text;
154
+ }
155
+ const [open, close] = this.getHighlightTags(format);
156
+ return text.replace(regex, `${open}$1${close}`);
157
+ }
158
+ getHighlightTags(format) {
159
+ switch (format) {
160
+ case 'blessed':
161
+ return ['{blue-bg}{white-fg}', '{/white-fg}{/blue-bg}'];
162
+ case 'ansi':
163
+ return ['\x1b[0;44m', '\x1b[0m'];
164
+ case 'html':
165
+ return ['<mark class="sk-search-match">', '</mark>'];
166
+ default:
167
+ return ['', ''];
168
+ }
169
+ }
170
+ // ── Private: compilation ──
171
+ recompile() {
172
+ // Reset cached state
173
+ this.compiledRegex = null;
174
+ this.regexError = null;
175
+ this.fuzzyWords = [];
176
+ switch (this.mode) {
177
+ case 'regex':
178
+ try {
179
+ this.compiledRegex = new RegExp(this.query, 'g');
180
+ this.regexError = null;
181
+ }
182
+ catch (e) {
183
+ this.regexError = e instanceof Error ? e.message : 'Invalid regex';
184
+ }
185
+ break;
186
+ case 'fuzzy':
187
+ this.fuzzyWords = this.query
188
+ .toLowerCase()
189
+ .split(/\s+/)
190
+ .filter((w) => w.length > 0);
191
+ break;
192
+ }
193
+ }
194
+ escapeRegex(s) {
195
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
196
+ }
197
+ }
@@ -0,0 +1,66 @@
1
+ /** Provider-aware cross-session search. */
2
+ import { resolveProjectIdentity } from '../paths';
3
+ async function enumerateSessions(provider, options) {
4
+ const workspace = options.projectPath
5
+ ? resolveProjectIdentity(options.projectPath).resolvedCwd
6
+ : undefined;
7
+ if (!options.projectSlug || workspace) {
8
+ if (provider.listSessionFilesAsync)
9
+ return provider.listSessionFilesAsync(workspace);
10
+ if (workspace) {
11
+ return provider
12
+ .findAllSessions(workspace)
13
+ .map((path) => ({ path, mtime: new Date(0), workspacePath: workspace }));
14
+ }
15
+ if (provider.listAllSessionFiles)
16
+ return provider.listAllSessionFiles();
17
+ }
18
+ // Older/custom providers can expose virtual project directories. Let the
19
+ // provider enumerate them instead of assuming a particular on-disk layout.
20
+ const files = [];
21
+ for (const folder of provider.getAllProjectFolders()) {
22
+ if (options.signal?.aborted)
23
+ break;
24
+ if (options.projectSlug && folder.encodedName !== options.projectSlug)
25
+ continue;
26
+ const paths = provider.id === 'codex'
27
+ ? provider.findAllSessions(folder.name)
28
+ : provider.findSessionsInDirectory(folder.dir);
29
+ files.push(...paths.map((path) => ({ path, mtime: folder.lastModified, workspacePath: folder.name })));
30
+ }
31
+ return files;
32
+ }
33
+ export async function searchSessions(provider, query, options = {}) {
34
+ const maxResults = options.maxResults ?? 50;
35
+ const results = [];
36
+ if (maxResults <= 0 || !query.trim() || options.signal?.aborted)
37
+ return results;
38
+ const files = await enumerateSessions(provider, options);
39
+ const seen = new Set();
40
+ files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
41
+ for (const file of files) {
42
+ if (results.length >= maxResults || options.signal?.aborted)
43
+ break;
44
+ if (seen.has(file.path))
45
+ continue;
46
+ seen.add(file.path);
47
+ try {
48
+ const hits = provider.searchInSession(file.path, query, maxResults - results.length);
49
+ for (const hit of hits.slice(0, maxResults - results.length)) {
50
+ results.push({
51
+ providerId: provider.id,
52
+ projectPath: hit.projectPath || file.workspacePath || '',
53
+ sessionPath: hit.sessionPath,
54
+ snippet: hit.line,
55
+ eventType: hit.eventType,
56
+ timestamp: hit.timestamp,
57
+ });
58
+ }
59
+ }
60
+ catch {
61
+ // An unreadable session must not hide matches in the remaining sessions.
62
+ }
63
+ await new Promise((resolve) => setImmediate(resolve));
64
+ }
65
+ return results;
66
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Size/mtime fingerprints for session sources.
3
+ *
4
+ * Shared by the observed-session collector, the V1 adapter's watch loop, and
5
+ * the usage cache, so "has this session changed?" is answered the same way
6
+ * everywhere without re-reading content.
7
+ *
8
+ * @module sessionFingerprint
9
+ */
10
+ import { statSync } from 'node:fs';
11
+ /** Structured size/mtime fingerprint; returns null for inaccessible or synthetic paths. */
12
+ export function fileFingerprintParts(sourcePath) {
13
+ try {
14
+ const stat = statSync(sourcePath);
15
+ return { sizeBytes: stat.size, mtimeMs: stat.mtimeMs };
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
22
+ export function fileFingerprint(sourcePath) {
23
+ const parts = fileFingerprintParts(sourcePath);
24
+ return parts ? fingerprintString(parts) : null;
25
+ }
26
+ /** Canonical `${sizeBytes}:${mtimeMs}` form of a fingerprint. */
27
+ export function fingerprintString(parts) {
28
+ return `${parts.sizeBytes}:${parts.mtimeMs}`;
29
+ }
30
+ /**
31
+ * Fingerprint for a session path, falling back to provider metadata (mtime
32
+ * only) for database-backed sessions whose path is synthetic.
33
+ */
34
+ export function sessionFingerprintParts(provider, sessionPath) {
35
+ const parts = fileFingerprintParts(sessionPath);
36
+ if (parts)
37
+ return parts;
38
+ const metadata = provider.getSessionMetadata?.(sessionPath);
39
+ return metadata ? { sizeBytes: 0, mtimeMs: metadata.mtime.getTime() } : null;
40
+ }
@@ -0,0 +1,198 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { EventAggregator } from './aggregation/EventAggregator';
4
+ import { ObservedSessionCollector, fileFingerprintParts } from './observedSessionCollector';
5
+ /**
6
+ * UI-independent session monitor for library consumers. It owns one provider
7
+ * reader and the canonical aggregator; hosts decide how often to call `poll()`.
8
+ */
9
+ export class SessionMonitor {
10
+ provider;
11
+ aggregator;
12
+ reader = null;
13
+ sessionPath = null;
14
+ listeners = new Set();
15
+ subscriptions = new Set();
16
+ replayOnAttach;
17
+ constructor(provider, options = {}) {
18
+ this.provider = provider;
19
+ this.replayOnAttach = options.replayOnAttach ?? true;
20
+ this.aggregator = new EventAggregator({ ...options, providerId: provider.id });
21
+ }
22
+ attach(sessionPath, replay = this.replayOnAttach) {
23
+ this.aggregator.reset();
24
+ this.reader = this.provider.createReader(sessionPath);
25
+ this.sessionPath = sessionPath;
26
+ if (replay)
27
+ this.consume(this.reader.readAll());
28
+ return this.getMetrics();
29
+ }
30
+ attachLatest(workspacePath, replay = this.replayOnAttach) {
31
+ const sessionPath = this.provider.findActiveSession(workspacePath);
32
+ return sessionPath ? this.attach(sessionPath, replay) : null;
33
+ }
34
+ poll() {
35
+ if (!this.reader)
36
+ return [];
37
+ if (this.reader.wasTruncated())
38
+ this.aggregator.reset();
39
+ const events = this.reader.readNew();
40
+ this.consume(events);
41
+ return events;
42
+ }
43
+ process(event) {
44
+ this.aggregator.processEvent(event);
45
+ for (const listener of this.listeners)
46
+ listener(event);
47
+ }
48
+ onEvent(listener) {
49
+ this.listeners.add(listener);
50
+ return () => this.listeners.delete(listener);
51
+ }
52
+ /**
53
+ * Watches the attached session with debouncing and catch-up polling. Matching
54
+ * fingerprint changes automatically call poll(); parsed events still flow
55
+ * through onEvent.
56
+ */
57
+ subscribe(options = {}) {
58
+ const source = {
59
+ providerId: this.provider.id,
60
+ discover: async () => {
61
+ if (!this.sessionPath)
62
+ return [];
63
+ let parts = fileFingerprintParts(this.sessionPath);
64
+ if (!parts) {
65
+ try {
66
+ const metadata = this.provider.getSessionMetadataAsync
67
+ ? await this.provider.getSessionMetadataAsync(this.sessionPath)
68
+ : this.provider.getSessionMetadata?.(this.sessionPath);
69
+ parts = metadata ? { sizeBytes: 0, mtimeMs: metadata.mtime.getTime() } : null;
70
+ }
71
+ catch {
72
+ parts = null;
73
+ }
74
+ }
75
+ if (!parts)
76
+ return [];
77
+ return [
78
+ {
79
+ sessionId: this.provider.getSessionId(this.sessionPath),
80
+ sourceKey: this.sessionPath,
81
+ fingerprintHint: `${parts.sizeBytes}:${parts.mtimeMs}`,
82
+ fingerprintParts: parts,
83
+ },
84
+ ];
85
+ },
86
+ read: () => null,
87
+ // One session is watched, so an event naming any other file costs
88
+ // nothing; an event naming this file costs one stat.
89
+ resolveReference: async (signal) => {
90
+ if (!signal.root || typeof signal.filename !== 'string' || !signal.filename) {
91
+ return { status: 'unknown' };
92
+ }
93
+ if (!this.sessionPath)
94
+ return { status: 'ignored' };
95
+ const eventPath = path.resolve(signal.root, signal.filename);
96
+ if (eventPath !== path.resolve(this.sessionPath))
97
+ return { status: 'ignored' };
98
+ const [reference] = await source.discover();
99
+ return reference
100
+ ? { status: 'present', reference }
101
+ : { status: 'missing', sessionId: this.provider.getSessionId(this.sessionPath) };
102
+ },
103
+ subscribe: (listener, sourceOptions) => {
104
+ const watchers = new Set();
105
+ let pollTimer = null;
106
+ let roots;
107
+ try {
108
+ roots = this.provider.getWatchRoots?.() ?? [this.provider.getProjectsBaseDir()];
109
+ }
110
+ catch {
111
+ roots = [];
112
+ }
113
+ for (const root of new Set(roots)) {
114
+ try {
115
+ const watcher = fs.watch(root, { persistent: false, recursive: true }, (eventType, filename) => listener({
116
+ trigger: 'event',
117
+ root,
118
+ eventType,
119
+ filename: filename === null || filename === undefined ? null : String(filename),
120
+ }));
121
+ watchers.add(watcher);
122
+ watcher.on('error', () => {
123
+ watchers.delete(watcher);
124
+ try {
125
+ watcher.close();
126
+ }
127
+ catch {
128
+ // Collector catch-up polling remains active.
129
+ }
130
+ });
131
+ }
132
+ catch {
133
+ // Collector catch-up polling remains active.
134
+ }
135
+ }
136
+ const pollIntervalMs = sourceOptions?.pollIntervalMs ?? 0;
137
+ if (pollIntervalMs > 0) {
138
+ pollTimer = setInterval(() => listener({ trigger: 'poll' }), pollIntervalMs);
139
+ pollTimer.unref?.();
140
+ }
141
+ return {
142
+ dispose: () => {
143
+ for (const watcher of watchers)
144
+ watcher.close();
145
+ watchers.clear();
146
+ if (pollTimer)
147
+ clearInterval(pollTimer);
148
+ },
149
+ };
150
+ },
151
+ };
152
+ const collector = new ObservedSessionCollector({ sources: [source] });
153
+ const changeSubscription = collector.subscribe((batch) => {
154
+ if (batch.changes.some((change) => change.reference.sourceKey === this.sessionPath)) {
155
+ this.poll();
156
+ }
157
+ try {
158
+ options.onChange?.(batch);
159
+ }
160
+ catch {
161
+ // Change callbacks are observational.
162
+ }
163
+ }, {
164
+ debounceMs: options.debounceMs,
165
+ pollIntervalMs: options.pollIntervalMs,
166
+ });
167
+ const disposable = {
168
+ dispose: () => {
169
+ changeSubscription.dispose();
170
+ collector.dispose();
171
+ this.subscriptions.delete(disposable);
172
+ },
173
+ };
174
+ this.subscriptions.add(disposable);
175
+ return disposable;
176
+ }
177
+ getMetrics() {
178
+ return this.aggregator.getMetrics();
179
+ }
180
+ getSessionPath() {
181
+ return this.sessionPath;
182
+ }
183
+ reset() {
184
+ this.reader?.reset();
185
+ this.aggregator.reset();
186
+ }
187
+ dispose() {
188
+ for (const subscription of [...this.subscriptions])
189
+ subscription.dispose();
190
+ this.listeners.clear();
191
+ this.reader = null;
192
+ this.provider.dispose();
193
+ }
194
+ consume(events) {
195
+ for (const event of events)
196
+ this.process(event);
197
+ }
198
+ }