@modelprofile.com/authswitch 3.2.0 → 3.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.
Files changed (38) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/accounts.d.ts +25 -2
  3. package/dist_ts/accounts.js +112 -15
  4. package/dist_ts/classes.accountlist.d.ts +2 -1
  5. package/dist_ts/classes.accountlist.js +8 -5
  6. package/dist_ts/classes.claudecodeharness.d.ts +2 -2
  7. package/dist_ts/classes.claudecodeharness.js +3 -3
  8. package/dist_ts/classes.claudestatus.d.ts +2 -2
  9. package/dist_ts/classes.claudestatus.js +7 -6
  10. package/dist_ts/classes.cli.d.ts +15 -0
  11. package/dist_ts/classes.cli.js +51 -16
  12. package/dist_ts/classes.codexharness.d.ts +2 -2
  13. package/dist_ts/classes.codexharness.js +3 -3
  14. package/dist_ts/classes.codexstatus.d.ts +3 -3
  15. package/dist_ts/classes.codexstatus.js +12 -11
  16. package/dist_ts/classes.fileharness.d.ts +2 -2
  17. package/dist_ts/classes.limits.js +29 -13
  18. package/dist_ts/classes.opencodeharness.d.ts +2 -2
  19. package/dist_ts/classes.opencodeharness.js +3 -3
  20. package/dist_ts/classes.tui.js +7 -4
  21. package/dist_ts/interfaces.harness.d.ts +17 -5
  22. package/dist_ts/interfaces.list.d.ts +5 -1
  23. package/package.json +2 -2
  24. package/readme.md +62 -13
  25. package/ts/00_commitinfo_data.ts +1 -1
  26. package/ts/accounts.ts +105 -12
  27. package/ts/classes.accountlist.ts +7 -4
  28. package/ts/classes.claudecodeharness.ts +3 -3
  29. package/ts/classes.claudestatus.ts +7 -6
  30. package/ts/classes.cli.ts +56 -15
  31. package/ts/classes.codexharness.ts +3 -3
  32. package/ts/classes.codexstatus.ts +12 -11
  33. package/ts/classes.fileharness.ts +2 -2
  34. package/ts/classes.limits.ts +32 -13
  35. package/ts/classes.opencodeharness.ts +3 -3
  36. package/ts/classes.tui.ts +6 -3
  37. package/ts/interfaces.harness.ts +19 -2
  38. package/ts/interfaces.list.ts +5 -1
@@ -1,5 +1,5 @@
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
4
 
5
5
  // Verified Chromium-compatible billing request profile, independent of any locally installed browser.
