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
package/src/entry/mcp/server.ts
CHANGED
|
@@ -4,24 +4,70 @@ 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 { errorMessage } from "../../domain/errors.ts";
|
|
7
|
+
import { type CatherdError, errorMessage } from "../../domain/errors.ts";
|
|
8
|
+
import {
|
|
9
|
+
isCoordinatorTool,
|
|
10
|
+
refusedForRole,
|
|
11
|
+
type RoleScope,
|
|
12
|
+
roleScopeError,
|
|
13
|
+
} from "../../domain/role-scope.ts";
|
|
14
|
+
import { takeBootLock } from "../../infra/boot-lock.ts";
|
|
8
15
|
import { resolveHost } from "../../infra/host-context.ts";
|
|
9
16
|
import { log } from "../../infra/log.ts";
|
|
10
17
|
import { currentSession } from "../../services/sessions.ts";
|
|
11
18
|
import { startNotifier } from "../../services/notifier.ts";
|
|
12
19
|
import type { Deps } from "../../services/ports.ts";
|
|
13
|
-
import { reconcileAll } from "../../services/reconcile.ts";
|
|
14
|
-
import {
|
|
20
|
+
import { adoptOwned, reconcileAll } from "../../services/reconcile.ts";
|
|
21
|
+
import { refreshIndex } from "../../services/catalog-service.ts";
|
|
15
22
|
import { defaultDeps } from "../deps.ts";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { registerProtocolTools } from "./protocol-tools.ts";
|
|
19
|
-
import { sdkToolError, toolOf } from "./result.ts";
|
|
20
|
-
import { registerRunTools } from "./run-tools.ts";
|
|
21
|
-
import { registerSetupTools } from "./setup-tools.ts";
|
|
23
|
+
import { handle, sdkToolError, toolOf } from "./result.ts";
|
|
24
|
+
import { registerSessionTools } from "./session-tools.ts";
|
|
22
25
|
|
|
23
26
|
type ObserveSession = (context: Deps) => (() => void) | undefined;
|
|
24
27
|
|
|
28
|
+
/** Spec 1.5 plan 21: a coordinator tool's description says, first, that only the orchestrator calls it. */
|
|
29
|
+
function orchestratorOnly(name: string, config: unknown): unknown {
|
|
30
|
+
const c = config as { description?: string };
|
|
31
|
+
return isCoordinatorTool(name) && c.description
|
|
32
|
+
? { ...c, description: `Orchestrator only (a role process gets E_ROLE_SCOPE). ${c.description}` }
|
|
33
|
+
: config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type RequestHandler = (
|
|
37
|
+
request: { params?: { name?: unknown; arguments?: unknown } },
|
|
38
|
+
extra: unknown,
|
|
39
|
+
) => unknown;
|
|
40
|
+
|
|
41
|
+
/** How often a session looks for the new reports of the 2.0 runs it owns. */
|
|
42
|
+
const REPORT_POLL_MS = 3_000;
|
|
43
|
+
|
|
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;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Spec 1.5 plan 21: in a role's process (`deps.role`) a coordinator tool call is refused with E_ROLE_SCOPE
|
|
50
|
+
* before anything else, its input check included, so a role learns at once that the tool is not its own. The
|
|
51
|
+
* SDK keeps its request handlers in a private map; test/entry/role-scope-mcp.test.ts fails if it moves.
|
|
52
|
+
*/
|
|
53
|
+
function refuseCoordinatorTools(server: McpServer, deps: Deps): void {
|
|
54
|
+
const handlers = (server.server as unknown as { _requestHandlers: Map<string, RequestHandler> })
|
|
55
|
+
._requestHandlers;
|
|
56
|
+
const callTool = handlers.get("tools/call");
|
|
57
|
+
if (!callTool) return;
|
|
58
|
+
handlers.set("tools/call", async (request, extra) => {
|
|
59
|
+
const role = deps.role;
|
|
60
|
+
const name = String(request.params?.name ?? "");
|
|
61
|
+
const refusal = role ? roleRefusal(name, request.params?.arguments, role) : null;
|
|
62
|
+
if (!refusal) return callTool(request, extra);
|
|
63
|
+
const r = await handle(() => {
|
|
64
|
+
throw refusal;
|
|
65
|
+
});
|
|
66
|
+
log("warn", "tool", { tool: name, ok: false, code: "E_ROLE_SCOPE" });
|
|
67
|
+
return r;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
25
71
|
type Handler = (...args: unknown[]) => CallToolResult | Promise<CallToolResult>;
|
|
26
72
|
|
|
27
73
|
/** Spec §10.2: every tool call is logged with its duration and outcome (the error code), never its input. */
|
|
@@ -38,7 +84,7 @@ function logToolCalls(
|
|
|
38
84
|
h: Handler,
|
|
39
85
|
) => unknown;
|
|
40
86
|
(server as unknown as { registerTool: typeof register }).registerTool = (name, config, handler) =>
|
|
41
|
-
register(name, config, async (...args: unknown[]) => {
|
|
87
|
+
register(name, orchestratorOnly(name, config), async (...args: unknown[]) => {
|
|
42
88
|
const started = Date.now();
|
|
43
89
|
const epoch = generation();
|
|
44
90
|
const extra = args.at(-1) as { _meta?: Record<string, unknown> } | undefined;
|
|
@@ -101,17 +147,20 @@ export function buildServer(deps: Deps = defaultDeps(), observe?: ObserveSession
|
|
|
101
147
|
log("warn", "tool", { tool: toolOf(message), ok: false, code });
|
|
102
148
|
return r;
|
|
103
149
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
registerDispatchTools(server, scoped);
|
|
107
|
-
registerSetupTools(server, scoped);
|
|
108
|
-
registerProtocolTools(server, scoped);
|
|
150
|
+
registerSessionTools(server, scoped);
|
|
151
|
+
refuseCoordinatorTools(server, deps);
|
|
109
152
|
return server;
|
|
110
153
|
}
|
|
111
154
|
|
|
112
155
|
/** Connect immediately; initialized triggers recovery without delaying the handshake. */
|
|
113
156
|
export async function startMcpServer(
|
|
114
|
-
o: {
|
|
157
|
+
o: {
|
|
158
|
+
transport?: Transport;
|
|
159
|
+
sync?: () => Promise<unknown>;
|
|
160
|
+
deps?: Deps;
|
|
161
|
+
/** plan 22: the boot lock (tests replace it); null while another live server holds it */
|
|
162
|
+
bootLock?: () => (() => void) | null;
|
|
163
|
+
} = {},
|
|
115
164
|
): Promise<void> {
|
|
116
165
|
const deps = o.deps ?? defaultDeps();
|
|
117
166
|
const observed = new Map<string, ReturnType<typeof startNotifier>>();
|
|
@@ -121,7 +170,7 @@ export async function startMcpServer(
|
|
|
121
170
|
const key = sessionKey(target);
|
|
122
171
|
let active = observed.get(key);
|
|
123
172
|
if (!active) {
|
|
124
|
-
active = startNotifier(context);
|
|
173
|
+
active = startNotifier(context, { reportPollMs: REPORT_POLL_MS });
|
|
125
174
|
observed.set(key, active);
|
|
126
175
|
}
|
|
127
176
|
const n = active;
|
|
@@ -134,6 +183,8 @@ export async function startMcpServer(
|
|
|
134
183
|
let generation = 0;
|
|
135
184
|
let notifier: ReturnType<typeof startNotifier> | undefined;
|
|
136
185
|
let recovery: Deps | undefined;
|
|
186
|
+
// plan 22: taken at the first initialize, held until this server closes; null: another server leads the boot
|
|
187
|
+
let boot: (() => void) | null | undefined;
|
|
137
188
|
const invalidate = () => {
|
|
138
189
|
generation++;
|
|
139
190
|
if (recovery) recovery.host = { host: "unknown", session: null, conflict: null };
|
|
@@ -143,10 +194,17 @@ export async function startMcpServer(
|
|
|
143
194
|
observed.clear();
|
|
144
195
|
closed();
|
|
145
196
|
};
|
|
146
|
-
server.server.onclose =
|
|
197
|
+
server.server.onclose = () => {
|
|
198
|
+
invalidate();
|
|
199
|
+
boot?.();
|
|
200
|
+
boot = undefined;
|
|
201
|
+
};
|
|
147
202
|
server.server.oninitialized = () => {
|
|
148
203
|
invalidate();
|
|
149
204
|
initialized();
|
|
205
|
+
boot ??= (o.bootLock ?? takeBootLock)();
|
|
206
|
+
const lead = boot !== null;
|
|
207
|
+
if (!lead) log("info", "boot", { skipped: "another catherd server runs the boot sync and reconcile" });
|
|
150
208
|
log("info", "session", {
|
|
151
209
|
host: deps.host.host,
|
|
152
210
|
conflict: deps.host.conflict,
|
|
@@ -158,18 +216,24 @@ export async function startMcpServer(
|
|
|
158
216
|
const epoch = generation;
|
|
159
217
|
recovery = { ...deps };
|
|
160
218
|
const context = recovery;
|
|
161
|
-
notifier = startNotifier(context);
|
|
219
|
+
notifier = startNotifier(context, { reportPollMs: REPORT_POLL_MS });
|
|
162
220
|
const active = notifier;
|
|
163
221
|
const target = currentSession(context);
|
|
164
222
|
if (target) observed.set(sessionKey(target), active);
|
|
165
223
|
void Promise.resolve()
|
|
166
224
|
.then(() => {
|
|
167
|
-
if (epoch === generation) return (o.sync ?? (() =>
|
|
225
|
+
if (lead && epoch === generation) return (o.sync ?? (() => refreshIndex({ auto: true })))();
|
|
168
226
|
})
|
|
169
|
-
.catch((e: unknown) => log("debug", "
|
|
227
|
+
.catch((e: unknown) => log("debug", "catalog", { error: errorMessage(e) }));
|
|
170
228
|
void (async () => {
|
|
171
229
|
try {
|
|
172
230
|
if (epoch !== generation) return;
|
|
231
|
+
// the leading server reconciles every run; this one still watches the live roles of the runs its own
|
|
232
|
+
// session owns (a coordinator's server restarted mid-run) and tells its session what it owns
|
|
233
|
+
if (!lead) {
|
|
234
|
+
adoptOwned(context);
|
|
235
|
+
return void active.scan();
|
|
236
|
+
}
|
|
173
237
|
const r = await reconcileAll(context);
|
|
174
238
|
if (epoch !== generation) return;
|
|
175
239
|
const shown = r.warnings.length;
|
|
@@ -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
|
+
}
|