catherd-cli 1.5.0 → 2.0.0-beta.20261022113309
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -208
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -247
- package/package.json +6 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +97 -421
- package/plugin/skills/catherd-setup/SKILL.md +20 -19
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +1 -2
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +4 -4
- package/src/cli.ts +22 -27
- package/src/domain/bars.ts +33 -107
- package/src/domain/blocked.ts +72 -0
- package/src/domain/branch.ts +51 -0
- package/src/domain/brief.ts +21 -6
- package/src/domain/catalog.ts +69 -324
- package/src/domain/cost.ts +15 -30
- package/src/domain/errors.ts +2 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -87
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +1 -0
- package/src/domain/ids.ts +13 -2
- package/src/domain/jev.ts +47 -2
- package/src/domain/judge.ts +124 -0
- package/src/domain/lane.ts +22 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +25 -8
- package/src/domain/notify.ts +32 -0
- package/src/domain/plan-doc.ts +198 -0
- package/src/domain/plan-lint.ts +141 -0
- package/src/domain/profile-rules.ts +101 -160
- package/src/domain/profile.ts +116 -63
- package/src/domain/record.ts +53 -2
- package/src/domain/role-prompts.ts +131 -45
- package/src/domain/role-scope.ts +4 -34
- package/src/domain/role-tools.ts +2 -2
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +17 -0
- package/src/domain/rules.ts +33 -0
- package/src/domain/run-events.ts +298 -0
- package/src/domain/run-machine.ts +668 -0
- package/src/domain/run-state.ts +1310 -0
- package/src/domain/scorecard.ts +197 -0
- package/src/domain/select.ts +151 -146
- package/src/domain/slopalytics.ts +72 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +25 -174
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +1 -1
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/init-command.ts +18 -68
- package/src/entry/knowledge-command.ts +45 -4
- package/src/entry/lock-command.ts +6 -7
- package/src/entry/mcp/role-server.ts +18 -0
- package/src/entry/mcp/server.ts +13 -40
- package/src/entry/mcp/session-tools.ts +149 -0
- package/src/entry/panel/openapi.ts +189 -0
- package/src/entry/panel/server.ts +410 -0
- package/src/entry/panel-command.ts +68 -0
- package/src/entry/profile-command.ts +10 -14
- package/src/entry/role-cli-command.ts +47 -3
- package/src/entry/runs-command.ts +3 -19
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/infra/env.ts +0 -2
- package/src/infra/git.ts +64 -0
- package/src/infra/heavy-lock.ts +45 -7
- package/src/infra/launch.ts +63 -7
- package/src/infra/memory.ts +45 -0
- package/src/infra/ntfy.ts +40 -0
- package/src/infra/open-url.ts +42 -0
- package/src/infra/paths.ts +2 -2
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/services/admission.ts +24 -9
- package/src/services/backends.ts +4 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -262
- package/src/services/credentials.ts +4 -20
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +5 -1
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor.ts +4 -9
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +12 -0
- package/src/services/gate-env.ts +1 -2
- package/src/services/gate-service.ts +21 -6
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-service.ts +34 -3
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +35 -8
- package/src/services/notifier.ts +69 -2
- package/src/services/notify-service.ts +26 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +30 -4
- package/src/services/preflight.ts +82 -8
- package/src/services/profile-service.ts +30 -3
- package/src/services/profile-store.ts +8 -3
- package/src/services/protocol.ts +4 -0
- package/src/services/provenance.ts +31 -90
- package/src/services/questions.ts +9 -0
- package/src/services/reconcile.ts +11 -0
- package/src/services/routing-service.ts +26 -15
- package/src/services/run-briefs.ts +477 -0
- package/src/services/run-client.ts +217 -0
- package/src/services/run-effects.ts +837 -0
- package/src/services/run-events-store.ts +174 -0
- package/src/services/run-evidence.ts +1 -1
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +55 -6
- package/src/services/run-store.ts +54 -7
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/summary.ts +27 -3
- package/src/services/tree-path.ts +27 -0
- package/src/services/workspace-admission.ts +5 -13
- package/src/services/workspace-service.ts +5 -0
- package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js +1 -0
- package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
- package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js +1 -0
- package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-B6GiwDkh.js +11 -0
- package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
- package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
- package/web/dist/assets/index-C5IbylvM.css +2 -0
- package/web/dist/assets/index-C5IbylvM.css.br +0 -0
- package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
- package/web/dist/assets/label-DFSYBc62.js +1 -0
- package/web/dist/assets/label-DFSYBc62.js.br +0 -0
- package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
- package/web/dist/assets/link-D8EcKZUt.js +1 -0
- package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
- package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
- package/web/dist/assets/login-DaN5HPPW.js +1 -0
- package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
- package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
- package/web/dist/assets/query-Cu--ccT3.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
- package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
- package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
- package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
- package/web/dist/assets/table-BOxXhqS4.js +1 -0
- package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
- package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
- package/web/dist/assets/time--9kSQbbp.js +1 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
- package/web/dist/index.html +15 -0
- package/catalog/ATTRIBUTION.md +0 -44
- package/catalog/scores.json +0 -3202
- package/catalog/sources.json +0 -97
- package/src/adapters/antigravity/events.ts +0 -131
- package/src/adapters/antigravity/home.ts +0 -46
- package/src/adapters/antigravity/index.ts +0 -335
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -187
- package/src/adapters/grok/index.ts +0 -358
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -132
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -131
- package/src/entry/mcp/protocol-tools.ts +0 -63
- package/src/entry/mcp/push-tools.ts +0 -16
- package/src/entry/mcp/run-tools.ts +0 -164
- package/src/entry/mcp/setup-tools.ts +0 -109
- package/src/entry/mcp/workspace-tools.ts +0 -142
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -385
- package/src/entry/tui/fixtures.ts +0 -637
- package/src/entry/tui/profile-edits.ts +0 -146
- package/src/entry/tui/profile-tree.ts +0 -429
- package/src/entry/tui/providers/app.tsx +0 -162
- package/src/entry/tui/providers/boundary.tsx +0 -57
- package/src/entry/tui/providers/data.tsx +0 -160
- package/src/entry/tui/providers/keymap.tsx +0 -166
- package/src/entry/tui/providers/root.tsx +0 -45
- package/src/entry/tui/providers/theme.tsx +0 -17
- package/src/entry/tui/providers/toast.tsx +0 -77
- package/src/entry/tui/run.tsx +0 -114
- package/src/entry/tui/state.ts +0 -400
- package/src/entry/tui/stories.ts +0 -114
- package/src/entry/tui/text.ts +0 -130
- package/src/entry/tui/theme.ts +0 -140
- package/src/entry/tui/views/app.tsx +0 -208
- package/src/entry/tui/views/dialogs.tsx +0 -41
- package/src/entry/tui/views/profile-actions.ts +0 -331
- package/src/entry/tui/views/profiles.tsx +0 -520
- package/src/entry/tui/views/runs.tsx +0 -619
- package/src/entry/tui/views/save-dialog.tsx +0 -344
- package/src/entry/tui/views/status.tsx +0 -213
- package/src/entry/tui/widgets/chrome.tsx +0 -114
- package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
- package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
- package/src/entry/tui/widgets/dialog-select.tsx +0 -212
- package/src/entry/tui/widgets/dialog.tsx +0 -68
- package/src/entry/tui/widgets/line.tsx +0 -92
- package/src/entry/tui/widgets/list.tsx +0 -197
- package/src/entry/tui/widgets/spinner.tsx +0 -49
- package/src/entry/tui/widgets/toast.tsx +0 -62
- package/src/infra/sources/arena.ts +0 -95
- package/src/infra/sources/artificial-analysis.ts +0 -119
- package/src/infra/sources/cache.ts +0 -126
- package/src/infra/sources/csv.ts +0 -38
- package/src/infra/sources/epoch.ts +0 -82
- package/src/infra/sources/http.ts +0 -75
- package/src/infra/sources/litellm.ts +0 -58
- package/src/infra/sources/models-dev.ts +0 -59
- package/src/infra/sources/openrouter-endpoints.ts +0 -52
- package/src/infra/sources/openrouter-models.ts +0 -60
- package/src/infra/sources/rows.ts +0 -38
- package/src/infra/sources/vectara.ts +0 -69
- package/src/infra/sources/zip.ts +0 -48
- package/src/services/catalog-refresh.ts +0 -201
- package/src/services/doctor-sources.ts +0 -76
- package/src/services/runs-page.ts +0 -290
- package/src/services/source-derive.ts +0 -397
- package/src/services/source-sync.ts +0 -312
- package/src/services/standins.ts +0 -238
- package/src/services/treat-likes.ts +0 -113
|
@@ -2,7 +2,15 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { defineCommand } from "citty";
|
|
3
3
|
import { CatherdError } from "../domain/errors.ts";
|
|
4
4
|
import { gateEnvLines, readGateEnv, removeGateEnv, setGateEnv } from "../services/gate-env.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { ROLES, type Role } from "../domain/roles.ts";
|
|
6
|
+
import {
|
|
7
|
+
addKnowledge,
|
|
8
|
+
knowledgeLines,
|
|
9
|
+
knowledgePath,
|
|
10
|
+
readKnowledge,
|
|
11
|
+
removeKnowledge,
|
|
12
|
+
standingRules,
|
|
13
|
+
} from "../services/run-service.ts";
|
|
6
14
|
import { JSON_ARG, printJson, refuseInRole } from "./cli-kit.ts";
|
|
7
15
|
|
|
8
16
|
/** `--repo`, else the repository the command runs in, as run_start and read_knowledge resolve it. */
|
|
@@ -13,8 +21,26 @@ const repoOf = (repo: string | undefined): string => resolve(repo ?? process.cwd
|
|
|
13
21
|
|
|
14
22
|
const show = defineCommand({
|
|
15
23
|
meta: { name: "show", description: "Print what catherd knows about the repo (its knowledge.md)" },
|
|
16
|
-
args: {
|
|
24
|
+
args: {
|
|
25
|
+
...REPO_ARG,
|
|
26
|
+
role: { type: "string", description: "only the rules that reach this role" },
|
|
27
|
+
...JSON_ARG,
|
|
28
|
+
},
|
|
17
29
|
async run({ args }) {
|
|
30
|
+
if (args.role !== undefined) {
|
|
31
|
+
if (!(ROLES as readonly string[]).includes(args.role))
|
|
32
|
+
throw new CatherdError("E_INPUT_INVALID", `"${args.role}" is not a role`, {
|
|
33
|
+
fix: `pass one of ${ROLES.join(", ")}`,
|
|
34
|
+
});
|
|
35
|
+
const rules = await standingRules(repoOf(args.repo), args.role as Role);
|
|
36
|
+
if (args.json) {
|
|
37
|
+
const at = await knowledgePath(repoOf(args.repo));
|
|
38
|
+
return printJson({ ...at, role: args.role, rules });
|
|
39
|
+
}
|
|
40
|
+
const out = rules || "catherd: no knowledge recorded yet for this repo";
|
|
41
|
+
process.stdout.write(`${out}\n`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
18
44
|
if (args.json) return printJson(await knowledgeLines(repoOf(args.repo)));
|
|
19
45
|
const text = await readKnowledge(repoOf(args.repo));
|
|
20
46
|
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
@@ -36,6 +62,21 @@ const add = defineCommand({
|
|
|
36
62
|
},
|
|
37
63
|
});
|
|
38
64
|
|
|
65
|
+
const rm = defineCommand({
|
|
66
|
+
meta: {
|
|
67
|
+
name: "rm",
|
|
68
|
+
description: "Remove the one line of the repo's knowledge.md that contains the text",
|
|
69
|
+
},
|
|
70
|
+
args: {
|
|
71
|
+
text: { type: "positional", required: true, description: "text of the one line to remove" },
|
|
72
|
+
...REPO_ARG,
|
|
73
|
+
},
|
|
74
|
+
async run({ args }) {
|
|
75
|
+
refuseInRole(process.env, "catherd knowledge rm");
|
|
76
|
+
console.log(await removeKnowledge(repoOf(args.repo), args.text));
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
39
80
|
const path = defineCommand({
|
|
40
81
|
meta: { name: "path", description: "Print the path of the repo's knowledge.md (it may not exist yet)" },
|
|
41
82
|
args: { ...REPO_ARG, ...JSON_ARG },
|
|
@@ -119,7 +160,7 @@ const env = defineCommand({
|
|
|
119
160
|
export const knowledgeCommand = defineCommand({
|
|
120
161
|
meta: {
|
|
121
162
|
name: "knowledge",
|
|
122
|
-
description: "What catherd knows about a repo: show, add or path, and its gate environment (env)",
|
|
163
|
+
description: "What catherd knows about a repo: show, add, rm or path, and its gate environment (env)",
|
|
123
164
|
},
|
|
124
|
-
subCommands: { show, add, path, env },
|
|
165
|
+
subCommands: { show, add, rm, path, env },
|
|
125
166
|
});
|
|
@@ -3,7 +3,7 @@ import { defineCommand } from "citty";
|
|
|
3
3
|
import { CatherdError } from "../domain/errors.ts";
|
|
4
4
|
import { scrubSecrets } from "../infra/env.ts";
|
|
5
5
|
import { gitToplevel } from "../infra/git.ts";
|
|
6
|
-
import { heavySlots, withHeavySlot, withRoleLock } from "../infra/heavy-lock.ts";
|
|
6
|
+
import { heavySlots, LOCK_HELD_ENV, withHeavySlot, withRoleLock } from "../infra/heavy-lock.ts";
|
|
7
7
|
import { activityReporter, DISPATCH_ID_ENV } from "../infra/lock-activity.ts";
|
|
8
8
|
import { killGroup } from "../infra/proc.ts";
|
|
9
9
|
import { activeName, readProfileDoc } from "../services/profile-store.ts";
|
|
@@ -138,8 +138,7 @@ export function roleLockOwnerOf(run: string | undefined, env: NodeJS.ProcessEnv)
|
|
|
138
138
|
return fromRole ? `run:${fromRole}` : `process:${process.pid}`;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
export const LOCK_HELD_ENV = "CATHERD_LOCK_HELD";
|
|
141
|
+
export { LOCK_HELD_ENV };
|
|
143
142
|
|
|
144
143
|
export const lockCommand = defineCommand({
|
|
145
144
|
meta: {
|
|
@@ -194,9 +193,9 @@ export const lockCommand = defineCommand({
|
|
|
194
193
|
// killed before its first command
|
|
195
194
|
const activity = activityReporter(process.env[DISPATCH_ID_ENV]);
|
|
196
195
|
const waiting = activity ? { onWait: activity.tick } : {};
|
|
197
|
-
const run = () =>
|
|
196
|
+
const run = (_slot: number, tmp: string) =>
|
|
198
197
|
runForwarding(argv, {
|
|
199
|
-
extraEnv: { [LOCK_HELD_ENV]: "1" },
|
|
198
|
+
extraEnv: { [LOCK_HELD_ENV]: "1", TMPDIR: tmp, GOTMPDIR: tmp },
|
|
200
199
|
...(activity ? { onOutput: activity.tick } : {}),
|
|
201
200
|
});
|
|
202
201
|
const role = args.role;
|
|
@@ -213,10 +212,10 @@ export const lockCommand = defineCommand({
|
|
|
213
212
|
? await withRoleLock(
|
|
214
213
|
role,
|
|
215
214
|
roleLockOwnerOf(args.run, process.env),
|
|
216
|
-
() => withHeavySlot(slots, run, waiting),
|
|
215
|
+
() => withHeavySlot(slots, run, { ...waiting, tmpBase: process.env.TMPDIR }),
|
|
217
216
|
waiting,
|
|
218
217
|
)
|
|
219
|
-
: await withHeavySlot(slots, run, waiting);
|
|
218
|
+
: await withHeavySlot(slots, run, { ...waiting, tmpBase: process.env.TMPDIR });
|
|
220
219
|
} finally {
|
|
221
220
|
activity?.done();
|
|
222
221
|
}
|
|
@@ -6,6 +6,7 @@ import { ID_PATTERN } from "../../domain/ids.ts";
|
|
|
6
6
|
import { ROLE_MCP_SERVER, type RoleMcpContext, roleMcpTools } from "../../domain/role-tools.ts";
|
|
7
7
|
import { log } from "../../infra/log.ts";
|
|
8
8
|
import { gateCheckOrList, gatePass } from "../../services/gate-service.ts";
|
|
9
|
+
import { runMilestoneCheck } from "../../services/milestone-check.ts";
|
|
9
10
|
import type { Deps } from "../../services/ports.ts";
|
|
10
11
|
import { readKnowledge, readRunFile, writeRunFile } from "../../services/run-service.ts";
|
|
11
12
|
import { findRun } from "../../services/run-store.ts";
|
|
@@ -103,6 +104,23 @@ export function buildRoleServer(context: RoleMcpContext, deps: Deps = defaultDep
|
|
|
103
104
|
(a) => call("gate_pass", () => gatePass(deps, a)),
|
|
104
105
|
);
|
|
105
106
|
}
|
|
107
|
+
if (tools.has("gate_run"))
|
|
108
|
+
server.registerTool(
|
|
109
|
+
"gate_run",
|
|
110
|
+
{
|
|
111
|
+
description:
|
|
112
|
+
"The verifier's full check of a milestone, run by catherd on the committed HEAD in a detached worker under the verifier lock and a heavy slot: { running: false, record (exit, failing lines, log, secrets, rerunOf), missing, audit } once done; land reads that record. The same run, milestone, command and HEAD answers the recorded result (force: true runs it again). Refused while tracked files are uncommitted. Past 10 minutes it answers { running: true }: call it again to wait for the same run. at: a checkout of the run's base you prepared, to record the base's own failures, which then do not block.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
run: boundRun,
|
|
115
|
+
milestone: z.string().regex(ID_PATTERN),
|
|
116
|
+
command: z.string().min(1),
|
|
117
|
+
at: z.string().min(1).optional(),
|
|
118
|
+
force: z.boolean().optional(),
|
|
119
|
+
},
|
|
120
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
121
|
+
},
|
|
122
|
+
(a) => call("gate_run", () => runMilestoneCheck(a)),
|
|
123
|
+
);
|
|
106
124
|
return server;
|
|
107
125
|
}
|
|
108
126
|
|
package/src/entry/mcp/server.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
5
5
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import { sessionKey, type HostContext } from "../../domain/host.ts";
|
|
7
|
-
import { type CatherdError, errorMessage
|
|
7
|
+
import { type CatherdError, errorMessage } from "../../domain/errors.ts";
|
|
8
8
|
import {
|
|
9
9
|
isCoordinatorTool,
|
|
10
10
|
refusedForRole,
|
|
@@ -14,21 +14,14 @@ import {
|
|
|
14
14
|
import { takeBootLock } from "../../infra/boot-lock.ts";
|
|
15
15
|
import { resolveHost } from "../../infra/host-context.ts";
|
|
16
16
|
import { log } from "../../infra/log.ts";
|
|
17
|
-
import { assertRoleMay } from "../../services/role-access.ts";
|
|
18
17
|
import { currentSession } from "../../services/sessions.ts";
|
|
19
18
|
import { startNotifier } from "../../services/notifier.ts";
|
|
20
19
|
import type { Deps } from "../../services/ports.ts";
|
|
21
20
|
import { adoptOwned, reconcileAll } from "../../services/reconcile.ts";
|
|
22
|
-
import {
|
|
21
|
+
import { refreshIndex } from "../../services/catalog-service.ts";
|
|
23
22
|
import { defaultDeps } from "../deps.ts";
|
|
24
|
-
import { registerDispatchTools } from "./dispatch-tools.ts";
|
|
25
|
-
import { registerLaneTools } from "./lane-tools.ts";
|
|
26
|
-
import { registerProtocolTools } from "./protocol-tools.ts";
|
|
27
|
-
import { registerPushTools } from "./push-tools.ts";
|
|
28
23
|
import { handle, sdkToolError, toolOf } from "./result.ts";
|
|
29
|
-
import {
|
|
30
|
-
import { registerSetupTools } from "./setup-tools.ts";
|
|
31
|
-
import { registerWorkspaceTools } from "./workspace-tools.ts";
|
|
24
|
+
import { registerSessionTools } from "./session-tools.ts";
|
|
32
25
|
|
|
33
26
|
type ObserveSession = (context: Deps) => (() => void) | undefined;
|
|
34
27
|
|
|
@@ -45,26 +38,12 @@ type RequestHandler = (
|
|
|
45
38
|
extra: unknown,
|
|
46
39
|
) => unknown;
|
|
47
40
|
|
|
48
|
-
/**
|
|
49
|
-
const
|
|
41
|
+
/** How often a session looks for the new reports of the 2.0 runs it owns. */
|
|
42
|
+
const REPORT_POLL_MS = 3_000;
|
|
50
43
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* run or outside its role's tool set, as `catherd run-file` and `catherd gate` refuse it.
|
|
55
|
-
*/
|
|
56
|
-
function roleRefusal(name: string, args: unknown, role: RoleScope): CatherdError | null {
|
|
57
|
-
if (refusedForRole(name, args, role) || name === "record_agent_run") return roleScopeError(name, role);
|
|
58
|
-
if (!RUN_BOUND_TOOLS.has(name)) return null;
|
|
59
|
-
const run = (args as { run?: unknown } | undefined)?.run;
|
|
60
|
-
try {
|
|
61
|
-
assertRoleMay(role, typeof run === "string" ? run : "", name);
|
|
62
|
-
return null;
|
|
63
|
-
} catch (e) {
|
|
64
|
-
if (isCatherdError(e)) return e;
|
|
65
|
-
throw e;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
44
|
+
/** Why a role process may not make this call on the full server (plan 21 review, finding 3), else null: a coordinator tool. */
|
|
45
|
+
const roleRefusal = (name: string, args: unknown, role: RoleScope): CatherdError | null =>
|
|
46
|
+
refusedForRole(name, args, role) ? roleScopeError(name, role) : null;
|
|
68
47
|
|
|
69
48
|
/**
|
|
70
49
|
* Spec 1.5 plan 21: in a role's process (`deps.role`) a coordinator tool call is refused with E_ROLE_SCOPE
|
|
@@ -168,13 +147,7 @@ export function buildServer(deps: Deps = defaultDeps(), observe?: ObserveSession
|
|
|
168
147
|
log("warn", "tool", { tool: toolOf(message), ok: false, code });
|
|
169
148
|
return r;
|
|
170
149
|
};
|
|
171
|
-
|
|
172
|
-
registerWorkspaceTools(server, scoped);
|
|
173
|
-
registerLaneTools(server, scoped);
|
|
174
|
-
registerDispatchTools(server, scoped);
|
|
175
|
-
registerSetupTools(server, scoped);
|
|
176
|
-
registerProtocolTools(server, scoped);
|
|
177
|
-
registerPushTools(server, scoped);
|
|
150
|
+
registerSessionTools(server, scoped);
|
|
178
151
|
refuseCoordinatorTools(server, deps);
|
|
179
152
|
return server;
|
|
180
153
|
}
|
|
@@ -197,7 +170,7 @@ export async function startMcpServer(
|
|
|
197
170
|
const key = sessionKey(target);
|
|
198
171
|
let active = observed.get(key);
|
|
199
172
|
if (!active) {
|
|
200
|
-
active = startNotifier(context);
|
|
173
|
+
active = startNotifier(context, { reportPollMs: REPORT_POLL_MS });
|
|
201
174
|
observed.set(key, active);
|
|
202
175
|
}
|
|
203
176
|
const n = active;
|
|
@@ -243,15 +216,15 @@ export async function startMcpServer(
|
|
|
243
216
|
const epoch = generation;
|
|
244
217
|
recovery = { ...deps };
|
|
245
218
|
const context = recovery;
|
|
246
|
-
notifier = startNotifier(context);
|
|
219
|
+
notifier = startNotifier(context, { reportPollMs: REPORT_POLL_MS });
|
|
247
220
|
const active = notifier;
|
|
248
221
|
const target = currentSession(context);
|
|
249
222
|
if (target) observed.set(sessionKey(target), active);
|
|
250
223
|
void Promise.resolve()
|
|
251
224
|
.then(() => {
|
|
252
|
-
if (lead && epoch === generation) return (o.sync ?? (() =>
|
|
225
|
+
if (lead && epoch === generation) return (o.sync ?? (() => refreshIndex({ auto: true })))();
|
|
253
226
|
})
|
|
254
|
-
.catch((e: unknown) => log("debug", "
|
|
227
|
+
.catch((e: unknown) => log("debug", "catalog", { error: errorMessage(e) }));
|
|
255
228
|
void (async () => {
|
|
256
229
|
try {
|
|
257
230
|
if (epoch !== generation) return;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CatherdError } from "../../domain/errors.ts";
|
|
4
|
+
import { BaseBranchSchema } from "../../domain/branch.ts";
|
|
5
|
+
import { ID_PATTERN } from "../../domain/ids.ts";
|
|
6
|
+
import { PROFILE_NAME, ProfilePatchSchema } from "../../domain/profile.ts";
|
|
7
|
+
import { knownQueueCapability } from "../../infra/codex-queue.ts";
|
|
8
|
+
import { gitToplevel } from "../../infra/git.ts";
|
|
9
|
+
import { redact } from "../../infra/log.ts";
|
|
10
|
+
import { registerSavedSecrets } from "../../services/jev-service.ts";
|
|
11
|
+
import { pausesOver } from "../../services/pause.ts";
|
|
12
|
+
import type { Deps } from "../../services/ports.ts";
|
|
13
|
+
import { inspectionHost } from "../../services/run-debug.ts";
|
|
14
|
+
import { answerUnit, cancelRun, profileTool, runResult, runStart } from "../../services/run-client.ts";
|
|
15
|
+
import { listRunViews, type RunView, runView } from "../../services/run-read.ts";
|
|
16
|
+
import { findRun, listRuns } from "../../services/run-store.ts";
|
|
17
|
+
import { handle } from "./result.ts";
|
|
18
|
+
|
|
19
|
+
const RUN = z.string().min(1);
|
|
20
|
+
const PROFILE = z.string().regex(PROFILE_NAME).optional();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The git toplevel of `repo` (default: this server's directory, null outside a repository). A `repo` given
|
|
24
|
+
* outside a repository is refused: never the global profile in its stead.
|
|
25
|
+
*/
|
|
26
|
+
const toplevel = async (repo: string | undefined): Promise<string | null> => {
|
|
27
|
+
const top = await gitToplevel(repo ?? process.cwd());
|
|
28
|
+
if (top === null && repo !== undefined)
|
|
29
|
+
throw new CatherdError("E_IO_PATH", `${repo} is not inside a git repository`, {
|
|
30
|
+
fix: "pass the path of the repository, or leave repo out for the active profile",
|
|
31
|
+
});
|
|
32
|
+
return top;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The runs `status` shows without a name: every unfinished one, else the newest. */
|
|
36
|
+
const unfinished = (v: RunView): boolean => (v.phase === null ? v.live.length > 0 : v.phase !== "ended");
|
|
37
|
+
|
|
38
|
+
/** The six tools of a session: a run is started, watched, answered, cancelled and read; the profile is its own. */
|
|
39
|
+
export function registerSessionTools(server: McpServer, deps: Deps): void {
|
|
40
|
+
server.registerTool(
|
|
41
|
+
"run_start",
|
|
42
|
+
{
|
|
43
|
+
description:
|
|
44
|
+
"Start a run that drives itself: catherd plans, dispatches, reviews, verifies and lands each milestone in a background daemon. Returns the run id and the ntfy url its questions go to.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
repo: z.string().min(1),
|
|
47
|
+
title: z.string().min(1),
|
|
48
|
+
a_lines: z.array(z.string().min(1)).min(1),
|
|
49
|
+
plan: z
|
|
50
|
+
.array(z.string().min(1))
|
|
51
|
+
.max(20)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(
|
|
54
|
+
"The reviewed plan files the run implements as written (paths, relative to repo or absolute). Without one, the architect writes a plan and the run waits for your approval.",
|
|
55
|
+
),
|
|
56
|
+
base: BaseBranchSchema.optional().describe(
|
|
57
|
+
"A branch on origin (main, staging): each milestone then gets its own branch and worktree from origin/<base>, and independent milestones run side by side; without it the run works in the repo.",
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
(a) =>
|
|
62
|
+
handle(() =>
|
|
63
|
+
runStart(deps, { repo: a.repo, title: a.title, aLines: a.a_lines, plan: a.plan, base: a.base }),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
server.registerTool(
|
|
68
|
+
"status",
|
|
69
|
+
{
|
|
70
|
+
description:
|
|
71
|
+
"Each run's phase, milestones, live roles, parked questions and quota holds, and whether its daemon is alive (a dead one is restarted). Without a run: every unfinished run, else the newest.",
|
|
72
|
+
inputSchema: { run: RUN.optional() },
|
|
73
|
+
},
|
|
74
|
+
(a) =>
|
|
75
|
+
handle(() => {
|
|
76
|
+
registerSavedSecrets();
|
|
77
|
+
const listing = a.run ? null : listRuns();
|
|
78
|
+
const views = a.run ? [runView(deps, a.run)] : listRunViews(deps);
|
|
79
|
+
const open = views.filter(unfinished);
|
|
80
|
+
const shown = a.run ? views : open.length ? open : views.slice(0, 1);
|
|
81
|
+
const queue =
|
|
82
|
+
deps.host.host === "codex" && !deps.host.conflict ? knownQueueCapability(process.env) : null;
|
|
83
|
+
return redact({
|
|
84
|
+
// a pause (the machine's, a workspace's) covers the runs shown, and comes first
|
|
85
|
+
paused: pausesOver(
|
|
86
|
+
(listing?.runs ?? [findRun(a.run as string)]).filter((r) => shown.some((v) => v.id === r.id)),
|
|
87
|
+
),
|
|
88
|
+
version: deps.version,
|
|
89
|
+
host: inspectionHost(deps.host),
|
|
90
|
+
queue,
|
|
91
|
+
runs: shown,
|
|
92
|
+
warnings: (listing?.corrupt ?? []).map((c) => `skipped run ${c.id}: ${c.reason}`),
|
|
93
|
+
});
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
server.registerTool(
|
|
98
|
+
"answer",
|
|
99
|
+
{
|
|
100
|
+
description: "Answer a parked unit (the run, a milestone or a lane): it runs again with your answer.",
|
|
101
|
+
inputSchema: { run: RUN, unit: z.string().min(1), answer: z.string().min(1) },
|
|
102
|
+
},
|
|
103
|
+
(a) => handle(() => answerUnit(deps, a)),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
server.registerTool(
|
|
107
|
+
"cancel",
|
|
108
|
+
{
|
|
109
|
+
description: "Stop the run: every live role is cancelled and the run ends.",
|
|
110
|
+
inputSchema: { run: RUN },
|
|
111
|
+
},
|
|
112
|
+
(a) => handle(() => cancelRun(deps, a)),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
server.registerTool(
|
|
116
|
+
"result",
|
|
117
|
+
{
|
|
118
|
+
description:
|
|
119
|
+
"The run's report (the MR body: digests, known issues, the judge's decisions), or with name one role's record.",
|
|
120
|
+
inputSchema: { run: RUN, name: z.string().regex(ID_PATTERN).optional() },
|
|
121
|
+
},
|
|
122
|
+
(a) => handle(() => runResult(deps, a)),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
server.registerTool(
|
|
126
|
+
"profile",
|
|
127
|
+
{
|
|
128
|
+
description:
|
|
129
|
+
"Read, validate or change a profile, or set notify_url, where parked questions are sent. Without a name: the profile this repo runs on (repo, default this server's directory).",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
action: z.enum(["get", "validate", "set"]),
|
|
132
|
+
name: PROFILE,
|
|
133
|
+
repo: z.string().min(1).optional(),
|
|
134
|
+
patch: ProfilePatchSchema.optional(),
|
|
135
|
+
notify_url: z.string().min(1).optional(),
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
(a) =>
|
|
139
|
+
handle(async () =>
|
|
140
|
+
profileTool(deps, {
|
|
141
|
+
action: a.action,
|
|
142
|
+
name: a.name,
|
|
143
|
+
repo: (await toplevel(a.repo)) ?? undefined,
|
|
144
|
+
patch: a.patch,
|
|
145
|
+
notifyUrl: a.notify_url,
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
CheckSchema,
|
|
4
|
+
IndexedPairSchema,
|
|
5
|
+
IssueSchema,
|
|
6
|
+
ProfileSavedSchema,
|
|
7
|
+
SetupViewSchema,
|
|
8
|
+
} from "../../services/panel-setup.ts";
|
|
9
|
+
import { ParkedViewSchema, RunViewSchema, TimelineEntrySchema } from "../../services/run-read.ts";
|
|
10
|
+
|
|
11
|
+
// The panel's HTTP API, in one table: it writes web/openapi.json (the web client is generated from it) and the
|
|
12
|
+
// server parses every request with the same schemas, so the document and the server cannot drift.
|
|
13
|
+
|
|
14
|
+
export const ErrorBodySchema = z.object({
|
|
15
|
+
error: z.object({ message: z.string(), type: z.string(), code: z.string(), fix: z.string().optional() }),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const Page = <T extends z.ZodType>(item: T) => z.object({ items: z.array(item), total: z.number().int() });
|
|
19
|
+
|
|
20
|
+
export const LoginBodySchema = z.object({ token: z.string().min(1).max(200) });
|
|
21
|
+
export const RungsBodySchema = z.object({ rungs: z.array(z.string().min(1)).max(50) });
|
|
22
|
+
export const AnswerBodySchema = z.object({
|
|
23
|
+
unit: z.string().min(1).max(100),
|
|
24
|
+
answer: z.string().trim().min(1).max(10000),
|
|
25
|
+
});
|
|
26
|
+
export const AnswerResultSchema = z.object({ unit: z.string(), parked: z.array(z.string()) });
|
|
27
|
+
export const CancelResultSchema = z.object({ cancelling: z.literal(true), live: z.array(z.string()) });
|
|
28
|
+
export const SessionSchema = z.object({ authenticated: z.literal(true) });
|
|
29
|
+
export const VersionSchema = z.object({ catherd: z.string() });
|
|
30
|
+
export const RunListSchema = Page(RunViewSchema);
|
|
31
|
+
export const TimelinePageSchema = Page(TimelineEntrySchema);
|
|
32
|
+
export const QuestionListSchema = Page(ParkedViewSchema);
|
|
33
|
+
|
|
34
|
+
const paging = {
|
|
35
|
+
limit: z.coerce.number().int().min(1).max(200).default(25),
|
|
36
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
37
|
+
};
|
|
38
|
+
export const ListRunsQuerySchema = z.object({
|
|
39
|
+
q: z.string().max(200).optional(),
|
|
40
|
+
phase: z.enum(["planning", "approval", "running", "ended"]).optional(),
|
|
41
|
+
...paging,
|
|
42
|
+
});
|
|
43
|
+
export const TimelineQuerySchema = z.object(paging);
|
|
44
|
+
export const QuestionsQuerySchema = z.object({
|
|
45
|
+
run: z.string().max(200).optional(),
|
|
46
|
+
q: z.string().max(200).optional(),
|
|
47
|
+
...paging,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type OperationId =
|
|
51
|
+
| "login"
|
|
52
|
+
| "logout"
|
|
53
|
+
| "getSession"
|
|
54
|
+
| "getVersion"
|
|
55
|
+
| "getSetup"
|
|
56
|
+
| "setRoleRungs"
|
|
57
|
+
| "listRuns"
|
|
58
|
+
| "getRun"
|
|
59
|
+
| "getRunTimeline"
|
|
60
|
+
| "cancelRun"
|
|
61
|
+
| "answerUnit"
|
|
62
|
+
| "listQuestions";
|
|
63
|
+
|
|
64
|
+
export interface Operation {
|
|
65
|
+
id: OperationId;
|
|
66
|
+
method: "GET" | "POST" | "PUT";
|
|
67
|
+
path: string;
|
|
68
|
+
public?: true;
|
|
69
|
+
query?: z.ZodObject;
|
|
70
|
+
body?: z.ZodType;
|
|
71
|
+
/** null: 204 */
|
|
72
|
+
response: z.ZodType | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const OPERATIONS: readonly Operation[] = [
|
|
76
|
+
{
|
|
77
|
+
id: "login",
|
|
78
|
+
method: "POST",
|
|
79
|
+
path: "/api/auth/login",
|
|
80
|
+
public: true,
|
|
81
|
+
body: LoginBodySchema,
|
|
82
|
+
response: null,
|
|
83
|
+
},
|
|
84
|
+
{ id: "logout", method: "POST", path: "/api/auth/logout", response: null },
|
|
85
|
+
{ id: "getSession", method: "GET", path: "/api/auth/session", response: SessionSchema },
|
|
86
|
+
{ id: "getVersion", method: "GET", path: "/api/system/version", response: VersionSchema },
|
|
87
|
+
{ id: "getSetup", method: "GET", path: "/api/setup", response: SetupViewSchema },
|
|
88
|
+
{
|
|
89
|
+
id: "setRoleRungs",
|
|
90
|
+
method: "PUT",
|
|
91
|
+
path: "/api/setup/roles/{role}",
|
|
92
|
+
body: RungsBodySchema,
|
|
93
|
+
response: ProfileSavedSchema,
|
|
94
|
+
},
|
|
95
|
+
{ id: "listRuns", method: "GET", path: "/api/runs", query: ListRunsQuerySchema, response: RunListSchema },
|
|
96
|
+
{ id: "getRun", method: "GET", path: "/api/runs/{id}", response: RunViewSchema },
|
|
97
|
+
{
|
|
98
|
+
id: "getRunTimeline",
|
|
99
|
+
method: "GET",
|
|
100
|
+
path: "/api/runs/{id}/timeline",
|
|
101
|
+
query: TimelineQuerySchema,
|
|
102
|
+
response: TimelinePageSchema,
|
|
103
|
+
},
|
|
104
|
+
{ id: "cancelRun", method: "POST", path: "/api/runs/{id}/cancel", response: CancelResultSchema },
|
|
105
|
+
{
|
|
106
|
+
id: "answerUnit",
|
|
107
|
+
method: "POST",
|
|
108
|
+
path: "/api/runs/{id}/answers",
|
|
109
|
+
body: AnswerBodySchema,
|
|
110
|
+
response: AnswerResultSchema,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "listQuestions",
|
|
114
|
+
method: "GET",
|
|
115
|
+
path: "/api/questions",
|
|
116
|
+
query: QuestionsQuerySchema,
|
|
117
|
+
response: QuestionListSchema,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const SCHEMAS: Record<string, z.ZodType> = {
|
|
122
|
+
RunView: RunViewSchema,
|
|
123
|
+
ParkedView: ParkedViewSchema,
|
|
124
|
+
TimelineEntry: TimelineEntrySchema,
|
|
125
|
+
SetupView: SetupViewSchema,
|
|
126
|
+
Check: CheckSchema,
|
|
127
|
+
Issue: IssueSchema,
|
|
128
|
+
IndexedPair: IndexedPairSchema,
|
|
129
|
+
ProfileSaved: ProfileSavedSchema,
|
|
130
|
+
ErrorBody: ErrorBodySchema,
|
|
131
|
+
RunList: RunListSchema,
|
|
132
|
+
TimelinePage: TimelinePageSchema,
|
|
133
|
+
QuestionList: QuestionListSchema,
|
|
134
|
+
Session: SessionSchema,
|
|
135
|
+
Version: VersionSchema,
|
|
136
|
+
LoginBody: LoginBodySchema,
|
|
137
|
+
RungsBody: RungsBodySchema,
|
|
138
|
+
AnswerBody: AnswerBodySchema,
|
|
139
|
+
AnswerResult: AnswerResultSchema,
|
|
140
|
+
CancelResult: CancelResultSchema,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const REF = "#/components/schemas/";
|
|
144
|
+
const nameOf = (s: z.ZodType): string => {
|
|
145
|
+
const hit = Object.entries(SCHEMAS).find(([, v]) => v === s);
|
|
146
|
+
if (!hit) throw new Error("an operation names a schema that panelOpenApi does not register");
|
|
147
|
+
return hit[0];
|
|
148
|
+
};
|
|
149
|
+
const refOf = (s: z.ZodType) => ({ $ref: REF + nameOf(s) });
|
|
150
|
+
const json = (s: z.ZodType) => ({ content: { "application/json": { schema: refOf(s) } } });
|
|
151
|
+
|
|
152
|
+
const without = ({ $schema: _s, $id: _i, ...rest }: Record<string, unknown>) => rest;
|
|
153
|
+
|
|
154
|
+
/** The OpenAPI 3.1 document of OPERATIONS; `info.version` is the API's, never the package's. */
|
|
155
|
+
export function panelOpenApi(): Record<string, unknown> {
|
|
156
|
+
const reg = z.registry<{ id: string }>();
|
|
157
|
+
for (const [id, s] of Object.entries(SCHEMAS)) reg.add(s, { id });
|
|
158
|
+
const out = z.toJSONSchema(reg, { uri: (id) => REF + id, io: "output" }) as {
|
|
159
|
+
schemas: Record<string, Record<string, unknown>>;
|
|
160
|
+
};
|
|
161
|
+
const schemas = Object.fromEntries(Object.entries(out.schemas).map(([k, v]) => [k, without(v)]));
|
|
162
|
+
const paths: Record<string, Record<string, unknown>> = {};
|
|
163
|
+
for (const op of OPERATIONS) {
|
|
164
|
+
const pathParams = [...op.path.matchAll(/\{(\w+)\}/g)].map((m) => ({
|
|
165
|
+
name: m[1],
|
|
166
|
+
in: "path",
|
|
167
|
+
required: true,
|
|
168
|
+
schema: { type: "string" },
|
|
169
|
+
}));
|
|
170
|
+
const queryParams = Object.entries(op.query?.shape ?? {}).map(([name, field]) => ({
|
|
171
|
+
name,
|
|
172
|
+
in: "query",
|
|
173
|
+
required: !(field as z.ZodType).safeParse(undefined).success,
|
|
174
|
+
schema: without(z.toJSONSchema(field as z.ZodType, { io: "input" }) as Record<string, unknown>),
|
|
175
|
+
}));
|
|
176
|
+
(paths[op.path] ??= {})[op.method.toLowerCase()] = {
|
|
177
|
+
operationId: op.id,
|
|
178
|
+
parameters: [...pathParams, ...queryParams],
|
|
179
|
+
...(op.body ? { requestBody: { required: true, ...json(op.body) } } : {}),
|
|
180
|
+
responses: {
|
|
181
|
+
[op.response ? "200" : "204"]: op.response
|
|
182
|
+
? { description: "ok", ...json(op.response) }
|
|
183
|
+
: { description: "no content" },
|
|
184
|
+
default: { description: "error", ...json(ErrorBodySchema) },
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return { openapi: "3.1.0", info: { title: "catherd panel", version: "1" }, paths, components: { schemas } };
|
|
189
|
+
}
|