@indigoai-us/hq-cli 5.345.91 → 5.345.93
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/CHANGELOG.md +17 -0
- package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +17 -4
- package/dist/command-catalog.generated.d.ts +252 -0
- package/dist/command-catalog.generated.js +306 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/capacity.d.ts +71 -0
- package/dist/commands/capacity.js +666 -0
- package/dist/commands/integrations-connect.js +64 -5
- package/dist/commands/lanes-accounts.d.ts +1 -0
- package/dist/commands/lanes-accounts.js +6 -0
- package/dist/lib/capacity/accounts.d.ts +36 -0
- package/dist/lib/capacity/accounts.js +90 -0
- package/dist/lib/capacity/collect.d.ts +33 -0
- package/dist/lib/capacity/collect.js +164 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +145 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +1211 -0
- package/dist/lib/capacity/collectors/claude-statusline.d.ts +72 -0
- package/dist/lib/capacity/collectors/claude-statusline.js +436 -0
- package/dist/lib/capacity/collectors/codex.d.ts +120 -0
- package/dist/lib/capacity/collectors/codex.js +1134 -0
- package/dist/lib/capacity/collectors/grok.d.ts +86 -0
- package/dist/lib/capacity/collectors/grok.js +588 -0
- package/dist/lib/capacity/lane-login.d.ts +29 -0
- package/dist/lib/capacity/lane-login.js +91 -0
- package/dist/lib/capacity/ledger.d.ts +399 -0
- package/dist/lib/capacity/ledger.js +1441 -0
- package/dist/lib/capacity/project.d.ts +12 -0
- package/dist/lib/capacity/project.js +264 -0
- package/dist/lib/capacity/read.d.ts +62 -0
- package/dist/lib/capacity/read.js +264 -0
- package/dist/lib/capacity/refusal.d.ts +30 -0
- package/dist/lib/capacity/refusal.js +183 -0
- package/dist/lib/capacity/scheduler.d.ts +76 -0
- package/dist/lib/capacity/scheduler.js +800 -0
- package/dist/lib/capacity/sign-in.d.ts +103 -0
- package/dist/lib/capacity/sign-in.js +517 -0
- package/dist/lib/capacity/widget.d.ts +22 -0
- package/dist/lib/capacity/widget.js +418 -0
- package/dist/lib/lanes/spawn.js +12 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/run.js +29 -1
- package/dist/lib/search-index/background.d.ts +31 -8
- package/dist/lib/search-index/background.js +86 -7
- package/dist/lib/search-index/index.d.ts +24 -0
- package/dist/lib/search-index/index.js +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CapacityAccount } from "./accounts.js";
|
|
2
|
+
import type { NowView } from "./ledger.js";
|
|
3
|
+
export interface ProjectCapacityOptions {
|
|
4
|
+
accounts: readonly CapacityAccount[];
|
|
5
|
+
/**
|
|
6
|
+
* Ledger account id → hashed profile account id. Return null when the
|
|
7
|
+
* profile call did not run. Unknown identities never merge across homes.
|
|
8
|
+
*/
|
|
9
|
+
identityKeyForAccount?: (accountId: string) => string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function projectCapacityView(view: NowView, options: ProjectCapacityOptions): NowView;
|
|
12
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Present the capacity ledger against the accounts that exist right now.
|
|
3
|
+
*
|
|
4
|
+
* Readings keep the label they were collected under. A rename, or two labels
|
|
5
|
+
* for one home, must show up in `hq capacity` without waiting for the next
|
|
6
|
+
* provider poll and without rewriting the ledger. Claude blocks merge when
|
|
7
|
+
* they share a home, or when both have the same cached profile identity key.
|
|
8
|
+
* A missing key (the token was never read, or it was expired) never merges.
|
|
9
|
+
* Raw ids are never stored or returned.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
const STATE_RANK = {
|
|
14
|
+
ok: 0,
|
|
15
|
+
"reset-pending": 1,
|
|
16
|
+
warn: 2,
|
|
17
|
+
exhausted: 3,
|
|
18
|
+
unknown: 4,
|
|
19
|
+
};
|
|
20
|
+
export function projectCapacityView(view, options) {
|
|
21
|
+
const annotated = view.providers.some((provider) => (provider.accounts?.length ?? 0) > 0);
|
|
22
|
+
if (!annotated || options.accounts.length === 0)
|
|
23
|
+
return view;
|
|
24
|
+
const identityKeyForAccount = options.identityKeyForAccount ?? (() => null);
|
|
25
|
+
return {
|
|
26
|
+
...view,
|
|
27
|
+
providers: view.providers.map((provider) => projectProvider(provider, options.accounts, identityKeyForAccount)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function projectProvider(provider, accounts, identityKeyForAccount) {
|
|
31
|
+
if (!provider.accounts || provider.accounts.length === 0)
|
|
32
|
+
return provider;
|
|
33
|
+
const mine = accounts.filter((account) => account.provider === provider.provider);
|
|
34
|
+
if (mine.length === 0)
|
|
35
|
+
return provider;
|
|
36
|
+
const byHome = new Map();
|
|
37
|
+
const homeByLabel = new Map();
|
|
38
|
+
for (const account of mine) {
|
|
39
|
+
const home = canonicalHome(account.home);
|
|
40
|
+
const list = byHome.get(home) ?? [];
|
|
41
|
+
list.push(account);
|
|
42
|
+
byHome.set(home, list);
|
|
43
|
+
homeByLabel.set(account.label, home);
|
|
44
|
+
}
|
|
45
|
+
const pieces = [];
|
|
46
|
+
for (const account of provider.accounts) {
|
|
47
|
+
const homes = new Set();
|
|
48
|
+
const namedHomes = account.homes
|
|
49
|
+
.map((home) => canonicalHome(home))
|
|
50
|
+
.filter((home) => byHome.has(home));
|
|
51
|
+
if (namedHomes.length > 0) {
|
|
52
|
+
for (const home of namedHomes)
|
|
53
|
+
homes.add(home);
|
|
54
|
+
}
|
|
55
|
+
else if (account.homes.length === 0) {
|
|
56
|
+
for (const label of account.labels) {
|
|
57
|
+
const home = homeByLabel.get(label);
|
|
58
|
+
if (home)
|
|
59
|
+
homes.add(home);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (homes.size === 0)
|
|
63
|
+
continue;
|
|
64
|
+
pieces.push({ account, homes: [...homes] });
|
|
65
|
+
}
|
|
66
|
+
if (pieces.length === 0)
|
|
67
|
+
return withoutAccounts(provider);
|
|
68
|
+
const parent = pieces.map((_, index) => index);
|
|
69
|
+
const find = (index) => {
|
|
70
|
+
let cursor = index;
|
|
71
|
+
while (parent[cursor] !== cursor)
|
|
72
|
+
cursor = parent[cursor] ?? cursor;
|
|
73
|
+
return cursor;
|
|
74
|
+
};
|
|
75
|
+
const union = (left, right) => {
|
|
76
|
+
const a = find(left);
|
|
77
|
+
const b = find(right);
|
|
78
|
+
if (a !== b)
|
|
79
|
+
parent[b] = a;
|
|
80
|
+
};
|
|
81
|
+
const seenHome = new Map();
|
|
82
|
+
const seenIdentity = new Map();
|
|
83
|
+
pieces.forEach((piece, index) => {
|
|
84
|
+
for (const home of piece.homes) {
|
|
85
|
+
const previous = seenHome.get(home);
|
|
86
|
+
if (previous === undefined)
|
|
87
|
+
seenHome.set(home, index);
|
|
88
|
+
else
|
|
89
|
+
union(previous, index);
|
|
90
|
+
}
|
|
91
|
+
const identity = identityKeyForAccount(piece.account.accountId);
|
|
92
|
+
if (!identity)
|
|
93
|
+
return;
|
|
94
|
+
const previousIdentity = seenIdentity.get(identity);
|
|
95
|
+
if (previousIdentity === undefined)
|
|
96
|
+
seenIdentity.set(identity, index);
|
|
97
|
+
else
|
|
98
|
+
union(previousIdentity, index);
|
|
99
|
+
});
|
|
100
|
+
const groups = new Map();
|
|
101
|
+
pieces.forEach((piece, index) => {
|
|
102
|
+
const root = find(index);
|
|
103
|
+
const list = groups.get(root) ?? [];
|
|
104
|
+
list.push(piece);
|
|
105
|
+
groups.set(root, list);
|
|
106
|
+
});
|
|
107
|
+
const projected = [];
|
|
108
|
+
for (const group of groups.values()) {
|
|
109
|
+
const homes = [];
|
|
110
|
+
for (const piece of group) {
|
|
111
|
+
for (const home of piece.homes) {
|
|
112
|
+
if (!homes.includes(home))
|
|
113
|
+
homes.push(home);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const labels = displayLabels(homes, byHome);
|
|
117
|
+
if (labels.length === 0)
|
|
118
|
+
continue;
|
|
119
|
+
projected.push(mergeGroup(group, homes, labels, provider.provider));
|
|
120
|
+
}
|
|
121
|
+
if (projected.length === 0)
|
|
122
|
+
return withoutAccounts(provider);
|
|
123
|
+
projected.sort((a, b) => (a.labels[0] ?? "").localeCompare(b.labels[0] ?? ""));
|
|
124
|
+
return recomputeFromAccounts(provider, projected);
|
|
125
|
+
}
|
|
126
|
+
function recomputeFromAccounts(provider, accounts) {
|
|
127
|
+
const limits = [];
|
|
128
|
+
for (const account of accounts) {
|
|
129
|
+
for (const limit of account.limits)
|
|
130
|
+
limits.push(limit);
|
|
131
|
+
}
|
|
132
|
+
let state = "ok";
|
|
133
|
+
if (limits.length === 0) {
|
|
134
|
+
state = accounts.some((account) => account.authState === "expired" || account.authState === "signed_out" || account.signedOut)
|
|
135
|
+
? "unknown"
|
|
136
|
+
: "ok";
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
for (const limit of limits) {
|
|
140
|
+
if (STATE_RANK[limit.state] > STATE_RANK[state])
|
|
141
|
+
state = limit.state;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
...provider,
|
|
146
|
+
accounts,
|
|
147
|
+
limits,
|
|
148
|
+
state,
|
|
149
|
+
lastError: state === "unknown" ? accounts.find((account) => account.reason)?.reason ?? provider.lastError : null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function withoutAccounts(provider) {
|
|
153
|
+
const next = { ...provider };
|
|
154
|
+
delete next.accounts;
|
|
155
|
+
return next;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* `default` stands for the provider's own home. A registry account that uses
|
|
159
|
+
* that same home (lanes calls it `a`) is not a second account.
|
|
160
|
+
*/
|
|
161
|
+
function displayLabels(homes, byHome) {
|
|
162
|
+
const labels = [];
|
|
163
|
+
let defaultHome = null;
|
|
164
|
+
for (const home of homes) {
|
|
165
|
+
for (const account of byHome.get(home) ?? []) {
|
|
166
|
+
if (account.label === "default")
|
|
167
|
+
defaultHome = home;
|
|
168
|
+
if (!labels.includes(account.label))
|
|
169
|
+
labels.push(account.label);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const hidden = new Set();
|
|
173
|
+
if (defaultHome) {
|
|
174
|
+
for (const account of byHome.get(defaultHome) ?? []) {
|
|
175
|
+
if (account.label !== "default")
|
|
176
|
+
hidden.add(account.label);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const shown = labels.filter((label) => !hidden.has(label));
|
|
180
|
+
const rest = shown.filter((label) => label !== "default").sort((a, b) => a.localeCompare(b));
|
|
181
|
+
return shown.includes("default") ? ["default", ...rest] : rest;
|
|
182
|
+
}
|
|
183
|
+
function mergeGroup(group, homes, labels, provider) {
|
|
184
|
+
const ok = group.filter((piece) => piece.account.authState === "ok" && piece.account.limits.length > 0);
|
|
185
|
+
const limits = mergeLimits((ok.length > 0 ? ok : group).map((piece) => piece.account));
|
|
186
|
+
const observedAt = latestObserved(group.map((piece) => piece.account));
|
|
187
|
+
const organization = latestOrganization(group.map((piece) => piece.account));
|
|
188
|
+
const accountId = (ok[0] ?? group[0])?.account.accountId ?? labels[0] ?? provider;
|
|
189
|
+
if (ok.length > 0) {
|
|
190
|
+
let state = "ok";
|
|
191
|
+
for (const limit of limits) {
|
|
192
|
+
if (STATE_RANK[limit.state] > STATE_RANK[state])
|
|
193
|
+
state = limit.state;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
accountId,
|
|
197
|
+
labels,
|
|
198
|
+
organization,
|
|
199
|
+
homes,
|
|
200
|
+
state,
|
|
201
|
+
signedOut: false,
|
|
202
|
+
authState: "ok",
|
|
203
|
+
observedAt,
|
|
204
|
+
reason: null,
|
|
205
|
+
fixCommand: null,
|
|
206
|
+
limits,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const problem = [...group].sort((a, b) => (b.account.observedAt ?? "").localeCompare(a.account.observedAt ?? ""))[0]?.account;
|
|
210
|
+
const expired = problem?.authState === "expired";
|
|
211
|
+
const signedOut = problem?.authState === "signed_out" || problem?.signedOut === true;
|
|
212
|
+
return {
|
|
213
|
+
accountId,
|
|
214
|
+
labels,
|
|
215
|
+
organization,
|
|
216
|
+
homes,
|
|
217
|
+
state: "unknown",
|
|
218
|
+
signedOut,
|
|
219
|
+
authState: expired ? "expired" : signedOut ? "signed_out" : problem?.authState ?? "unknown",
|
|
220
|
+
observedAt,
|
|
221
|
+
reason: problem?.reason ?? null,
|
|
222
|
+
fixCommand: problem?.fixCommand ?? null,
|
|
223
|
+
limits,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function mergeLimits(accounts) {
|
|
227
|
+
const byId = new Map();
|
|
228
|
+
for (const account of accounts) {
|
|
229
|
+
for (const limit of account.limits) {
|
|
230
|
+
const previous = byId.get(limit.limitId);
|
|
231
|
+
if (!previous || limit.observedAt >= previous.observedAt)
|
|
232
|
+
byId.set(limit.limitId, limit);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return [...byId.values()].sort((a, b) => a.limitId.localeCompare(b.limitId));
|
|
236
|
+
}
|
|
237
|
+
function latestObserved(accounts) {
|
|
238
|
+
let latest = "";
|
|
239
|
+
for (const account of accounts) {
|
|
240
|
+
if (account.observedAt && account.observedAt > latest)
|
|
241
|
+
latest = account.observedAt;
|
|
242
|
+
}
|
|
243
|
+
return latest || null;
|
|
244
|
+
}
|
|
245
|
+
function latestOrganization(accounts) {
|
|
246
|
+
const ordered = [...accounts].sort((a, b) => (a.observedAt ?? "").localeCompare(b.observedAt ?? ""));
|
|
247
|
+
let organization = null;
|
|
248
|
+
for (const account of ordered) {
|
|
249
|
+
if (account.organization)
|
|
250
|
+
organization = account.organization;
|
|
251
|
+
}
|
|
252
|
+
return organization;
|
|
253
|
+
}
|
|
254
|
+
/** Same directory, including macOS `/var` → `/private/var`. Missing paths stay resolved. */
|
|
255
|
+
function canonicalHome(home) {
|
|
256
|
+
const resolved = path.resolve(home);
|
|
257
|
+
try {
|
|
258
|
+
return fs.realpathSync(resolved);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return resolved;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { LedgerCorrupt, LedgerMissing, type LedgerOptions, type NowLimit, type NowProvider, type NowView } from "./ledger.js";
|
|
2
|
+
export { LedgerCorrupt, LedgerMissing };
|
|
3
|
+
/** Same window as `hq capacity`: generatedAt older than this is stale. */
|
|
4
|
+
export declare const ADVISORY_STALE_MS: number;
|
|
5
|
+
export interface AdvisoryNow extends NowView {
|
|
6
|
+
stale: boolean;
|
|
7
|
+
ageMs: number;
|
|
8
|
+
}
|
|
9
|
+
export interface AdvisoryProvider {
|
|
10
|
+
provider: string;
|
|
11
|
+
state: string;
|
|
12
|
+
weeklyUsedPercent: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface AdvisoryRecord {
|
|
15
|
+
at: string;
|
|
16
|
+
caller: string;
|
|
17
|
+
story: string | null;
|
|
18
|
+
chosenEngine: string;
|
|
19
|
+
naiveEngine: string | null;
|
|
20
|
+
agree: boolean;
|
|
21
|
+
providers: AdvisoryProvider[];
|
|
22
|
+
}
|
|
23
|
+
export interface DispatchAdvisoryInput {
|
|
24
|
+
chosenEngine: string;
|
|
25
|
+
caller: string;
|
|
26
|
+
story?: string | null;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
stderr?: {
|
|
29
|
+
write: (chunk: string) => boolean | void;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The weekly_all row the advisory quotes. Lowest usedPercent when several exist.
|
|
34
|
+
* Pass accountId to prefer that account, falling back to the lowest across accounts.
|
|
35
|
+
*/
|
|
36
|
+
export declare function weeklyLimit(provider: NowProvider, accountId?: string): NowLimit | null;
|
|
37
|
+
/** Caller's lane account when the home or label matches, else the lowest-used account. */
|
|
38
|
+
export declare function preferredAccountId(provider: NowProvider, env?: NodeJS.ProcessEnv): string | undefined;
|
|
39
|
+
export declare function weeklyUsedPercent(provider: NowProvider, env?: NodeJS.ProcessEnv): number | null;
|
|
40
|
+
export declare function formatSpan(seconds: number | null): string;
|
|
41
|
+
/**
|
|
42
|
+
* Read now.json. Throws LedgerMissing when the file is absent and
|
|
43
|
+
* LedgerCorrupt when it is not a view. `stale` and `ageMs` are about
|
|
44
|
+
* generatedAt, not about an individual limit.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readNow(options?: LedgerOptions): AdvisoryNow;
|
|
47
|
+
/**
|
|
48
|
+
* Provider with state ok and the lowest weekly_all usedPercent.
|
|
49
|
+
* Ties keep `chosenEngine` when it is one of the tied providers.
|
|
50
|
+
* Null when no ok provider has a weekly_all percent.
|
|
51
|
+
*/
|
|
52
|
+
export declare function naiveChoice(view: NowView, chosenEngine: string, env?: NodeJS.ProcessEnv): string | null;
|
|
53
|
+
/** One stderr line. The chosen engine is first and carries reset and age. */
|
|
54
|
+
export declare function formatAdvisory(view: NowView, chosenEngine: string, env?: NodeJS.ProcessEnv): string;
|
|
55
|
+
export declare function appendAdvisory(entry: AdvisoryRecord, options?: LedgerOptions): void;
|
|
56
|
+
/**
|
|
57
|
+
* Print one advisory line and append one JSONL record.
|
|
58
|
+
* Missing, disabled, stale, and corrupt ledgers print nothing and record nothing.
|
|
59
|
+
* Any other error becomes one stderr line and is not rethrown.
|
|
60
|
+
*/
|
|
61
|
+
export declare function emitDispatchAdvisory(input: DispatchAdvisoryInput): void;
|
|
62
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory reader for lanes and other dispatchers (US-010).
|
|
3
|
+
* Files only. Never blocks, reroutes, or mutates provider choice.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { LedgerCorrupt, LedgerMissing, capacityRoot, readCollectorMark, readNow as readLedgerNow, } from "./ledger.js";
|
|
9
|
+
export { LedgerCorrupt, LedgerMissing };
|
|
10
|
+
/** Same window as `hq capacity`: generatedAt older than this is stale. */
|
|
11
|
+
export const ADVISORY_STALE_MS = 5 * 60 * 1000;
|
|
12
|
+
function clock(env, options) {
|
|
13
|
+
const base = { ...options, env };
|
|
14
|
+
if (base.now)
|
|
15
|
+
return base;
|
|
16
|
+
const raw = env.HQ_CAPACITY_NOW?.trim();
|
|
17
|
+
if (!raw)
|
|
18
|
+
return base;
|
|
19
|
+
const parsed = Date.parse(raw);
|
|
20
|
+
if (Number.isNaN(parsed)) {
|
|
21
|
+
throw new Error(`HQ_CAPACITY_NOW must be a UTC timestamp, got '${raw}'`);
|
|
22
|
+
}
|
|
23
|
+
return { ...base, now: () => new Date(parsed) };
|
|
24
|
+
}
|
|
25
|
+
function ledgerOptions(env) {
|
|
26
|
+
const override = env.HQ_CAPACITY_ROOT?.trim();
|
|
27
|
+
if (override)
|
|
28
|
+
return clock(env, { root: override });
|
|
29
|
+
const home = env.HOME?.trim() || env.USERPROFILE?.trim() || os.homedir();
|
|
30
|
+
return clock(env, { home });
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Vitest inherits the operator's HOME. A test that did not point
|
|
34
|
+
* HQ_CAPACITY_ROOT at a fixture must not read or write ~/.hq/capacity.
|
|
35
|
+
*/
|
|
36
|
+
function suppressedByTestHarness(env) {
|
|
37
|
+
if (env.HQ_CAPACITY_ROOT?.trim())
|
|
38
|
+
return false;
|
|
39
|
+
// Spawn tests pass a partial env that omits VITEST. The process env is what
|
|
40
|
+
// vitest sets, and that env still inherits the operator's real HOME.
|
|
41
|
+
const marker = env.VITEST ?? process.env.VITEST;
|
|
42
|
+
return marker === "true" || marker === "1";
|
|
43
|
+
}
|
|
44
|
+
function freshness(generatedAt, nowMs) {
|
|
45
|
+
const at = Date.parse(generatedAt);
|
|
46
|
+
if (Number.isNaN(at))
|
|
47
|
+
return { stale: true, ageMs: 0 };
|
|
48
|
+
const ageMs = Math.max(0, nowMs - at);
|
|
49
|
+
return { stale: ageMs > ADVISORY_STALE_MS, ageMs };
|
|
50
|
+
}
|
|
51
|
+
const ACCOUNT_HOME_ENV = {
|
|
52
|
+
claude: "CLAUDE_CONFIG_DIR",
|
|
53
|
+
codex: "CODEX_HOME",
|
|
54
|
+
grok: "GROK_HOME",
|
|
55
|
+
};
|
|
56
|
+
function lowestWeekly(limits) {
|
|
57
|
+
let best = null;
|
|
58
|
+
for (const limit of limits) {
|
|
59
|
+
if (limit.kind !== "weekly_all" || typeof limit.usedPercent !== "number")
|
|
60
|
+
continue;
|
|
61
|
+
if (!best || (limit.usedPercent ?? Infinity) < (best.usedPercent ?? Infinity))
|
|
62
|
+
best = limit;
|
|
63
|
+
}
|
|
64
|
+
return best;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The weekly_all row the advisory quotes. Lowest usedPercent when several exist.
|
|
68
|
+
* Pass accountId to prefer that account, falling back to the lowest across accounts.
|
|
69
|
+
*/
|
|
70
|
+
export function weeklyLimit(provider, accountId) {
|
|
71
|
+
if (!accountId)
|
|
72
|
+
return lowestWeekly(provider.limits);
|
|
73
|
+
return lowestWeekly(provider.limits.filter((limit) => limit.accountId === accountId))
|
|
74
|
+
?? lowestWeekly(provider.limits);
|
|
75
|
+
}
|
|
76
|
+
/** Caller's lane account when the home or label matches, else the lowest-used account. */
|
|
77
|
+
export function preferredAccountId(provider, env) {
|
|
78
|
+
const accounts = provider.accounts;
|
|
79
|
+
if (!accounts || accounts.length === 0 || !env)
|
|
80
|
+
return undefined;
|
|
81
|
+
const key = ACCOUNT_HOME_ENV[provider.provider];
|
|
82
|
+
const raw = key ? env[key]?.trim() : "";
|
|
83
|
+
if (raw) {
|
|
84
|
+
const resolved = path.resolve(raw);
|
|
85
|
+
const byHome = accounts.find((account) => account.homes.some((home) => path.resolve(home) === resolved));
|
|
86
|
+
if (byHome)
|
|
87
|
+
return byHome.accountId;
|
|
88
|
+
const byLabel = accounts.find((account) => account.labels.includes(raw));
|
|
89
|
+
if (byLabel)
|
|
90
|
+
return byLabel.accountId;
|
|
91
|
+
}
|
|
92
|
+
let best = null;
|
|
93
|
+
for (const account of accounts) {
|
|
94
|
+
if (account.signedOut)
|
|
95
|
+
continue;
|
|
96
|
+
const limit = lowestWeekly(account.limits);
|
|
97
|
+
if (!limit || typeof limit.usedPercent !== "number")
|
|
98
|
+
continue;
|
|
99
|
+
if (!best || limit.usedPercent < best.used)
|
|
100
|
+
best = { accountId: account.accountId, used: limit.usedPercent };
|
|
101
|
+
}
|
|
102
|
+
return best?.accountId;
|
|
103
|
+
}
|
|
104
|
+
export function weeklyUsedPercent(provider, env) {
|
|
105
|
+
return weeklyLimit(provider, preferredAccountId(provider, env))?.usedPercent ?? null;
|
|
106
|
+
}
|
|
107
|
+
export function formatSpan(seconds) {
|
|
108
|
+
if (seconds === null || !Number.isFinite(seconds))
|
|
109
|
+
return "—";
|
|
110
|
+
const span = Math.max(0, Math.floor(seconds));
|
|
111
|
+
if (span < 60)
|
|
112
|
+
return `${span}s`;
|
|
113
|
+
const minutes = Math.floor(span / 60);
|
|
114
|
+
if (minutes < 60)
|
|
115
|
+
return `${minutes}m`;
|
|
116
|
+
const hours = Math.floor(minutes / 60);
|
|
117
|
+
const remainMinutes = minutes % 60;
|
|
118
|
+
if (hours < 48)
|
|
119
|
+
return remainMinutes === 0 ? `${hours}h` : `${hours}h ${remainMinutes}m`;
|
|
120
|
+
const days = Math.floor(hours / 24);
|
|
121
|
+
const remainHours = hours % 24;
|
|
122
|
+
return remainHours === 0 ? `${days}d` : `${days}d ${remainHours}h`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Read now.json. Throws LedgerMissing when the file is absent and
|
|
126
|
+
* LedgerCorrupt when it is not a view. `stale` and `ageMs` are about
|
|
127
|
+
* generatedAt, not about an individual limit.
|
|
128
|
+
*/
|
|
129
|
+
export function readNow(options) {
|
|
130
|
+
const env = options?.env ?? process.env;
|
|
131
|
+
const resolved = options?.root || options?.home || options?.now
|
|
132
|
+
? clock(env, options)
|
|
133
|
+
: ledgerOptions(env);
|
|
134
|
+
const view = readLedgerNow(resolved);
|
|
135
|
+
const nowMs = resolved.now ? resolved.now().getTime() : Date.now();
|
|
136
|
+
return { ...view, ...freshness(view.generatedAt, nowMs) };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Provider with state ok and the lowest weekly_all usedPercent.
|
|
140
|
+
* Ties keep `chosenEngine` when it is one of the tied providers.
|
|
141
|
+
* Null when no ok provider has a weekly_all percent.
|
|
142
|
+
*/
|
|
143
|
+
export function naiveChoice(view, chosenEngine, env) {
|
|
144
|
+
let best = null;
|
|
145
|
+
for (const provider of view.providers) {
|
|
146
|
+
if (provider.state !== "ok")
|
|
147
|
+
continue;
|
|
148
|
+
const used = weeklyUsedPercent(provider, env);
|
|
149
|
+
if (used === null)
|
|
150
|
+
continue;
|
|
151
|
+
if (!best
|
|
152
|
+
|| used < best.used
|
|
153
|
+
|| (used === best.used && provider.provider === chosenEngine)) {
|
|
154
|
+
best = { provider: provider.provider, used };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return best?.provider ?? null;
|
|
158
|
+
}
|
|
159
|
+
function observedAge(limit, generatedAt) {
|
|
160
|
+
if (!limit)
|
|
161
|
+
return "—";
|
|
162
|
+
const now = Date.parse(generatedAt);
|
|
163
|
+
const at = Date.parse(limit.observedAt);
|
|
164
|
+
if (Number.isNaN(now) || Number.isNaN(at))
|
|
165
|
+
return "—";
|
|
166
|
+
return `${formatSpan(Math.floor((now - at) / 1000))} ago`;
|
|
167
|
+
}
|
|
168
|
+
function percentText(limit) {
|
|
169
|
+
return limit && typeof limit.usedPercent === "number" ? `${limit.usedPercent}%` : "—";
|
|
170
|
+
}
|
|
171
|
+
/** One stderr line. The chosen engine is first and carries reset and age. */
|
|
172
|
+
export function formatAdvisory(view, chosenEngine, env) {
|
|
173
|
+
const ordered = [];
|
|
174
|
+
const chosen = view.providers.find((provider) => provider.provider === chosenEngine);
|
|
175
|
+
ordered.push(chosen ?? {
|
|
176
|
+
provider: chosenEngine,
|
|
177
|
+
state: "unknown",
|
|
178
|
+
lastError: null,
|
|
179
|
+
limits: [],
|
|
180
|
+
});
|
|
181
|
+
for (const provider of view.providers) {
|
|
182
|
+
if (provider.provider !== chosenEngine)
|
|
183
|
+
ordered.push(provider);
|
|
184
|
+
}
|
|
185
|
+
const parts = ordered.map((provider, index) => {
|
|
186
|
+
const limit = weeklyLimit(provider, preferredAccountId(provider, env));
|
|
187
|
+
const pct = percentText(limit);
|
|
188
|
+
if (index === 0) {
|
|
189
|
+
const resets = limit && typeof limit.secondsToReset === "number"
|
|
190
|
+
? formatSpan(limit.secondsToReset)
|
|
191
|
+
: "—";
|
|
192
|
+
return `${provider.provider} weekly ${pct} used, resets in ${resets} (observed ${observedAge(limit, view.generatedAt)})`;
|
|
193
|
+
}
|
|
194
|
+
return `${provider.provider} weekly ${pct}`;
|
|
195
|
+
});
|
|
196
|
+
return `capacity: ${parts.join("; ")}`;
|
|
197
|
+
}
|
|
198
|
+
export function appendAdvisory(entry, options) {
|
|
199
|
+
const env = options?.env ?? process.env;
|
|
200
|
+
const resolved = options?.root || options?.home
|
|
201
|
+
? { ...options, env }
|
|
202
|
+
: ledgerOptions(env);
|
|
203
|
+
const root = resolved.root ?? capacityRoot(env, resolved.home);
|
|
204
|
+
if (!fs.existsSync(root))
|
|
205
|
+
throw new LedgerMissing();
|
|
206
|
+
const day = /^\d{4}-\d{2}-\d{2}/.test(entry.at) ? entry.at.slice(0, 10) : new Date().toISOString().slice(0, 10);
|
|
207
|
+
const dest = path.join(root, `advisories-${day}.jsonl`);
|
|
208
|
+
fs.appendFileSync(dest, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
209
|
+
try {
|
|
210
|
+
fs.chmodSync(dest, 0o600);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// The line is already durable. Mode is best-effort on a race.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Print one advisory line and append one JSONL record.
|
|
218
|
+
* Missing, disabled, stale, and corrupt ledgers print nothing and record nothing.
|
|
219
|
+
* Any other error becomes one stderr line and is not rethrown.
|
|
220
|
+
*/
|
|
221
|
+
export function emitDispatchAdvisory(input) {
|
|
222
|
+
const env = input.env ?? process.env;
|
|
223
|
+
const stderr = input.stderr ?? process.stderr;
|
|
224
|
+
if (suppressedByTestHarness(env))
|
|
225
|
+
return;
|
|
226
|
+
try {
|
|
227
|
+
const options = ledgerOptions(env);
|
|
228
|
+
const mark = readCollectorMark(options);
|
|
229
|
+
if (mark?.enabled === false)
|
|
230
|
+
return;
|
|
231
|
+
let snap;
|
|
232
|
+
try {
|
|
233
|
+
snap = readNow(options);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (err instanceof LedgerMissing || err instanceof LedgerCorrupt)
|
|
237
|
+
return;
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
if (snap.stale)
|
|
241
|
+
return;
|
|
242
|
+
const naiveEngine = naiveChoice(snap, input.chosenEngine, env);
|
|
243
|
+
const record = {
|
|
244
|
+
at: (options.now ? options.now() : new Date()).toISOString(),
|
|
245
|
+
caller: input.caller,
|
|
246
|
+
story: input.story ?? null,
|
|
247
|
+
chosenEngine: input.chosenEngine,
|
|
248
|
+
naiveEngine,
|
|
249
|
+
agree: naiveEngine === input.chosenEngine,
|
|
250
|
+
providers: snap.providers.map((provider) => ({
|
|
251
|
+
provider: provider.provider,
|
|
252
|
+
state: provider.state,
|
|
253
|
+
weeklyUsedPercent: weeklyUsedPercent(provider, env),
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
appendAdvisory(record, options);
|
|
257
|
+
stderr.write(`${formatAdvisory(snap, input.chosenEngine, env)}\n`);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
261
|
+
stderr.write(`capacity advisory skipped: ${message}\n`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type LedgerOptions, type NowLimit } from "./ledger.js";
|
|
2
|
+
export declare const REFUSAL_PROVIDERS: readonly ["claude", "codex", "grok"];
|
|
3
|
+
export type RefusalProvider = (typeof REFUSAL_PROVIDERS)[number];
|
|
4
|
+
export declare const REFUSAL_PARSER_VERSION = "capacity-refusal.1";
|
|
5
|
+
export declare const REFUSAL_DEFAULT_LIMIT_ID = "weekly";
|
|
6
|
+
export declare const REFUSAL_HELP: string;
|
|
7
|
+
export declare class CapacityUsageError extends Error {
|
|
8
|
+
readonly exitCode: 2;
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export interface IngestRefusalInput {
|
|
12
|
+
provider?: string;
|
|
13
|
+
limitId?: string;
|
|
14
|
+
resetsAt?: string;
|
|
15
|
+
accountId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RefusalRecord {
|
|
18
|
+
provider: RefusalProvider;
|
|
19
|
+
accountId: string;
|
|
20
|
+
limitId: string;
|
|
21
|
+
source: "refusal";
|
|
22
|
+
confidence: "inferred";
|
|
23
|
+
observedAt: string;
|
|
24
|
+
state: NowLimit["state"];
|
|
25
|
+
resetsAt: string | null;
|
|
26
|
+
readingId: string;
|
|
27
|
+
usedPercent: number | null;
|
|
28
|
+
}
|
|
29
|
+
export declare function ingestRefusal(input: IngestRefusalInput, options?: LedgerOptions): RefusalRecord;
|
|
30
|
+
//# sourceMappingURL=refusal.d.ts.map
|