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.
- package/README.md +188 -169
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -239
- 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 -356
- package/plugin/skills/catherd-setup/SKILL.md +21 -20
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +19 -4
- package/src/adapters/claude-code/index.ts +126 -14
- package/src/adapters/codex/events.ts +10 -0
- package/src/adapters/codex/index.ts +17 -5
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +10 -4
- package/src/adapters/opencode/index.ts +38 -1
- package/src/cli.ts +27 -27
- package/src/domain/agents.ts +3 -1
- 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 +106 -0
- package/src/domain/catalog.ts +69 -313
- package/src/domain/changes.ts +23 -1
- package/src/domain/cost.ts +19 -29
- package/src/domain/errors.ts +6 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -75
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +53 -0
- package/src/domain/hints.ts +13 -2
- 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 +118 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +32 -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 +175 -138
- package/src/domain/profile.ts +140 -63
- package/src/domain/record.ts +68 -3
- package/src/domain/reply.ts +29 -0
- package/src/domain/role-prompts.ts +182 -56
- package/src/domain/role-scope.ts +76 -0
- package/src/domain/role-tools.ts +34 -0
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +90 -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 +229 -72
- package/src/domain/slopalytics.ts +72 -0
- package/src/domain/state.ts +5 -0
- package/src/domain/util.ts +22 -0
- package/src/domain/workspace.ts +130 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +24 -165
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +18 -2
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/deps.ts +2 -0
- package/src/entry/doctor-command.ts +20 -2
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +20 -69
- package/src/entry/knowledge-command.ts +121 -5
- package/src/entry/lock-command.ts +103 -8
- package/src/entry/mcp/handshake.ts +6 -2
- package/src/entry/mcp/role-bin.ts +20 -0
- package/src/entry/mcp/role-server.ts +134 -0
- package/src/entry/mcp/server.ts +85 -21
- 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/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +33 -18
- package/src/entry/role-cli-command.ts +159 -0
- package/src/entry/runs-command.ts +81 -27
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/workspace-command.ts +44 -0
- package/src/infra/boot-lock.ts +25 -0
- package/src/infra/delivery.ts +3 -0
- package/src/infra/dispatch-dir.ts +2 -0
- package/src/infra/env.ts +11 -3
- package/src/infra/git.ts +70 -0
- package/src/infra/heavy-lock.ts +133 -6
- package/src/infra/host-probe.ts +16 -0
- package/src/infra/launch.ts +63 -7
- package/src/infra/lock-activity.ts +88 -0
- package/src/infra/login-env.ts +48 -0
- 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 +37 -2
- package/src/infra/role-mcp.ts +69 -0
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/infra/supervisor.ts +107 -16
- package/src/services/admission.ts +133 -31
- package/src/services/backends.ts +4 -6
- package/src/services/budget.ts +26 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -254
- package/src/services/credentials.ts +15 -26
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +209 -17
- package/src/services/dispatches.ts +30 -2
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +72 -15
- package/src/services/end-push.ts +157 -0
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +24 -2
- package/src/services/gate-env.ts +217 -0
- package/src/services/gate-service.ts +231 -45
- package/src/services/global-install.ts +46 -13
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-edit.ts +154 -0
- package/src/services/lane-service.ts +291 -55
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +167 -29
- package/src/services/notifier.ts +124 -4
- package/src/services/notify-service.ts +26 -0
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +79 -17
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +60 -5
- package/src/services/preflight.ts +232 -24
- package/src/services/profile-service.ts +31 -3
- package/src/services/profile-store.ts +18 -6
- package/src/services/protocol.ts +123 -25
- package/src/services/provenance.ts +34 -89
- package/src/services/questions.ts +24 -0
- package/src/services/reconcile.ts +33 -1
- package/src/services/reentry.ts +7 -6
- package/src/services/role-access.ts +27 -0
- package/src/services/routing-service.ts +161 -42
- 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-pin.ts +125 -0
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +204 -23
- package/src/services/run-store.ts +159 -18
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/scratch.ts +60 -0
- package/src/services/sessions.ts +3 -1
- package/src/services/state.ts +21 -0
- package/src/services/summary.ts +76 -12
- package/src/services/tree-path.ts +27 -0
- package/src/services/verifier-brief.ts +23 -0
- package/src/services/verifier-step.ts +48 -0
- package/src/services/workspace-admission.ts +259 -0
- package/src/services/workspace-service.ts +259 -0
- package/src/services/workspace-store.ts +95 -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 -36
- 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 -320
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -132
- package/src/adapters/grok/index.ts +0 -339
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -125
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -84
- package/src/entry/mcp/protocol-tools.ts +0 -57
- package/src/entry/mcp/run-tools.ts +0 -148
- package/src/entry/mcp/setup-tools.ts +0 -108
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -381
- 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 -373
- 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 -573
- package/src/entry/tui/views/save-dialog.tsx +0 -340
- package/src/entry/tui/views/status.tsx +0 -209
- 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 -87
- package/src/infra/sources/artificial-analysis.ts +0 -112
- package/src/infra/sources/cache.ts +0 -124
- 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 -255
- package/src/services/source-derive.ts +0 -374
- package/src/services/source-sync.ts +0 -305
- package/src/services/standins.ts +0 -204
- 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("/");
|