@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
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Paddock
2
+
3
+ **Project-first launchpad for Claude Code** — server-hosted, persistent, resumable
4
+ sessions organized by project, in a web UI.
5
+
6
+ ```sh
7
+ npx @edspencer/paddock
8
+ ```
9
+
10
+ Then open <http://127.0.0.1:4000>.
11
+
12
+ > **First run downloads ~250 MB.** Paddock drives Claude Code, and the Claude
13
+ > Agent SDK ships a per-platform binary of that size. It cannot be skipped —
14
+ > installing with `--omit=optional` produces a Paddock whose chats all fail.
15
+ > Later runs reuse the npm cache and start immediately. For repeated use,
16
+ > `npm i -g @edspencer/paddock` is friendlier than bare `npx`.
17
+
18
+ ## Credentials
19
+
20
+ Paddock runs Claude Code on your behalf, so it needs Claude credentials:
21
+
22
+ ```sh
23
+ claude setup-token # Claude Max/Pro
24
+ export ANTHROPIC_API_KEY=sk-ant-… # or API billing
25
+ ```
26
+
27
+ If you already use Claude Code on this machine, your existing login is picked up
28
+ automatically and there is nothing to do.
29
+
30
+ ## Options
31
+
32
+ ```
33
+ -p, --port <port> HTTP/WS port (default 4000)
34
+ --host <host> Bind address (default 127.0.0.1)
35
+ -d, --data-dir <path> Projects + state (default ~/.paddock)
36
+ -v, --version Print the version
37
+ -h, --help Show help
38
+ ```
39
+
40
+ Your projects, chats and settings live in `~/.paddock` and persist between runs.
41
+
42
+ ## Security
43
+
44
+ Paddock binds **loopback only** with authentication disabled, which is the right
45
+ default for a laptop. It **refuses to start** if you bind a routable interface
46
+ while auth is off — to expose it on a network, configure `PADDOCK_AUTH_MODE`
47
+ first. See [AUTH.md](https://github.com/edspencer/paddock/blob/main/AUTH.md).
48
+
49
+ ## Requirements
50
+
51
+ Node.js 22 or newer.
52
+
53
+ ## Other ways to run it
54
+
55
+ A multi-arch Docker image is published alongside this package, and is the better
56
+ fit for a server deployment:
57
+
58
+ ```sh
59
+ docker run -d -p 4000:4000 -v /srv/paddock-data:/data \
60
+ -e CLAUDE_CODE_OAUTH_TOKEN=… ghcr.io/edspencer/paddock:latest
61
+ ```
62
+
63
+ ## Links
64
+
65
+ - [Documentation](https://github.com/edspencer/paddock#readme)
66
+ - [Issues](https://github.com/edspencer/paddock/issues)
67
+ - Built on [herdctl](https://github.com/edspencer/herdctl)
68
+
69
+ MIT © Ed Spencer
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Printed as a `preinstall` script of the PUBLISHED package, before npm starts
4
+ * fetching the dependency tree.
5
+ *
6
+ * Why this exists: Paddock depends on the Claude Agent SDK, whose per-platform
7
+ * binary is ~250 MB. That download dominates a first `npx @edspencer/paddock`
8
+ * and is long enough that, unannounced, it reads as a hang. It also cannot be
9
+ * trimmed — the binary arrives via a platform optionalDependency, and
10
+ * installing with `--omit=optional` produces a Paddock whose chats all fail.
11
+ *
12
+ * NOT wired into packages/server/package.json in the repo: a `preinstall` there
13
+ * would fire on every workspace `npm install` a contributor runs. It belongs to
14
+ * the synthesized publish manifest — see the npm packaging issue.
15
+ *
16
+ * npm may suppress this under `--silent` / non-TTY / some CI log levels, so it
17
+ * is a courtesy, not a guarantee. The same warning belongs in the README, where
18
+ * people actually look before running an unfamiliar command.
19
+ */
20
+ if (!process.env.PADDOCK_QUIET_INSTALL) {
21
+ process.stdout.write(
22
+ [
23
+ "",
24
+ " Paddock bundles the Claude Code runtime — about 250 MB on first install.",
25
+ " Later runs reuse the cache and start immediately.",
26
+ "",
27
+ ].join("\n") + "\n",
28
+ );
29
+ }
package/package.json CHANGED
@@ -1,9 +1,62 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.0.1",
4
- "description": "Placeholder to reserve the name and enable trusted publishing. See 0.55.x for the real package.",
5
- "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/edspencer/paddock.git" },
3
+ "version": "0.56.0",
4
+ "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "anthropic",
9
+ "ai",
10
+ "agents",
11
+ "paddock"
12
+ ],
7
13
  "homepage": "https://github.com/edspencer/paddock#readme",
8
- "publishConfig": { "access": "public" }
14
+ "bugs": {
15
+ "url": "https://github.com/edspencer/paddock/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/edspencer/paddock.git"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Ed Spencer",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "bin": {
28
+ "paddock": "packages/server/dist/cli/paddock.js"
29
+ },
30
+ "scripts": {
31
+ "preinstall": "node install-notice.mjs"
32
+ },
33
+ "dependencies": {
34
+ "@fastify/multipart": "8.3.1",
35
+ "@fastify/static": "7.0.4",
36
+ "@fastify/swagger": "8.15.0",
37
+ "@fastify/swagger-ui": "4.2.0",
38
+ "@fastify/websocket": "10.0.1",
39
+ "@herdctl/chat": "0.8.0",
40
+ "@herdctl/core": "5.29.1",
41
+ "@modelcontextprotocol/sdk": "1.29.0",
42
+ "fastify": "4.28.1",
43
+ "jose": "6.2.3",
44
+ "proxy-addr": "2.0.7",
45
+ "yaml": "2.5.1",
46
+ "zod": "4.4.3"
47
+ },
48
+ "optionalDependencies": {
49
+ "nodejs-whisper": "0.2.9"
50
+ },
51
+ "files": [
52
+ "packages/server/dist",
53
+ "packages/server/package.json",
54
+ "packages/web/dist",
55
+ "install-notice.mjs",
56
+ "README.md"
57
+ ],
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "provenance": true
61
+ }
9
62
  }
