@modelprofile.com/authswitch 6.2.0 → 6.3.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 +1 -1
- package/dist_ts/classes.cli.d.ts +56 -0
- package/dist_ts/classes.cli.js +238 -63
- package/dist_ts/classes.codexpreuse.js +2 -2
- package/dist_ts/classes.tui.js +51 -2
- package/dist_ts/preuse.d.ts +89 -2
- package/dist_ts/preuse.js +107 -2
- package/package.json +1 -1
- package/readme.md +84 -16
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.cli.ts +236 -54
- package/ts/classes.codexpreuse.ts +1 -1
- package/ts/classes.tui.ts +39 -2
- package/ts/preuse.ts +141 -3
package/ts/classes.cli.ts
CHANGED
|
@@ -8,16 +8,17 @@ import { AglAuthSwitchCoordinator, AuthSwitchOperations, authSwitchMutationRepla
|
|
|
8
8
|
import { readAccountList } from './classes.accountlist.js';
|
|
9
9
|
import { AccountListRenderer } from './classes.listrenderer.js';
|
|
10
10
|
import { accountLimits, activeAccounts, CondensedRenderer } from './classes.limits.js';
|
|
11
|
-
import { consoleTable } from './consoletable.js';
|
|
12
|
-
import { accountName, credentialDriftNote,
|
|
11
|
+
import { consoleHeading, consoleTable } from './consoletable.js';
|
|
12
|
+
import { accountName, credentialDriftNote, readAccountBadges, until, usagePercentText } from './accounts.js';
|
|
13
13
|
import { describeHarnessProcesses, describeStopOutcome } from './classes.harnessprocesses.js';
|
|
14
|
-
import { defaultPreusePrompt,
|
|
14
|
+
import { defaultPreusePrompt, preuseAccounts, preuseCountsText, PreuseError, preuseTargets, validatePreuseOptions,
|
|
15
|
+
type IPreuseRunSummary, type TPreuseAccountResult, type TPreuseOutcome, type TPreuseRunEvent } from './preuse.js';
|
|
15
16
|
import { parseCommandArgs, parseDurationOption, parseIntegerOption, UsageError } from './cliargs.js';
|
|
16
17
|
import { AuthSwitchWatch, watchEventText } from './classes.watch.js';
|
|
17
18
|
import { WatchBusyError, WatchLock } from './classes.watchlock.js';
|
|
18
19
|
import { authSwitchHome } from './classes.credentialstore.js';
|
|
19
20
|
import type { CodexSwitcher } from './classes.codexswitcher.js';
|
|
20
|
-
import type { IAuthHarness, IHarnessAccount, IHarnessOutcome, IHarnessProcess, IHarnessState, IHarnessStopOutcome
|
|
21
|
+
import type { IAuthHarness, IHarnessAccount, IHarnessOutcome, IHarnessProcess, IHarnessState, IHarnessStopOutcome } from './interfaces.harness.js';
|
|
21
22
|
import { bold, dim, green, orange, plainText, red } from './formatting.js';
|
|
22
23
|
|
|
23
24
|
const canPrompt = (): boolean =>
|
|
@@ -31,9 +32,56 @@ const STOP_COMMANDS = ['use', 'stash'];
|
|
|
31
32
|
/** Overviews across every registered harness; none of them changes which account is in use, and all support --json. */
|
|
32
33
|
const OVERVIEW_COMMANDS = ['list', 'ls', 'limits', 'active'];
|
|
33
34
|
const WATCH_USAGE = 'Usage: authswitch [harness] watch [harness] [--interval <duration>] [--threshold <percent>] [--dry-run] [--once] [--json]';
|
|
35
|
+
const PREUSE_USAGE = 'Usage: authswitch [harness] preuse <account>|--all [--prompt <text>] [--model <id>]';
|
|
36
|
+
/** How each outcome of a preuse run is titled in its results table. */
|
|
37
|
+
const PREUSE_OUTCOMES: Readonly<Record<TPreuseOutcome, string>> = { completed: 'Completed', skipped: 'Skipped', failed: 'Failed', interrupted: 'Interrupted' };
|
|
38
|
+
/** A cell of a preuse results row that has no value because nothing was sent for that account. */
|
|
39
|
+
const PREUSE_UNSENT = '-';
|
|
34
40
|
const WATCH_INTERVAL = { defaultMs: 120_000, minMs: 60_000, maxMs: 86_400_000 };
|
|
35
41
|
const WATCH_THRESHOLD = { default: 95, min: 50, max: 100 };
|
|
36
42
|
|
|
43
|
+
/** How one preuse run is asked for and reported; see `AuthSwitchCli.runPreuse`. */
|
|
44
|
+
interface IPreuseRunOptions {
|
|
45
|
+
prompt: string;
|
|
46
|
+
model?: string;
|
|
47
|
+
/** Number each account as it starts and end with the results table, instead of one account's reset schedule. */
|
|
48
|
+
several: boolean;
|
|
49
|
+
/** Report a stated refusal as a failure, which is the single named account's own contract. */
|
|
50
|
+
strict: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One account's row in the results table of a run over several accounts. */
|
|
54
|
+
interface IPreuseSummaryRow {
|
|
55
|
+
account: string;
|
|
56
|
+
outcome: string;
|
|
57
|
+
model: string;
|
|
58
|
+
tokens: string;
|
|
59
|
+
reset: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One account's results row, from what the run already read.
|
|
64
|
+
*
|
|
65
|
+
* An account that sent no prompt has no model, no token count and no deadline of its own, which the row
|
|
66
|
+
* says rather than filling in a zero. A completed prompt shows the first window of the schedule the run
|
|
67
|
+
* read afterwards, and says when the provider reported none or the lookup did not answer.
|
|
68
|
+
*/
|
|
69
|
+
const preuseSummaryRow = (resultArg: TPreuseAccountResult, nowArg: number): IPreuseSummaryRow => {
|
|
70
|
+
const account = accountName(resultArg.account);
|
|
71
|
+
if (resultArg.outcome !== 'completed') {
|
|
72
|
+
return { account, outcome: PREUSE_OUTCOMES[resultArg.outcome], model: PREUSE_UNSENT, tokens: PREUSE_UNSENT, reset: PREUSE_UNSENT };
|
|
73
|
+
}
|
|
74
|
+
const window = resultArg.schedule.kind === 'reported' ? resultArg.schedule.windows[0] : undefined;
|
|
75
|
+
return {
|
|
76
|
+
account,
|
|
77
|
+
outcome: PREUSE_OUTCOMES.completed,
|
|
78
|
+
model: plainText(resultArg.result.model),
|
|
79
|
+
tokens: [resultArg.result.inputTokens, resultArg.result.outputTokens, resultArg.result.totalTokens].map(count => count ?? 'unreported').join('/'),
|
|
80
|
+
reset: window ? until(window.resetAt, nowArg)
|
|
81
|
+
: resultArg.schedule.kind === 'reported' ? 'None reported' : resultArg.schedule.kind === 'notRead' ? 'Not read' : 'Unavailable',
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
37
85
|
/** The account a command acts on, named as it was offered, with the badge it was offered with. */
|
|
38
86
|
interface IChosenAccount {
|
|
39
87
|
id: string;
|
|
@@ -76,8 +124,9 @@ ${bold('Usage')}
|
|
|
76
124
|
authswitch list --json print all account information as JSON
|
|
77
125
|
authswitch limits one row per account and limit type: used % and reset countdown
|
|
78
126
|
authswitch active one row per provider: which account is in use, and since when
|
|
79
|
-
authswitch <harness> preuse <account
|
|
80
|
-
send one prompt through that account without switching
|
|
127
|
+
authswitch <harness> preuse <account>|--all [--prompt <text>] [--model <id>]
|
|
128
|
+
send one prompt through that account without switching;
|
|
129
|
+
--all does it for every account of the harness, one at a time
|
|
81
130
|
authswitch watch [harness] [--interval <duration>] [--threshold <percent>] [--dry-run] [--once] [--json]
|
|
82
131
|
check usage every 2m and switch to a better saved account
|
|
83
132
|
when the active one reaches the threshold (95%)
|
|
@@ -119,7 +168,10 @@ account is used up, to the one usable again first. No session of yours is stoppe
|
|
|
119
168
|
a Codex switch restarts Codex' own app-server; --dry-run only reports, --once checks once.
|
|
120
169
|
One watch runs per AUTHSWITCH_HOME.
|
|
121
170
|
|
|
122
|
-
Preuse consumes the selected account's quota
|
|
171
|
+
Preuse consumes the selected account's quota, and --all consumes every account's. Accounts run
|
|
172
|
+
one at a time, each with one request and no retry; Ctrl+C stops before the next account starts.
|
|
173
|
+
It exits 1 when a prompt failed, 130 when it was interrupted, and 2 on a usage error; --all also
|
|
174
|
+
exits 0 when an account was refused for a stated reason. Its default prompt is:
|
|
123
175
|
"${defaultPreusePrompt}"
|
|
124
176
|
|
|
125
177
|
${bold('Environment')}
|
|
@@ -321,18 +373,28 @@ ${bold('Environment')}
|
|
|
321
373
|
}
|
|
322
374
|
}
|
|
323
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Send the preuse prompt through one named account, through an account chosen here, or through every
|
|
378
|
+
* account of the harness.
|
|
379
|
+
*
|
|
380
|
+
* `--all` walks the harness's own account list, one request at a time, and ends with a table of what
|
|
381
|
+
* each account did; a single named account keeps its own contract, in which anything but a completed
|
|
382
|
+
* prompt is a failure. Neither form switches, refreshes or writes a login.
|
|
383
|
+
*/
|
|
324
384
|
private async commandPreuse(harnessArg: IAuthHarness | undefined, argsArg: string[]): Promise<number> {
|
|
325
385
|
let reference: string | undefined;
|
|
386
|
+
let all: boolean;
|
|
326
387
|
let prompt: string;
|
|
327
388
|
let model: string | undefined;
|
|
328
389
|
try {
|
|
329
|
-
const parsed = parseCommandArgs(argsArg, { values: ['--prompt', '--model'], flags: [], maxPositionals: 1,
|
|
330
|
-
usage: 'Usage: authswitch [harness] preuse <account> [--prompt <text>] [--model <id>]' });
|
|
390
|
+
const parsed = parseCommandArgs(argsArg, { values: ['--prompt', '--model'], flags: ['--all'], maxPositionals: 1, usage: PREUSE_USAGE });
|
|
331
391
|
[reference] = parsed.positionals;
|
|
392
|
+
all = parsed.flags.has('--all');
|
|
332
393
|
prompt = parsed.values.get('--prompt') ?? defaultPreusePrompt;
|
|
333
394
|
model = parsed.values.get('--model');
|
|
334
395
|
validatePreuseOptions({ prompt, model });
|
|
335
|
-
if (
|
|
396
|
+
if (all && reference !== undefined) throw new UsageError(`--all preuses every account of the harness, so it takes no account. ${PREUSE_USAGE}`);
|
|
397
|
+
if (!all && !reference && !canPrompt()) throw new UsageError('preuse requires an account or --all outside an interactive terminal.');
|
|
336
398
|
} catch (error) {
|
|
337
399
|
process.stderr.write(`${error instanceof UsageError || error instanceof PreuseError ? error.message : 'Invalid preuse arguments.'}\n`);
|
|
338
400
|
return 2;
|
|
@@ -341,63 +403,159 @@ ${bold('Environment')}
|
|
|
341
403
|
if (!harness) return canPrompt() ? 0 : 2;
|
|
342
404
|
if (!harness.preuseAccount) { process.stderr.write(`Preuse is not supported by ${plainText(harness.label)}.\n`); return 2; }
|
|
343
405
|
const state = await harness.readState();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
choices: [...state.accounts.map(item => ({ name: plainText(item.label), value: item.id })), { name: 'Back', value: '' }],
|
|
358
|
-
});
|
|
359
|
-
if (!answer) return 0;
|
|
360
|
-
account = state.accounts.find(item => item.id === answer);
|
|
406
|
+
if (all) {
|
|
407
|
+
const accounts = preuseTargets(state.accounts);
|
|
408
|
+
if (!accounts.length) { process.stderr.write(`${plainText(harness.label)} has no accounts to preuse.\n`); return 1; }
|
|
409
|
+
return await this.runPreuse(harness, accounts, { prompt, model, several: true, strict: false });
|
|
410
|
+
}
|
|
411
|
+
if (!reference) return await this.preuseFromPicker(harness, state, { prompt, model });
|
|
412
|
+
const matches = state.accounts.filter(item => item.id === reference || item.label === reference);
|
|
413
|
+
if (matches.length > 1) { process.stderr.write('That account reference is ambiguous; use an exact account ID.\n'); return 1; }
|
|
414
|
+
let account: IHarnessAccount | undefined = matches[0];
|
|
415
|
+
if (!account) {
|
|
416
|
+
const id = await this.resolveAccount(harness, reference);
|
|
417
|
+
if (id === null) return 1;
|
|
418
|
+
account = state.accounts.find(item => item.id === id);
|
|
361
419
|
}
|
|
362
420
|
if (!account) { process.stderr.write('The selected account is no longer available.\n'); return 1; }
|
|
421
|
+
return await this.runPreuse(harness, [account], { prompt, model, several: false, strict: true });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The account picker `preuse` shows without an account reference: one account, all of them, or Back.
|
|
426
|
+
*
|
|
427
|
+
* Choosing one account here is that account's consent, exactly as naming it on the command line is, and
|
|
428
|
+
* it keeps that command's contract. Choosing all of them spends quota on accounts the chooser did not
|
|
429
|
+
* name, so that answer is confirmed once and then runs as `--all` does, table and exit status included.
|
|
430
|
+
*/
|
|
431
|
+
private async preuseFromPicker(harnessArg: IAuthHarness, stateArg: IHarnessState, optionsArg: { prompt: string; model?: string }): Promise<number> {
|
|
432
|
+
const accounts = preuseTargets(stateArg.accounts);
|
|
433
|
+
const answer = await this.out.prompts.ask({
|
|
434
|
+
name: 'preuseAccount', type: 'list', message: `Which ${harnessArg.label} account should receive the preuse prompt? This consumes quota.`,
|
|
435
|
+
choices: this.preuseChoices(accounts),
|
|
436
|
+
});
|
|
437
|
+
if (!answer) return 0;
|
|
438
|
+
const selected = this.preuseSelection(answer, accounts);
|
|
439
|
+
if (!selected.length) { process.stderr.write('The selected account is no longer available.\n'); return 1; }
|
|
440
|
+
const several = selected.length > 1;
|
|
441
|
+
if (several && !await this.confirmPreuse(selected)) return 0;
|
|
442
|
+
return await this.runPreuse(harnessArg, selected, { ...optionsArg, several, strict: !several });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The choices an interactive preuse offers: every account, one of them, or Back.
|
|
447
|
+
*
|
|
448
|
+
* The guide and the bare `preuse` command offer the same list with the same wording. An account id is an
|
|
449
|
+
* opaque string, so every choice says which kind of answer it carries instead of relying on a sentinel an
|
|
450
|
+
* id could collide with. A single account is offered alone: "all" of one account is that same account.
|
|
451
|
+
*/
|
|
452
|
+
private preuseChoices(accountsArg: readonly IHarnessAccount[]): { name: string; value: string }[] {
|
|
453
|
+
return [
|
|
454
|
+
...(accountsArg.length > 1 ? [{ name: `All accounts (${accountsArg.length})`, value: 'all' }] : []),
|
|
455
|
+
...accountsArg.map(account => ({ name: accountName(account), value: `one:${account.id}` })),
|
|
456
|
+
{ name: 'Back', value: '' },
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The accounts an answer names; empty when the chosen account is no longer there. */
|
|
461
|
+
private preuseSelection(answerArg: string, accountsArg: readonly IHarnessAccount[]): IHarnessAccount[] {
|
|
462
|
+
return answerArg === 'all' ? [...accountsArg] : accountsArg.filter(account => `one:${account.id}` === answerArg);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** One confirmation, naming what the prompt goes to and that it consumes quota. */
|
|
466
|
+
private async confirmPreuse(accountsArg: readonly IHarnessAccount[]): Promise<boolean> {
|
|
467
|
+
return await this.out.prompts.ask({
|
|
468
|
+
name: 'confirmPreuse', type: 'confirm', default: false,
|
|
469
|
+
message: accountsArg.length === 1
|
|
470
|
+
? `Send one preuse prompt through ${plainText(accountsArg[0].label)}? This consumes its quota.`
|
|
471
|
+
: `Send one preuse prompt through each of these ${accountsArg.length} accounts? This consumes quota on every one of them.`,
|
|
472
|
+
}) === true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* One preuse run and its report, for the command line and for the guide.
|
|
477
|
+
*
|
|
478
|
+
* Ctrl+C aborts the run: the account in flight is reported as interrupted and no further account is
|
|
479
|
+
* started, because a prompt that may already have been sent is never repeated. The exit status is 130
|
|
480
|
+
* for an interrupted run, 1 when an account failed -- and, for a single named account, when it was
|
|
481
|
+
* refused -- and 0 otherwise. Usage errors return 2 before anything reaches this.
|
|
482
|
+
*/
|
|
483
|
+
private async runPreuse(harnessArg: IAuthHarness, accountsArg: readonly IHarnessAccount[], optionsArg: IPreuseRunOptions): Promise<number> {
|
|
363
484
|
const controller = new AbortController();
|
|
364
485
|
const cancel = () => controller.abort();
|
|
365
486
|
process.once('SIGINT', cancel);
|
|
366
487
|
process.once('SIGTERM', cancel);
|
|
367
488
|
try {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return controller.signal.aborted ? 130 : 1;
|
|
375
|
-
}
|
|
376
|
-
// Once inference completes, status or presentation failures must not report it as failed.
|
|
377
|
-
try {
|
|
378
|
-
process.stdout.write(`Prompt completed with ${plainText(result.model)}. Tokens: ${result.inputTokens ?? 'unreported'} input, ${result.outputTokens ?? 'unreported'} output, ${result.totalTokens ?? 'unreported'} total.\n`);
|
|
379
|
-
if (controller.signal.aborted) return 0;
|
|
380
|
-
const windows = orderedUsageWindows((await harness.readAccountStatus(account.id)).summary?.usageWindows);
|
|
381
|
-
if (windows?.length) {
|
|
382
|
-
const now = Date.now();
|
|
383
|
-
process.stdout.write('Reset schedule reported after the prompt:\n');
|
|
384
|
-
await consoleTable(this.out, windows, [
|
|
385
|
-
// The label already names the window by its length, so the column never repeats it.
|
|
386
|
-
{ key: 'window', title: 'Window', value: row => plainText(row.label) },
|
|
387
|
-
{ key: 'usage', title: 'Used', value: row => usagePercentText(row.usedPercent) },
|
|
388
|
-
{ key: 'reset', title: 'Reset in', value: row => until(row.resetAt, now) },
|
|
389
|
-
]);
|
|
390
|
-
} else process.stdout.write('Reset schedule could not be verified; the completed prompt will not be repeated.\n');
|
|
391
|
-
} catch {
|
|
392
|
-
process.stderr.write('Prompt completed, but its reset schedule could not be verified or displayed. The prompt will not be repeated.\n');
|
|
393
|
-
}
|
|
394
|
-
return 0;
|
|
489
|
+
const summary = await preuseAccounts(harnessArg, accountsArg, { prompt: optionsArg.prompt, model: optionsArg.model, signal: controller.signal },
|
|
490
|
+
event => this.writePreuseEvent(harnessArg, event, optionsArg.several));
|
|
491
|
+
if (optionsArg.several) await this.writePreuseSummary(summary);
|
|
492
|
+
else if (summary.results[0]) await this.writePreuseSchedule(summary.results[0]);
|
|
493
|
+
if (summary.counts.interrupted || summary.notRun.length) return 130;
|
|
494
|
+
return summary.counts.failed || (optionsArg.strict && summary.counts.skipped) ? 1 : 0;
|
|
395
495
|
} finally {
|
|
396
496
|
process.removeListener('SIGINT', cancel);
|
|
397
497
|
process.removeListener('SIGTERM', cancel);
|
|
398
498
|
}
|
|
399
499
|
}
|
|
400
500
|
|
|
501
|
+
/** One line per event; a run over several accounts numbers each account as it starts. */
|
|
502
|
+
private writePreuseEvent(harnessArg: IAuthHarness, eventArg: TPreuseRunEvent, severalArg: boolean): void {
|
|
503
|
+
if (eventArg.kind === 'started') {
|
|
504
|
+
const position = severalArg ? `[${eventArg.index + 1}/${eventArg.total}] ` : '';
|
|
505
|
+
process.stdout.write(`${position}Preusing ${plainText(eventArg.account.label)} (${plainText(harnessArg.label)})…\n`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (eventArg.result.outcome !== 'completed') { process.stderr.write(`${eventArg.result.reason}\n`); return; }
|
|
509
|
+
const result = eventArg.result.result;
|
|
510
|
+
process.stdout.write(`Prompt completed with ${plainText(result.model)}. Tokens: ${result.inputTokens ?? 'unreported'} input, ${result.outputTokens ?? 'unreported'} output, ${result.totalTokens ?? 'unreported'} total.\n`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* The reset schedule one completed prompt reported.
|
|
515
|
+
*
|
|
516
|
+
* Once inference completes, a schedule that could not be read or displayed is reported as exactly that:
|
|
517
|
+
* a completed prompt is never repeated over a lookup or presentation failure.
|
|
518
|
+
*/
|
|
519
|
+
private async writePreuseSchedule(resultArg: TPreuseAccountResult): Promise<void> {
|
|
520
|
+
if (resultArg.outcome !== 'completed' || resultArg.schedule.kind === 'notRead') return;
|
|
521
|
+
if (resultArg.schedule.kind === 'reported') {
|
|
522
|
+
const windows = [...resultArg.schedule.windows];
|
|
523
|
+
try {
|
|
524
|
+
if (!windows.length) { process.stdout.write('Reset schedule could not be verified; the completed prompt will not be repeated.\n'); return; }
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
process.stdout.write('Reset schedule reported after the prompt:\n');
|
|
527
|
+
await consoleTable(this.out, windows, [
|
|
528
|
+
// The label already names the window by its length, so the column never repeats it.
|
|
529
|
+
{ key: 'window', title: 'Window', value: row => plainText(row.label) },
|
|
530
|
+
{ key: 'usage', title: 'Used', value: row => usagePercentText(row.usedPercent) },
|
|
531
|
+
{ key: 'reset', title: 'Reset in', value: row => until(row.resetAt, now) },
|
|
532
|
+
]);
|
|
533
|
+
return;
|
|
534
|
+
} catch { /* A schedule that could not be displayed is reported below, like one that could not be read. */ }
|
|
535
|
+
}
|
|
536
|
+
process.stderr.write('Prompt completed, but its reset schedule could not be verified or displayed. The prompt will not be repeated.\n');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** What every account of a run did, in one table. The rows repeat readings the run already took. */
|
|
540
|
+
private async writePreuseSummary(summaryArg: IPreuseRunSummary): Promise<void> {
|
|
541
|
+
const now = Date.now();
|
|
542
|
+
const rows: IPreuseSummaryRow[] = [
|
|
543
|
+
...summaryArg.results.map(result => preuseSummaryRow(result, now)),
|
|
544
|
+
...summaryArg.notRun.map((account): IPreuseSummaryRow => ({
|
|
545
|
+
account: accountName(account), outcome: 'Not run', model: PREUSE_UNSENT, tokens: PREUSE_UNSENT, reset: PREUSE_UNSENT,
|
|
546
|
+
})),
|
|
547
|
+
];
|
|
548
|
+
consoleHeading('Preuse results');
|
|
549
|
+
await consoleTable(this.out, rows, [
|
|
550
|
+
{ key: 'account', title: 'Account', value: row => row.account },
|
|
551
|
+
{ key: 'outcome', title: 'Outcome', value: row => row.outcome },
|
|
552
|
+
{ key: 'model', title: 'Model', value: row => row.model },
|
|
553
|
+
{ key: 'tokens', title: 'Tokens in/out/total', value: row => row.tokens },
|
|
554
|
+
{ key: 'reset', title: 'Next reset', value: row => row.reset },
|
|
555
|
+
]);
|
|
556
|
+
process.stdout.write(`${preuseCountsText(summaryArg)}.\n`);
|
|
557
|
+
}
|
|
558
|
+
|
|
401
559
|
/**
|
|
402
560
|
* Watch usage and switch automatically; see `AuthSwitchWatch`. SIGINT and SIGTERM end it cleanly with status 0.
|
|
403
561
|
* `--once` runs one check and exits 1 when a switch it attempted did not complete.
|
|
@@ -505,6 +663,7 @@ ${bold('Environment')}
|
|
|
505
663
|
{ name: 'Switch to a saved account', value: 'use' },
|
|
506
664
|
{ name: 'Save the current login', value: 'stash' },
|
|
507
665
|
...(harnessArg.beginLogin ? [{ name: 'Log in and save another account', value: 'login' }] : []),
|
|
666
|
+
...(harnessArg.preuseAccount ? [{ name: 'Preuse an account or all accounts (consumes quota)', value: 'preuse' }] : []),
|
|
508
667
|
{ name: 'List accounts, subscription, usage and resets', value: 'list' },
|
|
509
668
|
{ name: harnessArg.diagnosticsLabel, value: 'doctor' },
|
|
510
669
|
{ name: 'Remove a saved account', value: 'drop' },
|
|
@@ -531,6 +690,7 @@ ${bold('Environment')}
|
|
|
531
690
|
if (mode === 'keep' || mode === 'clear') code = this.reportOutcome(await this.mutate(harnessArg, { harnessId: harnessArg.id, action: 'save', keepActive: mode === 'keep', accountId: active.id }));
|
|
532
691
|
break;
|
|
533
692
|
}
|
|
693
|
+
case 'preuse': code = await this.guidePreuse(harnessArg); break;
|
|
534
694
|
case 'list': code = await this.commandList([harnessArg]); break;
|
|
535
695
|
case 'doctor': code = this.reportOutcome(await harnessArg.diagnose()); break;
|
|
536
696
|
case 'drop': {
|
|
@@ -550,6 +710,28 @@ ${bold('Environment')}
|
|
|
550
710
|
}
|
|
551
711
|
}
|
|
552
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Preuse from the guide: one account or all of them, confirmed once before anything is sent.
|
|
715
|
+
*
|
|
716
|
+
* The prompt consumes quota, so the confirmation names what it will be sent to, and Back or a declined
|
|
717
|
+
* confirmation returns to the menu having sent nothing. A refusal an adapter states is not a guide
|
|
718
|
+
* failure, so the guide stays open; a failed prompt ends it, as every other failed action does.
|
|
719
|
+
*/
|
|
720
|
+
private async guidePreuse(harnessArg: IAuthHarness): Promise<number> {
|
|
721
|
+
const accounts = preuseTargets((await harnessArg.readState()).accounts);
|
|
722
|
+
if (!accounts.length) { process.stderr.write(`${red('no accounts to preuse')} - run \`authswitch ${harnessArg.id} stash\` first\n`); return 0; }
|
|
723
|
+
const answer = await this.out.prompts.ask({
|
|
724
|
+
name: 'preuseTarget', type: 'list', message: `Which ${harnessArg.label} account should receive the preuse prompt? This consumes quota.`,
|
|
725
|
+
choices: this.preuseChoices(accounts),
|
|
726
|
+
});
|
|
727
|
+
if (!answer) return 0;
|
|
728
|
+
const selected = this.preuseSelection(answer, accounts);
|
|
729
|
+
if (!selected.length) { process.stderr.write('The selected account is no longer available.\n'); return 0; }
|
|
730
|
+
// The guide confirms either answer: it is a menu one lands in, not a command that named its account.
|
|
731
|
+
if (!await this.confirmPreuse(selected)) return 0;
|
|
732
|
+
return await this.runPreuse(harnessArg, selected, { prompt: defaultPreusePrompt, several: selected.length > 1, strict: false });
|
|
733
|
+
}
|
|
734
|
+
|
|
553
735
|
private printCurrent(harnessArg: IAuthHarness, stateArg: IHarnessState): void {
|
|
554
736
|
process.stdout.write(`\n${bold(`Current ${harnessArg.label} account`)}\n`);
|
|
555
737
|
const active = stateArg.accounts.filter(account => account.isActive);
|
|
@@ -70,7 +70,7 @@ export class CodexPreuse {
|
|
|
70
70
|
} catch {
|
|
71
71
|
throw new PreuseError(requestStarted
|
|
72
72
|
? 'Preuse did not complete. Tokens may already have been consumed; no automatic retry was made. Check the account status before trying again.'
|
|
73
|
-
: 'Preuse was cancelled before the prompt was sent.');
|
|
73
|
+
: 'Preuse was cancelled before the prompt was sent.', requestStarted);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
package/ts/classes.tui.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import type { IAuthHarness, IHarnessOutcome, IHarnessProcess, IHarnessState, IHarnessStopOutcome } from './interfaces.harness.js';
|
|
2
|
+
import type { IAuthHarness, IHarnessAccount, IHarnessOutcome, IHarnessProcess, IHarnessState, IHarnessStopOutcome } from './interfaces.harness.js';
|
|
3
3
|
import { accountBadge, accountDetails, accountPlan, accountQuotaSummary, accountResets, accountState, accountUsageWindows, usageWindowName, until, readAccountRows, type IAccountRow } from './accounts.js';
|
|
4
|
+
import { defaultPreusePrompt, preuseAccounts, preuseCountsText, preuseTargets, type TPreuseAccountResult } from './preuse.js';
|
|
4
5
|
import { plainText } from './formatting.js';
|
|
5
6
|
import { AuthSwitchOperations, authSwitchMutationReplacesLogin, type TAuthSwitchMutation } from './classes.operations.js';
|
|
6
7
|
import { describeHarnessProcesses, describeStopOutcome } from './classes.harnessprocesses.js';
|
|
7
8
|
|
|
9
|
+
/** One account's preuse result, as the dashboard's activity log and footer report it. */
|
|
10
|
+
const preuseResultLine = (resultArg: TPreuseAccountResult): string => resultArg.outcome === 'completed'
|
|
11
|
+
? `${plainText(resultArg.account.label)}: completed with ${plainText(resultArg.result.model)}, ${resultArg.result.totalTokens ?? 'unreported'} tokens.`
|
|
12
|
+
: `${plainText(resultArg.account.label)}: ${resultArg.reason}`;
|
|
13
|
+
|
|
8
14
|
/** Owns account-management actions; terminal rendering and interaction belong to smartconsole. */
|
|
9
15
|
export class AuthSwitchTui {
|
|
10
16
|
constructor(private readonly harnesses: readonly IAuthHarness[], private readonly out: plugins.smartconsole.SmartConsole, private readonly operations = new AuthSwitchOperations()) {}
|
|
@@ -126,6 +132,30 @@ export class AuthSwitchTui {
|
|
|
126
132
|
await refresh(screen);
|
|
127
133
|
if (showOutcome(outcome)) ui.logs.append(`Now active: ${plainText(target.label)}${badge}`);
|
|
128
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* Send one preuse prompt through each of these accounts, after one confirmation that names the cost.
|
|
137
|
+
*
|
|
138
|
+
* The prompt consumes the account's own quota, so nothing is sent before consent, the accounts run
|
|
139
|
+
* one at a time through the shared runner, and the footer follows the run account by account. Closing
|
|
140
|
+
* the dashboard aborts it before the next account starts. Nothing is switched, saved or written.
|
|
141
|
+
*/
|
|
142
|
+
const preuse = async (screen: plugins.smartconsole.ITuiContext, accountsArg: readonly IHarnessAccount[]): Promise<void> => {
|
|
143
|
+
if (!harness.preuseAccount) { footer.setText(`${plainText(harness.label)} does not support preuse.`); return; }
|
|
144
|
+
if (!accountsArg.length) { footer.setText('Select an account to preuse.'); return; }
|
|
145
|
+
const message = accountsArg.length === 1
|
|
146
|
+
? `Send one preuse prompt through ${plainText(accountsArg[0].label)}? This consumes its quota.`
|
|
147
|
+
: `Send one preuse prompt through each of these ${accountsArg.length} ${plainText(harness.label)} accounts? This consumes quota on every one of them.`;
|
|
148
|
+
if (!await screen.confirm(message, { confirmLabel: 'Preuse' })) return;
|
|
149
|
+
if (screen.signal.aborted) return;
|
|
150
|
+
const summary = await preuseAccounts(harness, accountsArg, { prompt: defaultPreusePrompt, signal: screen.signal }, event => {
|
|
151
|
+
if (event.kind === 'started') footer.setText(`Preusing ${plainText(event.account.label)} (${event.index + 1}/${event.total})…`);
|
|
152
|
+
else { const line = preuseResultLine(event.result); ui.logs.append(line); footer.setText(line); }
|
|
153
|
+
void screen.invalidate();
|
|
154
|
+
});
|
|
155
|
+
// The prompts moved usage and reset deadlines, so the table is read again before the counts are shown.
|
|
156
|
+
await refresh(screen);
|
|
157
|
+
footer.setText(`Preuse: ${preuseCountsText(summary)}.`);
|
|
158
|
+
};
|
|
129
159
|
const remove = async (screen: plugins.smartconsole.ITuiContext): Promise<void> => {
|
|
130
160
|
const selected = table.selected;
|
|
131
161
|
if (!selected || (!selected.account.isStashed && selected.account.savedAt === null)) { footer.setText('Select an account with a saved copy.'); return; }
|
|
@@ -156,7 +186,7 @@ export class AuthSwitchTui {
|
|
|
156
186
|
return ui.column([
|
|
157
187
|
ui.text('authswitch · account management', { height: 1 }),
|
|
158
188
|
this.harnesses.length > 1 ? ui.row([ui.panel('Harnesses', harnessTable, { width: 23 }), accountView]) : accountView,
|
|
159
|
-
ui.text(
|
|
189
|
+
ui.text(`Enter switch · a save · c save+clear · d remove${harness.preuseAccount ? ' · p preuse · P preuse all' : ''} · r refresh · g diagnose · Tab focus · q quit`, { height: 2 }),
|
|
160
190
|
footer,
|
|
161
191
|
]);
|
|
162
192
|
},
|
|
@@ -167,6 +197,13 @@ export class AuthSwitchTui {
|
|
|
167
197
|
a: screen => perform(screen, () => save(screen, true)),
|
|
168
198
|
c: screen => perform(screen, () => save(screen, false)),
|
|
169
199
|
d: screen => perform(screen, () => remove(screen)),
|
|
200
|
+
// The key map is fixed for the session while the harness selector can change the harness, so both
|
|
201
|
+
// preuse keys are always bound and the action itself reports a harness that does not support it.
|
|
202
|
+
p: screen => perform(screen, async () => {
|
|
203
|
+
const selected = table.selected?.account.id;
|
|
204
|
+
await preuse(screen, preuseTargets((await harness.readState()).accounts).filter(account => account.id === selected));
|
|
205
|
+
}),
|
|
206
|
+
P: screen => perform(screen, async () => { await preuse(screen, preuseTargets((await harness.readState()).accounts)); }),
|
|
170
207
|
g: screen => perform(screen, async () => { showOutcome(await harness.diagnose()); }),
|
|
171
208
|
},
|
|
172
209
|
});
|
package/ts/preuse.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { orderedUsageWindows } from './accounts.js';
|
|
2
|
+
import type { IAuthHarness, IHarnessAccount, IHarnessPreuseOptions, IHarnessPreuseResult, IHarnessUsageWindow } from './interfaces.harness.js';
|
|
2
3
|
|
|
3
4
|
export const defaultPreusePrompt = 'Write 2000 words about strawberries.';
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Only fixed local messages may cross the inference error boundary.
|
|
8
|
+
*
|
|
9
|
+
* `requestStarted` says whether the prompt may already have reached the provider. False is a refusal that
|
|
10
|
+
* sent nothing -- an unsupported login, a missing credential, a model catalog that could not be read before
|
|
11
|
+
* the prompt -- so the account's quota is untouched. True, and any error that is not a `PreuseError` at all,
|
|
12
|
+
* means tokens may have been consumed, and such a request is never repeated automatically.
|
|
13
|
+
*/
|
|
14
|
+
export class PreuseError extends Error {
|
|
15
|
+
constructor(messageArg: string, public readonly requestStarted = false) { super(messageArg); }
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
export const validatePreuseOptions = (optionsArg: IHarnessPreuseOptions): void => {
|
|
9
19
|
if (typeof optionsArg.prompt !== 'string' || !optionsArg.prompt.trim() || Buffer.byteLength(optionsArg.prompt, 'utf8') > 16_384) {
|
|
@@ -13,3 +23,131 @@ export const validatePreuseOptions = (optionsArg: IHarnessPreuseOptions): void =
|
|
|
13
23
|
throw new PreuseError('The preuse model must be a valid model identifier of at most 128 characters.');
|
|
14
24
|
}
|
|
15
25
|
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What one account's turn in a run did.
|
|
29
|
+
*
|
|
30
|
+
* `skipped` is a refusal that sent nothing and consumed nothing, with the reason the adapter stated.
|
|
31
|
+
* `failed` and `interrupted` may both have consumed tokens, so neither is ever retried by the runner.
|
|
32
|
+
*/
|
|
33
|
+
export type TPreuseOutcome = 'completed' | 'skipped' | 'failed' | 'interrupted';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The reset schedule read after a prompt completed.
|
|
37
|
+
*
|
|
38
|
+
* `reported` carries what the provider answered, which may be no window at all; `unavailable` is a lookup
|
|
39
|
+
* that failed, and `notRead` is a run the user interrupted before it asked. None of them says anything
|
|
40
|
+
* about the completed prompt, which is never repeated because its schedule could not be confirmed.
|
|
41
|
+
*/
|
|
42
|
+
export type TPreuseSchedule =
|
|
43
|
+
| { kind: 'reported'; windows: readonly IHarnessUsageWindow[] }
|
|
44
|
+
| { kind: 'unavailable' }
|
|
45
|
+
| { kind: 'notRead' };
|
|
46
|
+
|
|
47
|
+
export type TPreuseAccountResult =
|
|
48
|
+
| { account: IHarnessAccount; outcome: 'completed'; result: IHarnessPreuseResult; schedule: TPreuseSchedule }
|
|
49
|
+
| { account: IHarnessAccount; outcome: Exclude<TPreuseOutcome, 'completed'>; reason: string };
|
|
50
|
+
|
|
51
|
+
/** One account starting, and that account's result. A run reports nothing else, and never the generated prose. */
|
|
52
|
+
export type TPreuseRunEvent =
|
|
53
|
+
| { kind: 'started'; index: number; total: number; account: IHarnessAccount }
|
|
54
|
+
| { kind: 'finished'; index: number; total: number; result: TPreuseAccountResult };
|
|
55
|
+
|
|
56
|
+
export interface IPreuseRunSummary {
|
|
57
|
+
results: readonly TPreuseAccountResult[];
|
|
58
|
+
counts: Readonly<Record<TPreuseOutcome, number>>;
|
|
59
|
+
/** Accounts an abort kept the run from reaching. Nothing was sent for them. */
|
|
60
|
+
notRun: readonly IHarnessAccount[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every account a run may preuse, in the order the harness lists them, each at most once.
|
|
65
|
+
*
|
|
66
|
+
* A harness reports an active login and that login's saved record as one account -- Codex keys both by
|
|
67
|
+
* email, the file harness merges the active login into its saved record by account id -- so the walk is
|
|
68
|
+
* the account list itself. The seen set keeps a repeated id from being charged twice regardless.
|
|
69
|
+
*/
|
|
70
|
+
export const preuseTargets = (accountsArg: readonly IHarnessAccount[]): IHarnessAccount[] => {
|
|
71
|
+
const seen = new Set<string>();
|
|
72
|
+
const targets: IHarnessAccount[] = [];
|
|
73
|
+
for (const account of accountsArg) {
|
|
74
|
+
if (seen.has(account.id)) continue;
|
|
75
|
+
seen.add(account.id);
|
|
76
|
+
targets.push(account);
|
|
77
|
+
}
|
|
78
|
+
return targets;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A run's counts in one line, for a command's closing line and for the dashboard footer.
|
|
83
|
+
*
|
|
84
|
+
* Completed accounts are always named, including none of them; every other count appears only when it
|
|
85
|
+
* happened, so a clean run reads as one fact rather than as four.
|
|
86
|
+
*/
|
|
87
|
+
export const preuseCountsText = (summaryArg: IPreuseRunSummary): string => [
|
|
88
|
+
`${summaryArg.counts.completed} completed`,
|
|
89
|
+
...(summaryArg.counts.skipped ? [`${summaryArg.counts.skipped} skipped`] : []),
|
|
90
|
+
...(summaryArg.counts.failed ? [`${summaryArg.counts.failed} failed`] : []),
|
|
91
|
+
...(summaryArg.counts.interrupted ? [`${summaryArg.counts.interrupted} interrupted`] : []),
|
|
92
|
+
...(summaryArg.notRun.length ? [`${summaryArg.notRun.length} not run`] : []),
|
|
93
|
+
].join(', ');
|
|
94
|
+
|
|
95
|
+
/** An adapter's own fixed message, or the fixed local text for an error that crossed the boundary unnamed. */
|
|
96
|
+
const preuseReason = (errorArg: unknown): string => errorArg instanceof PreuseError
|
|
97
|
+
? errorArg.message
|
|
98
|
+
: 'Preuse failed. Tokens may already have been consumed; check account status before trying again.';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The account's reset schedule, read after its prompt completed, exactly as the single-account command reads it.
|
|
102
|
+
*
|
|
103
|
+
* The read never activates or writes a login, and its failure never turns a completed prompt into a failed
|
|
104
|
+
* one: there is nothing to undo and nothing worth repeating. An interrupted run does not ask at all.
|
|
105
|
+
*/
|
|
106
|
+
const readPreuseSchedule = async (harnessArg: IAuthHarness, accountArg: IHarnessAccount, signalArg: AbortSignal): Promise<TPreuseSchedule> => {
|
|
107
|
+
if (signalArg.aborted) return { kind: 'notRead' };
|
|
108
|
+
try {
|
|
109
|
+
const status = await harnessArg.readAccountStatus(accountArg.id, { signal: signalArg });
|
|
110
|
+
return { kind: 'reported', windows: orderedUsageWindows(status.summary?.usageWindows) };
|
|
111
|
+
} catch { return { kind: 'unavailable' }; }
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Send one preuse prompt through each account in turn, and report what each one did.
|
|
116
|
+
*
|
|
117
|
+
* Accounts run strictly one after another, each with exactly one inference request and no retry, because
|
|
118
|
+
* every request costs the account's own quota. An aborted signal stops the run before the next account
|
|
119
|
+
* starts, and an account already in flight is reported as interrupted: its tokens may be gone either way,
|
|
120
|
+
* which is why nothing is repeated. Nothing is switched, refreshed, stopped or written anywhere.
|
|
121
|
+
*/
|
|
122
|
+
export const preuseAccounts = async (
|
|
123
|
+
harnessArg: IAuthHarness,
|
|
124
|
+
accountsArg: readonly IHarnessAccount[],
|
|
125
|
+
optionsArg: { prompt: string; model?: string; signal: AbortSignal },
|
|
126
|
+
reportArg: (eventArg: TPreuseRunEvent) => void,
|
|
127
|
+
): Promise<IPreuseRunSummary> => {
|
|
128
|
+
const preuseAccount = harnessArg.preuseAccount?.bind(harnessArg);
|
|
129
|
+
if (!preuseAccount) throw new PreuseError('This harness does not support preuse.');
|
|
130
|
+
validatePreuseOptions(optionsArg);
|
|
131
|
+
const results: TPreuseAccountResult[] = [];
|
|
132
|
+
const counts: Record<TPreuseOutcome, number> = { completed: 0, skipped: 0, failed: 0, interrupted: 0 };
|
|
133
|
+
const notRun: IHarnessAccount[] = [];
|
|
134
|
+
const total = accountsArg.length;
|
|
135
|
+
for (const [index, account] of accountsArg.entries()) {
|
|
136
|
+
if (optionsArg.signal.aborted) { notRun.push(account); continue; }
|
|
137
|
+
reportArg({ kind: 'started', index, total, account });
|
|
138
|
+
let result: TPreuseAccountResult;
|
|
139
|
+
try {
|
|
140
|
+
const completed = await preuseAccount(account.id, { prompt: optionsArg.prompt, model: optionsArg.model, signal: optionsArg.signal });
|
|
141
|
+
result = { account, outcome: 'completed', result: completed, schedule: await readPreuseSchedule(harnessArg, account, optionsArg.signal) };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// A cancelled account is interrupted whether or not its prompt went out; its reason says which.
|
|
144
|
+
const outcome = optionsArg.signal.aborted ? 'interrupted'
|
|
145
|
+
: error instanceof PreuseError && !error.requestStarted ? 'skipped' : 'failed';
|
|
146
|
+
result = { account, outcome, reason: preuseReason(error) };
|
|
147
|
+
}
|
|
148
|
+
results.push(result);
|
|
149
|
+
counts[result.outcome]++;
|
|
150
|
+
reportArg({ kind: 'finished', index, total, result });
|
|
151
|
+
}
|
|
152
|
+
return { results, counts, notRun };
|
|
153
|
+
};
|