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.
- package/CHANGELOG.md +40 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/shared.mjs +6 -3
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +627 -57
- package/neural-interface/i18n/pt-BR.json +626 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +255 -52
- package/neural-interface/lib/assistant-brains/claude.js +15 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-config.js +62 -14
- package/neural-interface/lib/assistant-dispatch.js +432 -125
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-persona.js +68 -11
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +307 -47
- package/neural-interface/lib/assistant-user-writer.js +4 -2
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +90 -3
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +3 -3
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-read.js +34 -2
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +16 -3
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/offline.html +16 -1
- package/neural-interface/public/onboarding.html +96 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
- package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
- package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
- package/neural-interface/public/shared/assistant/asst-render.js +38 -7
- package/neural-interface/public/shared/assistant/asst-route.js +72 -14
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +1 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +46 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-panel.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +1 -1
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/html-shell.js +1 -0
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
- package/neural-interface/public/shared/pwa-install-page.js +3 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-ia.js +15 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +278 -31
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +212 -213
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-loading.js +30 -6
- package/neural-interface/public/shared/ui-menubar.js +13 -3
- package/neural-interface/public/shared/ui-navbar.js +3 -1
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +55 -3
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +6 -0
- package/neural-interface/public/shared/ui-terminal.js +17 -0
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +2 -0
- package/neural-interface/public/sw.js +1 -1
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/server.js +1219 -608
- package/package.json +4 -1
- 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
|
+
}
|