@cortexkit/opencode-openai-auth 1.1.0 → 1.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/bin.js +8 -8
- package/dist/{codex-request-BXDXToyt.d.ts → codex-request-CjXETpTA.d.ts} +56 -24
- package/dist/index-03em8hv1.js +69 -0
- package/dist/index-6n4pnfkg.js +2 -0
- package/dist/{index-f2fmb596.js → index-bc89akak.js} +1 -1
- package/dist/{index-7c6sxtd6.js → index-ehjj89rx.js} +2 -2
- package/dist/index-f21addzm.js +90 -0
- package/dist/{index-k6cydkgt.js → index-ffg0sgg5.js} +2 -2
- package/dist/{index-zpm95mt5.js → index-ftkhhr6d.js} +1 -1
- package/dist/{index-4c8yw54k.js → index-htvcmtcx.js} +2 -2
- package/dist/{index-j8v2xwq6.js → index-qp5hdpw8.js} +1 -1
- package/dist/{index-msx2m78k.js → index-qxbchh2d.js} +2 -2
- package/dist/{index-m5qmvr6c.js → index-r2s51ac1.js} +2 -2
- package/dist/index-rwbrhkse.js +65 -0
- package/dist/{index-csky2xpg.js → index-xwvze96g.js} +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/{model-YRg7Uowo.d.ts → model-CQcw10oW.d.ts} +1 -1
- package/dist/{port-file-CxEpgSHY.d.ts → port-file-BDJnzgHH.d.ts} +1 -1
- package/dist/{protocol-CDV1Pc6h.d.ts → protocol-C-kagE6O.d.ts} +1 -1
- package/dist/rpc/port-file.d.ts +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +1 -1
- package/dist/rpc/protocol.js +1 -1
- package/dist/rpc/rpc-client.d.ts +3 -3
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/{sidebar-state-DjTWUbmf.d.ts → sidebar-state-BfQ4mpZO.d.ts} +98 -5
- package/dist/sidebar-state.d.ts +2 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-cf0nyh7n.js +1 -0
- package/dist/{token-rz4t3ay6.js → token-z6qq7a1d.js} +1 -1
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +2 -2
- package/dist/v2/server.d.ts +109 -2
- package/dist/v2/server.js +3 -3
- package/package.json +3 -3
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +135 -18
- package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +17 -0
- package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +38 -1
- package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +1 -1
- package/src/tui-compiled/raw/sidebar-state.js +63 -1
- package/src/tui-compiled/raw/tui.tsx +82 -30
- package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +135 -18
- package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +17 -0
- package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +38 -1
- package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +1 -1
- package/src/tui-compiled/runtime/sidebar-state.js +63 -1
- package/src/tui-compiled/runtime/tui.js +245 -208
- package/dist/index-0yh85bwb.js +0 -69
- package/dist/index-7eb3dw37.js +0 -90
- package/dist/index-8re8rprk.js +0 -2
- package/dist/index-edbdscy7.js +0 -65
- package/dist/map-Buq-GW7l.d.ts +0 -56
- package/dist/token-util-tpy2c7h6.js +0 -1
|
@@ -21,6 +21,11 @@ export class ClaustrumConsumer {
|
|
|
21
21
|
#connecting;
|
|
22
22
|
#refreshing;
|
|
23
23
|
#roster;
|
|
24
|
+
/**
|
|
25
|
+
* The grants named by this instance's latest discovery, with the view that
|
|
26
|
+
* discovery committed. Memory only: the roster file never carries it.
|
|
27
|
+
*/
|
|
28
|
+
#grantReach;
|
|
24
29
|
#timer;
|
|
25
30
|
#started = false;
|
|
26
31
|
#insula;
|
|
@@ -102,6 +107,21 @@ export class ClaustrumConsumer {
|
|
|
102
107
|
snapshot() {
|
|
103
108
|
return this.#roster;
|
|
104
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* What this consumer is granted, from its latest discovery, without any I/O.
|
|
112
|
+
* It explains an empty roster: `grants === 0` means no grant at all, while
|
|
113
|
+
* `grants > 0` means a grant that reaches no credential (`tuples` names its
|
|
114
|
+
* selector). Undefined means unknown, never zero: the vault or client is too
|
|
115
|
+
* old to send it, nothing was discovered yet, or the roster now in memory is
|
|
116
|
+
* a different view than the one this instance discovered (a peer process
|
|
117
|
+
* held the discovery lease and its newer roster was served instead).
|
|
118
|
+
*/
|
|
119
|
+
grantReach() {
|
|
120
|
+
const latest = this.#grantReach;
|
|
121
|
+
return latest && this.#roster?.view === latest.view
|
|
122
|
+
? latest.reach
|
|
123
|
+
: undefined;
|
|
124
|
+
}
|
|
105
125
|
/** Rows for `/routing`, from the last committed roster. */
|
|
106
126
|
routingRows() {
|
|
107
127
|
return vaultRoutingRows(this.#roster);
|
|
@@ -113,6 +133,7 @@ export class ClaustrumConsumer {
|
|
|
113
133
|
this.#refreshing = (async () => {
|
|
114
134
|
if (!(await this.#active())) {
|
|
115
135
|
this.#roster = undefined;
|
|
136
|
+
this.#grantReach = undefined;
|
|
116
137
|
this.#custody?.close();
|
|
117
138
|
this.#custody = undefined;
|
|
118
139
|
this.#insula?.release();
|
|
@@ -120,9 +141,17 @@ export class ClaustrumConsumer {
|
|
|
120
141
|
return undefined;
|
|
121
142
|
}
|
|
122
143
|
const custody = await this.#getCustody();
|
|
144
|
+
// Capture the inventory so its grant fields can be kept beside the roster;
|
|
145
|
+
// the roster file itself never stores them.
|
|
146
|
+
let discovered;
|
|
123
147
|
const roster = await refreshVaultRoster({
|
|
124
148
|
path: this.#options.rosterPath,
|
|
125
|
-
custody
|
|
149
|
+
custody: {
|
|
150
|
+
discover: async (signal) => {
|
|
151
|
+
discovered = await custody.discover(signal);
|
|
152
|
+
return discovered;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
126
155
|
isActive: () => this.#active(),
|
|
127
156
|
signal: this.#shutdown.signal,
|
|
128
157
|
projection: () => ({
|
|
@@ -135,6 +164,13 @@ export class ClaustrumConsumer {
|
|
|
135
164
|
this.#assertOpen();
|
|
136
165
|
const changed = roster?.view !== this.#roster?.view;
|
|
137
166
|
this.#roster = roster;
|
|
167
|
+
// Each discovery replaces the previous value, including with unknown. A
|
|
168
|
+
// refresh that served a peer's roster discovered nothing and leaves it;
|
|
169
|
+
// grantReach() hides it once the roster's view no longer matches.
|
|
170
|
+
if (discovered && roster?.view !== undefined)
|
|
171
|
+
this.#grantReach = discovered.grantReach
|
|
172
|
+
? { view: roster.view, reach: discovered.grantReach }
|
|
173
|
+
: undefined;
|
|
138
174
|
// Notify on view changes only: subscribers rewrite shared state on each
|
|
139
175
|
// notification, and the poll runs every few seconds.
|
|
140
176
|
if (roster && changed)
|
|
@@ -389,5 +425,6 @@ export class ClaustrumConsumer {
|
|
|
389
425
|
this.#insula?.release();
|
|
390
426
|
this.#insula = undefined;
|
|
391
427
|
this.#roster = undefined;
|
|
428
|
+
this.#grantReach = undefined;
|
|
392
429
|
}
|
|
393
430
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { quotaCodec } from "./053cc17b5419261e-codec.js";
|
|
2
|
-
export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
|
|
2
|
+
export { formatQuota, isQuotaStale, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
|
|
3
3
|
export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, decodeQuotaMap, emptyQuotaMap, isQuotaMap, } from "./905bffc0220f43d0-map.js";
|
|
4
4
|
export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from "./4f36d333d081d040-merge.js";
|
|
5
5
|
export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from "./39c3cb05738fcf6d-projection.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
formatQuota,
|
|
3
|
+
isQuotaStale,
|
|
3
4
|
projectQuota,
|
|
4
5
|
quotaWindowName
|
|
5
6
|
} from "./shared/d5a8f1061ee2f114-index.js";
|
|
@@ -266,7 +267,7 @@ export function resolveSessionSidebarRouting(state, sessionId, now = Date.now())
|
|
|
266
267
|
}
|
|
267
268
|
function vaultAccountsInPlacementOrder(state) {
|
|
268
269
|
const accounts = state.vaultAccounts ?? [], byId = new Map(accounts.map((account) => [account.id, account]));
|
|
269
|
-
return orderForPlacement(accounts.map((account) => account.id),
|
|
270
|
+
return orderForPlacement(accounts.map((account) => account.id), placementOf(state), vaultMainRouteId(accounts) ?? "").flatMap((id) => {
|
|
270
271
|
const account = byId.get(id);
|
|
271
272
|
return account ? [account] : [];
|
|
272
273
|
});
|
|
@@ -962,6 +963,67 @@ export function getCollapsedQuotaSummary(quota, now = Date.now()) {
|
|
|
962
963
|
text: !projection || projection.limits.length === 0 && !projection.budget ? null : formatQuota(projection, { now, form: "compact" })
|
|
963
964
|
};
|
|
964
965
|
}
|
|
966
|
+
function placementOf(state) {
|
|
967
|
+
return state.route === "sticky-balanced" ? "roster" : orderedPlacement(state.route);
|
|
968
|
+
}
|
|
969
|
+
export function getCollapsedAccountLines(state, options = {}) {
|
|
970
|
+
const now = options.now ?? Date.now(), activeId = resolveActiveAccount(state).id;
|
|
971
|
+
return (state.vaultAccounts !== void 0 ? vaultAccountsInPlacementOrder(state).map((account) => ({
|
|
972
|
+
id: account.id,
|
|
973
|
+
name: account.label ?? account.id,
|
|
974
|
+
quota: account.quota,
|
|
975
|
+
killed: account.killed
|
|
976
|
+
})) : localAccountsInPlacementOrder(state, activeId, options.showFallbacks ?? !0)).map((account) => {
|
|
977
|
+
const projection = sidebarQuotaProjection(account.quota, now), reading = projection && (projection.limits.length > 0 || projection.budget) ? projection : void 0;
|
|
978
|
+
return {
|
|
979
|
+
id: account.id,
|
|
980
|
+
name: account.name,
|
|
981
|
+
killed: account.killed,
|
|
982
|
+
active: account.id === activeId,
|
|
983
|
+
stale: reading ? isQuotaStale(reading, { now }) : !1,
|
|
984
|
+
text: reading ? formatQuota(reading, { now, form: "narrow" }) : null
|
|
985
|
+
};
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
function localAccountsInPlacementOrder(state, activeId, showFallbacks) {
|
|
989
|
+
const accounts = [
|
|
990
|
+
{
|
|
991
|
+
id: "main",
|
|
992
|
+
name: "main",
|
|
993
|
+
quota: state.main.quota,
|
|
994
|
+
killed: state.main.killed
|
|
995
|
+
},
|
|
996
|
+
...state.fallbacks.filter((account) => account.enabled && (showFallbacks || account.id === activeId)).map((account) => ({
|
|
997
|
+
id: account.id,
|
|
998
|
+
name: account.label ?? account.id,
|
|
999
|
+
quota: account.quota,
|
|
1000
|
+
killed: account.killed
|
|
1001
|
+
}))
|
|
1002
|
+
], byId = new Map(accounts.map((account) => [account.id, account]));
|
|
1003
|
+
return orderForPlacement(accounts.map((account) => account.id), placementOf(state), "main").flatMap((id) => {
|
|
1004
|
+
const account = byId.get(id);
|
|
1005
|
+
return account ? [account] : [];
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
export function displayWidth(text) {
|
|
1009
|
+
const bun = globalThis.Bun;
|
|
1010
|
+
return bun?.stringWidth ? bun.stringWidth(text) : Array.from(text).length;
|
|
1011
|
+
}
|
|
1012
|
+
export function fitCollapsedLabel(label, columns) {
|
|
1013
|
+
if (displayWidth(label) <= columns)
|
|
1014
|
+
return label;
|
|
1015
|
+
if (columns < 1)
|
|
1016
|
+
return "";
|
|
1017
|
+
let kept = "", used = 0;
|
|
1018
|
+
for (const char of label) {
|
|
1019
|
+
const width = displayWidth(char);
|
|
1020
|
+
if (used + width > columns - 1)
|
|
1021
|
+
break;
|
|
1022
|
+
kept += char;
|
|
1023
|
+
used += width;
|
|
1024
|
+
}
|
|
1025
|
+
return `${kept}\u2026`;
|
|
1026
|
+
}
|
|
965
1027
|
const PACING_MIN_ELAPSED_MS = 300000, PACING_MIN_ELAPSED_FRACTION = 0.01, ON_PACE_DELTA = 1;
|
|
966
1028
|
export function computeQuotaPacing(window, windowMs, now) {
|
|
967
1029
|
if (!window.resetsAt)
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
createEffect,
|
|
19
19
|
createSignal,
|
|
20
20
|
For,
|
|
21
|
-
|
|
21
|
+
Index,
|
|
22
22
|
onCleanup,
|
|
23
23
|
Show,
|
|
24
24
|
} from "solid-js"
|
|
@@ -28,8 +28,12 @@ import { createRpcClient } from "./rpc/rpc-client.js"
|
|
|
28
28
|
import { resolveRpcDir } from "./rpc/rpc-dir.js"
|
|
29
29
|
import {
|
|
30
30
|
type AccountQuota,
|
|
31
|
+
type CollapsedAccountLine,
|
|
31
32
|
computeQuotaPacing,
|
|
32
33
|
DEFAULT_SIDEBAR_STATE,
|
|
34
|
+
displayWidth,
|
|
35
|
+
fitCollapsedLabel,
|
|
36
|
+
getCollapsedAccountLines,
|
|
33
37
|
getCollapsedQuotaSummary,
|
|
34
38
|
getPresentQuotaWindows,
|
|
35
39
|
getSidebarState,
|
|
@@ -250,17 +254,72 @@ function StatRow(props: {
|
|
|
250
254
|
)
|
|
251
255
|
}
|
|
252
256
|
|
|
253
|
-
//
|
|
254
|
-
// right.
|
|
255
|
-
|
|
257
|
+
// One account in the collapsed view: label left; narrow quota text and the
|
|
258
|
+
// status dot right (red ⊘ when killed). The session's account keeps the
|
|
259
|
+
// usage tone; other accounts, and any account whose reading is stale, are
|
|
260
|
+
// drawn muted. The row measures its own width so only the label is cut (with
|
|
261
|
+
// …) when the line does not fit; the quota text is never cut.
|
|
262
|
+
function CollapsedAccountRow(props: {
|
|
256
263
|
theme: ThemeCurrent
|
|
257
|
-
|
|
258
|
-
|
|
264
|
+
line: CollapsedAccountLine
|
|
265
|
+
tone: Tone
|
|
259
266
|
}) {
|
|
267
|
+
const [width, setWidth] = createSignal(0)
|
|
268
|
+
let row: { width: number } | undefined
|
|
269
|
+
const dim = () => props.line.stale || !props.line.active
|
|
270
|
+
const label = () => {
|
|
271
|
+
const columns = width()
|
|
272
|
+
if (columns <= 0) return props.line.name
|
|
273
|
+
// The value side is the text plus ` ●`, or a lone `—`; one column
|
|
274
|
+
// separates it from the label.
|
|
275
|
+
const value =
|
|
276
|
+
props.line.text === null ? 1 : displayWidth(props.line.text) + 2
|
|
277
|
+
return fitCollapsedLabel(props.line.name, columns - value - 1)
|
|
278
|
+
}
|
|
260
279
|
return (
|
|
261
|
-
<box
|
|
262
|
-
|
|
263
|
-
|
|
280
|
+
<box
|
|
281
|
+
ref={(element: { width: number }) => {
|
|
282
|
+
row = element
|
|
283
|
+
}}
|
|
284
|
+
onSizeChange={() => setWidth(row?.width ?? 0)}
|
|
285
|
+
width='100%'
|
|
286
|
+
flexDirection='row'
|
|
287
|
+
justifyContent='space-between'
|
|
288
|
+
>
|
|
289
|
+
<text
|
|
290
|
+
fg={toneColor(props.theme, dim() ? 'muted' : 'text')}
|
|
291
|
+
wrapMode='none'
|
|
292
|
+
flexShrink={1}
|
|
293
|
+
>
|
|
294
|
+
{label()}
|
|
295
|
+
</text>
|
|
296
|
+
<Show
|
|
297
|
+
when={props.line.text !== null}
|
|
298
|
+
fallback={<text fg={props.theme.textMuted}>{'\u2014'}</text>}
|
|
299
|
+
>
|
|
300
|
+
<box flexDirection='row' flexShrink={0}>
|
|
301
|
+
<Show
|
|
302
|
+
when={!dim()}
|
|
303
|
+
fallback={
|
|
304
|
+
<text fg={toneColor(props.theme, 'muted')}>
|
|
305
|
+
{props.line.text}
|
|
306
|
+
</text>
|
|
307
|
+
}
|
|
308
|
+
>
|
|
309
|
+
<text fg={toneColor(props.theme, props.tone)}>
|
|
310
|
+
<b>{props.line.text}</b>
|
|
311
|
+
</text>
|
|
312
|
+
</Show>
|
|
313
|
+
<text
|
|
314
|
+
fg={toneColor(
|
|
315
|
+
props.theme,
|
|
316
|
+
props.line.killed ? 'err' : dim() ? 'muted' : props.tone,
|
|
317
|
+
)}
|
|
318
|
+
>
|
|
319
|
+
{props.line.killed ? ' \u2298' : ' \u25cf'}
|
|
320
|
+
</text>
|
|
321
|
+
</box>
|
|
322
|
+
</Show>
|
|
264
323
|
</box>
|
|
265
324
|
)
|
|
266
325
|
}
|
|
@@ -823,6 +882,10 @@ function QuotaSidebar(props: {
|
|
|
823
882
|
route: sessionRouting().route,
|
|
824
883
|
})
|
|
825
884
|
const activeAccount = () => resolveActiveAccount(sessionState())
|
|
885
|
+
const collapsedLines = () =>
|
|
886
|
+
getCollapsedAccountLines(sessionState(), {
|
|
887
|
+
showFallbacks: prefs().sections.fallbackAccounts,
|
|
888
|
+
})
|
|
826
889
|
const quotaLabelWidth = () => computeQuotaLabelWidth(renderedQuotas(state()))
|
|
827
890
|
const activeQuotaSummary = () =>
|
|
828
891
|
getCollapsedQuotaSummary(activeAccount().quota)
|
|
@@ -910,28 +973,17 @@ function QuotaSidebar(props: {
|
|
|
910
973
|
</Show>
|
|
911
974
|
</box>
|
|
912
975
|
|
|
913
|
-
{/* Collapsed:
|
|
976
|
+
{/* Collapsed: one line per account, narrow quota text + dot (red ⊘ when killed) */}
|
|
914
977
|
<Show when={collapsed() && hasData()}>
|
|
915
|
-
<
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
<text
|
|
925
|
-
fg={toneColor(
|
|
926
|
-
theme(),
|
|
927
|
-
activeAccount().killed ? 'err' : activeQuotaTone(),
|
|
928
|
-
)}
|
|
929
|
-
>
|
|
930
|
-
{activeAccount().killed ? ' \u2298' : ' \u25cf'}
|
|
931
|
-
</text>
|
|
932
|
-
</box>
|
|
933
|
-
</Show>
|
|
934
|
-
</CollapsedRow>
|
|
978
|
+
<Index each={collapsedLines()}>
|
|
979
|
+
{(line) => (
|
|
980
|
+
<CollapsedAccountRow
|
|
981
|
+
theme={theme()}
|
|
982
|
+
line={line()}
|
|
983
|
+
tone={activeQuotaTone()}
|
|
984
|
+
/>
|
|
985
|
+
)}
|
|
986
|
+
</Index>
|
|
935
987
|
</Show>
|
|
936
988
|
|
|
937
989
|
{/* Expanded: full sections. Also render when there's no data so the
|
|
@@ -63,8 +63,12 @@ export function quotaWindowName(limit) {
|
|
|
63
63
|
function clampPercent(remaining) {
|
|
64
64
|
return Math.min(100, Math.max(0, remaining));
|
|
65
65
|
}
|
|
66
|
+
/** A remaining percentage as the whole number every form shows. */
|
|
67
|
+
function wholePercent(remaining) {
|
|
68
|
+
return Math.round(clampPercent(remaining));
|
|
69
|
+
}
|
|
66
70
|
function percentLeft(remaining) {
|
|
67
|
-
return `${
|
|
71
|
+
return `${wholePercent(remaining)}% left`;
|
|
68
72
|
}
|
|
69
73
|
function resetDetails(resetsAt, checkedAt, now) {
|
|
70
74
|
const at = resetsAt === undefined ? Number.NaN : Date.parse(resetsAt);
|
|
@@ -98,6 +102,16 @@ function displayOrder(left, right) {
|
|
|
98
102
|
* and the optional age keep the shared formatter's wording.
|
|
99
103
|
*/
|
|
100
104
|
export function quotaWindowParts(quota, options) {
|
|
105
|
+
const { windows, age } = scopedWindowParts(quota, options);
|
|
106
|
+
const parts = windows.map(({ part }) => part);
|
|
107
|
+
return age === undefined ? { windows: parts } : { windows: parts, age };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* `quotaWindowParts` with each window's scope kept beside it. The narrow form
|
|
111
|
+
* needs the scope to name and pick model-scoped windows; the public parts do
|
|
112
|
+
* not carry it, so their shape stays as it was.
|
|
113
|
+
*/
|
|
114
|
+
function scopedWindowParts(quota, options) {
|
|
101
115
|
if (!quota)
|
|
102
116
|
return { windows: [] };
|
|
103
117
|
const { now } = options;
|
|
@@ -107,12 +121,15 @@ export function quotaWindowParts(quota, options) {
|
|
|
107
121
|
const windows = readings.map((limit) => {
|
|
108
122
|
const remainingPercent = clampPercent(limit.remainingPercent ?? 0);
|
|
109
123
|
return {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
scope: limit.scope,
|
|
125
|
+
part: {
|
|
126
|
+
kind: 'window',
|
|
127
|
+
name: quotaWindowName(limit),
|
|
128
|
+
figure: percentLeft(limit.remainingPercent ?? 0),
|
|
129
|
+
remainingPercent,
|
|
130
|
+
checkedAt: limit.checkedAt,
|
|
131
|
+
...resetDetails(limit.resetsAt, limit.checkedAt, now),
|
|
132
|
+
},
|
|
116
133
|
};
|
|
117
134
|
});
|
|
118
135
|
const budget = quota.budget;
|
|
@@ -124,32 +141,131 @@ export function quotaWindowParts(quota, options) {
|
|
|
124
141
|
? `credits ${percentLeft(remainingPercent)}`
|
|
125
142
|
: 'credits available';
|
|
126
143
|
windows.push({
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
scope: ALL_SCOPE,
|
|
145
|
+
spent: budget.reached,
|
|
146
|
+
part: {
|
|
147
|
+
kind: 'budget',
|
|
148
|
+
name: 'credits',
|
|
149
|
+
figure,
|
|
150
|
+
...(remainingPercent === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { remainingPercent: clampPercent(remainingPercent) }),
|
|
153
|
+
checkedAt: budget.checkedAt,
|
|
154
|
+
...resetDetails(budget.resetsAt, budget.checkedAt, now),
|
|
155
|
+
},
|
|
135
156
|
});
|
|
136
157
|
}
|
|
137
158
|
if (windows.length > 0) {
|
|
138
|
-
const age = now - Math.min(...windows.map((part) => part.checkedAt));
|
|
159
|
+
const age = now - Math.min(...windows.map(({ part }) => part.checkedAt));
|
|
139
160
|
if (age > (options.staleAfterMs ?? QUOTA_STALE_AFTER_MS)) {
|
|
140
161
|
return { windows, age: `checked ${shortSpan(age)} ago` };
|
|
141
162
|
}
|
|
142
163
|
}
|
|
143
164
|
return { windows };
|
|
144
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Whether the reading is stale: true exactly when the compact and full forms
|
|
168
|
+
* would end with `checked … ago`. The narrow form leaves that age out to stay
|
|
169
|
+
* short, so a caller drawing a narrow line uses this to dim it instead.
|
|
170
|
+
*/
|
|
171
|
+
export function isQuotaStale(quota, options) {
|
|
172
|
+
return scopedWindowParts(quota, options).age !== undefined;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The model a scope names, as a display name: any namespace before a colon
|
|
176
|
+
* is dropped (`model:claude-opus-4` → `claude-opus-4`) and the first letter is
|
|
177
|
+
* capitalized (`fable` → `Fable`); the rest stays as written.
|
|
178
|
+
*/
|
|
179
|
+
function scopeDisplayName(scope) {
|
|
180
|
+
const model = scope.slice(scope.indexOf(':') + 1);
|
|
181
|
+
return model.charAt(0).toUpperCase() + model.slice(1);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Short names for the model scopes in one row: the first two letters of each
|
|
185
|
+
* display name. Names that would read the same for different models are each
|
|
186
|
+
* extended one letter at a time until they differ (`Fable` and `Fabric` read
|
|
187
|
+
* `Fabl` and `Fabr`); a name that runs out of letters stays whole.
|
|
188
|
+
*/
|
|
189
|
+
function scopeAbbreviations(scopes) {
|
|
190
|
+
const names = [...new Set(scopes.map(scopeDisplayName))];
|
|
191
|
+
const lengths = new Map(names.map((name) => [name, 2]));
|
|
192
|
+
const short = (name) => name.slice(0, lengths.get(name));
|
|
193
|
+
for (;;) {
|
|
194
|
+
const byShort = new Map();
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
byShort.set(short(name), [...(byShort.get(short(name)) ?? []), name]);
|
|
197
|
+
}
|
|
198
|
+
let extended = false;
|
|
199
|
+
for (const group of byShort.values()) {
|
|
200
|
+
if (group.length < 2)
|
|
201
|
+
continue;
|
|
202
|
+
for (const name of group) {
|
|
203
|
+
const length = lengths.get(name) ?? 2;
|
|
204
|
+
if (length < name.length) {
|
|
205
|
+
lengths.set(name, length + 1);
|
|
206
|
+
extended = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!extended)
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
return new Map(scopes.map((scope) => [scope, short(scopeDisplayName(scope))]));
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The narrow form's pieces: every account-wide window as `5h 92%`, then at
|
|
217
|
+
* most one more item, with ` left` added once to the last piece. That item is
|
|
218
|
+
* the tightest (least remaining) of the model-scoped windows and the credit
|
|
219
|
+
* budget, so the line stays bounded whatever the account carries: a scoped
|
|
220
|
+
* window reads under its short model name (`Fa 98%`), the budget as `cr 75%`
|
|
221
|
+
* (`cr 0%` when spent). On a tie a scoped window wins over the budget, and
|
|
222
|
+
* among scoped windows the first in display order wins. The choice depends
|
|
223
|
+
* only on the reading, never on a width. A budget without a percentage cannot
|
|
224
|
+
* be chosen; when nothing at all is shown it keeps its compact wording
|
|
225
|
+
* (`credits available`). No age is added; `isQuotaStale` says whether the
|
|
226
|
+
* reading is stale. A window whose reset arrived after the reading keeps its
|
|
227
|
+
* figure, as in the other forms.
|
|
228
|
+
*/
|
|
229
|
+
function narrowTextParts(quota, options) {
|
|
230
|
+
const { windows } = scopedWindowParts(quota, options);
|
|
231
|
+
const general = windows.filter(({ part, scope }) => part.kind === 'window' && scope === ALL_SCOPE);
|
|
232
|
+
const scoped = windows.filter(({ part, scope }) => part.kind === 'window' && scope !== ALL_SCOPE);
|
|
233
|
+
const abbreviations = scopeAbbreviations(scoped.map(({ scope }) => scope));
|
|
234
|
+
// The items competing for the one slot after the account-wide windows, in
|
|
235
|
+
// tie-break order: scoped windows in display order, then the budget. A
|
|
236
|
+
// spent budget counts as 0% left; one without a percentage cannot compete.
|
|
237
|
+
const budget = windows.find(({ part }) => part.kind === 'budget');
|
|
238
|
+
const budgetLeft = budget?.spent ? 0 : budget?.part.remainingPercent;
|
|
239
|
+
const extras = [
|
|
240
|
+
...scoped.map(({ scope, part }) => ({
|
|
241
|
+
label: abbreviations.get(scope) ?? scope,
|
|
242
|
+
remaining: part.remainingPercent ?? 0,
|
|
243
|
+
})),
|
|
244
|
+
...(budgetLeft === undefined
|
|
245
|
+
? []
|
|
246
|
+
: [{ label: 'cr', remaining: budgetLeft }]),
|
|
247
|
+
];
|
|
248
|
+
// Strictly lower wins, so on a tie the earlier item keeps the slot.
|
|
249
|
+
const tightest = extras.reduce((best, item) => best === undefined || item.remaining < best.remaining ? item : best, undefined);
|
|
250
|
+
const pieces = general.map(({ part }) => `${part.name} ${wholePercent(part.remainingPercent ?? 0)}%`);
|
|
251
|
+
if (tightest) {
|
|
252
|
+
pieces.push(`${tightest.label} ${wholePercent(tightest.remaining)}%`);
|
|
253
|
+
}
|
|
254
|
+
if (pieces.length === 0)
|
|
255
|
+
return windows.map(({ part }) => part.figure);
|
|
256
|
+
return pieces.map((piece, index) => index === pieces.length - 1 ? `${piece} left` : piece);
|
|
257
|
+
}
|
|
145
258
|
/**
|
|
146
259
|
* The pieces of a quota text, in order: one per reported window
|
|
147
260
|
* (`5h 58% left, resets 2h`), then the credit budget when there is one, then
|
|
148
261
|
* `checked 3h ago` when the oldest piece shown is stale. Empty when nothing
|
|
149
262
|
* was reported; `formatQuota` turns that into a sentence. A caller printing
|
|
150
|
-
* one line per window (the terminal auth menu) uses these directly.
|
|
263
|
+
* one line per window (the terminal auth menu) uses these directly. In the
|
|
264
|
+
* narrow form the pieces are the narrow line's (`5h 92%`, `7d 66% left`).
|
|
151
265
|
*/
|
|
152
266
|
export function quotaTextParts(quota, options) {
|
|
267
|
+
if (options.form === 'narrow')
|
|
268
|
+
return narrowTextParts(quota, options);
|
|
153
269
|
const parts = quotaWindowParts(quota, options);
|
|
154
270
|
const full = (options.form ?? 'full') === 'full';
|
|
155
271
|
const text = parts.windows.map((part) => {
|
|
@@ -160,7 +276,8 @@ export function quotaTextParts(quota, options) {
|
|
|
160
276
|
}
|
|
161
277
|
/**
|
|
162
278
|
* A row's quota as one line: `5h 58% left · 7d 90% left` in the compact
|
|
163
|
-
* form, with `, resets 2h` after each window in the full form
|
|
279
|
+
* form, with `, resets 2h` after each window in the full form, and
|
|
280
|
+
* `5h 58% · 7d 90% left` in the narrow form. With nothing
|
|
164
281
|
* reported it is `NO_QUOTA_READING`, or `NO_LIMITS_REPORTED` when the
|
|
165
282
|
* provider answered without a limit for any window.
|
|
166
283
|
*/
|
|
@@ -83,6 +83,21 @@ function credentialTypeOf(row, family) {
|
|
|
83
83
|
return row.refreshAdapter === undefined ? 'api_key' : undefined;
|
|
84
84
|
return undefined;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The grants named in the vault's list reply, or undefined when it names none.
|
|
88
|
+
* Both fields must be present: a reply carrying only one of them is treated as
|
|
89
|
+
* unknown rather than completed with a guess. claustrum-client already refuses
|
|
90
|
+
* a malformed value, so nothing here validates it again.
|
|
91
|
+
*/
|
|
92
|
+
function grantReachOf(listed) {
|
|
93
|
+
const { grants, grantTuples } = listed;
|
|
94
|
+
if (grants === undefined || grantTuples === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
grants,
|
|
98
|
+
tuples: Object.freeze(grantTuples.map(({ selectorKind, selector, operation }) => Object.freeze({ selectorKind, selector, operation }))),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
86
101
|
/**
|
|
87
102
|
* Reads this consumer's vault credentials (list, fetch, 401 report), with the
|
|
88
103
|
* enrollment token as authorization. Used by every host of a plugin. There is
|
|
@@ -217,10 +232,12 @@ export class ClaustrumScopedCustody {
|
|
|
217
232
|
}
|
|
218
233
|
for (const record of skipped)
|
|
219
234
|
this.#logger.warn('skipped malformed vault record', record);
|
|
235
|
+
const grantReach = grantReachOf(inventory);
|
|
220
236
|
return {
|
|
221
237
|
view: inventory.view,
|
|
222
238
|
credentials: Object.freeze(credentials),
|
|
223
239
|
skipped: Object.freeze(skipped),
|
|
240
|
+
...(grantReach && { grantReach }),
|
|
224
241
|
};
|
|
225
242
|
}
|
|
226
243
|
/**
|
|
@@ -21,6 +21,11 @@ export class ClaustrumConsumer {
|
|
|
21
21
|
#connecting;
|
|
22
22
|
#refreshing;
|
|
23
23
|
#roster;
|
|
24
|
+
/**
|
|
25
|
+
* The grants named by this instance's latest discovery, with the view that
|
|
26
|
+
* discovery committed. Memory only: the roster file never carries it.
|
|
27
|
+
*/
|
|
28
|
+
#grantReach;
|
|
24
29
|
#timer;
|
|
25
30
|
#started = false;
|
|
26
31
|
#insula;
|
|
@@ -102,6 +107,21 @@ export class ClaustrumConsumer {
|
|
|
102
107
|
snapshot() {
|
|
103
108
|
return this.#roster;
|
|
104
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* What this consumer is granted, from its latest discovery, without any I/O.
|
|
112
|
+
* It explains an empty roster: `grants === 0` means no grant at all, while
|
|
113
|
+
* `grants > 0` means a grant that reaches no credential (`tuples` names its
|
|
114
|
+
* selector). Undefined means unknown, never zero: the vault or client is too
|
|
115
|
+
* old to send it, nothing was discovered yet, or the roster now in memory is
|
|
116
|
+
* a different view than the one this instance discovered (a peer process
|
|
117
|
+
* held the discovery lease and its newer roster was served instead).
|
|
118
|
+
*/
|
|
119
|
+
grantReach() {
|
|
120
|
+
const latest = this.#grantReach;
|
|
121
|
+
return latest && this.#roster?.view === latest.view
|
|
122
|
+
? latest.reach
|
|
123
|
+
: undefined;
|
|
124
|
+
}
|
|
105
125
|
/** Rows for `/routing`, from the last committed roster. */
|
|
106
126
|
routingRows() {
|
|
107
127
|
return vaultRoutingRows(this.#roster);
|
|
@@ -113,6 +133,7 @@ export class ClaustrumConsumer {
|
|
|
113
133
|
this.#refreshing = (async () => {
|
|
114
134
|
if (!(await this.#active())) {
|
|
115
135
|
this.#roster = undefined;
|
|
136
|
+
this.#grantReach = undefined;
|
|
116
137
|
this.#custody?.close();
|
|
117
138
|
this.#custody = undefined;
|
|
118
139
|
this.#insula?.release();
|
|
@@ -120,9 +141,17 @@ export class ClaustrumConsumer {
|
|
|
120
141
|
return undefined;
|
|
121
142
|
}
|
|
122
143
|
const custody = await this.#getCustody();
|
|
144
|
+
// Capture the inventory so its grant fields can be kept beside the roster;
|
|
145
|
+
// the roster file itself never stores them.
|
|
146
|
+
let discovered;
|
|
123
147
|
const roster = await refreshVaultRoster({
|
|
124
148
|
path: this.#options.rosterPath,
|
|
125
|
-
custody
|
|
149
|
+
custody: {
|
|
150
|
+
discover: async (signal) => {
|
|
151
|
+
discovered = await custody.discover(signal);
|
|
152
|
+
return discovered;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
126
155
|
isActive: () => this.#active(),
|
|
127
156
|
signal: this.#shutdown.signal,
|
|
128
157
|
projection: () => ({
|
|
@@ -135,6 +164,13 @@ export class ClaustrumConsumer {
|
|
|
135
164
|
this.#assertOpen();
|
|
136
165
|
const changed = roster?.view !== this.#roster?.view;
|
|
137
166
|
this.#roster = roster;
|
|
167
|
+
// Each discovery replaces the previous value, including with unknown. A
|
|
168
|
+
// refresh that served a peer's roster discovered nothing and leaves it;
|
|
169
|
+
// grantReach() hides it once the roster's view no longer matches.
|
|
170
|
+
if (discovered && roster?.view !== undefined)
|
|
171
|
+
this.#grantReach = discovered.grantReach
|
|
172
|
+
? { view: roster.view, reach: discovered.grantReach }
|
|
173
|
+
: undefined;
|
|
138
174
|
// Notify on view changes only: subscribers rewrite shared state on each
|
|
139
175
|
// notification, and the poll runs every few seconds.
|
|
140
176
|
if (roster && changed)
|
|
@@ -389,5 +425,6 @@ export class ClaustrumConsumer {
|
|
|
389
425
|
this.#insula?.release();
|
|
390
426
|
this.#insula = undefined;
|
|
391
427
|
this.#roster = undefined;
|
|
428
|
+
this.#grantReach = undefined;
|
|
392
429
|
}
|
|
393
430
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { quotaCodec } from "./053cc17b5419261e-codec.js";
|
|
2
|
-
export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
|
|
2
|
+
export { formatQuota, isQuotaStale, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, quotaWindowParts, } from "./0332701351b3eeec-format.js";
|
|
3
3
|
export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, decodeQuotaMap, emptyQuotaMap, isQuotaMap, } from "./905bffc0220f43d0-map.js";
|
|
4
4
|
export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from "./4f36d333d081d040-merge.js";
|
|
5
5
|
export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from "./39c3cb05738fcf6d-projection.js";
|