@@ -39,7 +39,7 @@ class StatusRequestError extends Error {}
39
39
  export class CodexAccountStatus {
40
40
  constructor(private readonly fetcher: typeof fetch = globalThis.fetch) {}
41
41
 
42
- public async read(rawArg: string, identityArg: ICodexIdentity): Promise<IHarnessAccountStatus> {
42
+ public async read(rawArg: string, identityArg: ICodexIdentity, optionsArg: IHarnessStatusOptions = {}): Promise<IHarnessAccountStatus> {
43
43
  if (identityArg.kind !== 'chatgpt') return {
44
44
  facts: [{ label: 'Authentication', value: 'API key' }, { label: 'Subscription and usage', value: 'Not exposed by Codex for API-key logins' }], problems: [],
45
45
  };
@@ -47,18 +47,18 @@ export class CodexAccountStatus {
47
47
  try { accessToken = text(record(record(JSON.parse(rawArg)).tokens).access_token); }
48
48
  catch { return { facts: [], problems: ['The login has no usable access token. Log in again with Codex, then save the account.'] }; }
49
49
  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);
50
+ return this.readCredential(accessToken, identityArg.accountId, identityArg.planType, optionsArg);
51
51
  }
52
52
 
53
53
  /** 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> {
54
+ public async readCredential(accessToken: string, accountId: string, planType?: string | null, optionsArg: IHarnessStatusOptions = {}): Promise<IHarnessAccountStatus> {
55
55
  const result: IHarnessAccountStatus = { facts: [], problems: [] };
56
56
  const identityArg = { accountId, planType };
57
57
  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),
58
+ this.get('wham/usage', accessToken, identityArg.accountId, optionsArg.signal),
59
+ this.get('wham/rate-limit-reset-credits', accessToken, identityArg.accountId, optionsArg.signal),
60
+ this.get('wham/profiles/me', accessToken, identityArg.accountId, optionsArg.signal),
61
+ this.get('accounts/check/v4-2023-04-27', accessToken, identityArg.accountId, optionsArg.signal),
62
62
  ]);
63
63
  const usage = sections[0];
64
64
  if (usage.status === 'fulfilled' && usage.value.account_id != null && usage.value.account_id !== identityArg.accountId) {
@@ -92,8 +92,9 @@ export class CodexAccountStatus {
92
92
  return result;
93
93
  }
94
94
 
95
- private async get(pathArg: string, tokenArg: string, accountIdArg: string): Promise<Record<string, unknown>> {
96
- const signal = AbortSignal.timeout(10_000);
95
+ private async get(pathArg: string, tokenArg: string, accountIdArg: string, cancelArg?: AbortSignal): Promise<Record<string, unknown>> {
96
+ const timeout = AbortSignal.timeout(10_000);
97
+ const signal = cancelArg ? AbortSignal.any([cancelArg, timeout]) : timeout;
97
98
  try {
98
99
  const response = await this.fetcher(`https://chatgpt.com/backend-api/${pathArg}`, {
99
100
  method: 'GET', redirect: 'error', signal,
@@ -125,7 +126,7 @@ export class CodexAccountStatus {
125
126
  catch { throw new StatusRequestError('The service returned invalid status data.'); }
126
127
  } catch (errorArg) {
127
128
  // Never surface remote bodies, request headers, token strings or arbitrary fetch errors.
128
- if (signal.aborted) throw new Error('The status request timed out.');
129
+ if (signal.aborted) throw new Error(timeout.aborted ? 'The status request timed out.' : 'The status request was cancelled.');
129
130
  if (errorArg instanceof StatusRequestError) throw errorArg;
130
131
  throw new Error('The status service could not be reached.');
131
132
  }
@@ -1,4 +1,4 @@
1
- import type { IAuthHarness, IHarnessAccountStatus, IHarnessCredentialDrift, IHarnessOutcome, IHarnessProcessControl, IHarnessState } from './interfaces.harness.js';
1
+ import type { IAuthHarness, IHarnessAccountStatus, IHarnessCredentialDrift, IHarnessOutcome, IHarnessProcessControl, IHarnessState, IHarnessStatusOptions } from './interfaces.harness.js';
2
2
  import { credentialHash, CredentialStore, type IStoredCredential, type ISwitchRecord } from './classes.credentialstore.js';
3
3
  import { pauseSynchronously } from './helpers.js';
4
4
  import { plainText } from './formatting.js';
@@ -28,7 +28,7 @@ export abstract class FileHarness implements IAuthHarness {
28
28
  /** Raw bytes of the native files a snapshot's credentials are taken from, in a fixed order. */
29
29
  protected abstract nativeSources(): (string | null)[];
30
30
  protected abstract inspect(credential: Record<string, unknown>, slotId: string): IFileAccount;
31
- public abstract readAccountStatus(id: string): Promise<IHarnessAccountStatus>;
31
+ public abstract readAccountStatus(id: string, optionsArg?: IHarnessStatusOptions): Promise<IHarnessAccountStatus>;
32
32
 
33
33
  /**
34
34
  * A snapshot proven not to have been taken across a native write.
@@ -1,8 +1,8 @@
1
1
  import * as plugins from './plugins.js';
2
- import { compactSince, compactUntil, credentialDriftNote, orderedUsageWindows } from './accounts.js';
2
+ import { accountTypeText, compactSince, compactUntil, credentialDriftNote, orderedUsageWindows } from './accounts.js';
3
3
  import { consoleHeading, consoleTable } from './classes.accountlist.js';
4
4
  import type { IAccountList, IAccountLimitRow, IAccountLimits, IActiveAccountDrift, IActiveAccountRow, IActiveAccounts, IHarnessAccountList } from './interfaces.list.js';
5
- import type { IHarnessCredentialDrift, TUsageSeverity } from './interfaces.harness.js';
5
+ import type { IAccountType, IHarnessCredentialDrift, TUsageSeverity } from './interfaces.harness.js';
6
6
  import { dim, plainText } from './formatting.js';
7
7
 
8
8
  type TListedAccount = IHarnessAccountList['accounts'][number];
@@ -46,6 +46,12 @@ const accountKeys = (harnessArg: IHarnessAccountList, accountArg: TListedAccount
46
46
  ...(accountArg.slotId === undefined ? {} : { slotId: plainText(accountArg.slotId) }),
47
47
  });
48
48
 
49
+ /** The account's plan, sanitised like every other label and copied so that no two rows share one object. */
50
+ const accountType = (accountArg: TListedAccount): IAccountType | null => {
51
+ const subscription = accountArg.status.summary?.subscription;
52
+ return subscription === undefined ? null : { plan: plainText(subscription.plan), source: subscription.source };
53
+ };
54
+
49
55
  const harnessProblem = (harnessArg: IHarnessAccountList): string | null =>
50
56
  harnessArg.problems.length ? harnessArg.problems.map(plainText).join(' ') : null;
51
57
 
@@ -76,6 +82,9 @@ const byProviderThenAccount = <TRow extends TCondensedRow>(rowsArg: TRow[]): TRo
76
82
  /** One colour line per harness: an OpenCode installation's slots share it. */
77
83
  const harnessRuns = (rowArg: TCondensedRow): string => rowArg.harnessId;
78
84
 
85
+ /** One Provider cell per provider name within a harness; two harnesses that share a label stay apart. */
86
+ const providerRuns = (rowArg: TCondensedRow): string => JSON.stringify([rowArg.harnessId, rowArg.provider]);
87
+
79
88
  /** One divider run per account identity; a provider-level row without an account is its own run. */
80
89
  const accountRuns = (rowArg: TCondensedRow): string => JSON.stringify([rowArg.harnessId, rowArg.slotId ?? null, rowArg.accountId]);
81
90
 
@@ -108,23 +117,23 @@ export const accountLimits = (listArg: IAccountList): IAccountLimits => {
108
117
  limitType: null, scope: null, windowSeconds: null, usedPercent: null, resetAt: null, resetsIn: UNAVAILABLE, severity: null, headline: false,
109
118
  } as const;
110
119
  if (problem !== null) {
111
- rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, isActive: false, ...empty, unavailableReason: problem });
120
+ rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, isActive: false, accountType: null, ...empty, unavailableReason: problem });
112
121
  continue;
