tide-commander 1.153.0 → 1.154.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 (70) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/assets/{BossLogsModal-Dgz-N5gw.js → BossLogsModal-Bo0pUCWU.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-BK_rYu8s.js → BossSpawnModal-BOOHaqaD.js} +1 -1
  4. package/dist/assets/{ControlsModal-hC_AiCpk.js → ControlsModal-DItGV-CW.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-D7ogyaZr.js → DockerLogsModal-DjAZ3GL7.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-BFzUOmwz.js → EmbeddedEditor-Cihkr8Xb.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-C_Yc5Ypv.js → GmailOAuthSetup-G1XqiM44.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-CefQSOYm.js → GoogleOAuthSetup-Ce6gojdm.js} +1 -1
  9. package/dist/assets/{IframeModal-BbeuiE04.js → IframeModal-CSSZJzPv.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-xh3TgJLH.js → IntegrationsPanel-EspPH09n.js} +2 -2
  11. package/dist/assets/{LogViewerModal-CE2L8nWJ.js → LogViewerModal-Bn-HDysM.js} +1 -1
  12. package/dist/assets/{MonitoringModal-A2SN3uAJ.js → MonitoringModal-DuSLTKVR.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-DdlhCa8u.js → PM2LogsModal-Bo2OtaQd.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-BvHo-OxE.js → RestoreArchivedAreaModal-BOx-V6tl.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-t6RuUegr.js → Scene2DCanvas-BIUbi14G.js} +1 -1
  16. package/dist/assets/{SceneManager-TQ2_AvFn.js → SceneManager-CIWrGc-K.js} +4 -4
  17. package/dist/assets/{SkillsPanel-PjzjcaoG.js → SkillsPanel-_KHtTDhC.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-ms4r32Z8.js → SlackMultiInstanceSetup-DbQ3rUtd.js} +1 -1
  19. package/dist/assets/{SpawnModal-B1FS8m9h.js → SpawnModal-CYEoOMPW.js} +1 -1
  20. package/dist/assets/{StatisticsModal-mmz8hnD5.js → StatisticsModal-BJ-coeis.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-BXoG-Xgd.js → SubordinateAssignmentModal-5Z3jvbM9.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-ODJyjvY-.js → TriggerManagerPanel-DvC0xP9r.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-9wtY0Tn3.js → WorkflowEditorPanel-6ym1_csr.js} +1 -1
  24. package/dist/assets/{area-logos-BLyzmh47.js → area-logos-mWPCtCnf.js} +1 -1
  25. package/dist/assets/{index-CN2p2fxH.js → index-8VVbTmtb.js} +1 -1
  26. package/dist/assets/index-BPjQZacc.css +1 -0
  27. package/dist/assets/index-C34l72MW.js +1 -0
  28. package/dist/assets/{index-1hwiSf7o.js → index-CS8OutmQ.js} +1 -1
  29. package/dist/assets/{index-Cf-PINah.js → index-CT2vtbed.js} +3 -3
  30. package/dist/assets/{index-D8vFfLs4.js → index-CY_NzVPm.js} +1 -1
  31. package/dist/assets/index-CiKbhMfN.js +1 -0
  32. package/dist/assets/{index-Ci2ZKu0n.js → index-CnESn83U.js} +2 -2
  33. package/dist/assets/{index-CBkh8xcE.js → index-DZiNmiyC.js} +1 -1
  34. package/dist/assets/{index-CdbLxNJr.js → index-DxyZ-rYT.js} +1 -1
  35. package/dist/assets/main-BHBs2E1L.css +1 -0
  36. package/dist/assets/{main-9TcmAtcq.js → main-D5j8Ul0m.js} +113 -113
  37. package/dist/assets/{web-BVIPIXA1.js → web-BvAQVbIF.js} +1 -1
  38. package/dist/assets/{web-BAkfcL4w.js → web-Cd0-XOkz.js} +1 -1
  39. package/dist/assets/{web-Cz3TYK-R.js → web-DMxl9AVF.js} +1 -1
  40. package/dist/build-info.json +1 -1
  41. package/dist/index.html +2 -2
  42. package/dist/locales/de/config.json +1 -0
  43. package/dist/locales/en/config.json +5 -1
  44. package/dist/locales/en/terminal.json +7 -1
  45. package/dist/locales/es/config.json +4 -1
  46. package/dist/locales/es/terminal.json +8 -0
  47. package/dist/locales/fr/config.json +1 -0
  48. package/dist/locales/hi/config.json +1 -0
  49. package/dist/locales/it/config.json +1 -0
  50. package/dist/locales/ja/config.json +1 -0
  51. package/dist/locales/pt/config.json +1 -0
  52. package/dist/locales/ru/config.json +1 -0
  53. package/dist/locales/zh-CN/config.json +1 -0
  54. package/dist/src/packages/server/codex/app-server/app-server-event-adapter.js +222 -0
  55. package/dist/src/packages/server/codex/app-server/app-server-process.js +304 -0
  56. package/dist/src/packages/server/codex/app-server/app-server-recovery-store.js +76 -0
  57. package/dist/src/packages/server/codex/app-server/codex-app-server-runner.js +453 -0
  58. package/dist/src/packages/server/routes/agents.js +29 -1
  59. package/dist/src/packages/server/routes/system.js +110 -1
  60. package/dist/src/packages/server/runtime/codex-runner-router.js +99 -0
  61. package/dist/src/packages/server/runtime/codex-runtime-provider.js +5 -3
  62. package/dist/src/packages/server/services/claude-credentials-service.js +208 -1
  63. package/dist/src/packages/server/services/claude-usage-service.js +35 -28
  64. package/dist/src/packages/server/services/provider-credentials-service.js +338 -0
  65. package/dist/src/packages/server/services/system-prompt-service.js +46 -0
  66. package/package.json +1 -1
  67. package/dist/assets/index-CbfoJ_Vc.js +0 -1
  68. package/dist/assets/index-E5IMeM80.js +0 -1
  69. package/dist/assets/index-XI0_ICy6.css +0 -1
  70. package/dist/assets/main-DP0iv8Le.css +0 -1
