tide-commander 1.153.0 → 1.155.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 (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/{BossLogsModal-Dgz-N5gw.js → BossLogsModal-OFobifVq.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-BK_rYu8s.js → BossSpawnModal-Dho-x6rm.js} +1 -1
  4. package/dist/assets/{ControlsModal-hC_AiCpk.js → ControlsModal-DJBDhvNO.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-D7ogyaZr.js → DockerLogsModal-C9TRTcsO.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-BFzUOmwz.js → EmbeddedEditor-GDE_Khwz.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-C_Yc5Ypv.js → GmailOAuthSetup-D9m5zMWC.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-CefQSOYm.js → GoogleOAuthSetup-BxkN7LjY.js} +1 -1
  9. package/dist/assets/{IframeModal-BbeuiE04.js → IframeModal-Do_I_wI7.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-xh3TgJLH.js → IntegrationsPanel-jRrwC2-G.js} +2 -2
  11. package/dist/assets/{LogViewerModal-CE2L8nWJ.js → LogViewerModal-BtcUe5i2.js} +1 -1
  12. package/dist/assets/{MonitoringModal-A2SN3uAJ.js → MonitoringModal-BTmSXrzv.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-DdlhCa8u.js → PM2LogsModal-CUf7NGHz.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-BvHo-OxE.js → RestoreArchivedAreaModal-B72UKeRP.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-t6RuUegr.js → Scene2DCanvas-BCfxX7JU.js} +1 -1
  16. package/dist/assets/{SceneManager-TQ2_AvFn.js → SceneManager-BQubdlgd.js} +4 -4
  17. package/dist/assets/{SkillsPanel-PjzjcaoG.js → SkillsPanel-3qreMiJs.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-ms4r32Z8.js → SlackMultiInstanceSetup-BSCT5nAB.js} +1 -1
  19. package/dist/assets/{SpawnModal-B1FS8m9h.js → SpawnModal-DaWjnugM.js} +1 -1
  20. package/dist/assets/{StatisticsModal-mmz8hnD5.js → StatisticsModal-DCjCAU2o.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-BXoG-Xgd.js → SubordinateAssignmentModal-9iPXraIi.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-ODJyjvY-.js → TriggerManagerPanel-B4dCLEi_.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-9wtY0Tn3.js → WorkflowEditorPanel-BaYXKfYH.js} +1 -1
  24. package/dist/assets/{area-logos-BLyzmh47.js → area-logos-CgVT13bp.js} +1 -1
  25. package/dist/assets/{index-CN2p2fxH.js → index-0y0WY2XK.js} +1 -1
  26. package/dist/assets/index-BPjQZacc.css +1 -0
  27. package/dist/assets/{index-CdbLxNJr.js → index-C-tbJxGb.js} +1 -1
  28. package/dist/assets/{index-1hwiSf7o.js → index-C3Yb-3TP.js} +1 -1
  29. package/dist/assets/index-C3jAbuv2.js +1 -0
  30. package/dist/assets/{index-Ci2ZKu0n.js → index-C7t_6aP3.js} +2 -2
  31. package/dist/assets/{index-CBkh8xcE.js → index-CetQ5OX1.js} +1 -1
  32. package/dist/assets/{index-Cf-PINah.js → index-D8dlRudN.js} +3 -3
  33. package/dist/assets/index-DNjdlOwh.js +1 -0
  34. package/dist/assets/{index-D8vFfLs4.js → index-DiZy3nuB.js} +1 -1
  35. package/dist/assets/main-BHBs2E1L.css +1 -0
  36. package/dist/assets/{main-9TcmAtcq.js → main-DDwa0x9e.js} +113 -113
  37. package/dist/assets/{web-BVIPIXA1.js → web-Dj2DYQG1.js} +1 -1
  38. package/dist/assets/{web-Cz3TYK-R.js → web-EBdZk56y.js} +1 -1
  39. package/dist/assets/{web-BAkfcL4w.js → web-PXjd4vZ8.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 +6 -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/opencode/json-event-parser.js +3 -1
  59. package/dist/src/packages/server/opencode/server/opencode-server-event-adapter.js +127 -0
  60. package/dist/src/packages/server/opencode/server/opencode-server-process.js +285 -0
  61. package/dist/src/packages/server/opencode/server/opencode-server-recovery-store.js +73 -0
  62. package/dist/src/packages/server/opencode/server/opencode-server-runner.js +384 -0
  63. package/dist/src/packages/server/routes/agents.js +57 -1
  64. package/dist/src/packages/server/routes/system.js +110 -1
  65. package/dist/src/packages/server/runtime/codex-runner-router.js +99 -0
  66. package/dist/src/packages/server/runtime/codex-runtime-provider.js +5 -3
  67. package/dist/src/packages/server/runtime/opencode-runner-router.js +93 -0
  68. package/dist/src/packages/server/runtime/opencode-runtime-provider.js +5 -3
  69. package/dist/src/packages/server/services/claude-credentials-service.js +208 -1
  70. package/dist/src/packages/server/services/claude-usage-service.js +35 -28
  71. package/dist/src/packages/server/services/provider-credentials-service.js +338 -0
  72. package/dist/src/packages/server/services/system-prompt-service.js +94 -0
  73. package/package.json +1 -1
  74. package/dist/assets/index-CbfoJ_Vc.js +0 -1
  75. package/dist/assets/index-E5IMeM80.js +0 -1
  76. package/dist/assets/index-XI0_ICy6.css +0 -1
  77. package/dist/assets/main-DP0iv8Le.css +0 -1
@@ -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() {
@@ -0,0 +1,93 @@
1
+ /**
2
+ * OpencodeRunnerRouter
3
+ *
4
+ * OpenCode agents can run two ways: the default per-turn `opencode run` runner
5
+ * (ClaudeRunner driving an OpencodeBackend), or the experimental persistent
6
+ * `opencode serve` runner that streams word-by-word `message.part.delta` tokens
7
+ * over SSE and survives commander restarts. A NEW launch's mode is decided
8
+ * per-run from the LIVE setting (isOpencodeServerModeEnabled) — no restart to
9
+ * switch — mirroring ClaudeRunnerRouter and CodexRunnerRouter.
10
+ */
11
+ import { ClaudeRunner } from '../claude/runner.js';
12
+ import { OpencodeBackend } from '../opencode/backend.js';
13
+ import { OpencodeServerRunner } from '../opencode/server/opencode-server-runner.js';
14
+ import { isOpencodeServerModeEnabled } from '../services/system-prompt-service.js';
15
+ import { createLogger } from '../utils/logger.js';
16
+ const log = createLogger('OpencodeRouter');
17
+ export class OpencodeRunnerRouter {
18
+ run_;
19
+ server;
20
+ owner = new Map();
21
+ constructor(callbacks) {
22
+ this.run_ = new ClaudeRunner(callbacks, new OpencodeBackend());
23
+ this.server = new OpencodeServerRunner(callbacks);
24
+ }
25
+ currentMode() {
26
+ return isOpencodeServerModeEnabled() ? 'server' : 'run';
27
+ }
28
+ runnerFor(mode) {
29
+ return mode === 'server' ? this.server : this.run_;
30
+ }
31
+ ownerOf(agentId) {
32
+ if (this.server.isRunning(agentId))
33
+ return 'server';
34
+ if (this.run_.isRunning(agentId))
35
+ return 'run';
36
+ return this.owner.get(agentId);
37
+ }
38
+ ownerRunner(agentId) {
39
+ return this.runnerFor(this.ownerOf(agentId) ?? this.currentMode());
40
+ }
41
+ start() {
42
+ this.run_.start?.();
43
+ this.server.start?.();
44
+ }
45
+ async run(request) {
46
+ const mode = this.currentMode();
47
+ const prev = this.ownerOf(request.agentId);
48
+ if (prev && prev !== mode) {
49
+ log.log(`Agent ${request.agentId.slice(0, 8)} switching ${prev} → ${mode}; stopping old runner first`);
50
+ await this.runnerFor(prev).stop(request.agentId, false);
51
+ }
52
+ this.owner.set(request.agentId, mode);
53
+ return this.runnerFor(mode).run(request);
54
+ }
55
+ sendMessage(agentId, message) {
56
+ return this.ownerRunner(agentId).sendMessage(agentId, message);
57
+ }
58
+ async stop(agentId, clearQueue) {
59
+ await this.run_.stop(agentId, clearQueue);
60
+ await this.server.stop(agentId, clearQueue);
61
+ this.owner.delete(agentId);
62
+ }
63
+ async stopAll(killProcesses, clearQueue) {
64
+ await this.run_.stopAll(killProcesses, clearQueue);
65
+ await this.server.stopAll(killProcesses, clearQueue);
66
+ this.owner.clear();
67
+ }
68
+ isRunning(agentId) {
69
+ return this.run_.isRunning(agentId) || this.server.isRunning(agentId);
70
+ }
71
+ hasRecentActivity(agentId, withinMs) {
72
+ return this.ownerRunner(agentId).hasRecentActivity(agentId, withinMs);
73
+ }
74
+ onNextActivity(agentId, callback) {
75
+ this.ownerRunner(agentId).onNextActivity(agentId, callback);
76
+ }
77
+ supportsStdin() {
78
+ return true;
79
+ }
80
+ closesStdinAfterPrompt() {
81
+ // Mode-dependent: `opencode run` closes stdin per turn, the server keeps the
82
+ // session open. Report per the current setting so the delivery path matches.
83
+ return this.currentMode() === 'run';
84
+ }
85
+ getTurnState(agentId) {
86
+ return this.ownerRunner(agentId).getTurnState?.(agentId);
87
+ }
88
+ getActiveProcessesState() {
89
+ const runState = this.run_.getActiveProcessesState().map((p) => ({ agentId: p.agentId, pid: p.pid }));
90
+ const serverState = this.server.getActiveProcessesState();
91
+ return [...runState, ...serverState];
92
+ }
93
+ }
@@ -1,9 +1,11 @@
1
- import { ClaudeRunner } from '../claude/runner.js';
2
- import { OpencodeBackend } from '../opencode/backend.js';
1
+ import { OpencodeRunnerRouter } from './opencode-runner-router.js';
3
2
  class OpencodeRuntimeProvider {
4
3
  name = 'opencode';
5
4
  createRunner(callbacks) {
6
- return new ClaudeRunner(callbacks, new OpencodeBackend());
5
+ // The router picks per-launch between the default `opencode run` runner and
6
+ // the experimental persistent `opencode serve` runner (streaming), driven by
7
+ // the live isOpencodeServerModeEnabled() setting.
8
+ return new OpencodeRunnerRouter(callbacks);
7
9
  }
8
10
  }
9
11
  export function createOpencodeRuntimeProvider() {
@@ -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 —