@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.
Files changed (294) hide show
  1. package/README.md +69 -0
  2. package/install-notice.mjs +29 -0
  3. package/package.json +58 -5
  4. package/packages/server/dist/adoptable.d.ts +127 -0
  5. package/packages/server/dist/adoptable.js +315 -0
  6. package/packages/server/dist/app.d.ts +67 -0
  7. package/packages/server/dist/app.js +325 -0
  8. package/packages/server/dist/archive.d.ts +42 -0
  9. package/packages/server/dist/archive.js +125 -0
  10. package/packages/server/dist/attachments-config.d.ts +114 -0
  11. package/packages/server/dist/attachments-config.js +165 -0
  12. package/packages/server/dist/attachments-hint.d.ts +53 -0
  13. package/packages/server/dist/attachments-hint.js +140 -0
  14. package/packages/server/dist/attachments.d.ts +33 -0
  15. package/packages/server/dist/attachments.js +117 -0
  16. package/packages/server/dist/auth.d.ts +84 -0
  17. package/packages/server/dist/auth.js +247 -0
  18. package/packages/server/dist/background.d.ts +36 -0
  19. package/packages/server/dist/background.js +137 -0
  20. package/packages/server/dist/bind-safety.d.ts +56 -0
  21. package/packages/server/dist/bind-safety.js +51 -0
  22. package/packages/server/dist/brand.d.ts +44 -0
  23. package/packages/server/dist/brand.js +88 -0
  24. package/packages/server/dist/chat-dto.d.ts +210 -0
  25. package/packages/server/dist/chat-dto.js +308 -0
  26. package/packages/server/dist/cli/import-chats.d.ts +21 -0
  27. package/packages/server/dist/cli/import-chats.js +197 -0
  28. package/packages/server/dist/cli/paddock.d.ts +2 -0
  29. package/packages/server/dist/cli/paddock.js +247 -0
  30. package/packages/server/dist/concurrency.d.ts +57 -0
  31. package/packages/server/dist/concurrency.js +71 -0
  32. package/packages/server/dist/config.d.ts +512 -0
  33. package/packages/server/dist/config.js +593 -0
  34. package/packages/server/dist/curation-config.d.ts +55 -0
  35. package/packages/server/dist/curation-config.js +64 -0
  36. package/packages/server/dist/event-bus.d.ts +87 -0
  37. package/packages/server/dist/event-bus.js +48 -0
  38. package/packages/server/dist/git.d.ts +128 -0
  39. package/packages/server/dist/git.js +446 -0
  40. package/packages/server/dist/github-auth.d.ts +44 -0
  41. package/packages/server/dist/github-auth.js +169 -0
  42. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  43. package/packages/server/dist/herdctl-agent-config.js +331 -0
  44. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  45. package/packages/server/dist/herdctl-agent-names.js +228 -0
  46. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  47. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  48. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  49. package/packages/server/dist/herdctl-jobs.js +225 -0
  50. package/packages/server/dist/herdctl.d.ts +572 -0
  51. package/packages/server/dist/herdctl.js +1218 -0
  52. package/packages/server/dist/hook-config.d.ts +217 -0
  53. package/packages/server/dist/hook-config.js +224 -0
  54. package/packages/server/dist/http-bytes.d.ts +24 -0
  55. package/packages/server/dist/http-bytes.js +52 -0
  56. package/packages/server/dist/index.d.ts +1 -0
  57. package/packages/server/dist/index.js +14 -0
  58. package/packages/server/dist/instance-config.d.ts +135 -0
  59. package/packages/server/dist/instance-config.js +345 -0
  60. package/packages/server/dist/localcommand.d.ts +23 -0
  61. package/packages/server/dist/localcommand.js +140 -0
  62. package/packages/server/dist/management-auth.d.ts +99 -0
  63. package/packages/server/dist/management-auth.js +165 -0
  64. package/packages/server/dist/management-config.d.ts +164 -0
  65. package/packages/server/dist/management-config.js +286 -0
  66. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  67. package/packages/server/dist/management-mcp-server.js +95 -0
  68. package/packages/server/dist/management-metadata.d.ts +67 -0
  69. package/packages/server/dist/management-metadata.js +56 -0
  70. package/packages/server/dist/management-ops.d.ts +110 -0
  71. package/packages/server/dist/management-ops.js +449 -0
  72. package/packages/server/dist/management-policy.d.ts +172 -0
  73. package/packages/server/dist/management-policy.js +259 -0
  74. package/packages/server/dist/message-provenance.d.ts +112 -0
  75. package/packages/server/dist/message-provenance.js +260 -0
  76. package/packages/server/dist/models.d.ts +164 -0
  77. package/packages/server/dist/models.js +208 -0
  78. package/packages/server/dist/openapi.d.ts +66 -0
  79. package/packages/server/dist/openapi.js +183 -0
  80. package/packages/server/dist/parent-detach.d.ts +30 -0
  81. package/packages/server/dist/parent-detach.js +109 -0
  82. package/packages/server/dist/preload.d.ts +43 -0
  83. package/packages/server/dist/preload.js +55 -0
  84. package/packages/server/dist/project-files.d.ts +76 -0
  85. package/packages/server/dist/project-files.js +191 -0
  86. package/packages/server/dist/project-mime.d.ts +35 -0
  87. package/packages/server/dist/project-mime.js +55 -0
  88. package/packages/server/dist/project-paths.d.ts +88 -0
  89. package/packages/server/dist/project-paths.js +126 -0
  90. package/packages/server/dist/project-types.d.ts +316 -0
  91. package/packages/server/dist/project-types.js +48 -0
  92. package/packages/server/dist/projects.d.ts +202 -0
  93. package/packages/server/dist/projects.js +894 -0
  94. package/packages/server/dist/queued-message.d.ts +35 -0
  95. package/packages/server/dist/queued-message.js +116 -0
  96. package/packages/server/dist/read-state.d.ts +46 -0
  97. package/packages/server/dist/read-state.js +139 -0
  98. package/packages/server/dist/recovery-config.d.ts +111 -0
  99. package/packages/server/dist/recovery-config.js +105 -0
  100. package/packages/server/dist/recovery.d.ts +262 -0
  101. package/packages/server/dist/recovery.js +490 -0
  102. package/packages/server/dist/resume-drain.d.ts +122 -0
  103. package/packages/server/dist/resume-drain.js +158 -0
  104. package/packages/server/dist/route-context.d.ts +168 -0
  105. package/packages/server/dist/route-context.js +39 -0
  106. package/packages/server/dist/route-errors.d.ts +8 -0
  107. package/packages/server/dist/route-errors.js +13 -0
  108. package/packages/server/dist/routes/chats.d.ts +12 -0
  109. package/packages/server/dist/routes/chats.js +1406 -0
  110. package/packages/server/dist/routes/git.d.ts +13 -0
  111. package/packages/server/dist/routes/git.js +236 -0
  112. package/packages/server/dist/routes/mcp.d.ts +83 -0
  113. package/packages/server/dist/routes/mcp.js +250 -0
  114. package/packages/server/dist/routes/meta.d.ts +15 -0
  115. package/packages/server/dist/routes/meta.js +447 -0
  116. package/packages/server/dist/routes/projects.d.ts +25 -0
  117. package/packages/server/dist/routes/projects.js +736 -0
  118. package/packages/server/dist/routes/triggers.d.ts +21 -0
  119. package/packages/server/dist/routes/triggers.js +324 -0
  120. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  121. package/packages/server/dist/routes/workspace-mount.js +32 -0
  122. package/packages/server/dist/routes.d.ts +26 -0
  123. package/packages/server/dist/routes.js +29 -0
  124. package/packages/server/dist/run-provenance.d.ts +109 -0
  125. package/packages/server/dist/run-provenance.js +206 -0
  126. package/packages/server/dist/runs.d.ts +92 -0
  127. package/packages/server/dist/runs.js +72 -0
  128. package/packages/server/dist/schedule-config.d.ts +68 -0
  129. package/packages/server/dist/schedule-config.js +75 -0
  130. package/packages/server/dist/schedule-session.d.ts +34 -0
  131. package/packages/server/dist/schedule-session.js +120 -0
  132. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  133. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  134. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  135. package/packages/server/dist/self-mcp-read.js +97 -0
  136. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  137. package/packages/server/dist/self-mcp-triggers.js +144 -0
  138. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  139. package/packages/server/dist/self-mcp-types.js +10 -0
  140. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  141. package/packages/server/dist/self-mcp-util.js +157 -0
  142. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  143. package/packages/server/dist/self-mcp-write.js +215 -0
  144. package/packages/server/dist/self-mcp.d.ts +95 -0
  145. package/packages/server/dist/self-mcp.js +448 -0
  146. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  147. package/packages/server/dist/send-file-mcp.js +281 -0
  148. package/packages/server/dist/session-hub.d.ts +186 -0
  149. package/packages/server/dist/session-hub.js +291 -0
  150. package/packages/server/dist/spawn-capability.d.ts +99 -0
  151. package/packages/server/dist/spawn-capability.js +106 -0
  152. package/packages/server/dist/star.d.ts +26 -0
  153. package/packages/server/dist/star.js +94 -0
  154. package/packages/server/dist/start.d.ts +1 -0
  155. package/packages/server/dist/start.js +21 -0
  156. package/packages/server/dist/subagents.d.ts +337 -0
  157. package/packages/server/dist/subagents.js +626 -0
  158. package/packages/server/dist/sweep.d.ts +122 -0
  159. package/packages/server/dist/sweep.js +696 -0
  160. package/packages/server/dist/tooldetails.d.ts +33 -0
  161. package/packages/server/dist/tooldetails.js +466 -0
  162. package/packages/server/dist/transcribe.d.ts +69 -0
  163. package/packages/server/dist/transcribe.js +119 -0
  164. package/packages/server/dist/transcripts.d.ts +42 -0
  165. package/packages/server/dist/transcripts.js +164 -0
  166. package/packages/server/dist/trigger-config.d.ts +278 -0
  167. package/packages/server/dist/trigger-config.js +417 -0
  168. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  169. package/packages/server/dist/trigger-runtime.js +82 -0
  170. package/packages/server/dist/trigger-session.d.ts +34 -0
  171. package/packages/server/dist/trigger-session.js +122 -0
  172. package/packages/server/dist/triggers.d.ts +83 -0
  173. package/packages/server/dist/triggers.js +124 -0
  174. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  175. package/packages/server/dist/trusted-proxy.js +157 -0
  176. package/packages/server/dist/turn-notice.d.ts +154 -0
  177. package/packages/server/dist/turn-notice.js +271 -0
  178. package/packages/server/dist/unread.d.ts +36 -0
  179. package/packages/server/dist/unread.js +123 -0
  180. package/packages/server/dist/usage.d.ts +66 -0
  181. package/packages/server/dist/usage.js +243 -0
  182. package/packages/server/dist/wake-injection.d.ts +156 -0
  183. package/packages/server/dist/wake-injection.js +136 -0
  184. package/packages/server/dist/whisper-local.d.ts +9 -0
  185. package/packages/server/dist/whisper-local.js +122 -0
  186. package/packages/server/dist/ws-context.d.ts +158 -0
  187. package/packages/server/dist/ws-context.js +1 -0
  188. package/packages/server/dist/ws-protocol.d.ts +423 -0
  189. package/packages/server/dist/ws-protocol.js +251 -0
  190. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  191. package/packages/server/dist/ws-self-mcp.js +28 -0
  192. package/packages/server/dist/ws-triggers.d.ts +29 -0
  193. package/packages/server/dist/ws-triggers.js +261 -0
  194. package/packages/server/dist/ws-turn.d.ts +86 -0
  195. package/packages/server/dist/ws-turn.js +751 -0
  196. package/packages/server/dist/ws.d.ts +62 -0
  197. package/packages/server/dist/ws.js +1024 -0
  198. package/packages/server/package.json +6 -0
  199. package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +2 -0
  200. package/packages/web/dist/assets/MarkdownRenderer-CJZFLj_8.js +30 -0
  201. package/packages/web/dist/assets/ProjectRedirect-BHwVbbqB.js +2 -0
  202. package/packages/web/dist/assets/ProjectView-BOvuAITg.js +13 -0
  203. package/packages/web/dist/assets/ProjectsGrid-BCojZOGY.js +2 -0
  204. package/packages/web/dist/assets/arc-BVyxvnkl.js +2 -0
  205. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-Cd9Puqg1.js +37 -0
  206. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  207. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-B_AQt_Rx.js +133 -0
  208. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  209. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-7e0TQtXB.js +11 -0
  210. package/packages/web/dist/assets/channel-C_1aISNn.js +2 -0
  211. package/packages/web/dist/assets/chunk-2J33WTMH-Dz4WZmUZ.js +2 -0
  212. package/packages/web/dist/assets/chunk-4BX2VUAB-DVWYldo6.js +2 -0
  213. package/packages/web/dist/assets/chunk-55IACEB6-BIMrizQd.js +2 -0
  214. package/packages/web/dist/assets/chunk-727SXJPM-CL2q9fF1.js +207 -0
  215. package/packages/web/dist/assets/chunk-AQP2D5EJ-BSq3HlQ9.js +232 -0
  216. package/packages/web/dist/assets/chunk-FMBD7UC4-Bgt64pLE.js +16 -0
  217. package/packages/web/dist/assets/chunk-ND2GUHAM-C8Pr2A38.js +2 -0
  218. package/packages/web/dist/assets/chunk-QZHKN3VN-BAOqh-MN.js +2 -0
  219. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +2 -0
  220. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +2 -0
  221. package/packages/web/dist/assets/core-lJeGACQg.js +4 -0
  222. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-BF4tameA.js +2 -0
  223. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  224. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  225. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  226. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  227. package/packages/web/dist/assets/dagre-BM42HDAG-DdsLM6ql.js +5 -0
  228. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  229. package/packages/web/dist/assets/diagram-2AECGRRQ-BQ78S_RM.js +44 -0
  230. package/packages/web/dist/assets/diagram-5GNKFQAL-CAKE6nHE.js +11 -0
  231. package/packages/web/dist/assets/diagram-KO2AKTUF-fEvHRXgr.js +4 -0
  232. package/packages/web/dist/assets/diagram-LMA3HP47-CtWsgeSm.js +25 -0
  233. package/packages/web/dist/assets/diagram-OG6HWLK6-Buog8k7_.js +25 -0
  234. package/packages/web/dist/assets/erDiagram-TEJ5UH35-CWvVVPZS.js +86 -0
  235. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-DsCGXtp4.js +163 -0
  236. package/packages/web/dist/assets/format-DamJQ9LO.js +2 -0
  237. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-CsdwIvvC.js +293 -0
  238. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-hZvrX1sD.js +107 -0
  239. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  240. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  241. package/packages/web/dist/assets/index-CGbZdrm8.js +71 -0
  242. package/packages/web/dist/assets/index-DJDQ210-.css +1 -0
  243. package/packages/web/dist/assets/infoDiagram-5YYISTIA-nE_Vtz_l.js +3 -0
  244. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  245. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  246. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js +71 -0
  247. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  248. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  249. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-mjU1fMc4.js +140 -0
  250. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  251. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-C_9xNaa3.js +90 -0
  252. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  253. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  254. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  255. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  256. package/packages/web/dist/assets/linear-BxKd91ho.js +2 -0
  257. package/packages/web/dist/assets/mermaid.core-C-CuhD8I.js +310 -0
  258. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bs6yfcZv.js +97 -0
  259. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  260. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  261. package/packages/web/dist/assets/pieDiagram-4H26LBE5-DNdqX7O5.js +31 -0
  262. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  263. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js +8 -0
  264. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  265. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-RmxMdm2q.js +85 -0
  266. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  267. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  268. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-OAAhWcfK.js +41 -0
  269. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  270. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-BWF7rCyN.js +163 -0
  271. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  272. package/packages/web/dist/assets/stateDiagram-AJRCARHV-M3b_zqqo.js +2 -0
  273. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +2 -0
  274. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  275. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-BpOCh63N.js +121 -0
  276. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  277. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-D_iPH45Q.js +35 -0
  278. package/packages/web/dist/assets/wardley-L42UT6IY-DVD9I2Om.js +174 -0
  279. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-Bn1IKwjn.js +79 -0
  280. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  281. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-DwtI0bkR.js +8 -0
  282. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  283. package/packages/web/dist/favicon.ico +0 -0
  284. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  285. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  286. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  287. package/packages/web/dist/icons/favicon-16.png +0 -0
  288. package/packages/web/dist/icons/favicon-32.png +0 -0
  289. package/packages/web/dist/icons/icon-192.png +0 -0
  290. package/packages/web/dist/icons/icon-512.png +0 -0
  291. package/packages/web/dist/icons/maskable-512.png +0 -0
  292. package/packages/web/dist/index.html +56 -0
  293. package/packages/web/dist/manifest.webmanifest +32 -0
  294. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Paddock-native "send file" MCP tool.
