@edspencer/paddock 0.0.1 → 0.57.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 +538 -0
- package/packages/server/dist/config.js +625 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/environment-prompt.d.ts +52 -0
- package/packages/server/dist/environment-prompt.js +61 -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 +623 -0
- package/packages/server/dist/herdctl.js +1279 -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 +143 -0
- package/packages/server/dist/instance-config.js +385 -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-Bypd_quL.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
- package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
- package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
- package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.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-C6Uuq-dq.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
- package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.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-B-YuzkF6.js +71 -0
- package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.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-Cs0D32Ry.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-Bc9fuI2i.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.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-BzGHkF9K.js +2 -0
- package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.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-CDmilo8S.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.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-BkUK73W0.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.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,447 @@
|
|
|
1
|
+
import { resolveAttachmentsConfig, maxFileBytes, isTypeAllowed, } from "../attachments-config.js";
|
|
2
|
+
import { inferAttachmentKind } from "../attachments-hint.js";
|
|
3
|
+
import { buildInstanceConfig, writeInstanceConfig, validatePatch, instanceConfigPath, InstanceConfigError, } from "../instance-config.js";
|
|
4
|
+
import { TranscriptionError } from "../transcribe.js";
|
|
5
|
+
import { resolveModels, resolveDefaultModel } from "../models.js";
|
|
6
|
+
import { sendProjectError } from "../route-errors.js";
|
|
7
|
+
import { cspFor, parseRangeHeader } from "../http-bytes.js";
|
|
8
|
+
export function registerMetaRoutes(app, ctx) {
|
|
9
|
+
const { herdctl, transcriber, attachments, cfg } = ctx;
|
|
10
|
+
// --- voice dictation (#voice): capability probe + transcription -------
|
|
11
|
+
// The composer polls this to decide whether to show a mic button. `available`
|
|
12
|
+
// is false on instances with dictation off (or a misconfigured remote).
|
|
13
|
+
app.get("/api/transcription", {
|
|
14
|
+
schema: {
|
|
15
|
+
tags: ["System"],
|
|
16
|
+
summary: "Voice-dictation capability probe",
|
|
17
|
+
description: "Reports whether voice dictation is enabled on this instance so the composer can decide whether to show a mic button. Returns a JSON object with `available` (boolean), `mode`, and `model`.",
|
|
18
|
+
response: {
|
|
19
|
+
200: {
|
|
20
|
+
description: "Transcription capability info.",
|
|
21
|
+
type: "object",
|
|
22
|
+
additionalProperties: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}, async () => ({
|
|
27
|
+
available: transcriber.available,
|
|
28
|
+
mode: transcriber.mode,
|
|
29
|
+
model: transcriber.model,
|
|
30
|
+
}));
|
|
31
|
+
// Transcribe a recorded audio blob (multipart `file`) → `{ text }`. The mic
|
|
32
|
+
// button records WebM/Opus in the browser and POSTs it here; the server runs
|
|
33
|
+
// whisper (remote OpenAI-compatible endpoint or local whisper.cpp).
|
|
34
|
+
app.post("/api/transcribe", {
|
|
35
|
+
schema: {
|
|
36
|
+
tags: ["System"],
|
|
37
|
+
summary: "Transcribe a recorded audio blob",
|
|
38
|
+
description: "Accepts `multipart/form-data` with a single `file` field (a recorded WebM/Opus audio blob) and runs whisper (remote OpenAI-compatible endpoint or local whisper.cpp). Returns a JSON object with `text`, `model`, `mode`, and `durationMs`. Returns 503 when dictation is disabled, 400 when no file is present, and 413 on oversize/malformed uploads.",
|
|
39
|
+
consumes: ["multipart/form-data"],
|
|
40
|
+
response: {
|
|
41
|
+
200: {
|
|
42
|
+
description: "Transcription result.",
|
|
43
|
+
type: "object",
|
|
44
|
+
additionalProperties: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}, async (req, reply) => {
|
|
49
|
+
if (!transcriber.available) {
|
|
50
|
+
return reply.code(503).send({ error: "voice dictation is not enabled on this instance" });
|
|
51
|
+
}
|
|
52
|
+
let part;
|
|
53
|
+
try {
|
|
54
|
+
part = await req.file();
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// @fastify/multipart throws on oversize / malformed uploads.
|
|
58
|
+
return reply.code(413).send({ error: err.message });
|
|
59
|
+
}
|
|
60
|
+
if (!part) {
|
|
61
|
+
return reply.code(400).send({ error: "no audio file in request" });
|
|
62
|
+
}
|
|
63
|
+
let audio;
|
|
64
|
+
try {
|
|
65
|
+
audio = await part.toBuffer();
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
// Size-limit overruns surface here too (streamed past the cap).
|
|
69
|
+
return reply.code(413).send({ error: err.message });
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const result = await transcriber.transcribe({
|
|
73
|
+
audio,
|
|
74
|
+
filename: part.filename || "dictation.webm",
|
|
75
|
+
mimeType: part.mimetype || "audio/webm",
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
text: result.text,
|
|
79
|
+
model: result.model,
|
|
80
|
+
mode: result.mode,
|
|
81
|
+
durationMs: result.durationMs,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const status = err instanceof TranscriptionError ? err.status : 502;
|
|
86
|
+
req.log.warn({ err }, "transcription failed");
|
|
87
|
+
return reply.code(status).send({ error: err.message });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// --- identity ----------------------------------------------------------
|
|
91
|
+
// The authenticated principal for this request (#189). In `none` mode this is
|
|
92
|
+
// the frozen anonymous principal (`{ username: "anonymous", anonymous: true }`);
|
|
93
|
+
// in trusted-header / jwt modes it's the real proxy/IdP identity. The web app
|
|
94
|
+
// uses it to surface who it is and to know whether read-state is user-keyed.
|
|
95
|
+
app.get("/api/me", {
|
|
96
|
+
schema: {
|
|
97
|
+
tags: ["System"],
|
|
98
|
+
summary: "Current authenticated principal",
|
|
99
|
+
description: "Returns the authenticated principal for this request. In `none` auth mode this is the frozen anonymous principal (`{ username: \"anonymous\", anonymous: true }`); in trusted-header / jwt modes it is the real proxy/IdP identity. Returns a JSON object describing the user.",
|
|
100
|
+
response: {
|
|
101
|
+
200: {
|
|
102
|
+
description: "The request principal.",
|
|
103
|
+
type: "object",
|
|
104
|
+
additionalProperties: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}, async (req) => req.user);
|
|
109
|
+
app.get("/api/health", {
|
|
110
|
+
schema: {
|
|
111
|
+
tags: ["System"],
|
|
112
|
+
summary: "Health check",
|
|
113
|
+
description: "Liveness probe. Returns a JSON object `{ ok: true }`.",
|
|
114
|
+
response: {
|
|
115
|
+
200: {
|
|
116
|
+
description: "Service is up.",
|
|
117
|
+
type: "object",
|
|
118
|
+
additionalProperties: true,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}, async () => ({ ok: true }));
|
|
123
|
+
// --- instance-wide settings (issue #385) --------------------------------
|
|
124
|
+
// A top-level admin screen over the frozen instance config. GET reports every
|
|
125
|
+
// surfaced field (value / default / editable / sensitive / env-shadow); PUT
|
|
126
|
+
// writes the editable subset to paddock.config.yaml (comment-preserving,
|
|
127
|
+
// atomic). Writes DO NOT hot-apply — the config is read once at boot and
|
|
128
|
+
// frozen — so a PUT returns `restartRequired: true` and the process keeps its
|
|
129
|
+
// current config until it restarts. This route mutates instance-wide config;
|
|
130
|
+
// it inherits whatever request-boundary auth the deployment configures (open
|
|
131
|
+
// in `none` mode, gated behind the proxy/IdP otherwise) — a role model is a
|
|
132
|
+
// follow-up per the ticket.
|
|
133
|
+
app.get("/api/instance-config", {
|
|
134
|
+
schema: {
|
|
135
|
+
tags: ["System"],
|
|
136
|
+
summary: "Read instance-wide config",
|
|
137
|
+
description: "Reports every surfaced instance-config field (value / default / editable / sensitive / env-shadow) for the top-level admin screen. Returns a JSON object keyed by field.",
|
|
138
|
+
response: {
|
|
139
|
+
200: {
|
|
140
|
+
description: "Instance config snapshot.",
|
|
141
|
+
type: "object",
|
|
142
|
+
additionalProperties: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}, async () => buildInstanceConfig(cfg));
|
|
147
|
+
app.put("/api/instance-config", {
|
|
148
|
+
schema: {
|
|
149
|
+
tags: ["System"],
|
|
150
|
+
summary: "Write instance-wide config",
|
|
151
|
+
description: "Writes the editable subset of the instance config to paddock.config.yaml (comment-preserving, atomic). Writes DO NOT hot-apply — the config is frozen at boot — so a successful write returns `{ restartRequired: true, configPath }` and the process keeps its current config until it restarts. Returns 400 on a malformed patch or invalid field, 500 on write failure.",
|
|
152
|
+
body: {
|
|
153
|
+
// Documentation-only (no validation/coercion): accept any/empty body.
|
|
154
|
+
type: ["object", "null"],
|
|
155
|
+
additionalProperties: true,
|
|
156
|
+
properties: {
|
|
157
|
+
patch: { description: "Map of config field name to new value to write." },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
response: {
|
|
161
|
+
200: {
|
|
162
|
+
description: "Write accepted; restart required to apply.",
|
|
163
|
+
type: "object",
|
|
164
|
+
additionalProperties: true,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}, async (req, reply) => {
|
|
169
|
+
const patch = req.body?.patch;
|
|
170
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
171
|
+
return reply.code(400).send({ error: "body must be { patch: { <field>: <value> } }" });
|
|
172
|
+
}
|
|
173
|
+
let pairs;
|
|
174
|
+
try {
|
|
175
|
+
pairs = validatePatch(patch);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (err instanceof InstanceConfigError) {
|
|
179
|
+
return reply.code(400).send({ error: err.message, field: err.field });
|
|
180
|
+
}
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
writeInstanceConfig(instanceConfigPath(cfg), pairs);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return reply.code(500).send({ error: `failed to write config file: ${err.message}` });
|
|
188
|
+
}
|
|
189
|
+
// The write lands in the file but NOT in the running (frozen) config.
|
|
190
|
+
return { restartRequired: true, configPath: instanceConfigPath(cfg) };
|
|
191
|
+
});
|
|
192
|
+
// Selectable models + the keeper/sweeper defaults (CONTRACT-v3 §3). The offered
|
|
193
|
+
// models are the instance ALLOW-LIST (issue #457 Step 2): the built-in catalog
|
|
194
|
+
// filtered to `cfg.models` (env `PADDOCK_MODELS` / YAML `models:`), so an
|
|
195
|
+
// operator can narrow the picker without touching the catalog's metadata. Unset
|
|
196
|
+
// ⇒ the full catalog (unchanged behaviour). `defaultModel` is the EFFECTIVE
|
|
197
|
+
// default for that list (DEFAULT_MODEL if still offered, else the first
|
|
198
|
+
// offered model). `driveModeDefault` is the box-wide
|
|
199
|
+
// `PADDOCK_DRIVE_MODE` (per instance, not static): the Settings tab shows
|
|
200
|
+
// it as the effective value a project inherits when its own `driveMode` is left
|
|
201
|
+
// on "Global default".
|
|
202
|
+
app.get("/api/models", {
|
|
203
|
+
schema: {
|
|
204
|
+
tags: ["System"],
|
|
205
|
+
summary: "Selectable models and instance defaults",
|
|
206
|
+
description: "Returns the models this instance offers (the built-in catalog filtered to the `PADDOCK_MODELS` / YAML `models:` allow-list; unset ⇒ the whole catalog) plus the default model and the box-wide defaults (drive mode, max spawn depth, recovery, attachments, curation) that projects fall back to when their own overrides are unset. `defaultModel` is the effective default for the offered list. Returns a JSON object with `models`, `defaultModel`, `driveModeDefault`, `maxSpawnDepthDefault`, `recoveryDefault`, `attachmentsDefault`, and `curationDefault`.",
|
|
207
|
+
response: {
|
|
208
|
+
200: {
|
|
209
|
+
description: "Model list and inherited defaults.",
|
|
210
|
+
type: "object",
|
|
211
|
+
additionalProperties: true,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
}, async () => {
|
|
216
|
+
const models = resolveModels(cfg.models);
|
|
217
|
+
return {
|
|
218
|
+
models,
|
|
219
|
+
defaultModel: resolveDefaultModel(models),
|
|
220
|
+
driveModeDefault: cfg.driveMode,
|
|
221
|
+
// Box-wide max spawn depth (PADDOCK_MAX_SPAWN_DEPTH) a project inherits when
|
|
222
|
+
// its own `maxSpawnDepth` is unset; shown as the effective value in Settings
|
|
223
|
+
// and used to label "Instance default" (issue #262).
|
|
224
|
+
maxSpawnDepthDefault: cfg.maxSpawnDepth,
|
|
225
|
+
// Box-wide keeper-chat recovery defaults (PADDOCK_RECOVERY_*) a project
|
|
226
|
+
// inherits when its own `recovery` override fields are unset (issue #301).
|
|
227
|
+
// The web resolves the effective value (project.recovery[field] ?? this) to
|
|
228
|
+
// gate the killed-task Continue affordance.
|
|
229
|
+
recoveryDefault: cfg.recovery,
|
|
230
|
+
// Box-wide inbound-attachment defaults (PADDOCK_ATTACHMENTS_*) a project
|
|
231
|
+
// inherits when its own `attachments` override fields are unset (issue #328).
|
|
232
|
+
// The composer resolves the effective value (project.attachments[field] ??
|
|
233
|
+
// this) to gate the picker + build the client-side accept/size guards.
|
|
234
|
+
attachmentsDefault: cfg.attachments,
|
|
235
|
+
// Box-wide sweeper-curation budgets (PADDOCK_CURATION_*) a project inherits
|
|
236
|
+
// when its own `curation` override fields are unset (issue #384). Settings
|
|
237
|
+
// shows these as the "Instance default" for each per-file token budget.
|
|
238
|
+
curationDefault: cfg.curation,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
app.get("/api/fleet", {
|
|
242
|
+
schema: {
|
|
243
|
+
tags: ["System"],
|
|
244
|
+
summary: "Fleet status and agents",
|
|
245
|
+
description: "Returns the herdctl fleet status and the list of agents. Returns a JSON object with `status` and `agents`; on failure returns `{ status: null, agents: [], error }`.",
|
|
246
|
+
response: {
|
|
247
|
+
200: {
|
|
248
|
+
description: "Fleet status and agent list.",
|
|
249
|
+
type: "object",
|
|
250
|
+
additionalProperties: true,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}, async () => {
|
|
255
|
+
try {
|
|
256
|
+
return { status: await herdctl.fleetStatus(), agents: await herdctl.agents() };
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
return { status: null, agents: [], error: err.message };
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
// Serve the RAW BYTES of a file the agent shared via `mcp__paddock__send_file`
|
|
263
|
+
// (issue #112). The bytes were copied into the attachment store AT SEND TIME
|
|
264
|
+
// and are addressed by an opaque id recorded in the chat transcript, so this
|
|
265
|
+
// endpoint only ever serves files that were explicitly sent — never an
|
|
266
|
+
// arbitrary path on the box. Used by the chat's image <img> and by the text
|
|
267
|
+
// fetch for file-kind sends, live and after reload. Locked down with the same
|
|
268
|
+
// nosniff + sandbox CSP as the project file raw endpoint.
|
|
269
|
+
//
|
|
270
|
+
// HTTP byte-range support (issue #126) is what makes an inline <video> play,
|
|
271
|
+
// especially on iOS Safari: it sends a `Range:` request and REFUSES to play if
|
|
272
|
+
// the server answers `200` with the whole body instead of `206 Partial Content`.
|
|
273
|
+
// So we always advertise `Accept-Ranges: bytes` and honor a `Range` header.
|
|
274
|
+
//
|
|
275
|
+
// CSP: the `sandbox` token is right for a directly-opened image/HTML/SVG (it
|
|
276
|
+
// stops a hostile file executing script in our origin), but it is meaningless
|
|
277
|
+
// for a media subresource and we keep it OFF for video/PDF so nothing can
|
|
278
|
+
// interfere with playback — those get a plain `default-src 'none'`. Everything
|
|
279
|
+
// else keeps the byte-for-byte `sandbox; default-src 'none'` as before.
|
|
280
|
+
app.get("/api/chat-files/:id", {
|
|
281
|
+
schema: {
|
|
282
|
+
tags: ["Attachments"],
|
|
283
|
+
summary: "Serve raw attachment bytes",
|
|
284
|
+
description: "Serves the raw file bytes of an attachment addressed by its opaque id (a file the agent shared via send_file, or a composer upload). Returns the raw bytes with `Accept-Ranges: bytes` — a `Range` request is honored with `206 Partial Content`, otherwise the full body with `200`; `416` for an unsatisfiable range and `404` when the id is unknown. Locked down with nosniff + a sandbox CSP.",
|
|
285
|
+
params: {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
id: {
|
|
289
|
+
type: "string",
|
|
290
|
+
description: "Opaque attachment id from the chat transcript.",
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
required: ["id"],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
}, async (req, reply) => {
|
|
297
|
+
const found = await attachments.read(req.params.id);
|
|
298
|
+
if (!found)
|
|
299
|
+
return reply.code(404).send({ error: "not_found" });
|
|
300
|
+
const { bytes, mime } = found;
|
|
301
|
+
const total = bytes.length;
|
|
302
|
+
const csp = cspFor(mime);
|
|
303
|
+
reply
|
|
304
|
+
.header("content-type", mime)
|
|
305
|
+
.header("content-disposition", "inline")
|
|
306
|
+
.header("x-content-type-options", "nosniff")
|
|
307
|
+
.header("content-security-policy", csp)
|
|
308
|
+
.header("cache-control", "private, max-age=300")
|
|
309
|
+
.header("accept-ranges", "bytes");
|
|
310
|
+
const range = parseRangeHeader(req.headers.range, total);
|
|
311
|
+
if (range === "unsatisfiable") {
|
|
312
|
+
// Malformed / out-of-bounds range → 416 with the resource's full size.
|
|
313
|
+
return reply.code(416).header("content-range", `bytes */${total}`).send();
|
|
314
|
+
}
|
|
315
|
+
if (range) {
|
|
316
|
+
const { start, end } = range;
|
|
317
|
+
return reply
|
|
318
|
+
.code(206)
|
|
319
|
+
.header("content-range", `bytes ${start}-${end}/${total}`)
|
|
320
|
+
.header("content-length", String(end - start + 1))
|
|
321
|
+
.send(bytes.subarray(start, end + 1));
|
|
322
|
+
}
|
|
323
|
+
// No (or unhandled) Range header → full body, 200.
|
|
324
|
+
return reply.send(bytes);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The workspace-scoped slice of this file: registered once per workspace mount
|
|
329
|
+
* (root and per project) — see `workspace-mount.ts`. Paths are relative to the
|
|
330
|
+
* mount.
|
|
331
|
+
*/
|
|
332
|
+
export function registerMetaWorkspaceRoutes(app, ctx) {
|
|
333
|
+
const { attachments, projects, cfg } = ctx;
|
|
334
|
+
// Inbound composer upload (issue #328 Phase 1). Copies each posted file's bytes
|
|
335
|
+
// into the attachment store (reusing the send_file store) and returns opaque
|
|
336
|
+
// ids the composer holds until send. The stored file DOUBLES as the on-disk copy
|
|
337
|
+
// the keeper's `Read` tool opens (via the absolute path threaded into the send
|
|
338
|
+
// prompt) AND the durable copy the transcript re-renders from (`/api/chat-files/:id`).
|
|
339
|
+
//
|
|
340
|
+
// Validation is SERVER-AUTHORITATIVE (the client mirrors it only for UX): the
|
|
341
|
+
// effective per-project config gates enabled/size/count/type. `:sessionId` is
|
|
342
|
+
// accepted for a not-yet-created chat too (a new chat has no id until its first
|
|
343
|
+
// frame) — storage is flat and doesn't need it; it only scopes the request.
|
|
344
|
+
app.post("/chats/:sessionId/upload", {
|
|
345
|
+
schema: {
|
|
346
|
+
tags: ["Attachments"],
|
|
347
|
+
summary: "Composer file upload",
|
|
348
|
+
description: "Accepts `multipart/form-data` with one or more `file` fields; copies each posted file's bytes into the attachment store and returns opaque ids the composer holds until send. Validation is server-authoritative (enabled/size/count/type per the effective project config). Returns a JSON object with `files` (each `{ id, filename, size, kind }`). Returns 403 when attachments are disabled, 413 on oversize/too-many files, 415 on a disallowed type, and 400 when no files are present.",
|
|
349
|
+
consumes: ["multipart/form-data"],
|
|
350
|
+
params: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {
|
|
353
|
+
slug: {
|
|
354
|
+
type: "string",
|
|
355
|
+
description: "Project slug.",
|
|
356
|
+
},
|
|
357
|
+
sessionId: {
|
|
358
|
+
type: "string",
|
|
359
|
+
description: "Chat session id; accepted for a not-yet-created chat too (it only scopes the request).",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
required: ["slug", "sessionId"],
|
|
363
|
+
},
|
|
364
|
+
response: {
|
|
365
|
+
200: {
|
|
366
|
+
description: "Stored attachment descriptors.",
|
|
367
|
+
type: "object",
|
|
368
|
+
additionalProperties: true,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
}, async (req, reply) => {
|
|
373
|
+
let project;
|
|
374
|
+
try {
|
|
375
|
+
project = await projects.get(req.params.slug);
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
return sendProjectError(reply, err);
|
|
379
|
+
}
|
|
380
|
+
const acfg = resolveAttachmentsConfig(project.attachments, cfg.attachments);
|
|
381
|
+
if (!acfg.enabled) {
|
|
382
|
+
return reply.code(403).send({ error: "attachments are disabled for this project" });
|
|
383
|
+
}
|
|
384
|
+
const maxBytes = maxFileBytes(acfg);
|
|
385
|
+
// Buffer + validate every part first; persist NOTHING until all pass, so a
|
|
386
|
+
// rejected file leaves the store untouched (atomic — the client's tray and
|
|
387
|
+
// the store never disagree). Memory is bounded by the size × count caps.
|
|
388
|
+
const collected = [];
|
|
389
|
+
try {
|
|
390
|
+
const parts = req.parts({
|
|
391
|
+
limits: { fileSize: maxBytes, files: acfg.maxFilesPerMessage },
|
|
392
|
+
});
|
|
393
|
+
for await (const part of parts) {
|
|
394
|
+
if (part.type !== "file")
|
|
395
|
+
continue;
|
|
396
|
+
const filename = part.filename || "upload";
|
|
397
|
+
if (!isTypeAllowed(acfg.allowedTypes, part.mimetype, filename)) {
|
|
398
|
+
// Drain the current part so the stream unwinds cleanly, then reject.
|
|
399
|
+
await part.toBuffer().catch(() => undefined);
|
|
400
|
+
return reply.code(415).send({
|
|
401
|
+
error: `file type not allowed: ${filename}${part.mimetype ? ` (${part.mimetype})` : ""}`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
let bytes;
|
|
405
|
+
try {
|
|
406
|
+
bytes = await part.toBuffer();
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// @fastify/multipart truncates + throws when a part exceeds fileSize.
|
|
410
|
+
return reply
|
|
411
|
+
.code(413)
|
|
412
|
+
.send({ error: `file too large (max ${acfg.maxFileSizeMb} MB): ${filename}` });
|
|
413
|
+
}
|
|
414
|
+
if (bytes.length > maxBytes) {
|
|
415
|
+
return reply
|
|
416
|
+
.code(413)
|
|
417
|
+
.send({ error: `file too large (max ${acfg.maxFileSizeMb} MB): ${filename}` });
|
|
418
|
+
}
|
|
419
|
+
collected.push({ filename, mimetype: part.mimetype, bytes });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
// Count-limit (too many parts) and other multipart failures land here.
|
|
424
|
+
const code = err.code;
|
|
425
|
+
if (code === "FST_FILES_LIMIT") {
|
|
426
|
+
return reply
|
|
427
|
+
.code(413)
|
|
428
|
+
.send({ error: `too many files (max ${acfg.maxFilesPerMessage} per message)` });
|
|
429
|
+
}
|
|
430
|
+
return reply.code(400).send({ error: err.message });
|
|
431
|
+
}
|
|
432
|
+
if (collected.length === 0) {
|
|
433
|
+
return reply.code(400).send({ error: "no files in request" });
|
|
434
|
+
}
|
|
435
|
+
const files = [];
|
|
436
|
+
for (const c of collected) {
|
|
437
|
+
const id = await attachments.save(c.bytes, c.filename);
|
|
438
|
+
files.push({
|
|
439
|
+
id,
|
|
440
|
+
filename: c.filename,
|
|
441
|
+
size: c.bytes.length,
|
|
442
|
+
kind: inferAttachmentKind(c.filename, c.mimetype),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return { files };
|
|
446
|
+
});
|
|
447
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project routes: the projects CRUD surface (list/create/get/update/delete +
|
|
3
|
+
* notebook→repo promote), the pin/unpin sibling-tab endpoints (#4), and the
|
|
4
|
+
* read-only file/changelog/overview/commands surface (#2/#3/#103/#259). The
|
|
5
|
+
* chat/trigger/git clusters live in their own sibling modules.
|
|
6
|
+
*/
|
|
7
|
+
import type { FastifyInstance } from "fastify";
|
|
8
|
+
import type { RouteCtx } from "../route-context.js";
|
|
9
|
+
/** One entry of a workspace's compact `chatTurns` list (see {@link buildChatTurns}). */
|
|
10
|
+
export interface ChatTurnSummary {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
lastTurnCompletedAt: string;
|
|
13
|
+
lastSeen?: number;
|
|
14
|
+
unread?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function registerProjectRoutes(app: FastifyInstance, ctx: RouteCtx): void;
|
|
17
|
+
/**
|
|
18
|
+
* The workspace-scoped project routes — everything addressed to ONE workspace.
|
|
19
|
+
*
|
|
20
|
+
* Registered twice by the composition root (see `workspace-mount.ts`): once at
|
|
21
|
+
* `/api/root` for the root workspace and once at `/api/projects/:slug` for a
|
|
22
|
+
* project. Paths here are relative to the workspace; handlers still read
|
|
23
|
+
* `req.params.slug`, which the root mount injects as `""`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerProjectWorkspaceRoutes(app: FastifyInstance, ctx: RouteCtx): void;
|