catherd-cli 1.4.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 (320) hide show
  1. package/README.md +188 -169
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -239
  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 -356
  11. package/plugin/skills/catherd-setup/SKILL.md +21 -20
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +19 -4
  14. package/src/adapters/claude-code/index.ts +126 -14
  15. package/src/adapters/codex/events.ts +10 -0
  16. package/src/adapters/codex/index.ts +17 -5
  17. package/src/adapters/cursor/home.ts +1 -1
  18. package/src/adapters/cursor/index.ts +10 -4
  19. package/src/adapters/opencode/index.ts +38 -1
  20. package/src/cli.ts +27 -27
  21. package/src/domain/agents.ts +3 -1
  22. package/src/domain/bars.ts +33 -107
  23. package/src/domain/blocked.ts +72 -0
  24. package/src/domain/branch.ts +51 -0
  25. package/src/domain/brief.ts +106 -0
  26. package/src/domain/catalog.ts +69 -313
  27. package/src/domain/changes.ts +23 -1
  28. package/src/domain/cost.ts +19 -29
  29. package/src/domain/errors.ts +6 -2
  30. package/src/domain/fail-lines.ts +26 -0
  31. package/src/domain/failover.ts +37 -75
  32. package/src/domain/findings.ts +47 -0
  33. package/src/domain/gate-brief.ts +53 -0
  34. package/src/domain/hints.ts +13 -2
  35. package/src/domain/ids.ts +13 -2
  36. package/src/domain/jev.ts +47 -2
  37. package/src/domain/judge.ts +124 -0
  38. package/src/domain/lane.ts +118 -3
  39. package/src/domain/milestone-memory.ts +53 -0
  40. package/src/domain/notice.ts +32 -8
  41. package/src/domain/notify.ts +32 -0
  42. package/src/domain/plan-doc.ts +198 -0
  43. package/src/domain/plan-lint.ts +141 -0
  44. package/src/domain/profile-rules.ts +175 -138
  45. package/src/domain/profile.ts +140 -63
  46. package/src/domain/record.ts +68 -3
  47. package/src/domain/reply.ts +29 -0
  48. package/src/domain/role-prompts.ts +182 -56
  49. package/src/domain/role-scope.ts +76 -0
  50. package/src/domain/role-tools.ts +34 -0
  51. package/src/domain/roles.ts +2 -0
  52. package/src/domain/route.ts +90 -0
  53. package/src/domain/rules.ts +33 -0
  54. package/src/domain/run-events.ts +298 -0
  55. package/src/domain/run-machine.ts +668 -0
  56. package/src/domain/run-state.ts +1310 -0
  57. package/src/domain/scorecard.ts +197 -0
  58. package/src/domain/select.ts +229 -72
  59. package/src/domain/slopalytics.ts +72 -0
  60. package/src/domain/state.ts +5 -0
  61. package/src/domain/util.ts +22 -0
  62. package/src/domain/workspace.ts +130 -0
  63. package/src/entry/answer-command.ts +39 -0
  64. package/src/entry/catalog-command.ts +24 -165
  65. package/src/entry/check-bin.ts +17 -0
  66. package/src/entry/cli-kit.ts +18 -2
  67. package/src/entry/daemon-bin.ts +22 -0
  68. package/src/entry/deps.ts +2 -0
  69. package/src/entry/doctor-command.ts +20 -2
  70. package/src/entry/glyphs.ts +15 -2
  71. package/src/entry/host-arg.ts +28 -0
  72. package/src/entry/init-command.ts +20 -69
  73. package/src/entry/knowledge-command.ts +121 -5
  74. package/src/entry/lock-command.ts +103 -8
  75. package/src/entry/mcp/handshake.ts +6 -2
  76. package/src/entry/mcp/role-bin.ts +20 -0
  77. package/src/entry/mcp/role-server.ts +134 -0
  78. package/src/entry/mcp/server.ts +85 -21
  79. package/src/entry/mcp/session-tools.ts +149 -0
  80. package/src/entry/panel/openapi.ts +189 -0
  81. package/src/entry/panel/server.ts +410 -0
  82. package/src/entry/panel-command.ts +68 -0
  83. package/src/entry/pause-command.ts +67 -0
  84. package/src/entry/profile-command.ts +33 -18
  85. package/src/entry/role-cli-command.ts +159 -0
  86. package/src/entry/runs-command.ts +81 -27
  87. package/src/entry/scorecard-command.ts +187 -0
  88. package/src/entry/supervise-bin.ts +1 -1
  89. package/src/entry/supervise-command.ts +30 -3
  90. package/src/entry/workspace-command.ts +44 -0
  91. package/src/infra/boot-lock.ts +25 -0
  92. package/src/infra/delivery.ts +3 -0
  93. package/src/infra/dispatch-dir.ts +2 -0
  94. package/src/infra/env.ts +11 -3
  95. package/src/infra/git.ts +70 -0
  96. package/src/infra/heavy-lock.ts +133 -6
  97. package/src/infra/host-probe.ts +16 -0
  98. package/src/infra/launch.ts +63 -7
  99. package/src/infra/lock-activity.ts +88 -0
  100. package/src/infra/login-env.ts +48 -0
  101. package/src/infra/memory.ts +45 -0
  102. package/src/infra/ntfy.ts +40 -0
  103. package/src/infra/open-url.ts +42 -0
  104. package/src/infra/paths.ts +37 -2
  105. package/src/infra/role-mcp.ts +69 -0
  106. package/src/infra/slopalytics.ts +30 -0
  107. package/src/infra/store.ts +6 -1
  108. package/src/infra/supervisor.ts +107 -16
  109. package/src/services/admission.ts +133 -31
  110. package/src/services/backends.ts +4 -6
  111. package/src/services/budget.ts +26 -6
  112. package/src/services/capture.ts +1 -22
  113. package/src/services/catalog-service.ts +155 -254
  114. package/src/services/credentials.ts +15 -26
  115. package/src/services/daemon.ts +181 -0
  116. package/src/services/dispatch-service.ts +209 -17
  117. package/src/services/dispatches.ts +30 -2
  118. package/src/services/doctor-access.ts +8 -3
  119. package/src/services/doctor-backends.ts +1 -1
  120. package/src/services/doctor-checks.ts +47 -0
  121. package/src/services/doctor-docker.ts +221 -0
  122. package/src/services/doctor-role-mcp.ts +65 -0
  123. package/src/services/doctor.ts +72 -15
  124. package/src/services/end-push.ts +157 -0
  125. package/src/services/failover-rungs.ts +109 -0
  126. package/src/services/finalize.ts +24 -2
  127. package/src/services/gate-env.ts +217 -0
  128. package/src/services/gate-service.ts +231 -45
  129. package/src/services/global-install.ts +46 -13
  130. package/src/services/jev-service.ts +2 -4
  131. package/src/services/lane-edit.ts +154 -0
  132. package/src/services/lane-service.ts +291 -55
  133. package/src/services/milestone-check.ts +433 -0
  134. package/src/services/milestone-tree.ts +170 -0
  135. package/src/services/milestones.ts +167 -29
  136. package/src/services/notifier.ts +124 -4
  137. package/src/services/notify-service.ts +26 -0
  138. package/src/services/orchestrator-wait.ts +82 -0
  139. package/src/services/panel-setup.ts +116 -0
  140. package/src/services/pause.ts +161 -0
  141. package/src/services/peek.ts +79 -17
  142. package/src/services/plan-input.ts +151 -0
  143. package/src/services/ports.ts +60 -5
  144. package/src/services/preflight.ts +232 -24
  145. package/src/services/profile-service.ts +31 -3
  146. package/src/services/profile-store.ts +18 -6
  147. package/src/services/protocol.ts +123 -25
  148. package/src/services/provenance.ts +34 -89
  149. package/src/services/questions.ts +24 -0
  150. package/src/services/reconcile.ts +33 -1
  151. package/src/services/reentry.ts +7 -6
  152. package/src/services/role-access.ts +27 -0
  153. package/src/services/routing-service.ts +161 -42
  154. package/src/services/run-briefs.ts +477 -0
  155. package/src/services/run-client.ts +217 -0
  156. package/src/services/run-effects.ts +837 -0
  157. package/src/services/run-events-store.ts +174 -0
  158. package/src/services/run-evidence.ts +1 -1
  159. package/src/services/run-pin.ts +125 -0
  160. package/src/services/run-read.ts +221 -0
  161. package/src/services/run-report.ts +176 -0
  162. package/src/services/run-service.ts +204 -23
  163. package/src/services/run-store.ts +159 -18
  164. package/src/services/scorecard-metrics.ts +86 -0
  165. package/src/services/scorecard.ts +382 -0
  166. package/src/services/scratch.ts +60 -0
  167. package/src/services/sessions.ts +3 -1
  168. package/src/services/state.ts +21 -0
  169. package/src/services/summary.ts +76 -12
  170. package/src/services/tree-path.ts +27 -0
  171. package/src/services/verifier-brief.ts +23 -0
  172. package/src/services/verifier-step.ts +48 -0
  173. package/src/services/workspace-admission.ts +259 -0
  174. package/src/services/workspace-service.ts +259 -0
  175. package/src/services/workspace-store.ts +95 -0
  176. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  177. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  178. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  179. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  180. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  181. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  182. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  183. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  184. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  185. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  186. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  187. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  188. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  189. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  190. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  191. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  192. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  193. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  194. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  195. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  196. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  197. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  198. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  199. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  200. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  201. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  202. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  203. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  204. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  205. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  206. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  207. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  208. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  209. package/web/dist/assets/index-C5IbylvM.css +2 -0
  210. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  211. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  212. package/web/dist/assets/label-DFSYBc62.js +1 -0
  213. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  214. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  215. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  216. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  217. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  218. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  219. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  220. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  221. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  222. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  223. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  224. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  225. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  226. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  227. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  228. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  229. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  230. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  231. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  232. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  233. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  234. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  235. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  236. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  237. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  238. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  239. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  240. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  241. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  242. package/web/dist/assets/time--9kSQbbp.js +1 -0
  243. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  244. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  245. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  246. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  247. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  248. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  249. package/web/dist/index.html +15 -0
  250. package/catalog/ATTRIBUTION.md +0 -36
  251. package/catalog/scores.json +0 -3202
  252. package/catalog/sources.json +0 -97
  253. package/src/adapters/antigravity/events.ts +0 -131
  254. package/src/adapters/antigravity/home.ts +0 -46
  255. package/src/adapters/antigravity/index.ts +0 -320
  256. package/src/adapters/antigravity/models.ts +0 -37
  257. package/src/adapters/grok/events.ts +0 -112
  258. package/src/adapters/grok/home.ts +0 -132
  259. package/src/adapters/grok/index.ts +0 -339
  260. package/src/adapters/grok/models.ts +0 -48
  261. package/src/domain/calibration.ts +0 -125
  262. package/src/domain/sources.ts +0 -144
  263. package/src/entry/mcp/dispatch-tools.ts +0 -52
  264. package/src/entry/mcp/lane-tools.ts +0 -84
  265. package/src/entry/mcp/protocol-tools.ts +0 -57
  266. package/src/entry/mcp/run-tools.ts +0 -148
  267. package/src/entry/mcp/setup-tools.ts +0 -108
  268. package/src/entry/tui/commands.ts +0 -563
  269. package/src/entry/tui/effects.ts +0 -381
  270. package/src/entry/tui/fixtures.ts +0 -637
  271. package/src/entry/tui/profile-edits.ts +0 -146
  272. package/src/entry/tui/profile-tree.ts +0 -429
  273. package/src/entry/tui/providers/app.tsx +0 -162
  274. package/src/entry/tui/providers/boundary.tsx +0 -57
  275. package/src/entry/tui/providers/data.tsx +0 -160
  276. package/src/entry/tui/providers/keymap.tsx +0 -166
  277. package/src/entry/tui/providers/root.tsx +0 -45
  278. package/src/entry/tui/providers/theme.tsx +0 -17
  279. package/src/entry/tui/providers/toast.tsx +0 -77
  280. package/src/entry/tui/run.tsx +0 -114
  281. package/src/entry/tui/state.ts +0 -373
  282. package/src/entry/tui/stories.ts +0 -114
  283. package/src/entry/tui/text.ts +0 -130
  284. package/src/entry/tui/theme.ts +0 -140
  285. package/src/entry/tui/views/app.tsx +0 -208
  286. package/src/entry/tui/views/dialogs.tsx +0 -41
  287. package/src/entry/tui/views/profile-actions.ts +0 -331
  288. package/src/entry/tui/views/profiles.tsx +0 -520
  289. package/src/entry/tui/views/runs.tsx +0 -573
  290. package/src/entry/tui/views/save-dialog.tsx +0 -340
  291. package/src/entry/tui/views/status.tsx +0 -209
  292. package/src/entry/tui/widgets/chrome.tsx +0 -114
  293. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  294. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  295. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  296. package/src/entry/tui/widgets/dialog.tsx +0 -68
  297. package/src/entry/tui/widgets/line.tsx +0 -92
  298. package/src/entry/tui/widgets/list.tsx +0 -197
  299. package/src/entry/tui/widgets/spinner.tsx +0 -49
  300. package/src/entry/tui/widgets/toast.tsx +0 -62
  301. package/src/infra/sources/arena.ts +0 -87
  302. package/src/infra/sources/artificial-analysis.ts +0 -112
  303. package/src/infra/sources/cache.ts +0 -124
  304. package/src/infra/sources/csv.ts +0 -38
  305. package/src/infra/sources/epoch.ts +0 -82
  306. package/src/infra/sources/http.ts +0 -75
  307. package/src/infra/sources/litellm.ts +0 -58
  308. package/src/infra/sources/models-dev.ts +0 -59
  309. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  310. package/src/infra/sources/openrouter-models.ts +0 -60
  311. package/src/infra/sources/rows.ts +0 -38
  312. package/src/infra/sources/vectara.ts +0 -69
  313. package/src/infra/sources/zip.ts +0 -48
  314. package/src/services/catalog-refresh.ts +0 -201
  315. package/src/services/doctor-sources.ts +0 -76
  316. package/src/services/runs-page.ts +0 -255
  317. package/src/services/source-derive.ts +0 -374
  318. package/src/services/source-sync.ts +0 -305
  319. package/src/services/standins.ts +0 -204
  320. package/src/services/treat-likes.ts +0 -97