3
+ *
4
+ * Exposes a single `paddock_send_file` tool to keeper agents via
5
+ * herdctl's `injectedMcpServers` mechanism. herdctl carries the def to the agent
6
+ * differently per runtime, and both auto-allowlist the server's `mcp__<name>__*`
7
+ * tools:
8
+ * - SDK runtime (chats, the `session` drive-mode default) — the def becomes an
9
+ * IN-PROCESS SDK MCP server via `createSdkMcpServer`; no bridge, no socket.
10
+ * - CLI runtime (the sweeper, triggers, and `driveMode: batch` chats) — the
11
+ * agent is a separate `claude -p` process that can't reach an in-process SDK
12
+ * server, so herdctl stands up a localhost HTTP MCP bridge per injected
13
+ * server instead.
14
+ * Either way this tool's handler runs inside the Paddock server process.
15
+ *
16
+ * ── How the rendered file reaches (and persists in) the chat ────────────────
17
+ * The tool returns a small JSON ENVELOPE as its result `output`. That output is
18
+ * the one representation that survives everywhere:
19
+ * - live: `@herdctl/chat`'s translator forwards it verbatim on the
20
+ * `chat:tool_call` event;
21
+ * - reload: `@herdctl/core`'s history parser preserves tool `output` verbatim
22
+ * (it only summarizes tool *input*).
23
+ * So the web renders from the tool call itself — no separate WS event, and a
24
+ * refresh shows exactly what was there live. See ChatPane's `sentFileFromToolCall`.
25
+ *
26
+ * Two ways to send a file:
27
+ * - Inline / virtual: `content` + `filename` (a real-looking but possibly
28
+ * fictional name). The content rides IN the envelope, so it stays in the
29
+ * transcript and in the agent's context (the agent authored it and can
30
+ * revise it on a later turn). Nothing needs to exist on disk.
31
+ * - Real file: `file_path` (relative to the agent's working directory). Only
32
+ * the PATH goes in the envelope — never the bytes — so the transcript stays
33
+ * lean and Paddock loads the bytes on demand (sandboxed) when rendering.
34
+ *
35
+ * The optional `kind` selects the renderer; when omitted it's inferred from the
36
+ * filename extension.
37
+ */
38
+ import { basename, extname, resolve } from "node:path";
39
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
40
+ /**
41
+ * Reject a runaway send; a chat attachment shouldn't be huge. Sized generously
42
+ * for video: a short screen recording (e.g. a Playwright `recordVideo`) routinely
43
+ * exceeds the old 25 MB image ceiling, so we allow up to 100 MB.
44
+ */
45
+ export const MAX_ATTACHMENT_BYTES = 100 * 1024 * 1024;
46
+ const MARKDOWN_EXT = new Set([".md", ".mdx", ".markdown"]);
47
+ const MERMAID_EXT = new Set([".mmd", ".mermaid"]);
48
+ const HTML_EXT = new Set([".html", ".htm"]);
49
+ const IMAGE_EXT = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg"]);
50
+ // Video extensions render inline as a <video> player. Note `.webm` (video) is
51
+ // distinct from `.webp` (image); the IMAGE check runs first so they never collide.
52
+ const VIDEO_EXT = new Set([".mp4", ".webm", ".mov", ".m4v"]);
53
+ const PDF_EXT = new Set([".pdf"]);
54
+ /** Extension -> language label for the code-block filename chrome. */
55
+ const LANGUAGE_BY_EXT = {
56
+ ".ts": "typescript",
57
+ ".tsx": "tsx",
58
+ ".js": "javascript",
59
+ ".jsx": "jsx",
60
+ ".mjs": "javascript",
61
+ ".cjs": "javascript",
62
+ ".py": "python",
63
+ ".rb": "ruby",
64
+ ".go": "go",
65
+ ".rs": "rust",
66
+ ".java": "java",
67
+ ".c": "c",
68
+ ".h": "c",
69
+ ".cpp": "cpp",
70
+ ".cc": "cpp",
71
+ ".cs": "csharp",
72
+ ".php": "php",
73
+ ".swift": "swift",
74
+ ".kt": "kotlin",
75
+ ".sh": "bash",
76
+ ".bash": "bash",
77
+ ".zsh": "bash",
78
+ ".sql": "sql",
79
+ ".json": "json",
80
+ ".yaml": "yaml",
81
+ ".yml": "yaml",
82
+ ".toml": "toml",
83
+ ".css": "css",
84
+ ".scss": "scss",
85
+ ".xml": "xml",
86
+ };
87
+ function inferKind(filename) {
88
+ const ext = extname(filename).toLowerCase();
89
+ if (MARKDOWN_EXT.has(ext))
90
+ return "markdown";
91
+ if (MERMAID_EXT.has(ext))
92
+ return "mermaid";
93
+ if (HTML_EXT.has(ext))
94
+ return "html";
95
+ // IMAGE before VIDEO so `.webp` (image) is never confused with `.webm` (video).
96
+ if (IMAGE_EXT.has(ext))
97
+ return "image";
98
+ if (VIDEO_EXT.has(ext))
99
+ return "video";
100
+ if (PDF_EXT.has(ext))
101
+ return "pdf";
102
+ if (ext in LANGUAGE_BY_EXT)
103
+ return "code";
104
+ return "text";
105
+ }
106
+ function inferLanguage(filename) {
107
+ return LANGUAGE_BY_EXT[extname(filename).toLowerCase()];
108
+ }
109
+ function ok(envelope) {
110
+ return { content: [{ type: "text", text: JSON.stringify(envelope) }] };
111
+ }
112
+ /**
113
+ * A short listing of a directory's top-level entries, to append to a
114
+ * file-not-found / escape error so the agent can immediately see the right
115
+ * filename instead of shelling out to `ls`/`find` to hunt for it (the main
116
+ * source of friction observed with screenshot flows). Files first, then dirs;
117
+ * capped so a large dir doesn't flood the tool result.
118
+ */
119
+ async function describeDir(dir) {
120
+ try {
121
+ const entries = await readdir(dir, { withFileTypes: true });
122
+ const files = entries.filter((e) => e.isFile()).map((e) => e.name);
123
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => `${e.name}/`);
124
+ const shown = [...files, ...dirs].slice(0, 40);
125
+ if (shown.length === 0)
126
+ return "It is currently empty.";
127
+ const more = files.length + dirs.length > shown.length ? ", …" : "";
128
+ return `Entries in it: ${shown.join(", ")}${more}.`;
129
+ }
130
+ catch {
131
+ return "";
132
+ }
133
+ }
134
+ function fail(text) {
135
+ return { content: [{ type: "text", text }], isError: true };
136
+ }
137
+ const TOOL_NAME = "send_file";
138
+ const SERVER_NAME = "paddock";
139
+ const TOOL_DESCRIPTION = "Render a file inline in the Paddock chat so the user sees it nicely formatted. " +
140
+ "Use this to share a document, code snippet, or diagram. Two modes: (1) pass `content` " +
141
+ "with a `filename` to render inline/virtual content that need not exist on disk (great " +
142
+ "for a code snippet or a Markdown/Mermaid block — the filename can be illustrative, e.g. " +
143
+ "`example.tsx` or `architecture.mmd`); or (2) pass `file_path` to render a real file from " +
144
+ "your working directory. Markdown renders formatted, ```mermaid``` blocks (or a .mmd file / " +
145
+ "kind:'mermaid') render as diagrams, and code renders with a filename header. Videos " +
146
+ "(mp4/webm) sent via a real `file_path` render as an inline player with controls — keep them " +
147
+ "short. A real .pdf (via `file_path`) renders inline in a scrollable PDF viewer. Prefer " +
148
+ "this over pasting long content into your text reply.";
149
+ const TOOL_INPUT_SCHEMA = {
150
+ type: "object",
151
+ properties: {
152
+ content: {
153
+ type: "string",
154
+ description: "Inline file contents to render (for a virtual/illustrative file). Provide this OR file_path.",
155
+ },
156
+ file_path: {
157
+ type: "string",
158
+ description: "Path to a real file to render, relative to (or absolute within) your working directory. Provide this OR content.",
159
+ },
160
+ filename: {
161
+ type: "string",
162
+ description: "Display filename shown in the file header (e.g. `example.tsx`, `README.md`, `flow.mmd`). Required with `content`; defaults to the basename of `file_path`.",
163
+ },
164
+ kind: {
165
+ type: "string",
166
+ enum: ["markdown", "mermaid", "code", "text", "html", "image", "video", "pdf"],
167
+ description: "Renderer to use. Optional — inferred from the filename extension when omitted.",
168
+ },
169
+ language: {
170
+ type: "string",
171
+ description: "Language hint for the `code` kind (e.g. `python`). Optional.",
172
+ },
173
+ message: {
174
+ type: "string",
175
+ description: "Optional note to show alongside the file.",
176
+ },
177
+ },
178
+ required: ["filename"],
179
+ };
180
+ function createHandler(context) {
181
+ return async (args) => {
182
+ try {
183
+ const rawContent = typeof args.content === "string" ? args.content : undefined;
184
+ const filePath = typeof args.file_path === "string" ? args.file_path : undefined;
185
+ const filenameArg = typeof args.filename === "string" ? args.filename : undefined;
186
+ const kindArg = typeof args.kind === "string" ? args.kind : undefined;
187
+ const languageArg = typeof args.language === "string" ? args.language : undefined;
188
+ const message = typeof args.message === "string" ? args.message : undefined;
189
+ if (rawContent === undefined && filePath === undefined) {
190
+ return fail("Error: provide either `content` (inline) or `file_path` (a real file).");
191
+ }
192
+ if (rawContent !== undefined) {
193
+ // Inline / virtual file — content rides in the envelope.
194
+ const filename = filenameArg ?? "snippet.txt";
195
+ const kind = kindArg ?? inferKind(filename);
196
+ if (kind === "image") {
197
+ return fail("Error: inline `content` cannot be an image; use `file_path` for images.");
198
+ }
199
+ if (kind === "video") {
200
+ return fail("Error: inline `content` cannot be a video; use `file_path` for videos.");
201
+ }
202
+ if (kind === "pdf") {
203
+ return fail("Error: inline `content` cannot be a PDF; use `file_path` for PDFs.");
204
+ }
205
+ return ok({
206
+ paddockSendFile: 1,
207
+ filename,
208
+ kind,
209
+ language: kind === "code" ? (languageArg ?? inferLanguage(filename)) : undefined,
210
+ source: "inline",
211
+ content: rawContent,
212
+ message,
213
+ });
214
+ }
215
+ // Real file — copy its bytes into the attachment store NOW, so the shared
216
+ // file is an immutable snapshot (renders forever, even if the original is
217
+ // later edited/deleted) and the render endpoint only ever serves files that
218
+ // were explicitly sent — never an arbitrary on-box path. Because we copy
219
+ // rather than reference, there's no sandbox: a relative path resolves
220
+ // against the working dir, an absolute path is used as-is.
221
+ const wd = context.workingDirectory;
222
+ const resolved = wd ? resolve(wd, filePath) : resolve(filePath);
223
+ let realPath;
224
+ try {
225
+ realPath = await realpath(resolved);
226
+ }
227
+ catch {
228
+ const where = wd ? ` Your working directory is ${wd}. ${await describeDir(wd)}` : "";
229
+ return fail(`Error: file not found: ${filePath} (looked for ${resolved}).${where} ` +
230
+ `Pass a path to a file that exists (screenshots from the browser tools are ` +
231
+ `saved in your working directory).`);
232
+ }
233
+ const info = await stat(realPath);
234
+ if (!info.isFile()) {
235
+ return fail(`Error: not a regular file: ${filePath}`);
236
+ }
237
+ if (info.size > MAX_ATTACHMENT_BYTES) {
238
+ return fail(`Error: file too large to send: ${info.size} bytes (limit ${MAX_ATTACHMENT_BYTES}).`);
239
+ }
240
+ const bytes = await readFile(realPath);
241
+ const filename = filenameArg ?? basename(resolved);
242
+ const kind = kindArg ?? inferKind(filename);
243
+ const attachmentId = await context.saveAttachment(bytes, filename);
244
+ return ok({
245
+ paddockSendFile: 1,
246
+ filename,
247
+ kind,
248
+ language: kind === "code" ? (languageArg ?? inferLanguage(filename)) : undefined,
249
+ source: "file",
250
+ attachmentId,
251
+ message,
252
+ });
253
+ }
254
+ catch (error) {
255
+ const msg = error instanceof Error ? error.message : String(error);
256
+ return fail(`Error sending file: ${msg}`);
257
+ }
258
+ };
259
+ }
260
+ /**
261
+ * Build the injected MCP server definition for the send-file tool, bound to a
262
+ * per-turn context. Pass the returned value under a stable server key (`paddock`)
263
+ * so the tool surfaces to the agent as `mcp__paddock__send_file`.
264
+ */
265
+ export function sendFileServerDef(context) {
266
+ return {
267
+ name: SERVER_NAME,
268
+ version: "0.1.0",
269
+ tools: [
270
+ {
271
+ name: TOOL_NAME,
272
+ description: TOOL_DESCRIPTION,
273
+ inputSchema: TOOL_INPUT_SCHEMA,
274
+ handler: createHandler(context),
275
+ },
276
+ ],
277
+ };
278
+ }
279
+ /** The record key + resulting fully-qualified tool name the agent sees. */
280
+ export const SEND_FILE_SERVER_KEY = SERVER_NAME;
281
+ export const SEND_FILE_TOOL_NAME = `mcp__${SERVER_NAME}__${TOOL_NAME}`;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * SessionHub — session-scoped fan-out + re-attach for in-flight chat turns.
3
+ *
4
+ * The problem it solves (issue #54): a turn's stream used to be bound to the ONE
5
+ * socket that sent `chat:send`. Frames were written straight to that socket and
6
+ * silently dropped whenever it wasn't OPEN. So if the socket died mid-turn (idle
7
+ * half-open drop, laptop sleep, wifi change, the client's own auto-reconnect
8
+ * swapping sockets), every remaining frame of the running turn was lost — the
9
+ * live stream stalled and only a full page reload (which re-hydrates from the
10
+ * persisted transcript) recovered it.
11
+ *
12
+ * The hub decouples a turn from any single socket:
13
+ *
14
+ * - Every emitted frame is buffered on the turn and stamped with a per-turn,
15
+ * monotonic `seq`.
16
+ * - Frames fan out to the turn's ORIGIN socket PLUS every socket currently
17
+ * subscribed to the turn's session (dead sockets are skipped, not dropped-
18
+ * on-the-floor-permanently — a live one still gets the frame).
19
+ * - A reconnecting client re-`attach`es to the session and replays exactly the
20
+ * frames it missed (everything with `seq` greater than the last it applied),
21
+ * so the stream resumes seamlessly with no gap and no duplication.
22
+ *
23
+ * A just-completed turn's buffer is retained briefly ({@link COMPLETED_TTL_MS})
24
+ * so a client that reconnects right at the end still receives the tail frames
25
+ * (including the terminal `chat:complete`) rather than hanging "streaming"
26
+ * forever.
27
+ *
28
+ * The hub is transport-shaped but transport-agnostic: it only needs a socket it
29
+ * can check `readyState` on and `send` a JSON string to (the `HubSocket`
30
+ * minimal interface), so it is unit-testable without a real WebSocket.
31
+ */
32
+ /** The slice of a WebSocket the hub needs — real `ws` sockets satisfy this. */
33
+ export interface HubSocket {
34
+ readyState: number;
35
+ /** WebSocket.OPEN (1) — read off the instance so tests can fake it. */
36
+ readonly OPEN: number;
37
+ send(data: string): void;
38
+ }
39
+ /** A server→client frame the hub buffers and fans out. `payload.seq` is stamped by the hub. */
40
+ export interface HubMessage {
41
+ type: string;
42
+ payload: Record<string, unknown>;
43
+ }
44
+ /**
45
+ * The frame shape `emit` accepts. `payload` is any object (the caller's typed
46
+ * routing payloads are interfaces without a string index signature, so they
47
+ * don't satisfy `Record<string, unknown>` directly); the hub treats it opaquely
48
+ * and only adds `seq`.
49
+ */
50
+ export interface HubMessageInput {
51
+ type: string;
52
+ payload: object;
53
+ }
54
+ interface BufferedFrame {
55
+ seq: number;
56
+ msg: HubMessage;
57
+ }
58
+ interface Turn {
59
+ projectSlug: string;
60
+ /** Null until the turn's session id is known (a brand-new chat resolves it mid-stream). */
61
+ sessionId: string | null;
62
+ jobId: string | null;
63
+ /**
64
+ * The socket that started the turn; always a fan-out target while OPEN. `null`
65
+ * for a server-initiated turn with no client watching (a scheduler-fired
66
+ * wake — Paddock#111): frames are still buffered for replay so a client that
67
+ * attaches mid-turn (or opens the chat afterward) sees the woken work.
68
+ */
69
+ origin: HubSocket | null;
70
+ frames: BufferedFrame[];
71
+ /** `seq` of `frames[0]` after any trim — replays older than this need a resync. */
72
+ baseSeq: number;
73
+ nextSeq: number;
74
+ running: boolean;
75
+ evictTimer: ReturnType<typeof setTimeout> | null;
76
+ }
77
+ /** How long a completed turn's buffer lingers so an end-of-turn reconnect still gets the tail. */
78
+ export declare const COMPLETED_TTL_MS = 60000;
79
+ /** The outcome of an {@link SessionHub.attach}. */
80
+ export type AttachResult = {
81
+ status: "none";
82
+ } | {
83
+ status: "replayed";
84
+ frames: number;
85
+ } | {
86
+ status: "resync";
87
+ projectSlug: string;
88
+ };
89
+ /**
90
+ * A session's live-turn status, surfaced so the UI can restore the Stop button
91
+ * and streaming indicators (issues #52/#53). Emitted on start/stop transitions
92
+ * via {@link SessionHub.onActive} and readable via {@link SessionHub.activeInfo}.
93
+ */
94
+ export interface ActiveInfo {
95
+ sessionId: string;
96
+ projectSlug: string;
97
+ /** The running turn's cancellable job id, if known yet (null early in a turn). */
98
+ jobId: string | null;
99
+ running: boolean;
100
+ }
101
+ export declare class SessionHub {
102
+ /** sessionId → the current (running or recently-completed) turn for that session. */
103
+ private bySession;
104
+ /** sessionId → sockets attached to it (fan-out targets beyond the origin). */
105
+ private subscribers;
106
+ /**
107
+ * Notified on every running-state transition (turn start / stop) so the WS
108
+ * layer can broadcast a `chat:active` signal to clients — powering the Stop
109
+ * button restore (#52) and streaming indicators (#53). Set by the WS layer.
110
+ */
111
+ onActive: ((info: ActiveInfo) => void) | null;
112
+ /**
113
+ * Begin tracking a turn. Pass `sessionId` when it's already known (a resumed
114
+ * chat / a slash command) so the turn is re-attachable from its very first
115
+ * frame; omit it for a brand-new chat and call {@link TurnHandle.setSession}
116
+ * once the id arrives on the stream.
117
+ */
118
+ startTurn(projectSlug: string, origin: HubSocket | null, sessionId?: string | null): TurnHandle;
119
+ /** Add a socket to a session's fan-out set (idempotent). */
120
+ subscribe(sessionId: string, socket: HubSocket): void;
121
+ /**
122
+ * Attach a (re)connecting socket to a session and, when `wantReplay`, catch it
123
+ * up on a still-live turn by replaying every buffered frame after `afterSeq`.
124
+ *
125
+ * `wantReplay` MUST be false for a fresh mount (one that also hydrates the
126
+ * transcript over REST) so buffered frames don't duplicate the transcript; it
127
+ * is true only for a genuine reconnect of a socket that was mid-turn, where
128
+ * `afterSeq` is the last seq the client applied and the replay is exactly the
129
+ * gap. Replays are written straight to `socket` (not the whole fan-out).
130
+ */
131
+ attach(sessionId: string, socket: HubSocket, opts: {
132
+ wantReplay: boolean;
133
+ afterSeq: number;
134
+ }): AttachResult;
135
+ /** Drop a socket from every session's fan-out set (call on socket close). */
136
+ unsubscribeSocket(socket: HubSocket): void;
137
+ /** Whether a session currently has a RUNNING turn (used by the active-turns signal). */
138
+ isRunning(sessionId: string): boolean;
139
+ /**
140
+ * Send a one-off control frame to every socket attached to a session — its
141
+ * (recently-)tracked turn's origin plus every subscribed socket (#245). Unlike
142
+ * {@link emit} this is not seq-stamped or buffered; it's for out-of-band signals
143
+ * like `chat:queued_flushed` that must reach a client that reconnected on a new
144
+ * socket after the origin died.
145
+ */
146
+ broadcast(sessionId: string, msg: HubMessage): void;
147
+ /** Whether a session currently has a RUNNING turn plus its job id, or null. */
148
+ activeInfo(sessionId: string): ActiveInfo | null;
149
+ /** Snapshot of every session with a running turn (for a newly-connected client). */
150
+ runningSessions(): ActiveInfo[];
151
+ /** Register a turn under its resolved session id, evicting any prior turn there. */
152
+ register(turn: Turn, sessionId: string): void;
153
+ /** Stamp `seq`, buffer (with trim), and fan out to origin + subscribers. */
154
+ emit(turn: Turn, base: HubMessageInput): void;
155
+ /** Mark a turn finished; retain its buffer briefly for an end-of-turn reconnect. */
156
+ end(turn: Turn): void;
157
+ /**
158
+ * Re-broadcast a running turn's active state once its jobId becomes known, so
159
+ * clients can arm the Stop button before the first content frame (Paddock#111).
160
+ * No-op if the turn isn't running or its session id isn't resolved yet (a new
161
+ * chat learns the jobId together with its session id on the first frame).
162
+ */
163
+ jobIdResolved(turn: Turn): void;
164
+ private fireActive;
165
+ private recipients;
166
+ private evict;
167
+ private static write;
168
+ }
169
+ /**
170
+ * A per-turn handle the WS layer drives: stamp/fan-out frames via {@link emit},
171
+ * learn the session id via {@link setSession}, and finish via {@link end}.
172
+ */
173
+ export declare class TurnHandle {
174
+ private readonly hub;
175
+ private readonly turn;
176
+ constructor(hub: SessionHub, turn: Turn);
177
+ get jobId(): string | null;
178
+ setJobId(id: string): void;
179
+ /** Register (or move) this turn under its now-known session id. Idempotent. */
180
+ setSession(sessionId: string): void;
181
+ /** Buffer + fan out one frame to every live socket attached to this turn's session. */
182
+ emit(base: HubMessageInput): void;
183
+ /** Finish the turn (buffer retained briefly for a late reconnect). */
184
+ end(): void;
185
+ }
186
+ export {};