@@ -0,0 +1,99 @@
1
+ /**
2
+ * CodexRunnerRouter
3
+ *
4
+ * Codex agents can run two ways: the default per-turn `codex exec` runner (the
5
+ * ClaudeRunner shell driving a CodexBackend), or the experimental persistent
6
+ * `codex app-server` runner that streams word-by-word agent-message deltas.
7
+ * Which one a NEW launch uses is decided per-run from the LIVE setting
8
+ * (isCodexAppServerModeEnabled) — no server restart needed to switch, mirroring
9
+ * ClaudeRunnerRouter's headless/interactive split.
10
+ *
11
+ * The router owns one instance of each runner and delegates the RuntimeRunner
12
+ * surface to whichever currently owns the agent (live thread/process wins over
13
+ * the setting), tearing the other down on a mode switch so one agent never has
14
+ * two live backends.
15
+ */
16
+ import { ClaudeRunner } from '../claude/runner.js';
17
+ import { CodexBackend } from '../codex/backend.js';
18
+ import { CodexAppServerRunner } from '../codex/app-server/codex-app-server-runner.js';
19
+ import { isCodexAppServerModeEnabled } from '../services/system-prompt-service.js';
20
+ import { createLogger } from '../utils/logger.js';
21
+ const log = createLogger('CodexRouter');
22
+ export class CodexRunnerRouter {
23
+ exec;
24
+ appServer;
25
+ owner = new Map();
26
+ constructor(callbacks) {
27
+ this.exec = new ClaudeRunner(callbacks, new CodexBackend());
28
+ this.appServer = new CodexAppServerRunner(callbacks);
29
+ }
30
+ currentMode() {
31
+ return isCodexAppServerModeEnabled() ? 'app-server' : 'exec';
32
+ }
33
+ runnerFor(mode) {
34
+ return mode === 'app-server' ? this.appServer : this.exec;
35
+ }
36
+ ownerOf(agentId) {
37
+ if (this.appServer.isRunning(agentId))
38
+ return 'app-server';
39
+ if (this.exec.isRunning(agentId))
40
+ return 'exec';
41
+ return this.owner.get(agentId);
42
+ }
43
+ ownerRunner(agentId) {
44
+ return this.runnerFor(this.ownerOf(agentId) ?? this.currentMode());
45
+ }
46
+ start() {
47
+ this.exec.start?.();
48
+ this.appServer.start?.();
49
+ }
50
+ async run(request) {
51
+ const mode = this.currentMode();
52
+ const prev = this.ownerOf(request.agentId);
53
+ if (prev && prev !== mode) {
54
+ log.log(`Agent ${request.agentId.slice(0, 8)} switching ${prev} → ${mode}; stopping old runner first`);
55
+ await this.runnerFor(prev).stop(request.agentId, false);
56
+ }
57
+ this.owner.set(request.agentId, mode);
58
+ return this.runnerFor(mode).run(request);
59
+ }
60
+ sendMessage(agentId, message) {
61
+ return this.ownerRunner(agentId).sendMessage(agentId, message);
62
+ }
63
+ async stop(agentId, clearQueue) {
64
+ await this.exec.stop(agentId, clearQueue);
65
+ await this.appServer.stop(agentId, clearQueue);
66
+ this.owner.delete(agentId);
67
+ }
68
+ async stopAll(killProcesses, clearQueue) {
69
+ await this.exec.stopAll(killProcesses, clearQueue);
70
+ await this.appServer.stopAll(killProcesses, clearQueue);
71
+ this.owner.clear();
72
+ }
73
+ isRunning(agentId) {
74
+ return this.exec.isRunning(agentId) || this.appServer.isRunning(agentId);
75
+ }
76
+ hasRecentActivity(agentId, withinMs) {
77
+ return this.ownerRunner(agentId).hasRecentActivity(agentId, withinMs);
78
+ }
79
+ onNextActivity(agentId, callback) {
80
+ this.ownerRunner(agentId).onNextActivity(agentId, callback);
81
+ }
82
+ supportsStdin() {
83
+ return true;
84
+ }
85
+ closesStdinAfterPrompt() {
86
+ // Mode-dependent: exec closes stdin per turn, app-server keeps it open. The
87
+ // caller uses this to decide queue-vs-write; report per current owner so the
88
+ // right delivery path is chosen for the agent that's actually running.
89
+ return this.currentMode() === 'exec';
90
+ }
91
+ getTurnState(agentId) {
92
+ return this.ownerRunner(agentId).getTurnState?.(agentId);
93
+ }
94
+ getActiveProcessesState() {
95
+ const execState = this.exec.getActiveProcessesState().map((p) => ({ agentId: p.agentId, pid: p.pid }));
96
+ const appState = this.appServer.getActiveProcessesState();
97
+ return [...execState, ...appState];
98
+ }
99
+ }
@@ -1,9 +1,11 @@
1
- import { ClaudeRunner } from '../claude/runner.js';
2
- import { CodexBackend } from '../codex/backend.js';
1
+ import { CodexRunnerRouter } from './codex-runner-router.js';
3
2
  class CodexRuntimeProvider {
4
3
  name = 'codex';
5
4
  createRunner(callbacks) {
6
- return new ClaudeRunner(callbacks, new CodexBackend());
5
+ // The router picks per-launch between the default `codex exec` runner and
6
+ // the experimental persistent `codex app-server` runner (streaming), driven
7
+ // by the live isCodexAppServerModeEnabled() setting.
8
+ return new CodexRunnerRouter(callbacks);
7
9
  }
8
10
  }