@@ -1,108 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { z } from "zod";
3
- import { CatherdError } from "../../domain/errors.ts";
4
- import { PROFILE_NAME, ProfilePatchSchema } from "../../domain/profile.ts";
5
- import { ROLES } from "../../domain/roles.ts";
6
- import { gitToplevel } from "../../infra/git.ts";
7
- import type { Deps } from "../../services/ports.ts";
8
- import { syncSources } from "../../services/source-sync.ts";
9
- import { handle } from "./result.ts";
10
-
11
- const PROFILE = z.string().regex(PROFILE_NAME).optional();
12
- const REPO = z.string().min(1).optional();
13
-
14
- /**
15
- * The git toplevel of `repo` (default: this server's directory, null outside a repository). A `repo`
16
- * given outside a repository is refused, as run_start does: never the global profile in its stead.
17
- */
18
- const toplevel = async (repo: string | undefined): Promise<string | null> => {
19
- const top = await gitToplevel(repo ?? process.cwd());
20
- if (top === null && repo !== undefined)
21
- throw new CatherdError("E_IO_PATH", `${repo} is not inside a git repository`, {
22
- fix: "pass the path of the repository, or leave repo out for the active profile",
23
- });
24
- return top;
25
- };
26
-
27
- export function registerSetupTools(server: McpServer, deps: Deps): void {
28
- server.registerTool(
29
- "catalog_query",
30
- {
31
- description:
32
- "Models catherd can place, with capabilities, the roles they can fill, their scored rungs (backend:model#effort), any 'treat like', their cost under the billing of `repo`'s profile, and whether this account's last listing offers them (`listed: false`: it does not); `enabled: false` rungs are unscored. Each score has its confidence, source and date (`from` names the rung a lent value belongs to); each model its price and speed facts, each rung catherd's own run evidence. Scored models first. opencode's models are as listed in `repo` (default: this server's directory), as route sees them there.",
33
- inputSchema: {
34
- repo: z.string().min(1).optional(),
35
- role: z.enum(ROLES).optional(),
36
- backend: z.string().optional(),
37
- text: z.string().optional(),
38
- scored_only: z.boolean().default(false),
39
- limit: z.number().int().min(1).max(500).default(50),
40
- },
41
- },
42
- (a) =>
43
- handle(async () => {
44
- // outside a git repository: the global listings and the active profile
45
- const repo = (await gitToplevel(a.repo ?? process.cwd())) ?? undefined;
46
- return deps.routing.catalog(
47
- { role: a.role, backend: a.backend, text: a.text, scoredOnly: a.scored_only, limit: a.limit, repo },
48
- deps.profiles.forRepo(repo ?? null).billing,
49
- );
50
- }),
51
- );
52
-
53
- server.registerTool(
54
- "catalog_sync",
55
- {
56
- description:
57
- "Fetch the public model facts and scores now (models.dev, OpenRouter, LiteLLM, Arena, Vectara, Epoch AI, and Artificial Analysis with the user's key): each source at most every 12 hours unless `force`. Returns the rungs newly scored, the user's treat-likes no longer needed (their rung now has values of its own), the sources that failed (each keeps its last good answer) and each source's state. route and catalog_query read the result at once.",
58
- inputSchema: { force: z.boolean().default(false) },
59
- },
60
- (a) =>
61
- handle(async () => {
62
- const r = await (deps.sync ?? ((o) => syncSources(o)))({ force: a.force });
63
- return {
64
- newlyScored: r.newlyScored,
65
- standInsNoLongerNeeded: r.noLongerNeeded,
66
- failed: r.failed,
67
- sources: r.sources,
68
- warnings: r.warnings,
69
- ...(r.busy ? { busy: "another sync was running; call catalog_sync again" } : {}),
70
- };
71
- }),
72
- );
73
-
74
- server.registerTool(
75
- "profile_get",
76
- {
77
- description:
78
- "A profile (without a name: the profile this repo runs on, i.e. the one bound to `repo`, default this server's directory, else the active one) as the run engine reads it, with every default filled in: per role its access mode, rungs and default rung, and whether its backend enforces the access (enforcement); billing, jev, harness isolation, failover, budget, timeouts, preflight, lock and notify. Also `active` (the global active profile), `here` (the profile this repo runs on) and every name.",
79
- inputSchema: { name: PROFILE, repo: REPO, raw: z.boolean().optional() },
80
- },
81
- (a) =>
82
- handle(async () =>
83
- a.raw && deps.profiles.raw
84
- ? deps.profiles.raw(a.name, await toplevel(a.repo))
85
- : deps.profiles.get(a.name, await toplevel(a.repo)),
86
- ),
87
- );
88
-
89
- server.registerTool(
90
- "profile_validate",
91
- {
92
- description:
93
- "Check a profile (without a name: the profile this repo runs on, as in profile_get). errors block a save: the worker disabled, an enabled role with no usable rung, an unscored rung without a 'treat like', a failover stand-in unscored or on the same quota, a backend catherd cannot run. warnings do not: an access mode other than the role's default, a model the backend's listing lacks, a stand-in that never runs. Each has a path, a message and often a fix.",
94
- inputSchema: { name: PROFILE, repo: REPO },
95
- },
96
- (a) => handle(async () => deps.profiles.validate(a.name, await toplevel(a.repo))),
97
- );
98
-
99
- server.registerTool(
100
- "profile_set",
101
- {
102
- description:
103
- "Apply a patch to a profile (without a name: the profile this repo runs on, as in profile_get; a new name starts from the default profile): objective, jev.use, billing, roles (enabled, access, network, rungs, defaultRung), harness isolation per backend, failover, budget, timeouts, preflight.confirm, lock.heavy and notify. Lists replace, maps merge, and null removes a key. An unknown key is refused. It validates first and writes nothing when invalid, unless the patch repairs an invalid profile: it fixes at least one of its errors and adds none, and then it saves with `errors` listing those still open. A save rewrites the agent files and relinks them. Returns saved, the errors and warnings, the diff, and newSessionNeededFor: the agents that apply from the next Claude Code session.",
104
- inputSchema: { name: PROFILE, repo: REPO, patch: ProfilePatchSchema },
105
- },
106
- (a) => handle(async () => deps.profiles.set(a.name, a.patch, await toplevel(a.repo))),
107
- );
108
- }
@@ -1,563 +0,0 @@
1
- // Command ids and default keys follow anomalyco/opencode packages/tui/src/config/keybind.ts
2
- // (MIT, © 2025 opencode); see THIRD_PARTY_NOTICES.md.
3
- import { z } from "zod";
4
- import { CatherdError } from "../../domain/errors.ts";
5
-
6
- /**
7
- * Where a command's keys are live (spec §9.2's context stack global → tab → pane → dialog → input).
8
- * - `global`: in every mode, dialogs included.
9
- * - `app`, `tab.*`, `list`: the base mode (no dialog open); their printable keys only while no text input
10
- * has focus.
11
- * - `row.*`: the base mode, and only while no text input has focus (their keys act on the selected row).
12
- * - `filter`: only while a list's filter input has focus.
13
- * - `dialog`: only while a dialog is open (the keymap's modal mode).
14
- */
15
- export const SCOPES = [
16
- "global",
17
- "app",
18
- "tab.status",
19
- "tab.profiles",
20
- "tab.runs",
21
- "list",
22
- "row.status",
23
- "row.profiles",
24
- "row.runs",
25
- "filter",
26
- "dialog",
27
- ] as const;
28
- export type Scope = (typeof SCOPES)[number];
29
-
30
- type Group = "App" | "Tabs" | "Profiles" | "Edit" | "Status" | "Runs" | "Lists" | "Dialogs";
31
-
32
- export interface CommandDef {
33
- id: string;
34
- title: string;
35
- group: Group;
36
- scope: Scope;
37
- /** default keys, in the keymap's syntax: `ctrl+s`, `shift+g`, `<leader>n`, `space`, `return` */
38
- keys: readonly string[];
39
- /** listed in the command palette */
40
- palette?: true;
41
- /** listed under "Suggested" while the palette's filter is empty */
42
- suggested?: true;
43
- /** footer order; commands without one are never hinted */
44
- hint?: number;
45
- /** the footer's label, when the title is too long for it */
46
- short?: string;
47
- /** the CLI command that does the same, shown in the palette (research C3: the TUI teaches the CLI) */
48
- cli?: string;
49
- }
50
-
51
- /** The leader key and its timeout (spec §9.2: `ctrl+x`, 2 s, as in opencode). */
52
- export const LEADER = "ctrl+x";
53
- export const LEADER_TIMEOUT_MS = 2_000;
54
-
55
- /** Every action in the TUI. The palette, footer, help and keys all come from this one table. */
56
- export const COMMANDS = [
57
- // global: live in every mode
58
- { id: "app.interrupt", title: "Cancel, or quit", group: "App", scope: "global", keys: ["ctrl+c"] },
59
- // hinted only while a dialog is open (the Footer), where esc cancels it
60
- {
61
- id: "app.back",
62
- title: "Back",
63
- group: "App",
64
- scope: "global",
65
- keys: ["escape"],
66
- hint: 80,
67
- short: "cancel",
68
- },
69
-
70
- // app: any tab, no dialog open
71
- {
72
- id: "app.palette",
73
- title: "Command palette",
74
- group: "App",
75
- scope: "app",
76
- keys: ["ctrl+p", ":"],
77
- hint: 90,
78
- short: "commands",
79
- },
80
- {
81
- id: "app.help",
82
- title: "Keyboard shortcuts",
83
- group: "App",
84
- scope: "app",
85
- keys: ["?"],
86
- palette: true,
87
- hint: 91,
88
- short: "help",
89
- },
90
- { id: "app.quit", title: "Quit", group: "App", scope: "app", keys: ["q", "<leader>q"], palette: true },
91
- {
92
- id: "app.stories",
93
- title: "Stories…",
94
- group: "App",
95
- scope: "app",
96
- keys: [],
97
- palette: true,
98
- suggested: true,
99
- },
100
- {
101
- id: "tab.status",
102
- title: "Go to Status",
103
- group: "Tabs",
104
- scope: "app",
105
- keys: ["1", "<leader>1"],
106
- palette: true,
107
- },
108
- {
109
- id: "tab.profiles",
110
- title: "Go to Profiles",
111
- group: "Tabs",
112
- scope: "app",
113
- keys: ["2", "<leader>2"],
114
- palette: true,
115
- },
116
- {
117
- id: "tab.runs",
118
- title: "Go to Runs",
119
- group: "Tabs",
120
- scope: "app",
121
- keys: ["3", "<leader>3"],
122
- palette: true,
123
- },
124
- { id: "tab.next", title: "Next tab", group: "Tabs", scope: "app", keys: ["]"] },
125
- { id: "tab.prev", title: "Previous tab", group: "Tabs", scope: "app", keys: ["["] },
126
- {
127
- id: "profile.new",
128
- title: "New profile…",
129
- group: "Profiles",
130
- scope: "app",
131
- keys: ["<leader>n"],
132
- palette: true,
133
- suggested: true,
134
- cli: "catherd profile new <name>",
135
- },
136
- {
137
- id: "profile.list",
138
- title: "Switch profile…",
139
- group: "Profiles",
140
- scope: "app",
141
- keys: ["<leader>l"],
142
- palette: true,
143
- suggested: true,
144
- cli: "catherd profile list",
145
- },
146
-
147
- // tab.status
148
- {
149
- id: "status.recheck",
150
- title: "Re-check setup",
151
- group: "Status",
152
- scope: "tab.status",
153
- keys: ["r"],
154
- palette: true,
155
- suggested: true,
156
- hint: 3,
157
- short: "re-check",
158
- cli: "catherd doctor",
159
- },
160
- {
161
- id: "status.copy",
162
- title: "Copy the fix command",
163
- group: "Status",
164
- scope: "tab.status",
165
- keys: ["y"],
166
- palette: true,
167
- hint: 2,
168
- short: "copy fix",
169
- },
170
-
171
- // tab.profiles
172
- {
173
- id: "profile.save",
174
- title: "Save profile…",
175
- group: "Profiles",
176
- scope: "tab.profiles",
177
- keys: ["ctrl+s"],
178
- palette: true,
179
- suggested: true,
180
- hint: 3,
181
- short: "save",
182
- cli: "catherd profile set <path> <value>",
183
- },
184
- {
185
- id: "profile.activate",
186
- title: "Make this profile active…",
187
- group: "Profiles",
188
- scope: "tab.profiles",
189
- keys: ["a"],
190
- palette: true,
191
- hint: 6,
192
- short: "activate",
193
- cli: "catherd profile use <name> [--repo]",
194
- },
195
- {
196
- id: "profile.copy",
197
- title: "Copy this profile…",
198
- group: "Profiles",
199
- scope: "tab.profiles",
200
- keys: [],
201
- palette: true,
202
- cli: "catherd profile copy <from> <name>",
203
- },
204
- {
205
- id: "profile.revert",
206
- title: "Discard unsaved changes…",
207
- group: "Profiles",
208
- scope: "tab.profiles",
209
- keys: [],
210
- palette: true,
211
- },
212
- {
213
- id: "catalog.refresh",
214
- title: "Sync the public sources and refresh the model catalog",
215
- group: "Profiles",
216
- scope: "tab.profiles",
217
- keys: ["r"],
218
- palette: true,
219
- cli: "catherd catalog sync",
220
- },
221
- {
222
- id: "edit.undo",
223
- title: "Undo",
224
- group: "Edit",
225
- scope: "tab.profiles",
226
- keys: ["<leader>u"],
227
- palette: true,
228
- hint: 7,
229
- short: "undo",
230
- },
231
- {
232
- id: "edit.redo",
233
- title: "Redo",
234
- group: "Edit",
235
- scope: "tab.profiles",
236
- keys: ["<leader>r"],
237
- palette: true,
238
- },
239
-
240
- // tab.runs
241
- {
242
- id: "runs.refresh",
243
- title: "Refresh now",
244
- group: "Runs",
245
- scope: "tab.runs",
246
- keys: ["r"],
247
- palette: true,
248
- hint: 3,
249
- short: "refresh",
250
- },
251
- {
252
- id: "runs.pause",
253
- title: "Pause or resume updates",
254
- group: "Runs",
255
- scope: "tab.runs",
256
- keys: ["p"],
257
- palette: true,
258
- hint: 4,
259
- short: "pause",
260
- },
261
-
262
- // list: every list and tree
263
- { id: "list.up", title: "Move up", group: "Lists", scope: "list", keys: ["up", "k"] },
264
- { id: "list.down", title: "Move down", group: "Lists", scope: "list", keys: ["down", "j"] },
265
- { id: "list.pageUp", title: "Page up", group: "Lists", scope: "list", keys: ["pageup"] },
266
- { id: "list.pageDown", title: "Page down", group: "Lists", scope: "list", keys: ["pagedown"] },
267
- { id: "list.first", title: "First row", group: "Lists", scope: "list", keys: ["home", "g"] },
268
- { id: "list.last", title: "Last row", group: "Lists", scope: "list", keys: ["end", "shift+g"] },
269
- {
270
- id: "list.filter",
271
- title: "Filter",
272
- group: "Lists",
273
- scope: "list",
274
- keys: ["/"],
275
- hint: 8,
276
- short: "filter",
277
- },
278
-
279
- // row.*: act on the selected row
280
- {
281
- id: "status.open",
282
- title: "Open",
283
- group: "Status",
284
- scope: "row.status",
285
- keys: ["return"],
286
- hint: 1,
287
- short: "open",
288
- },
289
- {
290
- id: "tree.toggle",
291
- title: "Toggle",
292
- group: "Profiles",
293
- scope: "row.profiles",
294
- keys: ["space"],
295
- hint: 1,
296
- short: "toggle",
297
- },
298
- {
299
- id: "tree.open",
300
- title: "Change or expand",
301
- group: "Profiles",
302
- scope: "row.profiles",
303
- keys: ["return"],
304
- hint: 2,
305
- short: "change",
306
- },
307
- { id: "tree.expand", title: "Expand", group: "Profiles", scope: "row.profiles", keys: ["right", "l"] },
308
- {
309
- id: "tree.detail",
310
- title: "Show the rung's values, sources and runs",
311
- group: "Profiles",
312
- scope: "row.profiles",
313
- keys: ["i"],
314
- palette: true,
315
- cli: "catherd catalog list --json",
316
- },
317
- {
318
- id: "tree.treatLike",
319
- title: "Treat the rung like another…",
320
- group: "Profiles",
321
- scope: "row.profiles",
322
- keys: ["t"],
323
- palette: true,
324
- cli: "catherd catalog treat-like --suggest <rung>",
325
- },
326
- {
327
- id: "tree.collapse",
328
- title: "Collapse, or go to the parent",
329
- group: "Profiles",
330
- scope: "row.profiles",
331
- keys: ["left", "h"],
332
- },
333
- {
334
- id: "runs.open",
335
- title: "Open the session, milestone or role",
336
- group: "Runs",
337
- scope: "row.runs",
338
- keys: ["return"],
339
- hint: 1,
340
- short: "open",
341
- },
342
- {
343
- id: "runs.cancel",
344
- title: "Cancel the selected role (press twice)",
345
- group: "Runs",
346
- scope: "row.runs",
347
- keys: ["ctrl+d"],
348
- hint: 2,
349
- short: "cancel",
350
- cli: "catherd runs cancel <run> <name>",
351
- },
352
-
353
- // filter: a list's filter input has focus
354
- {
355
- id: "filter.accept",
356
- title: "Keep the filter",
357
- group: "Lists",
358
- scope: "filter",
359
- keys: ["return"],
360
- hint: 1,
361
- short: "done",
362
- },
363
-
364
- // dialog: a dialog is open
365
- // hints with the same short label and order show as one: `↑↓ move`
366
- {
367
- id: "dialog.up",
368
- title: "Previous",
369
- group: "Dialogs",
370
- scope: "dialog",
371
- keys: ["up", "ctrl+p"],
372
- hint: 0,
373
- short: "move",
374
- },
375
- {
376
- id: "dialog.down",
377
- title: "Next",
378
- group: "Dialogs",
379
- scope: "dialog",
380
- keys: ["down", "ctrl+n"],
381
- hint: 0,
382
- short: "move",
383
- },
384
- { id: "dialog.pageUp", title: "Page up", group: "Dialogs", scope: "dialog", keys: ["pageup"] },
385
- { id: "dialog.pageDown", title: "Page down", group: "Dialogs", scope: "dialog", keys: ["pagedown"] },
386
- {
387
- id: "dialog.left",
388
- title: "Previous button",
389
- group: "Dialogs",
390
- scope: "dialog",
391
- keys: ["left", "shift+tab"],
392
- hint: 0,
393
- short: "button",
394
- },
395
- {
396
- id: "dialog.right",
397
- title: "Next button",
398
- group: "Dialogs",
399
- scope: "dialog",
400
- keys: ["right", "tab"],
401
- hint: 0,
402
- short: "button",
403
- },
404
- {
405
- id: "dialog.submit",
406
- title: "Choose",
407
- group: "Dialogs",
408
- scope: "dialog",
409
- keys: ["return"],
410
- hint: 1,
411
- short: "choose",
412
- },
413
- {
414
- id: "dialog.delete",
415
- title: "Delete (press twice)",
416
- group: "Dialogs",
417
- scope: "dialog",
418
- keys: ["ctrl+d"],
419
- hint: 2,
420
- short: "delete",
421
- },
422
- ] as const satisfies readonly CommandDef[];
423
-
424
- export type CommandId = (typeof COMMANDS)[number]["id"];
425
- export type Keybinds = Record<CommandId, readonly string[]>;
426
-
427
- const BY_ID = new Map<string, CommandDef>(COMMANDS.map((c) => [c.id, c]));
428
- export const commandsIn = (scope: Scope): CommandDef[] => COMMANDS.filter((c) => c.scope === scope);
429
-
430
- /** Keys a text input would take as typing: one character, `space`, or a shifted letter. */
431
- export const isPrintable = (key: string): boolean =>
432
- [...key].length === 1 || key === "space" || /^shift\+.$/.test(key);
433
-
434
- /** Scopes whose printable keys go quiet while a text input has focus. */
435
- const GUARDED: readonly Scope[] = ["app", "tab.status", "tab.profiles", "tab.runs", "list"];
436
- export const isGuarded = (scope: Scope): boolean => GUARDED.includes(scope);
437
-
438
- /** Commands only the ctrl+c layering may run; rebinding them could leave no way out. */
439
- const RESERVED: readonly CommandId[] = ["app.interrupt", "app.back"];
440
-
441
- export const DEFAULT_KEYS: Keybinds = Object.fromEntries(
442
- COMMANDS.map((c): [string, readonly string[]] => [c.id, c.keys]),
443
- ) as Keybinds;
444
-
445
- /** Spec §9.2 `config.json` `keybinds`: `{ <command id>: string | string[] | "none" }`. */
446
- const KeybindsSchema = z.record(z.string(), z.union([z.string().min(1), z.array(z.string().min(1))]));
447
-
448
- /** The stacks of scopes that can be live at once; a key must mean one thing in each. */
449
- export const CONTEXTS: readonly { tab: "status" | "profiles" | "runs" | null; typing: boolean }[] = [
450
- ...(["status", "profiles", "runs"] as const).flatMap((tab) => [
451
- { tab, typing: false },
452
- { tab, typing: true },
453
- ]),
454
- { tab: null, typing: true },
455
- ];
456
-
457
- /** The keys live in one context, with the command each would run. */
458
- export function keysIn(ctx: (typeof CONTEXTS)[number], keys: Keybinds): [string, CommandId][] {
459
- const scopes: Scope[] =
460
- ctx.tab === null
461
- ? ["global", "dialog"]
462
- : ["global", "app", `tab.${ctx.tab}`, "list", ctx.typing ? "filter" : `row.${ctx.tab}`];
463
- const out: [string, CommandId][] = [];
464
- for (const c of COMMANDS) {
465
- if (!scopes.includes(c.scope)) continue;
466
- for (const k of keys[c.id])
467
- if (!(ctx.typing && isGuarded(c.scope) && isPrintable(k))) out.push([k, c.id]);
468
- }
469
- return out;
470
- }
471
-
472
- /** Every key that means two things somewhere: `"<key>: <id>, <id> (<context>)"`. */
473
- export function duplicateKeys(keys: Keybinds): string[] {
474
- const out: string[] = [];
475
- for (const ctx of CONTEXTS) {
476
- const seen = new Map<string, CommandId>();
477
- for (const [k, id] of keysIn(ctx, keys)) {
478
- const first = seen.get(k);
479
- if (first && first !== id)
480
- out.push(`${k}: ${first}, ${id} (${ctx.tab ?? "dialog"}${ctx.typing && ctx.tab ? ", typing" : ""})`);
481
- else seen.set(k, id);
482
- }
483
- }
484
- return [...new Set(out)];
485
- }
486
-
487
- /**
488
- * The keys in force: the defaults with `config.json`'s `keybinds` over them. Unknown ids, reserved
489
- * commands, the leader as a command's own key, a typed key on a command live while the user types, and
490
- * overrides that make one key mean two things are refused with `E_CONFIG_KEYBIND`.
491
- */
492
- export function resolveKeybinds(raw: unknown): Keybinds {
493
- const keys: Keybinds = { ...DEFAULT_KEYS };
494
- if (raw === undefined) return keys;
495
- const parsed = KeybindsSchema.safeParse(raw);
496
- if (!parsed.success)
497
- throw new CatherdError("E_CONFIG_KEYBIND", "config.json keybinds must map command ids to keys", {
498
- fix: 'e.g. "keybinds": { "profile.save": "ctrl+w", "app.help": "none" }',
499
- });
500
- for (const [id, value] of Object.entries(parsed.data)) {
501
- if (!BY_ID.has(id))
502
- throw new CatherdError("E_CONFIG_KEYBIND", `config.json keybinds: no command "${id}"`, {
503
- fix: "the command palette (ctrl+p) and catherd's README list the command ids",
504
- });
505
- if ((RESERVED as readonly string[]).includes(id))
506
- throw new CatherdError("E_CONFIG_KEYBIND", `config.json keybinds: "${id}" cannot be rebound`, {
507
- fix: `remove "${id}" from keybinds`,
508
- });
509
- // a comma list is several keys, as in opencode's config
510
- const list = (Array.isArray(value) ? value : [value]).flatMap((v) => v.split(",").map((k) => k.trim()));
511
- if (list.some((k) => k.split(" ")[0] === LEADER))
512
- throw new CatherdError(
513
- "E_CONFIG_KEYBIND",
514
- `config.json keybinds: ${id} cannot take ${LEADER}, the leader`,
515
- {
516
- fix: `bind ${id} to another key, or to the leader and a key: "<leader>w"`,
517
- },
518
- );
519
- // dialog and filter commands are live while the user types: a typed key would take letters from the text
520
- const scope = (BY_ID.get(id) as CommandDef).scope;
521
- const typed = list.find(isPrintable);
522
- if ((scope === "dialog" || scope === "filter") && typed)
523
- throw new CatherdError(
524
- "E_CONFIG_KEYBIND",
525
- `config.json keybinds: ${id} works while you type, so it cannot take "${typed}"`,
526
- { fix: `bind ${id} to a key with ctrl or alt, or a named key such as "return"` },
527
- );
528
- keys[id as CommandId] = list.includes("none") ? [] : list.filter(Boolean);
529
- }
530
- const dup = duplicateKeys(keys);
531
- if (dup.length)
532
- throw new CatherdError(
533
- "E_CONFIG_KEYBIND",
534
- `config.json keybinds give one key two meanings: ${dup.join("; ")}`,
535
- {
536
- fix: 'bind one of them to another key, or to "none"',
537
- },
538
- );
539
- return keys;
540
- }
541
-
542
- const WORDS: Record<string, string> = {
543
- return: "enter",
544
- escape: "esc",
545
- pageup: "pgup",
546
- pagedown: "pgdn",
547
- up: "↑",
548
- down: "↓",
549
- left: "←",
550
- right: "→",
551
- };
552
- const PLAIN_WORDS: Record<string, string> = { up: "up", down: "down", left: "left", right: "right" };
553
-
554
- /** How a key reads in the footer, palette and help: `ctrl+x n`, `enter`, `G`, `↑`. */
555
- export function formatKey(key: string, plain = false): string {
556
- if (key.startsWith("<leader>")) return `${LEADER} ${formatKey(key.slice("<leader>".length), plain)}`;
557
- if (/^shift\+[a-z]$/.test(key)) return key.slice(-1).toUpperCase();
558
- return (plain ? PLAIN_WORDS[key] : undefined) ?? WORDS[key] ?? key;
559
- }
560
-
561
- /** All of a command's keys, `/`-joined, or "" when it has none. */
562
- export const formatKeys = (keys: readonly string[], plain = false): string =>
563
- keys.map((k) => formatKey(k, plain)).join("/");