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,247 @@
1
+ /**
2
+ * Runtime pricing catalog hydration (Node-only).
3
+ *
4
+ * Fetches LiteLLM's community-maintained model pricing catalog, caches the
5
+ * normalized result to disk, and pushes it into `modelInfo.ts` as an override
6
+ * map. Static `PRICING_TABLE` remains the offline baseline.
7
+ *
8
+ * This module lives on the `sidekick-shared/node` subpath so browser bundles
9
+ * (VS Code webviews) never pull it in. Webviews receive pre-computed costs
10
+ * from the extension host and don't need pricing data locally.
11
+ *
12
+ * Non-goals:
13
+ * - Perfect billing reconciliation. Best-effort estimate based on LiteLLM.
14
+ * - Aggressive refresh. Default 24h TTL is plenty for a pricing table.
15
+ * - Blocking startup. Every error path is non-fatal; static table keeps working.
16
+ *
17
+ * @module pricingCatalog
18
+ */
19
+ import { promises as fs } from 'node:fs';
20
+ import * as path from 'node:path';
21
+ import { _setCatalogContextWindows } from './modelContext';
22
+ import { _setPricingOverrides } from './modelInfo';
23
+ import { getConfigDir } from './paths';
24
+ import { atomicWriteJson } from './writers/atomic';
25
+ // ── Constants ──
26
+ export const LITELLM_CATALOG_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
27
+ const CACHE_FILE_NAME = 'pricing-catalog.json';
28
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
29
+ const DEFAULT_TIMEOUT_MS = 3_000;
30
+ // ── Public API ──
31
+ /**
32
+ * Hydrate the pricing override map.
33
+ *
34
+ * Order:
35
+ * 1. If a fresh on-disk cache exists (< ttlMs old), load it and return.
36
+ * 2. Otherwise attempt a network fetch; on success, persist + apply.
37
+ * 3. On any failure, load a stale cache if present; else leave overrides empty.
38
+ *
39
+ * Always resolves — never throws. Offline-safe.
40
+ */
41
+ export async function hydratePricingCatalog(options = {}) {
42
+ const { cacheDir = getConfigDir(), fetchImpl = globalThis.fetch, ttlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, url = LITELLM_CATALOG_URL, logger, offline = false, } = options;
43
+ const cachePath = path.join(cacheDir, CACHE_FILE_NAME);
44
+ const now = Date.now();
45
+ // 1. Fresh cache? (Offline callers take any cache, however old.)
46
+ const cached = await readCache(cachePath);
47
+ if (cached && (offline || now - Date.parse(cached.fetchedAt) < ttlMs)) {
48
+ return applyCacheFile(cached, 'cache');
49
+ }
50
+ // 2. Try network.
51
+ if (fetchImpl && !offline) {
52
+ const fetched = await fetchCatalog(url, fetchImpl, timeoutMs, logger);
53
+ if (fetched) {
54
+ const payload = {
55
+ fetchedAt: new Date(now).toISOString(),
56
+ url,
57
+ overrides: normalizeLiteLlmCatalog(fetched),
58
+ contextWindows: normalizeLiteLlmContextWindows(fetched),
59
+ };
60
+ await writeCache(cachePath, payload, logger);
61
+ return applyCacheFile(payload, 'network');
62
+ }
63
+ }
64
+ // 3. Fall back to stale cache if present.
65
+ if (cached) {
66
+ return applyCacheFile(cached, 'cache');
67
+ }
68
+ // 4. Nothing we can do — stay on static tables.
69
+ return {
70
+ source: 'offline',
71
+ entries: 0,
72
+ contextWindowEntries: 0,
73
+ fetchedAt: new Date(now).toISOString(),
74
+ };
75
+ }
76
+ /**
77
+ * Push a cache payload into both override maps and describe what was applied.
78
+ * `contextWindows` is absent on caches written before context hydration; those
79
+ * contribute no overrides rather than clearing whatever is already loaded.
80
+ */
81
+ function applyCacheFile(cache, source) {
82
+ const contextWindows = cache.contextWindows ?? {};
83
+ _setPricingOverrides(cache.overrides);
84
+ if (Object.keys(contextWindows).length > 0) {
85
+ _setCatalogContextWindows(contextWindows);
86
+ }
87
+ return {
88
+ source,
89
+ entries: Object.keys(cache.overrides).length,
90
+ contextWindowEntries: Object.keys(contextWindows).length,
91
+ fetchedAt: cache.fetchedAt,
92
+ };
93
+ }
94
+ /**
95
+ * Convert a LiteLLM catalog payload to our `ModelPricing` override map.
96
+ * Exported for tests; rarely called directly.
97
+ */
98
+ export function normalizeLiteLlmCatalog(raw) {
99
+ const out = {};
100
+ if (!raw || typeof raw !== 'object')
101
+ return out;
102
+ for (const [key, entry] of Object.entries(raw)) {
103
+ // LiteLLM ships a `sample_spec` header entry. Skip it.
104
+ if (key === 'sample_spec')
105
+ continue;
106
+ if (!entry || typeof entry !== 'object')
107
+ continue;
108
+ const pricing = liteLlmEntryToPricing(entry);
109
+ if (!pricing)
110
+ continue;
111
+ // Catalog keys use both `provider/model` and bare `model` forms. Record both
112
+ // the raw key and the bare-model form so lookups work regardless of caller.
113
+ out[key] = pricing;
114
+ const bare = stripProviderPrefix(key);
115
+ if (bare && bare !== key && !out[bare]) {
116
+ out[bare] = pricing;
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+ /**
122
+ * Convert a LiteLLM catalog payload to a model → context-window override map.
123
+ *
124
+ * Kept separate from `normalizeLiteLlmCatalog` rather than folded into its
125
+ * return type: that function is public API on a published package, and pricing
126
+ * and context coverage differ (an entry can have one without the other).
127
+ *
128
+ * Exported for tests; rarely called directly.
129
+ */
130
+ export function normalizeLiteLlmContextWindows(raw) {
131
+ const out = {};
132
+ if (!raw || typeof raw !== 'object')
133
+ return out;
134
+ // Bare-model aliases are gathered separately rather than written as we go. A
135
+ // `provider/model` entry describes that *provider's deployment* window, which
136
+ // is often a truncation of the model's own — the live catalog carries five
137
+ // different windows for `claude-sonnet-4`, from GitHub Copilot's 128K to
138
+ // Vertex's 1M. Taking whichever appeared first in the JSON is a coin flip, so
139
+ // an alias is only promoted when every provider mentioning it agrees.
140
+ const aliasWindows = new Map();
141
+ for (const [key, entry] of Object.entries(raw)) {
142
+ // LiteLLM ships a `sample_spec` header entry. Skip it.
143
+ if (key === 'sample_spec')
144
+ continue;
145
+ if (!entry || typeof entry !== 'object')
146
+ continue;
147
+ const window = entry.max_input_tokens;
148
+ if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
149
+ continue;
150
+ out[key] = window;
151
+ const bare = stripProviderPrefix(key);
152
+ if (bare && bare !== key) {
153
+ const seen = aliasWindows.get(bare) ?? new Set();
154
+ seen.add(window);
155
+ aliasWindows.set(bare, seen);
156
+ }
157
+ }
158
+ for (const [bare, windows] of aliasWindows) {
159
+ // A top-level entry for the same ID is authoritative — never shadow it.
160
+ if (out[bare] !== undefined)
161
+ continue;
162
+ // Providers disagree, so none of their numbers can be attributed to the
163
+ // bare model. Record nothing and let a more trusted layer answer.
164
+ if (windows.size !== 1)
165
+ continue;
166
+ const [only] = windows;
167
+ if (typeof only === 'number')
168
+ out[bare] = only;
169
+ }
170
+ return out;
171
+ }
172
+ // ── Internals ──
173
+ function liteLlmEntryToPricing(entry) {
174
+ const input = entry.input_cost_per_token;
175
+ const output = entry.output_cost_per_token;
176
+ // Need at least input+output to be useful.
177
+ if (typeof input !== 'number' || typeof output !== 'number')
178
+ return null;
179
+ // LiteLLM sometimes reports cached input under `input_cost_per_token_cached`.
180
+ const cacheRead = typeof entry.cache_read_input_token_cost === 'number'
181
+ ? entry.cache_read_input_token_cost
182
+ : typeof entry.input_cost_per_token_cached === 'number'
183
+ ? entry.input_cost_per_token_cached
184
+ : 0;
185
+ const cacheWrite = typeof entry.cache_creation_input_token_cost === 'number'
186
+ ? entry.cache_creation_input_token_cost
187
+ : 0;
188
+ return {
189
+ inputCostPerMillion: input * 1_000_000,
190
+ outputCostPerMillion: output * 1_000_000,
191
+ cacheWriteCostPerMillion: cacheWrite * 1_000_000,
192
+ cacheReadCostPerMillion: cacheRead * 1_000_000,
193
+ };
194
+ }
195
+ function stripProviderPrefix(key) {
196
+ const slash = key.indexOf('/');
197
+ if (slash < 0)
198
+ return null;
199
+ return key.slice(slash + 1);
200
+ }
201
+ async function readCache(cachePath) {
202
+ try {
203
+ const raw = await fs.readFile(cachePath, 'utf8');
204
+ const parsed = JSON.parse(raw);
205
+ if (!parsed ||
206
+ typeof parsed !== 'object' ||
207
+ typeof parsed.fetchedAt !== 'string' ||
208
+ !parsed.overrides ||
209
+ typeof parsed.overrides !== 'object') {
210
+ return null;
211
+ }
212
+ return parsed;
213
+ }
214
+ catch {
215
+ return null;
216
+ }
217
+ }
218
+ async function writeCache(cachePath, payload, logger) {
219
+ try {
220
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
221
+ // The CLI and the extension host can both refresh the catalog at once;
222
+ // the atomic writer keeps a reader from ever seeing a half-written file.
223
+ await atomicWriteJson(cachePath, payload);
224
+ }
225
+ catch (err) {
226
+ logger?.(`pricingCatalog: failed to write cache: ${String(err)}`);
227
+ }
228
+ }
229
+ async function fetchCatalog(url, fetchImpl, timeoutMs, logger) {
230
+ const controller = new AbortController();
231
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
232
+ try {
233
+ const res = await fetchImpl(url, { signal: controller.signal });
234
+ if (!res.ok) {
235
+ logger?.(`pricingCatalog: HTTP ${res.status} from ${url}`);
236
+ return null;
237
+ }
238
+ return await res.json();
239
+ }
240
+ catch (err) {
241
+ logger?.(`pricingCatalog: fetch failed: ${String(err)}`);
242
+ return null;
243
+ }
244
+ finally {
245
+ clearTimeout(timer);
246
+ }
247
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Cross-platform detection of running CLI/app processes that hold provider
3
+ * credentials in memory. Bounded, never throws, never spawns a shell.
4
+ */
5
+ import { execFile, spawnSync } from 'child_process';
6
+ import * as path from 'path';
7
+ const PROBE_TIMEOUT_MS = 4000;
8
+ const CLAUDE_NPM_ENTRY = /claude-code[\\/]cli\.js/;
9
+ const CODEX_NPM_ENTRY = /(?:@openai[\\/]codex|codex[\\/]bin)[\\/].*codex\.js/;
10
+ function parseUnixTable(stdout) {
11
+ const rows = [];
12
+ for (const line of stdout.split('\n')) {
13
+ const match = line.match(/^\s*(\d+)\s+(\S+)\s*(.*)$/);
14
+ if (!match)
15
+ continue;
16
+ rows.push({ pid: Number(match[1]), command: match[2], args: match[3] ?? '' });
17
+ }
18
+ return rows;
19
+ }
20
+ function parseWindowsCsv(stdout) {
21
+ const rows = [];
22
+ for (const line of stdout.split(/\r?\n/)) {
23
+ const match = line.match(/^"([^"]+)","(\d+)"/);
24
+ if (!match)
25
+ continue;
26
+ rows.push({ pid: Number(match[2]), command: match[1], args: '' });
27
+ }
28
+ return rows;
29
+ }
30
+ function matchRows(rows, names) {
31
+ const wanted = new Set(names);
32
+ const found = [];
33
+ for (const row of rows) {
34
+ if (!Number.isFinite(row.pid) || row.pid === process.pid)
35
+ continue;
36
+ const base = path.basename(row.command);
37
+ // The native Claude Code binary is invoked through a `claude` symlink but
38
+ // its executable is the versioned file (`.../versions/2.1.273`), so `comm`
39
+ // never says `claude`; argv[0] does.
40
+ const argv0 = row.args.trim().split(/\s+/)[0] ?? '';
41
+ const candidates = [base, base.replace(/\.exe$/i, ''), path.basename(argv0)];
42
+ let name = candidates.find((candidate) => candidate && wanted.has(candidate));
43
+ // An npm-installed `claude` runs under `node`; recognise it by its entry file.
44
+ if (!name &&
45
+ wanted.has('claude') &&
46
+ /^node(\.exe)?$/i.test(base) &&
47
+ CLAUDE_NPM_ENTRY.test(row.args)) {
48
+ name = 'claude';
49
+ }
50
+ if (!name &&
51
+ wanted.has('codex') &&
52
+ /^node(\.exe)?$/i.test(base) &&
53
+ CODEX_NPM_ENTRY.test(row.args))
54
+ name = 'codex';
55
+ if (!name &&
56
+ wanted.has('Codex') &&
57
+ /(?:^|[\\/])Codex\.app[\\/]/.test(row.command + ' ' + row.args))
58
+ name = 'Codex';
59
+ if (name)
60
+ found.push({ pid: row.pid, name, command: row.command });
61
+ }
62
+ return found;
63
+ }
64
+ function probeCommand() {
65
+ return process.platform === 'win32'
66
+ ? { command: 'tasklist', args: ['/FO', 'CSV', '/NH'] }
67
+ : { command: 'ps', args: ['-axo', 'pid=,comm=,args='] };
68
+ }
69
+ function parseOutput(stdout) {
70
+ return process.platform === 'win32' ? parseWindowsCsv(stdout) : parseUnixTable(stdout);
71
+ }
72
+ /** Sync sibling of {@link findRunningProcesses}; blocks up to 4 s. */
73
+ function inspectProcessesSync(names) {
74
+ if (names.length === 0)
75
+ return { ok: true, processes: [] };
76
+ try {
77
+ const { command, args } = probeCommand();
78
+ const result = spawnSync(command, args, {
79
+ encoding: 'utf8',
80
+ maxBuffer: 16 * 1024 * 1024,
81
+ timeout: PROBE_TIMEOUT_MS,
82
+ killSignal: 'SIGKILL',
83
+ windowsHide: true,
84
+ });
85
+ if (result.error || result.status !== 0)
86
+ return { ok: false, processes: [] };
87
+ return { ok: true, processes: matchRows(parseOutput(String(result.stdout ?? '')), names) };
88
+ }
89
+ catch {
90
+ return { ok: false, processes: [] };
91
+ }
92
+ }
93
+ function inspectProcesses(names) {
94
+ if (names.length === 0)
95
+ return Promise.resolve({ ok: true, processes: [] });
96
+ return new Promise((resolve) => {
97
+ try {
98
+ const { command, args } = probeCommand();
99
+ execFile(command, args, {
100
+ encoding: 'utf8',
101
+ timeout: PROBE_TIMEOUT_MS,
102
+ killSignal: 'SIGKILL',
103
+ windowsHide: true,
104
+ maxBuffer: 16 * 1024 * 1024,
105
+ }, (error, stdout) => {
106
+ resolve({
107
+ ok: !error,
108
+ processes: error ? [] : matchRows(parseOutput(String(stdout ?? '')), names),
109
+ });
110
+ });
111
+ }
112
+ catch {
113
+ resolve({ ok: false, processes: [] });
114
+ }
115
+ });
116
+ }
117
+ const CONSUMER_PROCESS_NAMES = {
118
+ 'claude-cli': 'claude',
119
+ 'claude-desktop': 'Claude',
120
+ 'codex-cli': 'codex',
121
+ 'codex-app': 'Codex',
122
+ };
123
+ export const ACCOUNT_CONSUMER_REACHABILITY = {
124
+ 'claude-cli': {
125
+ switched: true,
126
+ reachability: 'Running claude sessions keep the previous account until restarted; a running session can rotate the refresh token of the account you just left.',
127
+ },
128
+ 'claude-desktop': {
129
+ switched: false,
130
+ reachability: 'Claude Desktop is not switched by sidekick (it keeps its own web session) and its embedded engine can rotate the shared CLI token.',
131
+ },
132
+ 'codex-cli': {
133
+ switched: true,
134
+ reachability: 'Running codex sessions keep the previous account until restarted.',
135
+ },
136
+ 'codex-app': {
137
+ switched: true,
138
+ reachability: 'The Codex app shares the CLI login; restart it to pick up the switch. While running it can refresh and rotate the live token.',
139
+ },
140
+ 'vscode-extension-host': {
141
+ switched: true,
142
+ reachability: 'The Claude Code extension host keeps the previous account until the window reloads.',
143
+ },
144
+ };
145
+ function toConsumer(kind, pids) {
146
+ return { kind, pids, ...ACCOUNT_CONSUMER_REACHABILITY[kind] };
147
+ }
148
+ function groupConsumers(processes, kinds) {
149
+ const consumers = [];
150
+ for (const kind of kinds) {
151
+ const pids = processes.filter((p) => p.name === CONSUMER_PROCESS_NAMES[kind]).map((p) => p.pid);
152
+ if (pids.length > 0)
153
+ consumers.push(toConsumer(kind, pids));
154
+ }
155
+ return consumers;
156
+ }
157
+ function kindsForProvider(provider) {
158
+ if (provider === 'claude-code')
159
+ return ['claude-cli', 'claude-desktop'];
160
+ if (provider === 'codex')
161
+ return ['codex-cli', 'codex-app'];
162
+ return ['claude-cli', 'claude-desktop', 'codex-cli', 'codex-app'];
163
+ }
164
+ /**
165
+ * Which credential consumers are running right now, with the reachability
166
+ * sentence to show for each. Optionally scoped to one provider.
167
+ */
168
+ export async function detectRunningAccountConsumers(provider) {
169
+ const kinds = kindsForProvider(provider);
170
+ const processes = await findRunningProcesses(kinds.map((kind) => CONSUMER_PROCESS_NAMES[kind]));
171
+ return groupConsumers(processes, kinds);
172
+ }
173
+ /** Sync sibling of {@link detectRunningAccountConsumers}; blocks up to 4 s. */
174
+ export function detectRunningAccountConsumersSync(provider) {
175
+ const kinds = kindsForProvider(provider);
176
+ const processes = findRunningProcessesSync(kinds.map((kind) => CONSUMER_PROCESS_NAMES[kind]));
177
+ return groupConsumers(processes, kinds);
178
+ }
179
+ /** Build a consumer entry for a kind the host detects itself (e.g. its own extension host). */
180
+ export function describeAccountConsumer(kind, pids = []) {
181
+ return toConsumer(kind, pids);
182
+ }
183
+ /** Compatibility wrappers keep the established best-effort list API. */
184
+ export function findRunningProcessesSync(names) {
185
+ return inspectProcessesSync(names).processes;
186
+ }
187
+ export async function findRunningProcesses(names) {
188
+ return (await inspectProcesses(names)).processes;
189
+ }
190
+ /** Safety decisions must distinguish a failed process scan from an empty one. */
191
+ export function inspectRunningAccountConsumersSync(provider) {
192
+ const kinds = kindsForProvider(provider);
193
+ const result = inspectProcessesSync(kinds.map((kind) => CONSUMER_PROCESS_NAMES[kind]));
194
+ return { ok: result.ok, consumers: groupConsumers(result.processes, kinds) };
195
+ }
196
+ export async function inspectRunningAccountConsumers(provider) {
197
+ const kinds = kindsForProvider(provider);
198
+ const result = await inspectProcesses(kinds.map((kind) => CONSUMER_PROCESS_NAMES[kind]));
199
+ return { ok: result.ok, consumers: groupConsumers(result.processes, kinds) };
200
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @fileoverview One-time migration of legacy raw-slug project stores.
3
+ *
4
+ * 0.23.0 switched all readers and writers to the canonical
5
+ * (symlink-resolved) project slug. Workspaces whose literal path differs
6
+ * from its realpath still hold pre-0.23 data under the legacy raw slug;
7
+ * this helper copies those stores to the canonical slug so they stay
8
+ * visible without any manual merge.
9
+ *
10
+ * @module projectMigration
11
+ */
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { getConfigDir, getProjectDataPath, resolveProjectIdentity, } from './paths';
15
+ /** Project-scoped JSON store subdomains shared by the extension and CLI. */
16
+ const PROJECT_STORE_SUBDOMAINS = [
17
+ 'tasks',
18
+ 'decisions',
19
+ 'notifications',
20
+ 'knowledge-notes',
21
+ 'plans',
22
+ ];
23
+ /**
24
+ * Copies legacy raw-slug project stores to the canonical slug, once.
25
+ *
26
+ * Only copies when the canonical file does not exist yet, so it can never
27
+ * overwrite newer data; the legacy file is kept as a read-only fallback.
28
+ * Best-effort per store — a failed copy leaves that store on the legacy
29
+ * read path.
30
+ *
31
+ * @returns The canonical paths that were created (empty when none).
32
+ */
33
+ export function migrateLegacyProjectStores(cwdOrIdentity) {
34
+ const identity = typeof cwdOrIdentity === 'string' ? resolveProjectIdentity(cwdOrIdentity) : cwdOrIdentity;
35
+ if (!identity.hasLegacyMismatch)
36
+ return [];
37
+ const pairs = PROJECT_STORE_SUBDOMAINS.map((subdomain) => [
38
+ getProjectDataPath(identity.legacySlug, subdomain),
39
+ getProjectDataPath(identity.canonicalSlug, subdomain),
40
+ ]);
41
+ pairs.push([
42
+ path.join(getConfigDir(), 'handoffs', `${identity.legacySlug}-latest.md`),
43
+ path.join(getConfigDir(), 'handoffs', `${identity.canonicalSlug}-latest.md`),
44
+ ]);
45
+ const migrated = [];
46
+ for (const [legacyPath, canonicalPath] of pairs) {
47
+ try {
48
+ if (!fs.existsSync(legacyPath) || fs.existsSync(canonicalPath))
49
+ continue;
50
+ fs.mkdirSync(path.dirname(canonicalPath), { recursive: true });
51
+ fs.copyFileSync(legacyPath, canonicalPath, fs.constants.COPYFILE_EXCL);
52
+ migrated.push(canonicalPath);
53
+ }
54
+ catch {
55
+ // Best effort: an unmigrated store falls back to the legacy read path.
56
+ }
57
+ }
58
+ return migrated;
59
+ }