9
11
  export function createCodexRuntimeProvider() {
@@ -14,7 +14,7 @@ import * as os from 'os';
14
14
  import * as path from 'path';
15
15
  import * as crypto from 'crypto';
16
16
  import { createLogger } from '../utils/logger.js';
17
- import { resetClaudeRateLimitCache } from './claude-usage-service.js';
17
+ import { fetchClaudeRateLimitsForToken, resetClaudeRateLimitCache, } from './claude-usage-service.js';
18
18
  const log = createLogger('ClaudeCredentials');
19
19
  const CLAUDE_DIR = path.join(os.homedir(), '.claude');
20
20
  const ACTIVE_FILE = '.credentials.json';
@@ -157,6 +157,213 @@ export function listClaudeCredentialProfiles() {
157
157
  });
158
158
  return { claudeDir: dir, active, profiles };
159
159
  }
160
+ // The Claude Code CLI's public OAuth client (PKCE, no secret) — same values
161
+ // the CLI itself uses to refresh ~/.claude/.credentials.json.
162
+ const OAUTH_TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token';
163
+ const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
164
+ const OAUTH_REFRESH_TIMEOUT_MS = 10_000;
165
+ const GRANT_USAGE_TTL_OK_MS = 2 * 60 * 1000;
166
+ const GRANT_USAGE_TTL_ERR_MS = 45 * 1000;
167
+ // After a failed refresh, leave the grant alone for a while — retrying a
168
+ // rotated/invalid refresh token can trip reuse detection and revoke the grant.
169
+ const GRANT_REFRESH_FAIL_COOLDOWN_MS = 30 * 60 * 1000;
170
+ const grantUsageCache = new Map();
171
+ const grantUsageInFlight = new Map();
172
+ export function resetClaudeProfileUsageCacheForTests() {
173
+ grantUsageCache.clear();
174
+ grantUsageInFlight.clear();
175
+ }
176
+ /** Read the OAuth tokens from a credentials file (they never leave the server). */
177
+ function readProfileTokens(filePath) {
178
+ try {
179
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
180
+ const oauth = data?.claudeAiOauth;
181
+ return {
182
+ accessToken: typeof oauth?.accessToken === 'string' && oauth.accessToken ? oauth.accessToken : null,
183
+ expiresAt: typeof oauth?.expiresAt === 'number' ? oauth.expiresAt : null,
184
+ refreshToken: typeof oauth?.refreshToken === 'string' && oauth.refreshToken ? oauth.refreshToken : null,
185
+ refreshTokenExpiresAt: typeof oauth?.refreshTokenExpiresAt === 'number' ? oauth.refreshTokenExpiresAt : null,
186
+ };
187
+ }
188
+ catch {
189
+ return { accessToken: null, expiresAt: null, refreshToken: null, refreshTokenExpiresAt: null };
190
+ }
191
+ }
192
+ /** Refresh an OAuth grant the way the CLI does. Returns tokens or an error string. */
193
+ async function refreshOAuthGrant(refreshToken) {
194
+ try {
195
+ const response = await fetch(OAUTH_TOKEN_URL, {
196
+ method: 'POST',
197
+ headers: { 'Content-Type': 'application/json' },
198
+ body: JSON.stringify({
199
+ grant_type: 'refresh_token',
200
+ refresh_token: refreshToken,
201
+ client_id: OAUTH_CLIENT_ID,
202
+ }),
203
+ signal: AbortSignal.timeout(OAUTH_REFRESH_TIMEOUT_MS),
204
+ });
205
+ if (!response.ok) {
206
+ return { error: `Token refresh failed (${response.status}) — sign in to this account again` };
207
+ }
208
+ const body = await response.json();
209
+ const accessToken = typeof body.access_token === 'string' ? body.access_token : '';
210
+ if (!accessToken)
211
+ return { error: 'Token refresh returned no access token' };
212
+ const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : 3600;
213
+ return {
214
+ accessToken,
215
+ refreshToken: typeof body.refresh_token === 'string' && body.refresh_token ? body.refresh_token : null,
216
+ expiresAt: Date.now() + expiresIn * 1000,
217
+ };
218
+ }
219
+ catch (err) {
220
+ const reason = err?.name === 'TimeoutError' ? 'request timed out' : (err?.message ?? 'request failed');
221
+ return { error: `Could not reach the OAuth token endpoint (${reason})` };
222
+ }
223
+ }
224
+ /** Write refreshed tokens into a credentials file, preserving everything else. */
225
+ function writeRefreshedTokens(filePath, refreshed) {
226
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
227
+ const oauth = (data.claudeAiOauth ?? {});
228
+ oauth.accessToken = refreshed.accessToken;
229
+ oauth.expiresAt = refreshed.expiresAt;
230
+ if (refreshed.refreshToken)
231
+ oauth.refreshToken = refreshed.refreshToken;
232
+ data.claudeAiOauth = oauth;
233
+ writeCredentialsAtomic(filePath, JSON.stringify(data));
234
+ }
235
+ function grantCacheKey(token) {
236
+ return crypto.createHash('sha256').update(token).digest('hex').slice(0, 12);
237
+ }
238
+ /**
239
+ * Fetch (refreshing first when needed and allowed) the gauges for one grant.
240
+ * Cached per grant so repeated listings don't hammer Anthropic.
241
+ */
242
+ function fetchUsageForGrant(cacheKey, group) {
243
+ const cached = grantUsageCache.get(cacheKey);
244
+ if (cached && Date.now() < cached.validUntil)
245
+ return Promise.resolve(cached);
246
+ const inFlight = grantUsageInFlight.get(cacheKey);
247
+ if (inFlight)
248
+ return inFlight;
249
+ const promise = (async () => {
250
+ const now = Date.now();
251
+ const { tokens } = group;
252
+ let accessToken = tokens.accessToken;
253
+ let error = null;
254
+ const accessExpired = typeof tokens.expiresAt === 'number' && tokens.expiresAt <= now;
255
+ if (accessExpired) {
256
+ if (group.includesActive) {
257
+ // The CLI owns the active grant; it refreshes on its next session.
258
+ accessToken = null;
259
+ error = 'Token expired — start any Claude session to refresh it';
260
+ }
261
+ else if (!tokens.refreshToken) {
262
+ accessToken = null;
263
+ error = 'Token expired and no refresh token stored — sign in to this account again';
264
+ }
265
+ else if (typeof tokens.refreshTokenExpiresAt === 'number' && tokens.refreshTokenExpiresAt <= now) {
266
+ accessToken = null;
267
+ error = 'Session expired — sign in to this account again';
268
+ }
269
+ else {
270
+ const refreshed = await refreshOAuthGrant(tokens.refreshToken);
271
+ if ('error' in refreshed) {
272
+ accessToken = null;
273
+ error = refreshed.error;
274
+ const entry = {
275
+ rateLimits: null,
276
+ error,
277
+ fetchedAt: Date.now(),
278
+ validUntil: Date.now() + GRANT_REFRESH_FAIL_COOLDOWN_MS,
279
+ };
280
+ grantUsageCache.set(cacheKey, entry);
281
+ return entry;
282
+ }
283
+ for (const filePath of group.paths) {
284
+ try {
285
+ writeRefreshedTokens(filePath, refreshed);
286
+ }
287
+ catch (err) {
288
+ log.warn(`Refreshed grant but failed to persist to ${filePath}: ${err}`);
289
+ }
290
+ }
291
+ log.log(`Refreshed dormant Claude credentials for profile(s): ${group.ids.join(', ')}`);
292
+ accessToken = refreshed.accessToken;
293
+ }
294
+ }
295
+ if (!accessToken) {
296
+ const entry = {
297
+ rateLimits: null,
298
+ error: error ?? 'No OAuth token in credentials file',
299
+ fetchedAt: Date.now(),
300
+ validUntil: Date.now() + GRANT_USAGE_TTL_ERR_MS,
301
+ };
302
+ grantUsageCache.set(cacheKey, entry);
303
+ return entry;
304
+ }
305
+ const result = await fetchClaudeRateLimitsForToken(accessToken);
306
+ const entry = {
307
+ rateLimits: result.rateLimits,
308
+ error: result.error,
309
+ fetchedAt: Date.now(),
310
+ validUntil: Date.now() + (result.error ? GRANT_USAGE_TTL_ERR_MS : GRANT_USAGE_TTL_OK_MS),
311
+ };
312
+ grantUsageCache.set(cacheKey, entry);
313
+ return entry;
314
+ })().finally(() => grantUsageInFlight.delete(cacheKey));
315
+ grantUsageInFlight.set(cacheKey, promise);
316
+ return promise;
317
+ }
318
+ /**
319
+ * Session (5h) + weekly rate-limit gauges for every stored credential profile,
320
+ * keyed by profile id ("active" or name). Dormant grants are refreshed first
321
+ * (once per grant, tokens persisted back to every copy) so limits stay visible
322
+ * for accounts that haven't run in days.
323
+ */
324
+ export async function getClaudeCredentialProfilesUsage() {
325
+ const list = listClaudeCredentialProfiles();
326
+ const candidates = [...(list.active ? [list.active] : []), ...list.profiles];
327
+ // Group profiles by grant: same refresh token (or same access token when a
328
+ // file has no refresh token) = same login, refreshed and fetched once.
329
+ const groups = new Map();
330
+ const usage = [];
331
+ for (const meta of candidates) {
332
+ if (!meta.valid) {
333
+ usage.push({ id: meta.id, rateLimits: null, error: 'Invalid credentials file', fetchedAt: Date.now() });
334
+ continue;
335
+ }
336
+ const tokens = readProfileTokens(meta.path);
337
+ const grantToken = tokens.refreshToken ?? tokens.accessToken;
338
+ if (!grantToken) {
339
+ usage.push({ id: meta.id, rateLimits: null, error: 'No OAuth token in credentials file', fetchedAt: Date.now() });
340
+ continue;
341
+ }
342
+ const key = grantCacheKey(grantToken);
343
+ const existing = groups.get(key);
344
+ if (existing) {
345
+ existing.ids.push(meta.id);
346
+ if (!existing.paths.includes(meta.path))
347
+ existing.paths.push(meta.path);
348
+ existing.includesActive = existing.includesActive || meta.id === 'active';
349
+ // Prefer a non-expired access token from any copy of the grant.
350
+ const expired = typeof existing.tokens.expiresAt === 'number' && existing.tokens.expiresAt <= Date.now();
351
+ if (expired && tokens.accessToken && (tokens.expiresAt ?? 0) > Date.now()) {
352
+ existing.tokens = tokens;
353
+ }
354
+ }
355
+ else {
356
+ groups.set(key, { ids: [meta.id], paths: [meta.path], includesActive: meta.id === 'active', tokens });
357
+ }
358
+ }
359
+ await Promise.all(Array.from(groups.entries()).map(async ([key, group]) => {
360
+ const entry = await fetchUsageForGrant(key, group);
361
+ for (const id of group.ids) {
362
+ usage.push({ id, rateLimits: entry.rateLimits, error: entry.error, fetchedAt: entry.fetchedAt });
363
+ }
364
+ }));
365
+ return { usage };
366
+ }
160
367
  function assertValidName(name) {
161
368
  if (!isValidProfileName(name)) {
162
369
  throw new Error(`Invalid profile name "${name}". Use 1–64 chars: letters, digits, dot, underscore, hyphen; must start with alphanumeric.`);
@@ -304,35 +304,12 @@ function parseRetryAfterMs(headerValue) {
304
304
  return undefined;
305
305
  }
306
306
  /**
307
- * Fetch the live session/weekly rate-limit gauges the data behind the
308
- * progress bars in the CLI's `/usage` panel using the OAuth token the CLI
309
- * keeps in `~/.claude/.credentials.json`. Returns the gauges or an error
310
- * string (never throws); the caller folds either into the snapshot.
311
- *
312
- * This is the RAW network call. Go through `getClaudeRateLimits()` instead —
313
- * it caches, dedupes, and rate-limits calls to this so we don't hammer (and get
314
- * 429'd by) the Anthropic endpoint.
307
+ * Fetch the `/usage` gauges for an arbitrary Claude OAuth access token.
308
+ * RAW network call, no caching exported for the multi-account switcher,
309
+ * which shows each stored profile's session/weekly limits and does its own
310
+ * per-fingerprint caching (claude-credentials-service).
315
311
  */
316
- async function fetchClaudeRateLimitsFromApi() {
317
- let accessToken;
318
- try {
319
- if (!fs.existsSync(CREDENTIALS_PATH)) {
320
- return { rateLimits: null, error: 'No Claude CLI credentials found' };
321
- }
322
- const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf-8'));
323
- const oauth = credentials?.claudeAiOauth;
324
- if (!oauth || typeof oauth.accessToken !== 'string' || oauth.accessToken === '') {
325
- return { rateLimits: null, error: 'Claude CLI credentials are missing an OAuth token' };
326
- }
327
- if (typeof oauth.expiresAt === 'number' && oauth.expiresAt <= Date.now()) {
328
- return { rateLimits: null, error: 'Claude CLI OAuth token has expired — run any Claude session to refresh it' };
329
- }
330
- accessToken = oauth.accessToken;
331
- }
332
- catch (err) {
333
- log.warn(`Failed to read Claude credentials at ${CREDENTIALS_PATH}: ${err}`);
334
- return { rateLimits: null, error: 'Failed to read Claude CLI credentials' };
335
- }
312
+ export async function fetchClaudeRateLimitsForToken(accessToken) {
336
313
  try {
337
314
  const response = await fetch(OAUTH_USAGE_URL, {
338
315
  headers: {
@@ -368,6 +345,36 @@ async function fetchClaudeRateLimitsFromApi() {
368
345
  return { rateLimits: null, error: `Could not reach Anthropic usage endpoint (${reason})` };
369
346
  }
370
347
  }
348
+ /**
349
+ * Fetch the live session/weekly rate-limit gauges — the data behind the
350
+ * progress bars in the CLI's `/usage` panel — using the OAuth token the CLI
351
+ * keeps in `~/.claude/.credentials.json`. Returns the gauges or an error
352
+ * string (never throws); the caller folds either into the snapshot.
353
+ *
354
+ * This is the RAW network call. Go through `getClaudeRateLimits()` instead —
355
+ * it caches, dedupes, and rate-limits calls to this so we don't hammer (and get
356
+ * 429'd by) the Anthropic endpoint.
357
+ */
358
+ async function fetchClaudeRateLimitsFromApi() {
359
+ try {
360
+ if (!fs.existsSync(CREDENTIALS_PATH)) {
361
+ return { rateLimits: null, error: 'No Claude CLI credentials found' };
362
+ }
363
+ const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf-8'));
364
+ const oauth = credentials?.claudeAiOauth;
365
+ if (!oauth || typeof oauth.accessToken !== 'string' || oauth.accessToken === '') {
366
+ return { rateLimits: null, error: 'Claude CLI credentials are missing an OAuth token' };
367
+ }
368
+ if (typeof oauth.expiresAt === 'number' && oauth.expiresAt <= Date.now()) {
369
+ return { rateLimits: null, error: 'Claude CLI OAuth token has expired — run any Claude session to refresh it' };
370
+ }
371
+ return await fetchClaudeRateLimitsForToken(oauth.accessToken);
372
+ }
373
+ catch (err) {
374
+ log.warn(`Failed to read Claude credentials at ${CREDENTIALS_PATH}: ${err}`);
375
+ return { rateLimits: null, error: 'Failed to read Claude CLI credentials' };
376
+ }
377
+ }
371
378
  // Account-wide, so a single cache line serves every agent/client. Never keyed.
372
379
  let rateLimitCache = null;
373
380
  // Last successful gauges, kept so we can serve them (stale) while backing off —