113
122
  }
114
123
  if (!harness.accounts.length) {
115
- rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, isActive: false, ...empty,
124
+ rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, isActive: false, accountType: null, ...empty,
116
125
  unavailableReason: harness.saveUnavailableReason === null ? 'No accounts are known for this harness.' : plainText(harness.saveUnavailableReason) });
117
126
  continue;
118
127
  }
119
128
  for (const account of harness.accounts) {
120
129
  const windows = orderedUsageWindows(account.status.summary?.usageWindows);
121
130
  if (!windows.length) {
122
- rows.push({ ...accountKeys(harness, account), isActive: account.isActive, ...empty, unavailableReason: missingLimitReason(account) });
131
+ rows.push({ ...accountKeys(harness, account), isActive: account.isActive, accountType: accountType(account), ...empty, unavailableReason: missingLimitReason(account) });
123
132
  continue;
124
133
  }
125
134
  for (const window of windows) {
126
135
  rows.push({
127
- ...accountKeys(harness, account), isActive: account.isActive,
136
+ ...accountKeys(harness, account), isActive: account.isActive, accountType: accountType(account),
128
137
  limitType: plainText(window.label), scope: window.scope ?? 'account', windowSeconds: window.durationSeconds,
129
138
  usedPercent: window.usedPercent, resetAt: window.resetAt, resetsIn: compactUntil(window.resetAt, now, UNAVAILABLE),
130
139
  severity: window.severity ?? null, headline: window.headline === true, unavailableReason: null,
@@ -154,14 +163,14 @@ export const activeAccounts = (listArg: IAccountList): IActiveAccounts => {
154
163
  return index < 0 ? null : activeDrift(harness, drifts.splice(index, 1)[0]);
155
164
  };
156
165
  if (problem !== null) {
157
- rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, savedAt: null, savedAgo: UNAVAILABLE, source: 'none', unavailableReason: problem, drift: null });
166
+ rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, accountType: null, savedAt: null, savedAgo: UNAVAILABLE, source: 'none', unavailableReason: problem, drift: null });
158
167
  continue;
159
168
  }
