catherd-cli 1.5.0 → 2.0.0-beta.20261022113309
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 +164 -208
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -247
- package/package.json +6 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +97 -421
- package/plugin/skills/catherd-setup/SKILL.md +20 -19
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +1 -2
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +4 -4
- package/src/cli.ts +22 -27
- package/src/domain/bars.ts +33 -107
- package/src/domain/blocked.ts +72 -0
- package/src/domain/branch.ts +51 -0
- package/src/domain/brief.ts +21 -6
- package/src/domain/catalog.ts +69 -324
- package/src/domain/cost.ts +15 -30
- package/src/domain/errors.ts +2 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -87
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +1 -0
- package/src/domain/ids.ts +13 -2
- package/src/domain/jev.ts +47 -2
- package/src/domain/judge.ts +124 -0
- package/src/domain/lane.ts +22 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +25 -8
- package/src/domain/notify.ts +32 -0
- package/src/domain/plan-doc.ts +198 -0
- package/src/domain/plan-lint.ts +141 -0
- package/src/domain/profile-rules.ts +101 -160
- package/src/domain/profile.ts +116 -63
- package/src/domain/record.ts +53 -2
- package/src/domain/role-prompts.ts +131 -45
- package/src/domain/role-scope.ts +4 -34
- package/src/domain/role-tools.ts +2 -2
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +17 -0
- package/src/domain/rules.ts +33 -0
- package/src/domain/run-events.ts +298 -0
- package/src/domain/run-machine.ts +668 -0
- package/src/domain/run-state.ts +1310 -0
- package/src/domain/scorecard.ts +197 -0
- package/src/domain/select.ts +151 -146
- package/src/domain/slopalytics.ts +72 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +25 -174
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +1 -1
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/init-command.ts +18 -68
- package/src/entry/knowledge-command.ts +45 -4
- package/src/entry/lock-command.ts +6 -7
- package/src/entry/mcp/role-server.ts +18 -0
- package/src/entry/mcp/server.ts +13 -40
- package/src/entry/mcp/session-tools.ts +149 -0
- package/src/entry/panel/openapi.ts +189 -0
- package/src/entry/panel/server.ts +410 -0
- package/src/entry/panel-command.ts +68 -0
- package/src/entry/profile-command.ts +10 -14
- package/src/entry/role-cli-command.ts +47 -3
- package/src/entry/runs-command.ts +3 -19
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/infra/env.ts +0 -2
- package/src/infra/git.ts +64 -0
- package/src/infra/heavy-lock.ts +45 -7
- package/src/infra/launch.ts +63 -7
- package/src/infra/memory.ts +45 -0
- package/src/infra/ntfy.ts +40 -0
- package/src/infra/open-url.ts +42 -0
- package/src/infra/paths.ts +2 -2
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/services/admission.ts +24 -9
- package/src/services/backends.ts +4 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -262
- package/src/services/credentials.ts +4 -20
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +5 -1
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor.ts +4 -9
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +12 -0
- package/src/services/gate-env.ts +1 -2
- package/src/services/gate-service.ts +21 -6
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-service.ts +34 -3
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +35 -8
- package/src/services/notifier.ts +69 -2
- package/src/services/notify-service.ts +26 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +30 -4
- package/src/services/preflight.ts +82 -8
- package/src/services/profile-service.ts +30 -3
- package/src/services/profile-store.ts +8 -3
- package/src/services/protocol.ts +4 -0
- package/src/services/provenance.ts +31 -90
- package/src/services/questions.ts +9 -0
- package/src/services/reconcile.ts +11 -0
- package/src/services/routing-service.ts +26 -15
- package/src/services/run-briefs.ts +477 -0
- package/src/services/run-client.ts +217 -0
- package/src/services/run-effects.ts +837 -0
- package/src/services/run-events-store.ts +174 -0
- package/src/services/run-evidence.ts +1 -1
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +55 -6
- package/src/services/run-store.ts +54 -7
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/summary.ts +27 -3
- package/src/services/tree-path.ts +27 -0
- package/src/services/workspace-admission.ts +5 -13
- package/src/services/workspace-service.ts +5 -0
- package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js +1 -0
- package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
- package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js +1 -0
- package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-B6GiwDkh.js +11 -0
- package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
- package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
- package/web/dist/assets/index-C5IbylvM.css +2 -0
- package/web/dist/assets/index-C5IbylvM.css.br +0 -0
- package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
- package/web/dist/assets/label-DFSYBc62.js +1 -0
- package/web/dist/assets/label-DFSYBc62.js.br +0 -0
- package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
- package/web/dist/assets/link-D8EcKZUt.js +1 -0
- package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
- package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
- package/web/dist/assets/login-DaN5HPPW.js +1 -0
- package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
- package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
- package/web/dist/assets/query-Cu--ccT3.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
- package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
- package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
- package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
- package/web/dist/assets/table-BOxXhqS4.js +1 -0
- package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
- package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
- package/web/dist/assets/time--9kSQbbp.js +1 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
- package/web/dist/index.html +15 -0
- package/catalog/ATTRIBUTION.md +0 -44
- package/catalog/scores.json +0 -3202
- package/catalog/sources.json +0 -97
- package/src/adapters/antigravity/events.ts +0 -131
- package/src/adapters/antigravity/home.ts +0 -46
- package/src/adapters/antigravity/index.ts +0 -335
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -187
- package/src/adapters/grok/index.ts +0 -358
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -132
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -131
- package/src/entry/mcp/protocol-tools.ts +0 -63
- package/src/entry/mcp/push-tools.ts +0 -16
- package/src/entry/mcp/run-tools.ts +0 -164
- package/src/entry/mcp/setup-tools.ts +0 -109
- package/src/entry/mcp/workspace-tools.ts +0 -142
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -385
- package/src/entry/tui/fixtures.ts +0 -637
- package/src/entry/tui/profile-edits.ts +0 -146
- package/src/entry/tui/profile-tree.ts +0 -429
- package/src/entry/tui/providers/app.tsx +0 -162
- package/src/entry/tui/providers/boundary.tsx +0 -57
- package/src/entry/tui/providers/data.tsx +0 -160
- package/src/entry/tui/providers/keymap.tsx +0 -166
- package/src/entry/tui/providers/root.tsx +0 -45
- package/src/entry/tui/providers/theme.tsx +0 -17
- package/src/entry/tui/providers/toast.tsx +0 -77
- package/src/entry/tui/run.tsx +0 -114
- package/src/entry/tui/state.ts +0 -400
- package/src/entry/tui/stories.ts +0 -114
- package/src/entry/tui/text.ts +0 -130
- package/src/entry/tui/theme.ts +0 -140
- package/src/entry/tui/views/app.tsx +0 -208
- package/src/entry/tui/views/dialogs.tsx +0 -41
- package/src/entry/tui/views/profile-actions.ts +0 -331
- package/src/entry/tui/views/profiles.tsx +0 -520
- package/src/entry/tui/views/runs.tsx +0 -619
- package/src/entry/tui/views/save-dialog.tsx +0 -344
- package/src/entry/tui/views/status.tsx +0 -213
- package/src/entry/tui/widgets/chrome.tsx +0 -114
- package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
- package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
- package/src/entry/tui/widgets/dialog-select.tsx +0 -212
- package/src/entry/tui/widgets/dialog.tsx +0 -68
- package/src/entry/tui/widgets/line.tsx +0 -92
- package/src/entry/tui/widgets/list.tsx +0 -197
- package/src/entry/tui/widgets/spinner.tsx +0 -49
- package/src/entry/tui/widgets/toast.tsx +0 -62
- package/src/infra/sources/arena.ts +0 -95
- package/src/infra/sources/artificial-analysis.ts +0 -119
- package/src/infra/sources/cache.ts +0 -126
- package/src/infra/sources/csv.ts +0 -38
- package/src/infra/sources/epoch.ts +0 -82
- package/src/infra/sources/http.ts +0 -75
- package/src/infra/sources/litellm.ts +0 -58
- package/src/infra/sources/models-dev.ts +0 -59
- package/src/infra/sources/openrouter-endpoints.ts +0 -52
- package/src/infra/sources/openrouter-models.ts +0 -60
- package/src/infra/sources/rows.ts +0 -38
- package/src/infra/sources/vectara.ts +0 -69
- package/src/infra/sources/zip.ts +0 -48
- package/src/services/catalog-refresh.ts +0 -201
- package/src/services/doctor-sources.ts +0 -76
- package/src/services/runs-page.ts +0 -290
- package/src/services/source-derive.ts +0 -397
- package/src/services/source-sync.ts +0 -312
- package/src/services/standins.ts +0 -238
- package/src/services/treat-likes.ts +0 -113
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import type { DiscoveredModel } from "../adapters/backend.ts";
|
|
4
4
|
import { discovered, listingRepo, readDiscovery, writeDiscovery } from "../adapters/discovery.ts";
|
|
@@ -7,55 +7,32 @@ import "../adapters/all.ts";
|
|
|
7
7
|
import {
|
|
8
8
|
buildCatalog,
|
|
9
9
|
type Catalog,
|
|
10
|
-
CanonicalRung,
|
|
11
10
|
capableFor,
|
|
12
|
-
DIMS,
|
|
13
11
|
type Family,
|
|
14
12
|
type ModelsFile,
|
|
15
13
|
ModelsFileSchema,
|
|
16
|
-
type
|
|
17
|
-
OverrideSchema,
|
|
14
|
+
type Pair,
|
|
18
15
|
rungInfo,
|
|
19
|
-
type ScoresFile,
|
|
20
|
-
ScoresFileSchema,
|
|
21
|
-
scoresOf,
|
|
22
16
|
} from "../domain/catalog.ts";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
17
|
+
import { type BillingMode, type Cost, costOf, DEFAULT_BILLING } from "../domain/cost.ts";
|
|
18
|
+
import { errorMessage } from "../domain/errors.ts";
|
|
25
19
|
import { ADAPTER_IDS, tryParseRung } from "../domain/ids.ts";
|
|
26
|
-
import type { Kind } from "../domain/lane.ts";
|
|
27
|
-
import { routeAt } from "../domain/route.ts";
|
|
28
20
|
import { ROLES, type Role } from "../domain/roles.ts";
|
|
29
|
-
import { median } from "../domain/util.ts";
|
|
30
21
|
import { assetPath } from "../infra/assets.ts";
|
|
31
|
-
import { withFileLock } from "../infra/filelock.ts";
|
|
32
22
|
import { log } from "../infra/log.ts";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
23
|
+
import { withFileLock } from "../infra/filelock.ts";
|
|
24
|
+
import { indexPath } from "../infra/paths.ts";
|
|
25
|
+
import { type IndexFile, IndexFileSchema } from "../domain/slopalytics.ts";
|
|
26
|
+
import { fetchProxy, readIndexFile, writeIndexFile } from "../infra/slopalytics.ts";
|
|
27
|
+
import { PRIVATE_DIR, readVersioned } from "../infra/store.ts";
|
|
36
28
|
import type { CatalogFilter } from "./ports.ts";
|
|
37
|
-
import { listRuns, readAgentRuns, readRecords, readRoutes } from "./run-store.ts";
|
|
38
|
-
import { valuesUsed } from "./provenance.ts";
|
|
39
29
|
import { type EvidenceTable, evidenceLine, evidenceOf, runEvidence } from "./run-evidence.ts";
|
|
40
|
-
import { withStandIns } from "./standins.ts";
|
|
41
30
|
|
|
42
31
|
const DAY_MS = 24 * 3_600_000;
|
|
43
|
-
/** Spec §5.2: `secs_per_task` counts once a rung has this many of the user's own runs. */
|
|
44
|
-
const MIN_SAMPLES = 5;
|
|
45
32
|
|
|
46
33
|
let models: ModelsFile | null = null;
|
|
47
|
-
let scores: ScoresFile | null = null;
|
|
48
34
|
export const shippedModels = (): ModelsFile =>
|
|
49
35
|
(models ??= readVersioned(assetPath("catalog/models.json"), ModelsFileSchema, 1));
|
|
50
|
-
export const shippedScores = (): ScoresFile =>
|
|
51
|
-
(scores ??= readVersioned(assetPath("catalog/scores.json"), ScoresFileSchema, 1));
|
|
52
|
-
|
|
53
|
-
export const overridePath = (): string => join(configDir(), "catalog.override.json");
|
|
54
|
-
|
|
55
|
-
export function readOverride(): Override {
|
|
56
|
-
const file = overridePath();
|
|
57
|
-
return existsSync(file) ? readVersioned(file, OverrideSchema, 1) : OverrideSchema.parse({});
|
|
58
|
-
}
|
|
59
36
|
|
|
60
37
|
/**
|
|
61
38
|
* Every backend's last listing (spec §3.5 `<data>/discovery/<backend>.json`); for a backend listed per
|
|
@@ -70,80 +47,36 @@ function listedModels(repo?: string): Catalog["listed"] {
|
|
|
70
47
|
return out;
|
|
71
48
|
}
|
|
72
49
|
|
|
50
|
+
const RUN_INDEX = "slopalytics.json";
|
|
51
|
+
|
|
73
52
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* lane's route in force when it started; a run with none counts only under `|*`. Native `claude:`
|
|
77
|
-
* subagent runs (agents.jsonl, from record_agent_run) count too; one that names no lane counts only
|
|
78
|
-
* under `|*`.
|
|
53
|
+
* The shipped families on the Slopalytics index (the run's own pin when `runDir` holds one, else the machine's),
|
|
54
|
+
* with every listing (in `repo` for a per-repo backend).
|
|
79
55
|
*/
|
|
80
|
-
export function
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
) => {
|
|
90
|
-
let canonical: string;
|
|
91
|
-
try {
|
|
92
|
-
canonical = rungInfo(base, rung).canonical;
|
|
93
|
-
} catch {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
// the kind the lane was routed as when this run started, not its newest route
|
|
97
|
-
const kind: Kind | null = lane ? (routeAt(routes, lane, startedAt)?.kind ?? null) : null;
|
|
98
|
-
add(`${canonical}|*`, secs);
|
|
99
|
-
if (kind) add(`${canonical}|${kind}`, secs);
|
|
100
|
-
};
|
|
101
|
-
for (const run of listRuns().runs) {
|
|
102
|
-
// a run whose files cannot be read gives no timings; it never fails a route (1.2 minor)
|
|
103
|
-
try {
|
|
104
|
-
const routes = readRoutes(run);
|
|
105
|
-
for (const r of readRecords(run).records)
|
|
106
|
-
if (r.status === "ok") count(routes, r.rung, r.secs, r.lane, r.startedAt);
|
|
107
|
-
for (const a of readAgentRuns(run)) {
|
|
108
|
-
if (a.status !== "ok" || a.secs === null) continue;
|
|
109
|
-
// a row is written when the subagent ends; it started `secs` earlier
|
|
110
|
-
const lane = typeof a.lane === "string" ? a.lane : null;
|
|
111
|
-
const end = Date.parse(a.at);
|
|
112
|
-
// a duration past the Date range (record_agent_run takes any) keeps the end, not a RangeError
|
|
113
|
-
const start = new Date(end - a.secs * 1000);
|
|
114
|
-
const startedAt = Number.isNaN(start.getTime()) ? a.at : start.toISOString();
|
|
115
|
-
count(routes, a.rung, a.secs, lane, startedAt);
|
|
116
|
-
}
|
|
117
|
-
} catch (e) {
|
|
118
|
-
log("debug", "catalog", { run: run.id, timings: errorMessage(e) });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const secs: Catalog["secs"] = {};
|
|
122
|
-
for (const [k, xs] of groups) if (xs.length >= MIN_SAMPLES) secs[k] = median(xs) as number;
|
|
123
|
-
return secs;
|
|
56
|
+
export function loadCatalog(o: { repo?: string; runDir?: string } = {}): Catalog {
|
|
57
|
+
const pin = o.runDir === undefined ? null : join(o.runDir, RUN_INDEX);
|
|
58
|
+
// a missing pin falls back to the machine's index; one that exists but cannot be read is an error, not a fallback
|
|
59
|
+
const pinned = pin !== null && existsSync(pin) ? readVersioned(pin, IndexFileSchema, 1) : null;
|
|
60
|
+
return buildCatalog({
|
|
61
|
+
models: shippedModels(),
|
|
62
|
+
index: pinned ?? readIndexFile(indexPath()),
|
|
63
|
+
listed: listedModels(o.repo),
|
|
64
|
+
});
|
|
124
65
|
}
|
|
125
66
|
|
|
126
67
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* timings.
|
|
68
|
+
* What a run starts on: the index refreshed if it is due, read once, with the catalog built on exactly that
|
|
69
|
+
* read, so the start check and the pin written after it cannot disagree.
|
|
130
70
|
*/
|
|
131
|
-
export function
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
facts: synced?.facts,
|
|
141
|
-
features: synced?.features,
|
|
142
|
-
override: o.override ?? readOverride(),
|
|
143
|
-
listed: listedModels(o.repo),
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
146
|
-
return o.timings === false ? base : { ...base, secs: measuredSecs(base) };
|
|
71
|
+
export async function startIndex(repo: string): Promise<{ catalog: Catalog; index: IndexFile | null }> {
|
|
72
|
+
await refreshIndex({ auto: true });
|
|
73
|
+
const index = readIndexFile(indexPath());
|
|
74
|
+
return { catalog: buildCatalog({ models: shippedModels(), index, listed: listedModels(repo) }), index };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Pins `index` into the run: later routes of the run read it, whatever the machine's index becomes. */
|
|
78
|
+
export function writeRunIndex(runDir: string, index: IndexFile | null): void {
|
|
79
|
+
if (index) writeIndexFile(join(runDir, RUN_INDEX), index);
|
|
147
80
|
}
|
|
148
81
|
|
|
149
82
|
export interface Refreshed {
|
|
@@ -233,145 +166,19 @@ export async function freshenDiscovery(rungs: string[], now = Date.now(), repo?:
|
|
|
233
166
|
});
|
|
234
167
|
}
|
|
235
168
|
|
|
236
|
-
/** A rung given as `backend:model#effort` or as a canonical `model#effort`, in canonical form. */
|
|
237
|
-
function canonicalOf(c: Catalog, rung: string): string {
|
|
238
|
-
let canonical = rung;
|
|
239
|
-
try {
|
|
240
|
-
if (rung.includes(":")) canonical = rungInfo(c, rung).canonical;
|
|
241
|
-
} catch {
|
|
242
|
-
// a rung the catalog cannot resolve stays as given; the canonical check below refuses it
|
|
243
|
-
}
|
|
244
|
-
// a backend's own model id without its backend: the family that backend runs it for names it
|
|
245
|
-
const hash = canonical.lastIndexOf("#");
|
|
246
|
-
const model = canonical.slice(0, hash);
|
|
247
|
-
if (hash > 0 && !c.families.some((f) => f.id === model)) {
|
|
248
|
-
const family = c.families.find((f) => Object.values(f.on).some((m) => m?.id === model));
|
|
249
|
-
if (family) canonical = `${family.id}${canonical.slice(hash)}`;
|
|
250
|
-
}
|
|
251
|
-
if (!CanonicalRung.safeParse(canonical).success)
|
|
252
|
-
throw new CatherdError("E_INPUT_INVALID", `"${rung}" is not a rung`, {
|
|
253
|
-
fix: "name it as backend:model#effort or model#effort, e.g. codex:gpt-6-sol#high",
|
|
254
|
-
});
|
|
255
|
-
return canonical;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Spec §5.2 "treat like": the user maps an unscored rung onto a scored one in catalog.override.json. */
|
|
259
|
-
export async function saveTreatLike(rung: string, like: string): Promise<{ rung: string; like: string }> {
|
|
260
|
-
const c = loadCatalog({ timings: false });
|
|
261
|
-
const from = canonicalOf(c, rung);
|
|
262
|
-
const to = canonicalOf(c, like);
|
|
263
|
-
if (!c.scores[to])
|
|
264
|
-
throw new CatherdError("E_CONFIG_INVALID", `${like} has no scores of its own to lend`, {
|
|
265
|
-
fix: "treat it like a scored rung; catalog_query lists them",
|
|
266
|
-
});
|
|
267
|
-
if (from === to)
|
|
268
|
-
throw new CatherdError("E_CONFIG_INVALID", `${rung} cannot be treated like itself`, {
|
|
269
|
-
fix: "name a different, scored rung",
|
|
270
|
-
});
|
|
271
|
-
// a rung a sync scored on some dimensions may still borrow the others; one with every value `to` lends may not
|
|
272
|
-
const own = c.scores[from] ?? {};
|
|
273
|
-
if (DIMS.filter((d) => c.scores[to]?.[d]).every((d) => own[d]))
|
|
274
|
-
throw new CatherdError(
|
|
275
|
-
"E_CONFIG_INVALID",
|
|
276
|
-
`${from} has scores of its own; a treat-like would not change it`,
|
|
277
|
-
{
|
|
278
|
-
fix: "treat-like maps only unscored rungs; catalog_query shows each rung's scores",
|
|
279
|
-
},
|
|
280
|
-
);
|
|
281
|
-
ensurePrivateDir(dirname(overridePath()));
|
|
282
|
-
await withFileLock(overridePath(), () => {
|
|
283
|
-
const cur = readOverride();
|
|
284
|
-
const next = { ...cur, schema: 1, treatLike: { ...cur.treatLike, [from]: to } };
|
|
285
|
-
const checked = OverrideSchema.safeParse(next);
|
|
286
|
-
if (!checked.success)
|
|
287
|
-
throw new CatherdError(
|
|
288
|
-
"E_CONFIG_INVALID",
|
|
289
|
-
`the treat-like ${from} → ${to} would not be a valid override`,
|
|
290
|
-
{
|
|
291
|
-
fix: checked.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "),
|
|
292
|
-
},
|
|
293
|
-
);
|
|
294
|
-
writeJsonAtomic(overridePath(), next);
|
|
295
|
-
});
|
|
296
|
-
return { rung: from, like: to };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** A rung given as `backend:model#effort` or `model#effort`, as the canonical rung the override keys it by. */
|
|
300
|
-
export const canonicalRung = (rung: string): string => canonicalOf(loadCatalog({ timings: false }), rung);
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Spec 1.2 §6.4 `treat-like --clear|--reset`: removes the user's treat-likes for `rungs` (canonical), or every
|
|
304
|
-
* one with `"all"`, from catalog.override.json; the shipped ones are not the user's to remove. Returns the
|
|
305
|
-
* mappings removed. Leaves the override's other fields as they are.
|
|
306
|
-
*/
|
|
307
|
-
export async function removeTreatLikes(rungs: string[] | "all"): Promise<[string, string][]> {
|
|
308
|
-
if (!existsSync(overridePath())) return [];
|
|
309
|
-
let removed: [string, string][] = [];
|
|
310
|
-
await withFileLock(overridePath(), () => {
|
|
311
|
-
const cur = readOverride();
|
|
312
|
-
const gone = Object.entries(cur.treatLike).filter(([r]) => rungs === "all" || rungs.includes(r));
|
|
313
|
-
removed = gone;
|
|
314
|
-
if (gone.length === 0) return;
|
|
315
|
-
const treatLike = Object.fromEntries(
|
|
316
|
-
Object.entries(cur.treatLike).filter(([r]) => !gone.some(([g]) => g === r)),
|
|
317
|
-
);
|
|
318
|
-
writeJsonAtomic(overridePath(), { ...cur, schema: 1, treatLike });
|
|
319
|
-
});
|
|
320
|
-
return removed;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
169
|
/** The backend a billing or harness key bills or isolates: `opencode-go` is a provider of the opencode backend. */
|
|
324
170
|
export const backendOfKey = (key: string) => (key === "opencode-go" ? "opencode" : key);
|
|
325
171
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const info = rungInfo(c, rung);
|
|
337
|
-
const s = scoresOf(c, info.canonical);
|
|
338
|
-
const scored: Record<
|
|
339
|
-
string,
|
|
340
|
-
{
|
|
341
|
-
value: number;
|
|
342
|
-
benchmark: string;
|
|
343
|
-
confidence: string;
|
|
344
|
-
source: string;
|
|
345
|
-
date: string;
|
|
346
|
-
/** spec 1.2 §5.3: the rung a borrowed or inferred value belongs to */
|
|
347
|
-
from?: string;
|
|
348
|
-
/** what lent it: a treat-like's mapping (`like X`) or an inferred stand-in's guess (1.2 minor) */
|
|
349
|
-
lent?: "treat-like" | "stand-in";
|
|
350
|
-
}
|
|
351
|
-
> = {};
|
|
352
|
-
for (const v of valuesUsed(c, info.canonical))
|
|
353
|
-
scored[v.dim] = {
|
|
354
|
-
value: v.value,
|
|
355
|
-
benchmark: v.benchmark,
|
|
356
|
-
// a value lent by a treat-like or a stand-in is catherd's guess for this rung, whatever its own confidence
|
|
357
|
-
confidence: v.inferred ? "inferred" : v.confidence,
|
|
358
|
-
source: v.source,
|
|
359
|
-
date: v.date,
|
|
360
|
-
...(v.from ? { from: v.from } : {}),
|
|
361
|
-
...(v.lent ? { lent: v.lent } : {}),
|
|
362
|
-
};
|
|
363
|
-
const like = c.treatLike[info.canonical] ?? null;
|
|
364
|
-
return {
|
|
365
|
-
rung,
|
|
366
|
-
enabled: s !== null,
|
|
367
|
-
...(s === null ? { why: "unscored: map it with catherd catalog treat-like <rung> <scored rung>" } : {}),
|
|
368
|
-
scores: scored,
|
|
369
|
-
treatLike: s?.via ? like : null,
|
|
370
|
-
cost: costOf(info.family, effort, billing[info.key] ?? DEFAULT_BILLING[info.key]),
|
|
371
|
-
/** spec 1.2 §8: catherd's own runs on it, over every kind; never used for routing */
|
|
372
|
-
evidence: evidenceLine(evidenceOf(evidence, info.canonical)),
|
|
373
|
-
};
|
|
374
|
-
});
|
|
172
|
+
/** One indexed pair as `catalog_query` and `catalog list` show it. */
|
|
173
|
+
export interface IndexedPair {
|
|
174
|
+
rung: string;
|
|
175
|
+
intelligence: number;
|
|
176
|
+
price: Pair["price"];
|
|
177
|
+
speed: number | null;
|
|
178
|
+
context: number | null;
|
|
179
|
+
cost: Cost;
|
|
180
|
+
/** spec 1.2 §8: catherd's own runs on it, over every kind; never used for routing */
|
|
181
|
+
evidence: string | null;
|
|
375
182
|
}
|
|
376
183
|
|
|
377
184
|
export interface CatalogModel {
|
|
@@ -381,70 +188,156 @@ export interface CatalogModel {
|
|
|
381
188
|
model: string;
|
|
382
189
|
billing: string;
|
|
383
190
|
efforts: string[];
|
|
384
|
-
context: number | null;
|
|
385
191
|
capabilities: Family["capabilities"] | null;
|
|
386
192
|
roles: Role[];
|
|
387
193
|
listed: boolean | null;
|
|
388
194
|
notes: Record<string, string>;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
195
|
+
rungs: IndexedPair[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function pairsOf(
|
|
199
|
+
c: Catalog,
|
|
200
|
+
backend: string,
|
|
201
|
+
model: string,
|
|
202
|
+
billing: Partial<Record<string, BillingMode>>,
|
|
203
|
+
evidence: EvidenceTable,
|
|
204
|
+
): IndexedPair[] {
|
|
205
|
+
const probe = rungInfo(c, `${backend}:${model}#default`);
|
|
206
|
+
const efforts = [...new Set([...probe.efforts, "default"])];
|
|
207
|
+
return efforts
|
|
208
|
+
.flatMap((effort) => {
|
|
209
|
+
const rung = `${backend}:${model}#${effort}`;
|
|
210
|
+
const info = rungInfo(c, rung);
|
|
211
|
+
const pair = info.pair;
|
|
212
|
+
if (!pair) return [];
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
rung,
|
|
216
|
+
intelligence: pair.intelligence,
|
|
217
|
+
price: pair.price,
|
|
218
|
+
speed: pair.speed,
|
|
219
|
+
context: info.context,
|
|
220
|
+
cost: costOf(info.family, pair, effort, billing[info.key] ?? DEFAULT_BILLING[info.key]),
|
|
221
|
+
evidence: evidenceLine(evidenceOf(evidence, info.canonical)),
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
})
|
|
225
|
+
.sort((a, b) => a.intelligence - b.intelligence);
|
|
393
226
|
}
|
|
394
227
|
|
|
395
228
|
/**
|
|
396
|
-
* Spec §4.8 `catalog_query`: shipped families on each backend,
|
|
397
|
-
* a
|
|
229
|
+
* Spec §4.8 `catalog_query`: the indexed pairs of the shipped families on each backend, and of the listed models
|
|
230
|
+
* catherd has no family for; a model with no indexed pair is not listed. A backend listed per repository as it
|
|
231
|
+
* is listed in `f.repo` (a git toplevel), as `route` reads it.
|
|
398
232
|
*/
|
|
399
233
|
export function catalogQuery(
|
|
400
234
|
f: CatalogFilter,
|
|
401
235
|
billing: Partial<Record<string, BillingMode>> = {},
|
|
402
236
|
): { total: number; models: CatalogModel[] } {
|
|
403
|
-
const c = loadCatalog({
|
|
237
|
+
const c = loadCatalog({ repo: f.repo });
|
|
404
238
|
const evidence = runEvidence(c);
|
|
405
239
|
const rows: CatalogModel[] = [];
|
|
406
240
|
const known = new Set<string>();
|
|
407
|
-
const entry = (backend: string, model: string, fam: Family | null)
|
|
241
|
+
const entry = (backend: string, model: string, fam: Family | null) => {
|
|
242
|
+
const rungs = pairsOf(c, backend, model, billing, evidence);
|
|
243
|
+
if (rungs.length === 0) return;
|
|
408
244
|
const probe = rungInfo(c, `${backend}:${model}#default`);
|
|
409
|
-
|
|
245
|
+
rows.push({
|
|
410
246
|
id: fam?.id ?? model,
|
|
411
247
|
name: fam?.name ?? null,
|
|
412
248
|
backend,
|
|
413
249
|
model,
|
|
414
250
|
billing: probe.key,
|
|
415
|
-
efforts:
|
|
416
|
-
context: probe.context,
|
|
251
|
+
efforts: rungs.map((r) => r.rung.slice(r.rung.lastIndexOf("#") + 1)),
|
|
417
252
|
capabilities: fam?.capabilities ?? null,
|
|
418
253
|
roles: ROLES.filter((r) => capableFor(c, probe, r)),
|
|
419
254
|
listed: probe.listed,
|
|
420
255
|
notes: fam?.notes ?? {},
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
rungs: rungRows(c, backend, model, probe.efforts, billing, evidence),
|
|
424
|
-
};
|
|
256
|
+
rungs,
|
|
257
|
+
});
|
|
425
258
|
};
|
|
426
259
|
for (const fam of c.families)
|
|
427
260
|
for (const [key, on] of Object.entries(fam.on)) {
|
|
428
261
|
const backends = key === "claude-code" ? ["claude", "claude-code"] : [backendOfKey(key)];
|
|
429
|
-
for (const b of backends)
|
|
262
|
+
for (const b of backends) entry(b, on.id, fam);
|
|
430
263
|
known.add(`${backendOfKey(key)}:${on.id}`);
|
|
431
264
|
}
|
|
432
265
|
for (const [backend, l] of Object.entries(c.listed))
|
|
433
|
-
for (const m of l.models) if (!known.has(`${backend}:${m.id}`))
|
|
266
|
+
for (const m of l.models) if (!known.has(`${backend}:${m.id}`)) entry(backend, m.id, null);
|
|
434
267
|
const needle = f.text?.toLowerCase();
|
|
435
268
|
const kept = rows
|
|
436
269
|
.filter(
|
|
437
270
|
(m) =>
|
|
438
271
|
(!f.role || m.roles.includes(f.role)) &&
|
|
439
272
|
(!f.backend || m.backend === f.backend || m.billing === f.backend) &&
|
|
440
|
-
(!needle || `${m.id} ${m.model} ${m.name ?? ""}`.toLowerCase().includes(needle))
|
|
441
|
-
(!f.scoredOnly || m.rungs.some((r) => r.enabled)),
|
|
273
|
+
(!needle || `${m.id} ${m.model} ${m.name ?? ""}`.toLowerCase().includes(needle)),
|
|
442
274
|
)
|
|
443
|
-
.sort(
|
|
444
|
-
(a, b) =>
|
|
445
|
-
Number(b.rungs.some((r) => r.enabled)) - Number(a.rungs.some((r) => r.enabled)) ||
|
|
446
|
-
a.id.localeCompare(b.id) ||
|
|
447
|
-
a.backend.localeCompare(b.backend),
|
|
448
|
-
);
|
|
275
|
+
.sort((a, b) => a.id.localeCompare(b.id) || a.backend.localeCompare(b.backend));
|
|
449
276
|
return { total: kept.length, models: kept.slice(0, f.limit) };
|
|
450
277
|
}
|
|
278
|
+
|
|
279
|
+
export interface IndexStatus {
|
|
280
|
+
fetchedAt: string | null;
|
|
281
|
+
checkedAt: string | null;
|
|
282
|
+
pairs: number;
|
|
283
|
+
error: string | null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** How long a fetched index is trusted before the next automatic refresh. */
|
|
287
|
+
export const INDEX_TTL_MS = 3_600_000;
|
|
288
|
+
|
|
289
|
+
export function indexStatus(): IndexStatus {
|
|
290
|
+
const f = readIndexFile(indexPath());
|
|
291
|
+
return {
|
|
292
|
+
fetchedAt: f?.fetchedAt ?? null,
|
|
293
|
+
checkedAt: f?.checkedAt ?? null,
|
|
294
|
+
pairs: f?.models.length ?? 0,
|
|
295
|
+
error: f?.error?.message ?? null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Keeps `<data>/slopalytics.json` fresh: fetched when its last check is over an hour old (or `force`), the last
|
|
301
|
+
* good answer kept when the proxy fails and the error recorded beside it. Never throws. An `auto` refresh
|
|
302
|
+
* (MCP boot, `init`, run start) fetches nothing under `CATHERD_NO_SYNC=1`.
|
|
303
|
+
*/
|
|
304
|
+
export async function refreshIndex(
|
|
305
|
+
o: { force?: boolean; auto?: boolean; fetch?: typeof fetch; now?: number } = {},
|
|
306
|
+
): Promise<IndexStatus> {
|
|
307
|
+
if (o.auto && process.env.CATHERD_NO_SYNC === "1") return indexStatus();
|
|
308
|
+
const now = o.now ?? Date.now();
|
|
309
|
+
// read, fetch and write run under one lock: a refresh that fails reads the file as the one before it left it
|
|
310
|
+
try {
|
|
311
|
+
mkdirSync(dirname(indexPath()), { recursive: true, mode: PRIVATE_DIR });
|
|
312
|
+
await withFileLock(indexPath(), () => refreshLocked(o, now), { timeoutMs: 30_000 });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
log("debug", "catalog", { index: errorMessage(e) });
|
|
315
|
+
}
|
|
316
|
+
return indexStatus();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function refreshLocked(o: { force?: boolean; fetch?: typeof fetch }, now: number): Promise<void> {
|
|
320
|
+
const prev = readIndexFile(indexPath());
|
|
321
|
+
if (!o.force && prev && now - Date.parse(prev.checkedAt) < INDEX_TTL_MS) return;
|
|
322
|
+
const at = new Date(now).toISOString();
|
|
323
|
+
try {
|
|
324
|
+
const got = await fetchProxy({ fetch: o.fetch });
|
|
325
|
+
writeIndexFile(indexPath(), {
|
|
326
|
+
schema: 1,
|
|
327
|
+
checkedAt: at,
|
|
328
|
+
fetchedAt: got.fetchedAt,
|
|
329
|
+
error: null,
|
|
330
|
+
models: got.models,
|
|
331
|
+
});
|
|
332
|
+
} catch (e) {
|
|
333
|
+
const message = errorMessage(e);
|
|
334
|
+
log("debug", "catalog", { index: message });
|
|
335
|
+
writeIndexFile(indexPath(), {
|
|
336
|
+
schema: 1,
|
|
337
|
+
checkedAt: at,
|
|
338
|
+
fetchedAt: prev?.fetchedAt ?? null,
|
|
339
|
+
error: { at, message },
|
|
340
|
+
models: prev?.models ?? [],
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -3,22 +3,20 @@ import { join } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { CatherdError, isCatherdError } from "../domain/errors.ts";
|
|
5
5
|
import { withFileLockSync } from "../infra/filelock.ts";
|
|
6
|
-
import { log } from "../infra/log.ts";
|
|
7
6
|
import { configDir } from "../infra/paths.ts";
|
|
8
7
|
import { readVersioned, writeJsonAtomic } from "../infra/store.ts";
|
|
9
8
|
|
|
10
|
-
// Spec §10.4
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// Spec §10.4: `<config>/credentials.json`, mode 600, holds catherd's own key, Jev's (`typesafeApiKey`). The
|
|
10
|
+
// schema is a loose object, so a key one catherd does not know (a 1.5 file's `artificialAnalysisApiKey`)
|
|
11
|
+
// survives another's rewrite.
|
|
13
12
|
|
|
14
13
|
export const credentialsPath = (): string => join(configDir(), "credentials.json");
|
|
15
14
|
|
|
16
15
|
const CredentialsSchema = z.looseObject({
|
|
17
16
|
schema: z.literal(1).default(1),
|
|
18
17
|
typesafeApiKey: z.string().optional(),
|
|
19
|
-
artificialAnalysisApiKey: z.string().optional(),
|
|
20
18
|
});
|
|
21
|
-
export type CredentialField = "typesafeApiKey"
|
|
19
|
+
export type CredentialField = "typesafeApiKey";
|
|
22
20
|
|
|
23
21
|
/** How to repair a credentials file catherd cannot read: `init` alone refuses to overwrite it. */
|
|
24
22
|
const credentialsFix = (): string =>
|
|
@@ -58,17 +56,3 @@ export function saveCredential(field: CredentialField, key: string): void {
|
|
|
58
56
|
writeJsonAtomic(credentialsPath(), { ...cur, schema: 1, [field]: key.trim() }, { mode: 0o600 });
|
|
59
57
|
});
|
|
60
58
|
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Spec 1.2 §9: `ARTIFICIAL_ANALYSIS_API_KEY`, else the saved key; null when neither has one. The key is
|
|
64
|
-
* optional, so a credentials file that cannot be read means no key here; it is logged.
|
|
65
|
-
*/
|
|
66
|
-
export function aaKey(env: Record<string, string | undefined> = process.env): string | null {
|
|
67
|
-
const fromEnv = env.ARTIFICIAL_ANALYSIS_API_KEY?.trim();
|
|
68
|
-
if (fromEnv) return fromEnv;
|
|
69
|
-
const { key, problem } = savedCredential("artificialAnalysisApiKey");
|
|
70
|
-
if (problem) log("warn", "sources", { error: `no Artificial Analysis key: ${problem.message}` });
|
|
71
|
-
return key;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const saveAaKey = (key: string): void => saveCredential("artificialAnalysisApiKey", key);
|