synabun 2.0.5 → 2.0.7

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 (233) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/shared.mjs +6 -3
  6. package/lib/child-path.js +303 -0
  7. package/lib/claude-hooks.js +2 -1
  8. package/lib/data-home-migration.js +2 -0
  9. package/lib/desktop-app.js +265 -0
  10. package/lib/external-tools.js +7 -3
  11. package/lib/log-redact.js +102 -0
  12. package/lib/packaged-runtime.js +138 -11
  13. package/mcp-server/dist/http.d.ts +3 -1
  14. package/mcp-server/dist/http.d.ts.map +1 -1
  15. package/mcp-server/dist/http.js +15 -4
  16. package/mcp-server/dist/http.js.map +1 -1
  17. package/mcp-server/dist/index.d.ts.map +1 -1
  18. package/mcp-server/dist/index.js +9 -6
  19. package/mcp-server/dist/index.js.map +1 -1
  20. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  21. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  22. package/mcp-server/dist/services/budget-judgments.js +153 -0
  23. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  24. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  25. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  26. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  27. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  28. package/mcp-server/dist/services/identity.d.ts +11 -0
  29. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  30. package/mcp-server/dist/services/identity.js +24 -0
  31. package/mcp-server/dist/services/identity.js.map +1 -1
  32. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  33. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  34. package/mcp-server/dist/services/neural-interface.js +34 -2
  35. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  36. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  37. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  38. package/mcp-server/dist/services/typesafe-config.js +4 -1
  39. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  40. package/mcp-server/dist/tools/agents.d.ts +10 -7
  41. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/agents.js +34 -13
  43. package/mcp-server/dist/tools/agents.js.map +1 -1
  44. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  45. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  46. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  47. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  48. package/mcp-server/dist/tui.js +2 -1
  49. package/mcp-server/dist/tui.js.map +1 -1
  50. package/mcp-server/dist/version.d.ts +5 -0
  51. package/mcp-server/dist/version.d.ts.map +1 -0
  52. package/mcp-server/dist/version.js +21 -0
  53. package/mcp-server/dist/version.js.map +1 -0
  54. package/mcp-server/package-lock.json +2 -2
  55. package/mcp-server/package.json +1 -1
  56. package/mcp-server/run.mjs +15 -9
  57. package/mcp-server/src/http.ts +18 -3
  58. package/mcp-server/src/index.ts +9 -6
  59. package/mcp-server/src/services/budget-judgments.ts +183 -0
  60. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  61. package/mcp-server/src/services/identity.ts +27 -0
  62. package/mcp-server/src/services/neural-interface.ts +34 -2
  63. package/mcp-server/src/services/typesafe-config.ts +4 -1
  64. package/mcp-server/src/tools/agents.ts +33 -14
  65. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  66. package/mcp-server/src/tui.ts +2 -1
  67. package/mcp-server/src/version.ts +22 -0
  68. package/neural-interface/i18n/en.json +627 -57
  69. package/neural-interface/i18n/pt-BR.json +626 -56
  70. package/neural-interface/lib/account-usage.js +163 -0
  71. package/neural-interface/lib/assistant-api.js +255 -52
  72. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  73. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  74. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  75. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  76. package/neural-interface/lib/assistant-budget-split.js +408 -0
  77. package/neural-interface/lib/assistant-budget.js +94 -95
  78. package/neural-interface/lib/assistant-config.js +62 -14
  79. package/neural-interface/lib/assistant-dispatch.js +432 -125
  80. package/neural-interface/lib/assistant-jev.js +26 -3
  81. package/neural-interface/lib/assistant-persona.js +68 -11
  82. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  83. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  84. package/neural-interface/lib/assistant-route-gate.js +19 -3
  85. package/neural-interface/lib/assistant-router.js +373 -42
  86. package/neural-interface/lib/assistant-runtime.js +307 -47
  87. package/neural-interface/lib/assistant-user-writer.js +4 -2
  88. package/neural-interface/lib/augmented-path.js +317 -53
  89. package/neural-interface/lib/claude-accounts.js +342 -31
  90. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  91. package/neural-interface/lib/claude-executable.js +6 -1
  92. package/neural-interface/lib/claude-model-catalog.js +3 -3
  93. package/neural-interface/lib/claude-panel-auth.js +221 -0
  94. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  95. package/neural-interface/lib/cli-install-api.js +137 -0
  96. package/neural-interface/lib/cli-install.js +797 -0
  97. package/neural-interface/lib/cli-path.js +130 -0
  98. package/neural-interface/lib/cli-resolver.js +744 -0
  99. package/neural-interface/lib/cli-update-planner.js +143 -7
  100. package/neural-interface/lib/codex-account-auth.js +178 -0
  101. package/neural-interface/lib/codex-accounts.js +489 -0
  102. package/neural-interface/lib/codex-capabilities.js +2 -1
  103. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  104. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  105. package/neural-interface/lib/codex-panel-auth.js +62 -0
  106. package/neural-interface/lib/codex-session-settings.js +1 -1
  107. package/neural-interface/lib/codex-settings-schema.js +120 -0
  108. package/neural-interface/lib/desktop/config.js +3 -0
  109. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  110. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  111. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  112. package/neural-interface/lib/git-bash/install.js +529 -0
  113. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  114. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  115. package/neural-interface/lib/git-bash/manager.js +268 -0
  116. package/neural-interface/lib/git-bash/pins.js +206 -0
  117. package/neural-interface/lib/git-bash/pins.json +34 -0
  118. package/neural-interface/lib/git-bash/resolver.js +194 -0
  119. package/neural-interface/lib/git-read.js +34 -2
  120. package/neural-interface/lib/install-details.js +145 -0
  121. package/neural-interface/lib/mcp-installer.js +14 -2
  122. package/neural-interface/lib/native-loop-providers.js +16 -3
  123. package/neural-interface/lib/native-loop-runtime.js +9 -4
  124. package/neural-interface/lib/os-path.js +319 -0
  125. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  126. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  127. package/neural-interface/lib/pty-host/core.js +2 -1
  128. package/neural-interface/lib/pty-host/manager.js +27 -2
  129. package/neural-interface/lib/remote-policy.js +20 -11
  130. package/neural-interface/lib/session-title-generator.js +4 -3
  131. package/neural-interface/lib/system-about.js +340 -0
  132. package/neural-interface/lib/system-activity.js +66 -0
  133. package/neural-interface/lib/terminal-env.js +11 -3
  134. package/neural-interface/lib/terminal-path.js +55 -0
  135. package/neural-interface/lib/virtual-socket.js +12 -1
  136. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  137. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  138. package/neural-interface/lib/whatsapp/commands.js +13 -14
  139. package/neural-interface/lib/whatsapp/config.js +134 -20
  140. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  141. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  142. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  143. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  144. package/neural-interface/lib/whatsapp/manager.js +92 -32
  145. package/neural-interface/lib/whatsapp/paths.js +11 -1
  146. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  147. package/neural-interface/lib/whatsapp/redact.js +63 -0
  148. package/neural-interface/lib/whatsapp/service.js +343 -91
  149. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  150. package/neural-interface/package-lock.json +2 -2
  151. package/neural-interface/package.json +1 -1
  152. package/neural-interface/public/index.html +2 -0
  153. package/neural-interface/public/index2d.html +2 -0
  154. package/neural-interface/public/offline.html +16 -1
  155. package/neural-interface/public/onboarding.html +96 -2
  156. package/neural-interface/public/shared/account-i18n.js +7 -0
  157. package/neural-interface/public/shared/account-menu.js +539 -0
  158. package/neural-interface/public/shared/account-usage.js +19 -0
  159. package/neural-interface/public/shared/account-view.js +335 -0
  160. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  161. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  162. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  163. package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
  164. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  165. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  166. package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
  167. package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
  168. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  169. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  170. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  171. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  172. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  173. package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
  174. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  175. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  176. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  177. package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
  178. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  179. package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
  180. package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
  181. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  182. package/neural-interface/public/shared/claude-account-view.js +3 -0
  183. package/neural-interface/public/shared/cli-install-view.js +392 -0
  184. package/neural-interface/public/shared/cli-status.js +282 -22
  185. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  186. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  187. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  188. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  189. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  190. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  191. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  192. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  193. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  194. package/neural-interface/public/shared/git-bash-view.js +145 -0
  195. package/neural-interface/public/shared/html-shell.js +1 -0
  196. package/neural-interface/public/shared/i18n.js +19 -1
  197. package/neural-interface/public/shared/install-details-view.js +86 -0
  198. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
  199. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  200. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
  201. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  202. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  203. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  204. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  205. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  206. package/neural-interface/public/shared/storage.js +30 -3
  207. package/neural-interface/public/shared/styles.css +278 -31
  208. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  209. package/neural-interface/public/shared/ui-claude-panel.js +212 -213
  210. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  211. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  212. package/neural-interface/public/shared/ui-judgments.js +10 -1
  213. package/neural-interface/public/shared/ui-loading.js +30 -6
  214. package/neural-interface/public/shared/ui-menubar.js +13 -3
  215. package/neural-interface/public/shared/ui-navbar.js +3 -1
  216. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  217. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  218. package/neural-interface/public/shared/ui-sessions.js +9 -3
  219. package/neural-interface/public/shared/ui-settings.js +55 -3
  220. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  221. package/neural-interface/public/shared/ui-sync.js +6 -0
  222. package/neural-interface/public/shared/ui-terminal.js +17 -0
  223. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  224. package/neural-interface/public/shared/ui-update.js +594 -5
  225. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  226. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  227. package/neural-interface/public/shared/worker-end.js +2 -0
  228. package/neural-interface/public/sw.js +1 -1
  229. package/neural-interface/public/variant/2d/main.js +10 -1
  230. package/neural-interface/public/variant/3d/main.js +10 -1
  231. package/neural-interface/server.js +1219 -608
  232. package/package.json +4 -1
  233. package/setup.js +9 -8