160
169
  const active = harness.accounts.filter(account => account.isActive);
161
170
  if (!active.length) {
162
171
  const stashed = harness.accounts.filter(account => account.isStashed);
163
172
  rows.push({
164
- harnessId: harness.id, provider: providerName(harness), account: null, accountId: null,
173
+ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, accountType: null,
165
174
  savedAt: null, savedAgo: UNAVAILABLE, source: stashed.length ? 'stash only' : 'none',
166
175
  unavailableReason: harness.saveUnavailableReason !== null ? plainText(harness.saveUnavailableReason)
167
176
  : stashed.length ? `No login is active; ${stashed.length} saved account(s) can be activated with authswitch ${plainText(harness.id)} use.`
@@ -171,7 +180,7 @@ export const activeAccounts = (listArg: IAccountList): IActiveAccounts => {
171
180
  } else {
172
181
  for (const account of active) {
173
182
  rows.push({
174
- ...accountKeys(harness, account),
183
+ ...accountKeys(harness, account), accountType: accountType(account),
175
184
  savedAt: account.savedAt, savedAgo: compactSince(account.savedAt, now, UNAVAILABLE),
176
185
  source: 'credential file',
177
186
  unavailableReason: account.isStashed ? null : 'This active login is not saved yet; authswitch cannot restore it after a switch.',
@@ -188,6 +197,13 @@ export const activeAccounts = (listArg: IAccountList): IActiveAccounts => {
188
197
  return { schemaVersion: 1, generatedAt: listArg.generatedAt, complete: listArg.complete, active, notes: [...collectNotes(active), ...driftNotes, ...unattached] };
189
198
  };
190
199
 
200
+ /**
201
+ * The Account cell: the label, and the account type beneath it, dim. An unknown type keeps its line as
202
+ * `n/a`, so every account cell has the same shape; a provider row without an account has no type to name.
203
+ */
204
+ const accountCell = (rowArg: IAccountLimitRow): plugins.smartconsole.TText => rowArg.account === null ? UNAVAILABLE
205
+ : plugins.smartconsole.color.concat(`${rowArg.account}\n`, plugins.smartconsole.color.text(accountTypeText(rowArg.accountType) ?? UNAVAILABLE, { dim: true, bold: false }));
206
+
191
207
  const percent = (rowArg: IAccountLimitRow): string => rowArg.usedPercent === null ? UNAVAILABLE : `${rowArg.usedPercent}%`;
192
208
 
193
209
  const SEVERITY_COLORS: Record<TUsageSeverity, plugins.smartconsole.TColorName | undefined> = { normal: undefined, warning: 'orange', critical: 'red' };
@@ -209,10 +225,13 @@ export class CondensedRenderer {
209
225
 
210
226
  public async renderLimits(limitsArg: IAccountLimits): Promise<void> {
211
227
  consoleHeading('Account limits');
212
- if (!limitsArg.limits.length) { process.stdout.write(`${dim('No harnesses registered.')}\n`); return; }
213
- await consoleTable(this.out, limitsArg.limits, [
214
- { key: 'provider', title: 'Provider', value: row => row.provider },
215
- { key: 'account', title: 'Account', value: row => row.account ?? UNAVAILABLE, style: row => ({ bold: row.isActive }) },
228
+ // The span callbacks record these exact rows, so the table renders this array as it is.
229
+ const rows = limitsArg.limits;
230
+ if (!rows.length) { process.stdout.write(`${dim('No harnesses registered.')}\n`); return; }
231
+ await consoleTable(this.out, rows, [
232
+ { key: 'provider', title: 'Provider', value: row => row.provider, rowSpan: plugins.smartconsole.rowSpansBy(rows, providerRuns) },
233
+ { key: 'account', title: 'Account', value: row => row.account ?? UNAVAILABLE, render: accountCell,
234
+ style: row => ({ bold: row.isActive }), rowSpan: plugins.smartconsole.rowSpansBy(rows, accountRuns) },
216
235
  { key: 'limit', title: 'Limit type', value: row => row.limitType ?? UNAVAILABLE },
217
236
  { key: 'used', title: 'Used %', value: percent, align: 'right',
218
237
  style: row => ({ foreground: percentColor(row) }) },
@@ -4,7 +4,7 @@ import { CredentialStore, credentialHash, credentialRecord, credentialText, read
4
4
  import { HarnessProcesses } from './classes.harnessprocesses.js';
5
5
  import { CodexAccountStatus } from './classes.codexstatus.js';
6
6
  import { writeSecretFileAtomically } from './helpers.js';
7
- import type { IHarnessAccountStatus, IHarnessLoginOptions, IHarnessLoginProvider, IHarnessProcessControl } from './interfaces.harness.js';
7
+ import type { IHarnessAccountStatus, IHarnessLoginOptions, IHarnessLoginProvider, IHarnessProcessControl, IHarnessStatusOptions } from './interfaces.harness.js';
8
8
  import { beginSavedOpenAiLogin, openCodeCredentialFromLogin } from './classes.login.js';
9
9
  import { plainText } from './formatting.js';
10
10
 
@@ -100,12 +100,12 @@ export class OpenCodeHarness extends FileHarness {
100
100
  writeSecretFileAtomically(this.file, raw);
101
101
  if (readCredentialDocument(this.file).raw !== raw) throw new Error('Active login verification failed. The outgoing login remains saved.');
102
102
  }
103
- public async readAccountStatus(id: string): Promise<IHarnessAccountStatus> {
103
+ public async readAccountStatus(id: string, optionsArg?: IHarnessStatusOptions): Promise<IHarnessAccountStatus> {
104
104
  const account = this.readCredential(id);
105
105
  if (account.slotId === 'openai' && account.credential.type === 'oauth') {
106
106
  const info = this.openAiInfo(account.credential);
107
107
  if (!info.accountId) return { facts: [], problems: ['The OpenAI login has no account ID for a status lookup.'] };
108
- return this.status.readCredential(credentialText(account.credential.access)!, info.accountId, info.plan);
108
+ return this.status.readCredential(credentialText(account.credential.access)!, info.accountId, info.plan, optionsArg);
109
109
  }
110
110
  return { facts: [
111
111
  { section: 'Authentication', label: 'Provider', value: account.slotId },
package/ts/classes.tui.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type { IAuthHarness, IHarnessOutcome, IHarnessProcess, IHarnessState, IHarnessStopOutcome } from './interfaces.harness.js';
3
- import { accountDetails, accountPlan, accountQuotaSummary, accountResets, accountState, accountUsageWindows, usageWindowName, until, readAccountRows, type IAccountRow } from './accounts.js';
3
+ import { accountBadge, accountDetails, accountPlan, accountQuotaSummary, accountResets, accountState, accountUsageWindows, usageWindowName, until, readAccountRows, type IAccountRow } from './accounts.js';
4
4
  import { plainText } from './formatting.js';
5
5
  import { AuthSwitchOperations } from './classes.operations.js';
6
6
  import { describeHarnessProcesses, describeStopOutcome } from './classes.harnessprocesses.js';
@@ -115,11 +115,14 @@ export class AuthSwitchTui {
115
115
  const verified = await harness.readState();
116
116
  if (!verified.accounts.some(account => account.isActive && account.isStashed && account.id === active.id)) { footer.setText('The current login could not be verified as saved. Switching was cancelled.'); return; }
117
117
  }
118
- if (!await screen.confirm(`Switch ${plainText(harness.label)} to ${plainText(target.label)}?`, { confirmLabel: 'Switch' })) return;
118
+ // The dashboard already holds this account's status, so the badge costs no further lookup.
119
+ const badge = accountBadge(rows.find(row => row.account.id === target.id)?.status);
120
+ if (!await screen.confirm(`Switch ${plainText(harness.label)} to ${plainText(target.label)}${badge}?`, { confirmLabel: 'Switch' })) return;
119
121
  if (screen.signal.aborted) return;
120
122
  const outcome = await this.operations.run(harness, { harnessId: harness.id, action: 'switch', accountId: target.id },
121
123
  message => screen.confirm(message), settleInstances(screen));
122
- await refresh(screen); showOutcome(outcome);
124
+ await refresh(screen);
125
+ if (showOutcome(outcome)) ui.logs.append(`Now active: ${plainText(target.label)}${badge}`);
123
126
  };
124
127
  const remove = async (screen: plugins.smartconsole.ITuiContext): Promise<void> => {
125
128
  const selected = table.selected;
@@ -127,9 +127,20 @@ export interface IHarnessUsageWindow {
127
127
  headline?: true;
128
128
  }
129
129
 
130
+ /**
131
+ * The provider's own name for an account's plan, and where that name comes from.
132
+ *
133
+ * `live` was read from the provider in this lookup; `stored` is the last name the saved login
134
+ * carries, which is the only answer available while a provider cannot be reached.
135
+ */
136
+ export interface IAccountType {
137
+ plan: string;
138
+ source: 'live' | 'stored';
139
+ }
140
+
130
141
  export interface IHarnessStatusSummary {
131
142
  /** A plan name does not imply an active billing status. */
132
- subscription?: { plan: string; source: 'live' | 'stored' };
143
+ subscription?: IAccountType;
133
144
  /** Live billing information only. Never infer these fields from a plan or entitlement expiry. */
134
145
  billing?: { hasActiveSubscription?: boolean; autoRenew?: boolean; renewsAt?: string; cancelsAt?: string; expiresAt?: string };
135
146
  /** Missing windows are unknown, not unused. Times are ISO 8601 UTC. */
@@ -146,6 +157,12 @@ export interface IHarnessOutcome {
146
157
  problems: string[];
147
158
  }
148
159
 
160
+ /** A read-only status lookup. */
161
+ export interface IHarnessStatusOptions {
162
+ /** Cancels the lookup's requests; an adapter still bounds each request with its own timeout. */
163
+ signal?: AbortSignal;
164
+ }
165
+
149
166
  /** A single text-only inference request. Credentials remain inside the adapter. */
150
167
  export interface IHarnessPreuseOptions {
151
168
  prompt: string;
@@ -171,7 +188,7 @@ export interface IAuthHarness {
171
188
  readState(): THarnessResult<IHarnessState>;
172
189
  /** Running instances of this harness, when the adapter owns their lifecycle. */
173
190
  readonly processes?: IHarnessProcessControl;
174
- readAccountStatus(accountIdArg: string): Promise<IHarnessAccountStatus>;
191
+ readAccountStatus(accountIdArg: string, optionsArg?: IHarnessStatusOptions): Promise<IHarnessAccountStatus>;
175
192
  readonly loginProviders?: IHarnessLoginProvider[];
176
193
  beginLogin?(options: IHarnessLoginOptions): Promise<IHarnessLoginHandle>;
177
194
  /** Optional quota-consuming capability; never changes the active login. */
@@ -1,4 +1,4 @@
1
- import type { IHarnessAccount, IHarnessAccountStatus, IHarnessCredentialDrift, TUsageSeverity } from './interfaces.harness.js';
1
+ import type { IAccountType, IHarnessAccount, IHarnessAccountStatus, IHarnessCredentialDrift, TUsageSeverity } from './interfaces.harness.js';
2
2
 
3
3
  /** Credential-free, versioned output of list --json. Missing status fields stay omitted. */
4
4
  export interface IAccountList {
@@ -39,6 +39,8 @@ export interface IAccountLimitRow {
39
39
  accountId: string | null;
40
40
  slotId?: string;
41
41
  isActive: boolean;
42
+ /** The adapter's plan name (`max`, `pro`), sanitised but not display-formatted; null when none is known or the row names no account. */
43
+ accountType: IAccountType | null;
42
44
  /** The provider's own window name, or null when no limit data is available. */
43
45
  limitType: string | null;
44
46
  scope: 'account' | 'feature' | null;
@@ -70,6 +72,8 @@ export interface IActiveAccountRow {
70
72
  account: string | null;
71
73
  accountId: string | null;
72
74
  slotId?: string;
75
+ /** The adapter's plan name (`max`, `pro`), sanitised but not display-formatted; null when none is known or the row names no account. */
76
+ accountType: IAccountType | null;
73
77
  /** When authswitch last saved this login; null when it was never stashed. */
74
78
  savedAt: string | null;
75
79
  /** Human elapsed time for the same `savedAt`, relative to `generatedAt`. */