quotacap 0.0.34 → 0.0.35
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/README.md +1 -1
- package/dist/advisory/engine.d.ts +8 -3
- package/dist/advisory/engine.js +80 -48
- package/dist/advisory/snapshot.js +37 -21
- package/dist/advisory/types.d.ts +28 -2
- package/dist/advisory/types.js +3 -0
- package/dist/catalog/agy.d.ts +4 -0
- package/dist/catalog/agy.js +59 -0
- package/dist/catalog/claude.d.ts +3 -0
- package/dist/catalog/claude.js +70 -0
- package/dist/catalog/codex.d.ts +3 -0
- package/dist/catalog/codex.js +55 -0
- package/dist/catalog/grok.d.ts +3 -0
- package/dist/catalog/grok.js +36 -0
- package/dist/catalog/index.d.ts +17 -0
- package/dist/catalog/index.js +152 -0
- package/dist/catalog/kimi.d.ts +3 -0
- package/dist/catalog/kimi.js +51 -0
- package/dist/catalog/muse.d.ts +4 -0
- package/dist/catalog/muse.js +67 -0
- package/dist/catalog/types.d.ts +21 -0
- package/dist/catalog/types.js +3 -0
- package/dist/cli/advise.js +20 -0
- package/dist/cli/clients.d.ts +3 -0
- package/dist/cli/clients.js +2 -0
- package/dist/cli/models.d.ts +3 -0
- package/dist/cli/models.js +134 -0
- package/dist/cli/snapshot-source.js +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +2 -0
- package/dist/format/markdown.d.ts +1 -0
- package/dist/format/markdown.js +14 -0
- package/dist/format/rows.d.ts +1 -1
- package/dist/format/rows.js +24 -4
- package/dist/format/terminal.js +18 -1
- package/dist/http/server.d.ts +2 -0
- package/dist/http/server.js +100 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +31 -3
- package/dist/runtime/poll.d.ts +13 -0
- package/dist/runtime/poll.js +34 -2
- package/dist/runtime/service.js +9 -0
- package/dist/store/catalogs.d.ts +10 -0
- package/dist/store/catalogs.js +64 -0
- package/dist/store/db.js +27 -1
- package/dist/store/quotas.d.ts +22 -0
- package/dist/store/quotas.js +93 -8
- package/dist/version.js +1 -1
- package/dist/web/src/components/ClosedWeeks.d.ts +16 -0
- package/dist/web/src/components/ClosedWeeks.js +102 -0
- package/dist/web/src/components/PaceBar.d.ts +17 -5
- package/dist/web/src/components/PaceBar.js +51 -8
- package/dist/web/src/components/PacingLegend.js +1 -0
- package/dist/web/src/components/ProviderCard.js +3 -1
- package/dist/web/src/components/ProviderDrawer.js +7 -1
- package/dist/web/src/components/ProviderRow.js +3 -1
- package/dist/web/src/components/Recommendation.js +12 -3
- package/dist/web/src/components/ResetRail.js +2 -0
- package/dist/web/src/state.d.ts +20 -1
- package/dist/web/src/state.js +1 -0
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-Cm_QtC0G.js +12 -0
- package/web/dist/assets/index-DiOyUuQc.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CjNVp2Yx.css +0 -1
- package/web/dist/assets/index-irnos0UG.js +0 -12
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { trackedExecFile } from "../runtime/spawn.js";
|
|
2
|
+
// `grok models` prints a login preamble, a "Default model:" line, then an
|
|
3
|
+
// "Available models:" list of `* id (default)` / `- id` bullets (design §6.2).
|
|
4
|
+
export function parseGrokModels(stdout) {
|
|
5
|
+
const models = [];
|
|
6
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
7
|
+
const m = line.match(/^\s*[* -]\s+(\S+)/);
|
|
8
|
+
if (!m)
|
|
9
|
+
continue;
|
|
10
|
+
const id = m[1];
|
|
11
|
+
const entry = { id, displayName: id };
|
|
12
|
+
if (/\(default\)/i.test(line))
|
|
13
|
+
entry.default = true;
|
|
14
|
+
models.push(entry);
|
|
15
|
+
}
|
|
16
|
+
if (models.length === 0)
|
|
17
|
+
throw new Error("grok: no models found in `grok models` output");
|
|
18
|
+
return models;
|
|
19
|
+
}
|
|
20
|
+
const EXEC_TIMEOUT_MS = 8000;
|
|
21
|
+
export const grokCatalogFetcher = {
|
|
22
|
+
id: "grok",
|
|
23
|
+
async fetch() {
|
|
24
|
+
// Per-fetch catalog-owned signal: never the usage poll's shared controller.
|
|
25
|
+
const { stdout } = await trackedExecFile("grok", "grok", ["models"], {
|
|
26
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
27
|
+
signal: new AbortController().signal,
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
return { grok: parseGrokModels(stdout) };
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
throw new Error(`grok: failed to parse \`grok models\` output: ${e?.message ?? e}`);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CatalogFetcher, type CatalogView } from "./types.js";
|
|
2
|
+
export declare const CATALOG_CLIENT_TIMEOUT_MS = 30000;
|
|
3
|
+
export { MUSE_OVERALL_MS } from "./muse.js";
|
|
4
|
+
export declare const CATALOG_FAIL_COOLDOWN_MS = 60000;
|
|
5
|
+
export type { CatalogFetcher, CatalogView, ListedModel } from "./types.js";
|
|
6
|
+
export declare function catalogProviders(enabledProviders: string[]): string[];
|
|
7
|
+
export declare const catalogFetchers: Record<string, CatalogFetcher>;
|
|
8
|
+
export interface EnsureCatalogsOpts {
|
|
9
|
+
db: any;
|
|
10
|
+
wait: boolean;
|
|
11
|
+
refresh?: boolean;
|
|
12
|
+
providers: string[];
|
|
13
|
+
ttlHours: number;
|
|
14
|
+
now?: () => Date;
|
|
15
|
+
fetchers?: Record<string, CatalogFetcher>;
|
|
16
|
+
}
|
|
17
|
+
export declare function ensureCatalogs(opts: EnsureCatalogsOpts): Promise<Map<string, CatalogView>>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { getCatalog, upsertCatalog, markCatalogFailure } from "../store/catalogs.js";
|
|
2
|
+
import { classifyFailure } from "../diagnostics/failure.js";
|
|
3
|
+
import { agyCatalogFetcher } from "./agy.js";
|
|
4
|
+
import { claudeCatalogFetcher } from "./claude.js";
|
|
5
|
+
import { codexCatalogFetcher } from "./codex.js";
|
|
6
|
+
import { grokCatalogFetcher } from "./grok.js";
|
|
7
|
+
import { kimiCatalogFetcher } from "./kimi.js";
|
|
8
|
+
import { museCatalogFetcher } from "./muse.js";
|
|
9
|
+
export const CATALOG_CLIENT_TIMEOUT_MS = 30_000;
|
|
10
|
+
export { MUSE_OVERALL_MS } from "./muse.js";
|
|
11
|
+
export const CATALOG_FAIL_COOLDOWN_MS = 60_000;
|
|
12
|
+
// Returns enabled providers excluding manual, expanding agy to include synthetic agy:3p bucket.
|
|
13
|
+
export function catalogProviders(enabledProviders) {
|
|
14
|
+
const list = enabledProviders.filter((p) => p !== "manual");
|
|
15
|
+
if (enabledProviders.includes("agy") && !list.includes("agy:3p")) {
|
|
16
|
+
list.push("agy:3p");
|
|
17
|
+
}
|
|
18
|
+
return list;
|
|
19
|
+
}
|
|
20
|
+
// Fetcher map keyed by harness binary id. `agy:3p` resolves to the `agy`
|
|
21
|
+
// fetcher (one spawn writes both rows).
|
|
22
|
+
export const catalogFetchers = {
|
|
23
|
+
agy: agyCatalogFetcher,
|
|
24
|
+
claude: claudeCatalogFetcher,
|
|
25
|
+
codex: codexCatalogFetcher,
|
|
26
|
+
grok: grokCatalogFetcher,
|
|
27
|
+
kimi: kimiCatalogFetcher,
|
|
28
|
+
muse: museCatalogFetcher,
|
|
29
|
+
};
|
|
30
|
+
// In-process state: coalescing in-flight fetches and failure cooldown.
|
|
31
|
+
// Both reset on process restart (no SQLite column).
|
|
32
|
+
const inFlight = new Map();
|
|
33
|
+
const lastFailAt = new Map();
|
|
34
|
+
// Provider ids that share a fetcher. `agy:3p` → `agy`. Others map to
|
|
35
|
+
// themselves. Unknown ids get no fetcher.
|
|
36
|
+
function fetcherKeyFor(provider) {
|
|
37
|
+
if (provider === "agy:3p")
|
|
38
|
+
return "agy";
|
|
39
|
+
return provider;
|
|
40
|
+
}
|
|
41
|
+
// Both agy buckets that a single `agy` fetch produces.
|
|
42
|
+
const AGY_BUCKETS = ["agy", "agy:3p"];
|
|
43
|
+
function bucketsForFetcher(fetcherKey) {
|
|
44
|
+
if (fetcherKey === "agy")
|
|
45
|
+
return AGY_BUCKETS;
|
|
46
|
+
return [fetcherKey];
|
|
47
|
+
}
|
|
48
|
+
function needsFetch(view, ttlHours, refresh, fetcherKey, nowMs) {
|
|
49
|
+
// Missing / never fetched.
|
|
50
|
+
if (view.status === "unfetched")
|
|
51
|
+
return true;
|
|
52
|
+
// Stale or error: retry if forced, cooldown elapsed, or no cooldown armed
|
|
53
|
+
// (process restart). Cooldown is process-memory only. Check before TTL:
|
|
54
|
+
// an error row with null fetchedAt must still respect cooldown.
|
|
55
|
+
if (view.status === "stale" || view.status === "error") {
|
|
56
|
+
if (refresh)
|
|
57
|
+
return true;
|
|
58
|
+
const failTs = lastFailAt.get(fetcherKey);
|
|
59
|
+
if (failTs == null)
|
|
60
|
+
return true;
|
|
61
|
+
return nowMs - failTs >= CATALOG_FAIL_COOLDOWN_MS;
|
|
62
|
+
}
|
|
63
|
+
// TTL expired.
|
|
64
|
+
if (view.fetchedAt != null) {
|
|
65
|
+
const age = nowMs - Date.parse(view.fetchedAt);
|
|
66
|
+
if (age > ttlHours * 3600_000)
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
return true; // no fetchedAt is treated as expired
|
|
71
|
+
}
|
|
72
|
+
// Forced refresh.
|
|
73
|
+
if (refresh)
|
|
74
|
+
return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
export async function ensureCatalogs(opts) {
|
|
78
|
+
const { db, wait, refresh = false, providers, ttlHours, now = () => new Date() } = opts;
|
|
79
|
+
const fetchers = opts.fetchers ?? catalogFetchers;
|
|
80
|
+
const result = new Map();
|
|
81
|
+
if (!wait) {
|
|
82
|
+
for (const p of providers) {
|
|
83
|
+
result.set(p, getCatalog(db, p));
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
// Deduplicate providers to the fetcher key level.
|
|
88
|
+
const seenKeys = new Set();
|
|
89
|
+
const toFetch = new Map();
|
|
90
|
+
const providerToKey = new Map();
|
|
91
|
+
for (const p of providers) {
|
|
92
|
+
const key = fetcherKeyFor(p);
|
|
93
|
+
providerToKey.set(p, key);
|
|
94
|
+
if (p === "manual" || seenKeys.has(key))
|
|
95
|
+
continue;
|
|
96
|
+
seenKeys.add(key);
|
|
97
|
+
const fetcher = fetchers[key];
|
|
98
|
+
if (!fetcher)
|
|
99
|
+
continue;
|
|
100
|
+
// Read the primary bucket to decide whether a fetch is needed.
|
|
101
|
+
const view = getCatalog(db, key);
|
|
102
|
+
const nowMs = now().getTime();
|
|
103
|
+
if (needsFetch(view, ttlHours, refresh, key, nowMs)) {
|
|
104
|
+
toFetch.set(key, fetcher);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Launch fetches in parallel, coalescing with any already in-flight.
|
|
108
|
+
const pending = [];
|
|
109
|
+
for (const [key, fetcher] of toFetch) {
|
|
110
|
+
let p = inFlight.get(key);
|
|
111
|
+
if (!p) {
|
|
112
|
+
p = fetcher.fetch();
|
|
113
|
+
inFlight.set(key, p);
|
|
114
|
+
// Clean up in-flight on settle (success or failure).
|
|
115
|
+
p.then(() => inFlight.delete(key), () => inFlight.delete(key));
|
|
116
|
+
}
|
|
117
|
+
pending.push({ key, promise: p });
|
|
118
|
+
}
|
|
119
|
+
// Wait for all fetches.
|
|
120
|
+
const settled = await Promise.allSettled(pending.map((e) => e.promise));
|
|
121
|
+
for (let i = 0; i < pending.length; i++) {
|
|
122
|
+
const { key } = pending[i];
|
|
123
|
+
const outcome = settled[i];
|
|
124
|
+
const buckets = bucketsForFetcher(key);
|
|
125
|
+
const nowIso = now().toISOString();
|
|
126
|
+
if (outcome.status === "fulfilled") {
|
|
127
|
+
const lists = outcome.value;
|
|
128
|
+
for (const bucket of buckets) {
|
|
129
|
+
const listed = lists[bucket] ?? [];
|
|
130
|
+
upsertCatalog(db, bucket, listed, nowIso);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const err = outcome.reason;
|
|
135
|
+
lastFailAt.set(key, now().getTime());
|
|
136
|
+
const diag = classifyFailure(key, err);
|
|
137
|
+
for (const bucket of buckets) {
|
|
138
|
+
markCatalogFailure(db, bucket, {
|
|
139
|
+
diagnosticCode: diag.diagnosticCode,
|
|
140
|
+
summary: diag.summary,
|
|
141
|
+
action: diag.action,
|
|
142
|
+
errorDetail: diag.errorDetail,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Read final state for all requested providers.
|
|
148
|
+
for (const p of providers) {
|
|
149
|
+
result.set(p, getCatalog(db, p));
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { trackedExecFile } from "../runtime/spawn.js";
|
|
2
|
+
// `kimi provider list --json` (design §6.3): top-level `models` map keyed by
|
|
3
|
+
// "<provider>/<key>"; every entry is on the live picker. id is the entry's
|
|
4
|
+
// `model` when present, else the key's trailing segment. `default` only when a
|
|
5
|
+
// top-level defaultModel names the key (the 2026-09-16 spike JSON had none).
|
|
6
|
+
export function parseKimiProviderList(jsonText) {
|
|
7
|
+
const data = JSON.parse(jsonText);
|
|
8
|
+
const models = data?.models;
|
|
9
|
+
if (!models || typeof models !== "object" || Array.isArray(models)) {
|
|
10
|
+
throw new Error("kimi: provider list JSON has no models object");
|
|
11
|
+
}
|
|
12
|
+
const defaultModel = typeof data.defaultModel === "string" ? data.defaultModel : null;
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const [key, v] of Object.entries(models)) {
|
|
15
|
+
if (!v || typeof v !== "object")
|
|
16
|
+
continue;
|
|
17
|
+
const id = typeof v.model === "string" && v.model ? v.model : (key.split("/").pop() ?? key);
|
|
18
|
+
const entry = {
|
|
19
|
+
id,
|
|
20
|
+
displayName: typeof v.displayName === "string" && v.displayName ? v.displayName : id,
|
|
21
|
+
};
|
|
22
|
+
if (Array.isArray(v.supportEfforts)) {
|
|
23
|
+
const efforts = v.supportEfforts.filter((e) => typeof e === "string");
|
|
24
|
+
if (efforts.length > 0)
|
|
25
|
+
entry.efforts = efforts;
|
|
26
|
+
}
|
|
27
|
+
if (defaultModel !== null && key === defaultModel)
|
|
28
|
+
entry.default = true;
|
|
29
|
+
out.push(entry);
|
|
30
|
+
}
|
|
31
|
+
if (out.length === 0)
|
|
32
|
+
throw new Error("kimi: provider list JSON contained no models");
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
const EXEC_TIMEOUT_MS = 8000;
|
|
36
|
+
export const kimiCatalogFetcher = {
|
|
37
|
+
id: "kimi",
|
|
38
|
+
async fetch() {
|
|
39
|
+
// Per-fetch catalog-owned signal: never the usage poll's shared controller.
|
|
40
|
+
const { stdout } = await trackedExecFile("kimi", "kimi", ["provider", "list", "--json"], {
|
|
41
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
42
|
+
signal: new AbortController().signal,
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
return { kimi: parseKimiProviderList(stdout) };
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
throw new Error(`kimi: failed to parse \`kimi provider list --json\` output: ${e?.message ?? e}`);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { runPty, stripAnsi } from "../adapters/pty.js";
|
|
3
|
+
import { museProbeDir } from "../adapters/muse.js";
|
|
4
|
+
// Muse TUI `/model` picker (design §6.6). The statusline shows the current
|
|
5
|
+
// model (`muse-spark-1.3·max`) before the picker opens, so ids are collected
|
|
6
|
+
// only from the slice after the first "Choose model". The statusline id marks
|
|
7
|
+
// `default` when it also appears in the picker.
|
|
8
|
+
export function parseMuseModelPicker(transcript) {
|
|
9
|
+
const cleaned = stripAnsi(transcript);
|
|
10
|
+
const m = /Choose\s*model/i.exec(cleaned);
|
|
11
|
+
if (!m)
|
|
12
|
+
throw new Error("muse: no model picker (Choose model) in transcript");
|
|
13
|
+
const after = cleaned.slice(m.index + m[0].length);
|
|
14
|
+
const ids = [];
|
|
15
|
+
for (const im of after.matchAll(/muse-spark-[\w.-]+/g)) {
|
|
16
|
+
if (!ids.includes(im[0]))
|
|
17
|
+
ids.push(im[0]);
|
|
18
|
+
}
|
|
19
|
+
if (ids.length === 0)
|
|
20
|
+
throw new Error("muse: no muse-spark ids after the model picker");
|
|
21
|
+
const statusline = /muse-spark-[\w.-]+/.exec(cleaned.slice(0, m.index));
|
|
22
|
+
const statusId = statusline ? statusline[0] : null;
|
|
23
|
+
return ids.map((id) => {
|
|
24
|
+
const entry = { id, displayName: id };
|
|
25
|
+
if (statusId !== null && id === statusId)
|
|
26
|
+
entry.default = true;
|
|
27
|
+
return entry;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export const MUSE_OVERALL_MS = 28_000;
|
|
31
|
+
export const museCatalogFetcher = {
|
|
32
|
+
id: "muse",
|
|
33
|
+
async fetch() {
|
|
34
|
+
const cwd = museProbeDir();
|
|
35
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
36
|
+
// Catalog-owned overall abort (28s). Phase timers inside runPty are
|
|
37
|
+
// separate; this signal kills the child if everything combined exceeds
|
|
38
|
+
// the budget. Never the usage-poll shared controller.
|
|
39
|
+
const transcript = await runPty({
|
|
40
|
+
file: "muse",
|
|
41
|
+
args: ["--trust-workspace"],
|
|
42
|
+
cwd,
|
|
43
|
+
env: { MUSE_NO_AUTO_UPDATE: "1" },
|
|
44
|
+
cols: 140,
|
|
45
|
+
rows: 50,
|
|
46
|
+
readyRegex: /muse-spark|Muse Code \d/,
|
|
47
|
+
readyTimeoutMs: 8000,
|
|
48
|
+
settleDelayMs: 1000,
|
|
49
|
+
input: "/model",
|
|
50
|
+
submitInput: "\r",
|
|
51
|
+
submitAfterMs: 1500,
|
|
52
|
+
completionRegex: /Choose\s*model/i,
|
|
53
|
+
abortOn: /Do you trust this workspace|Working \(\d+s|Subscriptions aren't currently available|subscription_unavailable/,
|
|
54
|
+
timeoutMs: 14000,
|
|
55
|
+
respondToQueries: true,
|
|
56
|
+
maxBytes: 256 * 1024,
|
|
57
|
+
signal: AbortSignal.timeout(MUSE_OVERALL_MS),
|
|
58
|
+
label: "muse-catalog",
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
return { muse: parseMuseModelPicker(transcript) };
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
throw new Error(`muse: failed to parse /model picker: ${e?.message ?? e}`);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CatalogStatus = "ok" | "stale" | "error" | "unfetched";
|
|
2
|
+
export interface ListedModel {
|
|
3
|
+
id: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
default?: boolean;
|
|
6
|
+
efforts?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface CatalogView {
|
|
9
|
+
status: CatalogStatus;
|
|
10
|
+
fetchedAt: string | null;
|
|
11
|
+
listed: ListedModel[];
|
|
12
|
+
diagnosticCode?: string | null;
|
|
13
|
+
summary?: string | null;
|
|
14
|
+
action?: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface CatalogFetcher {
|
|
17
|
+
/** Harness binary / usage adapter id: claude, codex, kimi, grok, agy, muse. */
|
|
18
|
+
id: string;
|
|
19
|
+
fetch(): Promise<Record<string, ListedModel[]>>;
|
|
20
|
+
}
|
|
21
|
+
export declare function emptyCatalog(): CatalogView;
|
package/dist/cli/advise.js
CHANGED
|
@@ -7,6 +7,23 @@ import { isServiceManaged, runServiceCommand } from "../service/index.js";
|
|
|
7
7
|
import { detectChannel, printUpdateFooter, printVersionLine, readUpdateCache, refreshUpdateCache, updateFooter, } from "../runtime/updates.js";
|
|
8
8
|
import { ClientError, OFFLINE_LABEL, emptySnapshot, offlineOnlyClient, resolveSnapshot } from "./snapshot-source.js";
|
|
9
9
|
import { checkSkew, execSkewWarning, olderCliWarning, takeoverManaged, unmanagedSkewWarning, } from "./takeover.js";
|
|
10
|
+
function formatModelsLine(rec, now, ttlHours = 6) {
|
|
11
|
+
if (!rec.models || rec.models.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
const ids = rec.models.map((m) => m.id).join(", ");
|
|
14
|
+
let suffix = "";
|
|
15
|
+
if (rec.catalogStatus === "stale") {
|
|
16
|
+
suffix = " (stale)";
|
|
17
|
+
}
|
|
18
|
+
else if (rec.catalogFetchedAt) {
|
|
19
|
+
const ageMs = now.getTime() - new Date(rec.catalogFetchedAt).getTime();
|
|
20
|
+
const hours = Math.floor(ageMs / 3_600_000);
|
|
21
|
+
if (hours >= ttlHours) {
|
|
22
|
+
suffix = ` (listed ${hours}h ago)`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return `models: ${ids}${suffix}`;
|
|
26
|
+
}
|
|
10
27
|
export function registerAdviseCommand(program, deps) {
|
|
11
28
|
const createClient = deps.createClient ?? ((o) => createServiceClient(o));
|
|
12
29
|
const openDb = deps.openDb;
|
|
@@ -127,6 +144,9 @@ export function registerAdviseCommand(program, deps) {
|
|
|
127
144
|
console.log(JSON.stringify(rec, null, 2));
|
|
128
145
|
else {
|
|
129
146
|
console.log(`${rec.use}: ${rec.reason}`);
|
|
147
|
+
const modelsLine = formatModelsLine(rec, t, cfg.catalogTtlHours);
|
|
148
|
+
if (modelsLine)
|
|
149
|
+
console.log(modelsLine);
|
|
130
150
|
printVersionLine(o, VERSION, health?.version);
|
|
131
151
|
printUpdateFooter(o, footer);
|
|
132
152
|
}
|
package/dist/cli/clients.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { ServiceClient } from "../runtime/client.js";
|
|
3
|
+
import type { CatalogFetcher } from "../catalog/index.js";
|
|
3
4
|
import type { SleepFn } from "./takeover.js";
|
|
4
5
|
import type { UpdateCache } from "../runtime/updates.js";
|
|
5
6
|
export interface CreateClientOptions {
|
|
@@ -10,6 +11,7 @@ export interface CreateClientOptions {
|
|
|
10
11
|
export interface ClientCommandDeps {
|
|
11
12
|
createClient?: (opts: CreateClientOptions) => ServiceClient;
|
|
12
13
|
openDb?: (dbPath: string) => any;
|
|
14
|
+
migrate?: (db: any) => void;
|
|
13
15
|
resolveWidth?: () => {
|
|
14
16
|
columns: number | undefined;
|
|
15
17
|
tty: boolean;
|
|
@@ -24,5 +26,6 @@ export interface ClientCommandDeps {
|
|
|
24
26
|
readToken?: () => string | undefined;
|
|
25
27
|
};
|
|
26
28
|
checkUpdates?: () => Promise<UpdateCache | null>;
|
|
29
|
+
catalogFetchers?: Record<string, CatalogFetcher>;
|
|
27
30
|
}
|
|
28
31
|
export declare function registerClientCommands(program: Command, deps?: ClientCommandDeps): void;
|
package/dist/cli/clients.js
CHANGED
|
@@ -3,10 +3,12 @@ import { registerStatusCommand } from "./status.js";
|
|
|
3
3
|
import { registerAdviseCommand } from "./advise.js";
|
|
4
4
|
import { registerIngestCommand } from "./ingest.js";
|
|
5
5
|
import { registerProvidersCommand } from "./providers.js";
|
|
6
|
+
import { registerModelsCommand } from "./models.js";
|
|
6
7
|
export function registerClientCommands(program, deps) {
|
|
7
8
|
registerStatusCommand(program, deps ?? {});
|
|
8
9
|
registerAdviseCommand(program, deps ?? {});
|
|
9
10
|
registerProvidersCommand(program, deps ?? {});
|
|
11
|
+
registerModelsCommand(program, deps ?? {});
|
|
10
12
|
if (isExperimentalIngestEnabled()) {
|
|
11
13
|
registerIngestCommand(program, deps ?? {});
|
|
12
14
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ensureConfig, getDbPath } from "../config.js";
|
|
2
|
+
import { CATALOG_CLIENT_TIMEOUT_MS, ensureCatalogs, catalogProviders } from "../catalog/index.js";
|
|
3
|
+
import { openDb, migrate } from "../store/db.js";
|
|
4
|
+
import { getAllLatest } from "../store/quotas.js";
|
|
5
|
+
import { providerIdentity } from "../advisory/provider-names.js";
|
|
6
|
+
import { readToken } from "../runtime/token.js";
|
|
7
|
+
import { createServiceClient } from "../runtime/client.js";
|
|
8
|
+
import { OFFLINE_LABEL } from "./snapshot-source.js";
|
|
9
|
+
function projectModels(catalogs, quotaMap, providerNames) {
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const [id, catalog] of catalogs) {
|
|
12
|
+
const identity = providerIdentity(id, providerNames);
|
|
13
|
+
const quota = quotaMap.get(id);
|
|
14
|
+
const leftoverPct = quota?.usedPct != null ? Math.round((100 - quota.usedPct) * 10) / 10 : null;
|
|
15
|
+
out.push({
|
|
16
|
+
id,
|
|
17
|
+
displayName: identity.displayName,
|
|
18
|
+
harness: identity.harness,
|
|
19
|
+
vendor: identity.vendor,
|
|
20
|
+
leftoverPct,
|
|
21
|
+
resetsAt: quota?.resetsAt ?? null,
|
|
22
|
+
catalog,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function printTable(list) {
|
|
28
|
+
if (!list || list.length === 0) {
|
|
29
|
+
console.log("No model catalog data available.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const rows = list.map((entry) => {
|
|
33
|
+
const prov = entry.displayName ?? entry.id;
|
|
34
|
+
const leftover = entry.leftoverPct != null ? `${entry.leftoverPct}%` : "—";
|
|
35
|
+
const status = entry.catalog?.status ?? "unfetched";
|
|
36
|
+
const ids = (entry.catalog?.listed ?? []).map((m) => m.id).join(", ") || "(none)";
|
|
37
|
+
return { prov, leftover, status, ids };
|
|
38
|
+
});
|
|
39
|
+
const maxProv = Math.max(8, ...rows.map((r) => r.prov.length));
|
|
40
|
+
const maxLeft = Math.max(8, ...rows.map((r) => r.leftover.length));
|
|
41
|
+
const maxStat = Math.max(6, ...rows.map((r) => r.status.length));
|
|
42
|
+
const header = [
|
|
43
|
+
"PROVIDER".padEnd(maxProv),
|
|
44
|
+
"LEFTOVER".padEnd(maxLeft),
|
|
45
|
+
"STATUS".padEnd(maxStat),
|
|
46
|
+
"MODELS",
|
|
47
|
+
].join(" ");
|
|
48
|
+
const lines = rows.map((r) => [r.prov.padEnd(maxProv), r.leftover.padEnd(maxLeft), r.status.padEnd(maxStat), r.ids].join(" "));
|
|
49
|
+
console.log([header, ...lines].join("\n"));
|
|
50
|
+
}
|
|
51
|
+
export function registerModelsCommand(program, deps = {}) {
|
|
52
|
+
const createClient = deps.createClient ?? ((o) => createServiceClient(o));
|
|
53
|
+
const openDbFn = deps.openDb ?? openDb;
|
|
54
|
+
const migrateFn = deps.migrate ?? migrate;
|
|
55
|
+
const readTokenFn = deps.takeoverOpts?.readToken ?? readToken;
|
|
56
|
+
const now = deps.now ?? (() => new Date());
|
|
57
|
+
const exit = deps.exit ?? process.exit;
|
|
58
|
+
program
|
|
59
|
+
.command("models")
|
|
60
|
+
.description("list available models per quota bucket")
|
|
61
|
+
.option("--json", "machine-readable output")
|
|
62
|
+
.option("--provider <id>", "filter to a single provider")
|
|
63
|
+
.option("--refresh", "force refresh all catalogs")
|
|
64
|
+
.action(async (o) => {
|
|
65
|
+
const { config: cfg } = await ensureConfig();
|
|
66
|
+
const client = createClient({
|
|
67
|
+
port: cfg.port,
|
|
68
|
+
token: readTokenFn(),
|
|
69
|
+
timeoutMs: CATALOG_CLIENT_TIMEOUT_MS,
|
|
70
|
+
});
|
|
71
|
+
let body;
|
|
72
|
+
try {
|
|
73
|
+
const url = o.refresh
|
|
74
|
+
? (o.provider ? `/api/models/refresh?provider=${encodeURIComponent(o.provider)}` : "/api/models/refresh")
|
|
75
|
+
: (o.provider ? `/api/models?provider=${encodeURIComponent(o.provider)}` : "/api/models");
|
|
76
|
+
const method = o.refresh ? "post" : "get";
|
|
77
|
+
if (method === "post") {
|
|
78
|
+
body = await client.post(url, {});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
body = await client.get(url);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
// HTTP 4xx errors from a reachable service propagate as errors.
|
|
86
|
+
if (err?.status && err.status >= 400 && err.status < 500) {
|
|
87
|
+
console.error(err?.message ?? String(err));
|
|
88
|
+
exit(1);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Offline fallback when daemon is unreachable:
|
|
92
|
+
// quotacap models in offline mode may spawn fetchers (wait: true in-process).
|
|
93
|
+
console.error(OFFLINE_LABEL);
|
|
94
|
+
try {
|
|
95
|
+
const dbPath = getDbPath();
|
|
96
|
+
const db = openDbFn(dbPath);
|
|
97
|
+
migrateFn(db);
|
|
98
|
+
const providerParam = o.provider;
|
|
99
|
+
const allProviders = catalogProviders(cfg.enabledProviders);
|
|
100
|
+
const known = new Set(allProviders);
|
|
101
|
+
if (providerParam && !known.has(providerParam)) {
|
|
102
|
+
console.error(`unknown provider: ${providerParam}`);
|
|
103
|
+
exit(1);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const providers = providerParam ? [providerParam] : allProviders;
|
|
107
|
+
const catalogs = await ensureCatalogs({
|
|
108
|
+
db,
|
|
109
|
+
wait: true,
|
|
110
|
+
refresh: !!o.refresh,
|
|
111
|
+
providers,
|
|
112
|
+
ttlHours: cfg.catalogTtlHours,
|
|
113
|
+
now,
|
|
114
|
+
fetchers: deps.catalogFetchers,
|
|
115
|
+
});
|
|
116
|
+
const quotas = getAllLatest(db);
|
|
117
|
+
const quotaMap = new Map(quotas.map((q) => [q.provider, q]));
|
|
118
|
+
body = projectModels(catalogs, quotaMap, cfg.providerNames);
|
|
119
|
+
}
|
|
120
|
+
catch (offlineErr) {
|
|
121
|
+
console.error(offlineErr?.message ?? String(offlineErr));
|
|
122
|
+
exit(1);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const list = Array.isArray(body) ? body : (body?.models ?? body?.results ?? []);
|
|
127
|
+
if (o.json) {
|
|
128
|
+
console.log(JSON.stringify(body, null, 2));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
printTable(list);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const ProviderDisplayNameSchema: z.ZodEffects<z.ZodEffects<z.ZodS
|
|
|
5
5
|
declare const ConfigSchema: z.ZodObject<{
|
|
6
6
|
port: z.ZodDefault<z.ZodNumber>;
|
|
7
7
|
pollMinutes: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
catalogTtlHours: z.ZodDefault<z.ZodNumber>;
|
|
8
9
|
enabledProviders: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
9
10
|
knownProviders: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
10
11
|
providerNames: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>>;
|
|
@@ -12,6 +13,7 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
12
13
|
}, "strip", z.ZodTypeAny, {
|
|
13
14
|
port: number;
|
|
14
15
|
pollMinutes: number;
|
|
16
|
+
catalogTtlHours: number;
|
|
15
17
|
enabledProviders: string[];
|
|
16
18
|
knownProviders: string[];
|
|
17
19
|
providerNames: Record<string, string>;
|
|
@@ -19,6 +21,7 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
19
21
|
}, {
|
|
20
22
|
port?: number | undefined;
|
|
21
23
|
pollMinutes?: number | undefined;
|
|
24
|
+
catalogTtlHours?: number | undefined;
|
|
22
25
|
enabledProviders?: string[] | undefined;
|
|
23
26
|
knownProviders?: string[] | undefined;
|
|
24
27
|
providerNames?: Record<string, string> | undefined;
|
package/dist/config.js
CHANGED
|
@@ -33,6 +33,7 @@ export const ProviderDisplayNameSchema = z
|
|
|
33
33
|
const ConfigSchema = z.object({
|
|
34
34
|
port: z.number().default(8787),
|
|
35
35
|
pollMinutes: z.number().default(15),
|
|
36
|
+
catalogTtlHours: z.number().finite().positive().default(6),
|
|
36
37
|
enabledProviders: z.array(z.string()).default(["claude", "codex", "kimi", "grok", "agy", "muse"]),
|
|
37
38
|
knownProviders: z.array(z.string()).default(["claude", "codex", "kimi", "grok", "agy", "muse"]),
|
|
38
39
|
providerNames: z.record(z.string(), ProviderDisplayNameSchema).default({}),
|
|
@@ -85,6 +86,7 @@ const VALID_PROVIDER_IDS = Object.keys(adapters).sort();
|
|
|
85
86
|
const ServiceConfigSchema = z.object({
|
|
86
87
|
port: z.number().int().min(1).max(65535),
|
|
87
88
|
pollMinutes: z.number().finite().positive(),
|
|
89
|
+
catalogTtlHours: z.number().finite().positive().default(6),
|
|
88
90
|
enabledProviders: z
|
|
89
91
|
.array(z.string())
|
|
90
92
|
.superRefine((ids, ctx) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { StateSnapshot } from "../advisory/types.js";
|
|
2
2
|
export declare function renderMarkdownTable(snapshot: StateSnapshot, now: Date): string;
|
|
3
3
|
export declare function renderRecommendationSummary(snapshot: StateSnapshot): string;
|
|
4
|
+
export declare function renderModelsMarkdownTable(models: any[]): string;
|
package/dist/format/markdown.js
CHANGED
|
@@ -22,3 +22,17 @@ export function renderRecommendationSummary(snapshot) {
|
|
|
22
22
|
const rec = snapshot.recommendation;
|
|
23
23
|
return `${rec.use}: ${rec.reason}`;
|
|
24
24
|
}
|
|
25
|
+
export function renderModelsMarkdownTable(models) {
|
|
26
|
+
const lines = models.map((m) => {
|
|
27
|
+
const name = m.displayName ?? m.id;
|
|
28
|
+
const leftover = m.leftoverPct != null ? `${m.leftoverPct}%` : "—";
|
|
29
|
+
const status = m.catalog?.status ?? "unfetched";
|
|
30
|
+
const ids = (m.catalog?.listed ?? []).map((x) => x.id).join(", ") || "(none)";
|
|
31
|
+
return `| ${esc(name)} | ${leftover} | ${esc(status)} | ${esc(ids)} |`;
|
|
32
|
+
});
|
|
33
|
+
return [
|
|
34
|
+
"| Provider | Leftover | Status | Models |",
|
|
35
|
+
"|---|---|---|---|",
|
|
36
|
+
...lines,
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
package/dist/format/rows.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderSnapshot } from "../advisory/types.js";
|
|
2
|
-
export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Measuring" | "Not reporting";
|
|
2
|
+
export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Watch" | "Measuring" | "Not reporting";
|
|
3
3
|
export type RailCell = "used" | "tick" | "unused";
|
|
4
4
|
export type SortKey = "recommended" | "reset-asc" | "reset-desc" | "used-desc" | "used-asc";
|
|
5
5
|
export type CompactSuffix = "" | "!" | "~" | "?";
|