@edspencer/paddock 0.0.1 → 0.56.0
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 +69 -0
- package/install-notice.mjs +29 -0
- package/package.json +58 -5
- package/packages/server/dist/adoptable.d.ts +127 -0
- package/packages/server/dist/adoptable.js +315 -0
- package/packages/server/dist/app.d.ts +67 -0
- package/packages/server/dist/app.js +325 -0
- package/packages/server/dist/archive.d.ts +42 -0
- package/packages/server/dist/archive.js +125 -0
- package/packages/server/dist/attachments-config.d.ts +114 -0
- package/packages/server/dist/attachments-config.js +165 -0
- package/packages/server/dist/attachments-hint.d.ts +53 -0
- package/packages/server/dist/attachments-hint.js +140 -0
- package/packages/server/dist/attachments.d.ts +33 -0
- package/packages/server/dist/attachments.js +117 -0
- package/packages/server/dist/auth.d.ts +84 -0
- package/packages/server/dist/auth.js +247 -0
- package/packages/server/dist/background.d.ts +36 -0
- package/packages/server/dist/background.js +137 -0
- package/packages/server/dist/bind-safety.d.ts +56 -0
- package/packages/server/dist/bind-safety.js +51 -0
- package/packages/server/dist/brand.d.ts +44 -0
- package/packages/server/dist/brand.js +88 -0
- package/packages/server/dist/chat-dto.d.ts +210 -0
- package/packages/server/dist/chat-dto.js +308 -0
- package/packages/server/dist/cli/import-chats.d.ts +21 -0
- package/packages/server/dist/cli/import-chats.js +197 -0
- package/packages/server/dist/cli/paddock.d.ts +2 -0
- package/packages/server/dist/cli/paddock.js +247 -0
- package/packages/server/dist/concurrency.d.ts +57 -0
- package/packages/server/dist/concurrency.js +71 -0
- package/packages/server/dist/config.d.ts +512 -0
- package/packages/server/dist/config.js +593 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/event-bus.d.ts +87 -0
- package/packages/server/dist/event-bus.js +48 -0
- package/packages/server/dist/git.d.ts +128 -0
- package/packages/server/dist/git.js +446 -0
- package/packages/server/dist/github-auth.d.ts +44 -0
- package/packages/server/dist/github-auth.js +169 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
- package/packages/server/dist/herdctl-agent-config.js +331 -0
- package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
- package/packages/server/dist/herdctl-agent-names.js +228 -0
- package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
- package/packages/server/dist/herdctl-jobs-index.js +225 -0
- package/packages/server/dist/herdctl-jobs.d.ts +98 -0
- package/packages/server/dist/herdctl-jobs.js +225 -0
- package/packages/server/dist/herdctl.d.ts +572 -0
- package/packages/server/dist/herdctl.js +1218 -0
- package/packages/server/dist/hook-config.d.ts +217 -0
- package/packages/server/dist/hook-config.js +224 -0
- package/packages/server/dist/http-bytes.d.ts +24 -0
- package/packages/server/dist/http-bytes.js +52 -0
- package/packages/server/dist/index.d.ts +1 -0
- package/packages/server/dist/index.js +14 -0
- package/packages/server/dist/instance-config.d.ts +135 -0
- package/packages/server/dist/instance-config.js +345 -0
- package/packages/server/dist/localcommand.d.ts +23 -0
- package/packages/server/dist/localcommand.js +140 -0
- package/packages/server/dist/management-auth.d.ts +99 -0
- package/packages/server/dist/management-auth.js +165 -0
- package/packages/server/dist/management-config.d.ts +164 -0
- package/packages/server/dist/management-config.js +286 -0
- package/packages/server/dist/management-mcp-server.d.ts +20 -0
- package/packages/server/dist/management-mcp-server.js +95 -0
- package/packages/server/dist/management-metadata.d.ts +67 -0
- package/packages/server/dist/management-metadata.js +56 -0
- package/packages/server/dist/management-ops.d.ts +110 -0
- package/packages/server/dist/management-ops.js +449 -0
- package/packages/server/dist/management-policy.d.ts +172 -0
- package/packages/server/dist/management-policy.js +259 -0
- package/packages/server/dist/message-provenance.d.ts +112 -0
- package/packages/server/dist/message-provenance.js +260 -0
- package/packages/server/dist/models.d.ts +164 -0
- package/packages/server/dist/models.js +208 -0
- package/packages/server/dist/openapi.d.ts +66 -0
- package/packages/server/dist/openapi.js +183 -0
- package/packages/server/dist/parent-detach.d.ts +30 -0
- package/packages/server/dist/parent-detach.js +109 -0
- package/packages/server/dist/preload.d.ts +43 -0
- package/packages/server/dist/preload.js +55 -0
- package/packages/server/dist/project-files.d.ts +76 -0
- package/packages/server/dist/project-files.js +191 -0
- package/packages/server/dist/project-mime.d.ts +35 -0
- package/packages/server/dist/project-mime.js +55 -0
- package/packages/server/dist/project-paths.d.ts +88 -0
- package/packages/server/dist/project-paths.js +126 -0
- package/packages/server/dist/project-types.d.ts +316 -0
- package/packages/server/dist/project-types.js +48 -0
- package/packages/server/dist/projects.d.ts +202 -0
- package/packages/server/dist/projects.js +894 -0
- package/packages/server/dist/queued-message.d.ts +35 -0
- package/packages/server/dist/queued-message.js +116 -0
- package/packages/server/dist/read-state.d.ts +46 -0
- package/packages/server/dist/read-state.js +139 -0
- package/packages/server/dist/recovery-config.d.ts +111 -0
- package/packages/server/dist/recovery-config.js +105 -0
- package/packages/server/dist/recovery.d.ts +262 -0
- package/packages/server/dist/recovery.js +490 -0
- package/packages/server/dist/resume-drain.d.ts +122 -0
- package/packages/server/dist/resume-drain.js +158 -0
- package/packages/server/dist/route-context.d.ts +168 -0
- package/packages/server/dist/route-context.js +39 -0
- package/packages/server/dist/route-errors.d.ts +8 -0
- package/packages/server/dist/route-errors.js +13 -0
- package/packages/server/dist/routes/chats.d.ts +12 -0
- package/packages/server/dist/routes/chats.js +1406 -0
- package/packages/server/dist/routes/git.d.ts +13 -0
- package/packages/server/dist/routes/git.js +236 -0
- package/packages/server/dist/routes/mcp.d.ts +83 -0
- package/packages/server/dist/routes/mcp.js +250 -0
- package/packages/server/dist/routes/meta.d.ts +15 -0
- package/packages/server/dist/routes/meta.js +447 -0
- package/packages/server/dist/routes/projects.d.ts +25 -0
- package/packages/server/dist/routes/projects.js +736 -0
- package/packages/server/dist/routes/triggers.d.ts +21 -0
- package/packages/server/dist/routes/triggers.js +324 -0
- package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
- package/packages/server/dist/routes/workspace-mount.js +32 -0
- package/packages/server/dist/routes.d.ts +26 -0
- package/packages/server/dist/routes.js +29 -0
- package/packages/server/dist/run-provenance.d.ts +109 -0
- package/packages/server/dist/run-provenance.js +206 -0
- package/packages/server/dist/runs.d.ts +92 -0
- package/packages/server/dist/runs.js +72 -0
- package/packages/server/dist/schedule-config.d.ts +68 -0
- package/packages/server/dist/schedule-config.js +75 -0
- package/packages/server/dist/schedule-session.d.ts +34 -0
- package/packages/server/dist/schedule-session.js +120 -0
- package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
- package/packages/server/dist/self-mcp-descriptions.js +145 -0
- package/packages/server/dist/self-mcp-read.d.ts +15 -0
- package/packages/server/dist/self-mcp-read.js +97 -0
- package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
- package/packages/server/dist/self-mcp-triggers.js +144 -0
- package/packages/server/dist/self-mcp-types.d.ts +248 -0
- package/packages/server/dist/self-mcp-types.js +10 -0
- package/packages/server/dist/self-mcp-util.d.ts +73 -0
- package/packages/server/dist/self-mcp-util.js +157 -0
- package/packages/server/dist/self-mcp-write.d.ts +35 -0
- package/packages/server/dist/self-mcp-write.js +215 -0
- package/packages/server/dist/self-mcp.d.ts +95 -0
- package/packages/server/dist/self-mcp.js +448 -0
- package/packages/server/dist/send-file-mcp.d.ts +46 -0
- package/packages/server/dist/send-file-mcp.js +281 -0
- package/packages/server/dist/session-hub.d.ts +186 -0
- package/packages/server/dist/session-hub.js +291 -0
- package/packages/server/dist/spawn-capability.d.ts +99 -0
- package/packages/server/dist/spawn-capability.js +106 -0
- package/packages/server/dist/star.d.ts +26 -0
- package/packages/server/dist/star.js +94 -0
- package/packages/server/dist/start.d.ts +1 -0
- package/packages/server/dist/start.js +21 -0
- package/packages/server/dist/subagents.d.ts +337 -0
- package/packages/server/dist/subagents.js +626 -0
- package/packages/server/dist/sweep.d.ts +122 -0
- package/packages/server/dist/sweep.js +696 -0
- package/packages/server/dist/tooldetails.d.ts +33 -0
- package/packages/server/dist/tooldetails.js +466 -0
- package/packages/server/dist/transcribe.d.ts +69 -0
- package/packages/server/dist/transcribe.js +119 -0
- package/packages/server/dist/transcripts.d.ts +42 -0
- package/packages/server/dist/transcripts.js +164 -0
- package/packages/server/dist/trigger-config.d.ts +278 -0
- package/packages/server/dist/trigger-config.js +417 -0
- package/packages/server/dist/trigger-runtime.d.ts +94 -0
- package/packages/server/dist/trigger-runtime.js +82 -0
- package/packages/server/dist/trigger-session.d.ts +34 -0
- package/packages/server/dist/trigger-session.js +122 -0
- package/packages/server/dist/triggers.d.ts +83 -0
- package/packages/server/dist/triggers.js +124 -0
- package/packages/server/dist/trusted-proxy.d.ts +57 -0
- package/packages/server/dist/trusted-proxy.js +157 -0
- package/packages/server/dist/turn-notice.d.ts +154 -0
- package/packages/server/dist/turn-notice.js +271 -0
- package/packages/server/dist/unread.d.ts +36 -0
- package/packages/server/dist/unread.js +123 -0
- package/packages/server/dist/usage.d.ts +66 -0
- package/packages/server/dist/usage.js +243 -0
- package/packages/server/dist/wake-injection.d.ts +156 -0
- package/packages/server/dist/wake-injection.js +136 -0
- package/packages/server/dist/whisper-local.d.ts +9 -0
- package/packages/server/dist/whisper-local.js +122 -0
- package/packages/server/dist/ws-context.d.ts +158 -0
- package/packages/server/dist/ws-context.js +1 -0
- package/packages/server/dist/ws-protocol.d.ts +423 -0
- package/packages/server/dist/ws-protocol.js +251 -0
- package/packages/server/dist/ws-self-mcp.d.ts +52 -0
- package/packages/server/dist/ws-self-mcp.js +28 -0
- package/packages/server/dist/ws-triggers.d.ts +29 -0
- package/packages/server/dist/ws-triggers.js +261 -0
- package/packages/server/dist/ws-turn.d.ts +86 -0
- package/packages/server/dist/ws-turn.js +751 -0
- package/packages/server/dist/ws.d.ts +62 -0
- package/packages/server/dist/ws.js +1024 -0
- package/packages/server/package.json +6 -0
- package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-CJZFLj_8.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-BHwVbbqB.js +2 -0
- package/packages/web/dist/assets/ProjectView-BOvuAITg.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-BCojZOGY.js +2 -0
- package/packages/web/dist/assets/arc-BVyxvnkl.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-Cd9Puqg1.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-B_AQt_Rx.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-7e0TQtXB.js +11 -0
- package/packages/web/dist/assets/channel-C_1aISNn.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-Dz4WZmUZ.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-DVWYldo6.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-BIMrizQd.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-CL2q9fF1.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-BSq3HlQ9.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-Bgt64pLE.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-C8Pr2A38.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-BAOqh-MN.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +2 -0
- package/packages/web/dist/assets/core-lJeGACQg.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-BF4tameA.js +2 -0
- package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
- package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
- package/packages/web/dist/assets/css-DazXZka4.js +2 -0
- package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
- package/packages/web/dist/assets/dagre-BM42HDAG-DdsLM6ql.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-BQ78S_RM.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-CAKE6nHE.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-fEvHRXgr.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-CtWsgeSm.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-Buog8k7_.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-CWvVVPZS.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-DsCGXtp4.js +163 -0
- package/packages/web/dist/assets/format-DamJQ9LO.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-CsdwIvvC.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-hZvrX1sD.js +107 -0
- package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
- package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
- package/packages/web/dist/assets/index-CGbZdrm8.js +71 -0
- package/packages/web/dist/assets/index-DJDQ210-.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-nE_Vtz_l.js +3 -0
- package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
- package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
- package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js +71 -0
- package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
- package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
- package/packages/web/dist/assets/journeyDiagram-JHISSGLW-mjU1fMc4.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-C_9xNaa3.js +90 -0
- package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
- package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
- package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
- package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
- package/packages/web/dist/assets/linear-BxKd91ho.js +2 -0
- package/packages/web/dist/assets/mermaid.core-C-CuhD8I.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bs6yfcZv.js +97 -0
- package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
- package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
- package/packages/web/dist/assets/pieDiagram-4H26LBE5-DNdqX7O5.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-RmxMdm2q.js +85 -0
- package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
- package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
- package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-OAAhWcfK.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-BWF7rCyN.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-M3b_zqqo.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-BpOCh63N.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-D_iPH45Q.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-DVD9I2Om.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-Bn1IKwjn.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-DwtI0bkR.js +8 -0
- package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
- package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/packages/web/dist/icons/apple-touch-icon.png +0 -0
- package/packages/web/dist/icons/favicon-16.png +0 -0
- package/packages/web/dist/icons/favicon-32.png +0 -0
- package/packages/web/dist/icons/icon-192.png +0 -0
- package/packages/web/dist/icons/icon-512.png +0 -0
- package/packages/web/dist/icons/maskable-512.png +0 -0
- package/packages/web/dist/index.html +56 -0
- package/packages/web/dist/manifest.webmanifest +32 -0
- package/packages/web/dist/sw.js +139 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type PaddockConfig } from "./config.js";
|
|
2
|
+
/** Groups the Settings screen renders, in display order. */
|
|
3
|
+
export declare const GROUPS: {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}[];
|
|
8
|
+
type FieldType = "number" | "boolean" | "string" | "enum" | "string-list";
|
|
9
|
+
/** Outcome of coercing a raw patch value into a persistable one. */
|
|
10
|
+
type Coerced = {
|
|
11
|
+
ok: true;
|
|
12
|
+
value: unknown;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* One editable-or-displayed instance-config field. `key` is a stable dotted path
|
|
19
|
+
* used both to read the resolved value out of {@link PaddockConfig} and as the
|
|
20
|
+
* write path into the YAML document — the two shapes match by construction.
|
|
21
|
+
*/
|
|
22
|
+
interface FieldSpec {
|
|
23
|
+
/** Stable dotted id (also the YAML/`PaddockConfig` path). */
|
|
24
|
+
key: string;
|
|
25
|
+
group: string;
|
|
26
|
+
label: string;
|
|
27
|
+
help?: string;
|
|
28
|
+
type: FieldType;
|
|
29
|
+
/** Allowed values for `enum` fields. */
|
|
30
|
+
enumValues?: readonly string[];
|
|
31
|
+
/** Env var(s) that shadow this field; the first set one is reported. */
|
|
32
|
+
envVars: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* Env-shadow semantics. Default (`false`) = the var shadows only when set to a
|
|
35
|
+
* non-blank value, matching `envOr`/`envOpt` in config.ts. Set `true` for
|
|
36
|
+
* `browserMcp`, whose loader (`loadBrowserMcp`) treats ANY defined value —
|
|
37
|
+
* including blank — as authoritative (`env !== undefined`), so a defined-but-
|
|
38
|
+
* blank `PADDOCK_BROWSER_MCP` forces `false` and must render read-only here too.
|
|
39
|
+
*/
|
|
40
|
+
envShadowWhenDefined?: boolean;
|
|
41
|
+
/** Built-in default (what you get with neither env nor file). `null` ⇒ unset. */
|
|
42
|
+
default: unknown;
|
|
43
|
+
/** Whether the UI may edit + PUT this field. Read-only fields are display-only. */
|
|
44
|
+
editable: boolean;
|
|
45
|
+
/** Semi-sensitive (shown with a caution note); never carries a secret value. */
|
|
46
|
+
sensitive?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Validate + coerce a raw JSON patch value into the value written to YAML.
|
|
49
|
+
* Only defined for editable fields. Mirrors the loader's acceptance so a UI
|
|
50
|
+
* write can't produce a file the loader would reject (it would just degrade).
|
|
51
|
+
*/
|
|
52
|
+
coerce?: (raw: unknown) => Coerced;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The single source of truth for the instance-settings surface. Order within a
|
|
56
|
+
* group is display order. Editable fields carry a `coerce`; read-only fields
|
|
57
|
+
* don't. NO field here holds a secret value (that's why auth JWT internals and
|
|
58
|
+
* the transcription API key are absent).
|
|
59
|
+
*/
|
|
60
|
+
export declare const FIELDS: readonly FieldSpec[];
|
|
61
|
+
/** One field's DTO in the GET response. */
|
|
62
|
+
export interface InstanceConfigFieldDto {
|
|
63
|
+
key: string;
|
|
64
|
+
group: string;
|
|
65
|
+
label: string;
|
|
66
|
+
help?: string;
|
|
67
|
+
type: FieldType;
|
|
68
|
+
enumValues?: readonly string[];
|
|
69
|
+
value: unknown;
|
|
70
|
+
default: unknown;
|
|
71
|
+
editable: boolean;
|
|
72
|
+
sensitive: boolean;
|
|
73
|
+
envOverridden: boolean;
|
|
74
|
+
/** The env var shadowing this field (only when `envOverridden`). */
|
|
75
|
+
envVar?: string;
|
|
76
|
+
}
|
|
77
|
+
export interface InstanceConfigGroupDto {
|
|
78
|
+
id: string;
|
|
79
|
+
label: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
fields: InstanceConfigFieldDto[];
|
|
82
|
+
}
|
|
83
|
+
export interface InstanceConfigDto {
|
|
84
|
+
groups: InstanceConfigGroupDto[];
|
|
85
|
+
/** Absolute path of the file a PUT writes to (informational). */
|
|
86
|
+
configPath: string;
|
|
87
|
+
/** Always false — instance config is frozen at boot; edits need a restart. */
|
|
88
|
+
restartRequired: false;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the path a PUT writes to: an explicit `PADDOCK_CONFIG` env var wins
|
|
92
|
+
* (the same rule {@link import("./config.js").loadConfigFile} reads it back
|
|
93
|
+
* from), else `<dataDir>/paddock.config.yaml`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function instanceConfigPath(cfg: PaddockConfig): string;
|
|
96
|
+
/**
|
|
97
|
+
* Build the grouped GET DTO from the resolved (frozen) config + live env. Each
|
|
98
|
+
* field reports its current `value`, built-in `default`, and whether an env var
|
|
99
|
+
* currently shadows it (so the UI renders env-shadowed fields read-only).
|
|
100
|
+
*/
|
|
101
|
+
export declare function buildInstanceConfig(cfg: PaddockConfig): InstanceConfigDto;
|
|
102
|
+
/** A rejected PUT: which field failed and why (surfaced as a 400). */
|
|
103
|
+
export declare class InstanceConfigError extends Error {
|
|
104
|
+
readonly field?: string | undefined;
|
|
105
|
+
constructor(message: string, field?: string | undefined);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validate a patch of editable fields and coerce each value. Throws
|
|
109
|
+
* {@link InstanceConfigError} on the first offending field:
|
|
110
|
+
* - an unknown key,
|
|
111
|
+
* - a read-only / non-editable key,
|
|
112
|
+
* - a value the field's `coerce` rejects.
|
|
113
|
+
* Returns the coerced `{ key, value }` pairs ready to write. A field whose value
|
|
114
|
+
* is `null` (an editable optional being cleared) is returned with `value: null`
|
|
115
|
+
* so the writer can delete it from the file.
|
|
116
|
+
*/
|
|
117
|
+
export declare function validatePatch(patch: Record<string, unknown>): {
|
|
118
|
+
key: string;
|
|
119
|
+
value: unknown;
|
|
120
|
+
}[];
|
|
121
|
+
/**
|
|
122
|
+
* Write the validated pairs into `paddock.config.yaml`, preserving operator
|
|
123
|
+
* comments and unmanaged keys. Uses the `yaml` `Document` API to round-trip an
|
|
124
|
+
* existing file (or start a fresh document when none exists), then writes
|
|
125
|
+
* atomically (temp + rename). A `null` value deletes that key (clearing an
|
|
126
|
+
* optional back to its default). Returns the path written.
|
|
127
|
+
*
|
|
128
|
+
* Reuses no loader state — the caller passes the target path (from
|
|
129
|
+
* {@link instanceConfigPath}) so tests can point it anywhere.
|
|
130
|
+
*/
|
|
131
|
+
export declare function writeInstanceConfig(configPath: string, pairs: {
|
|
132
|
+
key: string;
|
|
133
|
+
value: unknown;
|
|
134
|
+
}[]): void;
|
|
135
|
+
export {};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance-wide settings surface (issue #385).
|
|
3
|
+
*
|
|
4
|
+
* Paddock's instance config (`PaddockConfig`) is resolved ONCE at boot and
|
|
5
|
+
* `Object.freeze`d — env over an optional `paddock.config.yaml` file over
|
|
6
|
+
* built-in defaults (see config.ts). Until now the only way to change a knob was
|
|
7
|
+
* to hand-edit that YAML (or an env var) and restart. This module backs a
|
|
8
|
+
* top-level admin Settings screen that reads the resolved config and writes the
|
|
9
|
+
* editable subset back to `paddock.config.yaml`.
|
|
10
|
+
*
|
|
11
|
+
* Three properties the screen depends on, enforced here:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Restart-required.** Writes go to the file only; the running process keeps
|
|
14
|
+
* its frozen config. Every field is `restart` in effect — the UI shows a
|
|
15
|
+
* persistent "takes effect after restart" banner.
|
|
16
|
+
* 2. **Env precedence.** `env > file > default`. A field also set by a
|
|
17
|
+
* `PADDOCK_*` env var is SHADOWED — writing it to the file has no effect
|
|
18
|
+
* while the env var is set. {@link buildInstanceConfig} reports `envOverridden`
|
|
19
|
+
* per field so the UI renders those read-only.
|
|
20
|
+
* 3. **Comment-preserving write.** {@link writeInstanceConfig} round-trips the
|
|
21
|
+
* file through the `yaml` `Document` API (not parse+stringify), so operator
|
|
22
|
+
* comments and any keys we don't manage survive. The write is atomic
|
|
23
|
+
* (temp + rename), and the file is created on first write if absent.
|
|
24
|
+
*
|
|
25
|
+
* Only the fields in {@link FIELDS} are exposed, and only those marked `editable`
|
|
26
|
+
* may be written — path/infra bindings (port, dataDir, …) and auth are read-only
|
|
27
|
+
* display. Secret values (a transcription API key, auth JWT internals) are never
|
|
28
|
+
* put in the descriptor table, so they can't leak into the API response.
|
|
29
|
+
*/
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import { parseDocument } from "yaml";
|
|
33
|
+
import { DRIVE_MODES, DEFAULT_DRIVE_MODE, MODELS, isKnownModel } from "./models.js";
|
|
34
|
+
import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capability.js";
|
|
35
|
+
import { DEFAULT_RECOVERY } from "./recovery-config.js";
|
|
36
|
+
import { DEFAULT_ATTACHMENTS, sanitizeAllowedTypes } from "./attachments-config.js";
|
|
37
|
+
import { DEFAULT_CURATION } from "./curation-config.js";
|
|
38
|
+
/** Groups the Settings screen renders, in display order. */
|
|
39
|
+
export const GROUPS = [
|
|
40
|
+
{ id: "curation", label: "Curation", description: "Per-file token budgets the post-turn sweeper keeps its curated files under." },
|
|
41
|
+
{ id: "sweeper", label: "Sweeper" },
|
|
42
|
+
{ id: "capabilities", label: "Capabilities", description: "What keeper agents are allowed to do. Most default off." },
|
|
43
|
+
{ id: "recovery", label: "Recovery" },
|
|
44
|
+
{ id: "attachments", label: "Attachments" },
|
|
45
|
+
{ id: "branding", label: "Branding" },
|
|
46
|
+
{ id: "transcription", label: "Transcription" },
|
|
47
|
+
{ id: "git", label: "Git identity" },
|
|
48
|
+
{ id: "logging", label: "Logging" },
|
|
49
|
+
{ id: "advanced", label: "Advanced (read-only)", description: "Process / filesystem bindings. Change these via env/redeploy, not the UI." },
|
|
50
|
+
];
|
|
51
|
+
// --- coercion helpers -------------------------------------------------------
|
|
52
|
+
const asBool = (raw) => typeof raw === "boolean" ? { ok: true, value: raw } : { ok: false, error: "must be a boolean" };
|
|
53
|
+
const posInt = (raw) => {
|
|
54
|
+
const n = Number(raw);
|
|
55
|
+
return Number.isFinite(n) && Number.isInteger(n) && n > 0
|
|
56
|
+
? { ok: true, value: n }
|
|
57
|
+
: { ok: false, error: "must be a positive integer" };
|
|
58
|
+
};
|
|
59
|
+
const nonNegInt = (raw) => {
|
|
60
|
+
const n = Number(raw);
|
|
61
|
+
return Number.isFinite(n) && Number.isInteger(n) && n >= 0
|
|
62
|
+
? { ok: true, value: n }
|
|
63
|
+
: { ok: false, error: "must be a non-negative integer" };
|
|
64
|
+
};
|
|
65
|
+
/** A non-negative number, OR null/empty-string to clear the override. */
|
|
66
|
+
const optNonNegNumber = (raw) => {
|
|
67
|
+
if (raw === null || raw === "" || raw === undefined)
|
|
68
|
+
return { ok: true, value: null };
|
|
69
|
+
const n = Number(raw);
|
|
70
|
+
return Number.isFinite(n) && n >= 0
|
|
71
|
+
? { ok: true, value: n }
|
|
72
|
+
: { ok: false, error: "must be a non-negative number (or blank to use the default)" };
|
|
73
|
+
};
|
|
74
|
+
const nonEmptyString = (raw) => typeof raw === "string" && raw.trim().length > 0
|
|
75
|
+
? { ok: true, value: raw.trim() }
|
|
76
|
+
: { ok: false, error: "must be a non-empty string" };
|
|
77
|
+
/** A string that may be blank (blank clears the override → falls back to default). */
|
|
78
|
+
const optString = (raw) => typeof raw === "string" ? { ok: true, value: raw.trim() } : { ok: false, error: "must be a string" };
|
|
79
|
+
const oneOf = (values) => (raw) => typeof raw === "string" && values.includes(raw)
|
|
80
|
+
? { ok: true, value: raw }
|
|
81
|
+
: { ok: false, error: `must be one of: ${values.join(", ")}` };
|
|
82
|
+
const spawnDepth = (raw) => {
|
|
83
|
+
const n = Number(raw);
|
|
84
|
+
return isValidMaxSpawnDepth(n)
|
|
85
|
+
? { ok: true, value: n }
|
|
86
|
+
: { ok: false, error: "must be a small non-negative integer" };
|
|
87
|
+
};
|
|
88
|
+
const hexColor = (raw) => typeof raw === "string" && /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(raw.trim())
|
|
89
|
+
? { ok: true, value: raw.trim() }
|
|
90
|
+
: { ok: false, error: "must be a hex color like #c2603c" };
|
|
91
|
+
const stringList = (raw) => {
|
|
92
|
+
const list = sanitizeAllowedTypes(raw);
|
|
93
|
+
return list ? { ok: true, value: list } : { ok: false, error: "must be a non-empty list of type/extension strings" };
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Coerce a raw model allow-list (issue #457 Step 2) into a de-duped list of
|
|
97
|
+
* KNOWN catalog ids. Accepts a real array or a comma-separated string. Rejects
|
|
98
|
+
* (400) an unknown id — operators pick from the built-in catalog by id, so a typo
|
|
99
|
+
* surfaces rather than silently offering nothing — and rejects an empty result
|
|
100
|
+
* (write at least one, or clear the whole field to fall back to the full catalog).
|
|
101
|
+
*/
|
|
102
|
+
const modelList = (raw) => {
|
|
103
|
+
const arr = Array.isArray(raw) ? raw : typeof raw === "string" ? raw.split(",") : null;
|
|
104
|
+
if (!arr)
|
|
105
|
+
return { ok: false, error: "must be a list of model ids" };
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const ids = [];
|
|
108
|
+
for (const item of arr) {
|
|
109
|
+
if (typeof item !== "string")
|
|
110
|
+
continue;
|
|
111
|
+
const id = item.trim();
|
|
112
|
+
if (!id || seen.has(id))
|
|
113
|
+
continue;
|
|
114
|
+
if (!isKnownModel(id))
|
|
115
|
+
return { ok: false, error: `unknown model id: ${id}` };
|
|
116
|
+
seen.add(id);
|
|
117
|
+
ids.push(id);
|
|
118
|
+
}
|
|
119
|
+
return ids.length > 0
|
|
120
|
+
? { ok: true, value: ids }
|
|
121
|
+
: { ok: false, error: "must include at least one known model id" };
|
|
122
|
+
};
|
|
123
|
+
const LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
124
|
+
// --- field catalog ----------------------------------------------------------
|
|
125
|
+
/**
|
|
126
|
+
* The single source of truth for the instance-settings surface. Order within a
|
|
127
|
+
* group is display order. Editable fields carry a `coerce`; read-only fields
|
|
128
|
+
* don't. NO field here holds a secret value (that's why auth JWT internals and
|
|
129
|
+
* the transcription API key are absent).
|
|
130
|
+
*/
|
|
131
|
+
export const FIELDS = [
|
|
132
|
+
// Curation — the motivating knobs (issue #379).
|
|
133
|
+
{ key: "curation.overviewMaxTokens", group: "curation", label: "OVERVIEW.md max tokens", type: "number", envVars: ["PADDOCK_CURATION_OVERVIEW_MAX_TOKENS"], default: DEFAULT_CURATION.overviewMaxTokens, editable: true, coerce: posInt },
|
|
134
|
+
{ key: "curation.changelogMaxTokens", group: "curation", label: "CHANGELOG.md max tokens", type: "number", envVars: ["PADDOCK_CURATION_CHANGELOG_MAX_TOKENS"], default: DEFAULT_CURATION.changelogMaxTokens, editable: true, coerce: posInt },
|
|
135
|
+
{ key: "curation.claudeMaxTokens", group: "curation", label: "CLAUDE.md max tokens", type: "number", envVars: ["PADDOCK_CURATION_CLAUDEMD_MAX_TOKENS"], default: DEFAULT_CURATION.claudeMaxTokens, editable: true, coerce: posInt },
|
|
136
|
+
// Sweeper.
|
|
137
|
+
{ key: "sweepMinIntervalMs", group: "sweeper", label: "Min sweep interval (ms)", help: "Minimum ms between post-turn sweeps for one project. Blank = default (5 min).", type: "number", envVars: ["PADDOCK_SWEEP_MIN_INTERVAL_MS"], default: null, editable: true, coerce: optNonNegNumber },
|
|
138
|
+
// Capabilities.
|
|
139
|
+
{ key: "driveMode", group: "capabilities", label: "Keeper drive mode", help: "session = persistent (streaming + cross-turn autonomy); batch = legacy one-shot.", type: "enum", enumValues: DRIVE_MODES, envVars: ["PADDOCK_DRIVE_MODE"], default: DEFAULT_DRIVE_MODE, editable: true, coerce: oneOf(DRIVE_MODES) },
|
|
140
|
+
{ key: "models", group: "capabilities", label: "Offered models", help: "Which built-in catalog models the picker offers, by id (e.g. claude-opus-5, claude-sonnet-5). Blank = offer all catalog models.", type: "string-list", envVars: ["PADDOCK_MODELS"], default: MODELS.map((m) => m.id), editable: true, coerce: modelList },
|
|
141
|
+
{ key: "nativeSystemPrompt", group: "capabilities", label: "Native system prompt", help: "Use Claude Code's native prompt + CLAUDE.md hierarchy (recommended).", type: "boolean", envVars: ["PADDOCK_NATIVE_PROMPT"], default: true, editable: true, coerce: asBool },
|
|
142
|
+
{ key: "selfMcpEnabled", group: "capabilities", label: "Self-management MCP (read)", help: "Let keepers list/read projects and other chats.", type: "boolean", envVars: ["PADDOCK_SELF_MCP"], default: false, editable: true, coerce: asBool },
|
|
143
|
+
{ key: "selfMcpWriteEnabled", group: "capabilities", label: "Self-management MCP (write)", help: "Let keepers create/fork/message chats (needs read enabled too).", type: "boolean", envVars: ["PADDOCK_SELF_MCP_WRITE"], default: false, editable: true, coerce: asBool },
|
|
144
|
+
{ key: "selfMcpProjectsEnabled", group: "capabilities", label: "Self-management MCP (projects)", help: "Let keepers create whole new projects, cloning a repo when repo-backed (needs self-MCP write).", type: "boolean", envVars: ["PADDOCK_SELF_MCP_PROJECTS"], default: false, editable: true, coerce: asBool },
|
|
145
|
+
{ key: "maxSpawnDepth", group: "capabilities", label: "Max spawn depth", help: "How deep a spawn tree may grow before children lose the self-MCP.", type: "number", envVars: ["PADDOCK_MAX_SPAWN_DEPTH"], default: DEFAULT_MAX_SPAWN_DEPTH, editable: true, coerce: spawnDepth },
|
|
146
|
+
{ key: "scheduleMutationEnabled", group: "capabilities", label: "Schedule mutation", help: "Allow programmatic schedule add/remove at runtime.", type: "boolean", envVars: ["PADDOCK_SCHEDULE_MUTATION"], default: false, editable: true, coerce: asBool },
|
|
147
|
+
{ key: "hooksMcpEnabled", group: "capabilities", label: "Hooks MCP", help: "Let agents declare/edit their own event hooks (needs self-MCP write).", type: "boolean", envVars: ["PADDOCK_HOOKS_MCP"], default: false, editable: true, coerce: asBool },
|
|
148
|
+
{ key: "browserMcp", group: "capabilities", label: "Browser MCP (Playwright)", help: "Give agents a headless Chromium browser MCP.", type: "boolean", envVars: ["PADDOCK_BROWSER_MCP"], envShadowWhenDefined: true, default: false, editable: true, coerce: asBool },
|
|
149
|
+
// Recovery (issue #301).
|
|
150
|
+
{ key: "recovery.surfaceKilledTask", group: "recovery", label: "Surface killed task", type: "boolean", envVars: ["PADDOCK_RECOVERY_SURFACE"], default: DEFAULT_RECOVERY.surfaceKilledTask, editable: true, coerce: asBool },
|
|
151
|
+
{ key: "recovery.autoReDrive", group: "recovery", label: "Auto re-drive", type: "boolean", envVars: ["PADDOCK_RECOVERY_AUTODRIVE"], default: DEFAULT_RECOVERY.autoReDrive, editable: true, coerce: asBool },
|
|
152
|
+
{ key: "recovery.debounceMs", group: "recovery", label: "Debounce (ms)", type: "number", envVars: ["PADDOCK_RECOVERY_DEBOUNCE_MS"], default: DEFAULT_RECOVERY.debounceMs, editable: true, coerce: nonNegInt },
|
|
153
|
+
{ key: "recovery.maxRetries", group: "recovery", label: "Max retries", type: "number", envVars: ["PADDOCK_RECOVERY_MAX_RETRIES"], default: DEFAULT_RECOVERY.maxRetries, editable: true, coerce: nonNegInt },
|
|
154
|
+
{ key: "recovery.limboTimeoutMs", group: "recovery", label: "Limbo timeout (ms)", type: "number", envVars: ["PADDOCK_RECOVERY_LIMBO_MS"], default: DEFAULT_RECOVERY.limboTimeoutMs, editable: true, coerce: nonNegInt },
|
|
155
|
+
// Attachments (issue #328).
|
|
156
|
+
{ key: "attachments.enabled", group: "attachments", label: "Enabled", type: "boolean", envVars: ["PADDOCK_ATTACHMENTS_ENABLED"], default: DEFAULT_ATTACHMENTS.enabled, editable: true, coerce: asBool },
|
|
157
|
+
{ key: "attachments.maxFileSizeMb", group: "attachments", label: "Max file size (MB)", type: "number", envVars: ["PADDOCK_ATTACHMENTS_MAX_FILE_SIZE_MB"], default: DEFAULT_ATTACHMENTS.maxFileSizeMb, editable: true, coerce: posInt },
|
|
158
|
+
{ key: "attachments.maxFilesPerMessage", group: "attachments", label: "Max files / message", type: "number", envVars: ["PADDOCK_ATTACHMENTS_MAX_FILES_PER_MESSAGE"], default: DEFAULT_ATTACHMENTS.maxFilesPerMessage, editable: true, coerce: posInt },
|
|
159
|
+
{ key: "attachments.allowedTypes", group: "attachments", label: "Allowed types", help: "MIME types / extensions (e.g. image/*, .pdf). * = allow all.", type: "string-list", envVars: ["PADDOCK_ATTACHMENTS_ALLOWED_TYPES"], default: [...DEFAULT_ATTACHMENTS.allowedTypes], editable: true, coerce: stringList },
|
|
160
|
+
// Branding (issue #34).
|
|
161
|
+
{ key: "brand.name", group: "branding", label: "Name", type: "string", envVars: ["PADDOCK_BRAND_NAME"], default: "Paddock", editable: true, coerce: nonEmptyString },
|
|
162
|
+
{ key: "brand.logo", group: "branding", label: "Logo", help: "An emoji/glyph, or a URL/path to an image.", type: "string", envVars: ["PADDOCK_BRAND_LOGO"], default: "🐎", editable: true, coerce: nonEmptyString },
|
|
163
|
+
{ key: "brand.accent", group: "branding", label: "Accent color", type: "string", envVars: ["PADDOCK_BRAND_ACCENT"], default: "#c2603c", editable: true, coerce: hexColor },
|
|
164
|
+
// Transcription (voice dictation). endpoint is semi-sensitive; apiKey is a
|
|
165
|
+
// secret and deliberately NOT surfaced here.
|
|
166
|
+
{ key: "transcription.mode", group: "transcription", label: "Mode", type: "enum", enumValues: ["off", "local", "remote"], envVars: ["PADDOCK_WHISPER_MODE"], default: "off", editable: true, coerce: oneOf(["off", "local", "remote"]) },
|
|
167
|
+
{ key: "transcription.model", group: "transcription", label: "Model", type: "string", envVars: ["PADDOCK_WHISPER_MODEL"], default: "base", editable: true, coerce: nonEmptyString },
|
|
168
|
+
{ key: "transcription.endpoint", group: "transcription", label: "Endpoint", help: "remote mode: OpenAI-compatible base URL.", type: "string", envVars: ["PADDOCK_WHISPER_ENDPOINT"], default: null, editable: true, sensitive: true, coerce: optString },
|
|
169
|
+
// Git identity.
|
|
170
|
+
{ key: "gitAuthor.name", group: "git", label: "Author name", type: "string", envVars: ["PADDOCK_GIT_AUTHOR_NAME"], default: "Paddock", editable: true, coerce: nonEmptyString },
|
|
171
|
+
{ key: "gitAuthor.email", group: "git", label: "Author email", type: "string", envVars: ["PADDOCK_GIT_AUTHOR_EMAIL"], default: "paddock@localhost", editable: true, coerce: nonEmptyString },
|
|
172
|
+
// Logging.
|
|
173
|
+
{ key: "logLevel", group: "logging", label: "Log level", type: "enum", enumValues: LOG_LEVELS, envVars: ["LOG_LEVEL"], default: "info", editable: true, coerce: oneOf(LOG_LEVELS) },
|
|
174
|
+
// Advanced — read-only display (process / filesystem bindings).
|
|
175
|
+
{ key: "port", group: "advanced", label: "Port", type: "number", envVars: ["PORT"], default: 4000, editable: false },
|
|
176
|
+
{ key: "host", group: "advanced", label: "Host", type: "string", envVars: ["HOST", "PADDOCK_HOST"], default: "127.0.0.1", editable: false },
|
|
177
|
+
{ key: "dataDir", group: "advanced", label: "Data dir", type: "string", envVars: ["PADDOCK_DATA_DIR"], default: null, editable: false },
|
|
178
|
+
{ key: "projectsRoot", group: "advanced", label: "Projects root", type: "string", envVars: ["PADDOCK_PROJECTS_DIR"], default: null, editable: false },
|
|
179
|
+
{ key: "stateDir", group: "advanced", label: "State dir", type: "string", envVars: ["PADDOCK_STATE_DIR"], default: null, editable: false },
|
|
180
|
+
{ key: "herdctlConfigPath", group: "advanced", label: "herdctl config path", type: "string", envVars: ["PADDOCK_HERDCTL_CONFIG"], default: null, editable: false },
|
|
181
|
+
{ key: "webDist", group: "advanced", label: "Web dist", type: "string", envVars: ["PADDOCK_WEB_DIST"], default: null, editable: false },
|
|
182
|
+
// Auth: read-only in v1 (misconfig can lock everyone out — issue #385). Only
|
|
183
|
+
// the mode is surfaced; JWT/JWKS internals stay out of the API.
|
|
184
|
+
{ key: "auth.mode", group: "advanced", label: "Auth mode", type: "string", envVars: ["PADDOCK_AUTH_MODE"], default: "none", editable: false, sensitive: true },
|
|
185
|
+
// GitHub client id: not a secret, but semi-sensitive — read-only display.
|
|
186
|
+
{ key: "githubClientId", group: "advanced", label: "GitHub client id", type: "string", envVars: ["PADDOCK_GITHUB_CLIENT_ID"], default: null, editable: false, sensitive: true },
|
|
187
|
+
];
|
|
188
|
+
/** Fast lookup + editable allowlist for the PUT path. */
|
|
189
|
+
const FIELD_BY_KEY = new Map(FIELDS.map((f) => [f.key, f]));
|
|
190
|
+
/** Read a dotted path out of an object, tolerating missing intermediates. */
|
|
191
|
+
function readPath(obj, dotted) {
|
|
192
|
+
let cur = obj;
|
|
193
|
+
for (const seg of dotted.split(".")) {
|
|
194
|
+
if (cur == null || typeof cur !== "object")
|
|
195
|
+
return undefined;
|
|
196
|
+
cur = cur[seg];
|
|
197
|
+
}
|
|
198
|
+
return cur;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The env var currently shadowing a field, or undefined. By default a var
|
|
202
|
+
* shadows only when set to a non-blank value (matching `envOr`/`envOpt`); when
|
|
203
|
+
* `whenDefined` is set, ANY defined value shadows (matching `loadBrowserMcp`'s
|
|
204
|
+
* `env !== undefined` semantics — a defined-but-blank var still forces `false`).
|
|
205
|
+
*/
|
|
206
|
+
function envOverride(envVars, whenDefined = false) {
|
|
207
|
+
for (const name of envVars) {
|
|
208
|
+
const v = process.env[name];
|
|
209
|
+
if (v === undefined)
|
|
210
|
+
continue;
|
|
211
|
+
if (whenDefined || v.trim().length > 0)
|
|
212
|
+
return name;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Default filename for the instance-config file, resolved under the data dir.
|
|
218
|
+
* Kept in sync with config.ts's private constant (both point at the same file);
|
|
219
|
+
* duplicated here so this module needn't reach into config.ts internals.
|
|
220
|
+
*/
|
|
221
|
+
const DEFAULT_CONFIG_FILENAME = "paddock.config.yaml";
|
|
222
|
+
/**
|
|
223
|
+
* Resolve the path a PUT writes to: an explicit `PADDOCK_CONFIG` env var wins
|
|
224
|
+
* (the same rule {@link import("./config.js").loadConfigFile} reads it back
|
|
225
|
+
* from), else `<dataDir>/paddock.config.yaml`.
|
|
226
|
+
*/
|
|
227
|
+
export function instanceConfigPath(cfg) {
|
|
228
|
+
const explicit = process.env.PADDOCK_CONFIG;
|
|
229
|
+
if (explicit && explicit.trim().length > 0) {
|
|
230
|
+
const p = explicit.trim();
|
|
231
|
+
return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
|
|
232
|
+
}
|
|
233
|
+
return path.join(cfg.dataDir, DEFAULT_CONFIG_FILENAME);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Build the grouped GET DTO from the resolved (frozen) config + live env. Each
|
|
237
|
+
* field reports its current `value`, built-in `default`, and whether an env var
|
|
238
|
+
* currently shadows it (so the UI renders env-shadowed fields read-only).
|
|
239
|
+
*/
|
|
240
|
+
export function buildInstanceConfig(cfg) {
|
|
241
|
+
const groups = GROUPS.map((g) => ({
|
|
242
|
+
id: g.id,
|
|
243
|
+
label: g.label,
|
|
244
|
+
description: g.description,
|
|
245
|
+
fields: [],
|
|
246
|
+
}));
|
|
247
|
+
const groupById = new Map(groups.map((g) => [g.id, g]));
|
|
248
|
+
for (const f of FIELDS) {
|
|
249
|
+
const shadow = envOverride(f.envVars, f.envShadowWhenDefined);
|
|
250
|
+
const raw = readPath(cfg, f.key);
|
|
251
|
+
const dto = {
|
|
252
|
+
key: f.key,
|
|
253
|
+
group: f.group,
|
|
254
|
+
label: f.label,
|
|
255
|
+
help: f.help,
|
|
256
|
+
type: f.type,
|
|
257
|
+
enumValues: f.enumValues,
|
|
258
|
+
// `undefined` (e.g. sweepMinIntervalMs unset, optional endpoint) → null so
|
|
259
|
+
// it JSON-serializes as an explicit absence rather than dropping the key.
|
|
260
|
+
value: raw === undefined ? null : raw,
|
|
261
|
+
default: f.default,
|
|
262
|
+
editable: f.editable,
|
|
263
|
+
sensitive: f.sensitive ?? false,
|
|
264
|
+
envOverridden: shadow !== undefined,
|
|
265
|
+
envVar: shadow,
|
|
266
|
+
};
|
|
267
|
+
groupById.get(f.group)?.fields.push(dto);
|
|
268
|
+
}
|
|
269
|
+
return { groups, configPath: instanceConfigPath(cfg), restartRequired: false };
|
|
270
|
+
}
|
|
271
|
+
/** A rejected PUT: which field failed and why (surfaced as a 400). */
|
|
272
|
+
export class InstanceConfigError extends Error {
|
|
273
|
+
field;
|
|
274
|
+
constructor(message, field) {
|
|
275
|
+
super(message);
|
|
276
|
+
this.field = field;
|
|
277
|
+
this.name = "InstanceConfigError";
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Validate a patch of editable fields and coerce each value. Throws
|
|
282
|
+
* {@link InstanceConfigError} on the first offending field:
|
|
283
|
+
* - an unknown key,
|
|
284
|
+
* - a read-only / non-editable key,
|
|
285
|
+
* - a value the field's `coerce` rejects.
|
|
286
|
+
* Returns the coerced `{ key, value }` pairs ready to write. A field whose value
|
|
287
|
+
* is `null` (an editable optional being cleared) is returned with `value: null`
|
|
288
|
+
* so the writer can delete it from the file.
|
|
289
|
+
*/
|
|
290
|
+
export function validatePatch(patch) {
|
|
291
|
+
const out = [];
|
|
292
|
+
for (const [key, raw] of Object.entries(patch)) {
|
|
293
|
+
const spec = FIELD_BY_KEY.get(key);
|
|
294
|
+
if (!spec)
|
|
295
|
+
throw new InstanceConfigError(`Unknown setting: ${key}`, key);
|
|
296
|
+
if (!spec.editable || !spec.coerce) {
|
|
297
|
+
throw new InstanceConfigError(`Setting is read-only: ${key}`, key);
|
|
298
|
+
}
|
|
299
|
+
const res = spec.coerce(raw);
|
|
300
|
+
if (!res.ok)
|
|
301
|
+
throw new InstanceConfigError(`${spec.label} (${key}) ${res.error}`, key);
|
|
302
|
+
out.push({ key, value: res.value });
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Write the validated pairs into `paddock.config.yaml`, preserving operator
|
|
308
|
+
* comments and unmanaged keys. Uses the `yaml` `Document` API to round-trip an
|
|
309
|
+
* existing file (or start a fresh document when none exists), then writes
|
|
310
|
+
* atomically (temp + rename). A `null` value deletes that key (clearing an
|
|
311
|
+
* optional back to its default). Returns the path written.
|
|
312
|
+
*
|
|
313
|
+
* Reuses no loader state — the caller passes the target path (from
|
|
314
|
+
* {@link instanceConfigPath}) so tests can point it anywhere.
|
|
315
|
+
*/
|
|
316
|
+
export function writeInstanceConfig(configPath, pairs) {
|
|
317
|
+
let raw = "";
|
|
318
|
+
try {
|
|
319
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
if (err.code !== "ENOENT")
|
|
323
|
+
throw err;
|
|
324
|
+
// Missing file → create-on-first-write (a fresh, comment-free document).
|
|
325
|
+
}
|
|
326
|
+
const doc = parseDocument(raw);
|
|
327
|
+
// An empty / comments-only file parses to a null document body; give it a map
|
|
328
|
+
// so setIn has somewhere to place keys.
|
|
329
|
+
if (doc.contents == null)
|
|
330
|
+
doc.contents = doc.createNode({});
|
|
331
|
+
for (const { key, value } of pairs) {
|
|
332
|
+
const p = key.split(".");
|
|
333
|
+
if (value === null)
|
|
334
|
+
doc.deleteIn(p);
|
|
335
|
+
else
|
|
336
|
+
doc.setIn(p, value);
|
|
337
|
+
}
|
|
338
|
+
const serialized = doc.toString();
|
|
339
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
340
|
+
// Atomic write: same-dir temp file + rename, so a reader never sees a
|
|
341
|
+
// half-written config (rename is atomic on the same filesystem).
|
|
342
|
+
const tmp = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
343
|
+
fs.writeFileSync(tmp, serialized, "utf8");
|
|
344
|
+
fs.renameSync(tmp, configPath);
|
|
345
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type EnrichedMessage } from "./subagents.js";
|
|
2
|
+
/** One recovered `local_command` stdout entry, anchored to its command echo. */
|
|
3
|
+
interface LocalCommandOutput {
|
|
4
|
+
/** The originating `<command-name>` echo's uuid (this entry's `parentUuid`). */
|
|
5
|
+
parentUuid?: string;
|
|
6
|
+
/** This entry's own uuid — a stable, reload-consistent key for the web. */
|
|
7
|
+
uuid?: string;
|
|
8
|
+
/** ISO timestamp copied from the source line (falls back to the anchor's). */
|
|
9
|
+
timestamp?: string;
|
|
10
|
+
/** The raw `<local-command-stdout>…</local-command-stdout>` content. */
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
/** Recover the dropped `local_command` stdout entries (file order) from a session. */
|
|
14
|
+
export declare function readLocalCommandOutputs(projectDir: string, sessionId: string): Promise<LocalCommandOutput[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Re-inject recovered `local_command` stdout as `role:"user"` messages (issue
|
|
17
|
+
* #158), each placed right after the `<command-name>` echo it belongs to (matched
|
|
18
|
+
* by `parentUuid` → the echo message's `uuid`). An output whose anchor isn't in the
|
|
19
|
+
* parsed messages (e.g. it was trimmed by a `limit`) is dropped rather than
|
|
20
|
+
* mis-positioned. A transcript with no local commands passes through untouched.
|
|
21
|
+
*/
|
|
22
|
+
export declare function enrichWithLocalCommands(projectDir: string, sessionId: string, messages: EnrichedMessage[]): Promise<EnrichedMessage[]>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local (client/TUI) slash-command output recovery — issue #158.
|
|
3
|
+
*
|
|
4
|
+
* The client-local display commands `/context` and `/usage` (and friends) render
|
|
5
|
+
* their output into a transcript entry that @herdctl/core's parser DROPS: a
|
|
6
|
+
* `type:"system"`, `subtype:"local_command"` line whose `content` is a
|
|
7
|
+
* `<local-command-stdout>…</local-command-stdout>` block (e.g. `/context`'s usage
|
|
8
|
+
* table). core keeps only `user`/`assistant` entries (`jsonl-parser.js`), and the
|
|
9
|
+
* live path is no better — the same output arrives as a `model:"<synthetic>"`
|
|
10
|
+
* assistant message that @herdctl/chat's translator treats as a placeholder and
|
|
11
|
+
* skips. So the output silently vanishes in BOTH paths, leaving only the
|
|
12
|
+
* `<command-name>` echo chip (already handled, #106) — the "renders as an empty /
|
|
13
|
+
* user bubble with no result" bug in #158.
|
|
14
|
+
*
|
|
15
|
+
* This pass mirrors the established raw-transcript recovery (`tooldetails.ts`,
|
|
16
|
+
* `subagents.ts`): it streams the raw JSONL, recovers each dropped `local_command`
|
|
17
|
+
* entry, and RE-INJECTS it as a plain `role:"user"` message carrying the original
|
|
18
|
+
* `<local-command-stdout>…` string, positioned right after the `<command-name>`
|
|
19
|
+
* echo it belongs to (matched by `parentUuid`). The web renderer already detects
|
|
20
|
+
* that block (`localCommandStdout`, ChatPane `commandOutput`) and shows it as a
|
|
21
|
+
* clean, labeled command-output block — so a reloaded chat renders the output
|
|
22
|
+
* consistently with the live surfacing (`ws.ts`), instead of dropping it. mtime
|
|
23
|
+
* cached; a transcript with no local commands is an early no-op.
|
|
24
|
+
*/
|
|
25
|
+
import { createReadStream } from "node:fs";
|
|
26
|
+
import { createInterface } from "node:readline";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { projectChatsDir } from "./transcripts.js";
|
|
29
|
+
import { SAFE_SEGMENT, statMtimeMs, mtimeCacheGet, mtimeCacheSet, } from "./subagents.js";
|
|
30
|
+
const outputsCache = new Map();
|
|
31
|
+
/** Recover the dropped `local_command` stdout entries (file order) from a session. */
|
|
32
|
+
export async function readLocalCommandOutputs(projectDir, sessionId) {
|
|
33
|
+
if (!SAFE_SEGMENT.test(sessionId))
|
|
34
|
+
return [];
|
|
35
|
+
return readFromFile(path.join(projectChatsDir(projectDir), `${sessionId}.jsonl`));
|
|
36
|
+
}
|
|
37
|
+
async function readFromFile(file) {
|
|
38
|
+
const mtimeMs = await statMtimeMs(file);
|
|
39
|
+
if (mtimeMs !== undefined) {
|
|
40
|
+
const cached = mtimeCacheGet(outputsCache, file, mtimeMs);
|
|
41
|
+
if (cached.hit)
|
|
42
|
+
return cached.value;
|
|
43
|
+
}
|
|
44
|
+
const value = await readFromFileUncached(file);
|
|
45
|
+
if (mtimeMs !== undefined)
|
|
46
|
+
mtimeCacheSet(outputsCache, file, mtimeMs, value);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
async function readFromFileUncached(file) {
|
|
50
|
+
const out = [];
|
|
51
|
+
const stream = createReadStream(file, { encoding: "utf8" });
|
|
52
|
+
stream.on("error", () => undefined);
|
|
53
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
54
|
+
try {
|
|
55
|
+
for await (const line of rl) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
continue;
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(trimmed);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (parsed.type !== "system" || parsed.subtype !== "local_command")
|
|
67
|
+
continue;
|
|
68
|
+
// The output block. Recent CC nests it in a `<local-command-stdout>` wrapper;
|
|
69
|
+
// keep the wrapper so the SAME web detector (`localCommandStdout`) that guards
|
|
70
|
+
// a user-entry form handles the recovered form identically.
|
|
71
|
+
//
|
|
72
|
+
// Only recover a block with non-empty inner text: mirror the web detector
|
|
73
|
+
// (`localCommandStdout`) and the live path (`extractLocalCommandOutput`), both
|
|
74
|
+
// of which drop empty blocks — otherwise an empty `<local-command-stdout></…>`
|
|
75
|
+
// (a display-only command that produced nothing) would be re-injected only to
|
|
76
|
+
// fall through to a raw-XML user bubble on reload, the exact bug #158 fixes.
|
|
77
|
+
const content = typeof parsed.content === "string" ? parsed.content : undefined;
|
|
78
|
+
const inner = content
|
|
79
|
+
? /^\s*<local-command-stdout>([\s\S]*)<\/local-command-stdout>\s*$/.exec(content)?.[1]?.trim()
|
|
80
|
+
: undefined;
|
|
81
|
+
if (!content || !inner)
|
|
82
|
+
continue;
|
|
83
|
+
out.push({
|
|
84
|
+
parentUuid: typeof parsed.parentUuid === "string" ? parsed.parentUuid : undefined,
|
|
85
|
+
uuid: typeof parsed.uuid === "string" ? parsed.uuid : undefined,
|
|
86
|
+
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : undefined,
|
|
87
|
+
content,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
rl.close();
|
|
96
|
+
stream.destroy();
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Re-inject recovered `local_command` stdout as `role:"user"` messages (issue
|
|
102
|
+
* #158), each placed right after the `<command-name>` echo it belongs to (matched
|
|
103
|
+
* by `parentUuid` → the echo message's `uuid`). An output whose anchor isn't in the
|
|
104
|
+
* parsed messages (e.g. it was trimmed by a `limit`) is dropped rather than
|
|
105
|
+
* mis-positioned. A transcript with no local commands passes through untouched.
|
|
106
|
+
*/
|
|
107
|
+
export async function enrichWithLocalCommands(projectDir, sessionId, messages) {
|
|
108
|
+
const outputs = await readLocalCommandOutputs(projectDir, sessionId);
|
|
109
|
+
if (outputs.length === 0)
|
|
110
|
+
return messages;
|
|
111
|
+
// Group recovered outputs by the uuid of the command echo they follow.
|
|
112
|
+
const byAnchor = new Map();
|
|
113
|
+
for (const o of outputs) {
|
|
114
|
+
if (!o.parentUuid)
|
|
115
|
+
continue;
|
|
116
|
+
const arr = byAnchor.get(o.parentUuid);
|
|
117
|
+
if (arr)
|
|
118
|
+
arr.push(o);
|
|
119
|
+
else
|
|
120
|
+
byAnchor.set(o.parentUuid, [o]);
|
|
121
|
+
}
|
|
122
|
+
if (byAnchor.size === 0)
|
|
123
|
+
return messages;
|
|
124
|
+
const result = [];
|
|
125
|
+
for (const m of messages) {
|
|
126
|
+
result.push(m);
|
|
127
|
+
const anchored = m.uuid ? byAnchor.get(m.uuid) : undefined;
|
|
128
|
+
if (!anchored)
|
|
129
|
+
continue;
|
|
130
|
+
for (const o of anchored) {
|
|
131
|
+
result.push({
|
|
132
|
+
role: "user",
|
|
133
|
+
content: o.content,
|
|
134
|
+
timestamp: o.timestamp ?? m.timestamp,
|
|
135
|
+
uuid: o.uuid,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|