@@ -0,0 +1,163 @@
1
+ // Subscription quota only. Credentials stay inside the reader; public values are
2
+ // constructed from numeric windows, never spread from a provider response.
3
+ import { readFile } from 'node:fs/promises';
4
+ import { execFile } from 'node:child_process';
5
+ import { createHash } from 'node:crypto';
6
+ import { join } from 'node:path';
7
+
8
+ const percent = n => typeof n === 'number' && Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : null;
9
+ const date = value => {
10
+ if (value == null) return null;
11
+ const ms = typeof value === 'number' ? value * (value < 1e12 ? 1000 : 1) : Date.parse(value);
12
+ return Number.isFinite(ms) && Math.abs(ms) <= 8.64e15 ? new Date(ms).toISOString() : null;
13
+ };
14
+ const unknown = (source = 'unknown', at = Date.now()) => ({ windows: [], remainingPercent: null, limited: null, resetsAt: null, source, fetchedAt: new Date(at).toISOString() });
15
+
16
+ export function normalizeAccountUsage(provider, raw, at = Date.now()) {
17
+ let windows = [], rejected = false;
18
+ if (provider === 'claude-code') {
19
+ windows = ['five_hour', 'seven_day', 'seven_day_opus', 'seven_day_sonnet'].map(kind => ({ kind, usedPercent: percent(raw?.[kind]?.utilization), resetsAt: date(raw?.[kind]?.resets_at) }));
20
+ } else {
21
+ const snapshot = raw?.rateLimits || raw;
22
+ // rateLimitsByLimitId can contain model-specific quotas; the account-wide
23
+ // rateLimits snapshot is the quota shared by the subscription.
24
+ windows = ['primary', 'secondary'].map(kind => ({ kind, usedPercent: percent(snapshot?.[kind]?.usedPercent), resetsAt: date(snapshot?.[kind]?.resetsAt) }));
25
+ rejected = !!snapshot?.rateLimitReachedType || snapshot?.spendControlReached === true || raw?.ordinaryUsageAllowed === false;
26
+ }
27
+ return summarize(windows, provider === 'codex' ? 'codex-app-server' : 'claude-oauth', at, rejected);
28
+ }
29
+
30
+ function summarize(input, source, at, rejected = false) {
31
+ // Once a window resets its old percentage is unknown, not an invented 0%.
32
+ const windows = input.filter(w => (w.usedPercent !== null || w.rejected) && (!w.resetsAt || Date.parse(w.resetsAt) > at));
33
+ if (!windows.length && !rejected) return unknown(source, at);
34
+ const values = windows.map(w => w.usedPercent).filter(n => n !== null);
35
+ const limited = rejected || windows.some(w => w.usedPercent >= 100 || w.rejected);
36
+ const binding = limited ? windows.filter(w => w.usedPercent >= 100 || w.rejected) : windows.filter(w => w.usedPercent === Math.max(...values));
37
+ const resets = binding.map(w => w.resetsAt).filter(Boolean).sort();
38
+ return { windows: windows.map(({ kind, usedPercent, resetsAt }) => ({ kind, usedPercent, resetsAt })), remainingPercent: limited ? 0 : values.length ? 100 - Math.max(...values) : null, limited, resetsAt: limited && binding.some(w => !w.resetsAt) ? null : resets.at(-1) || null, source, fetchedAt: new Date(at).toISOString() };
39
+ }
40
+
41
+ // Matches Claude Code's directory-scoped macOS keychain service (verified in
42
+ // installed CLI 2.1.302, D1). Never refresh or write OAuth credentials here.
43
+ export async function readClaudeUsageToken(row, { env = process.env, platform = process.platform, signal, now = Date.now, read = readFile, keychain = execFile } = {}) {
44
+ const config = row.id !== 'default' || !!env.CLAUDE_CONFIG_DIR;
45
+ const scope = row.id === 'default' && env.CLAUDE_SECURESTORAGE_CONFIG_DIR !== undefined ? env.CLAUDE_SECURESTORAGE_CONFIG_DIR : config ? row.home : '';
46
+ let credentials;
47
+ if (platform === 'darwin') {
48
+ const suffix = scope ? `-${createHash('sha256').update(scope.normalize('NFC')).digest('hex').slice(0, 8)}` : '';
49
+ const user = /^[a-zA-Z0-9._-]+$/.test(env.USER || '') ? env.USER : 'claude-code-user';
50
+ try {
51
+ const value = await new Promise((resolve, reject) => keychain('/usr/bin/security', ['find-generic-password', '-a', user, '-w', '-s', `Claude Code-credentials${suffix}`], { timeout: 1500, maxBuffer: 1024 * 1024, signal }, (error, stdout) => error ? reject(new Error('credential unavailable')) : resolve(stdout)));
52
+ credentials = JSON.parse(value);
53
+ } catch { /* Linux/file fallback is also used by some macOS installs. */ }
54
+ }
55
+ if (!credentials) {
56
+ try { credentials = JSON.parse(await read(join(scope || row.home, '.credentials.json'), { encoding: 'utf8', signal })); } catch { return null; }
57
+ }
58
+ const oauth = credentials?.claudeAiOauth;
59
+ if (!oauth || typeof oauth.accessToken !== 'string' || !oauth.accessToken || (oauth.expiresAt != null && Number(oauth.expiresAt) <= now())) return null;
60
+ return oauth.accessToken;
61
+ }
62
+
63
+ export function createAccountUsage({ accounts = {}, readCodex = null, fetchImpl = fetch, tokenReader = readClaudeUsageToken, now = Date.now, ttlMs = 90_000, timeoutMs = 3500 } = {}) {
64
+ const cache = new Map(), inflight = new Map(), overlays = new Map(), generations = new Map();
65
+ const keyOf = (p, id) => `${p}:${id}`;
66
+ const rowOf = (p, id) => accounts[p]?.describe(id);
67
+ const signature = row => JSON.stringify([row?.mode, row?.signedIn, row?.identity, row?.home]);
68
+ function note(provider, id, raw) {
69
+ const row = rowOf(provider, id);
70
+ if (row?.mode !== 'subscription' || row.signedIn !== true) return;
71
+ const at = now(), key = keyOf(provider, id);
72
+ if (provider === 'claude-code') {
73
+ const info = raw?.rate_limit_info || raw;
74
+ const kind = ['five_hour', 'seven_day', 'seven_day_opus', 'seven_day_sonnet', 'overage'].includes(info?.rateLimitType) ? info.rateLimitType : 'subscription';
75
+ const previous = overlays.get(key);
76
+ const windows = previous?.signature === signature(row) ? previous.windows.filter(w => w.kind !== kind) : [];
77
+ windows.push({ kind, usedPercent: percent(typeof info?.utilization === 'number' ? info.utilization * 100 : null), resetsAt: date(info?.resetsAt), rejected: info?.status === 'rejected', at });
78
+ overlays.set(key, { signature: signature(row), windows, at });
79
+ } else {
80
+ const value = normalizeAccountUsage(provider, raw, at);
81
+ cache.set(key, { signature: signature(row), value, at });
82
+ }
83
+ }
84
+ function overlay(provider, id, value) {
85
+ const item = overlays.get(keyOf(provider, id));
86
+ if (!item || item.signature !== signature(rowOf(provider, id))) return value;
87
+ const probeAt = cache.get(keyOf(provider, id))?.probeAt ?? Date.parse(value.fetchedAt);
88
+ const fresh = item.windows.filter(w => now() - w.at < ttlMs && (!value.windows.length || w.at >= probeAt) && (!w.resetsAt || Date.parse(w.resetsAt) > now()));
89
+ if (!fresh.length) return value;
90
+ const combined = summarize([...value.windows.filter(w => !fresh.some(f => f.kind === w.kind)), ...fresh], 'claude-sdk', now());
91
+ combined.fetchedAt = new Date(Math.max(...fresh.map(w => w.at), value.windows.length ? Date.parse(value.fetchedAt) : 0)).toISOString();
92
+ return combined;
93
+ }
94
+ function peek(provider, id) {
95
+ const row = rowOf(provider, id);
96
+ if (row?.mode === 'apiKey' || row?.mode === 'api') return unknown('not_applicable', now());
97
+ const item = cache.get(keyOf(provider, id));
98
+ const value = item?.signature === signature(row) && now() - item.at < ttlMs ? summarize(item.value.windows, item.value.source, Date.parse(item.value.fetchedAt), item.value.limited && !item.value.windows.some(w => w.usedPercent >= 100)) : unknown('unknown', now());
99
+ // Do not keep a limited window alive after its reset.
100
+ if (value.windows.some(w => w.resetsAt && Date.parse(w.resetsAt) <= now())) return overlay(provider, id, unknown('unknown', now()));
101
+ return overlay(provider, id, value);
102
+ }
103
+ async function read(provider, id, { force = false } = {}) {
104
+ const row = rowOf(provider, id), key = keyOf(provider, id), sig = signature(row);
105
+ const generation = generations.get(key) || 0;
106
+ const flightKey = JSON.stringify([key, sig, generation]);
107
+ if (!row || row.mode !== 'subscription' || row.signedIn !== true) return peek(provider, id);
108
+ const existing = cache.get(key);
109
+ if (!force && existing?.signature === sig && now() - existing.at < ttlMs && !existing.value.windows.some(w => w.resetsAt && Date.parse(w.resetsAt) <= now())) return peek(provider, id);
110
+ if (inflight.has(flightKey)) return inflight.get(flightKey);
111
+ const startedAt = now();
112
+ const job = (async () => {
113
+ const controller = new AbortController(); let timer;
114
+ try {
115
+ const raw = await Promise.race([
116
+ (async () => {
117
+ if (provider === 'codex') return readCodex?.(id, { signal: controller.signal, timeoutMs });
118
+ const token = await tokenReader(row, { signal: controller.signal, now });
119
+ if (!token || controller.signal.aborted) return null;
120
+ const response = await fetchImpl('https://api.anthropic.com/api/oauth/usage', { headers: { Authorization: `Bearer ${token}`, 'anthropic-beta': 'oauth-2025-04-20' }, signal: controller.signal, redirect: 'error' });
121
+ return response.ok ? response.json() : null;
122
+ })(),
123
+ new Promise(resolve => { timer = setTimeout(() => { controller.abort(); resolve(null); }, timeoutMs); }),
124
+ ]);
125
+ const at = now();
126
+ // A passive notification received during the probe is newer.
127
+ if ((generations.get(key) || 0) === generation && signature(rowOf(provider, id)) === sig && (!cache.has(key) || cache.get(key) === existing)) cache.set(key, { signature: sig, value: normalizeAccountUsage(provider, raw, at), at, probeAt: startedAt });
128
+ } catch {
129
+ if ((generations.get(key) || 0) === generation && signature(rowOf(provider, id)) === sig && (!cache.has(key) || cache.get(key) === existing)) cache.set(key, { signature: sig, value: unknown('unknown', now()), at: now(), probeAt: startedAt });
130
+ } finally { clearTimeout(timer); controller.abort(); inflight.delete(flightKey); }
131
+ return peek(provider, id);
132
+ })();
133
+ inflight.set(flightKey, job);
134
+ return job;
135
+ }
136
+ async function refresh(provider = null) {
137
+ await Promise.all(Object.entries(accounts).filter(([p]) => !provider || p === provider).flatMap(([p, service]) => service.list().map(row => read(p, row.id))));
138
+ return list();
139
+ }
140
+ function list() { return Object.fromEntries(Object.entries(accounts).map(([p, service]) => [p, service.listForClient().map(row => ({ ...row, usage: peek(p, row.id) }))])); }
141
+ function invalidate(p, id) { const key = keyOf(p, id); generations.set(key, (generations.get(key) || 0) + 1); cache.delete(key); overlays.delete(key); }
142
+ return { read, peek, note, refresh, list, invalidate };
143
+ }
144
+
145
+ /** Resolve once at route time. Concrete selections stay binding at dispatch. */
146
+ export function chooseUsageAccount(rows, choice, defaultId = 'default') {
147
+ const pinned = rows.find(row => row.id === choice);
148
+ const candidates = rows.filter(row => row.mode === 'subscription' && row.signedIn === true);
149
+ if (choice !== 'auto' && (!pinned || pinned.signedIn === false || pinned.mode !== 'subscription' || !pinned.usage?.limited)) return { row: pinned, reason: null };
150
+ const available = candidates.filter(row => !row.usage?.limited);
151
+ const known = available.filter(row => typeof row.usage?.remainingPercent === 'number');
152
+ const highest = Math.max(...known.map(row => row.usage.remainingPercent));
153
+ const best = known.filter(row => row.usage.remainingPercent === highest);
154
+ let row = (best.length > 1 ? available.find(row => row.id === defaultId) : null) || best.find(row => row.id === defaultId) || best[0];
155
+ if (!row) row = available.find(row => row.id === defaultId) || available[0];
156
+ let reason = choice === 'auto' ? 'account_auto' : 'account_limited';
157
+ if (!available.length && candidates.length) {
158
+ row = [...candidates].sort((a, b) => (Date.parse(a.usage?.resetsAt) || Infinity) - (Date.parse(b.usage?.resetsAt) || Infinity) || Number(b.id === defaultId) - Number(a.id === defaultId))[0];
159
+ reason = 'accounts_all_limited';
160
+ }
161
+ // With no eligible subscription, retain the existing default behavior.
162
+ return { row: row || (choice === 'auto' ? null : pinned), reason };
163
+ }