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.
Files changed (275) hide show
  1. package/README.md +164 -208
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -247
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -421
  11. package/plugin/skills/catherd-setup/SKILL.md +20 -19
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +1 -2
  14. package/src/adapters/cursor/home.ts +1 -1
  15. package/src/adapters/cursor/index.ts +4 -4
  16. package/src/cli.ts +22 -27
  17. package/src/domain/bars.ts +33 -107
  18. package/src/domain/blocked.ts +72 -0
  19. package/src/domain/branch.ts +51 -0
  20. package/src/domain/brief.ts +21 -6
  21. package/src/domain/catalog.ts +69 -324
  22. package/src/domain/cost.ts +15 -30
  23. package/src/domain/errors.ts +2 -2
  24. package/src/domain/fail-lines.ts +26 -0
  25. package/src/domain/failover.ts +37 -87
  26. package/src/domain/findings.ts +47 -0
  27. package/src/domain/gate-brief.ts +1 -0
  28. package/src/domain/ids.ts +13 -2
  29. package/src/domain/jev.ts +47 -2
  30. package/src/domain/judge.ts +124 -0
  31. package/src/domain/lane.ts +22 -3
  32. package/src/domain/milestone-memory.ts +53 -0
  33. package/src/domain/notice.ts +25 -8
  34. package/src/domain/notify.ts +32 -0
  35. package/src/domain/plan-doc.ts +198 -0
  36. package/src/domain/plan-lint.ts +141 -0
  37. package/src/domain/profile-rules.ts +101 -160
  38. package/src/domain/profile.ts +116 -63
  39. package/src/domain/record.ts +53 -2
  40. package/src/domain/role-prompts.ts +131 -45
  41. package/src/domain/role-scope.ts +4 -34
  42. package/src/domain/role-tools.ts +2 -2
  43. package/src/domain/roles.ts +2 -0
  44. package/src/domain/route.ts +17 -0
  45. package/src/domain/rules.ts +33 -0
  46. package/src/domain/run-events.ts +298 -0
  47. package/src/domain/run-machine.ts +668 -0
  48. package/src/domain/run-state.ts +1310 -0
  49. package/src/domain/scorecard.ts +197 -0
  50. package/src/domain/select.ts +151 -146
  51. package/src/domain/slopalytics.ts +72 -0
  52. package/src/entry/answer-command.ts +39 -0
  53. package/src/entry/catalog-command.ts +25 -174
  54. package/src/entry/check-bin.ts +17 -0
  55. package/src/entry/cli-kit.ts +1 -1
  56. package/src/entry/daemon-bin.ts +22 -0
  57. package/src/entry/glyphs.ts +15 -2
  58. package/src/entry/init-command.ts +18 -68
  59. package/src/entry/knowledge-command.ts +45 -4
  60. package/src/entry/lock-command.ts +6 -7
  61. package/src/entry/mcp/role-server.ts +18 -0
  62. package/src/entry/mcp/server.ts +13 -40
  63. package/src/entry/mcp/session-tools.ts +149 -0
  64. package/src/entry/panel/openapi.ts +189 -0
  65. package/src/entry/panel/server.ts +410 -0
  66. package/src/entry/panel-command.ts +68 -0
  67. package/src/entry/profile-command.ts +10 -14
  68. package/src/entry/role-cli-command.ts +47 -3
  69. package/src/entry/runs-command.ts +3 -19
  70. package/src/entry/scorecard-command.ts +187 -0
  71. package/src/entry/supervise-bin.ts +1 -1
  72. package/src/infra/env.ts +0 -2
  73. package/src/infra/git.ts +64 -0
  74. package/src/infra/heavy-lock.ts +45 -7
  75. package/src/infra/launch.ts +63 -7
  76. package/src/infra/memory.ts +45 -0
  77. package/src/infra/ntfy.ts +40 -0
  78. package/src/infra/open-url.ts +42 -0
  79. package/src/infra/paths.ts +2 -2
  80. package/src/infra/slopalytics.ts +30 -0
  81. package/src/infra/store.ts +6 -1
  82. package/src/services/admission.ts +24 -9
  83. package/src/services/backends.ts +4 -6
  84. package/src/services/capture.ts +1 -22
  85. package/src/services/catalog-service.ts +155 -262
  86. package/src/services/credentials.ts +4 -20
  87. package/src/services/daemon.ts +181 -0
  88. package/src/services/dispatch-service.ts +5 -1
  89. package/src/services/doctor-backends.ts +1 -1
  90. package/src/services/doctor-checks.ts +47 -0
  91. package/src/services/doctor.ts +4 -9
  92. package/src/services/failover-rungs.ts +109 -0
  93. package/src/services/finalize.ts +12 -0
  94. package/src/services/gate-env.ts +1 -2
  95. package/src/services/gate-service.ts +21 -6
  96. package/src/services/jev-service.ts +2 -4
  97. package/src/services/lane-service.ts +34 -3
  98. package/src/services/milestone-check.ts +433 -0
  99. package/src/services/milestone-tree.ts +170 -0
  100. package/src/services/milestones.ts +35 -8
  101. package/src/services/notifier.ts +69 -2
  102. package/src/services/notify-service.ts +26 -0
  103. package/src/services/panel-setup.ts +116 -0
  104. package/src/services/plan-input.ts +151 -0
  105. package/src/services/ports.ts +30 -4
  106. package/src/services/preflight.ts +82 -8
  107. package/src/services/profile-service.ts +30 -3
  108. package/src/services/profile-store.ts +8 -3
  109. package/src/services/protocol.ts +4 -0
  110. package/src/services/provenance.ts +31 -90
  111. package/src/services/questions.ts +9 -0
  112. package/src/services/reconcile.ts +11 -0
  113. package/src/services/routing-service.ts +26 -15
  114. package/src/services/run-briefs.ts +477 -0
  115. package/src/services/run-client.ts +217 -0
  116. package/src/services/run-effects.ts +837 -0
  117. package/src/services/run-events-store.ts +174 -0
  118. package/src/services/run-evidence.ts +1 -1
  119. package/src/services/run-read.ts +221 -0
  120. package/src/services/run-report.ts +176 -0
  121. package/src/services/run-service.ts +55 -6
  122. package/src/services/run-store.ts +54 -7
  123. package/src/services/scorecard-metrics.ts +86 -0
  124. package/src/services/scorecard.ts +382 -0
  125. package/src/services/summary.ts +27 -3
  126. package/src/services/tree-path.ts +27 -0
  127. package/src/services/workspace-admission.ts +5 -13
  128. package/src/services/workspace-service.ts +5 -0
  129. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  130. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  131. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  132. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  133. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  134. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  135. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  136. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  137. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  138. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  139. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  140. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  141. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  142. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  143. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  144. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  145. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  146. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  147. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  148. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  149. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  150. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  151. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  152. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  153. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  154. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  155. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  156. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  157. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  158. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  159. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  160. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  161. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  162. package/web/dist/assets/index-C5IbylvM.css +2 -0
  163. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  164. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  165. package/web/dist/assets/label-DFSYBc62.js +1 -0
  166. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  167. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  168. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  169. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  170. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  171. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  172. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  173. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  174. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  175. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  176. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  177. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  178. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  179. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  180. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  181. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  183. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  184. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  185. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  186. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  187. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  188. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  189. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  190. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  191. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  192. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  193. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  194. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  195. package/web/dist/assets/time--9kSQbbp.js +1 -0
  196. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  197. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  198. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  199. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  200. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  201. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  202. package/web/dist/index.html +15 -0
  203. package/catalog/ATTRIBUTION.md +0 -44
  204. package/catalog/scores.json +0 -3202
  205. package/catalog/sources.json +0 -97
  206. package/src/adapters/antigravity/events.ts +0 -131
  207. package/src/adapters/antigravity/home.ts +0 -46
  208. package/src/adapters/antigravity/index.ts +0 -335
  209. package/src/adapters/antigravity/models.ts +0 -37
  210. package/src/adapters/grok/events.ts +0 -112
  211. package/src/adapters/grok/home.ts +0 -187
  212. package/src/adapters/grok/index.ts +0 -358
  213. package/src/adapters/grok/models.ts +0 -48
  214. package/src/domain/calibration.ts +0 -132
  215. package/src/domain/sources.ts +0 -144
  216. package/src/entry/mcp/dispatch-tools.ts +0 -52
  217. package/src/entry/mcp/lane-tools.ts +0 -131
  218. package/src/entry/mcp/protocol-tools.ts +0 -63
  219. package/src/entry/mcp/push-tools.ts +0 -16
  220. package/src/entry/mcp/run-tools.ts +0 -164
  221. package/src/entry/mcp/setup-tools.ts +0 -109
  222. package/src/entry/mcp/workspace-tools.ts +0 -142
  223. package/src/entry/tui/commands.ts +0 -563
  224. package/src/entry/tui/effects.ts +0 -385
  225. package/src/entry/tui/fixtures.ts +0 -637
  226. package/src/entry/tui/profile-edits.ts +0 -146
  227. package/src/entry/tui/profile-tree.ts +0 -429
  228. package/src/entry/tui/providers/app.tsx +0 -162
  229. package/src/entry/tui/providers/boundary.tsx +0 -57
  230. package/src/entry/tui/providers/data.tsx +0 -160
  231. package/src/entry/tui/providers/keymap.tsx +0 -166
  232. package/src/entry/tui/providers/root.tsx +0 -45
  233. package/src/entry/tui/providers/theme.tsx +0 -17
  234. package/src/entry/tui/providers/toast.tsx +0 -77
  235. package/src/entry/tui/run.tsx +0 -114
  236. package/src/entry/tui/state.ts +0 -400
  237. package/src/entry/tui/stories.ts +0 -114
  238. package/src/entry/tui/text.ts +0 -130
  239. package/src/entry/tui/theme.ts +0 -140
  240. package/src/entry/tui/views/app.tsx +0 -208
  241. package/src/entry/tui/views/dialogs.tsx +0 -41
  242. package/src/entry/tui/views/profile-actions.ts +0 -331
  243. package/src/entry/tui/views/profiles.tsx +0 -520
  244. package/src/entry/tui/views/runs.tsx +0 -619
  245. package/src/entry/tui/views/save-dialog.tsx +0 -344
  246. package/src/entry/tui/views/status.tsx +0 -213
  247. package/src/entry/tui/widgets/chrome.tsx +0 -114
  248. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  249. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  250. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  251. package/src/entry/tui/widgets/dialog.tsx +0 -68
  252. package/src/entry/tui/widgets/line.tsx +0 -92
  253. package/src/entry/tui/widgets/list.tsx +0 -197
  254. package/src/entry/tui/widgets/spinner.tsx +0 -49
  255. package/src/entry/tui/widgets/toast.tsx +0 -62
  256. package/src/infra/sources/arena.ts +0 -95
  257. package/src/infra/sources/artificial-analysis.ts +0 -119
  258. package/src/infra/sources/cache.ts +0 -126
  259. package/src/infra/sources/csv.ts +0 -38
  260. package/src/infra/sources/epoch.ts +0 -82
  261. package/src/infra/sources/http.ts +0 -75
  262. package/src/infra/sources/litellm.ts +0 -58
  263. package/src/infra/sources/models-dev.ts +0 -59
  264. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  265. package/src/infra/sources/openrouter-models.ts +0 -60
  266. package/src/infra/sources/rows.ts +0 -38
  267. package/src/infra/sources/vectara.ts +0 -69
  268. package/src/infra/sources/zip.ts +0 -48
  269. package/src/services/catalog-refresh.ts +0 -201
  270. package/src/services/doctor-sources.ts +0 -76
  271. package/src/services/runs-page.ts +0 -290
  272. package/src/services/source-derive.ts +0 -397
  273. package/src/services/source-sync.ts +0 -312
  274. package/src/services/standins.ts +0 -238
  275. package/src/services/treat-likes.ts +0 -113
