@modelprofile.com/authswitch 3.0.0 → 3.2.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/accounts.d.ts +13 -5
- package/dist_ts/accounts.js +20 -5
- package/dist_ts/classes.accountlist.d.ts +9 -3
- package/dist_ts/classes.accountlist.js +66 -32
- package/dist_ts/classes.claudestatus.js +94 -18
- package/dist_ts/classes.limits.js +36 -10
- package/dist_ts/classes.tui.js +4 -3
- package/dist_ts/interfaces.harness.d.ts +18 -8
- package/dist_ts/interfaces.list.d.ts +5 -1
- package/package.json +3 -3
- package/readme.md +107 -33
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/accounts.ts +22 -8
- package/ts/classes.accountlist.ts +69 -33
- package/ts/classes.claudestatus.ts +102 -14
- package/ts/classes.limits.ts +43 -11
- package/ts/classes.tui.ts +5 -4
- package/ts/interfaces.harness.ts +20 -8
- package/ts/interfaces.list.ts +5 -1
|
@@ -1,8 +1,107 @@
|
|
|
1
1
|
import { credentialRecord, credentialText } from './classes.credentialstore.js';
|
|
2
|
-
import
|
|
2
|
+
import { plainText } from './formatting.js';
|
|
3
|
+
import type { IHarnessAccountStatus, IHarnessUsageWindow, TUsageSeverity } from './interfaces.harness.js';
|
|
3
4
|
|
|
4
5
|
class ClaudeStatusError extends Error {}
|
|
5
6
|
|
|
7
|
+
interface IClaudeUsageWindows {
|
|
8
|
+
windows: IHarnessUsageWindow[];
|
|
9
|
+
/** Server rows whose reset group the window model cannot represent, named by kind and group. */
|
|
10
|
+
unsupported: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Reset times are normalised to ISO 8601 UTC; an unparsable one rejects the whole usage response. */
|
|
14
|
+
const resetTimestamp = (value: unknown): string | null => value == null ? null : new Date(String(value)).toISOString();
|
|
15
|
+
|
|
16
|
+
const SERVER_NAME_LENGTH = 64;
|
|
17
|
+
|
|
18
|
+
/** A server-supplied name becomes label text: no control characters, bounded by code points, and never blank. */
|
|
19
|
+
const serverName = (value: unknown): string => {
|
|
20
|
+
const name = typeof value === 'string' ? Array.from(plainText(value).trim()).slice(0, SERVER_NAME_LENGTH).join('').trimEnd() : '';
|
|
21
|
+
if (!name) throw new Error('Unsupported Claude usage name.');
|
|
22
|
+
return name;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Keyed windows, read only from a usage body that carries no limits[] rows. */
|
|
26
|
+
const KEYED_WINDOWS = [
|
|
27
|
+
['five_hour', 'Claude five-hour', 18000, 'account'], ['seven_day', 'Claude weekly', 604800, 'account'],
|
|
28
|
+
['seven_day_oauth_apps', 'OAuth apps weekly', 604800, 'feature'], ['seven_day_opus', 'Opus weekly', 604800, 'feature'], ['seven_day_sonnet', 'Sonnet weekly', 604800, 'feature'],
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
const keyedWindows = (usage: Record<string, unknown>): IHarnessUsageWindow[] => {
|
|
32
|
+
const windows: IHarnessUsageWindow[] = [];
|
|
33
|
+
for (const [key, label, durationSeconds, scope] of KEYED_WINDOWS) {
|
|
34
|
+
if (usage[key] == null) continue;
|
|
35
|
+
const window = credentialRecord(usage[key]);
|
|
36
|
+
if (window.utilization == null) continue;
|
|
37
|
+
if (typeof window.utilization !== 'number' || !Number.isFinite(window.utilization) || window.utilization < 0) throw new Error('Unsupported Claude usage utilization.');
|
|
38
|
+
windows.push({ label, durationSeconds, scope, usedPercent: window.utilization, resetAt: resetTimestamp(window.resets_at) });
|
|
39
|
+
}
|
|
40
|
+
return windows;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The limits[] row groups the window model represents: each group's duration, the period word its
|
|
45
|
+
* labels use, and the kind of the group's unscoped account meter, which keeps the plain label.
|
|
46
|
+
*/
|
|
47
|
+
const LIMIT_GROUPS: ReadonlyMap<string, { durationSeconds: number; period: string; accountKind: string }> = new Map([
|
|
48
|
+
['session', { durationSeconds: 18000, period: 'five-hour', accountKind: 'session' }],
|
|
49
|
+
['weekly', { durationSeconds: 604800, period: 'weekly', accountKind: 'weekly_all' }],
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const isUsageSeverity = (value: unknown): value is TUsageSeverity => value === 'normal' || value === 'warning' || value === 'critical';
|
|
53
|
+
|
|
54
|
+
/** A scope part is absent (null or omitted) or names the model or surface a row is for. */
|
|
55
|
+
const scopeSubject = (value: unknown): string | undefined => value == null ? undefined : serverName(credentialRecord(value).display_name);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The server's usage rows, in the server's order. An unscoped row is an account meter; a model- or
|
|
59
|
+
* surface-scoped row is a feature quota named by the server's own label. Rows are classified on the
|
|
60
|
+
* kind and group exactly as sent; only the text shown to users is sanitised. Every row is validated
|
|
61
|
+
* before a row in an unrepresentable group is set aside, so a malformed row can never hide behind one.
|
|
62
|
+
*/
|
|
63
|
+
const limitWindows = (rows: readonly unknown[]): IClaudeUsageWindows => {
|
|
64
|
+
const windows: IHarnessUsageWindow[] = [];
|
|
65
|
+
const unsupported = new Set<string>();
|
|
66
|
+
for (const value of rows) {
|
|
67
|
+
const row = credentialRecord(value);
|
|
68
|
+
const { kind, group } = row;
|
|
69
|
+
if (typeof kind !== 'string' || typeof group !== 'string') throw new Error('Unsupported Claude usage meter.');
|
|
70
|
+
const kindName = serverName(kind);
|
|
71
|
+
const groupName = serverName(group);
|
|
72
|
+
const usedPercent = row.percent;
|
|
73
|
+
if (typeof usedPercent !== 'number' || !Number.isFinite(usedPercent) || usedPercent < 0) throw new Error('Unsupported Claude usage percentage.');
|
|
74
|
+
const resetAt = resetTimestamp(row.resets_at);
|
|
75
|
+
const scope: Record<string, unknown> = row.scope == null ? {} : credentialRecord(row.scope);
|
|
76
|
+
const model = scopeSubject(scope.model);
|
|
77
|
+
const surface = scopeSubject(scope.surface);
|
|
78
|
+
const subject = model ?? surface;
|
|
79
|
+
const meter = LIMIT_GROUPS.get(group);
|
|
80
|
+
if (!meter) {
|
|
81
|
+
unsupported.add(`${kindName} in group ${groupName}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const severity = row.severity;
|
|
85
|
+
windows.push({
|
|
86
|
+
label: subject === undefined ? `Claude ${meter.period}${kind === meter.accountKind ? '' : ` (${kindName})`}` : `${subject} ${meter.period}`,
|
|
87
|
+
durationSeconds: meter.durationSeconds, scope: subject === undefined ? 'account' : 'feature', usedPercent, resetAt,
|
|
88
|
+
...(isUsageSeverity(severity) ? { severity } : {}),
|
|
89
|
+
...(row.is_active === true ? { headline: true as const } : {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return { windows, unsupported: [...unsupported] };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A usage body's windows. Its limits[] rows are authoritative whenever the server sends any; the keyed
|
|
97
|
+
* fields describe the same meters and are read only from a body without rows.
|
|
98
|
+
*/
|
|
99
|
+
const claudeUsageWindows = (usage: Record<string, unknown>): IClaudeUsageWindows => {
|
|
100
|
+
const limits = usage.limits;
|
|
101
|
+
if (limits != null && !Array.isArray(limits)) throw new Error('Unsupported Claude usage limits.');
|
|
102
|
+
return Array.isArray(limits) && limits.length ? limitWindows(limits) : { windows: keyedWindows(usage), unsupported: [] };
|
|
103
|
+
};
|
|
104
|
+
|
|
6
105
|
/** Read-only Claude Code OAuth account endpoints. No refresh, inference, or browser session is used. */
|
|
7
106
|
export class ClaudeAccountStatus {
|
|
8
107
|
constructor(private readonly fetcher: typeof fetch = globalThis.fetch) {}
|
|
@@ -64,18 +163,7 @@ export class ClaudeAccountStatus {
|
|
|
64
163
|
} else result.problems.push(`Profile: ${profile.reason instanceof ClaudeStatusError ? profile.reason.message : 'Lookup failed.'}`);
|
|
65
164
|
if (usage.status === 'fulfilled') {
|
|
66
165
|
try {
|
|
67
|
-
const windows
|
|
68
|
-
for (const [key, label, durationSeconds, scope] of [
|
|
69
|
-
['five_hour', 'Claude five-hour', 18000, 'account'], ['seven_day', 'Claude weekly', 604800, 'account'],
|
|
70
|
-
['seven_day_oauth_apps', 'OAuth apps weekly', 604800, 'feature'], ['seven_day_opus', 'Opus weekly', 604800, 'feature'], ['seven_day_sonnet', 'Sonnet weekly', 604800, 'feature'],
|
|
71
|
-
] as const) {
|
|
72
|
-
if (usage.value[key] == null) continue;
|
|
73
|
-
const window = credentialRecord(usage.value[key]);
|
|
74
|
-
if (window.utilization == null) continue;
|
|
75
|
-
if (typeof window.utilization !== 'number' || !Number.isFinite(window.utilization) || window.utilization < 0) throw new Error();
|
|
76
|
-
const resetAt = window.resets_at == null ? null : new Date(String(window.resets_at)).toISOString();
|
|
77
|
-
windows.push({ label, durationSeconds, scope, usedPercent: window.utilization, resetAt });
|
|
78
|
-
}
|
|
166
|
+
const { windows, unsupported } = claudeUsageWindows(usage.value);
|
|
79
167
|
result.summary = { ...result.summary, usageWindows: windows };
|
|
80
168
|
for (const window of windows) result.facts.push({ section: 'Usage', summaryKey: 'usageWindows', label: window.label, value: `${window.usedPercent}% used; ${window.resetAt ? 'resets ' + window.resetAt : 'reset not scheduled'}` });
|
|
81
169
|
if (usage.value.extra_usage != null) {
|
|
@@ -90,7 +178,7 @@ export class ClaudeAccountStatus {
|
|
|
90
178
|
}
|
|
91
179
|
if (limit !== undefined && limit > 0 && used !== undefined) result.facts.push({ section: 'Extra usage', label: 'Monthly budget used', value: `${Math.round(used / limit * 10000) / 100}%` });
|
|
92
180
|
}
|
|
93
|
-
if (
|
|
181
|
+
if (unsupported.length) result.facts.push({ section: 'Availability', label: 'Unsupported limits', value: `The service returned limits without a supported reset window, which are not shown: ${unsupported.join(', ')}.` });
|
|
94
182
|
} catch { result.problems.push('Claude usage response is unsupported; missing values were not inferred.'); }
|
|
95
183
|
} else result.problems.push(`Usage: ${usage.reason instanceof ClaudeStatusError ? usage.reason.message : 'Lookup failed.'}`);
|
|
96
184
|
result.facts.push({ section: 'Availability', label: 'Billing dates and earned resets', value: 'Renewal dates, cancellation dates and earned reset credits are not exposed by these Claude Code endpoints.' });
|
package/ts/classes.limits.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import { 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 } from './interfaces.harness.js';
|
|
5
|
+
import type { IHarnessCredentialDrift, TUsageSeverity } from './interfaces.harness.js';
|
|
6
6
|
import { dim, plainText } from './formatting.js';
|
|
7
7
|
|
|
8
8
|
type TListedAccount = IHarnessAccountList['accounts'][number];
|
|
@@ -35,7 +35,7 @@ const missingLimitReason = (accountArg: TListedAccount): string => {
|
|
|
35
35
|
if (accountArg.status.summary?.usageWindows === undefined) {
|
|
36
36
|
return availability[0] ?? 'This login does not expose a usage or quota API.';
|
|
37
37
|
}
|
|
38
|
-
return 'The provider reported no usage window for this account.';
|
|
38
|
+
return 'The provider reported no usage window that authswitch can show for this account.';
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
const accountKeys = (harnessArg: IHarnessAccountList, accountArg: TListedAccount) => ({
|
|
@@ -49,10 +49,35 @@ const accountKeys = (harnessArg: IHarnessAccountList, accountArg: TListedAccount
|
|
|
49
49
|
const harnessProblem = (harnessArg: IHarnessAccountList): string | null =>
|
|
50
50
|
harnessArg.problems.length ? harnessArg.problems.map(plainText).join(' ') : null;
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
/** The identity fields both condensed row types share; labels are for display, these are for order and grouping. */
|
|
53
|
+
type TCondensedRow = Pick<IAccountLimitRow, 'harnessId' | 'provider' | 'account' | 'accountId' | 'slotId'>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Provider then account, keeping each account's windows in the order the adapter ranked them.
|
|
57
|
+
*
|
|
58
|
+
* Every harness and every account stays one consecutive run for the table's grouping: a harness is placed
|
|
59
|
+
* by its first provider name, so no other label sorts between its slots, and ids break label ties, so two
|
|
60
|
+
* harnesses sharing a label or two accounts sharing an email never interleave.
|
|
61
|
+
*/
|
|
62
|
+
const byProviderThenAccount = <TRow extends TCondensedRow>(rowsArg: TRow[]): TRow[] => {
|
|
63
|
+
const firstProvider = new Map<string, string>();
|
|
64
|
+
for (const row of rowsArg) {
|
|
65
|
+
const first = firstProvider.get(row.harnessId);
|
|
66
|
+
if (first === undefined || row.provider.localeCompare(first) < 0) firstProvider.set(row.harnessId, row.provider);
|
|
67
|
+
}
|
|
68
|
+
return [...rowsArg].sort((left, right) => firstProvider.get(left.harnessId)!.localeCompare(firstProvider.get(right.harnessId)!)
|
|
69
|
+
|| left.harnessId.localeCompare(right.harnessId)
|
|
70
|
+
|| left.provider.localeCompare(right.provider)
|
|
71
|
+
|| (left.account ?? '').localeCompare(right.account ?? '')
|
|
72
|
+
|| (left.slotId ?? '').localeCompare(right.slotId ?? '')
|
|
73
|
+
|| (left.accountId ?? '').localeCompare(right.accountId ?? ''));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** One colour line per harness: an OpenCode installation's slots share it. */
|
|
77
|
+
const harnessRuns = (rowArg: TCondensedRow): string => rowArg.harnessId;
|
|
78
|
+
|
|
79
|
+
/** One divider run per account identity; a provider-level row without an account is its own run. */
|
|
80
|
+
const accountRuns = (rowArg: TCondensedRow): string => JSON.stringify([rowArg.harnessId, rowArg.slotId ?? null, rowArg.accountId]);
|
|
56
81
|
|
|
57
82
|
const noteFor = (rowArg: { provider: string; account: string | null; unavailableReason: string | null }): string | null =>
|
|
58
83
|
rowArg.unavailableReason === null ? null
|
|
@@ -80,7 +105,7 @@ export const accountLimits = (listArg: IAccountList): IAccountLimits => {
|
|
|
80
105
|
for (const harness of listArg.harnesses) {
|
|
81
106
|
const problem = harnessProblem(harness);
|
|
82
107
|
const empty = {
|
|
83
|
-
limitType: null, scope: null, windowSeconds: null, usedPercent: null, resetAt: null, resetsIn: UNAVAILABLE,
|
|
108
|
+
limitType: null, scope: null, windowSeconds: null, usedPercent: null, resetAt: null, resetsIn: UNAVAILABLE, severity: null, headline: false,
|
|
84
109
|
} as const;
|
|
85
110
|
if (problem !== null) {
|
|
86
111
|
rows.push({ harnessId: harness.id, provider: providerName(harness), account: null, accountId: null, isActive: false, ...empty, unavailableReason: problem });
|
|
@@ -102,7 +127,7 @@ export const accountLimits = (listArg: IAccountList): IAccountLimits => {
|
|
|
102
127
|
...accountKeys(harness, account), isActive: account.isActive,
|
|
103
128
|
limitType: plainText(window.label), scope: window.scope ?? 'account', windowSeconds: window.durationSeconds,
|
|
104
129
|
usedPercent: window.usedPercent, resetAt: window.resetAt, resetsIn: compactUntil(window.resetAt, now, UNAVAILABLE),
|
|
105
|
-
unavailableReason: null,
|
|
130
|
+
severity: window.severity ?? null, headline: window.headline === true, unavailableReason: null,
|
|
106
131
|
});
|
|
107
132
|
}
|
|
108
133
|
}
|
|
@@ -165,6 +190,13 @@ export const activeAccounts = (listArg: IAccountList): IActiveAccounts => {
|
|
|
165
190
|
|
|
166
191
|
const percent = (rowArg: IAccountLimitRow): string => rowArg.usedPercent === null ? UNAVAILABLE : `${rowArg.usedPercent}%`;
|
|
167
192
|
|
|
193
|
+
const SEVERITY_COLORS: Record<TUsageSeverity, plugins.smartconsole.TColorName | undefined> = { normal: undefined, warning: 'orange', critical: 'red' };
|
|
194
|
+
|
|
195
|
+
/** The provider's own reading colours a window; without one, n/a and exhausted windows stand out. */
|
|
196
|
+
const percentColor = (rowArg: IAccountLimitRow): plugins.smartconsole.TColorName | undefined =>
|
|
197
|
+
rowArg.severity !== null ? SEVERITY_COLORS[rowArg.severity]
|
|
198
|
+
: rowArg.usedPercent === null ? 'orange' : rowArg.usedPercent >= 100 ? 'red' : undefined;
|
|
199
|
+
|
|
168
200
|
/** Condensed presentation for `authswitch limits` and `authswitch active`. */
|
|
169
201
|
export class CondensedRenderer {
|
|
170
202
|
constructor(private readonly out: plugins.smartconsole.SmartConsole) {}
|
|
@@ -183,9 +215,9 @@ export class CondensedRenderer {
|
|
|
183
215
|
{ key: 'account', title: 'Account', value: row => row.account ?? UNAVAILABLE, style: row => ({ bold: row.isActive }) },
|
|
184
216
|
{ key: 'limit', title: 'Limit type', value: row => row.limitType ?? UNAVAILABLE },
|
|
185
217
|
{ key: 'used', title: 'Used %', value: percent, align: 'right',
|
|
186
|
-
style: row => ({ foreground: row
|
|
218
|
+
style: row => ({ foreground: percentColor(row) }) },
|
|
187
219
|
{ key: 'reset', title: 'Resets in', value: row => row.resetsIn, align: 'right' },
|
|
188
|
-
]);
|
|
220
|
+
], { divider: accountRuns, colorLine: harnessRuns });
|
|
189
221
|
this.notes(limitsArg.notes, 'n/a — ');
|
|
190
222
|
}
|
|
191
223
|
|
|
@@ -203,7 +235,7 @@ export class CondensedRenderer {
|
|
|
203
235
|
style: row => ({ foreground: row.savedAt === null && row.account !== null ? 'orange' : undefined }) },
|
|
204
236
|
{ key: 'source', title: 'Source', value: row => row.drift === null ? row.source : `${row.source} (changed)`,
|
|
205
237
|
style: row => ({ foreground: row.drift !== null ? 'red' : row.source === 'credential file' ? 'green' : 'orange' }) },
|
|
206
|
-
]);
|
|
238
|
+
], { colorLine: harnessRuns });
|
|
207
239
|
this.notes(activeArg.notes, 'note — ');
|
|
208
240
|
}
|
|
209
241
|
}
|
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,
|
|
3
|
+
import { 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';
|
|
@@ -140,9 +140,10 @@ export class AuthSwitchTui {
|
|
|
140
140
|
const tabs = ui.tabs([{ title: 'Account details', content: details }, { title: 'Activity log', content: ui.logs }]);
|
|
141
141
|
await ui.run({
|
|
142
142
|
view: () => {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const shown = accountUsageWindows({ status: table.selected?.status }) ?? [];
|
|
144
|
+
const usage = shown.slice(0, 2).map(window => ui.progress(
|
|
145
|
+
`${usageWindowName(window, shown)} (${window.usedPercent}% used; reset in ${until(window.resetAt, Date.now())})`, Math.min(100, window.usedPercent), 100,
|
|
146
|
+
));
|
|
146
147
|
const accountView = ui.column([
|
|
147
148
|
ui.panel(`${plainText(harness.label)} accounts`, table),
|
|
148
149
|
ui.panel('Status and diagnostics', ui.column([...usage, tabs])),
|
package/ts/interfaces.harness.ts
CHANGED
|
@@ -108,20 +108,32 @@ export interface IHarnessStatusFact {
|
|
|
108
108
|
summaryKey?: 'subscription' | 'billing' | 'usageWindows' | 'resets' | 'resetDetails';
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** The provider's own reading of a usage window. Adapters set it only when the provider reports one of these. */
|
|
112
|
+
export type TUsageSeverity = 'normal' | 'warning' | 'critical';
|
|
113
|
+
|
|
114
|
+
export interface IHarnessUsageWindow {
|
|
115
|
+
label: string;
|
|
116
|
+
/**
|
|
117
|
+
* Feature-specific quotas (a model, a surface, a product feature) do not determine the general
|
|
118
|
+
* account summary unless the provider picks one as its headline. Omission means account.
|
|
119
|
+
*/
|
|
120
|
+
scope?: 'account' | 'feature';
|
|
121
|
+
durationSeconds: number;
|
|
122
|
+
usedPercent: number;
|
|
123
|
+
resetAt: string | null;
|
|
124
|
+
/** Omitted when the provider gives no reading; consumers then apply their own thresholds. */
|
|
125
|
+
severity?: TUsageSeverity;
|
|
126
|
+
/** Set when the provider picks this window as the one a single-value summary shows. */
|
|
127
|
+
headline?: true;
|
|
128
|
+
}
|
|
129
|
+
|
|
111
130
|
export interface IHarnessStatusSummary {
|
|
112
131
|
/** A plan name does not imply an active billing status. */
|
|
113
132
|
subscription?: { plan: string; source: 'live' | 'stored' };
|
|
114
133
|
/** Live billing information only. Never infer these fields from a plan or entitlement expiry. */
|
|
115
134
|
billing?: { hasActiveSubscription?: boolean; autoRenew?: boolean; renewsAt?: string; cancelsAt?: string; expiresAt?: string };
|
|
116
135
|
/** Missing windows are unknown, not unused. Times are ISO 8601 UTC. */
|
|
117
|
-
usageWindows?:
|
|
118
|
-
label: string;
|
|
119
|
-
/** Feature-specific quotas do not determine the general account summary. Omission means account. */
|
|
120
|
-
scope?: 'account' | 'feature';
|
|
121
|
-
durationSeconds: number;
|
|
122
|
-
usedPercent: number;
|
|
123
|
-
resetAt: string | null;
|
|
124
|
-
}[];
|
|
136
|
+
usageWindows?: IHarnessUsageWindow[];
|
|
125
137
|
resets?: {
|
|
126
138
|
available: number;
|
|
127
139
|
/** May be a partial list. Omission means the detail lookup was unavailable. */
|
package/ts/interfaces.list.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IHarnessAccount, IHarnessAccountStatus, IHarnessCredentialDrift } from './interfaces.harness.js';
|
|
1
|
+
import type { 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 {
|
|
@@ -48,6 +48,10 @@ export interface IAccountLimitRow {
|
|
|
48
48
|
resetAt: string | null;
|
|
49
49
|
/** Human countdown for the same `resetAt`, relative to `generatedAt`. */
|
|
50
50
|
resetsIn: string;
|
|
51
|
+
/** The provider's reading of this window; null when it gave none or the row has no window. */
|
|
52
|
+
severity: TUsageSeverity | null;
|
|
53
|
+
/** Whether the provider picks this window as the one a single-value summary shows. */
|
|
54
|
+
headline: boolean;
|
|
51
55
|
unavailableReason: string | null;
|
|
52
56
|
}
|
|
53
57
|
|