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.
- package/CHANGELOG.md +15 -0
- package/dist/assets/{BossLogsModal-Dgz-N5gw.js → BossLogsModal-Bo0pUCWU.js} +1 -1
- package/dist/assets/{BossSpawnModal-BK_rYu8s.js → BossSpawnModal-BOOHaqaD.js} +1 -1
- package/dist/assets/{ControlsModal-hC_AiCpk.js → ControlsModal-DItGV-CW.js} +1 -1
- package/dist/assets/{DockerLogsModal-D7ogyaZr.js → DockerLogsModal-DjAZ3GL7.js} +1 -1
- package/dist/assets/{EmbeddedEditor-BFzUOmwz.js → EmbeddedEditor-Cihkr8Xb.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-C_Yc5Ypv.js → GmailOAuthSetup-G1XqiM44.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-CefQSOYm.js → GoogleOAuthSetup-Ce6gojdm.js} +1 -1
- package/dist/assets/{IframeModal-BbeuiE04.js → IframeModal-CSSZJzPv.js} +1 -1
- package/dist/assets/{IntegrationsPanel-xh3TgJLH.js → IntegrationsPanel-EspPH09n.js} +2 -2
- package/dist/assets/{LogViewerModal-CE2L8nWJ.js → LogViewerModal-Bn-HDysM.js} +1 -1
- package/dist/assets/{MonitoringModal-A2SN3uAJ.js → MonitoringModal-DuSLTKVR.js} +1 -1
- package/dist/assets/{PM2LogsModal-DdlhCa8u.js → PM2LogsModal-Bo2OtaQd.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-BvHo-OxE.js → RestoreArchivedAreaModal-BOx-V6tl.js} +1 -1
- package/dist/assets/{Scene2DCanvas-t6RuUegr.js → Scene2DCanvas-BIUbi14G.js} +1 -1
- package/dist/assets/{SceneManager-TQ2_AvFn.js → SceneManager-CIWrGc-K.js} +4 -4
- package/dist/assets/{SkillsPanel-PjzjcaoG.js → SkillsPanel-_KHtTDhC.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-ms4r32Z8.js → SlackMultiInstanceSetup-DbQ3rUtd.js} +1 -1
- package/dist/assets/{SpawnModal-B1FS8m9h.js → SpawnModal-CYEoOMPW.js} +1 -1
- package/dist/assets/{StatisticsModal-mmz8hnD5.js → StatisticsModal-BJ-coeis.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-BXoG-Xgd.js → SubordinateAssignmentModal-5Z3jvbM9.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-ODJyjvY-.js → TriggerManagerPanel-DvC0xP9r.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-9wtY0Tn3.js → WorkflowEditorPanel-6ym1_csr.js} +1 -1
- package/dist/assets/{area-logos-BLyzmh47.js → area-logos-mWPCtCnf.js} +1 -1
- package/dist/assets/{index-CN2p2fxH.js → index-8VVbTmtb.js} +1 -1
- package/dist/assets/index-BPjQZacc.css +1 -0
- package/dist/assets/index-C34l72MW.js +1 -0
- package/dist/assets/{index-1hwiSf7o.js → index-CS8OutmQ.js} +1 -1
- package/dist/assets/{index-Cf-PINah.js → index-CT2vtbed.js} +3 -3
- package/dist/assets/{index-D8vFfLs4.js → index-CY_NzVPm.js} +1 -1
- package/dist/assets/index-CiKbhMfN.js +1 -0
- package/dist/assets/{index-Ci2ZKu0n.js → index-CnESn83U.js} +2 -2
- package/dist/assets/{index-CBkh8xcE.js → index-DZiNmiyC.js} +1 -1
- package/dist/assets/{index-CdbLxNJr.js → index-DxyZ-rYT.js} +1 -1
- package/dist/assets/main-BHBs2E1L.css +1 -0
- package/dist/assets/{main-9TcmAtcq.js → main-D5j8Ul0m.js} +113 -113
- package/dist/assets/{web-BVIPIXA1.js → web-BvAQVbIF.js} +1 -1
- package/dist/assets/{web-BAkfcL4w.js → web-Cd0-XOkz.js} +1 -1
- package/dist/assets/{web-Cz3TYK-R.js → web-DMxl9AVF.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +1 -0
- package/dist/locales/en/config.json +5 -1
- package/dist/locales/en/terminal.json +7 -1
- package/dist/locales/es/config.json +4 -1
- package/dist/locales/es/terminal.json +8 -0
- package/dist/locales/fr/config.json +1 -0
- package/dist/locales/hi/config.json +1 -0
- package/dist/locales/it/config.json +1 -0
- package/dist/locales/ja/config.json +1 -0
- package/dist/locales/pt/config.json +1 -0
- package/dist/locales/ru/config.json +1 -0
- package/dist/locales/zh-CN/config.json +1 -0
- package/dist/src/packages/server/codex/app-server/app-server-event-adapter.js +222 -0
- package/dist/src/packages/server/codex/app-server/app-server-process.js +304 -0
- package/dist/src/packages/server/codex/app-server/app-server-recovery-store.js +76 -0
- package/dist/src/packages/server/codex/app-server/codex-app-server-runner.js +453 -0
- package/dist/src/packages/server/routes/agents.js +29 -1
- package/dist/src/packages/server/routes/system.js +110 -1
- package/dist/src/packages/server/runtime/codex-runner-router.js +99 -0
- package/dist/src/packages/server/runtime/codex-runtime-provider.js +5 -3
- package/dist/src/packages/server/services/claude-credentials-service.js +208 -1
- package/dist/src/packages/server/services/claude-usage-service.js +35 -28
- package/dist/src/packages/server/services/provider-credentials-service.js +338 -0
- package/dist/src/packages/server/services/system-prompt-service.js +46 -0
- package/package.json +1 -1
- package/dist/assets/index-CbfoJ_Vc.js +0 -1
- package/dist/assets/index-E5IMeM80.js +0 -1
- package/dist/assets/index-XI0_ICy6.css +0 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
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
|
|
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 —
|