@@ -1,312 +0,0 @@
1
- import { buildCatalog, type Catalog, DIMS } from "../domain/catalog.ts";
2
- import { CatherdError, errorMessage } from "../domain/errors.ts";
3
- import {
4
- type Derived,
5
- SOURCE_IDS,
6
- type SourceId,
7
- type SourcesFile,
8
- SourcesFileSchema,
9
- } from "../domain/sources.ts";
10
- import { assetPath } from "../infra/assets.ts";
11
- import { log } from "../infra/log.ts";
12
- import { type AaAnswer, fetchArtificialAnalysis } from "../infra/sources/artificial-analysis.ts";
13
- import { fetchArena } from "../infra/sources/arena.ts";
14
- import {
15
- readCached,
16
- readDerived,
17
- readSyncState,
18
- tryLockSync,
19
- writeCached,
20
- writeDerived,
21
- writeSyncState,
22
- } from "../infra/sources/cache.ts";
23
- import { fetchEpoch } from "../infra/sources/epoch.ts";
24
- import { rateLimitRemaining, SourceError, type SourceTransport } from "../infra/sources/http.ts";
25
- import { fetchLiteLlm } from "../infra/sources/litellm.ts";
26
- import { fetchModelsDev } from "../infra/sources/models-dev.ts";
27
- import { fetchOpenRouterEndpoints } from "../infra/sources/openrouter-endpoints.ts";
28
- import { fetchOpenRouterModels } from "../infra/sources/openrouter-models.ts";
29
- import { fetchVectara } from "../infra/sources/vectara.ts";
30
- import { readVersioned } from "../infra/store.ts";
31
- import { readOverride, shippedModels, shippedScores } from "./catalog-service.ts";
32
- import { aaKey } from "./credentials.ts";
33
- import { derive, openRouterIds, type RawAnswers } from "./source-derive.ts";
34
-
35
- /** Spec 1.2 §3.2: one TTL for every source. */
36
- export const TTL_MS = 12 * 3_600_000;
37
- /** A source whose last attempt failed waits this long before a background sync tries it again. */
38
- const BACKOFF_MS = 3_600_000;
39
- /** How long a foreground sync waits for one already running. */
40
- const LOCK_WAIT_MS = 120_000;
41
-
42
- let sourcesFile: SourcesFile | null = null;
43
- /** `catalog/sources.json`: the sources, their licenses and attribution lines, the aliases. */
44
- export const shippedSources = (): SourcesFile =>
45
- (sourcesFile ??= readVersioned(assetPath("catalog/sources.json"), SourcesFileSchema, 1));
46
-
47
- export interface SyncOptions {
48
- /** ignore the TTL (and a failing source's backoff) */
49
- force?: boolean;
50
- /** the MCP server's boot sync: skips when another sync runs, and gives a failing source an hour's rest */
51
- background?: boolean;
52
- /** false: answer `busy` at once when another sync holds the lock (the catalog_sync tool), never wait */
53
- wait?: boolean;
54
- /** how every source is fetched; tests replace it */
55
- transport?: SourceTransport;
56
- /** the Artificial Analysis key; default: the env's, else the saved one */
57
- aaKey?: string | null;
58
- now?: () => number;
59
- }
60
-
61
- export interface SourceOutcome {
62
- source: SourceId;
63
- /** fetched now; fresh (within the TTL); failed (the last good answer kept); skipped (no key, or backing off) */
64
- state: "fetched" | "fresh" | "failed" | "skipped";
65
- /** when the answer in the cache was fetched; null when there is none */
66
- fetchedAt: string | null;
67
- error?: string;
68
- detail?: string;
69
- }
70
-
71
- export interface SyncReport {
72
- /** another sync held the lock (a background sync never waits): nothing was done */
73
- busy: boolean;
74
- sources: SourceOutcome[];
75
- /** canonical rungs with no value of their own before this sync and at least one after */
76
- newlyScored: string[];
77
- /** the user's treat-likes whose rung now has its own value on every dimension the stand-in lent */
78
- noLongerNeeded: { rung: string; like: string }[];
79
- failed: { source: SourceId; error: string }[];
80
- /** the cross-checks that failed (spec 1.2 §3.5) */
81
- warnings: string[];
82
- /** each source's ids no family matched (spec 1.2 §3.4) */
83
- unmatched: Record<string, string[]>;
84
- }
85
-
86
- const iso = (ms: number) => new Date(ms).toISOString();
87
-
88
- /** The catalog as routing would read it with `derived` as the synced layer. */
89
- const catalogWith = (derived: Derived | null, now: number): Catalog =>
90
- buildCatalog({
91
- models: shippedModels(),
92
- scores: shippedScores(),
93
- synced: derived?.scores,
94
- facts: derived?.facts,
95
- override: readOverride(),
96
- now,
97
- });
98
-
99
- const scored = (c: Catalog): Set<string> =>
100
- new Set(Object.entries(c.scores).flatMap(([rung, dims]) => (Object.keys(dims).length ? [rung] : [])));
101
-
102
- async function takeLock(background: boolean): Promise<(() => void) | null> {
103
- const deadline = Date.now() + LOCK_WAIT_MS;
104
- for (;;) {
105
- const release = tryLockSync();
106
- if (release || background) return release;
107
- if (Date.now() > deadline)
108
- throw new CatherdError("E_IO_LOCK", "another catalog sync is still running", {
109
- fix: "wait for it to finish, then run catherd catalog sync again",
110
- });
111
- await Bun.sleep(200);
112
- }
113
- }
114
-
115
- /** Every cached answer, as `derive` reads them (and `scripts/catalog-refresh.ts` rebuilds the shipped file). */
116
- export function cachedAnswers(): RawAnswers {
117
- const raw: RawAnswers = {};
118
- for (const id of SOURCE_IDS) {
119
- const c = readCached(id);
120
- if (c) raw[id] = { fetchedAt: c.fetchedAt, data: c.data };
121
- }
122
- return raw;
123
- }
124
-
125
- /**
126
- * Spec 1.2 §3.2, §3.3: fetches every source whose cached answer is older than the TTL (every source with
127
- * `force`), all in parallel, and writes each answer atomically with its fetch time. A source that fails keeps
128
- * its last good answer; the sync records the error and goes on. Then it derives the synced values from every
129
- * cached answer. One sync runs at a time: a background sync skips when another holds the lock, a foreground
130
- * one waits for it.
131
- */
132
- export async function syncSources(o: SyncOptions = {}): Promise<SyncReport> {
133
- const now = o.now ?? Date.now;
134
- const release = await takeLock(o.background === true || o.wait === false);
135
- if (!release)
136
- return {
137
- busy: true,
138
- sources: [],
139
- newlyScored: [],
140
- noLongerNeeded: [],
141
- failed: [],
142
- warnings: [],
143
- unmatched: {},
144
- };
145
- try {
146
- const before = catalogWith(readDerived(), now());
147
- const state = readSyncState();
148
- const key = o.aaKey === undefined ? aaKey() : o.aaKey;
149
- const t = o.transport ?? {};
150
- const outcomes = new Map<SourceId, SourceOutcome>();
151
- const fetchedAt = (id: SourceId) => state.sources[id]?.fetchedAt ?? null;
152
- const fresh = (id: SourceId) => {
153
- const at = fetchedAt(id);
154
- // a cache file that no longer reads (corrupt, or an older schema) is fetched again
155
- return !o.force && at !== null && now() - Date.parse(at) < TTL_MS && readCached(id) !== null;
156
- };
157
- const resting = (id: SourceId) => {
158
- const s = state.sources[id];
159
- return o.background && !o.force && s?.error && now() - Date.parse(s.lastAttemptAt) < BACKOFF_MS;
160
- };
161
-
162
- const run = async (id: SourceId, fetcher: () => Promise<unknown>): Promise<void> => {
163
- if (fresh(id)) return void outcomes.set(id, { source: id, state: "fresh", fetchedAt: fetchedAt(id) });
164
- if (resting(id))
165
- return void outcomes.set(id, {
166
- source: id,
167
- state: "skipped",
168
- fetchedAt: fetchedAt(id),
169
- detail: "failed within the hour; the next sync tries again",
170
- });
171
- const at = now();
172
- const prev = state.sources[id];
173
- try {
174
- const data = await fetcher();
175
- writeCached(id, data, at);
176
- const limit = id === "artificial-analysis" ? (data as AaAnswer).rateLimitRemaining : null;
177
- state.sources[id] = {
178
- fetchedAt: iso(at),
179
- lastAttemptAt: iso(at),
180
- error: null,
181
- rateLimitRemaining: limit,
182
- rateLimitAt: limit === null ? null : iso(at),
183
- };
184
- outcomes.set(id, { source: id, state: "fetched", fetchedAt: iso(at) });
185
- } catch (e) {
186
- const error = errorMessage(e);
187
- const limit = e instanceof SourceError ? rateLimitRemaining(e.headers) : null;
188
- // (1.2 minor) a failure with no header keeps the last count with the time that count was read
189
- state.sources[id] = {
190
- fetchedAt: prev?.fetchedAt ?? null,
191
- lastAttemptAt: iso(at),
192
- error,
193
- rateLimitRemaining: limit ?? prev?.rateLimitRemaining ?? null,
194
- rateLimitAt: limit === null ? (prev?.rateLimitAt ?? null) : iso(at),
195
- };
196
- outcomes.set(id, { source: id, state: "failed", fetchedAt: prev?.fetchedAt ?? null, error });
197
- log(o.background ? "debug" : "warn", "sources", { source: id, error });
198
- }
199
- };
200
-
201
- const orModels = run("openrouter-models", () => fetchOpenRouterModels(t));
202
- const jobs: Promise<void>[] = [
203
- orModels,
204
- run("models-dev", () => fetchModelsDev(t)),
205
- run("litellm", () => fetchLiteLlm(t)),
206
- run("arena", () => fetchArena(t)),
207
- run("vectara", () => fetchVectara(t)),
208
- run("epoch", () => fetchEpoch(t)),
209
- // plan 13 R-H: one request per catalog family OpenRouter lists, so it waits for OpenRouter's model list
210
- orModels.then(() =>
211
- run("openrouter-endpoints", async () => {
212
- const list = readCached("openrouter-models");
213
- if (!list) throw new Error("no OpenRouter model list to take the ids from");
214
- const ids = openRouterIds(list.data, { models: shippedModels(), sources: shippedSources() });
215
- return fetchOpenRouterEndpoints(ids, t);
216
- }),
217
- ),
218
- ];
219
- if (key) jobs.push(run("artificial-analysis", () => fetchArtificialAnalysis(key, t)));
220
- else
221
- outcomes.set("artificial-analysis", {
222
- source: "artificial-analysis",
223
- state: "skipped",
224
- fetchedAt: fetchedAt("artificial-analysis"),
225
- detail: "no key: catherd init, or export ARTIFICIAL_ANALYSIS_API_KEY",
226
- });
227
- await Promise.all(jobs);
228
- writeSyncState(state);
229
-
230
- const changed = [...outcomes.values()].some((x) => x.state === "fetched");
231
- const raw = cachedAnswers();
232
- if ((changed || readDerived() === null) && Object.keys(raw).length > 0)
233
- writeDerived(
234
- derive(raw, {
235
- models: shippedModels(),
236
- scores: shippedScores(),
237
- sources: shippedSources(),
238
- now: now(),
239
- }),
240
- );
241
- const derived = readDerived();
242
- const after = catalogWith(derived, now());
243
- const had = scored(before);
244
- const noLongerNeeded = Object.entries(after.treatLike).flatMap(([rung, t]) => {
245
- if (t.source !== "user") return [];
246
- const lends = DIMS.filter((d) => after.scores[t.like]?.[d]);
247
- const covered = (c: Catalog) => lends.length > 0 && lends.every((d) => c.scores[rung]?.[d]);
248
- return covered(after) && !covered(before) ? [{ rung, like: t.like }] : [];
249
- });
250
- const sources = SOURCE_IDS.map((id) => outcomes.get(id)).filter(
251
- (x): x is SourceOutcome => x !== undefined,
252
- );
253
- return {
254
- busy: false,
255
- sources,
256
- newlyScored: [...scored(after)].filter((r) => !had.has(r)).sort(),
257
- noLongerNeeded,
258
- failed: sources.flatMap((s) =>
259
- s.state === "failed" ? [{ source: s.source, error: s.error ?? "" }] : [],
260
- ),
261
- warnings: derived?.warnings ?? [],
262
- unmatched: derived?.unmatched ?? {},
263
- };
264
- } finally {
265
- release();
266
- }
267
- }
268
-
269
- /**
270
- * Spec 1.2 §3.2: the MCP server's boot sync, started without awaiting it. It never throws (a failure is
271
- * logged at debug) and never waits for another sync. `CATHERD_NO_SYNC=1` turns it off (tests, air-gapped
272
- * machines), as it does `init`'s sync; `catherd catalog sync` still runs.
273
- */
274
- export function backgroundSync(o: SyncOptions = {}): Promise<SyncReport | null> {
275
- if (process.env.CATHERD_NO_SYNC === "1") return Promise.resolve(null);
276
- return syncSources({ ...o, background: true }).catch((e: unknown) => {
277
- log("debug", "sources", { error: errorMessage(e) });
278
- return null;
279
- });
280
- }
281
-
282
- export interface SourceStatus {
283
- source: SourceId;
284
- name: string;
285
- /** when its cached answer was fetched; null when never */
286
- fetchedAt: string | null;
287
- error: string | null;
288
- }
289
-
290
- /** Spec 1.2 §9 doctor: each source's last fetch and error, the AA key, and AA's requests left. */
291
- export function sourcesStatus(): {
292
- sources: SourceStatus[];
293
- aaKey: boolean;
294
- rateLimitRemaining: number | null;
295
- rateLimitAt: string | null;
296
- } {
297
- const state = readSyncState();
298
- const aa = state.sources["artificial-analysis"];
299
- return {
300
- sources: shippedSources().sources.map((s) => ({
301
- source: s.id,
302
- name: s.name,
303
- fetchedAt: state.sources[s.id]?.fetchedAt ?? null,
304
- error: state.sources[s.id]?.error ?? null,
305
- })),
306
- aaKey: aaKey() !== null,
307
- rateLimitRemaining: aa?.rateLimitRemaining ?? null,
308
- // a state written before 1.5 has no rateLimitAt: its last attempt is the best guess
309
- rateLimitAt:
310
- aa?.rateLimitRemaining === null || aa === undefined ? null : (aa.rateLimitAt ?? aa.lastAttemptAt),
311
- };
312
- }
@@ -1,238 +0,0 @@
1
- import {
2
- type Catalog,
3
- DIMS,
4
- type Dim,
5
- type Family,
6
- type InferredStandIn,
7
- rungInfo,
8
- scoresOf,
9
- } from "../domain/catalog.ts";
10
- import { catalogRungs } from "../domain/failover.ts";
11
- import { EFFORT_ORDER } from "../domain/sources.ts";
12
-
13
- /**
14
- * Spec 1.2 §6.3: the features a rung is compared on. Numbers are z-scored across the catalog; `vendor` and
15
- * `family` are words, the same or not. Keyless: price, context, release date, vendor and family, the effort
16
- * (plan 14 Ruling 6), and the rung's own values on each dimension (the Arena and Epoch values, with the
17
- * shipped ones). With an Artificial Analysis key, its index, `hle`, `scicode`, `lcr`, cost per task and
18
- * tokens/s join them (`Catalog.features`).
19
- */
20
- export type Features = Record<string, number | string>;
21
-
22
- /** Spec 1.2 §6.3: a pair sharing fewer features than this is not suggested. */
23
- export const MIN_SHARED_FEATURES = 3;
24
-
25
- /**
26
- * Spec 1.5 plan 24: a rung stands in only when it has values of its own on at least this many dimensions. A
27
- * sparse row (GPT-6.1 Sol's one honesty figure) ranks near everything, for it has few features to differ on,
28
- * and would lend that one value to unrelated rungs.
29
- */
30
- export const MIN_STANDIN_DIMS = 3;
31
-
32
- export interface Suggestion {
33
- /** the canonical rung that would stand in */
34
- like: string;
35
- distance: number;
36
- /** the features the distance rests on: those both rungs have */
37
- features: string[];
38
- /** the dimensions it would lend: those the rung lacks and it has */
39
- lends: Dim[];
40
- }
41
-
42
- const DAY_MS = 86_400_000;
43
- const family = (c: Catalog, canonical: string): Family | null =>
44
- c.families.find((f) => f.id === canonical.slice(0, canonical.lastIndexOf("#"))) ?? null;
45
-
46
- /** The rung's own value per dimension: a value it borrows, or catherd's guess (`inferred`), never counts. */
47
- function ownValues(c: Catalog, canonical: string): Partial<Record<Dim, number>> {
48
- const out: Partial<Record<Dim, number>> = {};
49
- for (const d of DIMS) {
50
- const s = c.scores[canonical]?.[d];
51
- if (s && s.confidence !== "inferred") out[d] = s.value;
52
- }
53
- return out;
54
- }
55
-
56
- /** Spec 1.2 §6.3: a canonical rung's features, from its family, its effort, its own values and AA's. */
57
- export function featuresOf(c: Catalog, canonical: string): Features {
58
- const out: Features = {};
59
- const f = family(c, canonical);
60
- if (f) {
61
- out.price = Math.log10(f.price.input + f.price.output + 1e-6);
62
- const contexts = Object.values(f.on).flatMap((m) => (m ? [m.context] : []));
63
- if (contexts.length) out.context = Math.log10(Math.max(...contexts));
64
- if (f.releaseDate) out.release = Date.parse(f.releaseDate) / DAY_MS;
65
- const vendor = (f as { vendor?: unknown }).vendor;
66
- if (typeof vendor === "string") out.vendor = vendor;
67
- out.family = f.id;
68
- }
69
- const effort = (EFFORT_ORDER as readonly string[]).indexOf(canonical.slice(canonical.lastIndexOf("#") + 1));
70
- if (effort >= 0) out.effort = effort;
71
- for (const [d, v] of Object.entries(ownValues(c, canonical))) out[d] = v;
72
- for (const [k, v] of Object.entries(c.features[canonical] ?? {})) out[`aa.${k}`] = v;
73
- return out;
74
- }
75
-
76
- /** Every canonical rung the catalog can name, and every one it holds a value for. */
77
- export function canonicalRungs(c: Catalog): string[] {
78
- const out = new Set(Object.keys(c.scores));
79
- for (const r of catalogRungs(c)) {
80
- try {
81
- out.add(rungInfo(c, r).canonical);
82
- } catch {
83
- // catalogRungs only names parseable rungs
84
- }
85
- }
86
- return [...out].sort();
87
- }
88
-
89
- interface Scale {
90
- mean: number;
91
- sd: number;
92
- }
93
-
94
- /** Each numeric feature's mean and spread over `all`; a feature with no spread tells rungs apart by nothing. */
95
- function scales(all: Features[]): Map<string, Scale> {
96
- const values = new Map<string, number[]>();
97
- for (const f of all)
98
- for (const [k, v] of Object.entries(f))
99
- if (typeof v === "number") values.set(k, [...(values.get(k) ?? []), v]);
100
- const out = new Map<string, Scale>();
101
- for (const [k, xs] of values) {
102
- const mean = xs.reduce((s, x) => s + x, 0) / xs.length;
103
- const sd = Math.sqrt(xs.reduce((s, x) => s + (x - mean) ** 2, 0) / xs.length);
104
- if (sd > 0) out.set(k, { mean, sd });
105
- }
106
- return out;
107
- }
108
-
109
- /**
110
- * Spec 1.2 §6.3: the z-scored Euclidean distance over the features both have, as a root mean square so pairs
111
- * sharing different numbers of features compare (plan 14 Ruling 6); a word counts 0 when equal, else 1. Null
112
- * when they share fewer than MIN_SHARED_FEATURES.
113
- */
114
- function distance(a: Features, b: Features, z: Map<string, Scale>): { d: number; shared: string[] } | null {
115
- const shared: string[] = [];
116
- let sum = 0;
117
- for (const [k, va] of Object.entries(a)) {
118
- const vb = b[k];
119
- if (vb === undefined) continue;
120
- if (typeof va === "string" || typeof vb === "string") {
121
- shared.push(k);
122
- sum += va === vb ? 0 : 1;
123
- continue;
124
- }
125
- const s = z.get(k);
126
- if (!s) continue;
127
- shared.push(k);
128
- sum += ((va - vb) / s.sd) ** 2;
129
- }
130
- if (shared.length < MIN_SHARED_FEATURES) return null;
131
- return { d: Math.sqrt(sum / shared.length), shared };
132
- }
133
-
134
- /** The dimensions the bars use that `canonical` has no value for, of its own or through a treat-like. */
135
- export function missingDims(c: Catalog, canonical: string, dims: readonly Dim[] = barDimsIn(c)): Dim[] {
136
- const own = c.scores[canonical] ?? {};
137
- const like = c.treatLike[canonical]?.like;
138
- const lent = like ? (c.scores[like] ?? {}) : {};
139
- return dims.filter((d) => !own[d] && !lent[d]);
140
- }
141
-
142
- /**
143
- * Every dimension some bar of the catalog (the defaults, with the user's override) has a threshold on: the
144
- * dimensions a stand-in fills. No shipped bar uses `steer`, so it is never inferred by default; a user's steer
145
- * bar makes it one (plan 14 Ruling 7, as the code has always read it; spec 1.5 plan 24 aligns the words).
146
- */
147
- export function barDimsIn(c: Catalog): Dim[] {
148
- const used = new Set<string>();
149
- for (const kind of Object.values(c.bars))
150
- for (const bar of Object.values(kind))
151
- for (const [d, min] of Object.entries(bar)) if (min !== undefined) used.add(d);
152
- return DIMS.filter((d) => used.has(d));
153
- }
154
-
155
- class Ranker {
156
- private readonly all = new Map<string, Features>();
157
- private readonly z: Map<string, Scale>;
158
- constructor(private readonly c: Catalog) {
159
- for (const r of canonicalRungs(c)) this.all.set(r, featuresOf(c, r));
160
- this.z = scales([...this.all.values()]);
161
- }
162
- /**
163
- * The rungs with an own value on any of `dims` (and on at least MIN_STANDIN_DIMS dimensions in all), nearest
164
- * first; each with what it would lend.
165
- */
166
- rank(canonical: string, dims: readonly Dim[]): Suggestion[] {
167
- const mine = this.all.get(canonical) ?? featuresOf(this.c, canonical);
168
- const out: Suggestion[] = [];
169
- for (const [other, f] of this.all) {
170
- if (other === canonical) continue;
171
- const own = ownValues(this.c, other);
172
- if (Object.keys(own).length < MIN_STANDIN_DIMS) continue;
173
- const lends = dims.filter((d) => own[d] !== undefined);
174
- if (lends.length === 0) continue;
175
- const got = distance(mine, f, this.z);
176
- if (got) out.push({ like: other, distance: Number(got.d.toFixed(4)), features: got.shared, lends });
177
- }
178
- return out.sort((a, b) => a.distance - b.distance || a.like.localeCompare(b.like));
179
- }
180
- }
181
-
182
- /**
183
- * Spec 1.2 §6.4 `treat-like --suggest`: the `limit` nearest rungs that would lend `canonical` a value it lacks
184
- * on a dimension the bars use (any scored rung when it lacks none), with their distance and features.
185
- */
186
- export function suggestStandIns(c: Catalog, canonical: string, limit = 3): Suggestion[] {
187
- const lacking = missingDims(c, canonical);
188
- return new Ranker(c).rank(canonical, lacking.length ? lacking : DIMS).slice(0, limit);
189
- }
190
-
191
- /**
192
- * Spec 1.5 plan 24: the same effort of the family's predecessor (`Family.predecessor`), when it has a value on
193
- * `d` of its own; null otherwise.
194
- */
195
- function predecessorOf(c: Catalog, canonical: string, d: Dim): { like: string; value: number } | null {
196
- const pred = family(c, canonical)?.predecessor;
197
- if (!pred) return null;
198
- const like = `${pred}${canonical.slice(canonical.lastIndexOf("#"))}`;
199
- const s = c.scores[like]?.[d];
200
- return s ? { like, value: s.value } : null;
201
- }
202
-
203
- /**
204
- * Spec 1.2 §6.1: for every rung the catalog can name, per dimension the bars use that it has no value for
205
- * (of its own or through a treat-like), its nearest stand-in with a value there. A rung no rung is near
206
- * enough to (fewer than MIN_SHARED_FEATURES shared) gets none on that dimension. Spec 1.5 plan 24: a new
207
- * release of a family line takes at least its predecessor's value at the same effort, so a guess never puts it
208
- * below the model it replaces (the identity run's GPT-6.1 Sol at 37.2, under GPT-6 Luna).
209
- */
210
- export function inferStandIns(c: Catalog): Catalog["inferred"] {
211
- const dims = barDimsIn(c);
212
- const ranker = new Ranker(c);
213
- const out: Catalog["inferred"] = {};
214
- for (const canonical of canonicalRungs(c)) {
215
- const lacking = missingDims(c, canonical, dims);
216
- for (const d of lacking) {
217
- const best = ranker.rank(canonical, [d])[0];
218
- const pred = predecessorOf(c, canonical, d);
219
- const nearest = best ? c.scores[best.like]?.[d]?.value : undefined;
220
- let entry: InferredStandIn | null = best
221
- ? { like: best.like, distance: best.distance, features: best.features }
222
- : null;
223
- if (pred && (nearest === undefined || pred.value >= nearest))
224
- entry = { like: pred.like, distance: 0, features: ["predecessor"] };
225
- if (entry) (out[canonical] ??= {})[d] = entry;
226
- }
227
- }
228
- return out;
229
- }
230
-
231
- /** The catalog with its inferred stand-ins filled in: what routing, validation and the surfaces read. */
232
- export function withStandIns(c: Catalog): Catalog {
233
- return { ...c, inferred: inferStandIns(c) };
234
- }
235
-
236
- /** Whether `canonical` leans on an inferred stand-in for any value (`scoresOf`'s `inferred`). */
237
- export const leansOnStandIn = (c: Catalog, canonical: string): boolean =>
238
- (scoresOf(c, canonical)?.inferred.length ?? 0) > 0;
@@ -1,113 +0,0 @@
1
- import type { OrchestrationHost } from "../domain/host.ts";
2
- import { type Dim, scoresOf, rungInfo } from "../domain/catalog.ts";
3
- import { CatherdError } from "../domain/errors.ts";
4
- import { tryParseRung } from "../domain/ids.ts";
5
- import { ROLES } from "../domain/roles.ts";
6
- import { canonicalRung, loadCatalog, readOverride, removeTreatLikes } from "./catalog-service.ts";
7
- import { getProfile, listProfiles } from "./profile-store.ts";
8
- import { barDimsIn, type Suggestion, suggestStandIns } from "./standins.ts";
9
-
10
- /** A profile's rung that would lean on an inferred stand-in, or be left with no value at all (spec 1.2 §6.4). */
11
- export interface LeftOnStandIn {
12
- profile: string;
13
- rung: string;
14
- /** the dimensions an inferred stand-in would fill; empty when the rung is left unscored */
15
- dims: Dim[];
16
- /** no value of its own and no stand-in near enough: routing skips it */
17
- unscored: boolean;
18
- /**
19
- * (1.2 minor) the bar dimensions it keeps no value on at all, though it keeps others: present only when some
20
- * are; no bar that needs one is cleared
21
- */
22
- missing?: Dim[];
23
- }
24
-
25
- /**
26
- * Spec 1.2 §6.4: the rungs of every profile (enabled roles' rungs and failover stand-ins) that would lean on an
27
- * inferred stand-in, or be left with no value at all, once the user's treat-likes for `removed` (canonical
28
- * rungs, or all of them) are gone. Never removed silently.
29
- */
30
- export function leftOnStandIns(
31
- removed: string[] | "all",
32
- host: OrchestrationHost = "unknown",
33
- ): LeftOnStandIn[] {
34
- const cur = readOverride();
35
- const treatLike = Object.fromEntries(
36
- Object.entries(cur.treatLike).filter(([r]) => removed !== "all" && !removed.includes(r)),
37
- );
38
- const gone = removed === "all" ? Object.keys(cur.treatLike) : removed;
39
- const c = loadCatalog({ timings: false, override: { ...cur, treatLike } });
40
- const out: LeftOnStandIn[] = [];
41
- // an unknown host resolves no omitted architect/verifier rungs: name what either host would run
42
- const hosts: OrchestrationHost[] = host === "unknown" ? ["claude-code", "codex"] : [host];
43
- for (const profile of listProfiles()) {
44
- const rungs = new Set(
45
- hosts.flatMap((h) => {
46
- const p = getProfile(profile, h);
47
- return [
48
- ...ROLES.filter((r) => p.roles[r].enabled).flatMap((r) => p.roles[r].rungs),
49
- ...Object.values(p.failover),
50
- ];
51
- }),
52
- );
53
- for (const rung of rungs) {
54
- if (!tryParseRung(rung)) continue;
55
- const canonical = rungInfo(c, rung).canonical;
56
- if (!gone.includes(canonical)) continue;
57
- const s = scoresOf(c, canonical);
58
- if (!s) {
59
- out.push({ profile, rung, dims: [], unscored: true });
60
- continue;
61
- }
62
- const missing = barDimsIn(c).filter((d) => s.values[d] === undefined);
63
- if (s.inferred.length || missing.length)
64
- out.push({
65
- profile,
66
- rung,
67
- dims: s.inferred,
68
- unscored: false,
69
- ...(missing.length ? { missing } : {}),
70
- });
71
- }
72
- }
73
- return out;
74
- }
75
-
76
- /** Spec 1.2 §6.4 `--suggest`: the rung, in canonical form, and its three nearest stand-ins. */
77
- export function suggestFor(rung: string): { canonical: string; lacking: Dim[]; suggestions: Suggestion[] } {
78
- const c = loadCatalog({ timings: false });
79
- const canonical = canonicalRung(rung);
80
- const s = scoresOf(c, canonical);
81
- return {
82
- canonical,
83
- lacking: s ? s.inferred : [],
84
- suggestions: suggestStandIns(c, canonical),
85
- };
86
- }
87
-
88
- /**
89
- * Spec 1.2 §6.4 `--clear`: removes the user's treat-like for `rung`, naming first the profile rungs it
90
- * leaves on an inferred stand-in. A shipped treat-like is not the user's to remove.
91
- */
92
- export async function clearTreatLike(
93
- rung: string,
94
- host: OrchestrationHost = "unknown",
95
- ): Promise<{ rung: string; like: string; left: LeftOnStandIn[] }> {
96
- const canonical = canonicalRung(rung);
97
- const like = readOverride().treatLike[canonical];
98
- if (!like)
99
- throw new CatherdError("E_INPUT_INVALID", `${canonical} has no treat-like of yours to clear`, {
100
- fix: "catherd catalog list shows each rung's treat-like; a shipped one is not yours to clear",
101
- });
102
- const left = leftOnStandIns([canonical], host);
103
- await removeTreatLikes([canonical]);
104
- return { rung: canonical, like, left };
105
- }
106
-
107
- /** Spec 1.2 §6.4 `--reset`: removes every treat-like of the user's, naming first the rungs left on a stand-in. */
108
- export async function resetTreatLikes(
109
- host: OrchestrationHost = "unknown",
110
- ): Promise<{ removed: [string, string][]; left: LeftOnStandIn[] }> {
111
- const left = leftOnStandIns("all", host);
112
- return { removed: await removeTreatLikes("all"), left };
113
- }