@modelprofile.com/authswitch 3.2.0 → 4.0.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/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/accounts.d.ts +63 -13
- package/dist_ts/accounts.js +199 -31
- package/dist_ts/classes.accountlist.d.ts +0 -24
- package/dist_ts/classes.accountlist.js +2 -213
- package/dist_ts/classes.claudecodeharness.d.ts +61 -3
- package/dist_ts/classes.claudecodeharness.js +130 -14
- package/dist_ts/classes.claudecodelocks.d.ts +35 -0
- package/dist_ts/classes.claudecodelocks.js +117 -0
- package/dist_ts/classes.claudestatus.d.ts +17 -4
- package/dist_ts/classes.claudestatus.js +89 -73
- package/dist_ts/classes.claudetokenrefresh.d.ts +32 -0
- package/dist_ts/classes.claudetokenrefresh.js +77 -0
- package/dist_ts/classes.cli.d.ts +29 -7
- package/dist_ts/classes.cli.js +172 -78
- package/dist_ts/classes.codexharness.d.ts +6 -2
- package/dist_ts/classes.codexharness.js +7 -3
- package/dist_ts/classes.codexstatus.d.ts +5 -4
- package/dist_ts/classes.codexstatus.js +36 -19
- package/dist_ts/classes.credentialstore.d.ts +28 -2
- package/dist_ts/classes.credentialstore.js +41 -12
- package/dist_ts/classes.fileharness.d.ts +39 -19
- package/dist_ts/classes.fileharness.js +58 -24
- package/dist_ts/classes.limits.d.ts +46 -7
- package/dist_ts/classes.limits.js +106 -32
- package/dist_ts/classes.listrenderer.d.ts +17 -0
- package/dist_ts/classes.listrenderer.js +313 -0
- package/dist_ts/classes.login.d.ts +1 -1
- package/dist_ts/classes.login.js +1 -1
- package/dist_ts/classes.opencodeharness.d.ts +6 -2
- package/dist_ts/classes.opencodeharness.js +9 -5
- package/dist_ts/classes.operations.js +3 -2
- package/dist_ts/classes.tui.js +10 -6
- package/dist_ts/classes.watch.d.ts +108 -0
- package/dist_ts/classes.watch.js +219 -0
- package/dist_ts/classes.watchlock.d.ts +33 -0
- package/dist_ts/classes.watchlock.js +118 -0
- package/dist_ts/claudehttp.d.ts +39 -0
- package/dist_ts/claudehttp.js +83 -0
- package/dist_ts/cliargs.d.ts +36 -0
- package/dist_ts/cliargs.js +60 -0
- package/dist_ts/consoletable.d.ts +21 -0
- package/dist_ts/consoletable.js +63 -0
- package/dist_ts/helpers.d.ts +7 -0
- package/dist_ts/helpers.js +16 -1
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/interfaces.harness.d.ts +76 -15
- package/dist_ts/interfaces.list.d.ts +7 -1
- package/dist_ts/plugins.d.ts +7 -0
- package/dist_ts/plugins.js +6 -1
- package/dist_ts/ratelimit.d.ts +8 -0
- package/dist_ts/ratelimit.js +13 -0
- package/dist_ts/watchpolicy.d.ts +44 -0
- package/dist_ts/watchpolicy.js +82 -0
- package/package.json +5 -3
- package/readme.md +360 -106
- package/ts/00_commitinfo_data.ts +3 -3
- package/ts/accounts.ts +202 -34
- package/ts/classes.accountlist.ts +2 -216
- package/ts/classes.claudecodeharness.ts +126 -13
- package/ts/classes.claudecodelocks.ts +132 -0
- package/ts/classes.claudestatus.ts +88 -54
- package/ts/classes.claudetokenrefresh.ts +85 -0
- package/ts/classes.cli.ts +163 -63
- package/ts/classes.codexharness.ts +7 -3
- package/ts/classes.codexstatus.ts +29 -18
- package/ts/classes.credentialstore.ts +53 -9
- package/ts/classes.fileharness.ts +69 -31
- package/ts/classes.limits.ts +126 -33
- package/ts/classes.listrenderer.ts +328 -0
- package/ts/classes.login.ts +1 -1
- package/ts/classes.opencodeharness.ts +9 -5
- package/ts/classes.operations.ts +2 -1
- package/ts/classes.tui.ts +9 -5
- package/ts/classes.watch.ts +263 -0
- package/ts/classes.watchlock.ts +100 -0
- package/ts/claudehttp.ts +92 -0
- package/ts/cliargs.ts +71 -0
- package/ts/consoletable.ts +62 -0
- package/ts/helpers.ts +14 -0
- package/ts/index.ts +3 -0
- package/ts/interfaces.harness.ts +78 -6
- package/ts/interfaces.list.ts +7 -1
- package/ts/plugins.ts +9 -0
- package/ts/ratelimit.ts +14 -0
- package/ts/watchpolicy.ts +121 -0
package/ts/classes.cli.ts
CHANGED
|
@@ -5,25 +5,42 @@ import { OpenCodeHarness } from './classes.opencodeharness.js';
|
|
|
5
5
|
import { ClaudeCodeHarness } from './classes.claudecodeharness.js';
|
|
6
6
|
import { AuthSwitchTui } from './classes.tui.js';
|
|
7
7
|
import { AglAuthSwitchCoordinator, AuthSwitchOperations, type TAuthSwitchCoordinator, type TAuthSwitchMutation } from './classes.operations.js';
|
|
8
|
-
import {
|
|
8
|
+
import { readAccountList } from './classes.accountlist.js';
|
|
9
|
+
import { AccountListRenderer } from './classes.listrenderer.js';
|
|
9
10
|
import { accountLimits, activeAccounts, CondensedRenderer } from './classes.limits.js';
|
|
10
|
-
import {
|
|
11
|
+
import { consoleTable } from './consoletable.js';
|
|
12
|
+
import { accountName, credentialDriftNote, duration, orderedUsageWindows, readAccountBadges, until, usagePercentText } from './accounts.js';
|
|
11
13
|
import { describeHarnessProcesses, describeStopOutcome } from './classes.harnessprocesses.js';
|
|
12
14
|
import { defaultPreusePrompt, PreuseError, validatePreuseOptions } from './preuse.js';
|
|
15
|
+
import { parseCommandArgs, parseDurationOption, parseIntegerOption, UsageError } from './cliargs.js';
|
|
16
|
+
import { AuthSwitchWatch, watchEventText } from './classes.watch.js';
|
|
17
|
+
import { WatchBusyError, WatchLock } from './classes.watchlock.js';
|
|
18
|
+
import { authSwitchHome } from './classes.credentialstore.js';
|
|
13
19
|
import type { CodexSwitcher } from './classes.codexswitcher.js';
|
|
14
20
|
import type { IAuthHarness, IHarnessAccount, IHarnessOutcome, IHarnessProcess, IHarnessState, IHarnessStopOutcome, IHarnessPreuseResult } from './interfaces.harness.js';
|
|
15
21
|
import { bold, dim, green, orange, plainText, red } from './formatting.js';
|
|
16
22
|
|
|
17
23
|
const canPrompt = (): boolean =>
|
|
18
24
|
process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
|
|
19
|
-
const COMMANDS = ['login', 'stash', 'list', 'ls', 'limits', 'active', 'use', 'preuse', 'current', 'drop', 'rm', 'doctor'];
|
|
25
|
+
const COMMANDS = ['login', 'stash', 'list', 'ls', 'limits', 'active', 'use', 'preuse', 'watch', 'current', 'drop', 'rm', 'doctor'];
|
|
20
26
|
/** What to do about the harness's own running instances before a credential file is rewritten. */
|
|
21
27
|
export type TStopMode = 'stop' | 'force-stop' | 'keep-running';
|
|
22
28
|
const STOP_FLAGS = ['--stop', '--force-stop', '--keep-running'];
|
|
23
29
|
/** Commands that replace or clear a native login, and therefore offer to stop its instances. */
|
|
24
30
|
const STOP_COMMANDS = ['use', 'stash'];
|
|
25
|
-
/**
|
|
31
|
+
/** Overviews across every registered harness; none of them changes which account is in use, and all support --json. */
|
|
26
32
|
const OVERVIEW_COMMANDS = ['list', 'ls', 'limits', 'active'];
|
|
33
|
+
const WATCH_USAGE = 'Usage: authswitch [harness] watch [harness] [--interval <duration>] [--threshold <percent>] [--dry-run] [--once] [--json]';
|
|
34
|
+
const WATCH_INTERVAL = { defaultMs: 120_000, minMs: 60_000, maxMs: 86_400_000 };
|
|
35
|
+
const WATCH_THRESHOLD = { default: 95, min: 50, max: 100 };
|
|
36
|
+
|
|
37
|
+
/** The account a command acts on, named as it was offered, with the badge it was offered with. */
|
|
38
|
+
interface IChosenAccount {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
/** ` (type:Max) (5h:63%) (w:88%)`, empty when no part is known, or null when it was not read. */
|
|
42
|
+
badge: string | null;
|
|
43
|
+
}
|
|
27
44
|
|
|
28
45
|
/** Harness-independent command routing and interactive account management. */
|
|
29
46
|
export class AuthSwitchCli {
|
|
@@ -61,6 +78,9 @@ ${bold('Usage')}
|
|
|
61
78
|
authswitch active one row per provider: which account is in use, and since when
|
|
62
79
|
authswitch <harness> preuse <account> [--prompt <text>] [--model <id>]
|
|
63
80
|
send one prompt through that account without switching
|
|
81
|
+
authswitch watch [harness] [--interval <duration>] [--threshold <percent>] [--dry-run] [--once] [--json]
|
|
82
|
+
check usage every 2m and switch to a better saved account
|
|
83
|
+
when the active one reaches the threshold (95%)
|
|
64
84
|
authswitch <harness> stash [account/provider] [--keep]
|
|
65
85
|
save one active login; --keep leaves it active
|
|
66
86
|
authswitch <harness> use [account] [--stop|--force-stop|--keep-running]
|
|
@@ -80,14 +100,24 @@ ${bold('Options')}
|
|
|
80
100
|
-v, --version show the version
|
|
81
101
|
-i, --interactive open the guided account manager
|
|
82
102
|
--tui open the terminal management dashboard
|
|
83
|
-
--json JSON output for list / ls / limits / active
|
|
103
|
+
--json JSON output for list / ls / limits / active; one JSON event per line for
|
|
104
|
+
watch, written after the command (authswitch watch --json)
|
|
84
105
|
--stop stop this harness's running instances before the switch
|
|
85
106
|
--force-stop the same, then kill instances that ignore SIGTERM
|
|
86
107
|
--keep-running switch without stopping anything
|
|
87
108
|
|
|
88
|
-
A switch works while OpenCode or Claude Code is running.
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
A switch works while OpenCode or Claude Code is running. Claude Code picks the new login
|
|
110
|
+
up on its next request, so its sessions are stopped only on --stop or --force-stop.
|
|
111
|
+
OpenCode keeps the previous login in memory and can write it back at its next token
|
|
112
|
+
refresh, so an OpenCode switch offers to stop it first; without a terminal it leaves
|
|
113
|
+
it running and says so.
|
|
114
|
+
|
|
115
|
+
Watch covers the harnesses that support automatic switching (${[...this.harnesses.values()].filter(harness => harness.autoSwitch === true).map(harness => harness.id).join(', ') || 'none registered'}).
|
|
116
|
+
--interval takes seconds or a unit (120, 90s, 2m; from 1m to 1d); --threshold takes 50 to 100.
|
|
117
|
+
A switch goes to a saved account at least 10 points below the threshold, or, when every
|
|
118
|
+
account is used up, to the one usable again first. No session of yours is stopped, though
|
|
119
|
+
a Codex switch restarts Codex' own app-server; --dry-run only reports, --once checks once.
|
|
120
|
+
One watch runs per AUTHSWITCH_HOME.
|
|
91
121
|
|
|
92
122
|
Preuse consumes the selected account's quota. Its default prompt is:
|
|
93
123
|
"${defaultPreusePrompt}"
|
|
@@ -109,6 +139,11 @@ ${bold('Environment')}
|
|
|
109
139
|
const qualified = args[0] !== 'preuse';
|
|
110
140
|
return await this.commandPreuse(qualified ? this.harnesses.get(args[0]) : undefined, args.slice(qualified ? 2 : 1));
|
|
111
141
|
}
|
|
142
|
+
// Watch owns its options, --json among them.
|
|
143
|
+
if (args[0] === 'watch' || (this.harnesses.has(args[0]) && args[1] === 'watch')) {
|
|
144
|
+
const qualified = args[0] !== 'watch';
|
|
145
|
+
return await this.commandWatch(qualified ? this.harnesses.get(args[0]) : undefined, args.slice(qualified ? 2 : 1));
|
|
146
|
+
}
|
|
112
147
|
const jsonFlags = args.filter(value => value === '--json');
|
|
113
148
|
const json = jsonFlags.length === 1;
|
|
114
149
|
if (jsonFlags.length > 1) { process.stderr.write('Use --json only once.\n'); return 2; }
|
|
@@ -222,14 +257,17 @@ ${bold('Environment')}
|
|
|
222
257
|
* Decide what happens to the harness's own running instances before its credential file changes.
|
|
223
258
|
*
|
|
224
259
|
* The switch itself no longer depends on this: it writes atomically and re-saves the outgoing
|
|
225
|
-
* login either way.
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
260
|
+
* login either way. A harness that picks up swaps live needs nothing here, so its instances are
|
|
261
|
+
* only stopped on an explicit --stop or --force-stop. Any other running instance still costs
|
|
262
|
+
* durability -- it holds the previous login in memory and can write it back at its next refresh --
|
|
263
|
+
* so the instances are named and stopping them is offered, never assumed. Without a terminal and
|
|
264
|
+
* without a flag nothing is signalled, and only pids enumerated here for this user are ever
|
|
265
|
+
* signalled at all.
|
|
229
266
|
*/
|
|
230
267
|
private async settleRunningInstances(harnessArg: IAuthHarness, modeArg?: TStopMode): Promise<void> {
|
|
231
268
|
const control = harnessArg.processes;
|
|
232
|
-
|
|
269
|
+
const stopRequested = modeArg === 'stop' || modeArg === 'force-stop';
|
|
270
|
+
if (!control || (harnessArg.liveSwap === true && !stopRequested)) return;
|
|
233
271
|
let running: IHarnessProcess[];
|
|
234
272
|
try { running = control.list(); }
|
|
235
273
|
catch { process.stdout.write(`${orange(`Could not check for running ${plainText(harnessArg.label)} processes; continuing with the switch.`)}\n`); return; }
|
|
@@ -238,7 +276,7 @@ ${bold('Environment')}
|
|
|
238
276
|
if (control.stopUnavailableReason !== null) { process.stdout.write(`${dim(control.stopUnavailableReason)}\n`); return; }
|
|
239
277
|
const stoppable = running.filter(item => !item.isAncestor);
|
|
240
278
|
if (!stoppable.length || modeArg === 'keep-running') return;
|
|
241
|
-
let stop =
|
|
279
|
+
let stop = stopRequested;
|
|
242
280
|
if (modeArg === undefined) {
|
|
243
281
|
if (!canPrompt()) { process.stdout.write(`${dim('Leaving them running. Use --stop, --force-stop or --keep-running to decide this without a terminal.')}\n`); return; }
|
|
244
282
|
stop = await this.out.prompts.ask({
|
|
@@ -286,28 +324,18 @@ ${bold('Environment')}
|
|
|
286
324
|
|
|
287
325
|
private async commandPreuse(harnessArg: IAuthHarness | undefined, argsArg: string[]): Promise<number> {
|
|
288
326
|
let reference: string | undefined;
|
|
289
|
-
let prompt
|
|
327
|
+
let prompt: string;
|
|
290
328
|
let model: string | undefined;
|
|
291
|
-
const flags = new Set<string>();
|
|
292
329
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (flags.has(flag)) throw new PreuseError(`Use ${flag} only once.`);
|
|
299
|
-
flags.add(flag);
|
|
300
|
-
const value = separator < 0 ? argsArg[++index] : argument.slice(separator + 1);
|
|
301
|
-
if (value === undefined) throw new PreuseError(`${flag} requires a value.`);
|
|
302
|
-
if (flag === '--prompt') prompt = value; else model = value;
|
|
303
|
-
} else if (argument.startsWith('-') || reference !== undefined) {
|
|
304
|
-
throw new PreuseError('Usage: authswitch [harness] preuse <account> [--prompt <text>] [--model <id>]');
|
|
305
|
-
} else reference = argument;
|
|
306
|
-
}
|
|
330
|
+
const parsed = parseCommandArgs(argsArg, { values: ['--prompt', '--model'], flags: [], maxPositionals: 1,
|
|
331
|
+
usage: 'Usage: authswitch [harness] preuse <account> [--prompt <text>] [--model <id>]' });
|
|
332
|
+
[reference] = parsed.positionals;
|
|
333
|
+
prompt = parsed.values.get('--prompt') ?? defaultPreusePrompt;
|
|
334
|
+
model = parsed.values.get('--model');
|
|
307
335
|
validatePreuseOptions({ prompt, model });
|
|
308
|
-
if (!reference && !canPrompt()) throw new
|
|
336
|
+
if (!reference && !canPrompt()) throw new UsageError('preuse requires an account outside an interactive terminal.');
|
|
309
337
|
} catch (error) {
|
|
310
|
-
process.stderr.write(`${error instanceof PreuseError ? error.message : 'Invalid preuse arguments.'}\n`);
|
|
338
|
+
process.stderr.write(`${error instanceof UsageError || error instanceof PreuseError ? error.message : 'Invalid preuse arguments.'}\n`);
|
|
311
339
|
return 2;
|
|
312
340
|
}
|
|
313
341
|
const harness = harnessArg ?? await this.selectHarness();
|
|
@@ -354,13 +382,11 @@ ${bold('Environment')}
|
|
|
354
382
|
if (windows?.length) {
|
|
355
383
|
const now = Date.now();
|
|
356
384
|
process.stdout.write('Reset schedule reported after the prompt:\n');
|
|
357
|
-
|
|
358
|
-
for (const window of windows) process.stdout.write(`${plainText(window.label)} (${duration(window.durationSeconds)})\n Used: ${window.usedPercent}%\n Reset in: ${until(window.resetAt, now)}\n`);
|
|
359
|
-
} else await this.out.table(windows, { columns: [
|
|
385
|
+
await consoleTable(this.out, windows, [
|
|
360
386
|
{ key: 'window', title: 'Window', value: row => `${plainText(row.label)} (${duration(row.durationSeconds)})` },
|
|
361
|
-
{ key: 'usage', title: 'Used', value: row =>
|
|
387
|
+
{ key: 'usage', title: 'Used', value: row => usagePercentText(row.usedPercent) },
|
|
362
388
|
{ key: 'reset', title: 'Reset in', value: row => until(row.resetAt, now) },
|
|
363
|
-
]
|
|
389
|
+
]);
|
|
364
390
|
} else process.stdout.write('Reset schedule could not be verified; the completed prompt will not be repeated.\n');
|
|
365
391
|
} catch {
|
|
366
392
|
process.stderr.write('Prompt completed, but its reset schedule could not be verified or displayed. The prompt will not be repeated.\n');
|
|
@@ -372,6 +398,57 @@ ${bold('Environment')}
|
|
|
372
398
|
}
|
|
373
399
|
}
|
|
374
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Watch usage and switch automatically; see `AuthSwitchWatch`. SIGINT and SIGTERM end it cleanly with status 0.
|
|
403
|
+
* `--once` runs one check and exits 1 when a switch it attempted did not complete.
|
|
404
|
+
*/
|
|
405
|
+
private async commandWatch(harnessArg: IAuthHarness | undefined, argsArg: string[]): Promise<number> {
|
|
406
|
+
let options: { harnesses: IAuthHarness[]; intervalMs: number; threshold: number; dryRun: boolean; once: boolean; json: boolean };
|
|
407
|
+
try {
|
|
408
|
+
const parsed = parseCommandArgs(argsArg, { values: ['--interval', '--threshold'], flags: ['--dry-run', '--once', '--json', '--help', '-h'],
|
|
409
|
+
maxPositionals: harnessArg ? 0 : 1, usage: WATCH_USAGE });
|
|
410
|
+
if (parsed.flags.has('--help') || parsed.flags.has('-h')) { process.stdout.write(`${this.usage()}\n`); return 0; }
|
|
411
|
+
const named = parsed.positionals[0];
|
|
412
|
+
const harness = harnessArg ?? (named === undefined ? undefined : this.harnesses.get(named));
|
|
413
|
+
if (named !== undefined && !harness) throw new UsageError(`Unknown harness "${plainText(named)}". ${WATCH_USAGE}`);
|
|
414
|
+
if (harness && harness.autoSwitch !== true) throw new UsageError(`${plainText(harness.label)} does not support automatic switching.`);
|
|
415
|
+
const harnesses = (harness ? [harness] : [...this.harnesses.values()].filter(item => item.autoSwitch === true))
|
|
416
|
+
.sort((left, right) => left.label.localeCompare(right.label) || left.id.localeCompare(right.id));
|
|
417
|
+
if (!harnesses.length) throw new UsageError('No registered harness supports automatic switching.');
|
|
418
|
+
const interval = parsed.values.get('--interval');
|
|
419
|
+
const threshold = parsed.values.get('--threshold');
|
|
420
|
+
options = {
|
|
421
|
+
harnesses, dryRun: parsed.flags.has('--dry-run'), once: parsed.flags.has('--once'), json: parsed.flags.has('--json'),
|
|
422
|
+
intervalMs: interval === undefined ? WATCH_INTERVAL.defaultMs : parseDurationOption('--interval', interval, WATCH_INTERVAL),
|
|
423
|
+
threshold: threshold === undefined ? WATCH_THRESHOLD.default : parseIntegerOption('--threshold', threshold, WATCH_THRESHOLD),
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (!(error instanceof UsageError)) throw error;
|
|
427
|
+
process.stderr.write(`${error.message}\n`);
|
|
428
|
+
return 2;
|
|
429
|
+
}
|
|
430
|
+
const watch = new AuthSwitchWatch({
|
|
431
|
+
harnesses: options.harnesses, operations: this.operations, lock: new WatchLock(authSwitchHome()),
|
|
432
|
+
intervalMs: options.intervalMs, threshold: options.threshold, dryRun: options.dryRun,
|
|
433
|
+
onEvent: event => { process.stdout.write(`${options.json ? JSON.stringify(event) : watchEventText(event)}\n`); },
|
|
434
|
+
});
|
|
435
|
+
const controller = new AbortController();
|
|
436
|
+
const stop = () => controller.abort();
|
|
437
|
+
process.once('SIGINT', stop);
|
|
438
|
+
process.once('SIGTERM', stop);
|
|
439
|
+
try {
|
|
440
|
+
const completed = await watch.run(controller.signal, { once: options.once });
|
|
441
|
+
return options.once && !completed ? 1 : 0;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (!(error instanceof WatchBusyError)) throw error;
|
|
444
|
+
process.stderr.write(`${error.message}\n`);
|
|
445
|
+
return 1;
|
|
446
|
+
} finally {
|
|
447
|
+
process.removeListener('SIGINT', stop);
|
|
448
|
+
process.removeListener('SIGTERM', stop);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
375
452
|
private async selectHarness(): Promise<IAuthHarness | undefined> {
|
|
376
453
|
if (this.harnesses.size === 1) return this.harnesses.values().next().value;
|
|
377
454
|
if (this.harnesses.size === 0 || !canPrompt()) {
|
|
@@ -406,7 +483,7 @@ ${bold('Environment')}
|
|
|
406
483
|
for (const current of state.accounts.filter(account => account.isActive && !account.isStashed)) {
|
|
407
484
|
const answer = await this.out.prompts.ask({
|
|
408
485
|
name: 'saveCurrent', type: 'confirm',
|
|
409
|
-
message: `${
|
|
486
|
+
message: `${accountName(current)} is not saved in ${harnessArg.label}. Save this login before switching?`, default: true,
|
|
410
487
|
});
|
|
411
488
|
if (answer !== true) return 'declined';
|
|
412
489
|
if (this.reportOutcome(await this.mutate(harnessArg, { harnessId: harnessArg.id, action: 'save', keepActive: true, accountId: current.id })) !== 0) return 'failed';
|
|
@@ -457,12 +534,12 @@ ${bold('Environment')}
|
|
|
457
534
|
case 'list': code = await this.commandList([harnessArg]); break;
|
|
458
535
|
case 'doctor': code = this.reportOutcome(await harnessArg.diagnose()); break;
|
|
459
536
|
case 'drop': {
|
|
460
|
-
const
|
|
461
|
-
if (!
|
|
537
|
+
const target = await this.promptForAccount(harnessArg, `Which saved ${harnessArg.label} account should be removed?`, { allowBack: true, includeIncomplete: true });
|
|
538
|
+
if (!target) break;
|
|
462
539
|
const confirmation = await this.out.prompts.ask({
|
|
463
|
-
name: 'confirmDrop', type: 'confirm', message: `Remove the saved login for ${plainText(id)}? The active login stays as it is.`, default: false,
|
|
540
|
+
name: 'confirmDrop', type: 'confirm', message: `Remove the saved login for ${plainText(target.id)}? The active login stays as it is.`, default: false,
|
|
464
541
|
});
|
|
465
|
-
if (confirmation === true) code = this.reportOutcome(await harnessArg.removeAccount(id));
|
|
542
|
+
if (confirmation === true) code = this.reportOutcome(await harnessArg.removeAccount(target.id));
|
|
466
543
|
break;
|
|
467
544
|
}
|
|
468
545
|
case 'harness': return null;
|
|
@@ -482,7 +559,7 @@ ${bold('Environment')}
|
|
|
482
559
|
process.stdout.write(`${dim('no account is currently active')}\n`);
|
|
483
560
|
}
|
|
484
561
|
if (stateArg.saveUnavailableReason) process.stdout.write(`${dim(stateArg.saveUnavailableReason)}\n`);
|
|
485
|
-
for (const drift of stateArg.credentialDrift ?? []) process.stdout.write(`${orange(credentialDriftNote(harnessArg
|
|
562
|
+
for (const drift of stateArg.credentialDrift ?? []) process.stdout.write(`${orange(credentialDriftNote(harnessArg, drift))}\n`);
|
|
486
563
|
}
|
|
487
564
|
|
|
488
565
|
private async selectActive(harness: IAuthHarness, reference?: string): Promise<IHarnessAccount | undefined> {
|
|
@@ -492,7 +569,7 @@ ${bold('Environment')}
|
|
|
492
569
|
if (accounts.length === 1) return accounts[0];
|
|
493
570
|
if (!accounts.length) { process.stderr.write('No matching active login to save.\n'); return undefined; }
|
|
494
571
|
if (!canPrompt()) { process.stderr.write(`Several logins are active. Use authswitch ${harness.id} stash <account/provider> [--keep].\n`); return undefined; }
|
|
495
|
-
const id = await this.out.prompts.ask({ name: 'activeAccount', type: 'list', message: 'Which active login should be saved?', choices: accounts.map(account => ({ name:
|
|
572
|
+
const id = await this.out.prompts.ask({ name: 'activeAccount', type: 'list', message: 'Which active login should be saved?', choices: accounts.map(account => ({ name: accountName(account), value: account.id })) });
|
|
496
573
|
return accounts.find(account => account.id === id);
|
|
497
574
|
}
|
|
498
575
|
|
|
@@ -503,12 +580,13 @@ ${bold('Environment')}
|
|
|
503
580
|
}
|
|
504
581
|
|
|
505
582
|
/**
|
|
506
|
-
* The condensed views are projections of the same
|
|
507
|
-
* once, per account, with the same bounded timeouts and per-account failure
|
|
583
|
+
* The condensed views are projections of the same account list, which never changes which account is in
|
|
584
|
+
* use, so a provider is queried once, per account, with the same bounded timeouts and per-account failure
|
|
585
|
+
* isolation as `list`.
|
|
508
586
|
*/
|
|
509
587
|
private async commandCondensed(commandArg: 'limits' | 'active', harnessesArg: IAuthHarness[], jsonArg: boolean): Promise<number> {
|
|
510
588
|
const list = await readAccountList(harnessesArg);
|
|
511
|
-
const document = commandArg === 'limits' ? accountLimits(list) : activeAccounts(list);
|
|
589
|
+
const document = commandArg === 'limits' ? accountLimits(list, harnessesArg) : activeAccounts(list, harnessesArg);
|
|
512
590
|
if (jsonArg) process.stdout.write(`${JSON.stringify(document, null, 2)}\n`);
|
|
513
591
|
else if ('limits' in document) await new CondensedRenderer(this.out).renderLimits(document);
|
|
514
592
|
else await new CondensedRenderer(this.out).renderActive(document);
|
|
@@ -518,17 +596,10 @@ ${bold('Environment')}
|
|
|
518
596
|
private async commandList(harnessesArg: IAuthHarness[], jsonArg = false): Promise<number> {
|
|
519
597
|
const list = await readAccountList(harnessesArg);
|
|
520
598
|
if (jsonArg) process.stdout.write(`${JSON.stringify(list, null, 2)}\n`);
|
|
521
|
-
else await new AccountListRenderer(this.out).render(list);
|
|
599
|
+
else await new AccountListRenderer(this.out).render(list, harnessesArg);
|
|
522
600
|
return list.complete ? 0 : 1;
|
|
523
601
|
}
|
|
524
602
|
|
|
525
|
-
private printAccount(accountArg: IHarnessAccount): void {
|
|
526
|
-
const saved = accountArg.isStashed ? 'saved' : 'not saved';
|
|
527
|
-
process.stdout.write(`${accountArg.isActive ? green('*') : ' '} ${bold(plainText(accountArg.label))} ${dim(`(${saved})`)}\n`);
|
|
528
|
-
if (accountArg.savedAt) process.stdout.write(` ${dim('Saved: ' + plainText(accountArg.savedAt))}\n`);
|
|
529
|
-
for (const detail of accountArg.details) process.stdout.write(` ${detail}\n`);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
603
|
private async resolveAccount(harnessArg: IAuthHarness, referenceArg: string): Promise<string | null> {
|
|
533
604
|
const resolved = await harnessArg.resolveAccount(referenceArg);
|
|
534
605
|
if ('id' in resolved) return resolved.id;
|
|
@@ -538,6 +609,14 @@ ${bold('Environment')}
|
|
|
538
609
|
return null;
|
|
539
610
|
}
|
|
540
611
|
|
|
612
|
+
/**
|
|
613
|
+
* Activate a saved account and report what is now active with its badge.
|
|
614
|
+
*
|
|
615
|
+
* The badge is read once per command through the bounded status read: the chooser reads it before it
|
|
616
|
+
* offers the accounts and the report repeats it, while a target named on the command line is read only
|
|
617
|
+
* after its switch, so nothing waits on it. A read that fails or does not answer in time leaves the name
|
|
618
|
+
* alone and never decides the outcome.
|
|
619
|
+
*/
|
|
541
620
|
private async commandUse(harnessArg: IAuthHarness, referenceArg?: string, allowBackArg = false, stopModeArg?: TStopMode): Promise<number> {
|
|
542
621
|
let id: string | null = null;
|
|
543
622
|
if (referenceArg) {
|
|
@@ -548,21 +627,42 @@ ${bold('Environment')}
|
|
|
548
627
|
const offered = await this.offerToSave(harnessArg);
|
|
549
628
|
if (offered !== 'ready') return offered === 'failed' ? 1 : 0;
|
|
550
629
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
630
|
+
const target = id === null
|
|
631
|
+
? await this.promptForAccount(harnessArg, `Which ${harnessArg.label} account should be active?`, { allowBack: allowBackArg, withBadges: true })
|
|
632
|
+
: await this.namedAccount(harnessArg, id);
|
|
633
|
+
if (target === null) return allowBackArg ? 0 : 1;
|
|
634
|
+
const code = this.reportOutcome(await this.mutate(harnessArg, { harnessId: harnessArg.id, action: 'switch', accountId: target.id }, stopModeArg));
|
|
635
|
+
if (code !== 0) return code;
|
|
636
|
+
const badge = target.badge ?? (await readAccountBadges(harnessArg, [target.id])).get(target.id) ?? '';
|
|
637
|
+
process.stdout.write(`Now active: ${bold(target.name)}${badge}\n`);
|
|
638
|
+
return code;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* A switch target named on the command line; its badge is not read here, so the switch never waits on it.
|
|
643
|
+
*
|
|
644
|
+
* The reference already resolved to this id, so the name is presentation only: a state read that fails
|
|
645
|
+
* names the account by its id and leaves the switch, which reports any real credential problem, to run.
|
|
646
|
+
*/
|
|
647
|
+
private async namedAccount(harnessArg: IAuthHarness, idArg: string): Promise<IChosenAccount> {
|
|
648
|
+
let account: IHarnessAccount | undefined;
|
|
649
|
+
try { account = (await harnessArg.readState()).accounts.find(item => item.id === idArg); }
|
|
650
|
+
catch { account = undefined; }
|
|
651
|
+
return { id: idArg, name: account ? accountName(account) : plainText(idArg), badge: null };
|
|
554
652
|
}
|
|
555
653
|
|
|
556
|
-
private async promptForAccount(harnessArg: IAuthHarness, messageArg: string,
|
|
557
|
-
const accounts = (await harnessArg.readState()).accounts.filter((accountArg) => accountArg.isStashed || (
|
|
654
|
+
private async promptForAccount(harnessArg: IAuthHarness, messageArg: string, optionsArg: { allowBack?: boolean; includeIncomplete?: boolean; withBadges?: boolean } = {}): Promise<IChosenAccount | null> {
|
|
655
|
+
const accounts = (await harnessArg.readState()).accounts.filter((accountArg) => accountArg.isStashed || (optionsArg.includeIncomplete === true && accountArg.savedAt !== null));
|
|
558
656
|
if (accounts.length === 0) { process.stderr.write(`${red('no stashed accounts')} - run \`authswitch ${harnessArg.id} stash\` first\n`); return null; }
|
|
559
657
|
if (!canPrompt()) { process.stderr.write(`${red('an account is required')} without an interactive terminal: authswitch ${harnessArg.id} use <account>\n`); return null; }
|
|
658
|
+
const badges = optionsArg.withBadges ? await readAccountBadges(harnessArg, accounts.map((accountArg) => accountArg.id)) : null;
|
|
659
|
+
const offered = accounts.map((accountArg): IChosenAccount => ({ id: accountArg.id, name: accountName(accountArg), badge: badges === null ? null : badges.get(accountArg.id) ?? '' }));
|
|
560
660
|
const answer = await this.out.prompts.ask({
|
|
561
661
|
name: 'account', type: 'list', message: messageArg,
|
|
562
662
|
default: accounts.find((accountArg) => accountArg.isActive)?.id ?? accounts[0].id,
|
|
563
|
-
choices: [...accounts.map((accountArg) => ({ name: `${
|
|
564
|
-
...(
|
|
663
|
+
choices: [...accounts.map((accountArg, index) => ({ name: `${offered[index].name}${accountArg.isActive ? ' (active)' : ''}${offered[index].badge ?? ''}`, value: accountArg.id })),
|
|
664
|
+
...(optionsArg.allowBack ? [{ name: 'Back', value: null }] : [])],
|
|
565
665
|
});
|
|
566
|
-
return
|
|
666
|
+
return offered.find((accountArg) => accountArg.id === answer) ?? null;
|
|
567
667
|
}
|
|
568
668
|
}
|
|
@@ -2,7 +2,7 @@ import { CodexSwitcher } from './classes.codexswitcher.js';
|
|
|
2
2
|
import { CodexAccountStatus } from './classes.codexstatus.js';
|
|
3
3
|
import { CodexPreuse } from './classes.codexpreuse.js';
|
|
4
4
|
import { PreuseError } from './preuse.js';
|
|
5
|
-
import type { IAuthHarness, IHarnessAccount, IHarnessAccountStatus, IHarnessOutcome, IHarnessState, IHarnessPreuseOptions, IHarnessPreuseResult } from './interfaces.harness.js';
|
|
5
|
+
import type { IAuthHarness, IHarnessAccount, IHarnessAccountStatus, IHarnessOutcome, IHarnessState, IHarnessPreuseOptions, IHarnessPreuseResult, IHarnessStatusOptions } from './interfaces.harness.js';
|
|
6
6
|
import type { IStashListing } from './interfaces.js';
|
|
7
7
|
import { countPaired, enrollmentState } from './helpers.js';
|
|
8
8
|
import { beginSavedOpenAiLogin } from './classes.login.js';
|
|
@@ -16,6 +16,10 @@ export class CodexHarness implements IAuthHarness {
|
|
|
16
16
|
public readonly loginHint = 'Use authswitch codex login to sign in and save another account.';
|
|
17
17
|
public readonly loginProviders: IHarnessLoginProvider[] = [{ providerId: 'openai', label: 'OpenAI', flows: ['device'] }];
|
|
18
18
|
public readonly diagnosticsLabel = 'Check credential storage and remote control';
|
|
19
|
+
/** Running Codex sessions keep the login they loaded; the switch itself restarts the managed app-server. */
|
|
20
|
+
public readonly liveSwap = false;
|
|
21
|
+
/** Every saved ChatGPT login reports its usage, and a switch restarts only the managed app-server. */
|
|
22
|
+
public readonly autoSwitch = true;
|
|
19
23
|
|
|
20
24
|
constructor(
|
|
21
25
|
private readonly switcher = new CodexSwitcher(),
|
|
@@ -70,10 +74,10 @@ export class CodexHarness implements IAuthHarness {
|
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
public async readAccountStatus(accountIdArg: string): Promise<IHarnessAccountStatus> {
|
|
77
|
+
public async readAccountStatus(accountIdArg: string, optionsArg?: IHarnessStatusOptions): Promise<IHarnessAccountStatus> {
|
|
74
78
|
const credential = this.readAccountCredential(accountIdArg);
|
|
75
79
|
if (!credential) return { facts: [], problems: ['No matching credential is available for status lookup.'] };
|
|
76
|
-
return this.accountStatus.read(credential.raw, credential.identity);
|
|
80
|
+
return this.accountStatus.read(credential.raw, credential.identity, optionsArg);
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
public async preuseAccount(accountIdArg: string, optionsArg: IHarnessPreuseOptions): Promise<IHarnessPreuseResult> {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ICodexIdentity } from './interfaces.js';
|
|
2
|
-
import type { IHarnessAccountStatus, IHarnessStatusSummary } from './interfaces.harness.js';
|
|
2
|
+
import type { IHarnessAccountStatus, IHarnessStatusOptions, IHarnessStatusSummary } from './interfaces.harness.js';
|
|
3
3
|
import { commitinfo } from './00_commitinfo_data.js';
|
|
4
|
+
import { latestRetryAt, retryAtFrom } from './ratelimit.js';
|
|
4
5
|
|
|
5
6
|
// Verified Chromium-compatible billing request profile, independent of any locally installed browser.
|
|
6
7
|
const billingUserAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36';
|
|
@@ -31,15 +32,20 @@ const count = (valueArg: unknown): number => {
|
|
|
31
32
|
/** Only fixed, local diagnostics may cross the HTTP error boundary. */
|
|
32
33
|
class StatusRequestError extends Error {}
|
|
33
34
|
|
|
35
|
+
/** The service refused a section for too many requests; that says nothing about the account's usage. */
|
|
36
|
+
class StatusRateLimitError extends StatusRequestError {
|
|
37
|
+
constructor(public readonly retryAt: string | null) { super('The service is rate limiting requests (HTTP 429); try again later.'); }
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* Read-only ChatGPT status contract verified against Codex 0.154.0.
|
|
36
42
|
* These source-defined backend routes are isolated here. No token refresh,
|
|
37
43
|
* credential writes, app-server lifecycle changes or reset consumption occur.
|
|
38
44
|
*/
|
|
39
45
|
export class CodexAccountStatus {
|
|
40
|
-
constructor(private readonly fetcher: typeof fetch = globalThis.fetch) {}
|
|
46
|
+
constructor(private readonly fetcher: typeof fetch = globalThis.fetch, private readonly now: () => number = Date.now) {}
|
|
41
47
|
|
|
42
|
-
public async read(rawArg: string, identityArg: ICodexIdentity): Promise<IHarnessAccountStatus> {
|
|
48
|
+
public async read(rawArg: string, identityArg: ICodexIdentity, optionsArg: IHarnessStatusOptions = {}): Promise<IHarnessAccountStatus> {
|
|
43
49
|
if (identityArg.kind !== 'chatgpt') return {
|
|
44
50
|
facts: [{ label: 'Authentication', value: 'API key' }, { label: 'Subscription and usage', value: 'Not exposed by Codex for API-key logins' }], problems: [],
|
|
45
51
|
};
|
|
@@ -47,18 +53,18 @@ export class CodexAccountStatus {
|
|
|
47
53
|
try { accessToken = text(record(record(JSON.parse(rawArg)).tokens).access_token); }
|
|
48
54
|
catch { return { facts: [], problems: ['The login has no usable access token. Log in again with Codex, then save the account.'] }; }
|
|
49
55
|
if (!identityArg.accountId) return { facts: [], problems: ['The login has no account ID for a status lookup.'] };
|
|
50
|
-
return this.readCredential(accessToken, identityArg.accountId, identityArg.planType);
|
|
56
|
+
return this.readCredential(accessToken, identityArg.accountId, identityArg.planType, optionsArg);
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
/** Read the ChatGPT account directly, independent of a harness's on-disk format. */
|
|
54
|
-
public async readCredential(accessToken: string, accountId: string, planType?: string | null): Promise<IHarnessAccountStatus> {
|
|
60
|
+
public async readCredential(accessToken: string, accountId: string, planType?: string | null, optionsArg: IHarnessStatusOptions = {}): Promise<IHarnessAccountStatus> {
|
|
55
61
|
const result: IHarnessAccountStatus = { facts: [], problems: [] };
|
|
56
62
|
const identityArg = { accountId, planType };
|
|
57
63
|
const sections = await Promise.allSettled([
|
|
58
|
-
this.get('wham/usage', accessToken, identityArg.accountId),
|
|
59
|
-
this.get('wham/rate-limit-reset-credits', accessToken, identityArg.accountId),
|
|
60
|
-
this.get('wham/profiles/me', accessToken, identityArg.accountId),
|
|
61
|
-
this.get('accounts/check/v4-2023-04-27', accessToken, identityArg.accountId),
|
|
64
|
+
this.get('wham/usage', accessToken, identityArg.accountId, optionsArg.signal),
|
|
65
|
+
this.get('wham/rate-limit-reset-credits', accessToken, identityArg.accountId, optionsArg.signal),
|
|
66
|
+
this.get('wham/profiles/me', accessToken, identityArg.accountId, optionsArg.signal),
|
|
67
|
+
this.get('accounts/check/v4-2023-04-27', accessToken, identityArg.accountId, optionsArg.signal),
|
|
62
68
|
]);
|
|
63
69
|
const usage = sections[0];
|
|
64
70
|
if (usage.status === 'fulfilled' && usage.value.account_id != null && usage.value.account_id !== identityArg.accountId) {
|
|
@@ -85,6 +91,8 @@ export class CodexAccountStatus {
|
|
|
85
91
|
}
|
|
86
92
|
catch { result.problems.push(`${labels[index]}: the service returned an unsupported response; no values were inferred.`); }
|
|
87
93
|
}
|
|
94
|
+
const limited = sections.flatMap(section => section.status === 'rejected' && section.reason instanceof StatusRateLimitError ? [section.reason] : []);
|
|
95
|
+
if (limited.length) result.rateLimit = { retryAt: latestRetryAt(limited.map(error => error.retryAt)) };
|
|
88
96
|
if (!result.summary?.subscription && identityArg.planType) {
|
|
89
97
|
result.facts.unshift({ section: 'Subscription', summaryKey: 'subscription', label: 'Last known plan', value: `${identityArg.planType} (from stored login; not verified live)` });
|
|
90
98
|
result.summary = { ...result.summary, subscription: { plan: identityArg.planType, source: 'stored' } };
|
|
@@ -92,8 +100,9 @@ export class CodexAccountStatus {
|
|
|
92
100
|
return result;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
private async get(pathArg: string, tokenArg: string, accountIdArg: string): Promise<Record<string, unknown>> {
|
|
96
|
-
const
|
|
103
|
+
private async get(pathArg: string, tokenArg: string, accountIdArg: string, cancelArg?: AbortSignal): Promise<Record<string, unknown>> {
|
|
104
|
+
const timeout = AbortSignal.timeout(10_000);
|
|
105
|
+
const signal = cancelArg ? AbortSignal.any([cancelArg, timeout]) : timeout;
|
|
97
106
|
try {
|
|
98
107
|
const response = await this.fetcher(`https://chatgpt.com/backend-api/${pathArg}`, {
|
|
99
108
|
method: 'GET', redirect: 'error', signal,
|
|
@@ -104,6 +113,7 @@ export class CodexAccountStatus {
|
|
|
104
113
|
if (!response.ok) {
|
|
105
114
|
await response.body?.cancel();
|
|
106
115
|
if (response.headers.get('cf-mitigated') === 'challenge') throw new StatusRequestError('Blocked by a service verification challenge (Cloudflare); this lookup is unavailable to the current HTTP client.');
|
|
116
|
+
if (response.status === 429) throw new StatusRateLimitError(retryAtFrom(response.headers.get('retry-after'), this.now()));
|
|
107
117
|
if (response.status === 401) throw new StatusRequestError('Login expired or was rejected. Log in again with the account’s harness, then save the account.');
|
|
108
118
|
if (response.status === 403) throw new StatusRequestError('Access denied by the service (HTTP 403).');
|
|
109
119
|
throw new StatusRequestError(`Service returned HTTP ${response.status}.`);
|
|
@@ -125,7 +135,7 @@ export class CodexAccountStatus {
|
|
|
125
135
|
catch { throw new StatusRequestError('The service returned invalid status data.'); }
|
|
126
136
|
} catch (errorArg) {
|
|
127
137
|
// Never surface remote bodies, request headers, token strings or arbitrary fetch errors.
|
|
128
|
-
if (signal.aborted) throw new Error('The status request timed out.');
|
|
138
|
+
if (signal.aborted) throw new Error(timeout.aborted ? 'The status request timed out.' : 'The status request was cancelled.');
|
|
129
139
|
if (errorArg instanceof StatusRequestError) throw errorArg;
|
|
130
140
|
throw new Error('The status service could not be reached.');
|
|
131
141
|
}
|
|
@@ -156,9 +166,9 @@ export class CodexAccountStatus {
|
|
|
156
166
|
const append = (label: string, value: string) => facts.push({ section: 'Billing', summaryKey: 'billing', label, value });
|
|
157
167
|
if (billing.hasActiveSubscription !== undefined) append('Active subscription', billing.hasActiveSubscription ? 'yes' : 'no');
|
|
158
168
|
if (billing.autoRenew !== undefined) append('Automatic renewal', billing.autoRenew ? 'on' : 'off');
|
|
159
|
-
if (billing.renewsAt) append('Renewal date
|
|
160
|
-
if (billing.cancelsAt) append('Cancellation date
|
|
161
|
-
if (billing.expiresAt) append('Subscription expiry
|
|
169
|
+
if (billing.renewsAt) append('Renewal date', billing.renewsAt);
|
|
170
|
+
if (billing.cancelsAt) append('Cancellation date', billing.cancelsAt);
|
|
171
|
+
if (billing.expiresAt) append('Subscription expiry', billing.expiresAt);
|
|
162
172
|
return { facts, summary: { billing } };
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -171,8 +181,9 @@ export class CodexAccountStatus {
|
|
|
171
181
|
const appendLimit = (nameArg: string, valueArg: unknown, scopeArg: 'account' | 'feature') => {
|
|
172
182
|
if (valueArg == null) return;
|
|
173
183
|
const limit = record(valueArg);
|
|
174
|
-
|
|
175
|
-
facts.push({ section: 'Limits & credits', label: `${nameArg}
|
|
184
|
+
// The service's own reading of the same windows, so a view that shows the windows need not repeat it.
|
|
185
|
+
facts.push({ section: 'Limits & credits', summaryKey: 'usageWindows', label: `${nameArg} usage allowed`, value: boolean(limit.allowed) ? 'yes' : 'no' });
|
|
186
|
+
facts.push({ section: 'Limits & credits', summaryKey: 'usageWindows', label: `${nameArg} limit reached`, value: boolean(limit.limit_reached) ? 'yes' : 'no' });
|
|
176
187
|
for (const [key, name] of [['primary_window', 'primary'], ['secondary_window', 'secondary']]) {
|
|
177
188
|
if (limit[key] == null) continue;
|
|
178
189
|
const window = record(limit[key]);
|
|
@@ -250,7 +261,7 @@ export class CodexAccountStatus {
|
|
|
250
261
|
return { date, tokens: numeric(day.tokens) };
|
|
251
262
|
}).sort((leftArg, rightArg) => rightArg.date.localeCompare(leftArg.date));
|
|
252
263
|
for (const day of days.slice(0, 7)) facts.push({ section: 'Daily tokens', label: `Tokens ${day.date}`, value: day.tokens.toLocaleString('en-US') });
|
|
253
|
-
|
|
264
|
+
facts.push({ section: 'Daily tokens', label: 'Reported days', value: String(days.length) });
|
|
254
265
|
}
|
|
255
266
|
return facts;
|
|
256
267
|
}
|