@@ -0,0 +1,127 @@
1
+ import { type AdoptableSession } from "@herdctl/core";
2
+ import type { Project } from "./projects.js";
3
+ /** One working directory that has adoptable sessions, and which ones. */
4
+ export interface AdoptableSource {
5
+ /** The working directory whose transcript folder holds these sessions. */
6
+ sourceCwd: string;
7
+ /** Adoptable session ids, newest transcript first. */
8
+ sessionIds: string[];
9
+ }
10
+ /** A candidate that detection deliberately did NOT offer, and why. */
11
+ export interface FilteredSession {
12
+ sessionId: string;
13
+ sourceCwd: string;
14
+ reason: FilterReason;
15
+ }
16
+ /**
17
+ * Why a candidate was withheld. Both values are OURS (paddock-side noise
18
+ * filtering); the engine's own exclusions (sidechain / already-adopted /
19
+ * attributed-to-run) never reach us as candidates at all.
20
+ */
21
+ export type FilterReason = "too-small" | "slash-command-only";
22
+ /** What a project could import right now. */
23
+ export interface AdoptableSummary {
24
+ /** Total adoptable sessions across every source, after de-dup and filtering. */
25
+ count: number;
26
+ sources: AdoptableSource[];
27
+ /**
28
+ * Candidates withheld by the noise filter. Reported rather than dropped
29
+ * silently so "why is the count 5 and not 7?" always has an answer.
30
+ */
31
+ filtered: FilteredSession[];
32
+ }
33
+ /**
34
+ * Minimum transcript size to be worth offering, in bytes.
35
+ *
36
+ * Calibrated against real transcripts: ONE complete Claude Code user record
37
+ * (uuid, parentUuid, sessionId, cwd, version, gitBranch, timestamp, message …)
38
+ * measures ~350-450 bytes on its own. A file under 256 bytes therefore cannot
39
+ * hold even a single complete exchange — it is a zero-byte stub or a truncated
40
+ * fragment. The shortest genuine session in the QA corpus (one user message, no
41
+ * reply — an aborted chat, which IS worth importing) is 396 bytes, so this sits
42
+ * comfortably below the real floor rather than near it.
43
+ */
44
+ export declare const MIN_TRANSCRIPT_BYTES = 256;
45
+ /**
46
+ * Size ceiling under which a session that OPENS with a slash command is treated
47
+ * as noise.
48
+ *
49
+ * `/mcp`, `/status`, `/cost` and friends open a session that is nothing but the
50
+ * command and its output — real files on disk, but not conversations. The
51
+ * preview alone cannot settle it: the engine truncates a preview at 100 chars,
52
+ * and a session may legitimately open with `/review …` and then run for pages.
53
+ * So the preview says "this began as a command" and the size says "and it never
54
+ * went anywhere". 4 kB is roughly ten complete transcript records — comfortably
55
+ * more than a command plus its output, comfortably less than a conversation.
56
+ */
57
+ export declare const SLASH_COMMAND_MAX_BYTES = 4096;
58
+ /** The engine surface this module needs — narrowed so tests can fake it. */
59
+ export interface AdoptableFleet {
60
+ listAdoptableSessions(agentName: string, fromWorkingDir?: string): Promise<AdoptableSession[]>;
61
+ }
62
+ /**
63
+ * Per-instance adoptable-session detector with an mtime-keyed cache.
64
+ *
65
+ * ## What is cached, and why the key is sound
66
+ *
67
+ * The expensive work is (a) reading one transcript head per transcript folder on
68
+ * the machine to recover its cwd, and (b) an engine scan per matched source. The
69
+ * cheap work — one `readdir` plus a `stat` per folder — always runs, and
70
+ * produces the key.
71
+ *
72
+ * A cached answer is reused only while ALL of these are unchanged:
73
+ *
74
+ * - `<claudeHome>/projects` mtime+size — a transcript folder appeared or went.
75
+ * - each MATCHED folder's mtime+size — a transcript appeared or went inside it.
76
+ * - `<stateDir>/adopted-sessions` mtime+size — something was adopted/released,
77
+ * which is exactly what removes a session from the adoptable set.
78
+ * - `<stateDir>/jobs` mtime+size — a run completed, and a run record is what
79
+ * flips a native session to `attributed-to-run` (also excluded).
80
+ *
81
+ * Directory mtime changes when an entry is added or removed, NOT when an
82
+ * existing file is appended to — and appending to a transcript cannot change
83
+ * whether it is adoptable, so that granularity is precisely right rather than
84
+ * merely cheap. The one residual is an in-memory platform binding taken without
85
+ * touching either state dir; {@link invalidate} is called after every import for
86
+ * the case that actually matters.
87
+ */
88
+ export declare class AdoptableIndex {
89
+ private readonly claudeHomePath;
90
+ private readonly stateDir;
91
+ /** folder name → last-seen entry, reused while its `key` is unchanged. */
92
+ private readonly folders;
93
+ /** project key → cached summary + the composite key it was computed under. */
94
+ private readonly summaries;
95
+ constructor(claudeHomePath: string, stateDir: string);
96
+ /**
97
+ * Drop a project's cached summary (or every project's, with no argument).
98
+ * Called after an import, which changes the adoptable set in a way the caller
99
+ * already knows about and shouldn't have to wait for a directory mtime to
100
+ * reveal.
101
+ *
102
+ * NOTE the workspace key may be `""` (the root workspace) — this takes a
103
+ * string and tests `undefined` explicitly, never truthiness.
104
+ */
105
+ invalidate(projectKey?: string): void;
106
+ /**
107
+ * Enumerate `<claudeHome>/projects/*`, refreshing the recorded cwd only for
108
+ * folders whose file set changed since last time.
109
+ */
110
+ private scanFolders;
111
+ /**
112
+ * The candidate source working directories for a project, de-duplicated by the
113
+ * transcript folder they resolve to.
114
+ *
115
+ * ORIGIN-FIRST ordering (see {@link adoptableFor}'s session de-dup): matched
116
+ * external checkouts come first, the project's own working directory last.
117
+ */
118
+ private candidateSources;
119
+ /**
120
+ * What `project` could import, as served by the `adoptable-chats` endpoint.
121
+ *
122
+ * `agentName` is the adopting agent (the project's keeper); it is the metadata
123
+ * cache key the engine warms titles/previews under, so detection and the later
124
+ * adoption agree.
125
+ */
126
+ adoptableFor(fleet: AdoptableFleet, project: Project, agentName: string): Promise<AdoptableSummary>;
127
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Detection of a project's ADOPTABLE native Claude Code chats (#588).
3
+ *
4
+ * A project is backed by a working directory, and a user very often already has
5
+ * terminal `claude` history for that same directory — or for their own checkout
6
+ * of the same repo, somewhere else entirely on the disk. Those transcripts live
7
+ * in `<claudeHome>/projects/<encoded-cwd>/*.jsonl` and are invisible to paddock
8
+ * until they are adopted. This module answers "what could this project import?".
9
+ *
10
+ * The engine already knows how to decide whether ONE working directory's
11
+ * transcripts are adoptable (`fleet.listAdoptableSessions` — it excludes
12
+ * sidechains, already-adopted sessions and anything a real run is attributed
13
+ * to). What it cannot know is which working directories belong to a paddock
14
+ * project. That is this module's job, and it has three parts:
15
+ *
16
+ * 1. **Assemble candidate sources** — the project's own `workingDir`, plus any
17
+ * `<claudeHome>/projects/*` folder whose RECORDED cwd matches the project.
18
+ * 2. **Filter low-value noise** — a zero-byte transcript or a session that is
19
+ * nothing but a `/mcp` slash command is not a chat anyone wants imported.
20
+ * 3. **Cache** — the scan is O(all transcript folders on the machine), so it is
21
+ * keyed on directory mtimes and only redone when something could have moved.
22
+ *
23
+ * ## Why the recorded cwd, never the folder name
24
+ *
25
+ * `encodePathForCli` replaces every non-alphanumeric character with `-` and
26
+ * truncates+hashes past 200 chars. It is lossy and NOT invertible: `/a/b-c`,
27
+ * `/a-b/c` and `/a/b/c` all encode to `-a-b-c`. Deriving a cwd from a folder
28
+ * name is therefore impossible in general and silently wrong in particular. Each
29
+ * folder's real cwd is read out of the head of one of its transcripts
30
+ * (`readSessionCwd`), which is the authoritative, non-lossy source.
31
+ *
32
+ * A direct consequence: two DIFFERENT candidate cwds can name the SAME physical
33
+ * folder, which would offer every session in it twice. Sources are therefore
34
+ * de-duplicated by the folder's resolved real path, and sessions by id.
35
+ *
36
+ * ## The matching heuristic, and what it cannot do
37
+ *
38
+ * For a REPO-BACKED project a folder matches when its recorded cwd's BASENAME
39
+ * equals the project's checkout name (`repoCheckoutName(repo)`) — i.e. "the user
40
+ * has their own clone of this repo elsewhere". This is deliberately simple and
41
+ * is NOT identity-precise: two unrelated checkouts that happen to share a
42
+ * basename (`~/work/api` and `~/oss/api`) both match, and a clone the user
43
+ * renamed on disk does not match at all. Verifying the checkout's git remote
44
+ * would be exact; it also means running git in every candidate directory, which
45
+ * is the wrong cost for a count shown in a header. Noted as a known limit
46
+ * (#588 gotcha 6) rather than pretended away — the import is per-source, so a
47
+ * user who sees a source they don't recognise can decline it.
48
+ *
49
+ * For a NOTEBOOK project the match is exact cwd equality: its working directory
50
+ * IS the project directory, and nothing else is plausibly "the same project".
51
+ */
52
+ import { promises as fs } from "node:fs";
53
+ import path from "node:path";
54
+ import { encodePathForCli, readSessionCwd } from "@herdctl/core";
55
+ import { repoCheckoutName } from "./project-paths.js";
56
+ /**
57
+ * Minimum transcript size to be worth offering, in bytes.
58
+ *
59
+ * Calibrated against real transcripts: ONE complete Claude Code user record
60
+ * (uuid, parentUuid, sessionId, cwd, version, gitBranch, timestamp, message …)
61
+ * measures ~350-450 bytes on its own. A file under 256 bytes therefore cannot
62
+ * hold even a single complete exchange — it is a zero-byte stub or a truncated
63
+ * fragment. The shortest genuine session in the QA corpus (one user message, no
64
+ * reply — an aborted chat, which IS worth importing) is 396 bytes, so this sits
65
+ * comfortably below the real floor rather than near it.
66
+ */
67
+ export const MIN_TRANSCRIPT_BYTES = 256;
68
+ /**
69
+ * Size ceiling under which a session that OPENS with a slash command is treated
70
+ * as noise.
71
+ *
72
+ * `/mcp`, `/status`, `/cost` and friends open a session that is nothing but the
73
+ * command and its output — real files on disk, but not conversations. The
74
+ * preview alone cannot settle it: the engine truncates a preview at 100 chars,
75
+ * and a session may legitimately open with `/review …` and then run for pages.
76
+ * So the preview says "this began as a command" and the size says "and it never
77
+ * went anywhere". 4 kB is roughly ten complete transcript records — comfortably
78
+ * more than a command plus its output, comfortably less than a conversation.
79
+ */
80
+ export const SLASH_COMMAND_MAX_BYTES = 4096;
81
+ /**
82
+ * A preview that OPENS with a slash-command invocation.
83
+ *
84
+ * Claude Code writes an invocation either as bare text (`/mcp`) or inside the
85
+ * `<command-name>` envelope it uses for expanded commands; both are matched.
86
+ *
87
+ * The trailing lookahead is what keeps an absolute PATH from reading as a
88
+ * command: the command word must end at whitespace, at `<`, or at end-of-string,
89
+ * so a prompt beginning `/data/projects/foo …` does not match (the `/` after
90
+ * `/data` disqualifies it) while `/mcp`, `/mcp</command-name>` and
91
+ * `/review the auth refactor` all do.
92
+ */
93
+ const SLASH_COMMAND_ONLY_RE = /^\s*(?:<command-name>\s*)?\/[a-z][a-z0-9:_-]*(?![^\s<])/i;
94
+ /** Classify a candidate as noise, or `null` to keep it. */
95
+ function filterReasonFor(session) {
96
+ if (session.sizeBytes < MIN_TRANSCRIPT_BYTES)
97
+ return "too-small";
98
+ if (session.sizeBytes < SLASH_COMMAND_MAX_BYTES &&
99
+ session.preview !== undefined &&
100
+ SLASH_COMMAND_ONLY_RE.test(session.preview)) {
101
+ return "slash-command-only";
102
+ }
103
+ return null;
104
+ }
105
+ /** A directory's cache identity: its mtime + size, or `null` when absent. */
106
+ async function dirKey(dir) {
107
+ const st = await fs.stat(dir).catch(() => null);
108
+ return st ? `${st.mtimeMs}:${st.size}` : "-";
109
+ }
110
+ /**
111
+ * Per-instance adoptable-session detector with an mtime-keyed cache.
112
+ *
113
+ * ## What is cached, and why the key is sound
114
+ *
115
+ * The expensive work is (a) reading one transcript head per transcript folder on
116
+ * the machine to recover its cwd, and (b) an engine scan per matched source. The
117
+ * cheap work — one `readdir` plus a `stat` per folder — always runs, and
118
+ * produces the key.
119
+ *
120
+ * A cached answer is reused only while ALL of these are unchanged:
121
+ *
122
+ * - `<claudeHome>/projects` mtime+size — a transcript folder appeared or went.
123
+ * - each MATCHED folder's mtime+size — a transcript appeared or went inside it.
124
+ * - `<stateDir>/adopted-sessions` mtime+size — something was adopted/released,
125
+ * which is exactly what removes a session from the adoptable set.
126
+ * - `<stateDir>/jobs` mtime+size — a run completed, and a run record is what
127
+ * flips a native session to `attributed-to-run` (also excluded).
128
+ *
129
+ * Directory mtime changes when an entry is added or removed, NOT when an
130
+ * existing file is appended to — and appending to a transcript cannot change
131
+ * whether it is adoptable, so that granularity is precisely right rather than
132
+ * merely cheap. The one residual is an in-memory platform binding taken without
133
+ * touching either state dir; {@link invalidate} is called after every import for
134
+ * the case that actually matters.
135
+ */
136
+ export class AdoptableIndex {
137
+ claudeHomePath;
138
+ stateDir;
139
+ /** folder name → last-seen entry, reused while its `key` is unchanged. */
140
+ folders = new Map();
141
+ /** project key → cached summary + the composite key it was computed under. */
142
+ summaries = new Map();
143
+ constructor(claudeHomePath, stateDir) {
144
+ this.claudeHomePath = claudeHomePath;
145
+ this.stateDir = stateDir;
146
+ }
147
+ /**
148
+ * Drop a project's cached summary (or every project's, with no argument).
149
+ * Called after an import, which changes the adoptable set in a way the caller
150
+ * already knows about and shouldn't have to wait for a directory mtime to
151
+ * reveal.
152
+ *
153
+ * NOTE the workspace key may be `""` (the root workspace) — this takes a
154
+ * string and tests `undefined` explicitly, never truthiness.
155
+ */
156
+ invalidate(projectKey) {
157
+ if (projectKey === undefined)
158
+ this.summaries.clear();
159
+ else
160
+ this.summaries.delete(projectKey);
161
+ }
162
+ /**
163
+ * Enumerate `<claudeHome>/projects/*`, refreshing the recorded cwd only for
164
+ * folders whose file set changed since last time.
165
+ */
166
+ async scanFolders() {
167
+ const root = path.join(this.claudeHomePath, "projects");
168
+ const names = await fs.readdir(root).catch(() => []);
169
+ const out = [];
170
+ for (const name of names) {
171
+ const dir = path.join(root, name);
172
+ const st = await fs.stat(dir).catch(() => null);
173
+ if (!st?.isDirectory())
174
+ continue;
175
+ const key = `${st.mtimeMs}:${st.size}`;
176
+ const cached = this.folders.get(name);
177
+ if (cached && cached.key === key) {
178
+ out.push(cached);
179
+ continue;
180
+ }
181
+ const entry = {
182
+ name,
183
+ realPath: await fs.realpath(dir).catch(() => dir),
184
+ key,
185
+ cwd: await recordedCwd(dir),
186
+ };
187
+ this.folders.set(name, entry);
188
+ out.push(entry);
189
+ }
190
+ // Forget folders that no longer exist, so the map can't grow without bound.
191
+ const live = new Set(out.map((e) => e.name));
192
+ for (const name of [...this.folders.keys()])
193
+ if (!live.has(name))
194
+ this.folders.delete(name);
195
+ return out;
196
+ }
197
+ /**
198
+ * The candidate source working directories for a project, de-duplicated by the
199
+ * transcript folder they resolve to.
200
+ *
201
+ * ORIGIN-FIRST ordering (see {@link adoptableFor}'s session de-dup): matched
202
+ * external checkouts come first, the project's own working directory last.
203
+ */
204
+ async candidateSources(project, folders) {
205
+ const own = project.workingDir;
206
+ const ownFolder = path.join(this.claudeHomePath, "projects", encodePathForCli(own));
207
+ const ownReal = await fs.realpath(ownFolder).catch(() => ownFolder);
208
+ const matches = (cwd) => {
209
+ if (cwd === own)
210
+ return true;
211
+ // Repo-backed: "the user's own clone of the same repo", matched by the
212
+ // checkout basename. Deliberately loose — see the module header.
213
+ if (project.repo !== undefined && project.repo !== "") {
214
+ return path.basename(cwd) === repoCheckoutName(project.repo);
215
+ }
216
+ // Notebook: exact cwd equality only.
217
+ return false;
218
+ };
219
+ const seen = new Set([ownReal]);
220
+ const sources = [];
221
+ for (const folder of folders) {
222
+ if (folder.cwd === null || !matches(folder.cwd))
223
+ continue;
224
+ if (seen.has(folder.realPath))
225
+ continue;
226
+ seen.add(folder.realPath);
227
+ sources.push(folder.cwd);
228
+ }
229
+ // The project's own working directory goes LAST so that a session offered by
230
+ // both it and an external checkout is attributed to the external one — the
231
+ // origin, where the transcript actually lives, so a copy has something to
232
+ // copy (#588 gotcha 2).
233
+ sources.push(own);
234
+ return sources;
235
+ }
236
+ /**
237
+ * What `project` could import, as served by the `adoptable-chats` endpoint.
238
+ *
239
+ * `agentName` is the adopting agent (the project's keeper); it is the metadata
240
+ * cache key the engine warms titles/previews under, so detection and the later
241
+ * adoption agree.
242
+ */
243
+ async adoptableFor(fleet, project, agentName) {
244
+ const folders = await this.scanFolders();
245
+ const sources = await this.candidateSources(project, folders);
246
+ // Sentinel for "this folder recorded no cwd": it must be a string no real
247
+ // source can equal, and `""` will not do — the root workspace's key is `""`,
248
+ // so an empty cwd is a value that genuinely occurs here. Written as the
249
+ // escape, never a raw NUL byte: a raw NUL makes ripgrep classify the whole
250
+ // file as binary and skip it, so the file vanishes from code search (#570).
251
+ const matchedKeys = folders
252
+ .filter((f) => sources.includes(f.cwd ?? "\u0000"))
253
+ .map((f) => `${f.name}=${f.key}`)
254
+ .sort();
255
+ const key = [
256
+ await dirKey(path.join(this.claudeHomePath, "projects")),
257
+ await dirKey(path.join(this.stateDir, "adopted-sessions")),
258
+ await dirKey(path.join(this.stateDir, "jobs")),
259
+ ...matchedKeys,
260
+ ].join("|");
261
+ const cached = this.summaries.get(project.slug);
262
+ if (cached && cached.key === key)
263
+ return cached.summary;
264
+ const seen = new Set();
265
+ const out = [];
266
+ const filtered = [];
267
+ let count = 0;
268
+ for (const sourceCwd of sources) {
269
+ // Fault-isolated per source: an unreadable folder costs its own sessions,
270
+ // not the whole count.
271
+ const sessions = await fleet.listAdoptableSessions(agentName, sourceCwd).catch(() => []);
272
+ const ids = [];
273
+ for (const session of sessions) {
274
+ // De-dup by session id. Two candidate cwds can collide onto one folder
275
+ // under the lossy encoding, and a transcript may have been copied; either
276
+ // way one session must be offered once.
277
+ if (seen.has(session.sessionId))
278
+ continue;
279
+ seen.add(session.sessionId);
280
+ const reason = filterReasonFor(session);
281
+ if (reason !== null) {
282
+ filtered.push({ sessionId: session.sessionId, sourceCwd, reason });
283
+ continue;
284
+ }
285
+ ids.push(session.sessionId);
286
+ }
287
+ if (ids.length > 0) {
288
+ out.push({ sourceCwd, sessionIds: ids });
289
+ count += ids.length;
290
+ }
291
+ }
292
+ const summary = { count, sources: out, filtered };
293
+ this.summaries.set(project.slug, { key, summary });
294
+ return summary;
295
+ }
296
+ }
297
+ /**
298
+ * The cwd recorded inside a transcript folder, by reading the head of its
299
+ * newest-looking `.jsonl`.
300
+ *
301
+ * Every session in a folder shares the encoded cwd, so ONE read identifies the
302
+ * folder. Files are tried in readdir order until one yields a cwd, because a
303
+ * zero-byte or truncated transcript records none (the QA corpus has both).
304
+ */
305
+ async function recordedCwd(dir) {
306
+ const entries = await fs.readdir(dir).catch(() => []);
307
+ for (const entry of entries) {
308
+ if (!entry.endsWith(".jsonl"))
309
+ continue;
310
+ const cwd = await readSessionCwd(path.join(dir, entry)).catch(() => null);
311
+ if (cwd !== null && cwd !== "")
312
+ return cwd;
313
+ }
314
+ return null;
315
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * App factory — builds a fully-wired Fastify instance (REST + WS + optional
3
+ * static SPA) around a ProjectStore + HerdctlService + GitService + GithubAuth.
4
+ *
5
+ * Extracted from index.ts so tests can boot the REAL app in-process against a
6
+ * temp data dir (with a fake `claude` on PATH) without binding a port or
7
+ * registering signal handlers. `index.ts` calls `buildApp()` then `app.listen`.
8
+ *
9
+ * This is a testability seam only — the wiring is identical to the prior inline
10
+ * bootstrap; no behavior changed.
11
+ */
12
+ import { type FastifyInstance } from "fastify";
13
+ import { type PaddockConfig } from "./config.js";
14
+ import { ProjectStore } from "./projects.js";
15
+ import { HerdctlService } from "./herdctl.js";
16
+ import { GitService } from "./git.js";
17
+ import { GithubAuth } from "./github-auth.js";
18
+ import { type Transcriber } from "./transcribe.js";
19
+ import { SweepService } from "./sweep.js";
20
+ import { ArchiveStore } from "./archive.js";
21
+ import { StarStore } from "./star.js";
22
+ import { ReadStateStore } from "./read-state.js";
23
+ import { UnreadStore } from "./unread.js";
24
+ import { ParentDetachStore } from "./parent-detach.js";
25
+ import { QueuedMessageStore } from "./queued-message.js";
26
+ import { RunProvenanceStore } from "./run-provenance.js";
27
+ import { PaddockEventBus } from "./event-bus.js";
28
+ import { TriggerService } from "./triggers.js";
29
+ export interface BuiltApp {
30
+ app: FastifyInstance;
31
+ cfg: PaddockConfig;
32
+ projects: ProjectStore;
33
+ herdctl: HerdctlService;
34
+ git: GitService;
35
+ githubAuth: GithubAuth;
36
+ sweep: SweepService;
37
+ archive: ArchiveStore;
38
+ star: StarStore;
39
+ readState: ReadStateStore;
40
+ /** Per-user manual "unread" override sidecar (#458). */
41
+ unread: UnreadStore;
42
+ /** Explicit "detached from its parent" sidecar (#508) — beats both parent tiers. */
43
+ parentDetach: ParentDetachStore;
44
+ /** Per-chat creation provenance (#261), incl. the recorded parent edge (#485). */
45
+ runProvenance: RunProvenanceStore;
46
+ queuedMessage: QueuedMessageStore;
47
+ transcriber: Transcriber;
48
+ /** In-process lifecycle event bus (Epic T) — commit sites emit lifecycle events. */
49
+ events: PaddockEventBus;
50
+ /** Unified trigger registry (Epic T / T1) — the sole trigger CRUD surface. */
51
+ triggers: TriggerService;
52
+ /** Tear down the fleet + close the server (no process.exit, for tests). */
53
+ close: () => Promise<void>;
54
+ }
55
+ export interface BuildAppOptions {
56
+ /** Override the resolved config (tests pass a temp-dir config). */
57
+ config?: PaddockConfig;
58
+ /** Skip serving the built web SPA even if a dist exists (API-only). */
59
+ serveStatic?: boolean;
60
+ }
61
+ /**
62
+ * Construct and fully register the paddock app. Does NOT listen on a port and
63
+ * installs NO signal handlers — the caller (index.ts in prod, tests in CI)
64
+ * owns the lifecycle. The herdctl fleet is initialized + started; a failure
65
+ * there is logged and swallowed (project CRUD still works), matching prod.
66
+ */
67
+ export declare function buildApp(opts?: BuildAppOptions): Promise<BuiltApp>;